Compare commits
23 Commits
v0.0.6
...
cce7d29410
Author | SHA1 | Date | |
---|---|---|---|
cce7d29410 | |||
13f8d3fa8a | |||
02866a8cae | |||
fa86d3e707 | |||
7d1d1d7b2a | |||
789ecc29b3 | |||
08f2998a85 | |||
c882d7663d | |||
bfea4384ba | |||
6520e3d734 | |||
23ace8aac5 | |||
62e35de320 | |||
22e4ab01a1 | |||
b3aeed0caa | |||
5f3833e965 | |||
8b1cd9c052 | |||
bb9bc3d7b5 | |||
89b3354a80 | |||
488c723726 | |||
76d0785b33 | |||
add0af11ad | |||
2c0adf4a8b | |||
e8ac78cce6 |
105
Bench.cpp
105
Bench.cpp
@@ -258,4 +258,107 @@ void benchConflictSet() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) { benchConflictSet(); }
|
constexpr int kKeyLenForWorstCase = 50;
|
||||||
|
|
||||||
|
ConflictSet worstCaseConflictSetForRadixRangeRead(int cardinality) {
|
||||||
|
ConflictSet cs{0};
|
||||||
|
|
||||||
|
for (int i = 0; i < kKeyLenForWorstCase; ++i) {
|
||||||
|
for (int j = 0; j < cardinality; ++j) {
|
||||||
|
auto b = std::vector<uint8_t>(i, 0);
|
||||||
|
b.push_back(j);
|
||||||
|
auto e = std::vector<uint8_t>(i, 255);
|
||||||
|
e.push_back(255 - j);
|
||||||
|
weaselab::ConflictSet::WriteRange w[] = {{
|
||||||
|
{b.data(), int(b.size())},
|
||||||
|
{nullptr, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{e.data(), int(e.size())},
|
||||||
|
{nullptr, 0},
|
||||||
|
}};
|
||||||
|
std::sort(std::begin(w), std::end(w),
|
||||||
|
[](const auto &lhs, const auto &rhs) {
|
||||||
|
int cl = std::min(lhs.begin.len, rhs.begin.len);
|
||||||
|
if (cl > 0) {
|
||||||
|
int c = memcmp(lhs.begin.p, rhs.begin.p, cl);
|
||||||
|
if (c != 0) {
|
||||||
|
return c < 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lhs.begin.len < rhs.begin.len;
|
||||||
|
});
|
||||||
|
cs.addWrites(w, sizeof(w) / sizeof(w[0]), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defeat short-circuiting on the left
|
||||||
|
{
|
||||||
|
auto k = std::vector<uint8_t>(kKeyLenForWorstCase, 0);
|
||||||
|
weaselab::ConflictSet::WriteRange w[] = {
|
||||||
|
{
|
||||||
|
{k.data(), int(k.size())},
|
||||||
|
{nullptr, 0},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
cs.addWrites(w, sizeof(w) / sizeof(w[0]), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defeat short-circuiting on the right
|
||||||
|
{
|
||||||
|
auto k = std::vector<uint8_t>(kKeyLenForWorstCase, 255);
|
||||||
|
weaselab::ConflictSet::WriteRange w[] = {
|
||||||
|
{
|
||||||
|
{k.data(), int(k.size())},
|
||||||
|
{nullptr, 0},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
cs.addWrites(w, sizeof(w) / sizeof(w[0]), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
void benchWorstCaseForRadixRangeRead() {
|
||||||
|
ankerl::nanobench::Bench bench;
|
||||||
|
|
||||||
|
std::unique_ptr<ConflictSet> cs[256];
|
||||||
|
for (int i = 0; i < 256; ++i) {
|
||||||
|
cs[i] =
|
||||||
|
std::make_unique<ConflictSet>(worstCaseConflictSetForRadixRangeRead(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto begin = std::vector<uint8_t>(kKeyLenForWorstCase - 1, 0);
|
||||||
|
begin.push_back(1);
|
||||||
|
auto end = std::vector<uint8_t>(kKeyLenForWorstCase - 1, 255);
|
||||||
|
end.push_back(254);
|
||||||
|
|
||||||
|
weaselab::ConflictSet::Result result;
|
||||||
|
weaselab::ConflictSet::ReadRange r{
|
||||||
|
{begin.data(), int(begin.size())}, {end.data(), int(end.size())}, 0};
|
||||||
|
|
||||||
|
bench.run("worst case for radix tree", [&]() {
|
||||||
|
for (int i = 0; i < 256; ++i) {
|
||||||
|
result = weaselab::ConflictSet::TooOld;
|
||||||
|
cs[i]->check(&r, &result, 1);
|
||||||
|
if (result != weaselab::ConflictSet::Commit) {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// for (int i = 0; i < 256; ++i) {
|
||||||
|
// bench.run("worst case for radix tree, span " + std::to_string(i), [&]() {
|
||||||
|
// result = weaselab::ConflictSet::TooOld;
|
||||||
|
// cs[i]->check(&r, &result, 1);
|
||||||
|
// if (result != weaselab::ConflictSet::Commit) {
|
||||||
|
// abort();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
benchConflictSet();
|
||||||
|
benchWorstCaseForRadixRangeRead();
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.18)
|
cmake_minimum_required(VERSION 3.18)
|
||||||
project(
|
project(
|
||||||
conflict-set
|
conflict-set
|
||||||
VERSION 0.0.6
|
VERSION 0.0.7
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
"A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
|
"A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
|
||||||
HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
|
HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
|
||||||
|
254
ConflictSet.cpp
254
ConflictSet.cpp
@@ -294,6 +294,12 @@ struct Node48 : Node {
|
|||||||
int8_t nextFree;
|
int8_t nextFree;
|
||||||
int8_t index[256];
|
int8_t index[256];
|
||||||
Child children[kMaxNodes];
|
Child children[kMaxNodes];
|
||||||
|
uint8_t reverseIndex[kMaxNodes];
|
||||||
|
constexpr static int kMaxOfMaxPageSize = 8;
|
||||||
|
constexpr static int kMaxOfMaxShift =
|
||||||
|
std::countr_zero(uint32_t(kMaxOfMaxPageSize));
|
||||||
|
constexpr static int kMaxOfMaxTotalPages = kMaxNodes / kMaxOfMaxPageSize;
|
||||||
|
int64_t maxOfMax[kMaxOfMaxTotalPages];
|
||||||
|
|
||||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||||
|
|
||||||
@@ -308,6 +314,11 @@ struct Node256 : Node {
|
|||||||
constexpr static auto kType = Type_Node256;
|
constexpr static auto kType = Type_Node256;
|
||||||
BitSet bitSet;
|
BitSet bitSet;
|
||||||
Child children[256];
|
Child children[256];
|
||||||
|
constexpr static int kMaxOfMaxPageSize = 8;
|
||||||
|
constexpr static int kMaxOfMaxShift =
|
||||||
|
std::countr_zero(uint32_t(kMaxOfMaxPageSize));
|
||||||
|
constexpr static int kMaxOfMaxTotalPages = 256 / kMaxOfMaxPageSize;
|
||||||
|
int64_t maxOfMax[kMaxOfMaxTotalPages];
|
||||||
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
uint8_t *partialKey() { return (uint8_t *)(this + 1); }
|
||||||
void copyChildrenAndKeyFrom(const Node48 &other);
|
void copyChildrenAndKeyFrom(const Node48 &other);
|
||||||
void copyChildrenAndKeyFrom(const Node256 &other);
|
void copyChildrenAndKeyFrom(const Node256 &other);
|
||||||
@@ -405,6 +416,7 @@ inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
|||||||
kNodeCopySize);
|
kNodeCopySize);
|
||||||
assert(numChildren == Node16::kMaxNodes);
|
assert(numChildren == Node16::kMaxNodes);
|
||||||
memset(index, -1, sizeof(index));
|
memset(index, -1, sizeof(index));
|
||||||
|
memset(children, 0, sizeof(children));
|
||||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||||
bitSet.init();
|
bitSet.init();
|
||||||
nextFree = Node16::kMaxNodes;
|
nextFree = Node16::kMaxNodes;
|
||||||
@@ -415,6 +427,9 @@ inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
|||||||
children[i] = other.children[i];
|
children[i] = other.children[i];
|
||||||
assert(children[i].child->parent == &other);
|
assert(children[i].child->parent == &other);
|
||||||
children[i].child->parent = this;
|
children[i].child->parent = this;
|
||||||
|
reverseIndex[i] = x;
|
||||||
|
maxOfMax[i >> Node48::kMaxOfMaxShift] = std::max(
|
||||||
|
maxOfMax[i >> Node48::kMaxOfMaxShift], children[i].childMaxVersion);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,13 +437,17 @@ inline void Node48::copyChildrenAndKeyFrom(const Node16 &other) {
|
|||||||
inline void Node48::copyChildrenAndKeyFrom(const Node48 &other) {
|
inline void Node48::copyChildrenAndKeyFrom(const Node48 &other) {
|
||||||
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
||||||
kNodeCopySize);
|
kNodeCopySize);
|
||||||
memcpy(&bitSet, &other.bitSet,
|
bitSet = other.bitSet;
|
||||||
sizeof(*this) - sizeof(Node) - sizeof(children));
|
nextFree = other.nextFree;
|
||||||
|
memcpy(index, other.index, sizeof(index));
|
||||||
|
memset(children, 0, sizeof(children));
|
||||||
for (int i = 0; i < numChildren; ++i) {
|
for (int i = 0; i < numChildren; ++i) {
|
||||||
children[i] = other.children[i];
|
children[i] = other.children[i];
|
||||||
assert(children[i].child->parent == &other);
|
assert(children[i].child->parent == &other);
|
||||||
children[i].child->parent = this;
|
children[i].child->parent = this;
|
||||||
}
|
}
|
||||||
|
memcpy(reverseIndex, other.reverseIndex, sizeof(reverseIndex));
|
||||||
|
memcpy(maxOfMax, other.maxOfMax, sizeof(maxOfMax));
|
||||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +455,7 @@ inline void Node48::copyChildrenAndKeyFrom(const Node256 &other) {
|
|||||||
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
||||||
kNodeCopySize);
|
kNodeCopySize);
|
||||||
memset(index, -1, sizeof(index));
|
memset(index, -1, sizeof(index));
|
||||||
|
memset(children, 0, sizeof(children));
|
||||||
nextFree = other.numChildren;
|
nextFree = other.numChildren;
|
||||||
bitSet = other.bitSet;
|
bitSet = other.bitSet;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
@@ -448,6 +468,9 @@ inline void Node48::copyChildrenAndKeyFrom(const Node256 &other) {
|
|||||||
children[i] = other.children[c];
|
children[i] = other.children[c];
|
||||||
assert(children[i].child->parent == &other);
|
assert(children[i].child->parent == &other);
|
||||||
children[i].child->parent = this;
|
children[i].child->parent = this;
|
||||||
|
reverseIndex[i] = c;
|
||||||
|
maxOfMax[i >> Node48::kMaxOfMaxShift] = std::max(
|
||||||
|
maxOfMax[i >> Node48::kMaxOfMaxShift], children[i].childMaxVersion);
|
||||||
++i;
|
++i;
|
||||||
},
|
},
|
||||||
0, 256);
|
0, 256);
|
||||||
@@ -457,13 +480,17 @@ inline void Node48::copyChildrenAndKeyFrom(const Node256 &other) {
|
|||||||
inline void Node256::copyChildrenAndKeyFrom(const Node48 &other) {
|
inline void Node256::copyChildrenAndKeyFrom(const Node48 &other) {
|
||||||
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
memcpy((char *)this + kNodeCopyBegin, (char *)&other + kNodeCopyBegin,
|
||||||
kNodeCopySize);
|
kNodeCopySize);
|
||||||
memset(children, 0, sizeof(children));
|
|
||||||
bitSet = other.bitSet;
|
bitSet = other.bitSet;
|
||||||
|
memset(children, 0, sizeof(children));
|
||||||
|
memset(maxOfMax, 0, sizeof(maxOfMax));
|
||||||
bitSet.forEachInRange(
|
bitSet.forEachInRange(
|
||||||
[&](int c) {
|
[&](int c) {
|
||||||
children[c] = other.children[other.index[c]];
|
children[c] = other.children[other.index[c]];
|
||||||
assert(children[c].child->parent == &other);
|
assert(children[c].child->parent == &other);
|
||||||
children[c].child->parent = this;
|
children[c].child->parent = this;
|
||||||
|
maxOfMax[c >> Node256::kMaxOfMaxShift] =
|
||||||
|
std::max(maxOfMax[c >> Node256::kMaxOfMaxShift],
|
||||||
|
children[c].childMaxVersion);
|
||||||
},
|
},
|
||||||
0, 256);
|
0, 256);
|
||||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||||
@@ -481,6 +508,7 @@ inline void Node256::copyChildrenAndKeyFrom(const Node256 &other) {
|
|||||||
children[c].child->parent = this;
|
children[c].child->parent = this;
|
||||||
},
|
},
|
||||||
0, 256);
|
0, 256);
|
||||||
|
memcpy(maxOfMax, other.maxOfMax, sizeof(maxOfMax));
|
||||||
memcpy(partialKey(), &other + 1, partialKeyLen);
|
memcpy(partialKey(), &other + 1, partialKeyLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +560,7 @@ template <class T> struct BoundedFreeListAllocator {
|
|||||||
static_assert(std::derived_from<T, Node>);
|
static_assert(std::derived_from<T, Node>);
|
||||||
static_assert(std::is_trivial_v<T>);
|
static_assert(std::is_trivial_v<T>);
|
||||||
|
|
||||||
T *allocate(int partialKeyCapacity) {
|
T *allocate_helper(int partialKeyCapacity) {
|
||||||
if (freeList != nullptr) {
|
if (freeList != nullptr) {
|
||||||
T *n = (T *)freeList;
|
T *n = (T *)freeList;
|
||||||
VALGRIND_MAKE_MEM_DEFINED(freeList, sizeof(freeList));
|
VALGRIND_MAKE_MEM_DEFINED(freeList, sizeof(freeList));
|
||||||
@@ -560,6 +588,17 @@ template <class T> struct BoundedFreeListAllocator {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
T *allocate(int partialKeyCapacity) {
|
||||||
|
T *result = allocate_helper(partialKeyCapacity);
|
||||||
|
if constexpr (!std::is_same_v<T, Node0>) {
|
||||||
|
memset(result->children, 0, sizeof(result->children));
|
||||||
|
}
|
||||||
|
if constexpr (std::is_same_v<T, Node48> || std::is_same_v<T, Node256>) {
|
||||||
|
memset(result->maxOfMax, 0, sizeof(result->maxOfMax));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void release(T *p) {
|
void release(T *p) {
|
||||||
if (freeListBytes >= kFreeListMaxMemory) {
|
if (freeListBytes >= kFreeListMaxMemory) {
|
||||||
removeNode(p);
|
removeNode(p);
|
||||||
@@ -732,8 +771,9 @@ Node *&getChildExists(Node *self, uint8_t index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Precondition - an entry for index must exist in the node
|
int64_t maxVersion(Node *n, ConflictSet::Impl *);
|
||||||
int64_t &maxVersion(Node *n, ConflictSet::Impl *);
|
|
||||||
|
void setMaxVersion(Node *n, ConflictSet::Impl *, int64_t maxVersion);
|
||||||
|
|
||||||
Node *&getInTree(Node *n, ConflictSet::Impl *);
|
Node *&getInTree(Node *n, ConflictSet::Impl *);
|
||||||
|
|
||||||
@@ -1025,6 +1065,7 @@ Node *&getOrCreateChild(Node *&self, uint8_t index,
|
|||||||
assert(self48->nextFree < 48);
|
assert(self48->nextFree < 48);
|
||||||
int nextFree = self48->nextFree++;
|
int nextFree = self48->nextFree++;
|
||||||
self48->index[index] = nextFree;
|
self48->index[index] = nextFree;
|
||||||
|
self48->reverseIndex[nextFree] = index;
|
||||||
auto &result = self48->children[nextFree].child;
|
auto &result = self48->children[nextFree].child;
|
||||||
result = nullptr;
|
result = nullptr;
|
||||||
return result;
|
return result;
|
||||||
@@ -1210,7 +1251,7 @@ void maybeDownsize(Node *self, NodeAllocators *allocators,
|
|||||||
|
|
||||||
// Max versions are stored in the parent, so we need to update it now
|
// Max versions are stored in the parent, so we need to update it now
|
||||||
// that we have a new parent.
|
// that we have a new parent.
|
||||||
maxVersion(child, impl) = childMaxVersion;
|
setMaxVersion(child, impl, childMaxVersion);
|
||||||
|
|
||||||
getInTree(self, impl) = child;
|
getInTree(self, impl) = child;
|
||||||
allocators->node3.release(self3);
|
allocators->node3.release(self3);
|
||||||
@@ -1324,9 +1365,12 @@ Node *erase(Node *self, NodeAllocators *allocators, ConflictSet::Impl *impl,
|
|||||||
if (toRemoveChildrenIndex != lastChildrenIndex) {
|
if (toRemoveChildrenIndex != lastChildrenIndex) {
|
||||||
parent48->children[toRemoveChildrenIndex] =
|
parent48->children[toRemoveChildrenIndex] =
|
||||||
parent48->children[lastChildrenIndex];
|
parent48->children[lastChildrenIndex];
|
||||||
parent48->index[parent48->children[toRemoveChildrenIndex]
|
auto parentIndex =
|
||||||
.child->parentsIndex] = toRemoveChildrenIndex;
|
parent48->children[toRemoveChildrenIndex].child->parentsIndex;
|
||||||
|
parent48->index[parentIndex] = toRemoveChildrenIndex;
|
||||||
|
parent48->reverseIndex[toRemoveChildrenIndex] = parentIndex;
|
||||||
}
|
}
|
||||||
|
parent48->children[lastChildrenIndex].childMaxVersion = 0;
|
||||||
|
|
||||||
--parent->numChildren;
|
--parent->numChildren;
|
||||||
|
|
||||||
@@ -1621,75 +1665,113 @@ downLeftSpine:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the max version among all keys starting with the search path of n +
|
// Return whether or not the max version among all keys starting with the search
|
||||||
// [child], where child in (begin, end). Does not account for the range version
|
// path of n + [child], where child in (begin, end) is <= readVersion. Does not
|
||||||
// of firstGt(searchpath(n) + [end - 1])
|
// account for the range version of firstGt(searchpath(n) + [end - 1])
|
||||||
int64_t maxBetweenExclusive(Node *n, int begin, int end) {
|
bool checkMaxBetweenExclusive(Node *n, int begin, int end,
|
||||||
|
int64_t readVersion) {
|
||||||
assume(-1 <= begin);
|
assume(-1 <= begin);
|
||||||
assume(begin <= 256);
|
assume(begin <= 256);
|
||||||
assume(-1 <= end);
|
assume(-1 <= end);
|
||||||
assume(end <= 256);
|
assume(end <= 256);
|
||||||
assume(begin < end);
|
assume(begin < end);
|
||||||
int64_t result = std::numeric_limits<int64_t>::lowest();
|
|
||||||
{
|
{
|
||||||
int c = getChildGeq(n, begin + 1);
|
int c = getChildGeq(n, begin + 1);
|
||||||
if (c >= 0 && c < end) {
|
if (c >= 0 && c < end) {
|
||||||
auto *child = getChildExists(n, c);
|
auto *child = getChildExists(n, c);
|
||||||
if (child->entryPresent) {
|
if (child->entryPresent) {
|
||||||
result = std::max(result, child->entry.rangeVersion);
|
if (!(child->entry.rangeVersion <= readVersion)) {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
begin = c;
|
begin = c;
|
||||||
} else {
|
} else {
|
||||||
return result;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [begin, end) is now the half-open interval of children we're interested in.
|
||||||
|
|
||||||
|
const unsigned shiftUpperBound = end - begin;
|
||||||
|
const unsigned shiftAmount = begin;
|
||||||
|
auto inBounds = [&](unsigned c) { return c - shiftAmount < shiftUpperBound; };
|
||||||
|
|
||||||
switch (n->getType()) {
|
switch (n->getType()) {
|
||||||
case Type_Node0: // GCOVR_EXCL_LINE
|
case Type_Node0: // GCOVR_EXCL_LINE
|
||||||
// We would have returned above, after not finding a child
|
// We would have returned above, after not finding a child
|
||||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||||
case Type_Node3: {
|
case Type_Node3: {
|
||||||
auto *self = static_cast<Node3 *>(n);
|
auto *self = static_cast<Node3 *>(n);
|
||||||
for (int i = 0; i < self->numChildren && self->index[i] < end; ++i) {
|
bool result = true;
|
||||||
if (begin <= self->index[i]) {
|
for (int i = 0; i < 3; ++i) {
|
||||||
result = std::max(result, self->children[i].childMaxVersion);
|
result &= !((self->children[i].childMaxVersion > readVersion) &
|
||||||
}
|
inBounds(self->index[i]));
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
} break;
|
} break;
|
||||||
case Type_Node16: {
|
case Type_Node16: {
|
||||||
auto *self = static_cast<Node16 *>(n);
|
auto *self = static_cast<Node16 *>(n);
|
||||||
for (int i = 0; i < self->numChildren && self->index[i] < end; ++i) {
|
bool result = true;
|
||||||
if (begin <= self->index[i]) {
|
for (int i = 0; i < 16; ++i) {
|
||||||
result = std::max(result, self->children[i].childMaxVersion);
|
result &= !((self->children[i].childMaxVersion > readVersion) &
|
||||||
}
|
inBounds(self->index[i]));
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
} break;
|
} break;
|
||||||
case Type_Node48: {
|
case Type_Node48: {
|
||||||
auto *self = static_cast<Node48 *>(n);
|
auto *self = static_cast<Node48 *>(n);
|
||||||
self->bitSet.forEachInRange(
|
// Check all pages
|
||||||
[&](int i) {
|
bool result = true;
|
||||||
result =
|
for (int i = 0; i < Node48::kMaxOfMaxTotalPages; ++i) {
|
||||||
std::max(result, self->children[self->index[i]].childMaxVersion);
|
if (self->maxOfMax[i] > readVersion) {
|
||||||
},
|
for (int j = 0; j < Node48::kMaxOfMaxPageSize; ++j) {
|
||||||
begin, end);
|
int k = (i << Node48::kMaxOfMaxShift) + j;
|
||||||
break;
|
result &= !(self->children[k].childMaxVersion > readVersion &&
|
||||||
|
inBounds(self->reverseIndex[k]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
case Type_Node256: {
|
case Type_Node256: {
|
||||||
auto *self = static_cast<Node256 *>(n);
|
auto *self = static_cast<Node256 *>(n);
|
||||||
self->bitSet.forEachInRange(
|
// Check the first page
|
||||||
[&](int i) {
|
if (self->maxOfMax[begin >> Node256::kMaxOfMaxShift] > readVersion) {
|
||||||
result = std::max(result, self->children[i].childMaxVersion);
|
bool result = true;
|
||||||
},
|
for (int i = 0; i < Node256::kMaxOfMaxPageSize; ++i) {
|
||||||
begin, end);
|
int j = (begin & ~(Node256::kMaxOfMaxPageSize - 1)) + i;
|
||||||
break;
|
result &=
|
||||||
|
!((self->children[j].childMaxVersion > readVersion) & inBounds(j));
|
||||||
|
}
|
||||||
|
if (!result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check the last page
|
||||||
|
if (end >= 1 &&
|
||||||
|
self->maxOfMax[(end - 1) >> Node256::kMaxOfMaxShift] > readVersion) {
|
||||||
|
bool result = true;
|
||||||
|
for (int i = 0; i < Node256::kMaxOfMaxPageSize; ++i) {
|
||||||
|
int j = ((end - 1) & ~(Node256::kMaxOfMaxPageSize - 1)) + i;
|
||||||
|
result &=
|
||||||
|
!((self->children[j].childMaxVersion > readVersion) & inBounds(j));
|
||||||
|
}
|
||||||
|
if (!result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check inner pages
|
||||||
|
bool result = true;
|
||||||
|
for (int i = (begin >> Node256::kMaxOfMaxShift) + 1;
|
||||||
|
i < ((end - 1) >> Node256::kMaxOfMaxShift); ++i) {
|
||||||
|
result &= self->maxOfMax[i] <= readVersion;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
default: // GCOVR_EXCL_LINE
|
default: // GCOVR_EXCL_LINE
|
||||||
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||||
}
|
}
|
||||||
#if DEBUG_VERBOSE && !defined(NDEBUG)
|
return true;
|
||||||
fprintf(stderr, "At `%s', max version in (%02x, %02x) is %" PRId64 "\n",
|
|
||||||
getSearchPathPrintable(n).c_str(), begin, end, result);
|
|
||||||
#endif
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<uint8_t> getSearchPath(Arena &arena, Node *n) {
|
Vector<uint8_t> getSearchPath(Arena &arena, Node *n) {
|
||||||
@@ -1722,7 +1804,7 @@ bool checkRangeStartsWith(Node *n, std::span<const uint8_t> key, int begin,
|
|||||||
#endif
|
#endif
|
||||||
auto remaining = key;
|
auto remaining = key;
|
||||||
if (remaining.size() == 0) {
|
if (remaining.size() == 0) {
|
||||||
return maxBetweenExclusive(n, begin, end) <= readVersion;
|
return checkMaxBetweenExclusive(n, begin, end, readVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto *child = getChild(n, remaining[0]);
|
auto *child = getChild(n, remaining[0]);
|
||||||
@@ -1821,7 +1903,7 @@ struct CheckRangeLeftSide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchPathLen >= prefixLen) {
|
if (searchPathLen >= prefixLen) {
|
||||||
if (maxBetweenExclusive(n, remaining[0], 256) > readVersion) {
|
if (!checkMaxBetweenExclusive(n, remaining[0], 256, readVersion)) {
|
||||||
ok = false;
|
ok = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1963,7 +2045,7 @@ struct CheckRangeRightSide {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxBetweenExclusive(n, -1, remaining[0]) > readVersion) {
|
if (!checkMaxBetweenExclusive(n, -1, remaining[0], readVersion)) {
|
||||||
ok = false;
|
ok = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2174,7 +2256,7 @@ template <bool kBegin>
|
|||||||
allocators) = old;
|
allocators) = old;
|
||||||
old->parent = *self;
|
old->parent = *self;
|
||||||
old->parentsIndex = old->partialKey()[partialKeyIndex];
|
old->parentsIndex = old->partialKey()[partialKeyIndex];
|
||||||
maxVersion(old, impl) = oldMaxVersion;
|
setMaxVersion(old, impl, oldMaxVersion);
|
||||||
|
|
||||||
memmove(old->partialKey(), old->partialKey() + partialKeyIndex + 1,
|
memmove(old->partialKey(), old->partialKey() + partialKeyIndex + 1,
|
||||||
old->partialKeyLen - (partialKeyIndex + 1));
|
old->partialKeyLen - (partialKeyIndex + 1));
|
||||||
@@ -2198,9 +2280,8 @@ template <bool kBegin>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (kBegin) {
|
if constexpr (kBegin) {
|
||||||
auto &m = maxVersion(*self, impl);
|
assert(maxVersion(*self, impl) <= writeVersion);
|
||||||
assert(writeVersion >= m);
|
setMaxVersion(*self, impl, writeVersion);
|
||||||
m = writeVersion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.size() == 0) {
|
if (key.size() == 0) {
|
||||||
@@ -2208,9 +2289,8 @@ template <bool kBegin>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if constexpr (!kBegin) {
|
if constexpr (!kBegin) {
|
||||||
auto &m = maxVersion(*self, impl);
|
assert(maxVersion(*self, impl) <= writeVersion);
|
||||||
assert(writeVersion >= m);
|
setMaxVersion(*self, impl, writeVersion);
|
||||||
m = writeVersion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &child = getOrCreateChild(*self, key.front(), allocators);
|
auto &child = getOrCreateChild(*self, key.front(), allocators);
|
||||||
@@ -2221,8 +2301,7 @@ template <bool kBegin>
|
|||||||
child->partialKeyLen = 0;
|
child->partialKeyLen = 0;
|
||||||
child->parent = *self;
|
child->parent = *self;
|
||||||
child->parentsIndex = key.front();
|
child->parentsIndex = key.front();
|
||||||
maxVersion(child, impl) =
|
setMaxVersion(child, impl, kBegin ? writeVersion : 0);
|
||||||
kBegin ? writeVersion : std::numeric_limits<int64_t>::lowest();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self = &child;
|
self = &child;
|
||||||
@@ -2267,7 +2346,7 @@ void addPointWrite(Node *&root, int64_t oldestVersion,
|
|||||||
n->entryPresent = true;
|
n->entryPresent = true;
|
||||||
|
|
||||||
n->entry.pointVersion = writeVersion;
|
n->entry.pointVersion = writeVersion;
|
||||||
maxVersion(n, impl) = writeVersion;
|
setMaxVersion(n, impl, writeVersion);
|
||||||
n->entry.rangeVersion =
|
n->entry.rangeVersion =
|
||||||
p != nullptr ? p->entry.rangeVersion : oldestVersion;
|
p != nullptr ? p->entry.rangeVersion : oldestVersion;
|
||||||
} else {
|
} else {
|
||||||
@@ -2308,9 +2387,8 @@ void addWriteRange(Node *&root, int64_t oldestVersion,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &m = maxVersion(n, impl);
|
assert(maxVersion(n, impl) <= writeVersion);
|
||||||
assert(writeVersion >= m);
|
setMaxVersion(n, impl, writeVersion);
|
||||||
m = writeVersion;
|
|
||||||
|
|
||||||
remaining = remaining.subspan(n->partialKeyLen + 1,
|
remaining = remaining.subspan(n->partialKeyLen + 1,
|
||||||
remaining.size() - (n->partialKeyLen + 1));
|
remaining.size() - (n->partialKeyLen + 1));
|
||||||
@@ -2337,11 +2415,10 @@ void addWriteRange(Node *&root, int64_t oldestVersion,
|
|||||||
beginNode->entry.rangeVersion =
|
beginNode->entry.rangeVersion =
|
||||||
p != nullptr ? p->entry.rangeVersion : oldestVersion;
|
p != nullptr ? p->entry.rangeVersion : oldestVersion;
|
||||||
beginNode->entry.pointVersion = writeVersion;
|
beginNode->entry.pointVersion = writeVersion;
|
||||||
maxVersion(beginNode, impl) = writeVersion;
|
assert(maxVersion(beginNode, impl) <= writeVersion);
|
||||||
|
setMaxVersion(beginNode, impl, writeVersion);
|
||||||
}
|
}
|
||||||
auto &m = maxVersion(beginNode, impl);
|
setMaxVersion(beginNode, impl, writeVersion);
|
||||||
assert(writeVersion >= m);
|
|
||||||
m = writeVersion;
|
|
||||||
assert(writeVersion >= beginNode->entry.pointVersion);
|
assert(writeVersion >= beginNode->entry.pointVersion);
|
||||||
beginNode->entry.pointVersion = writeVersion;
|
beginNode->entry.pointVersion = writeVersion;
|
||||||
|
|
||||||
@@ -2356,8 +2433,8 @@ void addWriteRange(Node *&root, int64_t oldestVersion,
|
|||||||
auto *p = nextLogical(endNode);
|
auto *p = nextLogical(endNode);
|
||||||
endNode->entry.pointVersion =
|
endNode->entry.pointVersion =
|
||||||
p != nullptr ? p->entry.rangeVersion : oldestVersion;
|
p != nullptr ? p->entry.rangeVersion : oldestVersion;
|
||||||
auto &m = maxVersion(endNode, impl);
|
auto m = maxVersion(endNode, impl);
|
||||||
m = std::max(m, endNode->entry.pointVersion);
|
setMaxVersion(endNode, impl, std::max(m, endNode->entry.pointVersion));
|
||||||
}
|
}
|
||||||
endNode->entry.rangeVersion = writeVersion;
|
endNode->entry.rangeVersion = writeVersion;
|
||||||
|
|
||||||
@@ -2557,8 +2634,7 @@ struct __attribute__((visibility("hidden"))) ConflictSet::Impl {
|
|||||||
int64_t totalBytes = 0;
|
int64_t totalBytes = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Precondition - an entry for index must exist in the node
|
int64_t maxVersion(Node *n, ConflictSet::Impl *impl) {
|
||||||
int64_t &maxVersion(Node *n, ConflictSet::Impl *impl) {
|
|
||||||
int index = n->parentsIndex;
|
int index = n->parentsIndex;
|
||||||
n = n->parent;
|
n = n->parent;
|
||||||
if (n == nullptr) {
|
if (n == nullptr) {
|
||||||
@@ -2592,6 +2668,50 @@ int64_t &maxVersion(Node *n, ConflictSet::Impl *impl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setMaxVersion(Node *n, ConflictSet::Impl *impl, int64_t newMax) {
|
||||||
|
int index = n->parentsIndex;
|
||||||
|
n = n->parent;
|
||||||
|
if (n == nullptr) {
|
||||||
|
impl->rootMaxVersion = newMax;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (n->getType()) {
|
||||||
|
case Type_Node0: // GCOVR_EXCL_LINE
|
||||||
|
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||||
|
case Type_Node3: {
|
||||||
|
auto *n3 = static_cast<Node3 *>(n);
|
||||||
|
int i = getNodeIndex(n3, index);
|
||||||
|
n3->children[i].childMaxVersion = newMax;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case Type_Node16: {
|
||||||
|
auto *n16 = static_cast<Node16 *>(n);
|
||||||
|
int i = getNodeIndex(n16, index);
|
||||||
|
n16->children[i].childMaxVersion = newMax;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case Type_Node48: {
|
||||||
|
auto *n48 = static_cast<Node48 *>(n);
|
||||||
|
assert(n48->bitSet.test(index));
|
||||||
|
int i = n48->index[index];
|
||||||
|
n48->children[i].childMaxVersion = newMax;
|
||||||
|
n48->maxOfMax[i >> Node48::kMaxOfMaxShift] =
|
||||||
|
std::max(n48->maxOfMax[i >> Node48::kMaxOfMaxShift], newMax);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case Type_Node256: {
|
||||||
|
auto *n256 = static_cast<Node256 *>(n);
|
||||||
|
assert(n256->bitSet.test(index));
|
||||||
|
n256->children[index].childMaxVersion = newMax;
|
||||||
|
n256->maxOfMax[index >> Node256::kMaxOfMaxShift] =
|
||||||
|
std::max(n256->maxOfMax[index >> Node256::kMaxOfMaxShift], newMax);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default: // GCOVR_EXCL_LINE
|
||||||
|
__builtin_unreachable(); // GCOVR_EXCL_LINE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Node *&getInTree(Node *n, ConflictSet::Impl *impl) {
|
Node *&getInTree(Node *n, ConflictSet::Impl *impl) {
|
||||||
return n->parent == nullptr ? impl->root
|
return n->parent == nullptr ? impl->root
|
||||||
: getChildExists(n->parent, n->parentsIndex);
|
: getChildExists(n->parent, n->parentsIndex);
|
||||||
@@ -2847,7 +2967,7 @@ Iterator firstGeq(Node *n, std::string_view key) {
|
|||||||
[[maybe_unused]] int64_t checkMaxVersion(Node *root, Node *node,
|
[[maybe_unused]] int64_t checkMaxVersion(Node *root, Node *node,
|
||||||
int64_t oldestVersion, bool &success,
|
int64_t oldestVersion, bool &success,
|
||||||
ConflictSet::Impl *impl) {
|
ConflictSet::Impl *impl) {
|
||||||
int64_t expected = std::numeric_limits<int64_t>::lowest();
|
int64_t expected = 0;
|
||||||
if (node->entryPresent) {
|
if (node->entryPresent) {
|
||||||
expected = std::max(expected, node->entry.pointVersion);
|
expected = std::max(expected, node->entry.pointVersion);
|
||||||
}
|
}
|
||||||
|
38
README.md
38
README.md
@@ -58,27 +58,29 @@ Performance counters:
|
|||||||
|
|
||||||
## Skip list
|
## Skip list
|
||||||
|
|
||||||
| ns/op | op/s | err% | total | benchmark |
|
| ns/op | op/s | err% | total | benchmark
|
||||||
| -----: | -----------: | ---: | ----: | :---------------------------------- |
|
|--------------------:|--------------------:|--------:|----------:|:----------
|
||||||
| 246.99 | 4,048,700.59 | 0.2% | 0.01 | `point reads` |
|
| 256.89 | 3,892,784.92 | 0.3% | 0.01 | `point reads`
|
||||||
| 260.16 | 3,843,784.65 | 0.1% | 0.01 | `prefix reads` |
|
| 272.90 | 3,664,395.04 | 0.2% | 0.01 | `prefix reads`
|
||||||
| 493.35 | 2,026,953.19 | 0.1% | 0.01 | `range reads` |
|
| 507.22 | 1,971,549.50 | 0.7% | 0.01 | `range reads`
|
||||||
| 462.05 | 2,164,289.23 | 0.6% | 0.01 | `point writes` |
|
| 452.66 | 2,209,181.91 | 0.5% | 0.01 | `point writes`
|
||||||
| 448.19 | 2,231,205.25 | 0.9% | 0.01 | `prefix writes` |
|
| 438.09 | 2,282,619.96 | 0.4% | 0.01 | `prefix writes`
|
||||||
| 255.83 | 3,908,845.72 | 1.5% | 0.02 | `range writes` |
|
| 253.33 | 3,947,420.36 | 2.5% | 0.02 | `range writes`
|
||||||
| 582.63 | 1,716,349.02 | 1.3% | 0.01 | `monotonic increasing point writes` |
|
| 574.07 | 1,741,936.71 | 0.3% | 0.01 | `monotonic increasing point writes`
|
||||||
|
| 151,562.50 | 6,597.94 | 1.5% | 0.01 | `worst case for radix tree`
|
||||||
|
|
||||||
## Radix tree (this implementation)
|
## Radix tree (this implementation)
|
||||||
|
|
||||||
| ns/op | op/s | err% | total | benchmark |
|
| ns/op | op/s | err% | total | benchmark
|
||||||
| -----: | ------------: | ---: | ----: | :---------------------------------- |
|
|--------------------:|--------------------:|--------:|----------:|:----------
|
||||||
| 19.42 | 51,483,206.67 | 0.3% | 0.01 | `point reads` |
|
| 19.83 | 50,420,955.28 | 0.1% | 0.01 | `point reads`
|
||||||
| 58.43 | 17,115,612.57 | 0.1% | 0.01 | `prefix reads` |
|
| 55.95 | 17,872,542.40 | 0.5% | 0.01 | `prefix reads`
|
||||||
| 216.09 | 4,627,766.60 | 0.2% | 0.01 | `range reads` |
|
| 88.28 | 11,327,709.50 | 0.4% | 0.01 | `range reads`
|
||||||
| 28.35 | 35,267,567.72 | 0.2% | 0.01 | `point writes` |
|
| 29.15 | 34,309,531.64 | 0.5% | 0.01 | `point writes`
|
||||||
| 43.43 | 23,026,226.17 | 0.2% | 0.01 | `prefix writes` |
|
| 42.36 | 23,607,424.27 | 1.1% | 0.01 | `prefix writes`
|
||||||
| 50.00 | 20,000,000.00 | 0.0% | 0.01 | `range writes` |
|
| 50.00 | 20,000,000.00 | 0.0% | 0.01 | `range writes`
|
||||||
| 92.38 | 10,824,863.69 | 4.1% | 0.01 | `monotonic increasing point writes` |
|
| 93.52 | 10,692,413.79 | 3.3% | 0.01 | `monotonic increasing point writes`
|
||||||
|
| 2,388,417.00 | 418.69 | 0.4% | 0.03 | `worst case for radix tree`
|
||||||
|
|
||||||
# "Real data" test
|
# "Real data" test
|
||||||
|
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
___stack_chk_fail
|
||||||
|
___stack_chk_guard
|
||||||
__tlv_bootstrap
|
__tlv_bootstrap
|
||||||
_abort
|
_abort
|
||||||
_bzero
|
_bzero
|
||||||
|
@@ -115,7 +115,9 @@ class ConflictSet:
|
|||||||
|
|
||||||
def check(self, *reads: ReadRange) -> list[Result]:
|
def check(self, *reads: ReadRange) -> list[Result]:
|
||||||
r = (ctypes.c_int * len(reads))()
|
r = (ctypes.c_int * len(reads))()
|
||||||
self._lib.ConflictSet_check(self.p, *reads, r, 1)
|
self._lib.ConflictSet_check(
|
||||||
|
self.p, (ReadRange * len(reads))(*reads), r, len(reads)
|
||||||
|
)
|
||||||
return [Result(x) for x in r]
|
return [Result(x) for x in r]
|
||||||
|
|
||||||
def setOldestVersion(self, version: int) -> None:
|
def setOldestVersion(self, version: int) -> None:
|
||||||
|
@@ -21,7 +21,7 @@ limitations under the License.
|
|||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
namespace weaselab {
|
namespace weaselab {
|
||||||
/** A data structure for optimistic concurrency control on ranges of
|
/** A data structure for optimistic concurrency control on ranges of
|
||||||
* bitwise-lexicographically-ordered keys.
|
* bitwise-lexicographically-ordered keys. All versions must be >= 0.
|
||||||
*
|
*
|
||||||
* Thread safety:
|
* Thread safety:
|
||||||
* - It's safe to operate on two different ConflictSets in two different
|
* - It's safe to operate on two different ConflictSets in two different
|
||||||
@@ -106,7 +106,7 @@ private:
|
|||||||
#else
|
#else
|
||||||
|
|
||||||
/** A data structure for optimistic concurrency control on ranges of
|
/** A data structure for optimistic concurrency control on ranges of
|
||||||
* bitwise-lexicographically-ordered keys.
|
* bitwise-lexicographically-ordered keys. All versions must be >= 0.
|
||||||
*
|
*
|
||||||
* Thread safety:
|
* Thread safety:
|
||||||
* - It's safe to operate on two different ConflictSets in two different
|
* - It's safe to operate on two different ConflictSets in two different
|
||||||
|
1
paper/.gitignore
vendored
1
paper/.gitignore
vendored
@@ -10,3 +10,4 @@
|
|||||||
*.pdf
|
*.pdf
|
||||||
*.run.xml
|
*.run.xml
|
||||||
*.synctex.gz
|
*.synctex.gz
|
||||||
|
version.txt
|
||||||
|
@@ -6,4 +6,4 @@ paper.pdf: paper.tex $(wildcard *.tikz)
|
|||||||
latexmk -pdf
|
latexmk -pdf
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
xargs -I '{}' bash -c "rm -f {}" < .gitignore
|
grep -v version.txt .gitignore |xargs -I '{}' bash -c "rm -f {}"
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
\providecommand{\versionnumber}{0.0.6}
|
|
@@ -53,7 +53,7 @@ def test_conflict_set():
|
|||||||
assert cs.getBytes() - before > 0
|
assert cs.getBytes() - before > 0
|
||||||
assert cs.check(read(0, key)) == [Result.CONFLICT]
|
assert cs.check(read(0, key)) == [Result.CONFLICT]
|
||||||
cs.setOldestVersion(1)
|
cs.setOldestVersion(1)
|
||||||
assert cs.check(read(0, key)) == [Result.TOO_OLD]
|
assert cs.check(read(0, key), read(1, key)) == [Result.TOO_OLD, Result.COMMIT]
|
||||||
|
|
||||||
|
|
||||||
def test_inner_full_words():
|
def test_inner_full_words():
|
||||||
|
Reference in New Issue
Block a user