/* * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 * (the "License"). You may not use this work except in compliance with the License, which is * available at www.apache.org/licenses/LICENSE-2.0 * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied, as more fully set forth in the License. * * See the NOTICE file distributed with this work for information regarding copyright ownership. */ package alluxio.master.file.meta; import alluxio.AlluxioURI; import alluxio.exception.AccessControlException; import alluxio.exception.ExceptionMessage; import alluxio.exception.FileAlreadyExistsException; import alluxio.exception.InvalidPathException; import alluxio.master.file.meta.options.MountInfo; import alluxio.master.file.options.MountOptions; import alluxio.master.journal.JournalEntryIterable; import alluxio.proto.journal.File; import alluxio.proto.journal.File.AddMountPointEntry; import alluxio.proto.journal.Journal; import alluxio.resource.LockResource; import alluxio.underfs.UfsManager; import alluxio.underfs.UnderFileSystem; import alluxio.util.IdUtils; import alluxio.util.io.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; /** * This class is used for keeping track of Alluxio mount points. */ @ThreadSafe public final class MountTable implements JournalEntryIterable { private static final Logger LOG = LoggerFactory.getLogger(MountTable.class); public static final String ROOT = "/"; private final Lock mReadLock; private final Lock mWriteLock; /** Maps from Alluxio path string, to {@link MountInfo}. */ @GuardedBy("mLock") private final Map<String, MountInfo> mMountTable; /** The manager of all ufs. */ private final UfsManager mUfsManager; /** * Creates a new instance of {@link MountTable}. * * @param ufsManager the UFS manager */ public MountTable(UfsManager ufsManager) { final int initialCapacity = 10; mMountTable = new HashMap<>(initialCapacity); ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); mReadLock = lock.readLock(); mWriteLock = lock.writeLock(); mUfsManager = ufsManager; } @Override public Iterator<Journal.JournalEntry> getJournalEntryIterator() { final Iterator<Map.Entry<String, MountInfo>> it = mMountTable.entrySet().iterator(); return new Iterator<Journal.JournalEntry>() { /** mEntry is always set to the next non-root mount point if exists. */ private Map.Entry<String, MountInfo> mEntry = null; @Override public boolean hasNext() { if (mEntry != null) { return true; } while (it.hasNext()) { mEntry = it.next(); // Do not journal the root mount point. if (!mEntry.getKey().equals(ROOT)) { return true; } else { mEntry = null; } } return false; } @Override public Journal.JournalEntry next() { if (!hasNext()) { throw new NoSuchElementException(); } String alluxioPath = mEntry.getKey(); MountInfo info = mEntry.getValue(); mEntry = null; Map<String, String> properties = info.getOptions().getProperties(); List<File.StringPairEntry> protoProperties = new ArrayList<>(properties.size()); for (Map.Entry<String, String> property : properties.entrySet()) { protoProperties.add(File.StringPairEntry.newBuilder() .setKey(property.getKey()) .setValue(property.getValue()) .build()); } AddMountPointEntry addMountPoint = AddMountPointEntry.newBuilder().setAlluxioPath(alluxioPath) .setUfsPath(info.getUfsUri().toString()).setReadOnly(info.getOptions().isReadOnly()) .addAllProperties(protoProperties).setShared(info.getOptions().isShared()).build(); return Journal.JournalEntry.newBuilder().setAddMountPoint(addMountPoint).build(); } @Override public void remove() { throw new UnsupportedOperationException("Mountable#Iterator#remove is not supported."); } }; } /** * Mounts the given UFS path at the given Alluxio path. The Alluxio path should not be nested * under an existing mount point. * * @param alluxioUri an Alluxio path URI * @param ufsUri a UFS path URI * @param mountId the mount id * @param options the mount options * @throws FileAlreadyExistsException if the mount point already exists * @throws InvalidPathException if an invalid path is encountered */ public void add(AlluxioURI alluxioUri, AlluxioURI ufsUri, long mountId, MountOptions options) throws FileAlreadyExistsException, InvalidPathException { String alluxioPath = alluxioUri.getPath(); LOG.info("Mounting {} at {}", ufsUri, alluxioPath); try (LockResource r = new LockResource(mWriteLock)) { if (mMountTable.containsKey(alluxioPath)) { throw new FileAlreadyExistsException( ExceptionMessage.MOUNT_POINT_ALREADY_EXISTS.getMessage(alluxioPath)); } // Check all non-root mount points, to check if they're a prefix of the alluxioPath we're // trying to mount. Also make sure that the ufs path we're trying to mount is not a prefix // or suffix of any existing mount path. for (Map.Entry<String, MountInfo> entry : mMountTable.entrySet()) { String mountedAlluxioPath = entry.getKey(); AlluxioURI mountedUfsUri = entry.getValue().getUfsUri(); if (!mountedAlluxioPath.equals(ROOT) && PathUtils.hasPrefix(alluxioPath, mountedAlluxioPath)) { throw new InvalidPathException(ExceptionMessage.MOUNT_POINT_PREFIX_OF_ANOTHER.getMessage( mountedAlluxioPath, alluxioPath)); } if ((ufsUri.getScheme() == null || ufsUri.getScheme().equals(mountedUfsUri.getScheme())) && (ufsUri.getAuthority() == null || ufsUri.getAuthority() .equals(mountedUfsUri.getAuthority()))) { String ufsPath = ufsUri.getPath(); String mountedUfsPath = mountedUfsUri.getPath(); if (PathUtils.hasPrefix(ufsPath, mountedUfsPath)) { throw new InvalidPathException(ExceptionMessage.MOUNT_POINT_PREFIX_OF_ANOTHER .getMessage(mountedUfsUri.toString(), ufsUri.toString())); } if (PathUtils.hasPrefix(mountedUfsPath, ufsPath)) { throw new InvalidPathException(ExceptionMessage.MOUNT_POINT_PREFIX_OF_ANOTHER .getMessage(ufsUri.toString(), mountedUfsUri.toString())); } } } mMountTable.put(alluxioPath, new MountInfo(ufsUri, mountId, options)); } } /** * Clears all the mount point except the root. */ public void clear() { LOG.info("Clearing mount table (except the root)."); try (LockResource r = new LockResource(mWriteLock)) { MountInfo mountInfo = mMountTable.get(ROOT); mMountTable.clear(); if (mountInfo != null) { mMountTable.put(ROOT, mountInfo); } } } /** * Unmounts the given Alluxio path. The path should match an existing mount point. * * @param uri an Alluxio path URI * @return whether the operation succeeded or not */ public boolean delete(AlluxioURI uri) { String path = uri.getPath(); LOG.info("Unmounting {}", path); if (path.equals(ROOT)) { LOG.warn("Cannot unmount the root mount point."); return false; } try (LockResource r = new LockResource(mWriteLock)) { if (mMountTable.containsKey(path)) { mUfsManager.removeMount(mMountTable.get(path).getMountId()); mMountTable.remove(path); return true; } LOG.warn("Mount point {} does not exist.", path); return false; } } /** * Returns the closest ancestor mount point the given path is nested under. * * @param uri an Alluxio path URI * @return mount point the given Alluxio path is nested under * @throws InvalidPathException if an invalid path is encountered */ public String getMountPoint(AlluxioURI uri) throws InvalidPathException { String path = uri.getPath(); String mountPoint = null; try (LockResource r = new LockResource(mReadLock)) { for (Map.Entry<String, MountInfo> entry : mMountTable.entrySet()) { String alluxioPath = entry.getKey(); if (PathUtils.hasPrefix(path, alluxioPath) && (mountPoint == null || PathUtils.hasPrefix(alluxioPath, mountPoint))) { mountPoint = alluxioPath; } } return mountPoint; } } /** * Returns a copy of the current mount table, the mount table is a map from Alluxio file system * URIs to the corresponding mount point information. * * @return a copy of the current mount table */ public Map<String, MountInfo> getMountTable() { try (LockResource r = new LockResource(mReadLock)) { return new HashMap<>(mMountTable); } } /** * @param uri an Alluxio path URI * @return whether the given uri is a mount point */ public boolean isMountPoint(AlluxioURI uri) { try (LockResource r = new LockResource(mReadLock)) { return mMountTable.containsKey(uri.getPath()); } } /** * Resolves the given Alluxio path. If the given Alluxio path is nested under a mount point, the * resolution maps the Alluxio path to the corresponding UFS path. Otherwise, the resolution is a * no-op. * * @param uri an Alluxio path URI * @return the {@link Resolution} representing the UFS path * @throws InvalidPathException if an invalid path is encountered */ public Resolution resolve(AlluxioURI uri) throws InvalidPathException { try (LockResource r = new LockResource(mReadLock)) { String path = uri.getPath(); LOG.debug("Resolving {}", path); // This will re-acquire the read lock, but that is allowed. String mountPoint = getMountPoint(uri); if (mountPoint != null) { MountInfo info = mMountTable.get(mountPoint); AlluxioURI ufsUri = info.getUfsUri(); UnderFileSystem ufs = mUfsManager.get(info.getMountId()); AlluxioURI resolvedUri = ufs.resolveUri(ufsUri, path.substring(mountPoint.length())); return new Resolution(resolvedUri, ufs, info.getOptions().isShared(), info.getMountId()); } // TODO(binfan): throw exception as we should never reach here return new Resolution(uri, null, false, IdUtils.INVALID_MOUNT_ID); } } /** * Checks to see if a write operation is allowed for the specified Alluxio path, by determining * if it is under a readonly mount point. * * @param alluxioUri an Alluxio path URI * @throws InvalidPathException if the Alluxio path is invalid * @throws AccessControlException if the Alluxio path is under a readonly mount point */ public void checkUnderWritableMountPoint(AlluxioURI alluxioUri) throws InvalidPathException, AccessControlException { try (LockResource r = new LockResource(mReadLock)) { // This will re-acquire the read lock, but that is allowed. String mountPoint = getMountPoint(alluxioUri); MountInfo mountInfo = mMountTable.get(mountPoint); if (mountInfo.getOptions().isReadOnly()) { throw new AccessControlException(ExceptionMessage.MOUNT_READONLY, alluxioUri, mountPoint); } } } /** * @param mountId the given ufs id * @return the mount information with this id or null if this mount id is not found */ public MountInfo getMountInfo(long mountId) { try (LockResource r = new LockResource(mReadLock)) { for (Map.Entry<String, MountInfo> entry : mMountTable.entrySet()) { if (entry.getValue().getMountId() == mountId) { return entry.getValue(); } } } return null; } /** * This class represents a UFS path after resolution. The UFS URI and the {@link UnderFileSystem} * for the UFS path are available. */ public final class Resolution { private final AlluxioURI mUri; private final UnderFileSystem mUfs; private final boolean mShared; private final long mMountId; private Resolution(AlluxioURI uri, UnderFileSystem ufs, boolean shared, long mountId) { mUri = uri; mUfs = ufs; mShared = shared; mMountId = mountId; } /** * @return the URI in the ufs */ public AlluxioURI getUri() { return mUri; } /** * @return the {@link UnderFileSystem} instance */ public UnderFileSystem getUfs() { return mUfs; } /** * @return the shared option */ public boolean getShared() { return mShared; } /** * @return the id of this mount point */ public long getMountId() { return mMountId; } } }