├── .gitignore ├── README.md ├── SnapshotIsolation.cfg ├── SnapshotIsolation.tla ├── models └── IsConflictSerializable │ ├── MC.cfg │ ├── MC.tla │ └── SnapshotIsolation.tla └── traces └── read_only_anomaly.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | *.toolbox/ 3 | states/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Specification of Snapshot Isolation in TLA+ 2 | 3 | This is a TLA+ specification that can be used for exploring and understanding snapshot isolation. I wrote it partly as a personal exercise and partly as an attempt to share the ideas and semantics of snapshot isolation with other engineers in a precise manner. My goal was to make this spec as simple as possible without removing necessary details. I wanted to focus more on high level concepts than on how a particular system might actually implement snapshot isolation. The comments explain in more detail the structure of the model and the various correctness properties. I drew some inspiration (and a few of the more tricky definitions) from Chris Newcombe's specification of snapshot isolation, which is a bit more complex than mine. He presented a few of his specs in a "Debugging Designs" talk at a [HPTS conference in 2011](http://hpts.ws/papers/2011/agenda.html). His two snapshot isolation specs are very thorough and well documented. 4 | 5 | ## Checking Properties with TLC 6 | 7 | There a few properties already defined in the specification that you can try to verify yourself. Two concurrency anomalies that snapshot isolation allows, Write Skew and a ["read only" transaction anomaly](https://www.cs.umb.edu/~poneil/ROAnom.pdf) are included with examples, and there is a `ReadOnlyAnomaly` property that can be checked using TLC. The main invariant to check is that every history is serializable (which is expected to be violated by anomalous histories). To get started with a TLC model, you can use the following values for spec's `CONSTANT` parameters: 8 | 9 | Empty <- Model value 10 | txnIds <- {t0, t1, t2} (Symmetry set of model values) 11 | keys <- {k1, k2} (Symmetry set of model values) 12 | values <- {v1, v2} (Symmetry set of model values) 13 | 14 | You can choose `Spec` as the temporal formula and set either of the following two expressions as invariants to check: 15 | 16 | *Assert that all histories are serializable:* 17 | 18 | ```tla 19 | IsSerializable(txnHistory) 20 | ``` 21 | 22 | *Assert that there are no non-serializable histories with a read-only transaction anomaly:* 23 | 24 | ```tla 25 | ~ReadOnlyAnomaly(txnHistory) 26 | ``` 27 | 28 | Both of these invariants should be violated in specific cases. By running the model checker you can see what kinds of histories violate these invariants. 29 | 30 | ## Model Checking Statistics 31 | 32 | For reference, I was able to produce a trace that violated the `~ReadOnlyAnomaly(txnHistory)` invariant in 1 hour and 4 minutes running TLC on a 12-core (Intel i7-4930K CPU @ 3.40GHz) Ubuntu Linux workstation. It generated a bit over 405 million distinct states and it took a 12 step trace to violate the invariant. While TLC was running it seemed to produce at least ~80GB of auxiliary data on disk, but I did not measure it precisely. You can see the detailed output of this run below: 33 | 34 | TLC command line parameters: 35 | ``` 36 | % java tlc2.TLC -cleanup -gzip -workers 12 -metadir /hdd/tlc_states -config MC.cfg MC.tla 37 | 38 | TLC2 Version 2.12 of 29 January 2018 39 | Running breadth-first search Model-Checking with 12 workers on 12 cores with 7143MB heap and 64MB offheap memory (Linux 4.8.0-59-generic amd64, Oracle Corporation 1.8.0_131 x86_6 40 | 4). 41 | ``` 42 | 43 | TLC invariant violation output (only last state of trace shown): 44 | 45 | ``` 46 | State 12: 47 | /\ txnSnapshots = ( t0 :> (k1 :> Empty @@ k2 :> v1) @@ 48 | t1 :> (k1 :> v1 @@ k2 :> Empty) @@ 49 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 50 | /\ dataStore = (k1 :> v1 @@ k2 :> v1) 51 | /\ txnHistory = << [type |-> "begin", txnId |-> t0, time |-> 1], 52 | [type |-> "read", txnId |-> t0, key |-> k1, val |-> Empty], 53 | [type |-> "begin", txnId |-> t1, time |-> 2], 54 | [type |-> "write", txnId |-> t1, key |-> k1, val |-> v1], 55 | [type |-> "commit", txnId |-> t1, time |-> 3, updatedKeys |-> {k1}], 56 | [type |-> "begin", txnId |-> t2, time |-> 4], 57 | [type |-> "read", txnId |-> t2, key |-> k1, val |-> v1], 58 | [type |-> "read", txnId |-> t2, key |-> k2, val |-> Empty], 59 | [type |-> "write", txnId |-> t0, key |-> k2, val |-> v1], 60 | [type |-> "commit", txnId |-> t0, time |-> 5, updatedKeys |-> {k2}], 61 | [type |-> "commit", txnId |-> t2, time |-> 6, updatedKeys |-> {}] >> 62 | /\ clock = 6 63 | /\ runningTxns = {} 64 | 65 | 529068865 states generated, 405673796 distinct states found, 357574338 states left on queue. 66 | The depth of the complete state graph search is 13. 67 | The average outdegree of the complete state graph is 8 (minimum is 0, the maximum 17 and the 95th percentile is 12). 68 | Finished in 01h 04min at (2018-02-21 23:41:43) 69 | ``` 70 | 71 | Interestingly, running TLC in simulation mode (using the `-simulate` flag) produces a violating trace in just under 3 minutes, on the same hardware. This speedup may be due to the fact that, to produce this particular anomaly, a sufficiently long trace is required. Searching the state space in a breadth first manner (TLC's default) would require the checking of all possible "short" traces before testing out any longer ones. In fact, running TLC simulation in parallel, with 12 cores, often produced a violating trace in under a minute. It seems that simulation mode may be better at finding "interesting" traces more quickly than standard model checking model, at least for this particular model. 72 | 73 | ``` 74 | % java tlc2.TLC -simulate -cleanup -gzip -workers 12 -metadir /hdd/tlc_states -config MC.cfg MC.tla !10179 75 | TLC2 Version 2.12 of 29 January 2018 76 | Running Random Simulation with seed 6802238540282724400 with 12 workers on 12 cores with 7143MB heap and 64MB offheap memory (Linux 4.8.0-59-generic amd64, Oracle Corporation 1.8 77 | .0_131 x86_64). 78 | 79 | ... 80 | 81 | 82 | State 13: 83 | /\ txnSnapshots = ( t0 :> (k1 :> Empty @@ k2 :> v2) @@ 84 | t1 :> (k1 :> v2 @@ k2 :> Empty) @@ 85 | t2 :> (k1 :> v2 @@ k2 :> Empty) ) 86 | /\ dataStore = (k1 :> v2 @@ k2 :> v2) 87 | /\ txnHistory = << [type |-> "begin", txnId |-> t0, time |-> 1], 88 | [type |-> "begin", txnId |-> t1, time |-> 2], 89 | [type |-> "read", txnId |-> t0, key |-> k1, val |-> Empty], 90 | [type |-> "write", txnId |-> t1, key |-> k1, val |-> v2], 91 | [type |-> "commit", txnId |-> t1, time |-> 3, updatedKeys |-> {k1}], 92 | [type |-> "begin", txnId |-> t2, time |-> 4], 93 | [type |-> "read", txnId |-> t0, key |-> k2, val |-> Empty], 94 | [type |-> "read", txnId |-> t2, key |-> k1, val |-> v2], 95 | [type |-> "read", txnId |-> t2, key |-> k2, val |-> Empty], 96 | [type |-> "write", txnId |-> t0, key |-> k2, val |-> v2], 97 | [type |-> "commit", txnId |-> t2, time |-> 5, updatedKeys |-> {}], 98 | [type |-> "commit", txnId |-> t0, time |-> 6, updatedKeys |-> {k2}] >> 99 | /\ clock = 6 100 | /\ runningTxns = {} 101 | 102 | The number of states generated: 3489180 103 | Simulation using seed 6802238540282724400 and aril 4642022 104 | Progress: 3489180 states checked. 105 | Finished in 02min 42s at (2018-02-24 11:38:10) 106 | ``` 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /SnapshotIsolation.cfg: -------------------------------------------------------------------------------- 1 | INIT Init 2 | NEXT Next 3 | CONSTANT 4 | Empty = Empty 5 | txnIds = {t0, t1, t2} 6 | keys = {k1, k2} 7 | values = {v1, v2} 8 | INVARIANT NoReadOnlyAnomaly 9 | \* INVARIANT IsConflictSerializableInv 10 | SYMMETRY Symmetry -------------------------------------------------------------------------------- /SnapshotIsolation.tla: -------------------------------------------------------------------------------- 1 | ------------------------- MODULE SnapshotIsolation ------------------------- 2 | EXTENDS Naturals, FiniteSets, Sequences, TLC 3 | 4 | (**************************************************************************************************) 5 | (* *) 6 | (* This is a specification of snapshot isolation. It is based on various sources, integrating *) 7 | (* ideas and definitions from: *) 8 | (* *) 9 | (* ``Making Snapshot Isolation Serializable", Fekete et al., 2005 *) 10 | (* https://www.cse.iitb.ac.in/infolab/Data/Courses/CS632/2009/Papers/p492-fekete.pdf *) 11 | (* *) 12 | (* ``Serializable Isolation for Snapshot Databases", Cahill, 2009 *) 13 | (* https://ses.library.usyd.edu.au/bitstream/2123/5353/1/michael-cahill-2009-thesis.pdf *) 14 | (* *) 15 | (* ``A Read-Only Transaction Anomaly Under Snapshot Isolation", Fekete et al. *) 16 | (* https://www.cs.umb.edu/~poneil/ROAnom.pdf *) 17 | (* *) 18 | (* ``Debugging Designs", Chris Newcombe, 2011 *) 19 | (* https://github.com/pron/amazon-snapshot-spec/blob/master/DebuggingDesigns.pdf *) 20 | (* *) 21 | (* This spec tries to model things at a very high level of abstraction, so as to communicate the *) 22 | (* important concepts of snapshot isolation, as opposed to how a system might actually implement *) 23 | (* it. Correctness properties and their detailed explanations are included at the end of this *) 24 | (* spec. We draw the basic definition of snapshot isolation from Definition 1.1 of Fekete's *) 25 | (* "Read-Only" anomaly paper: *) 26 | (* *) 27 | (* *) 28 | (* "...we assume time is measured by a counter that advances whenever any *) 29 | (* transaction starts or commits, and we designate the time when transaction Ti starts as *) 30 | (* start(Ti) and the time when Ti commits as commit(Ti). *) 31 | (* *) 32 | (* Definition 1.1: Snapshot Isolation (SI). A transaction Ti executing under SI conceptually *) 33 | (* reads data from the committed state of the database as of time start(Ti) (the snapshot), and *) 34 | (* holds the results of its own writes in local memory store, so if it reads data it has written *) 35 | (* it will read its own output. Predicates evaluated by Ti are also based on rows and index *) 36 | (* entry versions from the committed state of the database at time start(Ti), adjusted to take *) 37 | (* Ti's own writes into account. Snapshot Isolation also must obey a "First Committer (Updater) *) 38 | (* Wins" rule...The interval in time from the start to the commit of a transaction, represented *) 39 | (* [Start(Ti), Commit(Ti)], is called its transactional lifetime. We say two transactions T1 and *) 40 | (* T2 are concurrent if their transactional lifetimes overlap, i.e., [start(T1), commit(T1)] ∩ *) 41 | (* [start(T2), commit(T2)] ≠ Φ. Writes by transactions active after Ti starts, i.e., writes by *) 42 | (* concurrent transactions, are not visible to Ti. When Ti is ready to commit, it obeys the *) 43 | (* First Committer Wins rule, as follows: Ti will successfully commit if and only if no *) 44 | (* concurrent transaction Tk has already committed writes (updates) of rows or index entries that *) 45 | (* Ti intends to write." *) 46 | (* *) 47 | (**************************************************************************************************) 48 | 49 | 50 | (**************************************************************************************************) 51 | (* The constant parameters of the spec. *) 52 | (**************************************************************************************************) 53 | 54 | \* Set of all transaction ids. 55 | CONSTANT txnIds 56 | 57 | \* Set of all data store keys/values. 58 | CONSTANT keys, values 59 | 60 | \* An empty value. 61 | CONSTANT Empty 62 | 63 | (**************************************************************************************************) 64 | (* The variables of the spec. *) 65 | (**************************************************************************************************) 66 | 67 | \* The clock, which measures 'time', is just a counter, that increments (ticks) 68 | \* whenever a transaction starts or commits. 69 | VARIABLE clock 70 | 71 | \* The set of all currently running transactions. 72 | VARIABLE runningTxns 73 | 74 | \* The full history of all transaction operations. It is modeled as a linear 75 | \* sequence of events. Such a history would likely never exist in a real implementation, 76 | \* but it is used in the model to check the properties of snapshot isolation. 77 | VARIABLE txnHistory 78 | 79 | \* (NOT NECESSARY) 80 | \* The key-value data store. In this spec, we model a data store explicitly, even though it is not actually 81 | \* used for the verification of any correctness properties. This was added initially as an attempt the make the 82 | \* spec more intuitive and understandable. It may play no important role at this point, however. If a property 83 | \* check was ever added for view serializability, this, and the set of transaction snapshots, may end up being 84 | \* useful. 85 | VARIABLE dataStore 86 | 87 | \* (NOT NECESSARY) 88 | \* The set of snapshots needed for all running transactions. Each snapshot 89 | \* represents the entire state of the data store as of a given point in time. 90 | \* It is a function from transaction ids to data store snapshots. This, like the 'dataStore' variable, may 91 | \* now be obsolete for a spec at this level of abstraction, since the correctness properties we check do not 92 | \* depend on the actual data being read/written. 93 | VARIABLE txnSnapshots 94 | 95 | vars == <> 96 | 97 | 98 | (**************************************************************************************************) 99 | (* Data type definitions. *) 100 | (**************************************************************************************************) 101 | 102 | DataStoreType == [keys -> (values \cup {Empty})] 103 | BeginOpType == [type : {"begin"} , txnId : txnIds , time : Nat] 104 | CommitOpType == [type : {"commit"} , txnId : txnIds , time : Nat, updatedKeys : SUBSET keys] 105 | WriteOpType == [type : {"write"} , txnId : txnIds , key: SUBSET keys , val : SUBSET values] 106 | ReadOpType == [type : {"read"} , txnId : txnIds , key: SUBSET keys , val : SUBSET values] 107 | AnyOpType == UNION {BeginOpType, CommitOpType, WriteOpType, ReadOpType} 108 | 109 | (**************************************************************************************************) 110 | (* The type invariant and initial predicate. *) 111 | (**************************************************************************************************) 112 | 113 | TypeInvariant == 114 | \* /\ txnHistory \in Seq(AnyOpType) seems expensive to check with TLC, so disable it. 115 | /\ dataStore \in DataStoreType 116 | /\ txnSnapshots \in [txnIds -> (DataStoreType \cup {Empty})] 117 | /\ runningTxns \in SUBSET [ id : txnIds, 118 | startTime : Nat, 119 | commitTime : Nat \cup {Empty}] 120 | 121 | Init == 122 | /\ runningTxns = {} 123 | /\ txnHistory = <<>> 124 | /\ clock = 0 125 | /\ txnSnapshots = [id \in txnIds |-> Empty] 126 | /\ dataStore = [k \in keys |-> Empty] 127 | 128 | (**************************************************************************************************) 129 | (* Helpers for querying transaction histories. *) 130 | (* *) 131 | (* These are parameterized on a transaction history and a transaction id, if applicable. *) 132 | (**************************************************************************************************) 133 | 134 | \* Generic TLA+ helper. 135 | Range(f) == {f[x] : x \in DOMAIN f} 136 | 137 | \* The begin or commit op for a given transaction id. 138 | BeginOp(h, txnId) == CHOOSE op \in Range(h) : op.txnId = txnId /\ op.type = "begin" 139 | CommitOp(h, txnId) == CHOOSE op \in Range(h) : op.txnId = txnId /\ op.type = "commit" 140 | 141 | \* The set of all committed/aborted transaction ids in a given history. 142 | CommittedTxns(h) == {op.txnId : op \in {op \in Range(h) : op.type = "commit"}} 143 | AbortedTxns(h) == {op.txnId : op \in {op \in Range(h) : op.type = "abort"}} 144 | 145 | \* The set of all read or write ops done by a given transaction. 146 | ReadsByTxn(h, txnId) == {op \in Range(h) : op.txnId = txnId /\ op.type = "read"} 147 | WritesByTxn(h, txnId) == {op \in Range(h) : op.txnId = txnId /\ op.type = "write"} 148 | 149 | \* The set of all keys read or written to by a given transaction. 150 | KeysReadByTxn(h, txnId) == { op.key : op \in ReadsByTxn(txnHistory, txnId)} 151 | KeysWrittenByTxn(h, txnId) == { op.key : op \in WritesByTxn(txnHistory, txnId)} 152 | 153 | \* The index of a given operation in the transaction history sequence. 154 | IndexOfOp(h, op) == CHOOSE i \in DOMAIN h : h[i] = op 155 | 156 | RunningTxnIds == {txn.id : txn \in runningTxns} 157 | 158 | (**************************************************************************************************) 159 | (* *) 160 | (* Action Definitions *) 161 | (* *) 162 | (**************************************************************************************************) 163 | 164 | 165 | (**************************************************************************************************) 166 | (* When a transaction starts, it gets a new, unique transaction id and is added to the set of *) 167 | (* running transactions. It also "copies" a local snapshot of the data store on which it will *) 168 | (* perform its reads and writes against. In a real system, this data would not be literally *) 169 | (* "copied", but this is the fundamental concept of snapshot isolation i.e. that each *) 170 | (* transaction appears to operate on its own local snapshot of the database. *) 171 | (**************************************************************************************************) 172 | StartTxn(newTxnId) == 173 | LET newTxn == 174 | [ id |-> newTxnId, 175 | startTime |-> clock+1, 176 | commitTime |-> Empty] IN 177 | \* Must choose an unused transaction id. There must be no other operation 178 | \* in the history that already uses this id. 179 | /\ ~\E op \in Range(txnHistory) : op.txnId = newTxnId 180 | \* Save a snapshot of current data store for this transaction, and 181 | \* and append its 'begin' event to the history. 182 | /\ txnSnapshots' = [txnSnapshots EXCEPT ![newTxnId] = dataStore] 183 | /\ LET beginOp == [ type |-> "begin", 184 | txnId |-> newTxnId, 185 | time |-> clock+1 ] IN 186 | txnHistory' = Append(txnHistory, beginOp) 187 | \* Add transaction to the set of active transactions. 188 | /\ runningTxns' = runningTxns \cup {newTxn} 189 | \* Tick the clock. 190 | /\ clock' = clock + 1 191 | /\ UNCHANGED <> 192 | 193 | 194 | (**************************************************************************************************) 195 | (* When a transaction T0 is ready to commit, it obeys the "First Committer Wins" rule. T0 will *) 196 | (* only successfully commit if no concurrent transaction has already committed writes of data *) 197 | (* objects that T0 intends to write. Transactions T0, T1 are considered concurrent if the *) 198 | (* intersection of their timespans is non empty i.e. *) 199 | (* *) 200 | (* [start(T0), commit(T0)] \cap [start(T1), commit(T1)] != {} *) 201 | (**************************************************************************************************) 202 | 203 | \* Checks whether a given transaction is allowed to commit, based on whether it conflicts 204 | \* with other concurrent transactions that have already committed. 205 | TxnCanCommit(txnId) == 206 | \E txn \in runningTxns : 207 | /\ txn.id = txnId 208 | /\ ~\E op \in Range(txnHistory) : 209 | /\ op.type = "commit" 210 | \* Did another transaction start after me. 211 | /\ txn.id = txnId /\ op.time > txn.startTime 212 | /\ KeysWrittenByTxn(txnHistory, txnId) \cap op.updatedKeys /= {} \* Must be no conflicting keys. 213 | 214 | CommitTxn(txnId) == 215 | \* Transaction must be able to commit i.e. have no write conflicts with concurrent. 216 | \* committed transactions. 217 | /\ txnId \in RunningTxnIds 218 | \* Must not be a no-op transaction. 219 | /\ (WritesByTxn(txnHistory, txnId) \cup ReadsByTxn(txnHistory, txnId)) /= {} 220 | /\ TxnCanCommit(txnId) 221 | /\ LET commitOp == [ type |-> "commit", 222 | txnId |-> txnId, 223 | time |-> clock + 1, 224 | updatedKeys |-> KeysWrittenByTxn(txnHistory, txnId)] IN 225 | txnHistory' = Append(txnHistory, commitOp) 226 | \* Merge this transaction's updates into the data store. If the 227 | \* transaction has updated a key, then we use its version as the new 228 | \* value for that key. Otherwise the key remains unchanged. 229 | /\ dataStore' = [k \in keys |-> IF k \in KeysWrittenByTxn(txnHistory, txnId) 230 | THEN txnSnapshots[txnId][k] 231 | ELSE dataStore[k]] 232 | \* Remove the transaction from the active set. 233 | /\ runningTxns' = {r \in runningTxns : r.id # txnId} 234 | /\ clock' = clock + 1 235 | \* We can leave the snapshot around, since it won't be used again. 236 | /\ UNCHANGED <> 237 | 238 | (**************************************************************************************************) 239 | (* In this spec, a transaction aborts if and only if it cannot commit, due to write conflicts. *) 240 | (**************************************************************************************************) 241 | AbortTxn(txnId) == 242 | \* If a transaction can't commit due to write conflicts, then it 243 | \* must abort. 244 | /\ txnId \in RunningTxnIds 245 | \* Must not be a no-op transaction. 246 | /\ (WritesByTxn(txnHistory, txnId) \cup ReadsByTxn(txnHistory, txnId)) /= {} 247 | /\ ~TxnCanCommit(txnId) 248 | /\ LET abortOp == [ type |-> "abort", 249 | txnId |-> txnId, 250 | time |-> clock + 1] IN 251 | txnHistory' = Append(txnHistory, abortOp) 252 | /\ runningTxns' = {r \in runningTxns : r.id # txnId} \* transaction is no longer running. 253 | /\ clock' = clock + 1 254 | \* No changes are made to the data store. 255 | /\ UNCHANGED <> 256 | 257 | (***************************************************************************************************) 258 | (* Read and write operations executed by transactions. *) 259 | (* *) 260 | (* As a simplification, and to limit the size of potential models, we allow transactions to only *) 261 | (* read or write to the same key once. The idea is that it limits the state space without loss *) 262 | (* of generality. *) 263 | (**************************************************************************************************) 264 | 265 | TxnRead(txnId, k) == 266 | \* Read from this transaction's snapshot. 267 | /\ txnId \in RunningTxnIds 268 | /\ LET valRead == txnSnapshots[txnId][k] 269 | readOp == [ type |-> "read", 270 | txnId |-> txnId, 271 | key |-> k, 272 | val |-> valRead] IN 273 | /\ k \notin KeysReadByTxn(txnHistory, txnId) 274 | /\ txnHistory' = Append(txnHistory, readOp) 275 | /\ UNCHANGED <> 276 | 277 | TxnUpdate(txnId, k, v) == 278 | /\ txnId \in RunningTxnIds 279 | /\ LET writeOp == [ type |-> "write", 280 | txnId |-> txnId, 281 | key |-> k, 282 | val |-> v] IN 283 | /\ k \notin KeysWrittenByTxn(txnHistory, txnId) 284 | \* We update the transaction's snapshot, not the actual data store. 285 | /\ LET updatedSnapshot == [txnSnapshots[txnId] EXCEPT ![k] = v] IN 286 | txnSnapshots' = [txnSnapshots EXCEPT ![txnId] = updatedSnapshot] 287 | /\ txnHistory' = Append(txnHistory, writeOp) 288 | /\ UNCHANGED <> 289 | 290 | (**************************************************************************************************) 291 | (* The next-state relation and spec definition. *) 292 | (* *) 293 | (* Since it is desirable to have TLC check for deadlock, which may indicate bugs in the spec or *) 294 | (* in the algorithm, we want to explicitly define what a "valid" termination state is. If all *) 295 | (* transactions have run and either committed or aborted, we consider that valid termination, and *) 296 | (* is allowed as an infinite suttering step. *) 297 | (* *) 298 | (* Also, once a transaction knows that it cannot commit due to write conflicts, we don't let it *) 299 | (* do any more reads or writes, so as to eliminate wasted operations. That is, once we know a *) 300 | (* transaction can't commit, we force its next action to be abort. *) 301 | (**************************************************************************************************) 302 | 303 | AllTxnsFinished == AbortedTxns(txnHistory) \cup CommittedTxns(txnHistory) = txnIds 304 | 305 | Next == 306 | \/ \E tid \in txnIds : StartTxn(tid) 307 | \* Ends a given transaction by either committing or aborting it. To exclude uninteresting 308 | \* histories, we require that a transaction does at least one operation before committing or aborting. 309 | \* Assumes that the given transaction is currently running. 310 | \/ \E tid \in txnIds : CommitTxn(tid) 311 | \/ \E tid \in txnIds : AbortTxn(tid) 312 | \* Transaction reads or writes a key. We limit transactions 313 | \* to only read or write the same key once. 314 | \/ \E tid \in txnIds, k \in keys : TxnRead(tid, k) 315 | \/ \E tid \in txnIds, k \in keys, v \in values : TxnUpdate(tid, k, v) 316 | \/ (AllTxnsFinished /\ UNCHANGED vars) 317 | 318 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 319 | 320 | 321 | ---------------------------------------------------------------------------------------------------- 322 | 323 | 324 | (**************************************************************************************************) 325 | (* *) 326 | (* Correctness Properties and Tests *) 327 | (* *) 328 | (**************************************************************************************************) 329 | 330 | 331 | 332 | (**************************************************************************************************) 333 | (* Operator for computing cycles in a given graph, defined by a set of edges. *) 334 | (* *) 335 | (* Returns a set containing all elements that participate in any cycle (i.e. union of all *) 336 | (* cycles), or an empty set if no cycle is found. *) 337 | (* *) 338 | (* Source: *) 339 | (* https://github.com/pron/amazon-snapshot-spec/blob/master/serializableSnapshotIsolation.tla. *) 340 | (**************************************************************************************************) 341 | FindAllNodesInAnyCycle(edges) == 342 | 343 | LET RECURSIVE findCycleNodes(_, _) (* startNode, visitedSet *) 344 | (* Returns a set containing all elements of some cycle starting at startNode, 345 | or an empty set if no cycle is found. 346 | *) 347 | findCycleNodes(node, visitedSet) == 348 | IF node \in visitedSet THEN 349 | {node} (* found a cycle, which includes node *) 350 | ELSE 351 | LET newVisited == visitedSet \union {node} 352 | neighbors == {to : <> \in 353 | {<> \in edges : from = node}} 354 | IN (* Explore neighbors *) 355 | UNION {findCycleNodes(neighbor, newVisited) : neighbor \in neighbors} 356 | 357 | startPoints == {from : <> \in edges} (* All nodes with an outgoing edge *) 358 | IN 359 | UNION {findCycleNodes(node, {}) : node \in startPoints} 360 | 361 | IsCycle(edges) == FindAllNodesInAnyCycle(edges) /= {} 362 | 363 | 364 | 365 | (**************************************************************************************************) 366 | (* *) 367 | (* Verifying Serializability *) 368 | (* *) 369 | (* --------------------------------------- *) 370 | (* *) 371 | (* For checking serializability of transaction histories we use the "Conflict Serializability" *) 372 | (* definition. This is slightly different than what is known as "View Serializability", but is *) 373 | (* suitable for verification, since it is efficient to check, whereas checking view *) 374 | (* serializability of a transaction schedule is known to be NP-complete. *) 375 | (* *) 376 | (* The definition of conflict serializability permits a more limited set of transaction *) 377 | (* histories. Intuitively, it can be viewed as checking whether a given schedule has the *) 378 | (* "potential" to produce a certain anomaly, even if the particular data values for a history *) 379 | (* make it serializable. Formally, we can think of the set of conflict serializable histories as *) 380 | (* a subset of all possible serializable histories. Alternatively, we can say that, for a given *) 381 | (* history H ConflictSerializable(H) => ViewSerializable(H). The converse, however, is not true. *) 382 | (* A history may be view serializable but not conflict serializable. *) 383 | (* *) 384 | (* In order to check for conflict serializability, we construct a multi-version serialization *) 385 | (* graph (MVSG). Details on MVSG can be found, among other places, in Cahill's thesis, Section *) 386 | (* 2.5.1. To construct the MVSG, we put an edge from one committed transaction T1 to another *) 387 | (* committed transaction T2 in the following situations: *) 388 | (* *) 389 | (* (WW-Dependency) *) 390 | (* T1 produces a version of x, and T2 produces a later version of x. *) 391 | (* *) 392 | (* (WR-Dependency) *) 393 | (* T1 produces a version of x, and T2 reads this (or a later) version of x. *) 394 | (* *) 395 | (* (RW-Dependency) *) 396 | (* T1 reads a version of x, and T2 produces a later version of x. This is *) 397 | (* the only case where T1 and T2 can run concurrently. *) 398 | (* *) 399 | (**************************************************************************************************) 400 | 401 | \* T1 wrote to a key that T2 then also wrote to. The First Committer Wins rule implies 402 | \* that T1 must have committed before T2 began. 403 | WWDependency(h, t1Id, t2Id) == 404 | \E op1 \in WritesByTxn(h, t1Id) : 405 | \E op2 \in WritesByTxn(h, t2Id) : 406 | /\ op1.key = op2.key 407 | /\ CommitOp(h, t1Id).time < CommitOp(h, t2Id).time 408 | 409 | \* T1 wrote to a key that T2 then later read, after T1 committed. 410 | WRDependency(h, t1Id, t2Id) == 411 | \E op1 \in WritesByTxn(h, t1Id) : 412 | \E op2 \in ReadsByTxn(h, t2Id) : 413 | /\ op1.key = op2.key 414 | /\ CommitOp(h, t1Id).time < BeginOp(h, t2Id).time 415 | 416 | \* T1 read a key that T2 then later wrote to. T1 must start before T2 commits, since this implies that T1 read 417 | \* a version of the key and T2 produced a later version of that ke, i.e. when it commits. T1, however, read 418 | \* an earlier version of that key, because it started before T2 committed. 419 | RWDependency(h, t1Id, t2Id) == 420 | \E op1 \in ReadsByTxn(h, t1Id) : 421 | \E op2 \in WritesByTxn(h, t2Id) : 422 | /\ op1.key = op2.key 423 | /\ BeginOp(h, t1Id).time < CommitOp(h, t2Id).time \* T1 starts before T2 commits. This means that T1 read 424 | 425 | 426 | \* Produces the serialization graph as defined above, for a given history. This graph is produced 427 | \* by defining the appropriate set comprehension, where the produced set contains all the edges of the graph. 428 | SerializationGraph(history) == 429 | LET committedTxnIds == CommittedTxns(history) IN 430 | {tedge \in (committedTxnIds \X committedTxnIds): 431 | /\ tedge[1] /= tedge[2] 432 | /\ \/ WWDependency(history, tedge[1], tedge[2]) 433 | \/ WRDependency(history, tedge[1], tedge[2]) 434 | \/ RWDependency(history, tedge[1], tedge[2])} 435 | 436 | \* The key property to verify i.e. serializability of transaction histories. 437 | IsConflictSerializable(h) == ~IsCycle(SerializationGraph(h)) 438 | 439 | \* Examples of each dependency type. 440 | HistWW == << [type |-> "begin" , txnId |-> 0 , time |-> 0], 441 | [type |-> "write" , txnId |-> 0 , key |-> "k1" , val |-> "v1"], 442 | [type |-> "commit" , txnId |-> 0 , time |-> 1, updatedKeys |-> {"k1"}], 443 | [type |-> "begin" , txnId |-> 1 , time |-> 2], 444 | [type |-> "write" , txnId |-> 1 , key |-> "k1" , val |-> "v1"], 445 | [type |-> "commit" , txnId |-> 1 , time |-> 3, updatedKeys |-> {"k1"}]>> 446 | 447 | HistWR == << [type |-> "begin" , txnId |-> 0 , time |-> 0], 448 | [type |-> "write" , txnId |-> 0 , key |-> "k1" , val |-> "v1"], 449 | [type |-> "commit" , txnId |-> 0 , time |-> 1, updatedKeys |-> {"k1"}], 450 | [type |-> "begin" , txnId |-> 1 , time |-> 2], 451 | [type |-> "read" , txnId |-> 1 , key |-> "k1" , val |-> "v1"], 452 | [type |-> "commit" , txnId |-> 1 , time |-> 3, updatedKeys |-> {}]>> 453 | 454 | HistRW == << [type |-> "begin" , txnId |-> 0 , time |-> 0], 455 | [type |-> "read" , txnId |-> 0 , key |-> "k1" , val |-> "empty"], 456 | [type |-> "begin" , txnId |-> 1 , time |-> 1], 457 | [type |-> "write" , txnId |-> 1 , key |-> "k1" , val |-> "v1"], 458 | [type |-> "commit" , txnId |-> 1 , time |-> 2, updatedKeys |-> {}], 459 | [type |-> "commit" , txnId |-> 0 , time |-> 3, updatedKeys |-> {"k1"}]>> 460 | 461 | \* A simple invariant to test the correctness of dependency definitions. 462 | WWDependencyCorrect == WWDependency(HistWW, 0, 1) 463 | WRDependencyCorrect == WRDependency(HistWR, 0, 1) 464 | RWDependencyCorrect == RWDependency(HistRW, 0, 1) 465 | MVSGDependencyCorrect == WWDependencyCorrect /\ WRDependencyCorrect /\ RWDependencyCorrect 466 | 467 | 468 | (**************************************************************************************************) 469 | (* Examples of concurrency phenomena under Snapshot Isolation. These are for demonstration *) 470 | (* purposes and can be used for checking the above definitions of serializability. *) 471 | (* *) 472 | (* Write Skew: *) 473 | (* *) 474 | (* Example history from Michael Cahill's Phd thesis: *) 475 | (* *) 476 | (* Section 2.5.1, pg. 16 *) 477 | (* https://ses.library.usyd.edu.au/bitstream/2123/5353/1/michael-cahill-2009-thesis.pdf *) 478 | (* *) 479 | (* H: r1(x=50) r1(y=50) r2(x=50) r2(y=50) w1(x=-20) w2(y=-30) c1 c2 *) 480 | (* *) 481 | (* *) 482 | (* Read-Only Anomaly: *) 483 | (* *) 484 | (* "A Read-Only Transaction Anomaly Under Snapshot Isolation", Fekete, O'Neil, O'Neil *) 485 | (* https://www.cs.umb.edu/~poneil/ROAnom.pdf *) 486 | (* *) 487 | (* *) 488 | (**************************************************************************************************) 489 | 490 | WriteSkewAnomalyTest == << 491 | [type |-> "begin", txnId |-> 1, time |-> 1], 492 | [type |-> "begin", txnId |-> 2, time |-> 2], 493 | [type |-> "read", txnId |-> 1, key |-> "X", val |-> "Empty"], 494 | [type |-> "read", txnId |-> 1, key |-> "Y", val |-> "Empty"], 495 | [type |-> "read", txnId |-> 2, key |-> "X", val |-> "Empty"], 496 | [type |-> "read", txnId |-> 2, key |-> "Y", val |-> "Empty"], 497 | [type |-> "write", txnId |-> 1, key |-> "X", val |-> 30], 498 | [type |-> "write", txnId |-> 2, key |-> "Y", val |-> 20], 499 | [type |-> "commit", txnId |-> 1, time |-> 3, updatedKeys |-> {"X"}], 500 | [type |-> "commit", txnId |-> 2, time |-> 4, updatedKeys |-> {"Y"}]>> 501 | 502 | ReadOnlyAnomalyTest == << 503 | [type |-> "begin", txnId |-> 0, time |-> 0], 504 | [type |-> "write", txnId |-> 0, key |-> "K_X", val |-> 0], 505 | [type |-> "write", txnId |-> 0, key |-> "K_Y", val |-> 0], 506 | [type |-> "commit", txnId |-> 0, time |-> 1, updatedKeys |-> {"K_X", "K_Y"}], 507 | 508 | (* the history from the paper *) 509 | [type |-> "begin", txnId |-> 2, time |-> 2], 510 | (* R2(X0,0) *) [type |-> "read", txnId |-> 2, key |-> "K_X", ver |-> "T_0"], 511 | (* R2(Y0,0) *) [type |-> "read", txnId |-> 2, key |-> "K_Y", ver |-> "T_0"], 512 | 513 | [type |-> "begin", txnId |-> 1, time |-> 3], 514 | (* R1(Y0,0) *) [type |-> "read", txnId |-> 1, key |-> "K_Y"], 515 | (* W1(Y1,20) *) [type |-> "write", txnId |-> 1, key |-> "K_Y", val |-> 20], 516 | (* C1 *) [type |-> "commit", txnId |-> 1, time |-> 4, updatedKeys |-> {"K_Y"}], 517 | 518 | [type |-> "begin", txnId |-> 3, time |-> 5], 519 | (* R3(X0,0) *) [type |-> "read", txnId |-> 3, key |-> "K_X", ver |-> "T_0"], 520 | (* R3(Y1,20) *) [type |-> "read", txnId |-> 3, key |-> "K_Y", ver |-> "T_1"], 521 | (* C3 *) [type |-> "commit", txnId |-> 3, time |-> 6, updatedKeys |-> {}], 522 | 523 | (* W2(X2,-11) *) [type |-> "write", txnId |-> 2, key |-> "K_X", val |-> (0 - 11)], 524 | (* C2 *) [type |-> "commit", txnId |-> 2, time |-> 7, updatedKeys |-> {"K_X"}] 525 | >> 526 | 527 | (**************************************************************************************************) 528 | (* Checks if a given history contains a "read-only" anomaly. In other words, is this a *) 529 | (* non-serializable transaction history such that it contains a read-only transaction T, and *) 530 | (* removing T from the history makes the history serializable. *) 531 | (**************************************************************************************************) 532 | 533 | ReadOnlyAnomaly(h) == 534 | /\ ~IsConflictSerializable(h) 535 | /\ \E txnId \in CommittedTxns(h) : 536 | \* Transaction only did reads. 537 | /\ WritesByTxn(h, txnId) = {} 538 | \* Removing the transaction makes the history serializable 539 | /\ LET txnOpsFilter(t) == (t.txnId # txnId) 540 | hWithoutTxn == SelectSeq(h, txnOpsFilter) IN 541 | IsConflictSerializable(hWithoutTxn) 542 | 543 | \* Invariant definitions. 544 | IsConflictSerializableInv == IsConflictSerializable(txnHistory) 545 | NoReadOnlyAnomaly == ~ReadOnlyAnomaly(txnHistory) 546 | 547 | (**************************************************************************************************) 548 | (* Checks if a given history contains a "write skew" anomaly. In other words, is this a *) 549 | (* non-serializable transaction history such that it contains two transactions T1, T2, where T1 *) 550 | (* writes to a key that T2 also writes to, and T1 commits before T2 starts. *) 551 | (**************************************************************************************************) 552 | 553 | --------------------------------------------------------------------------------------------------- 554 | --------------------------------------------------------------------------------------------------- 555 | --------------------------------------------------------------------------------------------------- 556 | 557 | 558 | 559 | (**************************************************************************************************) 560 | (* View Serializability (Experimental). *) 561 | (* *) 562 | (* A transaction history is view serializable if the reads and writes of all transaction *) 563 | (* oeprations are equivalent the reads and writes of some serial schedule. View serializability *) 564 | (* encodes a more intuitive notion of serializability i.e. the overall effect of a sequence of *) 565 | (* interleaved transactions is the same as if it they were executed in sequential order. *) 566 | (**************************************************************************************************) 567 | 568 | Maximum(S) == CHOOSE x \in S : \A y \in S : y <= x 569 | 570 | \* The set of all permutations of elements of a set S whose length are the cardinality of S. 571 | SeqPermutations(S) == LET D == 1..Cardinality(S) IN {f \in [D -> S] : \A w \in S : \E v \in D : f[v]=w} 572 | 573 | \* Flattens a sequence of sequences. 574 | RECURSIVE Flatten(_) 575 | Flatten(seq) == IF Len(seq) = 0 THEN <<>> ELSE Head(seq) \o Flatten(Tail(seq)) 576 | 577 | \* The subsequence of all operations executed by a given transaction id in a history. The original ordering 578 | \* of the operations is maintained. 579 | OpsForTxn(h, txnId) == SelectSeq(h, LAMBDA t : t.txnId = txnId) 580 | SerialHistories(h) == 581 | LET serialOrderings == SeqPermutations({ OpsForTxn(h, tid) : tid \in CommittedTxns(h) }) IN 582 | {Flatten(o) : o \in serialOrderings} 583 | 584 | \* We "execute" a given serial history. To do this, we really only need to determine what the new values of the 585 | \* 'read' operations are, since writes are not changed. To do this, we simply replace the value of each read operation 586 | \* in the history with the appropriate one. 587 | ExecuteSerialHistory(h) == 588 | [i \in DOMAIN h |-> 589 | IF h[i].type = "read" 590 | \* We need to determine what value to read for this operation; we use the 591 | \* the value of the last write to this key. 592 | THEN LET prevWriteOpInds == {ind \in DOMAIN h : 593 | /\ ind < i 594 | /\ h[ind].type = "write" 595 | /\ h[ind].key = h[i].key} IN 596 | IF prevWriteOpInds = {} 597 | THEN [h[i] EXCEPT !.val = Empty] 598 | ELSE LET latestWriteOpToKey == h[Maximum(prevWriteOpInds)] IN 599 | [h[i] EXCEPT !.val = latestWriteOpToKey.val] 600 | ELSE h[i]] 601 | 602 | IsViewEquivalent(h1, h2) == 603 | \A tid \in CommittedTxns(h1) : OpsForTxn(h1, tid) = OpsForTxn(h2, tid) 604 | 605 | ViewEquivalentHistory(h) == {ExecuteSerialHistory(serial) : serial \in 606 | {h2 \in SerialHistories(h) : IsViewEquivalent(h, ExecuteSerialHistory(h2))}} 607 | 608 | IsViewSerializable(h) == \E h2 \in SerialHistories(h) : IsViewEquivalent(h, ExecuteSerialHistory(h2)) 609 | 610 | ------------------------------------------------- 611 | 612 | \* Some model checking details. 613 | 614 | Symmetry == Permutations(keys) \cup Permutations(values) \cup Permutations(txnIds) 615 | 616 | ============================================================================= 617 | \* Modification History 618 | \* Last modified Tue Feb 27 12:56:09 EST 2018 by williamschultz 619 | \* Created Sat Jan 13 08:59:10 EST 2018 by williamschultz 620 | -------------------------------------------------------------------------------- /models/IsConflictSerializable/MC.cfg: -------------------------------------------------------------------------------- 1 | \* MV CONSTANT declarations 2 | CONSTANTS 3 | T0 = T0 4 | T1 = T1 5 | T2 = T2 6 | \* MV CONSTANT declarations 7 | CONSTANTS 8 | k1 = k1 9 | k2 = k2 10 | \* MV CONSTANT declarations 11 | CONSTANTS 12 | v1 = v1 13 | v2 = v2 14 | \* CONSTANT declarations 15 | CONSTANT Empty = Empty 16 | \* MV CONSTANT definitions 17 | CONSTANT 18 | txnIds <- const_1563466721960301000 19 | \* MV CONSTANT definitions 20 | CONSTANT 21 | keys <- const_1563466721960302000 22 | \* MV CONSTANT definitions 23 | CONSTANT 24 | values <- const_1563466721960303000 25 | \* SYMMETRY definition 26 | SYMMETRY symm_1563466721960304000 27 | \* CONSTANT definition 28 | CONSTANT 29 | Seq <- def_ov_1563466721960305000 30 | Nat <- def_ov_1563466721960306000 31 | \* SPECIFICATION definition 32 | SPECIFICATION 33 | spec_1563466721960308000 34 | \* INVARIANT definition 35 | INVARIANT 36 | inv_1563466721960309000 37 | \* Generated on Thu Jul 18 12:18:41 EDT 2019 -------------------------------------------------------------------------------- /models/IsConflictSerializable/MC.tla: -------------------------------------------------------------------------------- 1 | ---- MODULE MC ---- 2 | EXTENDS SnapshotIsolation, TLC 3 | 4 | \* MV CONSTANT declarations@modelParameterConstants 5 | CONSTANTS 6 | T0, T1, T2 7 | ---- 8 | 9 | \* MV CONSTANT declarations@modelParameterConstants 10 | CONSTANTS 11 | k1, k2 12 | ---- 13 | 14 | \* MV CONSTANT declarations@modelParameterConstants 15 | CONSTANTS 16 | v1, v2 17 | ---- 18 | 19 | \* MV CONSTANT definitions txnIds 20 | const_1563466721960301000 == 21 | {T0, T1, T2} 22 | ---- 23 | 24 | \* MV CONSTANT definitions keys 25 | const_1563466721960302000 == 26 | {k1, k2} 27 | ---- 28 | 29 | \* MV CONSTANT definitions values 30 | const_1563466721960303000 == 31 | {v1, v2} 32 | ---- 33 | 34 | \* SYMMETRY definition 35 | symm_1563466721960304000 == 36 | Permutations(const_1563466721960301000) \union Permutations(const_1563466721960302000) \union Permutations(const_1563466721960303000) 37 | ---- 38 | 39 | \* CONSTANT definition @modelParameterDefinitions:0 40 | def_ov_1563466721960305000(x) == 41 | UNION {[1..n -> x] : n \in Nat} 42 | ---- 43 | \* CONSTANT definition @modelParameterDefinitions:1 44 | def_ov_1563466721960306000 == 45 | 1..8 46 | ---- 47 | 48 | \* SPECIFICATION definition @modelBehaviorSpec:0 49 | spec_1563466721960308000 == 50 | Spec 51 | ---- 52 | \* INVARIANT definition @modelCorrectnessInvariants:0 53 | inv_1563466721960309000 == 54 | IsConflictSerializable(txnHistory) 55 | ---- 56 | ==================================================================================================== 57 | \* Modification History 58 | \* Created Thu Jul 18 12:18:41 EDT 2019 by williamschultz 59 | -------------------------------------------------------------------------------- /models/IsConflictSerializable/SnapshotIsolation.tla: -------------------------------------------------------------------------------- 1 | ------------------------- MODULE SnapshotIsolation ------------------------- 2 | EXTENDS Naturals, FiniteSets, Sequences, TLC 3 | 4 | (**************************************************************************************************) 5 | (* *) 6 | (* This is a specification of snapshot isolation. It is based on various sources, integrating *) 7 | (* ideas and definitions from: *) 8 | (* *) 9 | (* ``Making Snapshot Isolation Serializable", Fekete et al., 2005 *) 10 | (* https://www.cse.iitb.ac.in/infolab/Data/Courses/CS632/2009/Papers/p492-fekete.pdf *) 11 | (* *) 12 | (* ``Serializable Isolation for Snapshot Databases", Cahill, 2009 *) 13 | (* https://ses.library.usyd.edu.au/bitstream/2123/5353/1/michael-cahill-2009-thesis.pdf *) 14 | (* *) 15 | (* ``A Read-Only Transaction Anomaly Under Snapshot Isolation", Fekete et al. *) 16 | (* https://www.cs.umb.edu/~poneil/ROAnom.pdf *) 17 | (* *) 18 | (* ``Debugging Designs", Chris Newcombe, 2011 *) 19 | (* https://github.com/pron/amazon-snapshot-spec/blob/master/DebuggingDesigns.pdf *) 20 | (* *) 21 | (* This spec tries to model things at a very high level of abstraction, so as to communicate the *) 22 | (* important concepts of snapshot isolation, as opposed to how a system might actually implement *) 23 | (* it. Correctness properties and their detailed explanations are included at the end of this *) 24 | (* spec. We draw the basic definition of snapshot isolation from Definition 1.1 of Fekete's *) 25 | (* "Read-Only" anomaly paper: *) 26 | (* *) 27 | (* *) 28 | (* "...we assume time is measured by a counter that advances whenever any *) 29 | (* transaction starts or commits, and we designate the time when transaction Ti starts as *) 30 | (* start(Ti) and the time when Ti commits as commit(Ti). *) 31 | (* *) 32 | (* Definition 1.1: Snapshot Isolation (SI). A transaction Ti executing under SI conceptually *) 33 | (* reads data from the committed state of the database as of time start(Ti) (the snapshot), and *) 34 | (* holds the results of its own writes in local memory store, so if it reads data it has written *) 35 | (* it will read its own output. Predicates evaluated by Ti are also based on rows and index *) 36 | (* entry versions from the committed state of the database at time start(Ti), adjusted to take *) 37 | (* Ti's own writes into account. Snapshot Isolation also must obey a "First Committer (Updater) *) 38 | (* Wins" rule...The interval in time from the start to the commit of a transaction, represented *) 39 | (* [Start(Ti), Commit(Ti)], is called its transactional lifetime. We say two transactions T1 and *) 40 | (* T2 are concurrent if their transactional lifetimes overlap, i.e., [start(T1), commit(T1)] ∩ *) 41 | (* [start(T2), commit(T2)] ≠ Φ. Writes by transactions active after Ti starts, i.e., writes by *) 42 | (* concurrent transactions, are not visible to Ti. When Ti is ready to commit, it obeys the *) 43 | (* First Committer Wins rule, as follows: Ti will successfully commit if and only if no *) 44 | (* concurrent transaction Tk has already committed writes (updates) of rows or index entries that *) 45 | (* Ti intends to write." *) 46 | (* *) 47 | (**************************************************************************************************) 48 | 49 | 50 | (**************************************************************************************************) 51 | (* The constant parameters of the spec. *) 52 | (**************************************************************************************************) 53 | 54 | \* Set of all transaction ids. 55 | CONSTANT txnIds 56 | 57 | \* Set of all data store keys/values. 58 | CONSTANT keys, values 59 | 60 | \* An empty value. 61 | CONSTANT Empty 62 | 63 | (**************************************************************************************************) 64 | (* The variables of the spec. *) 65 | (**************************************************************************************************) 66 | 67 | \* The clock, which measures 'time', is just a counter, that increments (ticks) 68 | \* whenever a transaction starts or commits. 69 | VARIABLE clock 70 | 71 | \* The set of all currently running transactions. 72 | VARIABLE runningTxns 73 | 74 | \* The full history of all transaction operations. It is modeled as a linear 75 | \* sequence of events. Such a history would likely never exist in a real implementation, 76 | \* but it is used in the model to check the properties of snapshot isolation. 77 | VARIABLE txnHistory 78 | 79 | \* (NOT NECESSARY) 80 | \* The key-value data store. In this spec, we model a data store explicitly, even though it is not actually 81 | \* used for the verification of any correctness properties. This was added initially as an attempt the make the 82 | \* spec more intuitive and understandable. It may play no important role at this point, however. If a property 83 | \* check was ever added for view serializability, this, and the set of transaction snapshots, may end up being 84 | \* useful. 85 | VARIABLE dataStore 86 | 87 | \* (NOT NECESSARY) 88 | \* The set of snapshots needed for all running transactions. Each snapshot 89 | \* represents the entire state of the data store as of a given point in time. 90 | \* It is a function from transaction ids to data store snapshots. This, like the 'dataStore' variable, may 91 | \* now be obsolete for a spec at this level of abstraction, since the correctness properties we check do not 92 | \* depend on the actual data being read/written. 93 | VARIABLE txnSnapshots 94 | 95 | vars == <> 96 | 97 | 98 | (**************************************************************************************************) 99 | (* Data type definitions. *) 100 | (**************************************************************************************************) 101 | 102 | DataStoreType == [keys -> (values \cup {Empty})] 103 | BeginOpType == [type : {"begin"} , txnId : txnIds , time : Nat] 104 | CommitOpType == [type : {"commit"} , txnId : txnIds , time : Nat, updatedKeys : SUBSET keys] 105 | WriteOpType == [type : {"write"} , txnId : txnIds , key: SUBSET keys , val : SUBSET values] 106 | ReadOpType == [type : {"read"} , txnId : txnIds , key: SUBSET keys , val : SUBSET values] 107 | AnyOpType == UNION {BeginOpType, CommitOpType, WriteOpType, ReadOpType} 108 | 109 | (**************************************************************************************************) 110 | (* The type invariant and initial predicate. *) 111 | (**************************************************************************************************) 112 | 113 | TypeInvariant == 114 | \* /\ txnHistory \in Seq(AnyOpType) seems expensive to check with TLC, so disable it. 115 | /\ dataStore \in DataStoreType 116 | /\ txnSnapshots \in [txnIds -> (DataStoreType \cup {Empty})] 117 | /\ runningTxns \in SUBSET [ id : txnIds, 118 | startTime : Nat, 119 | commitTime : Nat \cup {Empty}] 120 | 121 | Init == 122 | /\ runningTxns = {} 123 | /\ txnHistory = <<>> 124 | /\ clock = 0 125 | /\ txnSnapshots = [id \in txnIds |-> Empty] 126 | /\ dataStore = [k \in keys |-> Empty] 127 | 128 | (**************************************************************************************************) 129 | (* Helpers for querying transaction histories. *) 130 | (* *) 131 | (* These are parameterized on a transaction history and a transaction id, if applicable. *) 132 | (**************************************************************************************************) 133 | 134 | \* Generic TLA+ helper. 135 | Range(f) == {f[x] : x \in DOMAIN f} 136 | 137 | \* The begin or commit op for a given transaction id. 138 | BeginOp(h, txnId) == CHOOSE op \in Range(h) : op.txnId = txnId /\ op.type = "begin" 139 | CommitOp(h, txnId) == CHOOSE op \in Range(h) : op.txnId = txnId /\ op.type = "commit" 140 | 141 | \* The set of all committed/aborted transaction ids in a given history. 142 | CommittedTxns(h) == {op.txnId : op \in {op \in Range(h) : op.type = "commit"}} 143 | AbortedTxns(h) == {op.txnId : op \in {op \in Range(h) : op.type = "abort"}} 144 | 145 | \* The set of all read or write ops done by a given transaction. 146 | ReadsByTxn(h, txnId) == {op \in Range(h) : op.txnId = txnId /\ op.type = "read"} 147 | WritesByTxn(h, txnId) == {op \in Range(h) : op.txnId = txnId /\ op.type = "write"} 148 | 149 | \* The set of all keys read or written to by a given transaction. 150 | KeysReadByTxn(h, txnId) == { op.key : op \in ReadsByTxn(txnHistory, txnId)} 151 | KeysWrittenByTxn(h, txnId) == { op.key : op \in WritesByTxn(txnHistory, txnId)} 152 | 153 | \* The index of a given operation in the transaction history sequence. 154 | IndexOfOp(h, op) == CHOOSE i \in DOMAIN h : h[i] = op 155 | 156 | (**************************************************************************************************) 157 | (* *) 158 | (* Action Definitions *) 159 | (* *) 160 | (**************************************************************************************************) 161 | 162 | 163 | (**************************************************************************************************) 164 | (* When a transaction starts, it gets a new, unique transaction id and is added to the set of *) 165 | (* running transactions. It also "copies" a local snapshot of the data store on which it will *) 166 | (* perform its reads and writes against. In a real system, this data would not be literally *) 167 | (* "copied", but this is the fundamental concept of snapshot isolation i.e. that each *) 168 | (* transaction appears to operate on its own local snapshot of the database. *) 169 | (**************************************************************************************************) 170 | StartTxn == \E newTxnId \in txnIds : 171 | LET newTxn == 172 | [ id |-> newTxnId, 173 | startTime |-> clock+1, 174 | commitTime |-> Empty] IN 175 | \* Must choose an unused transaction id. There must be no other operation 176 | \* in the history that already uses this id. 177 | /\ ~\E op \in Range(txnHistory) : op.txnId = newTxnId 178 | \* Save a snapshot of current data store for this transaction, and 179 | \* and append its 'begin' event to the history. 180 | /\ txnSnapshots' = [txnSnapshots EXCEPT ![newTxnId] = dataStore] 181 | /\ LET beginOp == [ type |-> "begin", 182 | txnId |-> newTxnId, 183 | time |-> clock+1 ] IN 184 | txnHistory' = Append(txnHistory, beginOp) 185 | \* Add transaction to the set of active transactions. 186 | /\ runningTxns' = runningTxns \cup {newTxn} 187 | \* Tick the clock. 188 | /\ clock' = clock + 1 189 | /\ UNCHANGED <> 190 | 191 | 192 | (**************************************************************************************************) 193 | (* When a transaction T0 is ready to commit, it obeys the "First Committer Wins" rule. T0 will *) 194 | (* only successfully commit if no concurrent transaction has already committed writes of data *) 195 | (* objects that T0 intends to write. Transactions T0, T1 are considered concurrent if the *) 196 | (* intersection of their timespans is non empty i.e. *) 197 | (* *) 198 | (* [start(T0), commit(T0)] \cap [start(T1), commit(T1)] != {} *) 199 | (**************************************************************************************************) 200 | 201 | \* Checks whether a given transaction is allowed to commit, based on whether it conflicts 202 | \* with other concurrent transactions that have already committed. 203 | TxnCanCommit(txn) == 204 | ~\E op \in Range(txnHistory) : 205 | /\ op.type = "commit" 206 | /\ op.time > txn.startTime 207 | /\ KeysWrittenByTxn(txnHistory, txn.id) \cap op.updatedKeys /= {} \* Must be no conflicting keys. 208 | 209 | CommitTxn(txn) == 210 | \* Transaction must be able to commit i.e. have no write conflicts with concurrent. 211 | \* committed transactions. 212 | /\ TxnCanCommit(txn) 213 | /\ LET commitOp == [ type |-> "commit", 214 | txnId |-> txn.id, 215 | time |-> clock + 1, 216 | updatedKeys |-> KeysWrittenByTxn(txnHistory, txn.id)] IN 217 | txnHistory' = Append(txnHistory, commitOp) 218 | \* Merge this transaction's updates into the data store. If the 219 | \* transaction has updated a key, then we use its version as the new 220 | \* value for that key. Otherwise the key remains unchanged. 221 | /\ dataStore' = [k \in keys |-> IF k \in KeysWrittenByTxn(txnHistory, txn.id) 222 | THEN txnSnapshots[txn.id][k] 223 | ELSE dataStore[k]] 224 | \* Remove the transaction from the active set. 225 | /\ runningTxns' = runningTxns \ {txn} 226 | /\ clock' = clock + 1 227 | \* We can leave the snapshot around, since it won't be used again. 228 | /\ UNCHANGED <> 229 | 230 | (**************************************************************************************************) 231 | (* In this spec, a transaction aborts if and only if it cannot commit, due to write conflicts. *) 232 | (**************************************************************************************************) 233 | AbortTxn(txn) == 234 | \* If a transaction can't commit due to write conflicts, then it 235 | \* must abort. 236 | /\ ~TxnCanCommit(txn) 237 | /\ LET abortOp == [ type |-> "abort", 238 | txnId |-> txn.id, 239 | time |-> clock + 1] IN 240 | txnHistory' = Append(txnHistory, abortOp) 241 | /\ runningTxns' = runningTxns \ {txn} \* transaction is no longer running. 242 | /\ clock' = clock + 1 243 | \* No changes are made to the data store. 244 | /\ UNCHANGED <> 245 | 246 | \* Ends a given transaction by either committing or aborting it. To exclude uninteresting 247 | \* histories, we require that a transaction does at least one operation before committing or aborting. 248 | \* Assumes that the given transaction is currently running. 249 | CompleteTxn(txn) == 250 | \* Must not be a no-op transaction. 251 | /\ (WritesByTxn(txnHistory, txn.id) \cup ReadsByTxn(txnHistory, txn.id)) /= {} 252 | \* Commit or abort the transaction. 253 | /\ \/ CommitTxn(txn) 254 | \/ AbortTxn(txn) 255 | 256 | (***************************************************************************************************) 257 | (* Read and write operations executed by transactions. *) 258 | (* *) 259 | (* As a simplification, and to limit the size of potential models, we allow transactions to only *) 260 | (* read or write to the same key once. The idea is that it limits the state space without loss *) 261 | (* of generality. *) 262 | (**************************************************************************************************) 263 | 264 | TxnRead(txn, k) == 265 | \* Read from this transaction's snapshot. 266 | LET valRead == txnSnapshots[txn.id][k] 267 | readOp == [ type |-> "read", 268 | txnId |-> txn.id, 269 | key |-> k, 270 | val |-> valRead] IN 271 | /\ txnHistory' = Append(txnHistory, readOp) 272 | /\ UNCHANGED <> 273 | 274 | TxnUpdate(txn, k, v) == 275 | LET writeOp == [ type |-> "write", 276 | txnId |-> txn.id, 277 | key |-> k, 278 | val |-> v] IN 279 | \* We update the transaction's snapshot, not the actual data store. 280 | /\ LET updatedSnapshot == [txnSnapshots[txn.id] EXCEPT ![k] = v] IN 281 | txnSnapshots' = [txnSnapshots EXCEPT ![txn.id] = updatedSnapshot] 282 | /\ txnHistory' = Append(txnHistory, writeOp) 283 | /\ UNCHANGED <> 284 | 285 | \* A read or write action by a running transaction. We limit transactions 286 | \* to only read or write the same key once. 287 | TxnReadWrite(txn) == 288 | \E k \in keys : 289 | \E v \in values : 290 | \/ TxnRead(txn, k) /\ k \notin KeysReadByTxn(txnHistory, txn.id) 291 | \/ TxnUpdate(txn, k, v) /\ k \notin KeysWrittenByTxn(txnHistory, txn.id) 292 | 293 | 294 | (**************************************************************************************************) 295 | (* The next-state relation and spec definition. *) 296 | (* *) 297 | (* Since it is desirable to have TLC check for deadlock, which may indicate bugs in the spec or *) 298 | (* in the algorithm, we want to explicitly define what a "valid" termination state is. If all *) 299 | (* transactions have run and either committed or aborted, we consider that valid termination, and *) 300 | (* is allowed as an infinite suttering step. *) 301 | (* *) 302 | (* Also, once a transaction knows that it cannot commit due to write conflicts, we don't let it *) 303 | (* do any more reads or writes, so as to eliminate wasted operations. That is, once we know a *) 304 | (* transaction can't commit, we force its next action to be abort. *) 305 | (**************************************************************************************************) 306 | 307 | AllTxnsFinished == AbortedTxns(txnHistory) \cup CommittedTxns(txnHistory) = txnIds 308 | 309 | Next == \/ StartTxn 310 | \/ \E txn \in runningTxns : 311 | \/ CompleteTxn(txn) 312 | \/ TxnReadWrite(txn) /\ TxnCanCommit(txn) 313 | \/ (AllTxnsFinished /\ UNCHANGED vars) 314 | 315 | Spec == Init /\ [][Next]_vars /\ WF_vars(Next) 316 | 317 | 318 | ---------------------------------------------------------------------------------------------------- 319 | 320 | 321 | (**************************************************************************************************) 322 | (* *) 323 | (* Correctness Properties and Tests *) 324 | (* *) 325 | (**************************************************************************************************) 326 | 327 | 328 | 329 | (**************************************************************************************************) 330 | (* Operator for computing cycles in a given graph, defined by a set of edges. *) 331 | (* *) 332 | (* Returns a set containing all elements that participate in any cycle (i.e. union of all *) 333 | (* cycles), or an empty set if no cycle is found. *) 334 | (* *) 335 | (* Source: *) 336 | (* https://github.com/pron/amazon-snapshot-spec/blob/master/serializableSnapshotIsolation.tla. *) 337 | (**************************************************************************************************) 338 | FindAllNodesInAnyCycle(edges) == 339 | 340 | LET RECURSIVE findCycleNodes(_, _) (* startNode, visitedSet *) 341 | (* Returns a set containing all elements of some cycle starting at startNode, 342 | or an empty set if no cycle is found. 343 | *) 344 | findCycleNodes(node, visitedSet) == 345 | IF node \in visitedSet THEN 346 | {node} (* found a cycle, which includes node *) 347 | ELSE 348 | LET newVisited == visitedSet \union {node} 349 | neighbors == {to : <> \in 350 | {<> \in edges : from = node}} 351 | IN (* Explore neighbors *) 352 | UNION {findCycleNodes(neighbor, newVisited) : neighbor \in neighbors} 353 | 354 | startPoints == {from : <> \in edges} (* All nodes with an outgoing edge *) 355 | IN 356 | UNION {findCycleNodes(node, {}) : node \in startPoints} 357 | 358 | IsCycle(edges) == FindAllNodesInAnyCycle(edges) /= {} 359 | 360 | 361 | 362 | (**************************************************************************************************) 363 | (* *) 364 | (* Verifying Serializability *) 365 | (* *) 366 | (* --------------------------------------- *) 367 | (* *) 368 | (* For checking serializability of transaction histories we use the "Conflict Serializability" *) 369 | (* definition. This is slightly different than what is known as "View Serializability", but is *) 370 | (* suitable for verification, since it is efficient to check, whereas checking view *) 371 | (* serializability of a transaction schedule is known to be NP-complete. *) 372 | (* *) 373 | (* The definition of conflict serializability permits a more limited set of transaction *) 374 | (* histories. Intuitively, it can be viewed as checking whether a given schedule has the *) 375 | (* "potential" to produce a certain anomaly, even if the particular data values for a history *) 376 | (* make it serializable. Formally, we can think of the set of conflict serializable histories as *) 377 | (* a subset of all possible serializable histories. Alternatively, we can say that, for a given *) 378 | (* history H ConflictSerializable(H) => ViewSerializable(H). The converse, however, is not true. *) 379 | (* A history may be view serializable but not conflict serializable. *) 380 | (* *) 381 | (* In order to check for conflict serializability, we construct a multi-version serialization *) 382 | (* graph (MVSG). Details on MVSG can be found, among other places, in Cahill's thesis, Section *) 383 | (* 2.5.1. To construct the MVSG, we put an edge from one committed transaction T1 to another *) 384 | (* committed transaction T2 in the following situations: *) 385 | (* *) 386 | (* (WW-Dependency) *) 387 | (* T1 produces a version of x, and T2 produces a later version of x. *) 388 | (* *) 389 | (* (WR-Dependency) *) 390 | (* T1 produces a version of x, and T2 reads this (or a later) version of x. *) 391 | (* *) 392 | (* (RW-Dependency) *) 393 | (* T1 reads a version of x, and T2 produces a later version of x. This is *) 394 | (* the only case where T1 and T2 can run concurrently. *) 395 | (* *) 396 | (**************************************************************************************************) 397 | 398 | \* T1 wrote to a key that T2 then also wrote to. The First Committer Wins rule implies 399 | \* that T1 must have committed before T2 began. 400 | WWDependency(h, t1Id, t2Id) == 401 | \E op1 \in WritesByTxn(h, t1Id) : 402 | \E op2 \in WritesByTxn(h, t2Id) : 403 | /\ op1.key = op2.key 404 | /\ CommitOp(h, t1Id).time < CommitOp(h, t2Id).time 405 | 406 | \* T1 wrote to a key that T2 then later read, after T1 committed. 407 | WRDependency(h, t1Id, t2Id) == 408 | \E op1 \in WritesByTxn(h, t1Id) : 409 | \E op2 \in ReadsByTxn(h, t2Id) : 410 | /\ op1.key = op2.key 411 | /\ CommitOp(h, t1Id).time < BeginOp(h, t2Id).time 412 | 413 | \* T1 read a key that T2 then later wrote to. T1 must start before T2 commits, since this implies that T1 read 414 | \* a version of the key and T2 produced a later version of that ke, i.e. when it commits. T1, however, read 415 | \* an earlier version of that key, because it started before T2 committed. 416 | RWDependency(h, t1Id, t2Id) == 417 | \E op1 \in ReadsByTxn(h, t1Id) : 418 | \E op2 \in WritesByTxn(h, t2Id) : 419 | /\ op1.key = op2.key 420 | /\ BeginOp(h, t1Id).time < CommitOp(h, t2Id).time \* T1 starts before T2 commits. This means that T1 read 421 | 422 | 423 | \* Produces the serialization graph as defined above, for a given history. This graph is produced 424 | \* by defining the appropriate set comprehension, where the produced set contains all the edges of the graph. 425 | SerializationGraph(history) == 426 | LET committedTxnIds == CommittedTxns(history) IN 427 | {<> \in (committedTxnIds \X committedTxnIds): 428 | /\ t1 /= t2 429 | /\ \/ WWDependency(history, t1, t2) 430 | \/ WRDependency(history, t1, t2) 431 | \/ RWDependency(history, t1, t2)} 432 | 433 | \* The key property to verify i.e. serializability of transaction histories. 434 | IsConflictSerializable(h) == ~IsCycle(SerializationGraph(h)) 435 | 436 | \* Examples of each dependency type. 437 | HistWW == << [type |-> "begin" , txnId |-> 0 , time |-> 0], 438 | [type |-> "write" , txnId |-> 0 , key |-> "k1" , val |-> "v1"], 439 | [type |-> "commit" , txnId |-> 0 , time |-> 1, updatedKeys |-> {"k1"}], 440 | [type |-> "begin" , txnId |-> 1 , time |-> 2], 441 | [type |-> "write" , txnId |-> 1 , key |-> "k1" , val |-> "v1"], 442 | [type |-> "commit" , txnId |-> 1 , time |-> 3, updatedKeys |-> {"k1"}]>> 443 | 444 | HistWR == << [type |-> "begin" , txnId |-> 0 , time |-> 0], 445 | [type |-> "write" , txnId |-> 0 , key |-> "k1" , val |-> "v1"], 446 | [type |-> "commit" , txnId |-> 0 , time |-> 1, updatedKeys |-> {"k1"}], 447 | [type |-> "begin" , txnId |-> 1 , time |-> 2], 448 | [type |-> "read" , txnId |-> 1 , key |-> "k1" , val |-> "v1"], 449 | [type |-> "commit" , txnId |-> 1 , time |-> 3, updatedKeys |-> {}]>> 450 | 451 | HistRW == << [type |-> "begin" , txnId |-> 0 , time |-> 0], 452 | [type |-> "read" , txnId |-> 0 , key |-> "k1" , val |-> "empty"], 453 | [type |-> "begin" , txnId |-> 1 , time |-> 1], 454 | [type |-> "write" , txnId |-> 1 , key |-> "k1" , val |-> "v1"], 455 | [type |-> "commit" , txnId |-> 1 , time |-> 2, updatedKeys |-> {}], 456 | [type |-> "commit" , txnId |-> 0 , time |-> 3, updatedKeys |-> {"k1"}]>> 457 | 458 | \* A simple invariant to test the correctness of dependency definitions. 459 | WWDependencyCorrect == WWDependency(HistWW, 0, 1) 460 | WRDependencyCorrect == WRDependency(HistWR, 0, 1) 461 | RWDependencyCorrect == RWDependency(HistRW, 0, 1) 462 | MVSGDependencyCorrect == WWDependencyCorrect /\ WRDependencyCorrect /\ RWDependencyCorrect 463 | 464 | 465 | (**************************************************************************************************) 466 | (* Examples of concurrency phenomena under Snapshot Isolation. These are for demonstration *) 467 | (* purposes and can be used for checking the above definitions of serializability. *) 468 | (* *) 469 | (* Write Skew: *) 470 | (* *) 471 | (* Example history from Michael Cahill's Phd thesis: *) 472 | (* *) 473 | (* Section 2.5.1, pg. 16 *) 474 | (* https://ses.library.usyd.edu.au/bitstream/2123/5353/1/michael-cahill-2009-thesis.pdf *) 475 | (* *) 476 | (* H: r1(x=50) r1(y=50) r2(x=50) r2(y=50) w1(x=-20) w2(y=-30) c1 c2 *) 477 | (* *) 478 | (* *) 479 | (* Read-Only Anomaly: *) 480 | (* *) 481 | (* "A Read-Only Transaction Anomaly Under Snapshot Isolation", Fekete, O'Neil, O'Neil *) 482 | (* https://www.cs.umb.edu/~poneil/ROAnom.pdf *) 483 | (* *) 484 | (* *) 485 | (**************************************************************************************************) 486 | 487 | WriteSkewAnomalyTest == << 488 | [type |-> "begin", txnId |-> 1, time |-> 1], 489 | [type |-> "begin", txnId |-> 2, time |-> 2], 490 | [type |-> "read", txnId |-> 1, key |-> "X", val |-> "Empty"], 491 | [type |-> "read", txnId |-> 1, key |-> "Y", val |-> "Empty"], 492 | [type |-> "read", txnId |-> 2, key |-> "X", val |-> "Empty"], 493 | [type |-> "read", txnId |-> 2, key |-> "Y", val |-> "Empty"], 494 | [type |-> "write", txnId |-> 1, key |-> "X", val |-> 30], 495 | [type |-> "write", txnId |-> 2, key |-> "Y", val |-> 20], 496 | [type |-> "commit", txnId |-> 1, time |-> 3, updatedKeys |-> {"X"}], 497 | [type |-> "commit", txnId |-> 2, time |-> 4, updatedKeys |-> {"Y"}]>> 498 | 499 | ReadOnlyAnomalyTest == << 500 | [type |-> "begin", txnId |-> 0, time |-> 0], 501 | [type |-> "write", txnId |-> 0, key |-> "K_X", val |-> 0], 502 | [type |-> "write", txnId |-> 0, key |-> "K_Y", val |-> 0], 503 | [type |-> "commit", txnId |-> 0, time |-> 1, updatedKeys |-> {"K_X", "K_Y"}], 504 | 505 | (* the history from the paper *) 506 | [type |-> "begin", txnId |-> 2, time |-> 2], 507 | (* R2(X0,0) *) [type |-> "read", txnId |-> 2, key |-> "K_X", ver |-> "T_0"], 508 | (* R2(Y0,0) *) [type |-> "read", txnId |-> 2, key |-> "K_Y", ver |-> "T_0"], 509 | 510 | [type |-> "begin", txnId |-> 1, time |-> 3], 511 | (* R1(Y0,0) *) [type |-> "read", txnId |-> 1, key |-> "K_Y"], 512 | (* W1(Y1,20) *) [type |-> "write", txnId |-> 1, key |-> "K_Y", val |-> 20], 513 | (* C1 *) [type |-> "commit", txnId |-> 1, time |-> 4, updatedKeys |-> {"K_Y"}], 514 | 515 | [type |-> "begin", txnId |-> 3, time |-> 5], 516 | (* R3(X0,0) *) [type |-> "read", txnId |-> 3, key |-> "K_X", ver |-> "T_0"], 517 | (* R3(Y1,20) *) [type |-> "read", txnId |-> 3, key |-> "K_Y", ver |-> "T_1"], 518 | (* C3 *) [type |-> "commit", txnId |-> 3, time |-> 6, updatedKeys |-> {}], 519 | 520 | (* W2(X2,-11) *) [type |-> "write", txnId |-> 2, key |-> "K_X", val |-> (0 - 11)], 521 | (* C2 *) [type |-> "commit", txnId |-> 2, time |-> 7, updatedKeys |-> {"K_X"}] 522 | >> 523 | 524 | (**************************************************************************************************) 525 | (* Checks if a given history contains a "read-only" anomaly. In other words, is this a *) 526 | (* non-serializable transaction history such that it contains a read-only transaction T, and *) 527 | (* removing T from the history makes the history serializable. *) 528 | (**************************************************************************************************) 529 | 530 | ReadOnlyAnomaly(h) == 531 | /\ ~IsConflictSerializable(h) 532 | /\ \E txnId \in CommittedTxns(h) : 533 | \* Transaction only did reads. 534 | /\ WritesByTxn(h, txnId) = {} 535 | \* Removing the transaction makes the history serializable 536 | /\ LET txnOpsFilter(t) == (t.txnId # txnId) 537 | hWithoutTxn == SelectSeq(h, txnOpsFilter) IN 538 | IsConflictSerializable(hWithoutTxn) 539 | 540 | 541 | --------------------------------------------------------------------------------------------------- 542 | --------------------------------------------------------------------------------------------------- 543 | --------------------------------------------------------------------------------------------------- 544 | 545 | 546 | 547 | (**************************************************************************************************) 548 | (* View Serializability (Experimental). *) 549 | (* *) 550 | (* A transaction history is view serializable if the reads and writes of all transaction *) 551 | (* oeprations are equivalent the reads and writes of some serial schedule. View serializability *) 552 | (* encodes a more intuitive notion of serializability i.e. the overall effect of a sequence of *) 553 | (* interleaved transactions is the same as if it they were executed in sequential order. *) 554 | (**************************************************************************************************) 555 | 556 | Maximum(S) == CHOOSE x \in S : \A y \in S : y <= x 557 | 558 | \* The set of all permutations of elements of a set S whose length are the cardinality of S. 559 | SeqPermutations(S) == LET D == 1..Cardinality(S) IN {f \in [D -> S] : \A w \in S : \E v \in D : f[v]=w} 560 | 561 | \* Flattens a sequence of sequences. 562 | RECURSIVE Flatten(_) 563 | Flatten(seq) == IF Len(seq) = 0 THEN <<>> ELSE Head(seq) \o Flatten(Tail(seq)) 564 | 565 | \* The subsequence of all operations executed by a given transaction id in a history. The original ordering 566 | \* of the operations is maintained. 567 | OpsForTxn(h, txnId) == SelectSeq(h, LAMBDA t : t.txnId = txnId) 568 | SerialHistories(h) == 569 | LET serialOrderings == SeqPermutations({ OpsForTxn(h, tid) : tid \in CommittedTxns(h) }) IN 570 | {Flatten(o) : o \in serialOrderings} 571 | 572 | \* We "execute" a given serial history. To do this, we really only need to determine what the new values of the 573 | \* 'read' operations are, since writes are not changed. To do this, we simply replace the value of each read operation 574 | \* in the history with the appropriate one. 575 | ExecuteSerialHistory(h) == 576 | [i \in DOMAIN h |-> 577 | IF h[i].type = "read" 578 | \* We need to determine what value to read for this operation; we use the 579 | \* the value of the last write to this key. 580 | THEN LET prevWriteOpInds == {ind \in DOMAIN h : 581 | /\ ind < i 582 | /\ h[ind].type = "write" 583 | /\ h[ind].key = h[i].key} IN 584 | IF prevWriteOpInds = {} 585 | THEN [h[i] EXCEPT !.val = Empty] 586 | ELSE LET latestWriteOpToKey == h[Maximum(prevWriteOpInds)] IN 587 | [h[i] EXCEPT !.val = latestWriteOpToKey.val] 588 | ELSE h[i]] 589 | 590 | IsViewEquivalent(h1, h2) == 591 | \A tid \in CommittedTxns(h1) : OpsForTxn(h1, tid) = OpsForTxn(h2, tid) 592 | 593 | ViewEquivalentHistory(h) == {ExecuteSerialHistory(serial) : serial \in 594 | {h2 \in SerialHistories(h) : IsViewEquivalent(h, ExecuteSerialHistory(h2))}} 595 | 596 | IsViewSerializable(h) == \E h2 \in SerialHistories(h) : IsViewEquivalent(h, ExecuteSerialHistory(h2)) 597 | 598 | 599 | ============================================================================= 600 | \* Modification History 601 | \* Last modified Tue Feb 27 12:56:09 EST 2018 by williamschultz 602 | \* Created Sat Jan 13 08:59:10 EST 2018 by williamschultz 603 | -------------------------------------------------------------------------------- /traces/read_only_anomaly.txt: -------------------------------------------------------------------------------- 1 | TLC2 Version 2.18 of Day Month 20?? (rev: 4b513ad) 2 | Running Random Simulation with seed 6716861192212514348 with 8 workers on 8 cores with 3641MB heap and 64MB offheap memory [pid: 58400] (Mac OS X 14.3 aarch64, Oracle Corporation 22.0.1 x86_64). 3 | Parsing file /Users/willyschultz/Dropbox/Projects/TLA+/Specs/SnapshotIsolation/SnapshotIsolation.tla 4 | Parsing file /private/var/folders/52/bz5jx0_s56bbsnb0vxt468v00000gn/T/tlc-14601478954400213710/Naturals.tla (jar:file:/usr/local/tla2tools-v1.8.jar!/tla2sany/StandardModules/Naturals.tla) 5 | Parsing file /private/var/folders/52/bz5jx0_s56bbsnb0vxt468v00000gn/T/tlc-14601478954400213710/FiniteSets.tla (jar:file:/usr/local/tla2tools-v1.8.jar!/tla2sany/StandardModules/FiniteSets.tla) 6 | Parsing file /private/var/folders/52/bz5jx0_s56bbsnb0vxt468v00000gn/T/tlc-14601478954400213710/Sequences.tla (jar:file:/usr/local/tla2tools-v1.8.jar!/tla2sany/StandardModules/Sequences.tla) 7 | Parsing file /private/var/folders/52/bz5jx0_s56bbsnb0vxt468v00000gn/T/tlc-14601478954400213710/TLC.tla (jar:file:/usr/local/tla2tools-v1.8.jar!/tla2sany/StandardModules/TLC.tla) 8 | Semantic processing of module Naturals 9 | Semantic processing of module Sequences 10 | Semantic processing of module FiniteSets 11 | Semantic processing of module TLC 12 | Semantic processing of module SnapshotIsolation 13 | Starting... (2024-11-20 20:28:27) 14 | Computed 1 initial states... 15 | Error: Invariant NoReadOnlyAnomaly is violated. 16 | Error: The behavior up to this point is: 17 | State 1: 18 | /\ txnSnapshots = (t0 :> Empty @@ t1 :> Empty @@ t2 :> Empty) 19 | /\ dataStore = (k1 :> Empty @@ k2 :> Empty) 20 | /\ txnHistory = <<>> 21 | /\ clock = 0 22 | /\ runningTxns = {} 23 | 24 | State 2: 25 | /\ txnSnapshots = (t0 :> Empty @@ t1 :> Empty @@ t2 :> (k1 :> Empty @@ k2 :> Empty)) 26 | /\ dataStore = (k1 :> Empty @@ k2 :> Empty) 27 | /\ txnHistory = <<[type |-> "begin", txnId |-> t2, time |-> 1]>> 28 | /\ clock = 1 29 | /\ runningTxns = {[id |-> t2, startTime |-> 1, commitTime |-> Empty]} 30 | 31 | State 3: 32 | /\ txnSnapshots = (t0 :> Empty @@ t1 :> Empty @@ t2 :> (k1 :> v1 @@ k2 :> Empty)) 33 | /\ dataStore = (k1 :> Empty @@ k2 :> Empty) 34 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 35 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1] >> 36 | /\ clock = 1 37 | /\ runningTxns = {[id |-> t2, startTime |-> 1, commitTime |-> Empty]} 38 | 39 | State 4: 40 | /\ txnSnapshots = ( t0 :> Empty @@ 41 | t1 :> (k1 :> Empty @@ k2 :> Empty) @@ 42 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 43 | /\ dataStore = (k1 :> Empty @@ k2 :> Empty) 44 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 45 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 46 | [type |-> "begin", txnId |-> t1, time |-> 2] >> 47 | /\ clock = 2 48 | /\ runningTxns = { [id |-> t1, startTime |-> 2, commitTime |-> Empty], 49 | [id |-> t2, startTime |-> 1, commitTime |-> Empty] } 50 | 51 | State 5: 52 | /\ txnSnapshots = ( t0 :> Empty @@ 53 | t1 :> (k1 :> Empty @@ k2 :> Empty) @@ 54 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 55 | /\ dataStore = (k1 :> v1 @@ k2 :> Empty) 56 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 57 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 58 | [type |-> "begin", txnId |-> t1, time |-> 2], 59 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}] >> 60 | /\ clock = 3 61 | /\ runningTxns = {[id |-> t1, startTime |-> 2, commitTime |-> Empty]} 62 | 63 | State 6: 64 | /\ txnSnapshots = ( t0 :> Empty @@ 65 | t1 :> (k1 :> Empty @@ k2 :> Empty) @@ 66 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 67 | /\ dataStore = (k1 :> v1 @@ k2 :> Empty) 68 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 69 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 70 | [type |-> "begin", txnId |-> t1, time |-> 2], 71 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}], 72 | [type |-> "read", txnId |-> t1, key |-> k1, val |-> Empty] >> 73 | /\ clock = 3 74 | /\ runningTxns = {[id |-> t1, startTime |-> 2, commitTime |-> Empty]} 75 | 76 | State 7: 77 | /\ txnSnapshots = ( t0 :> Empty @@ 78 | t1 :> (k1 :> Empty @@ k2 :> v1) @@ 79 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 80 | /\ dataStore = (k1 :> v1 @@ k2 :> Empty) 81 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 82 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 83 | [type |-> "begin", txnId |-> t1, time |-> 2], 84 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}], 85 | [type |-> "read", txnId |-> t1, key |-> k1, val |-> Empty], 86 | [type |-> "write", txnId |-> t1, key |-> k2, val |-> v1] >> 87 | /\ clock = 3 88 | /\ runningTxns = {[id |-> t1, startTime |-> 2, commitTime |-> Empty]} 89 | 90 | State 8: 91 | /\ txnSnapshots = ( t0 :> (k1 :> v1 @@ k2 :> Empty) @@ 92 | t1 :> (k1 :> Empty @@ k2 :> v1) @@ 93 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 94 | /\ dataStore = (k1 :> v1 @@ k2 :> Empty) 95 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 96 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 97 | [type |-> "begin", txnId |-> t1, time |-> 2], 98 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}], 99 | [type |-> "read", txnId |-> t1, key |-> k1, val |-> Empty], 100 | [type |-> "write", txnId |-> t1, key |-> k2, val |-> v1], 101 | [type |-> "begin", txnId |-> t0, time |-> 4] >> 102 | /\ clock = 4 103 | /\ runningTxns = { [id |-> t0, startTime |-> 4, commitTime |-> Empty], 104 | [id |-> t1, startTime |-> 2, commitTime |-> Empty] } 105 | 106 | State 9: 107 | /\ txnSnapshots = ( t0 :> (k1 :> v1 @@ k2 :> Empty) @@ 108 | t1 :> (k1 :> Empty @@ k2 :> v1) @@ 109 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 110 | /\ dataStore = (k1 :> v1 @@ k2 :> Empty) 111 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 112 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 113 | [type |-> "begin", txnId |-> t1, time |-> 2], 114 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}], 115 | [type |-> "read", txnId |-> t1, key |-> k1, val |-> Empty], 116 | [type |-> "write", txnId |-> t1, key |-> k2, val |-> v1], 117 | [type |-> "begin", txnId |-> t0, time |-> 4], 118 | [type |-> "read", txnId |-> t0, key |-> k1, val |-> v1] >> 119 | /\ clock = 4 120 | /\ runningTxns = { [id |-> t0, startTime |-> 4, commitTime |-> Empty], 121 | [id |-> t1, startTime |-> 2, commitTime |-> Empty] } 122 | 123 | State 10: 124 | /\ txnSnapshots = ( t0 :> (k1 :> v1 @@ k2 :> Empty) @@ 125 | t1 :> (k1 :> Empty @@ k2 :> v1) @@ 126 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 127 | /\ dataStore = (k1 :> v1 @@ k2 :> Empty) 128 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 129 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 130 | [type |-> "begin", txnId |-> t1, time |-> 2], 131 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}], 132 | [type |-> "read", txnId |-> t1, key |-> k1, val |-> Empty], 133 | [type |-> "write", txnId |-> t1, key |-> k2, val |-> v1], 134 | [type |-> "begin", txnId |-> t0, time |-> 4], 135 | [type |-> "read", txnId |-> t0, key |-> k1, val |-> v1], 136 | [type |-> "read", txnId |-> t0, key |-> k2, val |-> Empty] >> 137 | /\ clock = 4 138 | /\ runningTxns = { [id |-> t0, startTime |-> 4, commitTime |-> Empty], 139 | [id |-> t1, startTime |-> 2, commitTime |-> Empty] } 140 | 141 | State 11: 142 | /\ txnSnapshots = ( t0 :> (k1 :> v1 @@ k2 :> Empty) @@ 143 | t1 :> (k1 :> Empty @@ k2 :> v1) @@ 144 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 145 | /\ dataStore = (k1 :> v1 @@ k2 :> Empty) 146 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 147 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 148 | [type |-> "begin", txnId |-> t1, time |-> 2], 149 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}], 150 | [type |-> "read", txnId |-> t1, key |-> k1, val |-> Empty], 151 | [type |-> "write", txnId |-> t1, key |-> k2, val |-> v1], 152 | [type |-> "begin", txnId |-> t0, time |-> 4], 153 | [type |-> "read", txnId |-> t0, key |-> k1, val |-> v1], 154 | [type |-> "read", txnId |-> t0, key |-> k2, val |-> Empty], 155 | [type |-> "commit", txnId |-> t0, time |-> 5, updatedKeys |-> {}] >> 156 | /\ clock = 5 157 | /\ runningTxns = {[id |-> t1, startTime |-> 2, commitTime |-> Empty]} 158 | 159 | State 12: 160 | /\ txnSnapshots = ( t0 :> (k1 :> v1 @@ k2 :> Empty) @@ 161 | t1 :> (k1 :> Empty @@ k2 :> v1) @@ 162 | t2 :> (k1 :> v1 @@ k2 :> Empty) ) 163 | /\ dataStore = (k1 :> v1 @@ k2 :> v1) 164 | /\ txnHistory = << [type |-> "begin", txnId |-> t2, time |-> 1], 165 | [type |-> "write", txnId |-> t2, key |-> k1, val |-> v1], 166 | [type |-> "begin", txnId |-> t1, time |-> 2], 167 | [type |-> "commit", txnId |-> t2, time |-> 3, updatedKeys |-> {k1}], 168 | [type |-> "read", txnId |-> t1, key |-> k1, val |-> Empty], 169 | [type |-> "write", txnId |-> t1, key |-> k2, val |-> v1], 170 | [type |-> "begin", txnId |-> t0, time |-> 4], 171 | [type |-> "read", txnId |-> t0, key |-> k1, val |-> v1], 172 | [type |-> "read", txnId |-> t0, key |-> k2, val |-> Empty], 173 | [type |-> "commit", txnId |-> t0, time |-> 5, updatedKeys |-> {}], 174 | [type |-> "commit", txnId |-> t1, time |-> 6, updatedKeys |-> {k2}] >> 175 | /\ clock = 6 176 | /\ runningTxns = {} 177 | 178 | The number of states generated: 2624759 179 | Simulation using seed 6716861192212514348 and aril 0 180 | Progress: 2624866 states checked, 238625 traces generated (trace length: mean=2, var(x)=81, sd=9) 181 | Finished in 23s at (2024-11-20 20:28:50) --------------------------------------------------------------------------------