98 lines
3 KiB
C++
98 lines
3 KiB
C++
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// One writer per file: a second writer in another process or in this one
|
||
|
|
// gets LockedError, readers are refused while a writer holds the file,
|
||
|
|
// readers coexist with each other, and a read-only table refuses writes.
|
||
|
|
|
||
|
|
#include <sys/wait.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
|
||
|
|
import Crafter.DB;
|
||
|
|
import std;
|
||
|
|
|
||
|
|
using namespace Crafter;
|
||
|
|
namespace fs = std::filesystem;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
int failures = 0;
|
||
|
|
|
||
|
|
void Check(bool condition, std::string_view what) {
|
||
|
|
if (!condition) {
|
||
|
|
std::println("FAIL: {}", what);
|
||
|
|
++failures;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
struct Item {
|
||
|
|
std::string name;
|
||
|
|
static constexpr std::uint32_t version = 1;
|
||
|
|
};
|
||
|
|
|
||
|
|
// Returns the child's exit code: 0 = opened fine, 3 = LockedError, 4 = other.
|
||
|
|
int ChildOpens(const fs::path& path, bool readOnly) {
|
||
|
|
const pid_t pid = fork();
|
||
|
|
if (pid == 0) {
|
||
|
|
try {
|
||
|
|
DB::Table<Item> table(path, { .mode = DB::OpenMode::OpenExisting, .readOnly = readOnly });
|
||
|
|
_exit(0);
|
||
|
|
} catch (const DB::LockedError&) {
|
||
|
|
_exit(3);
|
||
|
|
} catch (...) {
|
||
|
|
_exit(4);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
int status = 0;
|
||
|
|
waitpid(pid, &status, 0);
|
||
|
|
return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
const fs::path dir = fs::temp_directory_path() / "crafter-db-lock";
|
||
|
|
fs::remove_all(dir);
|
||
|
|
const fs::path path = dir / "items.cdb";
|
||
|
|
|
||
|
|
{
|
||
|
|
DB::Table<Item> writer(path);
|
||
|
|
writer.Append({ "one" });
|
||
|
|
Check(ChildOpens(path, false) == 3, "second writer in another process gets LockedError");
|
||
|
|
Check(ChildOpens(path, true) == 3, "reader in another process is refused while a writer holds the file");
|
||
|
|
|
||
|
|
bool threw = false;
|
||
|
|
try {
|
||
|
|
DB::Table<Item> second(path);
|
||
|
|
} catch (const DB::LockedError&) {
|
||
|
|
threw = true;
|
||
|
|
}
|
||
|
|
Check(threw, "second Table on the same file in this process gets LockedError");
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
DB::Table<Item> reader(path, { .readOnly = true });
|
||
|
|
Check(ChildOpens(path, true) == 0, "readers coexist across processes");
|
||
|
|
Check(ChildOpens(path, false) == 3, "writer is refused while a reader holds the file");
|
||
|
|
|
||
|
|
bool threw = false;
|
||
|
|
try {
|
||
|
|
reader.Append({ "two" });
|
||
|
|
} catch (const DB::ReadOnlyError&) {
|
||
|
|
threw = true;
|
||
|
|
}
|
||
|
|
Check(threw, "Append on a read-only table throws ReadOnlyError");
|
||
|
|
threw = false;
|
||
|
|
try {
|
||
|
|
reader.Erase(0);
|
||
|
|
} catch (const DB::ReadOnlyError&) {
|
||
|
|
threw = true;
|
||
|
|
}
|
||
|
|
Check(threw, "Erase on a read-only table throws ReadOnlyError");
|
||
|
|
Check(reader.Size() == 1 && reader[0][&Item::name] == "one", "read-only table reads");
|
||
|
|
}
|
||
|
|
|
||
|
|
Check(ChildOpens(path, false) == 0, "writer opens once everyone is gone");
|
||
|
|
|
||
|
|
fs::remove_all(dir);
|
||
|
|
if (failures == 0) std::println("ok");
|
||
|
|
return failures == 0 ? 0 : 1;
|
||
|
|
}
|