inodes: Use ImmediateFuture in most part of checkout

Summary:
The checkout code is a heavy user of `QueuedImmediateExecutor` due to relying
on `folly::Future`, the immediate executor is used to convert `ImmediateFuture`
onto `folly::Future`. Unfortunately, this transformation is unsafe and leads to
deadlocks.

The full migration of checkout to `ImmediateFuture` is done in the next diff,
and as a result still relies on `QueuedImmediateExecutor` in `EdenMount`.

As part of this diff, the CheckoutAction is greatly simplified, from relying on
refcounts and `Promise`, the code now merely makes use of future chaining, thus
making it easier to read.

Reviewed By: genevievehelsel

Differential Revision: D50347461

fbshipit-source-id: 8159f523f6fbe4414b99ecb924ba0446f7edeb6e
This commit is contained in:
Xavier Deguillard 2023-10-24 12:03:55 -07:00 committed by Facebook GitHub Bot
parent f713eef690
commit 048eec83e8
5 changed files with 125 additions and 223 deletions

View File

@ -63,83 +63,34 @@ PathComponentPiece CheckoutAction::getEntryName() const {
: newScmEntry_.value().first;
}
class CheckoutAction::LoadingRefcount {
public:
explicit LoadingRefcount(CheckoutAction* action) : action_(action) {
action_->numLoadsPending_.fetch_add(1);
}
LoadingRefcount(LoadingRefcount&& other) noexcept : action_(other.action_) {
other.action_ = nullptr;
}
LoadingRefcount& operator=(LoadingRefcount&& other) noexcept {
decref();
action_ = other.action_;
other.action_ = nullptr;
return *this;
}
~LoadingRefcount() {
decref();
}
/**
* Implement the arrow operator, so that LoadingRefcount can be used like a
* pointer. This allows users to easily call through it into the underlying
* CheckoutAction methods.
*/
CheckoutAction* operator->() const {
return action_;
}
private:
void decref() {
if (action_) {
auto oldCount = action_->numLoadsPending_.fetch_sub(1);
if (oldCount == 1) {
// We were the last load to complete. We can perform the action now.
action_->allLoadsComplete();
}
}
}
CheckoutAction* action_;
};
Future<InvalidationRequired> CheckoutAction::run(
ImmediateFuture<InvalidationRequired> CheckoutAction::run(
CheckoutContext* ctx,
ObjectStore* store) {
// Immediately create one LoadingRefcount, to ensure that our
// numLoadsPending_ refcount does not drop to 0 until after we have started
// all required load operations.
//
// Even if all loads complete immediately, allLoadsComplete() won't be called
// until this LoadingRefcount is destroyed.
LoadingRefcount refcount{this};
std::vector<ImmediateFuture<folly::Unit>> loadFutures;
try {
// Load the Blob or Tree for the old TreeEntry.
if (oldScmEntry_.has_value()) {
const auto& oldEntry = oldScmEntry_.value();
if (oldEntry.second.isTree()) {
store->getTree(oldEntry.second.getHash(), ctx->getFetchContext())
.thenValue([rc = LoadingRefcount(this)](
std::shared_ptr<const Tree> oldTree) {
rc->setOldTree(std::move(oldTree));
})
.semi()
.via(&folly::QueuedImmediateExecutor::instance())
.thenError([rc = LoadingRefcount(this)](exception_wrapper&& ew) {
rc->error("error getting old tree", std::move(ew));
});
loadFutures.emplace_back(
store->getTree(oldEntry.second.getHash(), ctx->getFetchContext())
.thenValue([self = shared_from_this()](
std::shared_ptr<const Tree> oldTree) {
self->setOldTree(std::move(oldTree));
})
.thenError([self = shared_from_this()](exception_wrapper&& ew) {
self->error("error getting old tree", std::move(ew));
}));
} else {
store->getBlobSha1(oldEntry.second.getHash(), ctx->getFetchContext())
.thenValue([rc = LoadingRefcount(this)](Hash20 oldBlobSha1) {
rc->setOldBlob(std::move(oldBlobSha1));
})
.semi()
.via(&folly::QueuedImmediateExecutor::instance())
.thenError([rc = LoadingRefcount(this)](exception_wrapper&& ew) {
rc->error("error getting old blob Sha1", std::move(ew));
});
loadFutures.emplace_back(
store
->getBlobSha1(oldEntry.second.getHash(), ctx->getFetchContext())
.thenValue([self = shared_from_this()](Hash20 oldBlobSha1) {
self->setOldBlob(std::move(oldBlobSha1));
})
.thenError([self = shared_from_this()](exception_wrapper&& ew) {
self->error("error getting old blob Sha1", std::move(ew));
}));
}
}
@ -147,41 +98,61 @@ Future<InvalidationRequired> CheckoutAction::run(
if (newScmEntry_.has_value()) {
const auto& newEntry = newScmEntry_.value();
if (newEntry.second.isTree()) {
store->getTree(newEntry.second.getHash(), ctx->getFetchContext())
.thenValue([rc = LoadingRefcount(this)](
std::shared_ptr<const Tree> newTree) {
rc->setNewTree(std::move(newTree));
})
.semi()
.via(&folly::QueuedImmediateExecutor::instance())
.thenError([rc = LoadingRefcount(this)](exception_wrapper&& ew) {
rc->error("error getting new tree", std::move(ew));
});
loadFutures.emplace_back(
store->getTree(newEntry.second.getHash(), ctx->getFetchContext())
.thenValue([self = shared_from_this()](
std::shared_ptr<const Tree> newTree) {
self->setNewTree(std::move(newTree));
})
.thenError([self = shared_from_this()](exception_wrapper&& ew) {
self->error("error getting new tree", std::move(ew));
}));
} else {
// We don't actually compare the new blob to anything, so we don't need
// to fetch it. This just marks that the new inode will be a file.
LoadingRefcount(this)->setNewBlob();
setNewBlob();
}
}
// If we were constructed with a Future<InodePtr>, wait for it.
if (!inode_) {
XCHECK(inodeFuture_.valid());
std::move(inodeFuture_)
.thenValue([rc = LoadingRefcount(this)](InodePtr inode) {
rc->setInode(std::move(inode));
})
.thenError([rc = LoadingRefcount(this)](exception_wrapper&& ew) {
rc->error("error getting inode", std::move(ew));
});
loadFutures.emplace_back(
std::move(inodeFuture_)
.thenValue([self = shared_from_this()](InodePtr inode) {
self->setInode(std::move(inode));
})
.thenError([self = shared_from_this()](exception_wrapper&& ew) {
self->error("error getting inode", std::move(ew));
})
.semi());
}
} catch (...) {
auto ew = exception_wrapper{std::current_exception()};
refcount->error(
"error preparing to load data for checkout action", std::move(ew));
error("error preparing to load data for checkout action", std::move(ew));
}
return promise_.getFuture();
return collectAll(std::move(loadFutures))
.thenValue(
[self = shared_from_this()](
auto&&) -> ImmediateFuture<InvalidationRequired> {
if (!self->errors_.empty()) {
// If multiple errors occurred, we log them all, but only
// propagate up the first one. If necessary we could change this
// to create a single exception that contains all of the messages
// concatenated together.
XLOG(ERR) << "multiple errors while attempting to load data for "
"checkout action:";
for (const auto& ew : self->errors_) {
XLOG(ERR) << "CheckoutAction error: "
<< folly::exceptionStr(ew);
}
return makeImmediateFuture<InvalidationRequired>(
self->errors_[0]);
}
return self->doAction();
});
}
void CheckoutAction::setOldTree(std::shared_ptr<const Tree> tree) {
@ -220,71 +191,17 @@ void CheckoutAction::error(
errors_.push_back(std::move(ew));
}
void CheckoutAction::allLoadsComplete() noexcept {
if (!ensureDataReady()) {
// ensureDataReady() will fulfilled promise_ with an exception
return;
}
try {
doAction().thenTry([this](folly::Try<InvalidationRequired>&& t) {
this->promise_.setTry(std::move(t));
});
} catch (...) {
auto ew = exception_wrapper{std::current_exception()};
promise_.setException(std::move(ew));
}
}
bool CheckoutAction::ensureDataReady() noexcept {
if (!errors_.empty()) {
// If multiple errors occurred, we log them all, but only propagate
// up the first one. If necessary we could change this to create
// a single exception that contains all of the messages concatenated
// together.
if (errors_.size() > 1) {
XLOG(ERR) << "multiple errors while attempting to load data for "
"checkout action:";
for (const auto& ew : errors_) {
XLOG(ERR) << "CheckoutAction error: " << folly::exceptionStr(ew);
}
}
promise_.setException(errors_[0]);
return false;
}
// Make sure we actually have all the data we need.
// (Just in case something went wrong when wiring up the callbacks in such a
// way that we also failed to call error().)
if (oldScmEntry_.has_value() && (!oldTree_ && !oldBlobSha1_)) {
promise_.setException(
std::runtime_error("failed to load data for old TreeEntry"));
return false;
}
if (newScmEntry_.has_value() && (!newTree_ && !newBlobMarker_)) {
promise_.setException(
std::runtime_error("failed to load data for new TreeEntry"));
return false;
}
if (!inode_) {
promise_.setException(std::runtime_error("failed to load affected inode"));
return false;
}
return true;
}
Future<InvalidationRequired> CheckoutAction::doAction() {
ImmediateFuture<InvalidationRequired> CheckoutAction::doAction() {
// All the data is ready and we're ready to go!
// Check for conflicts first.
return hasConflict().thenValue(
[this](
bool conflictWasAddedToCtx) -> folly::Future<InvalidationRequired> {
[self = shared_from_this()](
bool conflictWasAddedToCtx) -> ImmediateFuture<InvalidationRequired> {
// Note that even if we know we are not going to apply the changes, we
// must still run hasConflict() first because we rely on its
// side-effects.
if (conflictWasAddedToCtx && !ctx_->forceUpdate()) {
if (conflictWasAddedToCtx && !self->ctx_->forceUpdate()) {
// We only report conflicts for files, not directories. The only
// possible conflict that can occur here if this inode is a TreeInode
// is that the old source control state was for a file. There aren't
@ -302,18 +219,18 @@ Future<InvalidationRequired> CheckoutAction::doAction() {
// into a PathComponent owned either by oldScmEntry_ or newScmEntry_.
// Therefore don't move these scm entries, to make sure we don't
// invalidate the PathComponentPiece data.
auto parent = inode_->getParent(ctx_->renameLock());
auto parent = self->inode_->getParent(self->ctx_->renameLock());
return parent->checkoutUpdateEntry(
ctx_,
getEntryName(),
std::move(inode_),
std::move(oldTree_),
std::move(newTree_),
newScmEntry_);
self->ctx_,
self->getEntryName(),
std::move(self->inode_),
std::move(self->oldTree_),
std::move(self->newTree_),
self->newScmEntry_);
});
}
Future<bool> CheckoutAction::hasConflict() {
ImmediateFuture<bool> CheckoutAction::hasConflict() {
if (oldTree_) {
auto treeInode = inode_.asTreePtrOrNull();
if (!treeInode) {
@ -346,7 +263,7 @@ Future<bool> CheckoutAction::hasConflict() {
oldScmEntry_.value().second.getType(),
ctx_->getWindowsSymlinksEnabled()),
ctx_->getFetchContext())
.thenValue([this](bool isSame) {
.thenValue([self = shared_from_this()](bool isSame) {
if (isSame) {
// no conflict
return false;
@ -358,13 +275,12 @@ Future<bool> CheckoutAction::hasConflict() {
// conflict.
// - If the file does not exist in the new tree, then this is a
// MODIFIED_REMOVED conflict.
auto conflictType = newScmEntry_ ? ConflictType::MODIFIED_MODIFIED
: ConflictType::MODIFIED_REMOVED;
ctx_->addConflict(conflictType, inode_.get());
auto conflictType = self->newScmEntry_
? ConflictType::MODIFIED_MODIFIED
: ConflictType::MODIFIED_REMOVED;
self->ctx_->addConflict(conflictType, self->inode_.get());
return true;
})
.semi()
.via(&folly::QueuedImmediateExecutor::instance());
});
}
XDCHECK(!oldScmEntry_) << "Both oldTree_ and oldBlob_ are nullptr, "

View File

@ -14,6 +14,7 @@
#include "eden/fs/fuse/Invalidation.h"
#include "eden/fs/inodes/InodePtr.h"
#include "eden/fs/model/Tree.h"
#include "eden/fs/utils/ImmediateFuture.h"
namespace folly {
class exception_wrapper;
@ -41,7 +42,7 @@ class ObjectStore;
* CheckoutAction objects are ever created for these cases, since these actions
* can be taken immediately.
*/
class CheckoutAction {
class CheckoutAction : public std::enable_shared_from_this<CheckoutAction> {
public:
/**
* Create a CheckoutAction with an already loaded Inode object.
@ -95,13 +96,11 @@ class CheckoutAction {
* whether the caller is responsible for invalidating the directory's inode
* cache in the kernel.
*/
FOLLY_NODISCARD folly::Future<InvalidationRequired> run(
FOLLY_NODISCARD ImmediateFuture<InvalidationRequired> run(
CheckoutContext* ctx,
ObjectStore* store);
private:
class LoadingRefcount;
enum InternalConstructor {
INTERNAL,
};
@ -121,13 +120,13 @@ class CheckoutAction {
void allLoadsComplete() noexcept;
bool ensureDataReady() noexcept;
folly::Future<bool> hasConflict();
ImmediateFuture<bool> hasConflict();
/**
* Return whether the directory's contents have changed and the
* inode's readdir cache must be flushed.
*/
FOLLY_NODISCARD folly::Future<InvalidationRequired> doAction();
FOLLY_NODISCARD ImmediateFuture<InvalidationRequired> doAction();
/**
* The context for the in-progress checkout operation.
@ -186,11 +185,7 @@ class CheckoutAction {
* The errors vector keeps track of any errors that occurred while trying to
* load the data needed to perform the checkout action.
*/
// TODO: Add locking?
std::vector<folly::exception_wrapper> errors_;
/**
* The promise that we will fulfil when the CheckoutAction is complete.
*/
folly::Promise<InvalidationRequired> promise_;
};
} // namespace facebook::eden

View File

@ -1513,13 +1513,13 @@ folly::Future<CheckoutResult> EdenMount::checkout(
return serverState_->getFaultInjector()
.checkAsync("inodeCheckout", getPath().view())
.semi()
.via(getServerThreadPool().get())
.thenValue([ctx, treeResults = std::move(treeResults), rootInode](
auto&&) mutable {
auto& [fromTree, toTree] = treeResults;
return rootInode->checkout(ctx.get(), fromTree.tree, toTree.tree);
});
})
.semi()
.via(&folly::QueuedImmediateExecutor::instance());
})
.thenValue([ctx, checkoutTimes, stopWatch, snapshotHash](auto&&) {
checkoutTimes->didCheckout = stopWatch.elapsed();

View File

@ -3029,7 +3029,7 @@ ImmediateFuture<Unit> TreeInode::computeDiff(
});
}
Future<Unit> TreeInode::checkout(
ImmediateFuture<Unit> TreeInode::checkout(
CheckoutContext* ctx,
std::shared_ptr<const Tree> fromTree,
std::shared_ptr<const Tree> toTree) {
@ -3038,8 +3038,8 @@ Future<Unit> TreeInode::checkout(
<< " --> "
<< (toTree ? toTree->getHash().toLogString() : "<none>");
vector<unique_ptr<CheckoutAction>> actions;
vector<IncompleteInodeLoad> pendingLoads;
std::vector<std::shared_ptr<CheckoutAction>> actions;
std::vector<IncompleteInodeLoad> pendingLoads;
// This default to true on Windows to always make sure that the directory is
// a placeholder and is safe to be dematerialized. On Windows, adding a
@ -3062,21 +3062,19 @@ Future<Unit> TreeInode::checkout(
}
// Now start all of the checkout actions
vector<Future<InvalidationRequired>> actionFutures;
std::vector<ImmediateFuture<InvalidationRequired>> actionFutures;
actionFutures.reserve(actions.size());
for (const auto& action : actions) {
actionFutures.emplace_back(action->run(ctx, &getObjectStore()));
}
ImmediateFuture<Unit> faultFuture =
auto faultFuture =
getMount()->getServerState()->getFaultInjector().checkAsync(
"TreeInode::checkout", getLogPath(), ctx->isDryRun());
folly::SemiFuture<vector<folly::Try<facebook::eden::InvalidationRequired>>>
collectFuture = folly::collectAll(actionFutures);
auto collectFuture = collectAll(std::move(actionFutures));
// Wait for all of the actions, and record any errors.
return std::move(faultFuture)
.semi()
.toUnsafeFuture()
.thenValue([collectFuture = std::move(collectFuture)](auto&&) mutable {
return std::move(collectFuture);
})
@ -3086,7 +3084,8 @@ Future<Unit> TreeInode::checkout(
toTree = std::move(toTree),
actions = std::move(actions),
shouldInvalidateDirectory](
vector<folly::Try<InvalidationRequired>> actionResults) mutable {
vector<folly::Try<InvalidationRequired>> actionResults) mutable
-> ImmediateFuture<folly::Unit> {
// Record any errors that occurred
size_t numErrors = 0;
for (size_t n = 0; n < actionResults.size(); ++n) {
@ -3127,25 +3126,16 @@ Future<Unit> TreeInode::checkout(
});
}
auto fut = std::move(invalidation)
.thenValue([self,
ctx,
toTree = std::move(toTree),
numErrors](auto&&) {
// Update our state in the overlay
self->saveOverlayPostCheckout(ctx, toTree.get());
return std::move(invalidation)
.thenValue(
[self, ctx, toTree = std::move(toTree), numErrors](auto&&) {
// Update our state in the overlay
self->saveOverlayPostCheckout(ctx, toTree.get());
XLOG(DBG4) << "checkout: finished update of "
<< self->getLogPath() << ": "
<< numErrors << " errors";
});
if (fut.isReady()) {
return folly::makeFuture(std::move(fut).getTry());
} else {
return std::move(fut).semi().via(
self->getMount()->getServerThreadPool().get());
}
XLOG(DBG4) << "checkout: finished update of "
<< self->getLogPath() << ": " << numErrors
<< " errors";
});
});
}
@ -3206,7 +3196,7 @@ void TreeInode::computeCheckoutActions(
CheckoutContext* ctx,
const Tree* fromTree,
const Tree* toTree,
vector<unique_ptr<CheckoutAction>>& actions,
vector<std::shared_ptr<CheckoutAction>>& actions,
vector<IncompleteInodeLoad>& pendingLoads,
bool& wasDirectoryListModified) {
// Grab the contents_ lock for the duration of this function
@ -3234,7 +3224,7 @@ void TreeInode::computeCheckoutActions(
auto newIter = toTree ? toTree->cbegin() : emptyEntries.cbegin();
auto newEnd = toTree ? toTree->cend() : emptyEntries.cend();
while (true) {
unique_ptr<CheckoutAction> action;
std::shared_ptr<CheckoutAction> action;
if (oldIter == oldEnd) {
if (newIter == newEnd) {
@ -3304,7 +3294,7 @@ void TreeInode::computeCheckoutActions(
}
}
unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
std::shared_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
CheckoutContext* ctx,
TreeInodeState& state,
const Tree::value_type* oldScmEntry,
@ -3357,7 +3347,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
auto& entry = it->second;
if (auto childPtr = entry.getInodePtr()) {
// If the inode is already loaded, create a CheckoutAction to process it
return make_unique<CheckoutAction>(
return std::make_shared<CheckoutAction>(
ctx, oldScmEntry, newScmEntry, std::move(childPtr));
}
@ -3383,7 +3373,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
// CheckoutAction to process it once it is loaded.
auto inodeFuture = loadChildLocked(
contents, name, entry, pendingLoads, ctx->getFetchContext());
return make_unique<CheckoutAction>(
return std::make_shared<CheckoutAction>(
ctx, oldScmEntry, newScmEntry, std::move(inodeFuture));
} else {
XLOG(DBG6) << "not loading child: inode=" << getNodeId()
@ -3415,7 +3405,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
// for sure is to load the inode.
auto inodeFuture = loadChildLocked(
contents, name, entry, pendingLoads, ctx->getFetchContext());
return make_unique<CheckoutAction>(
return std::make_shared<CheckoutAction>(
ctx, oldScmEntry, newScmEntry, std::move(inodeFuture));
}
case ObjectComparison::Identical:
@ -3435,7 +3425,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
if (entry.isDirectory()) {
auto inodeFuture = loadChildLocked(
contents, name, entry, pendingLoads, ctx->getFetchContext());
return make_unique<CheckoutAction>(
return std::make_shared<CheckoutAction>(
ctx, oldScmEntry, newScmEntry, std::move(inodeFuture));
}
@ -3475,7 +3465,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
<< getNodeId() << " child=" << name;
auto inodeFuture = loadChildLocked(
contents, name, entry, pendingLoads, ctx->getFetchContext());
return make_unique<CheckoutAction>(
return std::make_shared<CheckoutAction>(
ctx, oldScmEntry, newScmEntry, std::move(inodeFuture));
}
}
@ -3520,7 +3510,7 @@ unique_ptr<CheckoutAction> TreeInode::processCheckoutEntry(
return nullptr;
}
std::unique_ptr<CheckoutAction> TreeInode::processAbsentCheckoutEntry(
std::shared_ptr<CheckoutAction> TreeInode::processAbsentCheckoutEntry(
CheckoutContext* ctx,
TreeInodeState& state,
const Tree::value_type* oldScmEntry,
@ -3602,7 +3592,7 @@ PathComponent getInodeName(CheckoutContext* ctx, const InodePtr& inode) {
}
} // namespace
Future<InvalidationRequired> TreeInode::checkoutUpdateEntry(
ImmediateFuture<InvalidationRequired> TreeInode::checkoutUpdateEntry(
CheckoutContext* ctx,
PathComponentPiece name,
InodePtr inode,
@ -3722,7 +3712,8 @@ Future<InvalidationRequired> TreeInode::checkoutUpdateEntry(
parentInode = inodePtrFromThis(),
treeInode,
windowsSymlinksEnabled,
newScmEntry](auto&&) mutable -> folly::Future<InvalidationRequired> {
newScmEntry](
auto&&) mutable -> ImmediateFuture<InvalidationRequired> {
if (ctx->isDryRun()) {
// If this is a dry run, simply report conflicts and don't update
// or invalidate the inode.

View File

@ -13,9 +13,9 @@
#include <optional>
#include "eden/common/utils/FileOffset.h"
#include "eden/fs/fuse/Invalidation.h"
#include "eden/fs/inodes/CheckoutAction.h"
#include "eden/fs/inodes/DirEntry.h"
#include "eden/fs/inodes/InodeBase.h"
#include "eden/fs/model/Tree.h"
#include "eden/fs/utils/PathFuncs.h"
namespace facebook::eden {
@ -350,7 +350,7 @@ class TreeInode final : public InodeBaseMetadata<DirContents> {
* @return Returns a future that will be fulfilled once this tree and all of
* its children have been updated.
*/
FOLLY_NODISCARD folly::Future<folly::Unit> checkout(
FOLLY_NODISCARD ImmediateFuture<folly::Unit> checkout(
CheckoutContext* ctx,
std::shared_ptr<const Tree> fromTree,
std::shared_ptr<const Tree> toTree);
@ -489,7 +489,7 @@ class TreeInode final : public InodeBaseMetadata<DirContents> {
* This entry will refer to a tree if and only if the newTree parameter
* is non-null.
*/
FOLLY_NODISCARD folly::Future<InvalidationRequired> checkoutUpdateEntry(
FOLLY_NODISCARD ImmediateFuture<InvalidationRequired> checkoutUpdateEntry(
CheckoutContext* ctx,
PathComponentPiece name,
InodePtr inode,
@ -778,7 +778,7 @@ class TreeInode final : public InodeBaseMetadata<DirContents> {
CheckoutContext* ctx,
const Tree* fromTree,
const Tree* toTree,
std::vector<std::unique_ptr<CheckoutAction>>& actions,
std::vector<std::shared_ptr<CheckoutAction>>& actions,
std::vector<IncompleteInodeLoad>& pendingLoads,
bool& wasDirectoryListModified);
@ -787,9 +787,9 @@ class TreeInode final : public InodeBaseMetadata<DirContents> {
* modified the directory contents, which implies the return value is nullptr.
*
* This function could return a std::variant of InvalidationRequired and
* std::unique_ptr<CheckoutAction> instead of setting a boolean.
* std::shared_ptr<CheckoutAction> instead of setting a boolean.
*/
std::unique_ptr<CheckoutAction> processCheckoutEntry(
std::shared_ptr<CheckoutAction> processCheckoutEntry(
CheckoutContext* ctx,
TreeInodeState& contents,
const Tree::value_type* oldScmEntry,
@ -797,7 +797,7 @@ class TreeInode final : public InodeBaseMetadata<DirContents> {
std::vector<IncompleteInodeLoad>& pendingLoads,
bool& wasDirectoryListModified);
std::unique_ptr<CheckoutAction> processAbsentCheckoutEntry(
std::shared_ptr<CheckoutAction> processAbsentCheckoutEntry(
CheckoutContext* ctx,
TreeInodeState& state,
const Tree::value_type* oldScmEntry,