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

Fix rare error in ReadWriteLock when writers complete immediately #12105

Merged
merged 2 commits into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/12105.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix an extremely rare, long-standing bug in `ReadWriteLock` that would cause an error when a newly unblocked writer completes instantly.
5 changes: 4 additions & 1 deletion synapse/util/async_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,10 @@ def _ctx_manager() -> Iterator[None]:
finally:
with PreserveLoggingContext():
new_defer.callback(None)
if self.key_to_current_writer[key] == new_defer:
# `self.key_to_current_writer[key]` may be missing if there was another
# writer waiting for us and it completed entirely within the
# `new_defer.callback()` call above.
if self.key_to_current_writer.get(key) == new_defer:
self.key_to_current_writer.pop(key)

return _ctx_manager()
Expand Down
30 changes: 30 additions & 0 deletions tests/util/test_rwlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

from twisted.internet import defer
from twisted.internet.defer import Deferred

from synapse.util.async_helpers import ReadWriteLock

Expand Down Expand Up @@ -83,3 +84,32 @@ def test_rwlock(self):
self.assertTrue(d.called)
with d.result:
pass

def test_lock_handoff_to_nonblocking_writer(self):
"""Test a writer handing the lock to another writer that completes instantly."""
rwlock = ReadWriteLock()
key = "key"

unblock: "Deferred[None]" = Deferred()

async def blocking_write():
with await rwlock.write(key):
await unblock

async def nonblocking_write():
with await rwlock.write(key):
pass

d1 = defer.ensureDeferred(blocking_write())
d2 = defer.ensureDeferred(nonblocking_write())
self.assertFalse(d1.called)
self.assertFalse(d2.called)

# Unblock the first writer. The second writer will complete without blocking.
unblock.callback(None)
self.assertTrue(d1.called)
self.assertTrue(d2.called)

# The `ReadWriteLock` should operate as normal.
d3 = defer.ensureDeferred(nonblocking_write())
self.assertTrue(d3.called)