inodes: update SNAPSHOT at the beginning of checkout

Summary:
During checkout, the overlay is updated from the leaf up to the root, and thus
halfway through the operation, the overlay contains hashes from both the old
commit and the new commit. When the operation finishes, this discrepency no
longer exist as the overlay has been fully updated to the destination commit.

However, if EdenFS either crashes, or is being killed during the checkout
operation, the overlay will be left in this half updated state. Restarting
EdenFS and running a status or checkout operation will walk the inodes
hierarchy and construct them from the overlay, forcing a large amount of blobs
and manifests to be fetched from the network. This can be disruptive and lead
to updates/status taking a very long time due to potentially having to fetch
the entirety of the repository.

Thankfully, EdenFS can recognize this situation by simply updating its SNAPSHOT
file at the beginning of the checkout operation. If EdenFS crashes or is
killed, Mercurial will send EdenFS its own parent commit, which EdenFS can
compare and disagree about, failing the operation instead of fetching large
parts of the repository.

Reviewed By: chadaustin

Differential Revision: D33591210

fbshipit-source-id: b724429bb848425d2b1684b160ad8e1bdfee252a
This commit is contained in:
Xavier Deguillard 2022-02-01 20:45:41 -08:00 committed by Facebook GitHub Bot
parent 572b6d9a8f
commit 9b3486480b
3 changed files with 41 additions and 19 deletions

View File

@ -35,31 +35,47 @@ CheckoutContext::CheckoutContext(
CheckoutContext::~CheckoutContext() {}
void CheckoutContext::start(RenameLock&& renameLock) {
renameLock_ = std::move(renameLock);
}
Future<vector<CheckoutConflict>> CheckoutContext::finish(
void CheckoutContext::start(
RenameLock&& renameLock,
EdenMount::ParentLock::LockedPtr&& parentLock,
RootId newSnapshot) {
renameLock_ = std::move(renameLock);
// Only update the parent if it is not a dry run.
if (!isDryRun()) {
std::optional<RootId> oldParent;
if (parentLock) {
XCHECK(parentLock->checkoutInProgress);
oldParent = parentLock->commitHash;
// Update the in-memory snapshot ID
parentLock->commitHash = newSnapshot;
}
auto config = mount_->getCheckoutConfig();
// Save the new snapshot hash to the config
config->setParentCommit(std::move(newSnapshot));
if (!oldParent.has_value()) {
config->setParentCommit(std::move(newSnapshot));
} else {
config->setCheckoutInProgress(oldParent.value(), newSnapshot);
}
XLOG(DBG1) << "updated snapshot for " << config->getMountPath() << " from "
<< (oldParent.has_value() ? oldParent->value() : "<none>")
<< " to " << newSnapshot;
}
}
parentLock.unlock();
Future<vector<CheckoutConflict>> CheckoutContext::finish(RootId newSnapshot) {
auto config = mount_->getCheckoutConfig();
auto parentCommit = config->getParentCommit();
if (parentCommit.isCheckoutInProgress()) {
XCHECK_EQ(
parentCommit.getCurrentRootId(ParentCommit::RootIdPreference::To)
.value(),
newSnapshot);
config->setParentCommit(newSnapshot);
}
// Release the rename lock.
// This allows any filesystem unlink() or rename() operations to proceed.

View File

@ -73,8 +73,15 @@ class CheckoutContext {
/**
* Start the checkout operation.
*
* As a side effect, this updates the SNAPSHOT file on disk, in the case
* where EdenFS is killed or crashes during checkout, this allows EdenFS to
* detect that Mercurial is out of date.
*/
void start(RenameLock&& renameLock);
void start(
RenameLock&& renameLock,
EdenMount::ParentLock::LockedPtr&& parentLock,
RootId newSnapshot);
/**
* Complete the checkout operation
@ -82,9 +89,7 @@ class CheckoutContext {
* Returns the list of conflicts and errors that were encountered during the
* operation.
*/
folly::Future<std::vector<CheckoutConflict>> finish(
EdenMount::ParentLock::LockedPtr&& parentLock,
RootId newSnapshot);
folly::Future<std::vector<CheckoutConflict>> finish(RootId newSnapshot);
void addConflict(ConflictType type, RelativePathPiece path);
void

View File

@ -693,14 +693,13 @@ folly::Future<SetPathObjectIdResultAndTimes> EdenMount::setPathObjectId(
auto [targetTreeInode, incomingTree] = results;
targetTreeInode->unloadChildrenUnreferencedByFs();
// TODO(@yipu): Remove rename lock
ctx->start(this->acquireRenameLock());
ctx->start(this->acquireRenameLock(), {}, rootId);
setPathObjectIdTime->didAcquireRenameLock = stopWatch.elapsed();
return targetTreeInode->checkout(ctx.get(), nullptr, incomingTree);
})
.thenValue([this, ctx, setPathObjectIdTime, stopWatch, rootId](auto&&) {
.thenValue([ctx, setPathObjectIdTime, stopWatch, rootId](auto&&) {
setPathObjectIdTime->didCheckout = stopWatch.elapsed();
// Complete and save the new snapshot
return ctx->finish(parentState_.wlock(), rootId);
return ctx->finish(rootId);
})
.thenValue([ctx, setPathObjectIdTime, stopWatch](
std::vector<CheckoutConflict>&& conflicts) {
@ -1351,8 +1350,10 @@ folly::Future<CheckoutResult> EdenMount::checkout(
checkoutTimes->didDiff = stopWatch.elapsed();
// Perform the requested checkout operation after the journal diff
// completes.
ctx->start(this->acquireRenameLock());
// completes. This also updates the SNAPSHOT file to make sure that an
// interrupted checkout can be properly detected.
auto renameLock = this->acquireRenameLock();
ctx->start(std::move(renameLock), parentState_.wlock(), snapshotHash);
checkoutTimes->didAcquireRenameLock = stopWatch.elapsed();
@ -1390,11 +1391,11 @@ folly::Future<CheckoutResult> EdenMount::checkout(
return rootInode->checkout(ctx.get(), fromTree, toTree);
});
})
.thenValue([this, ctx, checkoutTimes, stopWatch, snapshotHash](auto&&) {
.thenValue([ctx, checkoutTimes, stopWatch, snapshotHash](auto&&) {
checkoutTimes->didCheckout = stopWatch.elapsed();
// Complete the checkout
return ctx->finish(parentState_.wlock(), snapshotHash);
return ctx->finish(snapshotHash);
})
.ensure([this]() {
// Checkout completed, make sure to always reset the checkoutInProgress