sapling/eden/fs/utils/WinError.cpp
Xavier Deguillard 2bfdf6f481 prjfs: handle concurrent file/directory removal
Summary:
A while back, we saw that concurrent directory creation would lead to EdenFS
being confused and failing to record some of the created directories. This then
caused EdenFS to no longer being in sync with what was on disk. To handle this
case, we've had to manually creating these directories recursively.

What I didn't realize at the time was that these concurrent notifications could
also happen on removal this time, and if a directory removal notification wins
the race against the removal of its last children, that directory wouldn't be
removed and EdenFS would once again be confused about the state of the
repository.

Fixing this is a bit trickier than directory creation as it's more racier.
Consider a directory that is being removed, and then immediately recreated with
a file in it in a different process. The naive approach of simply force
removing all of the children of a directory when handling the removal
notification would clash with the file creation. We could argue that nobody
should be doing this, but there would be an unhandled race, and thus a bug
where data would potentially be lost[0].

We can however fix this bug slightly differently. For file/directory removal,
we can actually hook onto the pre-callback, ie: one that happens before the
file/directory is no longer visible on disk. This inherently eliminate the race
altogether as the callback will be guaranteed to run when none of its children
are present, and if a race happens with a file creation in it, we can simply
fail the removal properly.

The only tricky bit is for the renaming logic, as renaming a file is logically
a removal followed by a creation. For that reason, I've moved part of the
renaming bits to the pre-callback too.

In theory, this change may negatively affect workloads that do concurrent
directory removal as the duration during which a file/directory is visible
ondisk now includes the EdenFS callback while it didn't before. Such workflows
should be fairly rare and/or redirected to avoid EdenFS altogether if
performance matters.

[0]: This left-over file that EdenFS wouldn't be aware of would also later
cause the checkout code to fail due to invalidation failures triggered when
trying to invalidate that directory. This would be fairly hard to debug.

Reviewed By: fanzeyi

Differential Revision: D25112381

fbshipit-source-id: 9300499ce872ad93d0a687f0e61b7e2a9caf9556
2020-12-04 14:25:44 -08:00

103 lines
2.9 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.
*/
#ifdef _WIN32
#include "eden/fs/utils/WinError.h"
#include <iostream>
#include <memory>
#include <sstream>
namespace facebook {
namespace eden {
std::string win32ErrorToString(uint32_t error) {
struct LocalDeleter {
void operator()(HLOCAL p) noexcept {
::LocalFree(p);
}
};
LPSTR messageBufferRaw = nullptr;
// By default, FormatMessageA will terminate the string with "\r\n",
// and the mis-named (and mis-documented) FORMAT_MESSAGE_MAX_WIDTH_MASK flag
// will remove these (but add a whitespace instead).
size_t size = FormatMessageA(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK,
nullptr,
error,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPSTR)&messageBufferRaw,
0,
nullptr);
// Get a unique_ptr to the raw buffer, so it's released in case of an
// exception.
std::unique_ptr<char, LocalDeleter> messageBuffer{messageBufferRaw};
std::stringstream stream;
if ((size > 0) && (messageBuffer)) {
stream << "Error (0x" << std::hex << error << ") " << messageBuffer.get();
} else {
stream << "Error (0x" << std::hex << error << ") Unknown Error";
}
return stream.str();
}
const char* Win32ErrorCategory::name() const noexcept {
return "Win32 Error";
}
std::string Win32ErrorCategory::message(int error) const {
return win32ErrorToString(error);
}
const std::error_category& Win32ErrorCategory::get() noexcept {
static class Win32ErrorCategory cat;
return cat;
}
const char* HResultErrorCategory::name() const noexcept {
return "HRESULT Error";
}
std::string HResultErrorCategory::message(int error) const {
return win32ErrorToString(error);
}
const std::error_category& HResultErrorCategory::get() noexcept {
static class HResultErrorCategory cat;
return cat;
}
HRESULT exceptionToHResult(const std::exception& ex) noexcept {
if (auto e = dynamic_cast<const std::system_error*>(&ex)) {
auto code = e->code();
XLOG(ERR) << e->what() << ": " << code;
if (code.category() == HResultErrorCategory::get()) {
return code.value();
}
if (code.category() == Win32ErrorCategory::get()) {
return HRESULT_FROM_WIN32(code.value());
}
if (code.category() == std::generic_category()) {
switch (code.value()) {
case ENOTEMPTY:
return HRESULT_FROM_WIN32(ERROR_DIR_NOT_EMPTY);
}
}
return HRESULT_FROM_WIN32(ERROR_ERRORS_ENCOUNTERED);
} else if (auto e = dynamic_cast<const std::bad_alloc*>(&ex)) {
return E_OUTOFMEMORY;
} else {
XLOG(ERR) << ex.what();
return HRESULT_FROM_WIN32(ERROR_ERRORS_ENCOUNTERED);
}
}
} // namespace eden
} // namespace facebook
#endif