Skip to content

Commit ef52427

Browse files
author
mukeshjc
committed
snapshot isolation level
if the current transaction A's write set intersects with any other transaction B committed since transaction A started, we must abort.
1 parent aa34cf1 commit ef52427

File tree

2 files changed

+103
-0
lines changed

2 files changed

+103
-0
lines changed

main_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,31 @@ func TestRepeatableRead(t *testing.T) {
172172
utils.AssertEq(res, "", "c5 get x")
173173
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c5 get x")
174174
}
175+
176+
// Snapshot Isolation shares all the same visibility rules as Repeatable Read, the tests get to be a little simpler!
177+
// We'll simply test that two transactions attempting to commit a write to the same key fail. Or specifically: that the second transaction cannot commit.
178+
func TestSnapshotIsolation(t *testing.T) {
179+
database := mvcc.NewDatabase(mvcc.SnapshotIsolation)
180+
181+
c1 := database.NewConnection()
182+
c1.MustExecCommand("begin", nil)
183+
184+
c2 := database.NewConnection()
185+
c2.MustExecCommand("begin", nil)
186+
187+
c3 := database.NewConnection()
188+
c3.MustExecCommand("begin", nil)
189+
190+
c1.MustExecCommand("set", []string{"x", "hey"})
191+
c1.MustExecCommand("commit", nil)
192+
193+
c2.MustExecCommand("set", []string{"x", "hey"})
194+
195+
res, err := c2.ExecCommand("commit", nil)
196+
utils.AssertEq(res, "", "c2 commit")
197+
utils.AssertEq(err.Error(), "write-write conflict", "c2 commit")
198+
199+
// But unrelated keys cause no conflict.
200+
c3.MustExecCommand("set", []string{"y", "no conflict"})
201+
c3.MustExecCommand("commit", nil)
202+
}

mvcc/database.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package mvcc
22

33
import (
4+
"fmt"
5+
46
"github.com/tidwall/btree"
57

68
"github.com/mukeshjc/mvcc-isolation/v2/utils"
@@ -73,6 +75,18 @@ func (d *Database) newTransaction() *Transaction {
7375
func (d *Database) completeTransaction(t *Transaction, state TransactionState) error {
7476
utils.Debug("completing transaction ", t.id)
7577

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+
7690
// update transactions.
7791
t.state = state
7892
d.transactions.Set(t.id, *t)
@@ -133,6 +147,7 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
133147

134148
// Repeatable Read, Snapshot Isolation and Serializable further restricts Read Committed so only versions from transactions that completed before this one started are visible.
135149
// 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
136151
// As it happens, this is the same logic that will be necessary for Snapshot Isolation and Serializable Isolation.
137152
// The additional logic (that makes Snapshot Isolation and Serializable Isolation different) happens at commit time.
138153

@@ -175,3 +190,63 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
175190

176191
return true
177192
}
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

Comments
 (0)