We provide a new specification for concurrent maps with version control
Conflicting writes are handled by a novel resource algebra for the $\textsf{argmax}$ operation
Skip Lists
(continue below)
Overview
A skip list contains two sentinel nodes with keys MIN and MAX, linked in HMAX lists
Each list is a sublist of its lower level and all lists are sorted by key
Elements can be skipped by searching the lists in the higher levels
JellyFish
The JellyFish skip list implements a map, storing key-value pairs
Each node contains a vertical list, a timeline with timestamped values
To append a new value, the timestamp must be at least as recent as the head’s
Concurrent Updates
Updating threads employ lazy synchronization through locks
Traversal is done until the bottom, locking the key’s predecessor
If the key does not exist, then a new node is linked to a random number of levels
The lock is released after insertion, locking the predecessor in the next level
Insertions are done bottom-up to maintain the sublist relation
Updates follow the same initial steps, locking the key’s predecessor in the bottom level
If a node already exists for the key, then the new value will be appended to the vertical list
Reasoning about Timestamped Domains
(continue below)
Ghost state in Iris
Ghost state matches the physical state of shared data with an abstract state
For concurrent updates to be proven consistent, ghost state is defined as a resource algebra
Two elements define a resource algebra (RA):
The operator is commutative and associative, making the order of operations irrelevant
Consider the following ghost variable $a$, which is composed by $f$ and $a^\prime$
We can split the ghost variable into two separate ghost resources …
… and perform an update on one of them using the RA operator
The resources can be joined to obtain the composition of all elements
The elements can be composed in any order, due to commutativity and associativity
The local update is equivalent to updating the original decomposed variable
$a$
$ \ \gamma $
Domain: type of the ghost variable
Operator: splits, joins and updates variables
$ a \cdot b = b \cdot a \qquad \qquad (a \cdot b) \cdot c = a \cdot (b \cdot c)$
$a$
$ \ \gamma $$f \cdot a^\prime$
$ \ \gamma $$f$
$ \ \gamma $$*$$a^\prime$
$ \ \gamma $$f$
$ \ \gamma $$*$$a^\prime \cdot x$
$ \ \gamma $$f \cdot a^\prime \cdot x$
$ \ \gamma $$a \cdot x$
$ \ \gamma $
Map Composition
We need to consider map composition to abstract the physical state of JellyFish
For different keys, the combined map will contain both key-value pairs
But what happens when both threads associate different values to the same key?
The key will be associated to the composition of both values using another RA
$ M_1 \cdot M_2 = M_1 \cup M_2 $
$ \{ \ k_1 : x \ \} \cup \{ \ k_2 : y \ \} $
$ \{ \ k : x \ \} \cup \{ \ k : y \ \} $
$ \{ \ k : x \cdot y \ \} $
Value Composition
Keys are associated to their most recent value, which will have the greatest timestamp
If $i < j$, then the pair with value $a$ is discarded, returning the pair with value $b$
For equal timestamps, both values are possible, depending on the scheduler
As a result, the abstract map associates each key to a set of possible values
A unit element is also necessary to apply the update rules on maps
The $\textsf{argmax}$ operator returns that value
$ (a, i) \cdot (b, j) = (b, j) $
$ (a, i) \cdot (b, i) = (a \cup b, i) $
$ (a, i) \cdot \textsf{botZ} = (a, i) $
Map Specification
(continue below)
Map Resources
We define the following representation predicate:
$p$ is the left sentinel pointer
$M$ reflects partial knowledge of the map
$q$ is a fraction between $0$ and $1$
$\gamma$ refers to the required ghost state
These resources can be split into smaller fractions …
… and combined to obtain the larger fractions
$ \textsf{IsSkipList}(p, M, q, \gamma) $
$ \textsf{IsSkipList}(\textcolor{red}{p}, M, q, \gamma) $
$ \textsf{IsSkipList}(p, \textcolor{red}{M}, q, \gamma) $
$ \textsf{IsSkipList}(p, M, \textcolor{red}{q}, \gamma) $
$ \textsf{IsSkipList}(p, M, q, \textcolor{red}{\gamma}) $
$ \textsf{IsSkipList}(p, M_1 \cup M_2, q_1 + q_2, \gamma) $
The Hoare triple for $\textsf{new}$ is straightforward
No resources are needed as a precondition …
… and we obtain the full fraction of an empty map
$\textsf{put}$ receives a key, value and timestamp
Holding partial knowledge of the map, …
… $\textsf{put}$ updates the map with the new value
Concurrent updates are handled by the RAs
$ \left\{ \ \textsf{IsSkipList}(p, M, q, \gamma) \ \right\} \\ $
$ \textsf{put} \ p \ k \ v \ t \\ $
$ \left\{ \ \textsf{IsSkipList}(p, M \cup \{ \ k : (\{ v \}, t) \ \}, q, \gamma) \ \right\} $
Triple for lookups
$\textsf{get}$ performs a search for a key
We consider exclusive ownership of the map
The map remains unchanged after the search
The result is empty if the key is not in the map
Otherwise, it must be one of the values in the map
The parameter $p$ points to the left sentinel node
The pointer must be persistent
The left sentinel must have key $\textsf{MIN}$
$ \exists \ head. \ p \hookrightarrow_\square head * head\textsf{.key} = \textsf{MIN} $
$ \exists \ head. \ \textcolor{red}{p \hookrightarrow_\square head} * head\textsf{.key} = \textsf{MIN} $
$ \exists \ head. \ p \hookrightarrow_\square head * \textcolor{red}{head\textsf{.key} = \textsf{MIN}} $
Iris Invariants
Shared resources are expressed in Iris through invariants
Invariant assertions are kept within a solid border …
… and associated to a given namespace
$ I $
$ \ \mathcal{N} $$ I $
$ \ \mathcal{N} $$ I $
$ \ \textcolor{red}{\mathcal{N}} $
Bottom List
Each level will contain its own invariant describing the level’s linked list
Each invariant will have a unique namespace provided by $\textsf{levelN}$
A unique invariant is defined for the bottom list, describing the full map
The invariant requires the left sentinel node …
… and the ghost names for the bottom level
Each sublist invariant will be defined with the same predicate
The sublist invariant and namespace are parameterized by the current level
The ghost names from the current level and its lower level are both required
$\textsf{IsSkipList}$ is thus defined with …
… the left sentinel assertions, …
… the bottom list invariant, …
… the invariants for all sublists and …
… a ghost variable for a partial view of the map
$ \textsf{IsSkipList}(p, M, q, \gamma) \triangleq $$ \exists \ head. \ p \hookrightarrow_\square head * head\textsf{.key} = \textsf{MIN} $$*$
Partial views are obtained through an authoritative resource algebra
Resources can be either authoritative ($\bullet$) or fragments ($\circ$)
Composition of all fragments yields the authoritative resource
Fragments will serve as partial views for the full authoritative map
Fragment composition is performed by the underlying RA
Fractions indicate whether we have composed all existing fragments
A node’s vertical list should store some value node at its head
The map should associate the node’s key to the value node’s timestamp …
… and to a set of values containing the value from the value node
$ \exists \ v. \ node\textsf{.val} \hookrightarrow_{\frac{1}{2}} v $
$ \exists \ vs. \ M[node\textsf{.key}] = \textsf{Some}(vs, v\textsf{.ts}) $
$ v\textsf{.val} \in vs $
Sortedness
The set is a sorted list, including the sentinels
$ L_\textsf{cat} \triangleq [head] +\kern-1.3ex+\kern0.8ex L +\kern-1.3ex+\kern0.8ex [\textsf{tail}] $
Successor Chain
Each physical node should point to its successor in the abstract list
The array entry is mutable and stores a pointer for some successor node
The successor pointer is immutable and must point to the corresponding successor
$ \textsf{IsNext}(lvl, pred, succ) \triangleq $
$ \exists \ s. \ pred\textsf{.next}[lvl] \hookrightarrow_{\frac{1}{2}} s $
$ * \ s \hookrightarrow_\square succ $
Lock Resources
Each node holds a lock for the given level, along with the corresponding lock invariant
The lock resources include the node’s successor and the successor’s value
Both pointers are fractional to allow read access to threads which have not acquired the lock
$\textsf{BotListInv}$ is thus defined with …
… the authoritative map, …
… the authoritative set, …
… their node-wise equivalence, …
… the sorted list, …
… the successor chain and the lock resources …
… and a set of tokens for the sublist relation
$ \textsf{BotListInv}(head, \gamma) \triangleq \exists \ M, S, L. $
$ \bullet \ M $
$ \ \gamma_F^{\phantom{0}} $$ * $
$ M\textsf{.keys} = S\textsf{.keys} \ * $
$ \textsf{KeyRange} \setminus S\textsf{.keys} $
$ \ \gamma_T^{\phantom{0}} $$*$$ \bullet \ S $
$ \ \gamma_A^{\phantom{0}} $$*$$ S \equiv_P L * \ \textsf{Sorted}(L_\textsf{cat}) \ * $
For two consecutive levels, the upper level must be a sublist of the lower level
As such, upper level nodes keep fragments of the lower level’s authoritative set
Ghost Tokens
Upon insertion, the code does not check if the key already exists in the upper levels
Each level will hold a set of ghost tokens to be removed and associated with upper level nodes
Tokens are exclusive, so any new node will have a different token than other existing nodes
Lock Resources
The lock resources no longer include the successor’s value
$ \textsf{InSubLock}(n, lvl) \triangleq \exists \ s. \ n\textsf{.next}[lvl] \hookrightarrow_{\frac{1}{2}} s $
Invariant Definition
$\textsf{SublistInv}$ is thus defined with …
… the authoritative set and sorted list, …
… the successor chain and the lock resources, …
… the set of available tokens …
… and the fragments and tokens of each node
$\textsf{udpate}$ controls which values are appended to vertical lists
Ownership of the initial value is required
The update does not occur if the new timestamp is less recent than the head’s timestamp
Otherwise, the value is appended to the vertical list
The chain is made persistent, yielding an immutable history of values
$$ \{ \ ... \ node\textsf{.val} \hookrightarrow_{\frac{1}{2}} val \ ... \ \} $$
$$ \ \textsf{update} \ node \ v \ t \ $$
$ \left\{ \ ... \
\begin{matrix}
\textbf{\textsf{if}} \ t < val\textsf{.ts} \ \textbf{\textsf{then}} \ node\textsf{.val} \hookrightarrow_{\frac{1}{2}} val \\
\phantom{\textbf{\textsf{else}} \ \exists \ p. \ node\textsf{.val} \hookrightarrow_{\frac{1}{2}} (v, t, p) * p \hookrightarrow_\square val}
\end{matrix}
\ ... \ \right\} $
$ \left\{ \ ... \
\begin{matrix}
\textbf{\textsf{if}} \ t < val\textsf{.ts} \ \textbf{\textsf{then}} \ node\textsf{.val} \hookrightarrow_{\frac{1}{2}} val \\
\textbf{\textsf{else}} \ \exists \ p. \ node\textsf{.val} \hookrightarrow_{\frac{1}{2}} (v, t, p) * p \hookrightarrow_\square val
\end{matrix}
\ ... \ \right\} $
In their work, they present Polaris, an extension of Iris to support probabilistic reasoning.
In Polaris, they proved correctness and probabilistic properties of a 2-level concurrent skip list
We generalize their correctness proofs to any number levels, simplifying the required ghost state
Logical Atomicity
Logically atomic triples state that the operation takes place at a single atomic step
Non-atomic operations can be seen as atomic, whose changes are caused by that atomic step
$ \left\langle \ P \ \right\rangle $
$ \ e \ $
$ \left\langle \ v. \ Q(v) \ \right\rangle $
Key-Value Specifications
From a logically atomic map specification, Xiong et al. (ESOP 2017) derive a key-value specification
Such specifications allow reasoning about individual keys rather than the entire state of the map
Any algebra (e.g., our $\textsf{argmax}$ RA) can be built on top of the specification to reason about clients
$ \left\langle \ \textsf{Key}(p, k, v_i^?, \gamma) \ \right\rangle $
$ \ \textsf{put} \ p \ k \ v \ t \ $
$ \left\langle \ \textsf{Key}(p, k, v_i^? \cdot \textsf{Some}(v, t), \gamma) \ \right\rangle $
Conclusion
(continue below)
Future Work
Verifying other skip list implementations (e.g., the original JellyFish)
Proving logical atomicity for the map operations of lazy JellyFish
Deriving a key-value specification from the logically atomic map specification
Constructing new RAs for verifying other types of clients (e.g., producer-consumer)