|
1 | 1 | package mvcc
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "fmt" |
| 5 | + |
4 | 6 | "github.com/tidwall/btree"
|
5 | 7 |
|
6 | 8 | "github.com/mukeshjc/mvcc-isolation/v2/utils"
|
@@ -73,6 +75,18 @@ func (d *Database) newTransaction() *Transaction {
|
73 | 75 | func (d *Database) completeTransaction(t *Transaction, state TransactionState) error {
|
74 | 76 | utils.Debug("completing transaction ", t.id)
|
75 | 77 |
|
| 78 | + if state == CommittedTransaction { |
| 79 | + // Snapshot Isolation is the same as Repeatable Read but with one additional rule: the keys written by any two concurrent committed transactions must not overlap. |
| 80 | + if t.isolation == SnapshotIsolation { |
| 81 | + if d.hasConflict(t, func(t1 *Transaction, t2 *Transaction) bool { |
| 82 | + return setsShareKeys(t1.writeset, t2.writeset) |
| 83 | + }) { |
| 84 | + d.completeTransaction(t, RolledBackTransaction) |
| 85 | + return fmt.Errorf("write-write conflict") |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + |
76 | 90 | // update transactions.
|
77 | 91 | t.state = state
|
78 | 92 | d.transactions.Set(t.id, *t)
|
@@ -133,6 +147,7 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
|
133 | 147 |
|
134 | 148 | // Repeatable Read, Snapshot Isolation and Serializable further restricts Read Committed so only versions from transactions that completed before this one started are visible.
|
135 | 149 | // we will add additional checks for the Read Committed logic that make sure the value was not created and not deleted within a transaction that started before this transaction started.
|
| 150 | + // https://jepsen.io/consistency/models/repeatable-read |
136 | 151 | // As it happens, this is the same logic that will be necessary for Snapshot Isolation and Serializable Isolation.
|
137 | 152 | // The additional logic (that makes Snapshot Isolation and Serializable Isolation different) happens at commit time.
|
138 | 153 |
|
@@ -175,3 +190,63 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
|
175 | 190 |
|
176 | 191 | return true
|
177 | 192 | }
|
| 193 | + |
| 194 | +// In a snapshot isolated system, each transaction appears to operate on an independent, consistent snapshot of the database. |
| 195 | +// Its changes are visible only to that transaction until commit time, when all changes become visible atomically to any transaction which begins at a later time. |
| 196 | +// If transaction T1 has modified an object x, and another transaction T2 committed a write to x after T1’s snapshot began, and before T1’s commit, then T1 must abort. |
| 197 | +// Snapshot Isolation is the same as Repeatable Read but with one additional rule: the keys written by any two concurrent committed transactions must not overlap. |
| 198 | +// https://jepsen.io/consistency/models/snapshot-isolation |
| 199 | + |
| 200 | +// when a transaction A goes to commit, it will run a conflict test for any transaction B that has committed since this transaction A started to check for clashes in the keys written |
| 201 | + |
| 202 | +// a helper for iterating through all relevant transactions, running a check function for any transaction that has committed. |
| 203 | +func (d *Database) hasConflict(t1 *Transaction, conflictFn func(*Transaction, *Transaction) bool) bool { |
| 204 | + iter := d.transactions.Iter() |
| 205 | + |
| 206 | + // first see if there is any conflict with transactions that were in progress when this one started. |
| 207 | + inprogressIter := t1.inprogress.Iter() |
| 208 | + for ok := inprogressIter.First(); ok; ok = inprogressIter.Next() { |
| 209 | + id := inprogressIter.Key() |
| 210 | + found := iter.Seek(id) |
| 211 | + if !found { |
| 212 | + continue |
| 213 | + } |
| 214 | + t2 := iter.Value() |
| 215 | + if t2.state == CommittedTransaction { |
| 216 | + if conflictFn(t1, &t2) { |
| 217 | + return true |
| 218 | + } |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + // then see if there is any conflict with transactions that started and committed after this one started. |
| 223 | + for id := t1.id; id < d.nextTransactionId; id++ { |
| 224 | + found := iter.Seek(id) |
| 225 | + if !found { |
| 226 | + continue |
| 227 | + } |
| 228 | + t2 := iter.Value() |
| 229 | + if t2.state == CommittedTransaction { |
| 230 | + if conflictFn(t1, &t2) { |
| 231 | + return true |
| 232 | + } |
| 233 | + } |
| 234 | + } |
| 235 | + |
| 236 | + return false |
| 237 | +} |
| 238 | + |
| 239 | +func setsShareKeys(s1 btree.Set[string], s2 btree.Set[string]) bool { |
| 240 | + s1Iter := s1.Iter() |
| 241 | + s2Iter := s2.Iter() |
| 242 | + |
| 243 | + for ok := s1Iter.First(); ok; ok = s1Iter.Next() { |
| 244 | + s1Key := s1Iter.Key() |
| 245 | + found := s2Iter.Seek(s1Key) |
| 246 | + if found { |
| 247 | + return true |
| 248 | + } |
| 249 | + } |
| 250 | + |
| 251 | + return false |
| 252 | +} |
0 commit comments