alluxio.master.file.meta.UfsSyncPathCache Maven / Gradle / Ivy
The newest version!
/*
* 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 static alluxio.util.io.PathUtils.cleanPath;
import alluxio.AlluxioURI;
import alluxio.collections.Pair;
import alluxio.conf.Configuration;
import alluxio.conf.PropertyKey;
import alluxio.exception.InvalidPathException;
import alluxio.file.options.DescendantType;
import alluxio.resource.LockResource;
import alluxio.util.io.PathUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Clock;
import java.util.Optional;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
/**
* A cache of paths that have been synchronized or have been marked as not being synchronized with
* the UFS. Paths are mapped to time values that indicate their last synchronization or
* invalidation.
*
* Synchronization times are updated only at the root path for which a sync was performed by
* {@link alluxio.master.file.InodeSyncStream}.
* Each path contains the following values concerning synchronization:
* - recursiveSyncTime: the last time a sync was successful on this path
* - directChildSyncTime: the last time a sync was successful on the path which (at least)
* included itself and its direct children
* - recursiveSyncTime: the last time a sync was successful on this path which
* included itself and all its children
* Each path additionally contains the following values considering invalidation:
* - invalidationTime: the last time this exact path was invalidated
* - directChildInvalidation: the last time a direct child of this path was invalidated
* - recursiveChildInvalidation: the last time a non-direct child of this path was invalidated
* - isFile: when checking if a file needs to be synced, any descendant type from the most
* recent sync will be valid
*
* Whenever an invalidation is received the path, and its parents up to the root have their
* appropriate invalidation times updated. Validation times are updated on the path
* when {@link #notifySyncedPath} is called on the root sync path after a successful sync.
* An invalidation is received either when a client calls
* {@link alluxio.master.file.DefaultFileSystemMaster#needsSync} to notify that a path
* needs synchronization, or when a file is updated by an external Alluxio cluster, and
* cross cluster sync is enabled.
*
* Checking if a path needs to be synchronized involves checking the appropriate validation
* and invalidation times at each path component up to the root.
*/
@ThreadSafe
public class UfsSyncPathCache {
private static final Logger LOG = LoggerFactory.getLogger(UfsSyncPathCache.class);
public final Cache mItems;
private final Clock mClock;
// we keep the root as a separate value as we never want to evict it,
// and instead we just update it under a lock
private final SyncState mRoot = new SyncState(false);
private final Lock mRootLock = new ReentrantLock();
/**
* Creates a new instance of {@link UfsSyncPathCache}.
* @param clock the clock to use to compute sync times
*/
public UfsSyncPathCache(Clock clock) {
this(clock, null);
}
@VisibleForTesting
UfsSyncPathCache(Clock clock, @Nullable BiConsumer onRemoval) {
mClock = Preconditions.checkNotNull(clock);
mItems = CacheBuilder.newBuilder().concurrencyLevel(
Configuration.getInt(PropertyKey.MASTER_UFS_PATH_CACHE_THREADS))
.removalListener(
(removal) -> {
if (removal.wasEvicted() && removal.getKey() != null && removal.getValue() != null) {
if (onRemoval != null) {
onRemoval.accept((String) removal.getKey(), (SyncState) removal.getValue());
}
onCacheEviction((String) removal.getKey(), (SyncState) removal.getValue());
}
})
.maximumSize(Configuration.getInt(
PropertyKey.MASTER_UFS_PATH_CACHE_CAPACITY))
.build();
}
/**
* Called when an element is evicted from the cache.
* @param path the path
* @param state the state
*/
void onCacheEviction(String path, SyncState state) {
try {
// On eviction, we must mark our parent as needing a sync with our invalidation time.
// Note that if the parent has a more recent sync time than this updated invalidation
// time the parent will still not need a sync
notifyInvalidationInternal(PathUtils.getParentCleaned(path), state.mInvalidationTime);
} catch (InvalidPathException e) {
throw new RuntimeException("Should not have an invalid path in the cache", e);
}
}
/**
* Called when starting a sync.
* @return the time at the start of the sync
*/
public long recordStartSync() {
return mClock.millis();
}
/**
* Get sync times for a given path if they exist in the cache.
* @param path the path to check
* @return a pair of sync times, where element 0 is the direct sync time
* and element 1 is the recursive sync time
*/
public Optional> getSyncTimesForPath(AlluxioURI path) {
SyncState state = path.getPath().equals(AlluxioURI.SEPARATOR)
? mRoot : mItems.getIfPresent(path.getPath());
return Optional.ofNullable(state).map(syncState ->
new Pair<>(syncState.mSyncTime, syncState.mRecursiveSyncTime));
}
/**
* Check if sync should happen.
* A path is checked starting from the full path, all the way up to the root.
* At each path the largest validation time and invalidation time is computed depending
* on its descendant type. After each path component is checked, if the last invalidation
* time is more recent than the last validation time then a sync is needed. Otherwise,
* a sync is needed based on the difference between
* the current time and the last sync time and the interval.
* @param path the path to check
* @param intervalMs the frequency in ms that the sync should happen
* @param descendantType the descendant type of the operation being performed
* @return a {@link SyncCheck} object indicating if a sync should be performed
*/
public SyncCheck shouldSyncPath(AlluxioURI path, long intervalMs, DescendantType descendantType)
throws InvalidPathException {
int parentLevel = 0;
String currPath = cleanPath(path.getPath());
long lastSyncTime = 0;
long lastInvalidationTime = 0;
SyncState syncState;
boolean isFile = false;
while (true) {
syncState = currPath.equals(AlluxioURI.SEPARATOR) ? mRoot : mItems.getIfPresent(currPath);
if (syncState != null) {
// we always check if the current path has been invalidated
lastInvalidationTime = Math.max(lastInvalidationTime,
syncState.mInvalidationTime);
switch (parentLevel) {
case 0: // the base path
isFile = syncState.mIsFile;
// if the last sync on this path was a file, then we can use
// the normal validation time, as it has no children
lastSyncTime = isFile ? Math.max(lastSyncTime, syncState.mSyncTime)
: lastSyncTime;
switch (descendantType) {
case NONE:
// we are syncing no children, so we use our validation time
lastSyncTime = Math.max(lastSyncTime, syncState.mSyncTime);
break;
case ONE:
lastSyncTime = Math.max(lastSyncTime, syncState.mDirectChildrenSyncTime);
// since we are syncing the children, we must check if a child was invalidated
lastInvalidationTime = Math.max(lastInvalidationTime,
syncState.mDirectChildrenInvalidation);
break;
case ALL:
// if the last sync on this path was a file, then we can use
// the normal validation time, as it has no children
lastSyncTime = Math.max(lastSyncTime, syncState.mRecursiveSyncTime);
// since we are syncing recursively, we must check if any recursive
// child was invalidated
lastInvalidationTime = Math.max(lastInvalidationTime,
syncState.mRecursiveChildrenInvalidation);
break;
default:
throw new RuntimeException("Unexpected descendant type " + descendantType);
}
break;
case 1: // at the parent path
lastSyncTime = Math.max(lastSyncTime,
// if the child is a file we can use the direct sync time
isFile ? syncState.mDirectChildrenSyncTime :
// if we are only syncing the child we can use the direct sync time
descendantType != DescendantType.NONE ? syncState.mRecursiveSyncTime
: syncState.mDirectChildrenSyncTime);
break;
default: // at a higher ancestor path
lastSyncTime = Math.max(lastSyncTime, syncState.mRecursiveSyncTime);
}
}
if (currPath.equals(AlluxioURI.SEPARATOR)) {
break;
}
currPath = PathUtils.getParentCleaned(currPath);
parentLevel++;
}
return computeSyncResult(path, lastSyncTime, lastInvalidationTime, intervalMs);
}
private SyncCheck computeSyncResult(AlluxioURI path, long lastValidationTime,
long lastInvalidationTime, long intervalMs) {
long currentTime = mClock.millis();
SyncCheck result;
if (intervalMs == 0) {
// Always sync.
result = SyncCheck.shouldSyncWithTime(lastValidationTime);
} else if (lastInvalidationTime == 0 && intervalMs < 0) {
// if no invalidation, and a negative interval then do not sync
result = SyncCheck.shouldNotSyncWithTime(lastValidationTime);
} else if (lastInvalidationTime >= lastValidationTime) {
// if a recent invalidation then we always sync
result = SyncCheck.shouldSyncWithTime(lastValidationTime);
} else if (intervalMs < 0) {
// do not sync with a negative interval
result = SyncCheck.shouldNotSyncWithTime(lastValidationTime);
} else if ((currentTime - lastValidationTime) >= intervalMs) {
// syncing is needed based on an interval
result = SyncCheck.shouldSyncWithTime(lastValidationTime);
} else {
// syncing is not needed based on an interval
result = SyncCheck.shouldNotSyncWithTime(lastValidationTime);
}
LOG.debug("Result of should sync path {}: {}, invalidation time: {}, validation time: {},"
+ " clock time: {}",
result, path, lastInvalidationTime, lastValidationTime, currentTime);
return result;
}
/**
* Notify that a path has been invalidated.
* @param path the path
*/
@VisibleForTesting
public void notifyInvalidation(AlluxioURI path) throws InvalidPathException {
String currPath = cleanPath(path.getPath());
long time = mClock.millis();
notifyInvalidationInternal(currPath, time);
}
private void notifyInvalidationInternal(String currPath, long time)
throws InvalidPathException {
LOG.debug("Set sync invalidation for path {} at time {}", currPath, time);
int parentLevel = 0;
if (currPath.equals(AlluxioURI.SEPARATOR)) {
try (LockResource ignored = new LockResource(mRootLock)) {
mRoot.setInvalidationTime(time);
}
} else {
mItems.asMap().compute(currPath, (key, state) -> {
if (state == null) {
state = new SyncState(false);
}
// set the invalidation time for the path
state.setInvalidationTime(time);
return state;
});
}
while (!currPath.equals(AlluxioURI.SEPARATOR)) {
currPath = PathUtils.getParentCleaned(currPath);
parentLevel++;
if (currPath.equals(AlluxioURI.SEPARATOR)) {
try (LockResource ignored = new LockResource(mRootLock)) {
updateParentInvalidation(mRoot, time, parentLevel);
}
} else {
final int finalParentLevel = parentLevel;
mItems.asMap().compute(currPath, (key, state) -> {
if (state == null) {
state = new SyncState(false);
}
updateParentInvalidation(state, time, finalParentLevel);
return state;
});
}
}
}
private void updateParentInvalidation(SyncState state, long time, long parentLevel) {
state.setIsFile(false);
if (parentLevel == 1) {
// the parent has had a direct child invalidated
state.setDirectChildInvalidation(time);
}
// the parent has had a recursive child invalidated
state.setRecursiveChildInvalidation(time);
}
/**
* Notify sync happened.
* @param path the synced path
* @param descendantType the descendant type for the performed operation
* @param startTime the time at which the sync was started
* @param syncTime the time to set the sync success to, if null then the current
* clock time is used
* @param isFile true if the synced path is a file
* @return the sync state
*/
public SyncState notifySyncedPath(
AlluxioURI path, DescendantType descendantType, long startTime, @Nullable Long syncTime,
boolean isFile) {
long time = syncTime == null ? startTime :
Math.min(startTime, syncTime);
if (path.getPath().equals(AlluxioURI.SEPARATOR)) {
try (LockResource ignored = new LockResource(mRootLock)) {
Preconditions.checkState(!isFile);
updateSyncState(mRoot, time, startTime, false, descendantType);
return mRoot;
}
} else {
return mItems.asMap().compute(path.getPath(), (key, state) -> {
if (state == null) {
state = new SyncState(isFile);
}
updateSyncState(state, time, startTime, isFile, descendantType);
return state;
});
}
}
private void updateSyncState(
SyncState state, long time, long startTime, boolean isFile, DescendantType descendantType) {
state.setIsFile(isFile);
state.setValidationTime(time, descendantType);
// we can reset the invalidation times, as long as we have not received
// an invalidation since the sync was started
if (descendantType == DescendantType.ALL && state.mInvalidationTime < startTime) {
state.mInvalidationTime = 0;
}
if (descendantType == DescendantType.ALL && state.mRecursiveChildrenInvalidation < startTime) {
state.mRecursiveChildrenInvalidation = 0;
}
if ((descendantType == DescendantType.ALL || descendantType == DescendantType.ONE)
&& state.mDirectChildrenInvalidation < startTime) {
state.mDirectChildrenInvalidation = 0;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy