Skip to content

Commit 50186b9

Browse files
Rework concurrency control to avoid thread locks (#54)
This patch was created by Hailey Somerville in #52 (comment). All the credit to her. Co-authored-by: Hailey Somerville <[email protected]>
1 parent 7cce173 commit 50186b9

File tree

2 files changed

+37
-28
lines changed

2 files changed

+37
-28
lines changed

lib/action_cable/subscription_adapter/solid_cable.rb

+36-26
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "action_cable/subscription_adapter/base"
44
require "action_cable/subscription_adapter/channel_prefix"
55
require "action_cable/subscription_adapter/subscriber_map"
6+
require "concurrent/atomic/semaphore"
67

78
module ActionCable
89
module SubscriptionAdapter
@@ -38,34 +39,53 @@ def listener
3839
end
3940

4041
class Listener < ::ActionCable::SubscriptionAdapter::SubscriberMap
42+
Stop = Class.new(Exception)
43+
4144
def initialize(event_loop)
4245
super()
4346

4447
@event_loop = event_loop
4548

49+
# Critical section begins with 0 permits. It can be understood as
50+
# being "normally held" by the listener thread. It is released
51+
# for specific sections of code, rather than acquired.
52+
@critical = Concurrent::Semaphore.new(0)
53+
4654
@thread = Thread.new do
47-
Thread.current.abort_on_exception = true
4855
listen
4956
end
5057
end
5158

5259
def listen
5360
loop do
54-
break unless running?
55-
56-
with_polling_volume { broadcast_messages }
61+
begin
62+
instance = interruptible { Rails.application.executor.run! }
63+
with_polling_volume { broadcast_messages }
64+
ensure
65+
instance.complete! if instance
66+
end
5767

58-
interruptible_sleep ::SolidCable.polling_interval
68+
interruptible { sleep ::SolidCable.polling_interval }
5969
end
70+
rescue Stop
71+
ensure
72+
@critical.release
6073
end
6174

62-
def shutdown
63-
self.running = false
64-
wake_up
75+
def interruptible
76+
@critical.release
77+
yield
78+
ensure
79+
@critical.acquire
80+
end
6581

66-
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
67-
thread&.join
68-
end
82+
def shutdown
83+
@critical.acquire
84+
# We have the critical permit, and so the listen thread must be
85+
# safe to interrupt.
86+
thread.raise(Stop)
87+
@critical.release
88+
thread.join
6989
end
7090

7191
def add_channel(channel, on_success)
@@ -83,15 +103,7 @@ def invoke_callback(*)
83103

84104
private
85105
attr_reader :event_loop, :thread
86-
attr_writer :running, :last_id
87-
88-
def running?
89-
if defined?(@running)
90-
@running
91-
else
92-
self.running = true
93-
end
94-
end
106+
attr_writer :last_id
95107

96108
def last_id
97109
@last_id ||= ::SolidCable::Message.maximum(:id) || 0
@@ -102,12 +114,10 @@ def channels
102114
end
103115

104116
def broadcast_messages
105-
Rails.application.executor.wrap do
106-
::SolidCable::Message.broadcastable(channels, last_id).
107-
each do |message|
108-
broadcast(message.channel, message.payload)
109-
self.last_id = message.id
110-
end
117+
::SolidCable::Message.broadcastable(channels, last_id).
118+
each do |message|
119+
broadcast(message.channel, message.payload)
120+
self.last_id = message.id
111121
end
112122
end
113123

test/config_stubs.rb

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ def executor
1818
end
1919

2020
class ExectorStub
21-
def wrap(&block)
22-
block.call
21+
def run!
2322
end
2423
end
2524
end

0 commit comments

Comments
 (0)