Java tutorial
// -*- mode:Java; tab-width:2; c-basic-offset:2; indent-tabs-mode:t -*- /** * * Licensed under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. * * * Implements the Hadoop FS interfaces to allow applications to store * files in Ceph. */ package org.apache.hadoop.fs.ceph; import java.io.IOException; import java.io.FileNotFoundException; import java.io.OutputStream; import java.net.URI; import java.net.InetAddress; import java.util.EnumSet; import java.lang.Math; import java.util.ArrayList; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.BlockLocation; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FileUtil; import org.apache.hadoop.fs.Path; import org.apache.hadoop.fs.permission.FsPermission; import org.apache.hadoop.util.Progressable; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.net.DNS; /** * <p> * A {@link FileSystem} backed by <a href="http://ceph.newdream.net">Ceph.</a>. * This will not start a Ceph instance; one must already be running. * </p> * Configuration of the CephFileSystem is handled via a few Hadoop * Configuration properties: <br> * fs.ceph.monAddr -- the ip address/port of the monitor to connect to. <br> * fs.ceph.libDir -- the directory that libcephfs and libhadoopceph are * located in. This assumes Hadoop is being run on a linux-style machine * with names like libcephfs.so. * fs.ceph.commandLine -- if you prefer you can fill in this property * just as you would when starting Ceph up from the command line. Specific * properties override any configuration specified here. * <p> * You can also enable debugging of the CephFileSystem and Ceph itself: <br> * fs.ceph.debug -- if 'true' will print out method enter/exit messages, * plus a little more. * fs.ceph.clientDebug/fs.ceph.messengerDebug -- will print out debugging * from the respective Ceph system of at least that importance. */ public class CephFileSystem extends FileSystem { private static final Log LOG = LogFactory.getLog(CephFileSystem.class); private URI uri; private Path workingDir; private final Path root; private CephFS ceph = null; private static String CEPH_NAMESERVER; private static final String CEPH_NAMESERVER_KEY = "fs.ceph.nameserver"; private static final String CEPH_NAMESERVER_DEFAULT = "localhost"; /** * Create a new CephFileSystem. */ public CephFileSystem() { root = new Path("/"); } /** * Used for testing purposes, this constructor * sets the given CephFS instead of defaulting to a * CephTalker (with its assumed real Ceph instance to talk to). */ public CephFileSystem(CephFS ceph_fs) { super(); root = new Path("/"); ceph = ceph_fs; } /** * Lets you get the URI of this CephFileSystem. * @return the URI. */ public URI getUri() { LOG.debug("getUri:exit with return " + uri); return uri; } /** * Should be called after constructing a CephFileSystem but before calling * any other methods. * Starts up the connection to Ceph, reads in configuraton options, etc. * @param uri The URI for this filesystem. * @param conf The Hadoop Configuration to retrieve properties from. * @throws IOException if necessary properties are unset. */ @Override public void initialize(URI uri, Configuration conf) throws IOException { super.initialize(uri, conf); setConf(conf); this.uri = URI.create(uri.getScheme() + "://" + uri.getAuthority()); if (ceph == null) { ceph = new CephTalker(conf, LOG); } CEPH_NAMESERVER = conf.get(CEPH_NAMESERVER_KEY, CEPH_NAMESERVER_DEFAULT); // build up the arguments for Ceph String arguments = "CephFSInterface"; arguments += conf.get("fs.ceph.commandLine", ""); if (conf.get("fs.ceph.clientDebug") != null) { arguments += " --debug_client "; arguments += conf.get("fs.ceph.clientDebug"); } if (conf.get("fs.ceph.messengerDebug") != null) { arguments += " --debug_ms "; arguments += conf.get("fs.ceph.messengerDebug"); } if (conf.get("fs.ceph.monAddr") != null) { arguments += " -m "; arguments += conf.get("fs.ceph.monAddr"); } arguments += " --client-readahead-max-periods=" + conf.get("fs.ceph.readahead", "1"); // make sure they gave us a ceph monitor address or conf file LOG.info("initialize:Ceph initialization arguments: " + arguments); if ((conf.get("fs.ceph.monAddr") == null) && (arguments.indexOf("-m") == -1) && (arguments.indexOf("-c") == -1)) { LOG.fatal("initialize:You need to specify a Ceph monitor address."); throw new IOException("You must specify a Ceph monitor address or config file!"); } // Initialize the client if (!ceph.ceph_initializeClient(arguments, conf.getInt("fs.ceph.blockSize", 1 << 26))) { LOG.fatal("initialize:Ceph initialization failed!"); throw new IOException("Ceph initialization failed!"); } LOG.info("initialize:Ceph initialized client. Setting cwd to /"); ceph.ceph_setcwd("/"); LOG.debug("initialize:exit"); this.workingDir = getHomeDirectory(); } /** * Close down the CephFileSystem. Runs the base-class close method * and then kills the Ceph client itself. */ @Override public void close() throws IOException { LOG.debug("close:enter"); super.close(); // this method does stuff, make sure it's run! LOG.trace("close: Calling ceph_kill_client from Java"); ceph.ceph_kill_client(); LOG.debug("close:exit"); } /** * Get an FSDataOutputStream to append onto a file. * @param file The File you want to append onto * @param bufferSize Ceph does internal buffering but you can buffer in the Java code as well if you like. * @param progress The Progressable to report progress to. * Reporting is limited but exists. * @return An FSDataOutputStream that connects to the file on Ceph. * @throws IOException If the file cannot be found or appended to. */ public FSDataOutputStream append(Path file, int bufferSize, Progressable progress) throws IOException { LOG.debug("append:enter with path " + file + " bufferSize " + bufferSize); Path abs_path = makeAbsolute(file); if (progress != null) { progress.progress(); } LOG.trace("append: Entering ceph_open_for_append from Java"); int fd = ceph.ceph_open_for_append(getCephPath(abs_path)); LOG.trace("append: Returned to Java"); if (progress != null) { progress.progress(); } if (fd < 0) { // error in open throw new IOException("append: Open for append failed on path \"" + abs_path.toString() + "\""); } CephOutputStream cephOStream = new CephOutputStream(getConf(), ceph, fd, bufferSize); LOG.debug("append:exit"); return new FSDataOutputStream(cephOStream, statistics); } /** * Get the current working directory for the given file system * @return the directory Path */ public Path getWorkingDirectory() { return workingDir; } /** * Set the current working directory for the given file system. All relative * paths will be resolved relative to it. * * @param dir The directory to change to. */ @Override public void setWorkingDirectory(Path dir) { workingDir = makeAbsolute(dir); } /** * Return only the path component from a potentially fully qualified path. */ private String getCephPath(Path path) { if (!path.isAbsolute()) { throw new IllegalArgumentException("Path must be absolute: " + path); } return path.toUri().getPath(); } /** * Check if a path exists. * Overriden because it's moderately faster than the generic implementation. * @param path The file to check existence on. * @return true if the file exists, false otherwise. */ @Override public boolean exists(Path path) throws IOException { LOG.debug("exists:enter with path " + path); boolean result; Path abs_path = makeAbsolute(path); if (abs_path.equals(root)) { result = true; } else { LOG.trace("exists:Calling ceph_exists from Java on path " + abs_path.toString()); result = ceph.ceph_exists(getCephPath(abs_path)); LOG.trace("exists:Returned from ceph_exists to Java"); } LOG.debug("exists:exit with value " + result); return result; } /** * Create a directory and any nonexistent parents. Any portion * of the directory tree can exist without error. * @param path The directory path to create * @param perms The permissions to apply to the created directories. * @return true if successful, false otherwise * @throws IOException if the path is a child of a file. */ @Override public boolean mkdirs(Path path, FsPermission perms) throws IOException { LOG.debug("mkdirs:enter with path " + path); Path abs_path = makeAbsolute(path); LOG.trace("mkdirs:calling ceph_mkdirs from Java"); int result = ceph.ceph_mkdirs(getCephPath(abs_path), (int) perms.toShort()); if (result != 0) { LOG.warn("mkdirs: make directory " + abs_path + "Failing with result " + result); if (-ceph.ENOTDIR == result) { throw new IOException("Parent path is not a directory"); } return false; } else { LOG.debug("mkdirs:exiting succesfully"); return true; } } /** * Check if a path is a file. This is moderately faster than the * generic implementation. * @param path The path to check. * @return true if the path is definitely a file, false otherwise. */ @Override public boolean isFile(Path path) throws IOException { LOG.debug("isFile:enter with path " + path); Path abs_path = makeAbsolute(path); boolean result; if (abs_path.equals(root)) { result = false; } else { LOG.trace("isFile:entering ceph_isfile from Java"); result = ceph.ceph_isfile(getCephPath(abs_path)); } LOG.debug("isFile:exit with result " + result); return result; } /** * Get stat information on a file. This does not fill owner or group, as * Ceph's support for these is a bit different than HDFS'. * @param path The path to stat. * @return FileStatus object containing the stat information. * @throws FileNotFoundException if the path could not be resolved. */ public FileStatus getFileStatus(Path path) throws IOException { LOG.debug("getFileStatus:enter with path " + path); Path abs_path = makeAbsolute(path); // sadly, Ceph doesn't really do uids/gids just yet, but // everything else is filled FileStatus status; Stat lstat = new Stat(); LOG.trace("getFileStatus: calling ceph_stat from Java"); if (ceph.ceph_stat(getCephPath(abs_path), lstat)) { status = new FileStatus(lstat.size, lstat.is_dir, ceph.ceph_replication(getCephPath(abs_path)), lstat.block_size, lstat.mod_time, lstat.access_time, new FsPermission((short) lstat.mode), System.getProperty("user.name"), null, path.makeQualified(this)); } else { // fail out throw new FileNotFoundException("org.apache.hadoop.fs.ceph.CephFileSystem: File " + path + " does not exist or could not be accessed"); } LOG.debug("getFileStatus:exit"); return status; } /** * Get the FileStatus for each listing in a directory. * @param path The directory to get listings from. * @return FileStatus[] containing one FileStatus for each directory listing; * null if path does not exist. */ public FileStatus[] listStatus(Path path) throws IOException { LOG.debug("listStatus:enter with path " + path); Path abs_path = makeAbsolute(path); Path[] paths = listPaths(abs_path); if (paths != null) { FileStatus[] statuses = new FileStatus[paths.length]; for (int i = 0; i < paths.length; ++i) { statuses[i] = getFileStatus(paths[i]); } LOG.debug("listStatus:exit"); return statuses; } if (isFile(path)) { return new FileStatus[] { getFileStatus(path) }; } return null; } @Override public void setPermission(Path p, FsPermission permission) throws IOException { LOG.debug("setPermission:enter with path " + p + " and permissions " + permission); Path abs_path = makeAbsolute(p); LOG.trace("setPermission:calling ceph_setpermission from Java"); ceph.ceph_setPermission(getCephPath(abs_path), permission.toShort()); LOG.debug("setPermission:exit"); } /** * Set access/modification times of a file. * @param p The path * @param mtime Set modification time in number of millis since Jan 1, 1970. * @param atime Set access time in number of millis since Jan 1, 1970. */ @Override public void setTimes(Path p, long mtime, long atime) throws IOException { LOG.debug("setTimes:enter with path " + p + " mtime:" + mtime + " atime:" + atime); Path abs_path = makeAbsolute(p); LOG.trace("setTimes:calling ceph_setTimes from Java"); int r = ceph.ceph_setTimes(getCephPath(abs_path), mtime, atime); if (r < 0) { throw new IOException("Failed to set times on path " + abs_path.toString() + " Error code: " + r); } LOG.debug("setTimes:exit"); } /** * Create a new file and open an FSDataOutputStream that's connected to it. * @param path The file to create. * @param permission The permissions to apply to the file. * @param overwrite If true, overwrite any existing file with * this name; otherwise don't. * @param bufferSize Ceph does internal buffering, but you can buffer * in the Java code too if you like. * @param replication Ignored by Ceph. This can be * configured via Ceph configuration. * @param blockSize Ignored by Ceph. You can set client-wide block sizes * via the fs.ceph.blockSize param if you like. * @param progress A Progressable to report back to. * Reporting is limited but exists. * @return An FSDataOutputStream pointing to the created file. * @throws IOException if the path is an * existing directory, or the path exists but overwrite is false, or there is a * failure in attempting to open for append with Ceph. */ public FSDataOutputStream create(Path path, FsPermission permission, boolean overwrite, int bufferSize, short replication, long blockSize, Progressable progress) throws IOException { LOG.debug("create:enter with path " + path); Path abs_path = makeAbsolute(path); if (progress != null) { progress.progress(); } // We ignore replication since that's not configurable here, and // progress reporting is quite limited. // Required semantics: if the file exists, overwrite if 'overwrite' is set; // otherwise, throw an exception // Step 1: existence test boolean exists = exists(abs_path); if (exists) { if (getFileStatus(abs_path).isDir()) { throw new IOException( "create: Cannot overwrite existing directory \"" + path.toString() + "\" with a file"); } if (!overwrite) { throw new IOException("createRaw: Cannot open existing file \"" + abs_path.toString() + "\" for writing without overwrite flag"); } } if (progress != null) { progress.progress(); } // Step 2: create any nonexistent directories in the path if (!exists) { Path parent = abs_path.getParent(); if (parent != null) { // if parent is root, we're done int r = ceph.ceph_mkdirs(getCephPath(parent), permission.toShort()); if (!(r == 0 || r == -ceph.EEXIST)) { throw new IOException("Error creating parent directory; code: " + r); } } if (progress != null) { progress.progress(); } } // Step 3: open the file LOG.trace("calling ceph_open_for_overwrite from Java"); int fh = ceph.ceph_open_for_overwrite(getCephPath(abs_path), (int) permission.toShort()); if (progress != null) { progress.progress(); } LOG.trace("Returned from ceph_open_for_overwrite to Java with fh " + fh); if (fh < 0) { throw new IOException("create: Open for overwrite failed on path \"" + path.toString() + "\""); } // Step 4: create the stream OutputStream cephOStream = new CephOutputStream(getConf(), ceph, fh, bufferSize); LOG.debug("create:exit"); return new FSDataOutputStream(cephOStream, statistics); } /** * Open a Ceph file and attach the file handle to an FSDataInputStream. * @param path The file to open * @param bufferSize Ceph does internal buffering; but you can buffer in * the Java code too if you like. * @return FSDataInputStream reading from the given path. * @throws IOException if the path DNE or is a * directory, or there is an error getting data to set up the FSDataInputStream. */ public FSDataInputStream open(Path path, int bufferSize) throws IOException { LOG.debug("open:enter with path " + path); Path abs_path = makeAbsolute(path); int fh = ceph.ceph_open_for_read(getCephPath(abs_path)); if (fh < 0) { // uh-oh, something's bad! if (fh == -ceph.ENOENT) { // well that was a stupid open throw new IOException("open: absolute path \"" + abs_path.toString() + "\" does not exist"); } else { // hrm...the file exists but we can't open it :( throw new IOException("open: Failed to open file " + abs_path.toString()); } } if (getFileStatus(abs_path).isDir()) { // yes, it is possible to open Ceph directories // but that doesn't mean you should in Hadoop! ceph.ceph_close(fh); throw new IOException("open: absolute path \"" + abs_path.toString() + "\" is a directory!"); } Stat lstat = new Stat(); LOG.trace("open:calling ceph_stat from Java"); ceph.ceph_stat(getCephPath(abs_path), lstat); LOG.trace("open:returned to Java"); long size = lstat.size; if (size < 0) { throw new IOException("Failed to get file size for file " + abs_path.toString() + " but succeeded in opening file. Something bizarre is going on."); } FSInputStream cephIStream = new CephInputStream(getConf(), ceph, fh, size, bufferSize); LOG.debug("open:exit"); return new FSDataInputStream(cephIStream); } /** * Rename a file or directory. * @param src The current path of the file/directory * @param dst The new name for the path. * @return true if the rename succeeded, false otherwise. */ @Override public boolean rename(Path src, Path dst) throws IOException { LOG.debug("rename:enter with src:" + src + " and dest:" + dst); Path abs_src = makeAbsolute(src); Path abs_dst = makeAbsolute(dst); LOG.trace("calling ceph_rename from Java"); boolean result = ceph.ceph_rename(getCephPath(abs_src), getCephPath(abs_dst)); if (!result) { boolean isDir = false; try { isDir = getFileStatus(abs_dst).isDir(); } catch (FileNotFoundException e) { } if (isDir) { // move the srcdir into destdir LOG.debug("ceph_rename failed but dst is a directory!"); Path new_dst = new Path(abs_dst, abs_src.getName()); result = rename(abs_src, new_dst); LOG.debug("attempt to move " + abs_src.toString() + " to " + new_dst.toString() + "has result:" + result); } } LOG.debug("rename:exit with result: " + result); return result; } /* * Attempt to convert an IP into its hostname */ private String[] ips2Hosts(String[] ips) { ArrayList<String> hosts = new ArrayList<String>(); for (String ip : ips) { try { String host = DNS.reverseDns(InetAddress.getByName(ip), CEPH_NAMESERVER); if (host.charAt(host.length() - 1) == '.') { host = host.substring(0, host.length() - 1); } hosts.add(host); /* append */ } catch (Exception e) { LOG.error("reverseDns [" + ip + "] failed: " + e); } } return hosts.toArray(new String[hosts.size()]); } /** * Get a BlockLocation object for each block in a file. * * Note that this doesn't include port numbers in the name field as * Ceph handles slow/down servers internally. This data should be used * only for selecting which servers to run which jobs on. * * @param file A FileStatus object corresponding to the file you want locations for. * @param start The offset of the first part of the file you are interested in. * @param len The amount of the file past the offset you are interested in. * @return A BlockLocation[] where each object corresponds to a block within * the given range. */ @Override public BlockLocation[] getFileBlockLocations(FileStatus file, long start, long len) throws IOException { Path abs_path = makeAbsolute(file.getPath()); int fh = ceph.ceph_open_for_read(getCephPath(abs_path)); if (fh < 0) { LOG.error("getFileBlockLocations:got error " + fh + ", exiting and returning null!"); return null; } long blockSize = ceph.ceph_getblocksize(getCephPath(abs_path)); BlockLocation[] locations = new BlockLocation[(int) Math.ceil(len / (float) blockSize)]; for (int i = 0; i < locations.length; ++i) { long offset = start + i * blockSize; long blockStart = start + i * blockSize - (start % blockSize); String ips[] = ceph.ceph_hosts(fh, offset); String hosts[] = ips2Hosts(ips); locations[i] = new BlockLocation(null, hosts, blockStart, blockSize); LOG.debug("getFileBlockLocations: location[" + i + "]: " + locations[i]); } ceph.ceph_close(fh); return locations; } @Deprecated public boolean delete(Path path) throws IOException { return delete(path, false); } /** * Delete the given path, and optionally its children. * @param path the path to delete. * @param recursive If the path is a non-empty directory and this is false, * delete will throw an IOException. If path is a file this is ignored. * @return true if the delete succeeded, false otherwise (including if * path doesn't exist). * @throws IOException if you attempt to non-recursively delete a directory, * or you attempt to delete the root directory. */ public boolean delete(Path path, boolean recursive) throws IOException { LOG.debug("delete:enter with path " + path + " and recursive=" + recursive); Path abs_path = makeAbsolute(path); // sanity check if (abs_path.equals(root)) { throw new IOException("Error: deleting the root directory is a Bad Idea."); } if (!exists(abs_path)) { return false; } // if the path is a file, try to delete it. if (isFile(abs_path)) { LOG.trace("delete:calling ceph_unlink from Java with path " + abs_path); boolean result = ceph.ceph_unlink(getCephPath(abs_path)); if (!result) { LOG.error("delete: failed to delete file \"" + abs_path.toString() + "\"."); } LOG.debug("delete:exit with success=" + result); return result; } /* The path is a directory, so recursively try to delete its contents, and then delete the directory. */ // get the entries; listPaths will remove . and .. for us Path[] contents = listPaths(abs_path); if (contents == null) { LOG.error("delete: Failed to read contents of directory \"" + abs_path.toString() + "\" while trying to delete it, BAILING"); return false; } if (!recursive && contents.length > 0) { throw new IOException("Directories must be deleted recursively!"); } // delete the entries LOG.debug("delete: recursively calling delete on contents of " + abs_path); for (Path p : contents) { if (!delete(p, true)) { LOG.error("delete: Failed to delete file \"" + p.toString() + "\" while recursively deleting \"" + abs_path.toString() + "\", BAILING"); return false; } } // if we've come this far it's a now-empty directory, so delete it! boolean result = ceph.ceph_rmdir(getCephPath(abs_path)); if (!result) { LOG.error("delete: failed to delete \"" + abs_path.toString() + "\", BAILING"); } LOG.debug("delete:exit"); return result; } /** * Returns the default replication value of 1. This may * NOT be the actual value, as replication is controlled * by a separate Ceph configuration. */ @Override public short getDefaultReplication() { return 1; } /** * Get the default block size. * @return the default block size, in bytes, as a long. */ @Override public long getDefaultBlockSize() { return getConf().getInt("fs.ceph.blockSize", 1 << 26); } /** * Adds the working directory to path if path is not already * an absolute path. The URI scheme is not removed here. It * is removed only when users (e.g. ceph native calls) need * the path-only portion. */ private Path makeAbsolute(Path path) { if (path.isAbsolute()) { return path; } return new Path(workingDir, path); } private Path[] listPaths(Path path) throws IOException { LOG.debug("listPaths:enter with path " + path); String dirlist[]; Path abs_path = makeAbsolute(path); // If it's a directory, get the listing. Otherwise, complain and give up. LOG.debug("calling ceph_getdir from Java with path " + abs_path); dirlist = ceph.ceph_getdir(getCephPath(abs_path)); LOG.debug("returning from ceph_getdir to Java"); if (dirlist == null) { return null; } // convert the strings to Paths Path[] paths = new Path[dirlist.length]; for (int i = 0; i < dirlist.length; ++i) { LOG.trace("Raw enumeration of paths in \"" + abs_path.toString() + "\": \"" + dirlist[i] + "\""); // convert each listing to an absolute path Path raw_path = new Path(dirlist[i]); if (raw_path.isAbsolute()) { paths[i] = raw_path; } else { paths[i] = new Path(abs_path, raw_path); } } LOG.debug("listPaths:exit"); return paths; } static class Stat { public long size; public boolean is_dir; public long block_size; public long mod_time; public long access_time; public int mode; public Stat() { } } }