Paper tweaks
All checks were successful
Tests / Clang total: 1130, passed: 1130
Clang |Total|New|Outstanding|Fixed|Trend
|:-:|:-:|:-:|:-:|:-:
|0|0|0|0|:clap:
Tests / SIMD fallback total: 1130, passed: 1130
Tests / Release [gcc] total: 1130, passed: 1130
GNU C Compiler (gcc) |Total|New|Outstanding|Fixed|Trend
|:-:|:-:|:-:|:-:|:-:
|0|0|0|0|:clap:
Tests / Release [gcc,aarch64] total: 844, passed: 844
Tests / Coverage total: 848, passed: 848
weaselab/conflict-set/pipeline/head This commit looks good
All checks were successful
Tests / Clang total: 1130, passed: 1130
Clang |Total|New|Outstanding|Fixed|Trend
|:-:|:-:|:-:|:-:|:-:
|0|0|0|0|:clap:
Tests / SIMD fallback total: 1130, passed: 1130
Tests / Release [gcc] total: 1130, passed: 1130
GNU C Compiler (gcc) |Total|New|Outstanding|Fixed|Trend
|:-:|:-:|:-:|:-:|:-:
|0|0|0|0|:clap:
Tests / Release [gcc,aarch64] total: 844, passed: 844
Tests / Coverage total: 848, passed: 848
weaselab/conflict-set/pipeline/head This commit looks good
This commit is contained in:
@@ -23,10 +23,11 @@
|
||||
|
||||
FoundationDB \cite{DBLP:conf/sigmod/ZhouXSNMTABSLRD21} provides serializability using a specialized data structure called \emph{lastCommit} \footnote{See Algorithm 1 referenced in \cite{DBLP:conf/sigmod/ZhouXSNMTABSLRD21}.} to implement optimistic concurrency control \cite{kung1981optimistic}.
|
||||
This data structure encodes the write sets for recent transactions as a map from key ranges (represented as bitwise-lexicographically-ordered half-open intervals) to most recent write versions.
|
||||
\emph{lastCommit} operates on logical keys and is agnostic to the physical structure of the data store, so it should be straightforward to use outside of FoundationDB.
|
||||
Before a transaction is allowed to commit, its read set is checked for writes that happened after its read version.
|
||||
FoundationDB implements \emph{lastCommit} as a version-augmented probabilistic skip list \cite{10.1145/78973.78977}.
|
||||
In this paper, we propose an alternative implementation as a version-augmented Adaptive Radix Tree (ART) \cite{DBLP:conf/icde/LeisK013}, and evaluate its performance.
|
||||
This implementation is available at \url{https://git.weaselab.dev/weaselab/conflict-set/releases} as a C or C++ library.
|
||||
\emph{lastCommit} operates on logical keys and is agnostic to the physical structure of the data store, so it should be straightforward to use outside of FoundationDB.
|
||||
|
||||
\section{Introduction}
|
||||
|
||||
@@ -41,7 +42,7 @@ Scanning through every recent point write intersecting a large range read would
|
||||
This suggests we consider augmenting \cite{cormen2022introduction} an ordered data structure to make checking the max version of a range sublinear.
|
||||
Since finding the maximum of a set of elements is a decomposable search problem \cite{bentley1979decomposable}, we could apply the general technique using \texttt{std::max} as our binary operation, and \texttt{MIN\_INT} as our identity.
|
||||
Algorithmically, this describes FoundationDB's skip list.
|
||||
We can also consider any other ordered data structure to augment, such as any variant of a balanced binary search tree \cite{adelson1962algorithm,guibas1978dichromatic,seidel1996randomized}, a b-tree \cite{comer1979ubiquitous}, or a radix tree \cite{DBLP:conf/icde/LeisK013,binna2018hot}.
|
||||
We can also consider any other ordered data structure to augment, such as any variant of a balanced search tree \cite{adelson1962algorithm,guibas1978dichromatic,seidel1996randomized,comer1979ubiquitous}, or a radix tree \cite{DBLP:conf/icde/LeisK013,binna2018hot}.
|
||||
|
||||
Let's compare the relevant properties of our candidate data structures for insertion/update and read operations.
|
||||
After insertion, the max version along the search path must reflect the update.
|
||||
@@ -65,7 +66,7 @@ We now propose our design for an augmented radix tree implementation of \emph{la
|
||||
The design is the same as the Adaptive Radix Tree \cite{DBLP:conf/icde/LeisK013}, but each node in the tree is annotated with either one field \emph{max}, or three fields: \emph{max}, \emph{point}, and \emph{range}.
|
||||
\emph{max} represents the maximum version among all keys starting with the prefix associated with the node's place in the tree (i.e. the search path from the root to this node).
|
||||
\emph{point} represents the version of the exact key matching this node's prefix.
|
||||
\emph{range} represents the version of all keys $k$ such that this is the first node greater than $k$ with all three fields set.
|
||||
\emph{range} represents the version of all keys $k$ such that there is no node matching $k$ and this is the first node greater than $k$ with all three fields set.
|
||||
See figure \ref{fig:tree} for an example tree after inserting
|
||||
$[AND, ANT) \rightarrow 1$,
|
||||
$\{ANY\} \rightarrow 2$,
|
||||
@@ -183,7 +184,7 @@ We track the rate of insertions of new nodes and make sure that our incremental
|
||||
|
||||
\subsection{Adding point writes}
|
||||
|
||||
A point write of $k$ at version $v$ simply sets $max \gets v$ \footnote{Recall that write versions are non-decreasing.} for every node along $k$'s search path, and sets $range$ for $k$'s node to the $range$ of the first node greater than $k$, or \emph{oldest version} if none exists.
|
||||
A point write of $k$ at version $v$ simply sets $max \gets v$ \footnote{Write versions are non-decreasing.} for every node along $k$'s search path, and sets $range$ for $k$'s node to the $range$ of the first node greater than $k$, or \emph{oldest version} if none exists.
|
||||
|
||||
\subsection{Adding range writes}
|
||||
|
||||
@@ -198,7 +199,7 @@ The correctness of \emph{lastCommit} is critically important, as a bug would lik
|
||||
The main technique is to let libfuzzer \cite{libfuzzer} generate sequences of arbitrary operations, and apply each sequence to both the optimized radix tree and a naive implementation based on an unaugmented ordered map that serves as the specification of the intended behavior.
|
||||
After libfuzzer generates inputs with broad code coverage, we use libfuzzer's ``corpus minimization'' feature to pare down the test inputs without losing coverage (as measured by libfuzzer) into a fixed set of tests short enough that it's feasible to run interactively during development.
|
||||
In order to keep these test inputs short, we constrain the size of keys at the loss of some generality.
|
||||
We believe there isn't anything in the implementation particularly sensitive to the exact length of keys \footnote{\texttt{longestCommonPrefix} is a possible exception, but its length sensitivity is well encapsulated}.
|
||||
We believe there isn't anything in the implementation particularly sensitive to the exact length of keys \footnote{\texttt{longestCommonPrefix} (a routine in the implementation) is a possible exception, but its length sensitivity is well encapsulated}.
|
||||
Libfuzzer's minimized corpus achieves 98\% line coverage on its own.
|
||||
We regenerate the corpus on an ad hoc basis by running libfuzzer for a few cpu-hours, during which it tests millions of unique inputs.
|
||||
|
||||
@@ -210,11 +211,11 @@ Therefore we also run the test inputs linking directly to the final release arti
|
||||
When testing the final artifacts, we do not assert internal invariants as we lack convenient access to the internals.
|
||||
As a defense against possible bugs in compilers' sanitizer and optimizer passes \cite{10.1145/3591257}, we also test with sanitizers enabled and optimizations disabled, and test with both clang and gcc.
|
||||
|
||||
We audited the 2\% of lines that were not covered by libfuzzer \footnote{In order to see the uncovered lines for yourself, exclude all tests containing the word ``script'' with \texttt{ctest -E script}. Look in \texttt{Jenkinsfile} for an example of how to measure coverage.} and found the following:
|
||||
We audited the 2\% of lines that were not covered by libfuzzer \footnote{In order to see the uncovered lines for yourself, exclude all tests containing the word ``script'' with \texttt{ctest -E script}. Look in \texttt{Jenkinsfile} in the root of the source tree for an example of how to measure coverage.} and found the following:
|
||||
\begin{itemize}
|
||||
\item Three occurrences which can be reached from an input that libfuzzer could theoretically generate. In each case the uncovered code is straightforward, and is exercised from an entry point by a manually written test.
|
||||
\item One occurrence which requires a large number of operations, and cannot be reached from an input satisfying the size constraints we impose on libfuzzer. This code is also straightforward, and is exercised from an entry point by a manually written test. The purpose of this code is to keep memory usage in check, and so it's expected that it cannot be reached without a large number of operations.
|
||||
\item One occurrence which is not reachable from any entry point, but is exercised when asserting internal invariants. This line is now suppressed with an explanatory comment.
|
||||
\item One occurrence which is not reachable from any entry point. This line is now suppressed with an explanatory comment.
|
||||
\end{itemize}
|
||||
|
||||
We assert 100\% line coverage in continuous integration, which is achieved with a few caveats.
|
||||
|
Reference in New Issue
Block a user