-
Notifications
You must be signed in to change notification settings - Fork 200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Pluscal version of the key-value store with snapshot isolation,… #84
Changes from 4 commits
245ef9f
fbaf36a
013bdd1
386b1f1
f4f5984
adb3d2a
8687adf
94fa1b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
--------------------------- MODULE ClientCentric --------------------------- | ||
EXTENDS Naturals, TLC, Sequences, FiniteSets, Util | ||
VARIABLES Keys, Values | ||
\* TODO InitValue could be bottom (_|_) | ||
|
||
\* TLA+ specifications of Client Centric Isolation Specification by Crooks et al: https://dl.acm.org/doi/10.1145/3087801.3087802 | ||
\* TLA+ specifications by Tim Soethout ([email protected]) | ||
|
||
\* A database `State` is represented by keys with corresponding values | ||
State == [Keys -> Values] | ||
|
||
\* An `Operation` is a read or write of a key and value | ||
Operation == [op: {"read", "write"}, key: Keys, value: Values] | ||
|
||
\* Helpers representing Reads and Writes | ||
r(k,v) == [op |-> "read", key |-> k, value |-> v] | ||
w(k,v) == [op |-> "write", key |-> k, value |-> v] | ||
|
||
\* A `Transaction` is a total order of `Operation`s | ||
\* Transaction == [ops: Seq(Operation), start: TimeStamp, commit: TimeStamp] | ||
Transaction == Seq(Operation) | ||
\* For simplicity we store start and commit in a lookup function | ||
TimeStamp == Nat | ||
TransactionTimes == [t \in Transaction |-> [start: TimeStamp, commit: TimeStamp]] | ||
|
||
\* "An execution e for a set of transactions | ||
\* T is a totally ordered set defined by the pair (Se,−−t \in T−→), | ||
\* where Se is the set of states generated by applying, | ||
\* starting from the system’s initial state, a permutation of all the transactions in T ." | ||
ExecutionElem == [parentState: State, transaction: Transaction] | ||
\* resultState is the parentState of the next transaction, but not used in the isolation definitions. | ||
\* ExecutionElem == [parentState: State, transaction: Transaction, resultState: State] | ||
\* We represent an `Execution` as a sequence of `Transaction`s with their corresponding parent state. | ||
\* Note: This execution does therefor not contain the "final state" of the execution, since it is not a parent state of a transaction. | ||
Execution == Seq(ExecutionElem) | ||
|
||
\* Seq | ||
executionStates(execution) == [ i \in 1..Len(execution) |-> execution[i].parentState ] | ||
\* Set | ||
executionTransactions(execution) == { ep.transaction : ep \in SeqToSet(execution) } | ||
|
||
\* "The parent state is the last state in the execution | ||
\* Definition 1: s -T-> s' ≡ [(k,v) ∈ s ∧ (k,v) \notin s'] => k ∈ W_T /\ w(k,v) ∈ Σ_T => (k,v) ∈ s. | ||
muratdem marked this conversation as resolved.
Show resolved
Hide resolved
|
||
\* We refer to s as the parent state of T (denoted as sp,T ); to the | ||
\* transaction that generated s as Ts ; and to the set of keys in which | ||
\* s and s′ differ as ∆(s,s′)" | ||
parentState(execution, transaction) == | ||
LET ind == CHOOSE i \in 1..Len(execution): execution[i].transaction = transaction | ||
IN execution[ind].parentState | ||
|
||
\* w(k,v) -to-> r(k,v) | ||
\* check reads and writes, implicit because of "write" check in ReadStates | ||
earlierInTransaction(transaction, op1, op2) == Index(transaction, op1) < Index(transaction, op2) | ||
|
||
\* state1 -*-> state2 | ||
beforeOrEqualInExecution(execution, state1, state2) == | ||
LET states == executionStates(execution) | ||
IN Index(states, state1) <= Index(states, state2) | ||
|
||
\* Read states: from which states can the operation read it's value | ||
ReadStates(execution, operation, transaction) == | ||
LET Se == SeqToSet(executionStates(execution)) | ||
sp == parentState(execution, transaction) | ||
IN { s \in Se: | ||
/\ beforeOrEqualInExecution(execution, s, sp) \* s -*-> s_p: restrict the valid read states for the operations in T to be no later than sp | ||
/\ \/ s[operation.key] = operation.value \* (k,v) \in s | ||
\/ \E write \in SeqToSet(transaction): | ||
/\ write.op = "write" /\ write.key = operation.key /\ write.value = operation.value | ||
/\ earlierInTransaction(transaction, write, operation) \* w(k,v)-to->r(k,v) | ||
\* "By convention, write operations have read states too: for a write operation in T , they include all states in Se up to and including T ’s parent state." | ||
\/ operation.op = "write" | ||
} | ||
|
||
Preread(execution, transaction) == | ||
\A operation \in SeqToSet(transaction): ReadStates(execution, operation, transaction) /= {} | ||
|
||
PrereadAll(execution, transactions) == | ||
\A transaction \in transactions: Preread(execution, transaction) | ||
|
||
\* A state `s` is complete for `T` in `e` if every operation in `T` can read from `s` | ||
Complete(execution, transaction, state) == | ||
LET setOfAllReadStatesOfOperation(transactionOperations) == | ||
{ ReadStates(execution, operation, transaction) : operation \in SeqToSet(transactionOperations) } | ||
\* also include all states for when the transaction contains no operations | ||
readStatesForEmptyTransaction == { s \in SeqToSet(executionStates(execution)) : beforeOrEqualInExecution(execution, s, parentState(execution, transaction)) } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see how this definition (or its use) is restricted to the case "when the transaction contains no operation"? Perhaps rephrase the comment? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ClientCentric is not my module, written by Tim, so I am reluctant to make any change there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. /cc @TimSoethout There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I remember correctly, this is where I deviated from the original definitions in the paper by Crooks (https://www.cs.cornell.edu/lorenzo/papers/Crooks17Seeing.pdf). The comment is mainly there to upgrade the definition to also allow transactions with an empty operation set, while still satisfying the isolation level. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Has this conversation been resolved? If yes, it looks like this PR is ready to be merged. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I certainly have no objection to merging the PR, although I don't think that the wording of the comment has been changed (Tim's suggestion clarifies the intent of the definition IMHO). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @muratdem can you do the finishing touches (wording of this comment + removal There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I will also pull some global variables to be process local. |
||
IN state \in INTERSECTION(setOfAllReadStatesOfOperation(transaction) \union { readStatesForEmptyTransaction } ) | ||
|
||
\* "the write set of T comprises the keys that T updates: WT = {k|w(k, v) ∈ ΣT }. | ||
\* For simplicity of exposition, we assume that a transaction only writes a key once." | ||
WriteSet(transaction) == | ||
LET writes == { operation \in SeqToSet(transaction) : operation.op = "write" } | ||
IN { operation.key : operation \in writes } | ||
\* "Denoting the set of keys in which s and s′ differ as ∆(s, s′), we express this as NO-CONF_T (s) ≡ ∆(s, sp) ∩ WT = ∅" | ||
NoConf(execution, transaction, state) == | ||
LET Sp == parentState(execution, transaction) | ||
delta == { key \in DOMAIN Sp : Sp[key] /= state[key] } | ||
IN delta \intersect WriteSet(transaction) = {} | ||
|
||
\* `t1` comes before `t2` in wall clock/oracle time | ||
ComesStrictBefore(t1, t2, timestamps) == timestamps[t1].commit < timestamps[t2].start | ||
|
||
\* Given system state and single transaction (seq of operations), determines new state | ||
effects(state, transaction) == | ||
ReduceSeq(LAMBDA o, newState: IF o.op = "write" THEN [newState EXCEPT ![o.key] = o.value] ELSE newState, transaction, state) | ||
|
||
\* Lists all possible permutations of executions given set of transactions | ||
executions(initialState, transactions) == | ||
\* All possible permutations | ||
LET orderings == PermSeqs(transactions) | ||
\* initialState == [k \in Keys |-> InitValue] \* makes it level-1 therefor pass it in | ||
accummulator == [ execution |-> <<>>, nextState |-> initialState ] | ||
IN { LET executionAcc == ReduceSeq( | ||
\* store ExecutionElem in accumulator | ||
LAMBDA t, acc: [ execution |-> Append(acc.execution, [parentState |-> acc.nextState, transaction |-> t]) | ||
\* calcultate next state | ||
, nextState |-> effects(acc.nextState,t) | ||
], | ||
ordering, accummulator) | ||
\* recover ExecutionElems | ||
IN executionAcc.execution | ||
: ordering \in orderings } | ||
|
||
\* Helper: checks if specific execution satisfies given commit test | ||
executionSatisfiesCT(execution, commitTest(_,_)) == | ||
LET transactions == executionTransactions(execution) | ||
IN \A transaction \in transactions: commitTest(transaction, execution) | ||
|
||
\* tests there exists an execution for `transactions`, that satisfies the isolation level given by `commitTest` | ||
\* "Definition 5 Given a set of transactions T and their read states, | ||
\* a storagesystem satisfies an isolation level I iff ∃e:∀t ∈ T :CTI(t,e)." | ||
satisfyIsolationLevel(initialState, transactions, commitTest(_,_)) == | ||
\E execution \in executions(initialState, transactions): \A transaction \in transactions: | ||
\* PrintT(<<"try execution:",execution>>) => | ||
commitTest(transaction, execution) | ||
|
||
\* Serializability commit test | ||
CT_SER(transaction, execution) == | ||
Complete(execution, transaction, parentState(execution, transaction)) | ||
Serializability(initialState, transactions) == satisfyIsolationLevel(initialState, transactions, CT_SER) | ||
|
||
\*SerializabilityDebug(initialState, transactions) == | ||
\* \* if no executions satify commit test, print all executions | ||
\* \/ (~\E execution \in executions(initialState, transactions): \A transaction \in transactions: | ||
\* CT_SER(transaction, execution)) => \A execution \in executions(initialState, transactions): PrintT(<<"Execution not Serializable:",execution>>) | ||
\* \* fall back to normal check | ||
\* \/ \E execution \in executions(initialState, transactions): \A transaction \in transactions: CT_SER(transaction, execution) | ||
|
||
SerializabilityDebug(initialState, transactions) == | ||
~ Serializability(initialState, transactions) => Print(<<"Executions not Serializable:", executions(initialState, transactions)>>, FALSE) | ||
|
||
\* Snapshot Isolation | ||
CT_SI(transaction, execution) == \E state \in SeqToSet(executionStates(execution)): | ||
Complete(execution, transaction, state) /\ NoConf(execution, transaction, state) | ||
SnapshotIsolation(initialState, transactions) == satisfyIsolationLevel(initialState, transactions, CT_SI) | ||
|
||
\* Strict Serializability: ∀T ∈T:T <s T => s_T′ -*-> s_T. | ||
CT_SSER(timestamps, transaction, execution) == | ||
LET Sp == parentState(execution, transaction) | ||
IN /\ Complete(execution, transaction, Sp) | ||
/\ \A otherTransaction \in executionTransactions(execution): | ||
ComesStrictBefore(otherTransaction, transaction, timestamps) => | ||
beforeOrEqualInExecution(execution, parentState(execution, otherTransaction), Sp) | ||
\* For now inline `satisfyIsolationLevel` instead of `satisfyIsolationLevel(transactions, CT_SSER(timestamps)) because partial functions are not supported/hard` | ||
StrictSerializability(initialState, transactions, timestamps) == | ||
\E execution \in executions(initialState, transactions): \A transaction \in transactions: CT_SSER(timestamps, transaction, execution) | ||
|
||
\* Read Committed | ||
CT_RC(transaction, execution) == Preread(execution, transaction) | ||
ReadCommitted(initialState, transactions) == satisfyIsolationLevel(initialState, transactions, CT_RC) | ||
|
||
\* Read Uncommitted | ||
CT_RU(transaction, execution) == TRUE | ||
ReadUncommitted(initialState, transactions) == satisfyIsolationLevel(initialState, transactions, CT_RU) | ||
|
||
\* Check types in derived specification | ||
TypeOKT(transactions) == | ||
\* /\ InitValue \in Values | ||
/\ transactions \subseteq Transaction | ||
|
||
TypeOK(transactions, execution) == | ||
/\ TypeOKT(transactions) | ||
\* /\ PrintT(State) | ||
/\ execution \in Execution | ||
|
||
============================================================================= |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it useful to declare these as variable rather than constant parameters? If they were constants, all definitions in this module would be constant level as well, which may avoid trouble further down the line. Also note that the parameters are actually instantiated by constants in module KVsnap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ClientCentric is not my module, written by Tim, so I am reluctant to make any change there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/cc @TimSoethout
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The module is instantiated as seen in: https://github.com/tlaplus/Examples/pull/84/files/adb3d2aa3f62812a2becd5c2f6700d69eb1c5134#diff-ccf178cee4a4265b5f8405a78d596e79973a841e20119a281c19775a1ecae139R14
CC == INSTANCE ClientCentric WITH Keys <- Key, Values <- TxId \union {NoVal}
If something similar is possible with constant parameters, and it is more idiomatic TLA, you should definitely go for constant parameters.
FYI I have no issues in changing/updating the code in any way. Glad that your are using it and/or inspired by my work. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instantiation is certainly possible for constant as well as variable parameters. Declaring Keys and Values to be variables allows these sets to change during an execution, which might be interesting for some applications, but I don't know what the original paper had in mind. If they are constants, the module is a constant module (since all operator definitions are of constant level as well), which allows operators to be instantiated at constant, variable or action level. Potential complications with non-constant modules are described in section 17.8 of Specifying Systems. However, my comment was just a caveat for later, there is nothing wrong with the modules as they are written, and they are accepted by TLC.