Skip to content

Commit 674b4eb

Browse files
committed
Support PreferRepeatableRead on explicit transactions by retrying
1 parent bb93ed7 commit 674b4eb

File tree

3 files changed

+94
-14
lines changed

3 files changed

+94
-14
lines changed

gel/options.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,18 @@ class IsolationLevel:
5757
PreferRepeatableRead = "PreferRepeatableRead"
5858

5959
@staticmethod
60-
def _to_start_tx_str(v):
60+
def _to_start_tx_str(v, optimistic_isolation):
6161
if (
6262
v == IsolationLevel.Serializable
63-
# We may *prefer* repeatable read, but we have not yet
64-
# implemented it for explicit transactions. (Which will
65-
# require some gnarly retry-and-caching logic.)
66-
or v == IsolationLevel.PreferRepeatableRead
6763
):
6864
return 'SERIALIZABLE'
6965
elif v == IsolationLevel.RepeatableRead:
7066
return 'REPEATABLE READ'
67+
elif v == IsolationLevel.PreferRepeatableRead:
68+
if optimistic_isolation:
69+
return 'REPEATABLE READ'
70+
else:
71+
return 'SERIALIZABLE'
7172
else:
7273
raise ValueError(
7374
f"Invalid isolation_level value for transaction(): {self}"
@@ -134,10 +135,11 @@ def __init__(
134135
def defaults(cls):
135136
return cls()
136137

137-
def start_transaction_query(self):
138+
def start_transaction_query(self, optimistic_isolation: bool):
138139
options = []
139140
if self._isolation is not None:
140-
level = IsolationLevel._to_start_tx_str(self._isolation)
141+
level = IsolationLevel._to_start_tx_str(
142+
self._isolation, optimistic_isolation)
141143
options.append(f'ISOLATION {level}')
142144

143145
if self._readonly is not None:

gel/transaction.py

+30-4
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,9 @@ def _make_start_query(self):
8686
raise errors.InterfaceError(
8787
'cannot start; the transaction is already started')
8888

89-
return self._options.start_transaction_query()
89+
return self._options.start_transaction_query(
90+
optimistic_isolation=self.__retry._optimistic_rr
91+
)
9092

9193
def _make_commit_query(self):
9294
self.__check_state('commit')
@@ -175,9 +177,17 @@ async def _exit(self, extype, ex):
175177
await self._client._impl.release(self._connection)
176178

177179
if (
178-
extype is not None and
179-
issubclass(extype, errors.EdgeDBError) and
180-
ex.has_tag(errors.SHOULD_RETRY)
180+
extype is not None
181+
and issubclass(extype, errors.CapabilityError)
182+
# XXX: This is not the best way to check this
183+
and 'REPEATABLE READ' in str(ex)
184+
):
185+
return self.__retry._retry_rr_failure(ex)
186+
187+
if (
188+
extype is not None
189+
and issubclass(extype, errors.EdgeDBError)
190+
and ex.has_tag(errors.SHOULD_RETRY)
181191
):
182192
return self.__retry._retry(ex)
183193

@@ -233,6 +243,12 @@ def __init__(self, owner):
233243
self._next_backoff = 0
234244
self._options = owner._options
235245

246+
prefer_rr = (
247+
self._options.transaction_options._isolation
248+
== options.IsolationLevel.PreferRepeatableRead
249+
)
250+
self._optimistic_rr = prefer_rr
251+
236252
def _retry(self, exc):
237253
self._last_exception = exc
238254
rule = self._options.retry_options.get_rule_for_exception(exc)
@@ -241,3 +257,13 @@ def _retry(self, exc):
241257
self._done = False
242258
self._next_backoff = rule.backoff(self._iteration)
243259
return True
260+
261+
def _retry_rr_failure(self, exc):
262+
# Retry a failure due to REPEATABLE READ not working
263+
if not self._optimistic_rr:
264+
return False
265+
self._optimistic_rr = False
266+
# Decrement _iteration count, since this one doesn't really count.
267+
self._iteration -= 1
268+
self._done = False
269+
return True

tests/test_sync_tx.py

+55-3
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,17 @@ class TestSyncTx(tb.SyncQueryTestCase):
3131
CREATE TYPE test::TransactionTest EXTENDING std::Object {
3232
CREATE PROPERTY name -> std::str;
3333
};
34-
'''
3534
36-
TEARDOWN = '''
37-
DROP TYPE test::TransactionTest;
35+
CREATE TYPE test::Tmp {
36+
CREATE REQUIRED PROPERTY tmp -> std::str;
37+
};
38+
CREATE TYPE test::TmpConflict {
39+
CREATE REQUIRED PROPERTY tmp -> std::str {
40+
CREATE CONSTRAINT exclusive;
41+
}
42+
};
43+
44+
CREATE TYPE test::TmpConflictChild extending test::TmpConflict;
3845
'''
3946

4047
def test_sync_transaction_regular_01(self):
@@ -101,6 +108,51 @@ async def test_sync_transaction_kinds(self):
101108
with tx:
102109
pass
103110

111+
def test_sync_transaction_prefer_rr(self):
112+
if (
113+
str(self.server_version.stage) != 'dev'
114+
and (self.server_version.major, self.server_version.minor) < (6, 5)
115+
):
116+
self.skipTest("DML in RepeatableRead not supported yet")
117+
con = self.client.with_transaction_options(
118+
edgedb.TransactionOptions(
119+
isolation=edgedb.IsolationLevel.PreferRepeatableRead
120+
)
121+
)
122+
# A transaction that needs to be serializable
123+
for tx in con.transaction():
124+
with tx:
125+
res1 = tx.query_single('''
126+
select {
127+
ins := (insert test::TmpConflict { tmp := "test1" }),
128+
level := sys::get_transaction_isolation(),
129+
}
130+
''')
131+
132+
res2 = tx.query_single('''
133+
select {
134+
ins := (insert test::TmpConflict { tmp := "test2" }),
135+
level := sys::get_transaction_isolation(),
136+
}
137+
''')
138+
139+
# N.B: res1 will be RepeatableRead on the first
140+
# iteration, maybe, but contingent on the second query
141+
# succeeding it will be Serializable!
142+
self.assertEqual(str(res1.level), 'Serializable')
143+
self.assertEqual(str(res2.level), 'Serializable')
144+
145+
# And one that doesn't
146+
for tx in con.transaction():
147+
with tx:
148+
res = tx.query_single('''
149+
select {
150+
ins := (insert test::Tmp { tmp := "test" }),
151+
level := sys::get_transaction_isolation(),
152+
}
153+
''')
154+
self.assertEqual(str(res.level), 'RepeatableRead')
155+
104156
def test_sync_transaction_commit_failure(self):
105157
with self.assertRaises(edgedb.errors.QueryError):
106158
for tx in self.client.transaction():

0 commit comments

Comments
 (0)