sapling/eden/fs/config/CheckoutConfig.cpp
Xavier Deguillard 34edb7b618 win: re-use guid for the lifetime of the checkout
Summary:
On Windows, the GUID of the mount point identifies the virtualization instance,
that GUID is then propagated automatically to the created placeholders when
these are created as a response to a getPlaceholderInfo callback.

When the placeholders are created by EdenFS when invalidating directories we
have to pass GUID. The documentation isn't clear about whether that GUID needs
to be identical to the mount point GUID, but for a very long time these have
been mismatching due to the mount point GUID being generated at startup time
and not re-used.

One of the most common issue that users have reported is that sometimes
operations on the repository start failing with the error "The provider that
supports file system virtualization is temporarily unavailable". Looking at the
output of `fsutil reparsepoint query` for all the directories from the file
that triggers the error to the root of the repositories, shows that one of the
folder and its descendant don't share the same GUID, removing it solves the
issue.

It's not clear to me why this issue doesn't always reproduce when restarting
EdenFS, but a simple step that we can take to solve this is to always re-use
the GUID, and that hopefully will lead to the GUID always being the same and
the error to go away.

Reviewed By: fanzeyi

Differential Revision: D25513122

fbshipit-source-id: 0058dedbd7fd8ccae1c9527612ac220bc6775c69
2020-12-15 08:07:49 -08:00

208 lines
6.7 KiB
C++

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
#include "eden/fs/config/CheckoutConfig.h"
#include <cpptoml.h> // @manual=fbsource//third-party/cpptoml:cpptoml
#include <folly/Range.h>
#include <folly/String.h>
#include <folly/io/Cursor.h>
#include <folly/io/IOBuf.h>
#include <folly/json.h>
#include "eden/fs/utils/FileUtils.h"
#include "eden/fs/utils/PathMap.h"
using folly::ByteRange;
using folly::IOBuf;
using folly::StringPiece;
namespace {
// TOML config file for the individual client.
const facebook::eden::RelativePathPiece kCheckoutConfig{"config.toml"};
// Keys for the TOML config file.
constexpr folly::StringPiece kRepoSection{"repository"};
constexpr folly::StringPiece kRepoSourceKey{"path"};
constexpr folly::StringPiece kRepoTypeKey{"type"};
constexpr folly::StringPiece kRepoCaseSensitiveKey{"case-sensitive"};
#ifdef _WIN32
constexpr folly::StringPiece kRepoGuid{"guid"};
#endif
// Files of interest in the client directory.
const facebook::eden::RelativePathPiece kSnapshotFile{"SNAPSHOT"};
const facebook::eden::RelativePathPiece kOverlayDir{"local"};
// File holding mapping of client directories.
const facebook::eden::RelativePathPiece kClientDirectoryMap{"config.json"};
// Constants for use with the SNAPSHOT file
//
// The SNAPSHOT file format is:
// - 4 byte identifier: "eden"
// - 4 byte format version number (big endian)
// - 20 byte commit ID
// - (Optional 20 byte commit ID, only present when there are 2 parents)
constexpr folly::StringPiece kSnapshotFileMagic{"eden"};
enum : uint32_t {
kSnapshotHeaderSize = 8,
kSnapshotFormatVersion = 1,
};
} // namespace
namespace facebook {
namespace eden {
CheckoutConfig::CheckoutConfig(
AbsolutePathPiece mountPath,
AbsolutePathPiece clientDirectory)
: clientDirectory_(clientDirectory), mountPath_(mountPath) {}
ParentCommits CheckoutConfig::getParentCommits() const {
// Read the snapshot.
auto snapshotFile = getSnapshotPath();
auto snapshotFileContents = readFile(snapshotFile).value();
StringPiece contents{snapshotFileContents};
if (!contents.startsWith(kSnapshotFileMagic)) {
// Try reading an old-style SNAPSHOT file that just contains a single
// commit ID, as an ASCII hexadecimal string.
//
// TODO: In the not-to-distant future we can remove support for this old
// format, and simply throw an exception here if the snapshot file does not
// start with the correct identifier bytes.
auto snapshotID = folly::trimWhitespace(contents);
return ParentCommits{Hash{snapshotID}};
}
if (contents.size() < kSnapshotHeaderSize) {
throw std::runtime_error(folly::sformat(
"eden SNAPSHOT file is too short ({} bytes): {}",
contents.size(),
snapshotFile));
}
IOBuf buf(IOBuf::WRAP_BUFFER, ByteRange{contents});
folly::io::Cursor cursor(&buf);
cursor += kSnapshotFileMagic.size();
auto version = cursor.readBE<uint32_t>();
if (version != kSnapshotFormatVersion) {
throw std::runtime_error(folly::sformat(
"unsupported eden SNAPSHOT file format (version {}): {}",
uint32_t{version},
snapshotFile));
}
auto sizeLeft = cursor.length();
if (sizeLeft != Hash::RAW_SIZE && sizeLeft != (Hash::RAW_SIZE * 2)) {
throw std::runtime_error(folly::sformat(
"unexpected length for eden SNAPSHOT file ({} bytes): {}",
contents.size(),
snapshotFile));
}
ParentCommits parents;
cursor.pull(parents.parent1().mutableBytes().data(), Hash::RAW_SIZE);
if (!cursor.isAtEnd()) {
parents.parent2() = Hash{};
cursor.pull(parents.parent2()->mutableBytes().data(), Hash::RAW_SIZE);
}
return parents;
}
void CheckoutConfig::setParentCommits(const ParentCommits& parents) const {
std::array<uint8_t, kSnapshotHeaderSize + (2 * Hash::RAW_SIZE)> buffer;
IOBuf buf(IOBuf::WRAP_BUFFER, ByteRange{buffer});
folly::io::RWPrivateCursor cursor{&buf};
// Snapshot file format:
// 4-byte identifier: "eden"
cursor.push(ByteRange{kSnapshotFileMagic});
// 4-byte format version identifier
cursor.writeBE<uint32_t>(kSnapshotFormatVersion);
// 20-byte commit ID: parent1
cursor.push(parents.parent1().getBytes());
// Optional 20-byte commit ID: parent2
if (parents.parent2().has_value()) {
cursor.push(parents.parent2()->getBytes());
XCHECK(cursor.isAtEnd());
}
size_t writtenSize = cursor - folly::io::RWPrivateCursor{&buf};
ByteRange snapshotData{buffer.data(), writtenSize};
writeFileAtomic(getSnapshotPath(), snapshotData).value();
}
void CheckoutConfig::setParentCommits(Hash parent1, std::optional<Hash> parent2)
const {
return setParentCommits(ParentCommits{parent1, parent2});
}
const AbsolutePath& CheckoutConfig::getClientDirectory() const {
return clientDirectory_;
}
bool CheckoutConfig::getCaseSensitive() const {
return caseSensitive_;
}
AbsolutePath CheckoutConfig::getSnapshotPath() const {
return clientDirectory_ + kSnapshotFile;
}
AbsolutePath CheckoutConfig::getOverlayPath() const {
return clientDirectory_ + kOverlayDir;
}
std::unique_ptr<CheckoutConfig> CheckoutConfig::loadFromClientDirectory(
AbsolutePathPiece mountPath,
AbsolutePathPiece clientDirectory) {
// Extract repository name from the client config file
auto configPath = clientDirectory + kCheckoutConfig;
auto configRoot = cpptoml::parse_file(configPath.c_str());
// Construct CheckoutConfig object
auto config = std::make_unique<CheckoutConfig>(mountPath, clientDirectory);
// Load repository information
auto repository = configRoot->get_table(kRepoSection.str());
config->repoType_ = *repository->get_as<std::string>(kRepoTypeKey.str());
config->repoSource_ = *repository->get_as<std::string>(kRepoSourceKey.str());
// Read optional case-sensitivity.
auto caseSensitive = repository->get_as<bool>(kRepoCaseSensitiveKey.str());
config->caseSensitive_ =
caseSensitive ? *caseSensitive : kPathMapDefaultCaseSensitive;
#ifdef _WIN32
auto guid = repository->get_as<std::string>(kRepoGuid.str());
config->repoGuid_ = guid ? Guid{*guid} : Guid::generate();
#endif
return config;
}
folly::dynamic CheckoutConfig::loadClientDirectoryMap(
AbsolutePathPiece edenDir) {
// Extract the JSON and strip any comments.
auto configJsonFile = edenDir + kClientDirectoryMap;
auto jsonContents = readFile(configJsonFile).value();
auto jsonWithoutComments = folly::json::stripComments(jsonContents);
if (jsonWithoutComments.empty()) {
return folly::dynamic::object();
}
// Parse the comment-free JSON while tolerating trailing commas.
folly::json::serialization_opts options;
options.allow_trailing_comma = true;
return folly::parseJson(jsonWithoutComments, options);
}
} // namespace eden
} // namespace facebook