Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit deedb91

Browse files
authored
Fix MultiWriterIdGenerator.current_position. (#8257)
It did not correctly handle IDs finishing being persisted out of order, resulting in the `current_position` lagging until new IDs are persisted.
1 parent cca03db commit deedb91

File tree

3 files changed

+88
-6
lines changed

3 files changed

+88
-6
lines changed

changelog.d/8257.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix non-user visible bug in implementation of `MultiWriterIdGenerator.get_current_token_for_writer`.

synapse/storage/util/id_generators.py

+37-6
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ def __init__(
224224
# should be less than the minimum of this set (if not empty).
225225
self._unfinished_ids = set() # type: Set[int]
226226

227+
# Set of local IDs that we've processed that are larger than the current
228+
# position, due to there being smaller unpersisted IDs.
229+
self._finished_ids = set() # type: Set[int]
230+
227231
# We track the max position where we know everything before has been
228232
# persisted. This is done by a) looking at the min across all instances
229233
# and b) noting that if we have seen a run of persisted positions
@@ -348,17 +352,44 @@ def get_next_txn(self, txn: LoggingTransaction):
348352

349353
def _mark_id_as_finished(self, next_id: int):
350354
"""The ID has finished being processed so we should advance the
351-
current poistion if possible.
355+
current position if possible.
352356
"""
353357

354358
with self._lock:
355359
self._unfinished_ids.discard(next_id)
360+
self._finished_ids.add(next_id)
361+
362+
new_cur = None
363+
364+
if self._unfinished_ids:
365+
# If there are unfinished IDs then the new position will be the
366+
# largest finished ID less than the minimum unfinished ID.
367+
368+
finished = set()
369+
370+
min_unfinshed = min(self._unfinished_ids)
371+
for s in self._finished_ids:
372+
if s < min_unfinshed:
373+
if new_cur is None or new_cur < s:
374+
new_cur = s
375+
else:
376+
finished.add(s)
377+
378+
# We clear these out since they're now all less than the new
379+
# position.
380+
self._finished_ids = finished
381+
else:
382+
# There are no unfinished IDs so the new position is simply the
383+
# largest finished one.
384+
new_cur = max(self._finished_ids)
385+
386+
# We clear these out since they're now all less than the new
387+
# position.
388+
self._finished_ids.clear()
356389

357-
# Figure out if its safe to advance the position by checking there
358-
# aren't any lower allocated IDs that are yet to finish.
359-
if all(c > next_id for c in self._unfinished_ids):
390+
if new_cur:
360391
curr = self._current_positions.get(self._instance_name, 0)
361-
self._current_positions[self._instance_name] = max(curr, next_id)
392+
self._current_positions[self._instance_name] = max(curr, new_cur)
362393

363394
self._add_persisted_position(next_id)
364395

@@ -428,7 +459,7 @@ def _add_persisted_position(self, new_id: int):
428459
# We move the current min position up if the minimum current positions
429460
# of all instances is higher (since by definition all positions less
430461
# that that have been persisted).
431-
min_curr = min(self._current_positions.values())
462+
min_curr = min(self._current_positions.values(), default=0)
432463
self._persisted_upto_position = max(min_curr, self._persisted_upto_position)
433464

434465
# We now iterate through the seen positions, discarding those that are

tests/storage/test_id_generators.py

+50
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,56 @@ async def _get_next_async():
122122
self.assertEqual(id_gen.get_positions(), {"master": 8})
123123
self.assertEqual(id_gen.get_current_token_for_writer("master"), 8)
124124

125+
def test_out_of_order_finish(self):
126+
"""Test that IDs persisted out of order are correctly handled
127+
"""
128+
129+
# Prefill table with 7 rows written by 'master'
130+
self._insert_rows("master", 7)
131+
132+
id_gen = self._create_id_generator()
133+
134+
self.assertEqual(id_gen.get_positions(), {"master": 7})
135+
self.assertEqual(id_gen.get_current_token_for_writer("master"), 7)
136+
137+
ctx1 = self.get_success(id_gen.get_next())
138+
ctx2 = self.get_success(id_gen.get_next())
139+
ctx3 = self.get_success(id_gen.get_next())
140+
ctx4 = self.get_success(id_gen.get_next())
141+
142+
s1 = ctx1.__enter__()
143+
s2 = ctx2.__enter__()
144+
s3 = ctx3.__enter__()
145+
s4 = ctx4.__enter__()
146+
147+
self.assertEqual(s1, 8)
148+
self.assertEqual(s2, 9)
149+
self.assertEqual(s3, 10)
150+
self.assertEqual(s4, 11)
151+
152+
self.assertEqual(id_gen.get_positions(), {"master": 7})
153+
self.assertEqual(id_gen.get_current_token_for_writer("master"), 7)
154+
155+
ctx2.__exit__(None, None, None)
156+
157+
self.assertEqual(id_gen.get_positions(), {"master": 7})
158+
self.assertEqual(id_gen.get_current_token_for_writer("master"), 7)
159+
160+
ctx1.__exit__(None, None, None)
161+
162+
self.assertEqual(id_gen.get_positions(), {"master": 9})
163+
self.assertEqual(id_gen.get_current_token_for_writer("master"), 9)
164+
165+
ctx4.__exit__(None, None, None)
166+
167+
self.assertEqual(id_gen.get_positions(), {"master": 9})
168+
self.assertEqual(id_gen.get_current_token_for_writer("master"), 9)
169+
170+
ctx3.__exit__(None, None, None)
171+
172+
self.assertEqual(id_gen.get_positions(), {"master": 11})
173+
self.assertEqual(id_gen.get_current_token_for_writer("master"), 11)
174+
125175
def test_multi_instance(self):
126176
"""Test that reads and writes from multiple processes are handled
127177
correctly.

0 commit comments

Comments
 (0)