229 lines
7.0 KiB
C++
229 lines
7.0 KiB
C++
#include "ConflictSet.h"
|
|
|
|
#include <cassert>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
namespace {
|
|
// A node in the tree representing write conflict history. This tree maintains
|
|
// several invariants:
|
|
|
|
// 1. BST invariant: all keys in the tree rooted at the left child of a node
|
|
// compare less than that node's key, and all keys in the tree rooted at the
|
|
// right child of a node compare greater than that node's key.
|
|
// 2. Heap invariant: the priority of a node is greater than all the priorities
|
|
// of its children (transitively)
|
|
// 3. Max invariant: `maxVersion` is the max among all values of `pointVersion`
|
|
// and `beyondVersion` for this node and its children (transitively)
|
|
// 4. The lowest key (an empty byte sequence) is always physically present in
|
|
// the tree so that "last less than or equal" queries are always well-defined.
|
|
|
|
// Logically, the contents of the tree represent a "range map" where all of the
|
|
// infinitely many points in the key space are associated with a writeVersion.
|
|
// If a point is physically present in the tree, then its writeVersion is its
|
|
// node's `pointVersion`. Otherwise, its writeVersion is the `rangeVersion` of
|
|
// the node with the last key less than point.
|
|
struct Node {
|
|
// See "Max invariant" above
|
|
int64_t maxVersion;
|
|
// The write version of the point in the key space represented by this node's
|
|
// key
|
|
int64_t pointVersion;
|
|
// The write version of the range immediately after this node's key, until
|
|
// just before the next key in the tree. I.e. (this key, next key)
|
|
int64_t rangeVersion;
|
|
// child[0] is the left child or nullptr. child[1] is the right child or
|
|
// nullptr
|
|
Node *child[2];
|
|
// The parent of this node in the tree, or nullptr if this node is the root
|
|
Node *parent;
|
|
// As a treap, this tree satisfies the heap invariant on each node's priority
|
|
uint32_t priority;
|
|
// The length of this node's key
|
|
int len;
|
|
// The contents of this node's key
|
|
// uint8_t[len];
|
|
|
|
auto operator<=>(const Node &other) const {
|
|
const int minLen = std::min(len, other.len);
|
|
const int c = memcmp(this + 1, &other + 1, minLen);
|
|
return c != 0 ? c <=> 0 : len <=> other.len;
|
|
}
|
|
auto operator<=>(std::string_view other) const {
|
|
const int minLen = std::min<int>(len, other.size());
|
|
const int c = memcmp(this + 1, other.data(), minLen);
|
|
return c != 0 ? c <=> 0 : len <=> int(other.size());
|
|
}
|
|
};
|
|
|
|
// TODO: use a better prng. This is technically vulnerable to a
|
|
// denial-of-service attack that can make conflict-checking linear in the
|
|
// number of nodes in the tree.
|
|
thread_local uint32_t gSeed = 1013904223L;
|
|
uint32_t fastRand() {
|
|
auto result = gSeed;
|
|
gSeed = gSeed * 1664525L + 1013904223L;
|
|
return result;
|
|
}
|
|
|
|
// Note: `rangeVersion` is left uninitialized.
|
|
Node *createNode(std::string_view key, Node *parent, int64_t pointVersion) {
|
|
assert(key.size() <= std::numeric_limits<int>::max());
|
|
Node *result = (Node *)malloc(sizeof(Node) + key.size());
|
|
result->maxVersion = pointVersion;
|
|
result->pointVersion = pointVersion;
|
|
result->child[0] = nullptr;
|
|
result->child[1] = nullptr;
|
|
result->parent = parent;
|
|
result->priority = fastRand();
|
|
result->len = key.size();
|
|
memcpy(result + 1, key.data(), key.size());
|
|
return result;
|
|
}
|
|
|
|
void destroyNode(Node *node) {
|
|
assert(node->child[0] == nullptr);
|
|
assert(node->child[1] == nullptr);
|
|
free(node);
|
|
}
|
|
|
|
// Return a pointer to the node whose key immediately follows `n`'s key (if
|
|
// `dir` is false, precedes). Return nullptr if none exists.
|
|
[[maybe_unused]] Node *next(Node *n, bool dir) {
|
|
// Traverse left spine of right child (when moving right, i.e. dir = true)
|
|
if (n->child[dir]) {
|
|
n = n->child[dir];
|
|
while (n->child[!dir]) {
|
|
n = n->child[!dir];
|
|
}
|
|
} else {
|
|
// Search upward for a node such that we're the left child (when moving
|
|
// right, i.e. dir = true)
|
|
while (n->parent && n == n->parent->child[dir]) {
|
|
n = n->parent;
|
|
}
|
|
n = n->parent;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// Return a pointer to the node whose key is greatest among keys in the tree
|
|
// rooted at `n` (if dir = false, least). Return nullptr if none exists (i.e.
|
|
// `n` is null).
|
|
[[maybe_unused]] Node *extrema(Node *n, bool dir) {
|
|
if (n == nullptr) {
|
|
return nullptr;
|
|
}
|
|
while (n->child[dir] != nullptr) {
|
|
n = n->child[dir];
|
|
}
|
|
return n;
|
|
}
|
|
|
|
[[maybe_unused]] void debugPrintDot(FILE *file, Node *node) {
|
|
|
|
struct DebugDotPrinter {
|
|
|
|
explicit DebugDotPrinter(FILE *file) : file(file) {}
|
|
|
|
void print(Node *node) {
|
|
for (int i = 0; i < 2; ++i) {
|
|
if (node->child[0] != nullptr) {
|
|
fprintf(file, " _%.*s -> _%.*s;\n", node->len,
|
|
(const char *)(node + 1), node->child[0]->len,
|
|
(const char *)(node->child[0] + 1));
|
|
print(node->child[0]);
|
|
} else {
|
|
fprintf(file, " _%.*s -> null%d;\n", node->len,
|
|
(const char *)(node + 1), id);
|
|
fprintf(file, " null%d [shape=point];\n", id);
|
|
++id;
|
|
}
|
|
}
|
|
}
|
|
int id = 0;
|
|
FILE *file;
|
|
};
|
|
|
|
fprintf(file, "digraph TreeSet {\n");
|
|
fprintf(file, " node [fontname=\"Scientifica\"];\n");
|
|
for (auto iter = extrema(node, false); iter != nullptr;
|
|
iter = next(iter, true)) {
|
|
fprintf(file, " _%.*s;\n", node->len, (const char *)(node + 1));
|
|
}
|
|
if (node != nullptr) {
|
|
DebugDotPrinter{file}.print(node);
|
|
}
|
|
fprintf(file, "}\n");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct ConflictSet::Impl {
|
|
Node *root;
|
|
int64_t oldestVersion;
|
|
explicit Impl(int64_t oldestVersion) noexcept
|
|
: root(createNode("", nullptr, oldestVersion)),
|
|
oldestVersion(oldestVersion) {
|
|
root->rangeVersion = oldestVersion;
|
|
}
|
|
void check(const ReadRange *reads, Result *results, int count) const {}
|
|
|
|
void addWrites(const WriteRange *writes, int count) {}
|
|
|
|
void setOldestVersion(int64_t oldestVersion) {
|
|
assert(oldestVersion > this->oldestVersion);
|
|
this->oldestVersion = oldestVersion;
|
|
}
|
|
|
|
~Impl() {
|
|
std::vector<Node *> toFree;
|
|
if (root != nullptr) {
|
|
toFree.push_back(root);
|
|
}
|
|
while (toFree.size() > 0) {
|
|
Node *n = toFree.back();
|
|
toFree.pop_back();
|
|
for (int i = 0; i < 2; ++i) {
|
|
auto *c = std::exchange(n->child[i], nullptr);
|
|
if (c != nullptr) {
|
|
toFree.push_back(c);
|
|
}
|
|
}
|
|
destroyNode(n);
|
|
}
|
|
}
|
|
};
|
|
|
|
void ConflictSet::check(const ReadRange *reads, Result *results,
|
|
int count) const {
|
|
return impl->check(reads, results, count);
|
|
}
|
|
|
|
void ConflictSet::addWrites(const WriteRange *writes, int count) {
|
|
return impl->addWrites(writes, count);
|
|
}
|
|
|
|
void ConflictSet::setOldestVersion(int64_t oldestVersion) {
|
|
return impl->setOldestVersion(oldestVersion);
|
|
}
|
|
|
|
ConflictSet::ConflictSet(int64_t oldestVersion)
|
|
: impl(new Impl{oldestVersion}) {}
|
|
|
|
ConflictSet::~ConflictSet() { delete impl; }
|
|
|
|
ConflictSet::ConflictSet(ConflictSet &&other) noexcept
|
|
: impl(std::exchange(other.impl, nullptr)) {}
|
|
|
|
ConflictSet &ConflictSet::operator=(ConflictSet &&other) noexcept {
|
|
impl = std::exchange(other.impl, nullptr);
|
|
return *this;
|
|
}
|
|
|
|
#ifdef ENABLE_TESTS
|
|
int main(void) {
|
|
ConflictSet::Impl cs{0};
|
|
debugPrintDot(stdout, cs.root);
|
|
}
|
|
#endif |