Skip to content

Commit 63f94f7

Browse files
author
mukeshjc
committed
repeatable read isolation level
the same as read committed but with the following anomaly not allowed: P2 (“Non-repeatable read”): SQL-transaction T1 reads a row. SQL-transaction T2 then modifies or deletes that row and performs a COMMIT. If T1 then attempts to reread the row, it may receive the modified value or discover that the row has been deleted. Thus breaking the consistency guarantee from ACID properties.
1 parent 62a17b9 commit 63f94f7

File tree

2 files changed

+116
-4
lines changed

2 files changed

+116
-4
lines changed

main_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,74 @@ func TestReadCommitted(t *testing.T) {
101101
utils.AssertEq(res, "", "c4 get x")
102102
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c4 get x")
103103
}
104+
105+
func TestRepeatableRead(t *testing.T) {
106+
database := mvcc.NewDatabase(mvcc.RepeatableReadIsolation)
107+
108+
c1 := database.NewConnection()
109+
c1.MustExecCommand("begin", nil)
110+
111+
c2 := database.NewConnection()
112+
c2.MustExecCommand("begin", nil)
113+
114+
// local change is visible locally
115+
c1.MustExecCommand("set", []string{"x", "hey"})
116+
res := c1.MustExecCommand("get", []string{"x"})
117+
utils.AssertEq(res, "hey", "c1 get x")
118+
119+
// update not available to this transaction since it is not committed
120+
res, err := c2.ExecCommand("get", []string{"x"})
121+
utils.AssertEq(res, "", "c2 get x")
122+
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")
123+
124+
c1.MustExecCommand("commit", nil)
125+
126+
// even after committing the update isn't visible because c1 was in-progress when c2 began
127+
res, err = c2.ExecCommand("get", []string{"x"})
128+
utils.AssertEq(res, "", "c2 get x")
129+
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")
130+
131+
// but is available in a new transaction
132+
c3 := database.NewConnection()
133+
c3.MustExecCommand("begin", nil)
134+
135+
res = c3.MustExecCommand("get", []string{"x"})
136+
utils.AssertEq(res, "hey", "c3 get x")
137+
138+
// local change is visible locally
139+
c3.MustExecCommand("set", []string{"x", "yall"})
140+
res = c3.MustExecCommand("get", []string{"x"})
141+
utils.AssertEq(res, "yall", "c3 get x")
142+
143+
// But not on the other connection, again.
144+
res, err = c2.ExecCommand("get", []string{"x"})
145+
utils.AssertEq(res, "", "c2 get x")
146+
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")
147+
148+
c3.MustExecCommand("rollback", nil)
149+
150+
// And still not, regardless of rollback, because it's an older
151+
// transaction.
152+
res, err = c2.ExecCommand("get", []string{"x"})
153+
utils.AssertEq(res, "", "c2 get x")
154+
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c2 get x")
155+
156+
// And again the rollbacked set is still not on a new transaction.
157+
c4 := database.NewConnection()
158+
res = c4.MustExecCommand("begin", nil)
159+
160+
res = c4.MustExecCommand("get", []string{"x"})
161+
utils.AssertEq(res, "hey", "c4 get x")
162+
163+
c4.MustExecCommand("delete", []string{"x"})
164+
c4.MustExecCommand("commit", nil)
165+
166+
// But the delete is visible to new transactions now that this
167+
// has been committed.
168+
c5 := database.NewConnection()
169+
res = c5.MustExecCommand("begin", nil)
170+
171+
res, err = c5.ExecCommand("get", []string{"x"})
172+
utils.AssertEq(res, "", "c5 get x")
173+
utils.AssertEq(err.Error(), "cannot get key that doesn't exist", "c5 get x")
174+
}

mvcc/database.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
117117
return false
118118
}
119119

120-
// ... by other transaction and it is committed, then it's no good.
121-
if value.txEndId > 0 && d.transactionState(value.txEndId).state == CommittedTransaction {
120+
// ... by other transaction that is committed, then it's no good.
121+
if d.transactionState(value.txEndId).state == CommittedTransaction {
122122
return false
123123
}
124124
}
@@ -131,6 +131,47 @@ func (d *Database) isVisible(t *Transaction, value Value) bool {
131131
// Another transaction B may have committed changes between two statements in this transaction A.
132132
}
133133

134-
utils.Assert(false, "unsupported isolation level")
135-
return false
134+
// Repeatable Read, Snapshot Isolation and Serializable further restricts Read Committed so only versions from transactions that completed before this one started are visible.
135+
// 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.
136+
// As it happens, this is the same logic that will be necessary for Snapshot Isolation and Serializable Isolation.
137+
// The additional logic (that makes Snapshot Isolation and Serializable Isolation different) happens at commit time.
138+
139+
utils.Assert(t.isolation == RepeatableReadIsolation || t.isolation == SnapshotIsolation || t.isolation == SerializableIsolation, "unsupported isolation level")
140+
141+
////// now the specifics for a RepeatableReadIsolation level and above, rest of the checks for stricter isolation levels happens at Commit Time.
142+
143+
// ignore values from transactions started after the current one
144+
if value.txStartId > t.id {
145+
return false
146+
}
147+
148+
// ignore values created from transactions in-progress i.e. ongoing when this transaction began but may have committed when this transaction was in progress.
149+
// if we didn't check for this, then our current transaction may have performed some reads at the beginning, then an in-progress transaction committed and if we made
150+
// another read, we might see the values because now that would be a committed transaction as per ReadCommittedIsolation level. Thus it would be a dirty read and violate
151+
// RepeatableReadIsolation guarantee.
152+
if t.inprogress.Contains(value.txStartId) {
153+
return false
154+
}
155+
156+
////// a copy of all checks we did for ReadUncommittedIsolation is below with slight **MODIFICATION** to the second statement in the bigger IF block
157+
158+
// If the value wasn't created by current transaction and the other transaction that created it isn't committed yet, then it's no good.
159+
if value.txStartId != t.id && d.transactionState(value.txStartId).state != CommittedTransaction {
160+
return false
161+
}
162+
163+
// If the value was deleted ...
164+
if value.txEndId > 0 {
165+
// ... in the current transaction, then it's no good
166+
if value.txEndId == t.id {
167+
return false
168+
}
169+
170+
// ... by other transaction **that began before the current one** and it is committed, then it's no good.
171+
if value.txEndId < t.id && d.transactionState(value.txEndId).state == CommittedTransaction {
172+
return false
173+
}
174+
}
175+
176+
return true
136177
}

0 commit comments

Comments
 (0)