Compare commits
14 Commits
v0.0.2
...
fdb05e0e33
Author | SHA1 | Date | |
---|---|---|---|
fdb05e0e33 | |||
7c27d4a972 | |||
738de01cb4 | |||
325cab6a95 | |||
0b2821941a | |||
a40b5dcd74 | |||
193b1926ff | |||
1c900c5a8c | |||
90fdcdd51a | |||
eb3f6823eb | |||
1534e10b75 | |||
3c100ccee8 | |||
5cf45d1c35 | |||
4f97932893 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.cache
|
||||
__pycache__
|
||||
build
|
||||
|
@@ -1,11 +1,11 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||
rev: 6d365699efc33b1b432eab5b4ae331a19e1857de # frozen: v18.1.2
|
||||
rev: 6d365699efc33b1b432eab5b4ae331a19e1857de # frozen: v18.1.2
|
||||
hooks:
|
||||
- id: clang-format
|
||||
exclude: ".*third_party/.*"
|
||||
- repo: https://github.com/cheshirekow/cmake-format-precommit
|
||||
rev: e2c2116d86a80e72e7146a06e68b7c228afc6319 # frozen: v0.6.13
|
||||
rev: e2c2116d86a80e72e7146a06e68b7c228afc6319 # frozen: v0.6.13
|
||||
hooks:
|
||||
- id: cmake-format
|
||||
- repo: local
|
||||
@@ -13,7 +13,7 @@ repos:
|
||||
- id: debug verbose check
|
||||
name: disallow checking in DEBUG_VERBOSE=1
|
||||
description: disallow checking in DEBUG_VERBOSE=1
|
||||
entry: '^#define DEBUG_VERBOSE 1$'
|
||||
entry: "^#define DEBUG_VERBOSE 1$"
|
||||
language: pygrep
|
||||
types: [c++]
|
||||
- repo: local
|
||||
@@ -21,10 +21,14 @@ repos:
|
||||
- id: debug verbose check
|
||||
name: disallow checking in SHOW_MEMORY=1
|
||||
description: disallow checking in SHOW_MEMORY=1
|
||||
entry: '^#define SHOW_MEMORY 1$'
|
||||
entry: "^#define SHOW_MEMORY 1$"
|
||||
language: pygrep
|
||||
types: [c++]
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: a23f6b85d0fdd5bb9d564e2579e678033debbdff # frozen: v0.10.0.1
|
||||
rev: a23f6b85d0fdd5bb9d564e2579e678033debbdff # frozen: v0.10.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
- id: shellcheck
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 552baf822992936134cbd31a38f69c8cfe7c0f05 # frozen: 24.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
@@ -1,13 +1,15 @@
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
project(
|
||||
conflict-set
|
||||
VERSION 0.0.2
|
||||
VERSION 0.0.4
|
||||
DESCRIPTION
|
||||
"A data structure for optimistic concurrency control on ranges of bitwise-lexicographically-ordered keys."
|
||||
HOMEPAGE_URL "https://git.weaselab.dev/weaselab/conflict-set"
|
||||
LANGUAGES C CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
|
||||
file(WRITE ${CMAKE_BINARY_DIR}/version.txt ${PROJECT_VERSION})
|
||||
|
||||
include(CMakePushCheckState)
|
||||
include(CheckCXXCompilerFlag)
|
||||
include(CheckIncludeFileCXX)
|
||||
@@ -128,7 +130,7 @@ endif()
|
||||
if(APPLE)
|
||||
add_custom_command(
|
||||
TARGET ${PROJECT_NAME}-static
|
||||
PRE_BUILD
|
||||
PRE_LINK
|
||||
COMMAND ${CMAKE_SOURCE_DIR}/privatize_symbols_macos.sh
|
||||
$<TARGET_OBJECTS:${PROJECT_NAME}-object>)
|
||||
else()
|
||||
@@ -333,6 +335,7 @@ endif()
|
||||
# packaging
|
||||
|
||||
set(CPACK_PACKAGE_CONTACT andrew@weaselab.dev)
|
||||
set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME all)
|
||||
|
||||
set(CPACK_PACKAGE_VENDOR "Weaselab")
|
||||
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE")
|
||||
@@ -353,6 +356,20 @@ else()
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6 (>= 2.14)")
|
||||
endif()
|
||||
|
||||
# macos
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET 11.0)
|
||||
if(APPLE)
|
||||
find_program(PANDOC_EXE pandoc)
|
||||
if(PANDOC_EXE)
|
||||
execute_process(COMMAND ${PANDOC_EXE} ${CMAKE_SOURCE_DIR}/README.md -o
|
||||
${CMAKE_BINARY_DIR}/README.txt)
|
||||
set(CPACK_RESOURCE_FILE_README ${CMAKE_BINARY_DIR}/README.txt)
|
||||
endif()
|
||||
configure_file(${CMAKE_SOURCE_DIR}/LICENSE ${CMAKE_BINARY_DIR}/LICENSE.txt
|
||||
COPYONLY)
|
||||
set(CPACK_RESOURCE_FILE_LICENSE ${CMAKE_BINARY_DIR}/LICENSE.txt)
|
||||
endif()
|
||||
|
||||
include(CPack)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
@@ -377,8 +394,11 @@ install(
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
|
||||
|
||||
install(DIRECTORY include/
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME})
|
||||
|
||||
install(EXPORT ${PROJECT_NAME}Config
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake)
|
||||
|
||||
cpack_add_component(all)
|
||||
|
36
README.md
36
README.md
@@ -58,27 +58,27 @@ Performance counters:
|
||||
|
||||
## Skip list
|
||||
|
||||
| ns/op | op/s | err% | total | benchmark
|
||||
|--------------------:|--------------------:|--------:|----------:|:----------
|
||||
| 246.99 | 4,048,700.59 | 0.2% | 0.01 | `point reads`
|
||||
| 260.16 | 3,843,784.65 | 0.1% | 0.01 | `prefix reads`
|
||||
| 493.35 | 2,026,953.19 | 0.1% | 0.01 | `range reads`
|
||||
| 462.05 | 2,164,289.23 | 0.6% | 0.01 | `point writes`
|
||||
| 448.19 | 2,231,205.25 | 0.9% | 0.01 | `prefix writes`
|
||||
| 255.83 | 3,908,845.72 | 1.5% | 0.02 | `range writes`
|
||||
| 582.63 | 1,716,349.02 | 1.3% | 0.01 | `monotonic increasing point writes`
|
||||
| ns/op | op/s | err% | total | benchmark |
|
||||
| -----: | -----------: | ---: | ----: | :---------------------------------- |
|
||||
| 246.99 | 4,048,700.59 | 0.2% | 0.01 | `point reads` |
|
||||
| 260.16 | 3,843,784.65 | 0.1% | 0.01 | `prefix reads` |
|
||||
| 493.35 | 2,026,953.19 | 0.1% | 0.01 | `range reads` |
|
||||
| 462.05 | 2,164,289.23 | 0.6% | 0.01 | `point writes` |
|
||||
| 448.19 | 2,231,205.25 | 0.9% | 0.01 | `prefix writes` |
|
||||
| 255.83 | 3,908,845.72 | 1.5% | 0.02 | `range writes` |
|
||||
| 582.63 | 1,716,349.02 | 1.3% | 0.01 | `monotonic increasing point writes` |
|
||||
|
||||
## Radix tree (this implementation)
|
||||
|
||||
| ns/op | op/s | err% | total | benchmark
|
||||
|--------------------:|--------------------:|--------:|----------:|:----------
|
||||
| 19.42 | 51,483,206.67 | 0.3% | 0.01 | `point reads`
|
||||
| 58.43 | 17,115,612.57 | 0.1% | 0.01 | `prefix reads`
|
||||
| 216.09 | 4,627,766.60 | 0.2% | 0.01 | `range reads`
|
||||
| 28.35 | 35,267,567.72 | 0.2% | 0.01 | `point writes`
|
||||
| 43.43 | 23,026,226.17 | 0.2% | 0.01 | `prefix 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`
|
||||
| ns/op | op/s | err% | total | benchmark |
|
||||
| -----: | ------------: | ---: | ----: | :---------------------------------- |
|
||||
| 19.42 | 51,483,206.67 | 0.3% | 0.01 | `point reads` |
|
||||
| 58.43 | 17,115,612.57 | 0.1% | 0.01 | `prefix reads` |
|
||||
| 216.09 | 4,627,766.60 | 0.2% | 0.01 | `range reads` |
|
||||
| 28.35 | 35,267,567.72 | 0.2% | 0.01 | `point writes` |
|
||||
| 43.43 | 23,026,226.17 | 0.2% | 0.01 | `prefix 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` |
|
||||
|
||||
# "Real data" test
|
||||
|
||||
|
@@ -4,4 +4,5 @@ _bzero
|
||||
_free
|
||||
_malloc
|
||||
_memcpy
|
||||
_memmove
|
||||
_memmove
|
||||
dyld_stub_binder
|
126
conflict_set.py
Normal file
126
conflict_set.py
Normal file
@@ -0,0 +1,126 @@
|
||||
import ctypes
|
||||
import enum
|
||||
import os
|
||||
|
||||
from typing import Optional
|
||||
|
||||
_lib = None
|
||||
for f in (
|
||||
os.path.dirname(__file__) + "/build/radix_tree/libconflict-set.so.0",
|
||||
os.path.dirname(__file__) + "/build/radix_tree/libconflict-set.0.dylib",
|
||||
):
|
||||
try:
|
||||
_lib = ctypes.cdll.LoadLibrary(f)
|
||||
except:
|
||||
pass
|
||||
|
||||
if _lib is None:
|
||||
import sys
|
||||
|
||||
print("Could not find libconflict-set", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class _Key(ctypes.Structure):
|
||||
_fields_ = [("p", ctypes.POINTER(ctypes.c_ubyte)), ("len", ctypes.c_int)]
|
||||
|
||||
|
||||
class ReadRange(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("begin", _Key),
|
||||
("end", _Key),
|
||||
("readVersion", ctypes.c_int64),
|
||||
]
|
||||
|
||||
|
||||
class WriteRange(ctypes.Structure):
|
||||
_fields_ = [("begin", _Key), ("end", _Key)]
|
||||
|
||||
|
||||
_lib.ConflictSet_create.argtypes = (ctypes.c_int64,)
|
||||
_lib.ConflictSet_create.restype = ctypes.c_void_p
|
||||
|
||||
_lib.ConflictSet_check.argtypes = (
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(ReadRange),
|
||||
ctypes.POINTER(ctypes.c_int),
|
||||
ctypes.c_int,
|
||||
)
|
||||
|
||||
_lib.ConflictSet_addWrites.argtypes = (
|
||||
ctypes.c_void_p,
|
||||
ctypes.POINTER(WriteRange),
|
||||
ctypes.c_int,
|
||||
ctypes.c_int64,
|
||||
)
|
||||
|
||||
_lib.ConflictSet_setOldestVersion.argtypes = (ctypes.c_void_p, ctypes.c_int64)
|
||||
|
||||
_lib.ConflictSet_destroy.argtypes = (ctypes.c_void_p,)
|
||||
|
||||
_lib.ConflictSet_getBytes.argtypes = (ctypes.c_void_p,)
|
||||
_lib.ConflictSet_getBytes.restype = ctypes.c_int64
|
||||
|
||||
|
||||
class Result(enum.Enum):
|
||||
COMMIT = 0
|
||||
CONFLICT = 1
|
||||
TOO_OLD = 2
|
||||
|
||||
|
||||
def write(begin: bytes, end: Optional[bytes] = None) -> WriteRange:
|
||||
b = (ctypes.c_ubyte * len(begin))()
|
||||
b.value = begin
|
||||
if end is None:
|
||||
e = (ctypes.c_ubyte * 0)()
|
||||
e.value = b""
|
||||
else:
|
||||
e = (ctypes.c_ubyte * len(end))()
|
||||
e.value = end
|
||||
return WriteRange(_Key(b, len(b)), _Key(e, len(e)))
|
||||
|
||||
|
||||
def read(version: int, begin: bytes, end: Optional[bytes] = None) -> ReadRange:
|
||||
b = (ctypes.c_ubyte * len(begin))()
|
||||
b.value = begin
|
||||
if end is None:
|
||||
e = (ctypes.c_ubyte * 0)()
|
||||
e.value = b""
|
||||
else:
|
||||
e = (ctypes.c_ubyte * len(end))()
|
||||
e.value = end
|
||||
return ReadRange(_Key(b, len(b)), _Key(e, len(e)), version)
|
||||
|
||||
|
||||
class ConflictSet:
|
||||
def __init__(self, version: int = 0) -> None:
|
||||
self.p = _lib.ConflictSet_create(version)
|
||||
|
||||
def addWrites(self, version: int, *writes: WriteRange):
|
||||
_lib.ConflictSet_addWrites(
|
||||
self.p, (WriteRange * len(writes))(*writes), len(writes), version
|
||||
)
|
||||
|
||||
def check(self, *reads: ReadRange) -> list[Result]:
|
||||
r = (ctypes.c_int * len(reads))()
|
||||
_lib.ConflictSet_check(self.p, *reads, r, 1)
|
||||
return [Result(x) for x in r]
|
||||
|
||||
def setOldestVersion(self, version: int) -> None:
|
||||
_lib.ConflictSet_setOldestVersion(self.p, version)
|
||||
|
||||
def getBytes(self) -> int:
|
||||
return _lib.ConflictSet_getBytes(self.p)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def close(self) -> None:
|
||||
if self.p is not None:
|
||||
_lib.ConflictSet_destroy(self.p)
|
||||
self.p = None
|
||||
|
||||
def __exit__(self, exception_type, exception_value, exception_traceback):
|
||||
if self.p is not None:
|
||||
_lib.ConflictSet_destroy(self.p)
|
||||
self.p = None
|
33
package_macos.sh
Executable file
33
package_macos.sh
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
umask 022
|
||||
|
||||
SRC_DIR="${0%/*}"
|
||||
BUILD_ARM="$(mktemp -d -t conflict-set-arm)"
|
||||
BUILD_X86="$(mktemp -d -t conflict-set-x86)"
|
||||
|
||||
cmake_args=(-DCMAKE_CXX_FLAGS=-DNVALGRIND -DCPACK_PACKAGING_INSTALL_PREFIX=/usr/local)
|
||||
|
||||
cmake -S"$SRC_DIR" -B"$BUILD_ARM" -DCMAKE_OSX_ARCHITECTURES=arm64 "${cmake_args[@]}"
|
||||
cmake --build "$BUILD_ARM" --target conflict-set --target conflict-set-static
|
||||
|
||||
cmake -S"$SRC_DIR" -B"$BUILD_X86" -DCMAKE_OSX_ARCHITECTURES=x86_64 "${cmake_args[@]}"
|
||||
cmake --build "$BUILD_X86" --target conflict-set --target conflict-set-static
|
||||
|
||||
VERSION="$(cat "$BUILD_ARM/version.txt")"
|
||||
|
||||
lipo -create "$BUILD_ARM/radix_tree/libconflict-set.$VERSION.dylib" "$BUILD_X86/radix_tree/libconflict-set.$VERSION.dylib" -output "libconflict-set.$VERSION.dylib.tmp"
|
||||
lipo -create "$BUILD_ARM"/libconflict-set-static.a "$BUILD_X86"/libconflict-set-static.a -output libconflict-set-static.a.tmp
|
||||
|
||||
mv "libconflict-set.$VERSION.dylib.tmp" "$BUILD_ARM/radix_tree/libconflict-set.$VERSION.dylib"
|
||||
mv libconflict-set-static.a.tmp "$BUILD_ARM/libconflict-set-static.a"
|
||||
|
||||
pushd "$BUILD_ARM"
|
||||
cpack -G productbuild
|
||||
popd
|
||||
|
||||
mv "$BUILD_ARM/conflict-set-$VERSION-Darwin.pkg" .
|
||||
|
||||
rm -rf "$BUILD_ARM" "$BUILD_X86"
|
@@ -155,3 +155,16 @@ keywords = {data structures, searching, trees}
|
||||
year={1981},
|
||||
publisher={ACM New York, NY, USA}
|
||||
}
|
||||
|
||||
@article{Lemire_2018,
|
||||
title={Roaring bitmaps: Implementation of an optimized software library},
|
||||
volume={48},
|
||||
ISSN={1097-024X},
|
||||
url={http://dx.doi.org/10.1002/spe.2560},
|
||||
DOI={10.1002/spe.2560},
|
||||
number={4},
|
||||
journal={Software: Practice and Experience},
|
||||
publisher={Wiley},
|
||||
author={Lemire, Daniel and Kaser, Owen and Kurz, Nathan and Deri, Luca and O’Hara, Chris and Saint‐Jacques, François and Ssi‐Yan‐Kai, Gregory},
|
||||
year={2018},
|
||||
month=jan, pages={867–895} }
|
||||
|
@@ -6,6 +6,7 @@
|
||||
\usepackage{tikz}
|
||||
\usepackage{tikzscale}
|
||||
\usepackage[edges]{forest}
|
||||
\usepackage{amsmath}
|
||||
|
||||
\title{ARTful Conflict Checking for FoundationDB}
|
||||
\author{Andrew Noyes \thanks{\href{mailto:andrew@weaselab.dev}{andrew@weaselab.dev}}}
|
||||
@@ -84,6 +85,78 @@ As an optimization, during the search phase for a point read we can inspect the
|
||||
If the max version among all keys starting with a prefix of $k$ is less than or equal to $r$, then $v_{k} \leq r$.
|
||||
|
||||
\subsection{Checking range reads}
|
||||
|
||||
Checking range reads is more involved. Logically the idea is to partition the range read so that each partition is a single point or coincides with the set of keys beginning with a prefix.
|
||||
The max version of the set of keys starting with a prefix is then $max$ of the node associated with the prefix if such a node exists, and $range$ of the next node with a $range$ field otherwise.
|
||||
|
||||
Let's start with partitioning the range in the case where the beginning of the range is a prefix of the end of the range.
|
||||
We'll be able to use this as a subroutine in the general case.
|
||||
Suppose our range is $[a_{0}\dots a_{k}, a_{0}\dots a_{n})$ where $k < n$.
|
||||
The partition starts with the singleton set containing the first key in the range.
|
||||
\[
|
||||
\{a_{0}\dots a_{k}\}
|
||||
\]
|
||||
or equivalently
|
||||
\[
|
||||
[a_{0}\dots a_{k}, a_{0}\dots a_{k} 0)
|
||||
\]
|
||||
and continues with a sequence of prefix ranges ending in each digit up until $a_{k+1}$.
|
||||
Recall that the range $[a_{0}\dots a_{k} 0, a_{0}\dots a_{k} 1)$ is equivalent to the set of keys starting with $a_{0}\dots a_{k} 0$.
|
||||
|
||||
\begin{align*}
|
||||
\dots \quad \cup \quad & [a_{0}\dots a_{k} 0, a_{0}\dots a_{k} 1) \quad \cup \\
|
||||
& [a_{0}\dots a_{k} 1, a_{0}\dots a_{k} 2) \quad \cup \\
|
||||
& \dots \\
|
||||
& [a_{0}\dots a_{k} (a_{k+1}-1), a_{0}\dots a_{k+1})
|
||||
\end{align*}
|
||||
|
||||
The remainder of the partition begins with the singleton set
|
||||
\[
|
||||
\dots \quad \cup \quad [a_{0}\dots a_{k + 1}, a_{0}\dots a_{k + 1} 0)
|
||||
\]
|
||||
and proceeds as above until a range ending at $a_{0}\dots a_{n}$.
|
||||
|
||||
Let's now consider a range where begin is not a prefix of end.
|
||||
|
||||
\[
|
||||
[a_{0}\dots a_{m}, b_{0}\dots b_{n})
|
||||
\]
|
||||
|
||||
Let $i$ be the lowest index such that $a_{i} \neq b_{i}$.
|
||||
For brevity we will elide the common prefix up until $i$ in the following discussion.
|
||||
We'll start with partitioning this range coarsely:
|
||||
|
||||
\begin{align*}
|
||||
& [a_{i}\dots a_{m}, a_{i} + 1) \quad \cup \\
|
||||
& [a_{i} + 1, a_{i} + 2) \quad \cup \\
|
||||
& \dots \\
|
||||
& [b_{i} - 1, b_{i}) \quad \cup \\
|
||||
& [b_{i}, b_{i}\dots b_{n})
|
||||
\end{align*}
|
||||
|
||||
The last range has a begin that's a prefix of end, and so we'll partition that as before.
|
||||
The inner ranges are already prefix ranges.
|
||||
This leaves only $[a_{i}\dots a_{m}, a_{i} + 1)$.
|
||||
|
||||
If $m = i$, then this range is adjacent to the first inner range above, and we're done.
|
||||
Otherwise we'll partition this into
|
||||
|
||||
\begin{align*}
|
||||
& [a_{i}\dots a_{m}, a_{i}\dots (a_{m} + 1)) \quad \cup \\
|
||||
& \dots \\
|
||||
& [a_{i}\dots 254, a_{i}\dots 255)
|
||||
\end{align*}
|
||||
|
||||
and repeat with $m \gets m - 1$ until we are adjacent to the first inner range.
|
||||
|
||||
A few notes on implementation:
|
||||
\begin{itemize}
|
||||
\item{For clarity, the above algorithm decouples the logical partitioning from the physical structure of the tree. An optimized implementation would merge adjacent prefix ranges that don't correspond to nodes in the tree as it scans, so that it only calculates the version of merged ranges once. Additionally, our implementation stores an index of which child pointers are valid as a bitset for Node48 and Node256, using techniques inspired by \cite{Lemire_2018}.}
|
||||
\item{In order to avoid many costly pointer indirections, we can store the max version not in each node itself but next to each node's parent pointer. Without this, the range read performance is not competetive with the skip list.}
|
||||
\item{An optimized implementation would construct the partition of $[a_{i}\dots a_{m}, a_{i} + 1)$ in reverse order, as it descends along the search path to $[a_{i}\dots a_{m})$}
|
||||
\end{itemize}
|
||||
|
||||
|
||||
\subsection{Adding point writes}
|
||||
\subsection{Adding range writes}
|
||||
\subsection{Reclaiming old entries}
|
||||
|
@@ -2,5 +2,7 @@
|
||||
|
||||
# This has the effect of making visibility=hidden symbols private in object files
|
||||
for obj in "$@" ; do
|
||||
ld -r "$obj" -o "$obj.tmp" && mv "$obj.tmp" "$obj"
|
||||
ld -r "$obj" -o "$obj.tmp"
|
||||
touch -r "$obj" "$obj.tmp"
|
||||
mv "$obj.tmp" "$obj"
|
||||
done
|
||||
|
12
test_conflict_set.py
Normal file
12
test_conflict_set.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from conflict_set import *
|
||||
|
||||
|
||||
def test_conflict_set():
|
||||
with ConflictSet() as cs:
|
||||
before = cs.getBytes()
|
||||
key = b"a key"
|
||||
cs.addWrites(1, write(key))
|
||||
assert cs.getBytes() - before > 0
|
||||
assert cs.check(read(0, key)) == [Result.CONFLICT]
|
||||
cs.setOldestVersion(1)
|
||||
assert cs.check(read(0, key)) == [Result.TOO_OLD]
|
Reference in New Issue
Block a user