Skip to content

Commit bc2576c

Browse files
committed
fix: do not absorb a signal while subscribing
A notification arriving after listen released the mutex but before wait snapshotted the counter became the new baseline, and its broadcast reached no registered waiter, so the role slept until its polling interval expired. Snapshot before subscribing and re-check before blocking, so a signal delivered while a waiter was getting ready is observed rather than swallowed.
1 parent 3f4d299 commit bc2576c

3 files changed

Lines changed: 28 additions & 1 deletion

File tree

lib/solid_objects/wake_up_adapters/redis.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,17 @@ def signal
4949
false
5050
end
5151

52+
# The counter is snapshotted before subscribing, and re-checked before
53+
# blocking, so a signal delivered while this caller was still getting
54+
# ready is observed rather than absorbed into the new baseline.
5255
# @rbs (timeout: Numeric) -> bool
5356
def wait(timeout:)
57+
signalled = mutex.synchronize { @signalled }
5458
return paced_failure(timeout) unless listen
5559

5660
mutex.synchronize do
57-
signalled = @signalled
61+
return true unless @signalled == signalled
62+
5863
condition.wait(mutex, timeout.to_f)
5964
@signalled != signalled
6065
end

sig/generated/lib/solid_objects/wake_up_adapters/redis.rbs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ module SolidObjects
4040
# @rbs () -> bool
4141
def signal: () -> bool
4242

43+
# The counter is snapshotted before subscribing, and re-checked before
44+
# blocking, so a signal delivered while this caller was still getting
45+
# ready is observed rather than absorbed into the new baseline.
4346
# @rbs (timeout: Numeric) -> bool
4447
def wait: (timeout: Numeric) -> bool
4548

test/integration/redis_wake_up_test.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ class RedisWakeUpTest < ActiveSupport::TestCase
7878
assert subscribers.alive?, "one background subscription should serve the process"
7979
end
8080

81+
test "a signal arriving while a waiter is subscribing is not absorbed" do
82+
@adapter.listen
83+
real = @adapter.method(:listen)
84+
# Publish inside listen, modelling a signal that lands after the counter
85+
# would have been snapshotted but before the waiter blocks.
86+
@adapter.define_singleton_method(:listen) do
87+
result = real.call
88+
SolidObjects::WakeUpAdapters::Redis.new(url: REDIS_URL).tap(&:signal).stop
89+
sleep 0.1
90+
result
91+
end
92+
started = monotonic_now
93+
94+
woken = @adapter.wait(timeout: 5)
95+
96+
assert woken, "the signal must not be absorbed by the subscribing waiter"
97+
assert_operator monotonic_now - started, :<, 1.0
98+
end
99+
81100
test "signalling never raises into the caller" do
82101
broken = SolidObjects::WakeUpAdapters::Redis.new(url: "redis://127.0.0.1:1/0")
83102

0 commit comments

Comments
 (0)