cable_room 0.6.2.beta1 → 0.7.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +122 -0
- data/README.md +662 -48
- data/cable_room.gemspec +5 -2
- data/exe/cable_room +8 -0
- data/lib/cable_room/broadcaster.rb +116 -0
- data/lib/cable_room/bus.rb +372 -0
- data/lib/cable_room/cli.rb +237 -0
- data/lib/cable_room/config.rb +112 -0
- data/lib/cable_room/host/bus_inbound.rb +36 -0
- data/lib/cable_room/host/runner.rb +577 -0
- data/lib/cable_room/host/supervisor.rb +275 -0
- data/lib/cable_room/host/worker_pool.rb +37 -0
- data/lib/cable_room/host.rb +482 -0
- data/lib/cable_room/membership_store.rb +105 -0
- data/lib/cable_room/migration.rb +586 -0
- data/lib/cable_room/periodic_timer.rb +18 -0
- data/lib/cable_room/placement.rb +260 -0
- data/lib/cable_room/ports.rb +20 -50
- data/lib/cable_room/railtie.rb +3 -12
- data/lib/cable_room/room/base.rb +45 -39
- data/lib/cable_room/room/host_adapter.rb +52 -0
- data/lib/cable_room/room/lifecycle.rb +26 -9
- data/lib/cable_room/room/port_management.rb +70 -2
- data/lib/cable_room/room/reaping.rb +34 -1
- data/lib/cable_room/room/snapshotting.rb +82 -0
- data/lib/cable_room/room/threading.rb +2 -2
- data/lib/cable_room/room/user_management.rb +27 -0
- data/lib/cable_room/room.rb +5 -2
- data/lib/cable_room/room_harness.rb +168 -0
- data/lib/cable_room/room_member.rb +293 -69
- data/lib/cable_room/room_proxy_channel.rb +13 -2
- data/lib/cable_room/snapshot.rb +136 -0
- data/lib/cable_room/version.rb +1 -1
- data/lib/cable_room.rb +57 -2
- metadata +26 -9
- data/lib/cable_room/channel_base.rb +0 -247
- data/lib/cable_room/channel_tracker.rb +0 -130
- data/lib/cable_room/room/channel_adapter.rb +0 -18
data/lib/cable_room.rb
CHANGED
|
@@ -8,10 +8,18 @@ require 'redlock'
|
|
|
8
8
|
require 'rufus-scheduler'
|
|
9
9
|
|
|
10
10
|
require_relative 'cable_room/railtie'
|
|
11
|
+
require_relative 'cable_room/config'
|
|
12
|
+
require_relative 'cable_room/broadcaster'
|
|
11
13
|
|
|
12
|
-
require_relative 'cable_room/
|
|
13
|
-
|
|
14
|
+
require_relative 'cable_room/bus'
|
|
15
|
+
|
|
16
|
+
require_relative 'cable_room/periodic_timer'
|
|
17
|
+
require_relative 'cable_room/snapshot'
|
|
18
|
+
require_relative 'cable_room/host'
|
|
19
|
+
require_relative 'cable_room/placement'
|
|
20
|
+
require_relative 'cable_room/migration'
|
|
14
21
|
require_relative 'cable_room/ports'
|
|
22
|
+
require_relative 'cable_room/membership_store'
|
|
15
23
|
require_relative 'cable_room/room_member'
|
|
16
24
|
require_relative 'cable_room/room_proxy_channel'
|
|
17
25
|
require_relative 'cable_room/room/'
|
|
@@ -36,6 +44,27 @@ module CableRoom
|
|
|
36
44
|
warn "CableRoom.error_handler raised #{handler_error.class}: #{handler_error.message}"
|
|
37
45
|
end
|
|
38
46
|
|
|
47
|
+
# The active CableRoom::Config. Built on first use with the defaults plus any env
|
|
48
|
+
# overrides, so an app that never calls `configure` still gets a valid config.
|
|
49
|
+
def config
|
|
50
|
+
@config ||= Config.new.apply_env_overrides.validate!
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Yields the config so an app can set the knobs, then re-applies the env overrides
|
|
54
|
+
# (CABLE_ROOM_HOST, CABLE_ROOM_BROADCASTER) and checks every value. Bad values raise
|
|
55
|
+
# here, at boot, instead of somewhere deep in a room later.
|
|
56
|
+
def configure
|
|
57
|
+
cfg = @config || Config.new
|
|
58
|
+
yield cfg if block_given?
|
|
59
|
+
@config = cfg.apply_env_overrides.validate!
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Drops the current config so the next `config` call rebuilds it, which also makes
|
|
63
|
+
# `Broadcaster.current` rebuild (and forget any `Broadcaster.current=` override). Meant for specs.
|
|
64
|
+
def reset_config!
|
|
65
|
+
@config = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
39
68
|
def redis_pool
|
|
40
69
|
require 'rediconn'
|
|
41
70
|
@redis_pool ||= RediConn::RedisConnection.create(env_prefix: "CABLEROOM")
|
|
@@ -50,5 +79,31 @@ module CableRoom
|
|
|
50
79
|
CableRoom.redis,
|
|
51
80
|
])
|
|
52
81
|
end
|
|
82
|
+
|
|
83
|
+
# The process-wide Redis bus (see CableRoom::Bus). Built on first use.
|
|
84
|
+
def bus
|
|
85
|
+
@bus ||= Bus.new
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Call this in a child process right after `fork`, before it touches Redis or hosts a room.
|
|
89
|
+
# `cable_room server --workers N` does it for every worker (see Host::Supervisor).
|
|
90
|
+
#
|
|
91
|
+
# A forked child starts with copies of the parent's objects but only one thread, so anything
|
|
92
|
+
# that owns a socket or a thread is broken in the child: the Bus subscriber thread doesn't
|
|
93
|
+
# exist, its mutex may be held by nobody, and a Redis socket is shared with the parent, so
|
|
94
|
+
# both processes would read each other's replies. Dropping the memos means the next caller
|
|
95
|
+
# builds fresh ones. The connection pool and redis-client also notice the fork on their own
|
|
96
|
+
# (both hook `Process._fork` and drop inherited sockets), and Rails does the same for
|
|
97
|
+
# ActiveRecord, so those need nothing from us; this keeps the gem's rule simple: nothing
|
|
98
|
+
# Redis-backed survives a fork.
|
|
99
|
+
#
|
|
100
|
+
# The Host goes too. A parent that forks workers must never host rooms itself, and a Host
|
|
101
|
+
# copied from a parent would have no worker threads and no scheduler thread behind it.
|
|
102
|
+
def after_fork!
|
|
103
|
+
@bus = nil
|
|
104
|
+
@lock_manager = nil
|
|
105
|
+
@redis_pool = nil
|
|
106
|
+
Host.replace_current(nil)
|
|
107
|
+
end
|
|
53
108
|
end
|
|
54
109
|
end
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cable_room
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0.beta2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Knapp
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
@@ -77,14 +77,14 @@ dependencies:
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - ">="
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '0'
|
|
81
|
-
type: :
|
|
80
|
+
version: '5.0'
|
|
81
|
+
type: :runtime
|
|
82
82
|
prerelease: false
|
|
83
83
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
84
|
requirements:
|
|
85
85
|
- - ">="
|
|
86
86
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '0'
|
|
87
|
+
version: '5.0'
|
|
88
88
|
- !ruby/object:Gem::Dependency
|
|
89
89
|
name: rspec
|
|
90
90
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -101,32 +101,49 @@ dependencies:
|
|
|
101
101
|
version: '3'
|
|
102
102
|
email:
|
|
103
103
|
- eknapp@instructure.com
|
|
104
|
-
executables:
|
|
104
|
+
executables:
|
|
105
|
+
- cable_room
|
|
105
106
|
extensions: []
|
|
106
107
|
extra_rdoc_files: []
|
|
107
108
|
files:
|
|
109
|
+
- CHANGELOG.md
|
|
108
110
|
- README.md
|
|
109
111
|
- cable_room.gemspec
|
|
112
|
+
- exe/cable_room
|
|
110
113
|
- lib/cable_room.rb
|
|
111
|
-
- lib/cable_room/
|
|
112
|
-
- lib/cable_room/
|
|
114
|
+
- lib/cable_room/broadcaster.rb
|
|
115
|
+
- lib/cable_room/bus.rb
|
|
116
|
+
- lib/cable_room/cli.rb
|
|
117
|
+
- lib/cable_room/config.rb
|
|
118
|
+
- lib/cable_room/host.rb
|
|
119
|
+
- lib/cable_room/host/bus_inbound.rb
|
|
120
|
+
- lib/cable_room/host/runner.rb
|
|
121
|
+
- lib/cable_room/host/supervisor.rb
|
|
122
|
+
- lib/cable_room/host/worker_pool.rb
|
|
123
|
+
- lib/cable_room/membership_store.rb
|
|
124
|
+
- lib/cable_room/migration.rb
|
|
125
|
+
- lib/cable_room/periodic_timer.rb
|
|
126
|
+
- lib/cable_room/placement.rb
|
|
113
127
|
- lib/cable_room/ports.rb
|
|
114
128
|
- lib/cable_room/railtie.rb
|
|
115
129
|
- lib/cable_room/room.rb
|
|
116
130
|
- lib/cable_room/room/base.rb
|
|
117
131
|
- lib/cable_room/room/broadcasting.rb
|
|
118
132
|
- lib/cable_room/room/callbacks.rb
|
|
119
|
-
- lib/cable_room/room/
|
|
133
|
+
- lib/cable_room/room/host_adapter.rb
|
|
120
134
|
- lib/cable_room/room/input_handling.rb
|
|
121
135
|
- lib/cable_room/room/lifecycle.rb
|
|
122
136
|
- lib/cable_room/room/port_management.rb
|
|
123
137
|
- lib/cable_room/room/port_policies.rb
|
|
124
138
|
- lib/cable_room/room/port_scoping.rb
|
|
125
139
|
- lib/cable_room/room/reaping.rb
|
|
140
|
+
- lib/cable_room/room/snapshotting.rb
|
|
126
141
|
- lib/cable_room/room/threading.rb
|
|
127
142
|
- lib/cable_room/room/user_management.rb
|
|
143
|
+
- lib/cable_room/room_harness.rb
|
|
128
144
|
- lib/cable_room/room_member.rb
|
|
129
145
|
- lib/cable_room/room_proxy_channel.rb
|
|
146
|
+
- lib/cable_room/snapshot.rb
|
|
130
147
|
- lib/cable_room/version.rb
|
|
131
148
|
homepage: https://instructure.com
|
|
132
149
|
licenses: []
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
module CableRoom
|
|
2
|
-
class ChannelBase < ActionCable::Channel::Base
|
|
3
|
-
attr_reader :room, :tenant
|
|
4
|
-
attr_reader :server, :logger
|
|
5
|
-
delegate :event_loop, :worker_pool, :pubsub, to: :server
|
|
6
|
-
|
|
7
|
-
def initialize(lock_info, room_class, key, config)
|
|
8
|
-
# We don't really have a "connection" in the ActionCable sense, so
|
|
9
|
-
# stuff in something that half looks like one
|
|
10
|
-
super(DummyConnection.new(self), "Room[]", {})
|
|
11
|
-
|
|
12
|
-
# Used mainly for logs and being able to follow a specific Room instance
|
|
13
|
-
@uuid = SecureRandom.hex(6)
|
|
14
|
-
|
|
15
|
-
@mutex = Monitor.new
|
|
16
|
-
@lock_info = lock_info
|
|
17
|
-
@current_state = :initializing
|
|
18
|
-
|
|
19
|
-
@tenant = Apartment::Tenant.current if defined?(Apartment)
|
|
20
|
-
|
|
21
|
-
@server = ChannelTracker.instance
|
|
22
|
-
@logger = ActionCable::Connection::TaggedLoggerProxy.new(
|
|
23
|
-
@server.logger,
|
|
24
|
-
tags: ["#{room_class.name} #{@uuid}"]
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
logger.info "Initializing new #{room_class.name}"
|
|
28
|
-
logger.info " UUID: #{@uuid}"
|
|
29
|
-
logger.info " Key: #{room_class.room_port_key(key)}"
|
|
30
|
-
|
|
31
|
-
@watchdog_interval = config[:watchdog_interval]
|
|
32
|
-
@lock_duration = config[:lock_duration]
|
|
33
|
-
|
|
34
|
-
@processing_work = false
|
|
35
|
-
@work_queue = []
|
|
36
|
-
|
|
37
|
-
@room = room_class.new(self, key)
|
|
38
|
-
@server.track_room_channel self
|
|
39
|
-
ping_watchdog
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.channel_name
|
|
43
|
-
module_parent.name
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def stream_from(...)
|
|
47
|
-
raise ArgumentError, "Block required" unless block_given?
|
|
48
|
-
super
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def state
|
|
52
|
-
@current_state
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
after_subscribe do
|
|
56
|
-
begin
|
|
57
|
-
@current_state = :starting
|
|
58
|
-
@room.send(:_startup)
|
|
59
|
-
@current_state = :started
|
|
60
|
-
rescue => e
|
|
61
|
-
terminate!
|
|
62
|
-
raise e
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
after_unsubscribe do
|
|
67
|
-
@mutex.synchronize do
|
|
68
|
-
@current_state = :shutting_down
|
|
69
|
-
stop_all_streams
|
|
70
|
-
begin
|
|
71
|
-
@room.send(:_shutdown)
|
|
72
|
-
ensure
|
|
73
|
-
terminate!
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def ping_watchdog
|
|
79
|
-
return if state == :dead
|
|
80
|
-
|
|
81
|
-
logger.debug "Ping watchdog"
|
|
82
|
-
@last_watchdog_ping_at = Time.current
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def check_room_watchdog
|
|
86
|
-
@mutex.synchronize do
|
|
87
|
-
return if state == :dead || state == :shutting_down
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
relock = CableRoom.lock_manager.lock(@lock_info[:resource], @lock_duration.in_milliseconds, extend: @lock_info)
|
|
91
|
-
unless relock
|
|
92
|
-
logger.warn "Lost lock, shutting down"
|
|
93
|
-
unsubscribe_from_channel
|
|
94
|
-
return
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
unless @last_watchdog_ping_at && @last_watchdog_ping_at > @watchdog_interval.ago
|
|
98
|
-
logger.warn "Watchdog timeout for room #{@room.class.name}[#{@room.key}], shutting down"
|
|
99
|
-
initiate_shutdown("Watchdog timeout")
|
|
100
|
-
return
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def initiate_shutdown(reason)
|
|
105
|
-
@mutex.synchronize do
|
|
106
|
-
return if @current_state == :dead || @current_state == :shutting_down
|
|
107
|
-
|
|
108
|
-
logger.info "Initiating shutdown: #{reason}"
|
|
109
|
-
|
|
110
|
-
# Stop streams immediately to prevent further messages from being added
|
|
111
|
-
stop_all_streams
|
|
112
|
-
|
|
113
|
-
# Append the final unsubscribe to the work queue so we can process remaining messages first
|
|
114
|
-
post_work(async: false) do
|
|
115
|
-
unsubscribe_from_channel
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
@current_state = :shutting_down
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def terminate!
|
|
123
|
-
@mutex.synchronize do
|
|
124
|
-
stop_all_streams
|
|
125
|
-
@current_state = :dead
|
|
126
|
-
CableRoom.lock_manager.unlock(@lock_info) if @lock_info
|
|
127
|
-
server.untrack_room_channel self
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def _post_wrapped_work(async: false, silent: false, &blk)
|
|
132
|
-
if async
|
|
133
|
-
# Async stuff is mostly untracked - we just post it to the worker pool and forget about it
|
|
134
|
-
worker_pool.executor.post(&blk)
|
|
135
|
-
else
|
|
136
|
-
@mutex.synchronize do
|
|
137
|
-
if @current_state == :dead || @current_state == :shutting_down
|
|
138
|
-
raise "Attempt to post work to dead or shutting down room" unless silent
|
|
139
|
-
return
|
|
140
|
-
end
|
|
141
|
-
@work_queue << blk
|
|
142
|
-
end
|
|
143
|
-
schedule_work
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def post_work(**kwargs, &blk)
|
|
148
|
-
_post_wrapped_work(**kwargs) do
|
|
149
|
-
worker_pool.invoke(self, :instance_exec, connection: self, &blk)
|
|
150
|
-
rescue => e
|
|
151
|
-
report_work_error(e)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Work errors are swallowed so one bad message can't take the Room down with it. Log the
|
|
156
|
-
# backtrace and hand the error to the application so the failure is still discoverable.
|
|
157
|
-
def report_work_error(error)
|
|
158
|
-
logger.error "Error during work execution: #{error.class.name}: #{error.message}"
|
|
159
|
-
Array(error.backtrace).first(20).each { |line| logger.error " #{line}" }
|
|
160
|
-
|
|
161
|
-
CableRoom.report_error(
|
|
162
|
-
error,
|
|
163
|
-
room: room,
|
|
164
|
-
room_class: room&.class,
|
|
165
|
-
room_key: room&.key,
|
|
166
|
-
channel: self
|
|
167
|
-
)
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def beat
|
|
171
|
-
post_work(async: true) do
|
|
172
|
-
check_room_watchdog
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def transmit(*args)
|
|
177
|
-
logger.info("Channel.transmit called, ignoring: #{args.inspect}")
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
protected
|
|
181
|
-
|
|
182
|
-
def start_periodic_timer(callback, every:)
|
|
183
|
-
raise "Attempt to start periodic timer on a dead room" if state == :dead || state == :shutting_down
|
|
184
|
-
|
|
185
|
-
job = connection.server.scheduler.schedule_every(every) do
|
|
186
|
-
post_work(async: false, silent: true) do
|
|
187
|
-
instance_exec(&callback)
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
PeriodicTimer.new(job)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def schedule_work
|
|
195
|
-
@mutex.synchronize do
|
|
196
|
-
return if @processing_work
|
|
197
|
-
|
|
198
|
-
work = @work_queue.shift
|
|
199
|
-
return unless work
|
|
200
|
-
|
|
201
|
-
@processing_work = true
|
|
202
|
-
|
|
203
|
-
worker_pool.executor.post do
|
|
204
|
-
begin
|
|
205
|
-
work.call
|
|
206
|
-
ensure
|
|
207
|
-
@mutex.synchronize do
|
|
208
|
-
@processing_work = false
|
|
209
|
-
end
|
|
210
|
-
schedule_work
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
# ActionCable's `stop_periodic_timers` calls #shutdown on whatever #start_periodic_timer
|
|
218
|
-
# returned, but Rufus jobs are cancelled with #unschedule. Adapt the one to the other here
|
|
219
|
-
# rather than aliasing #shutdown onto Rufus::Scheduler::Job for the whole process.
|
|
220
|
-
class PeriodicTimer
|
|
221
|
-
attr_reader :job
|
|
222
|
-
|
|
223
|
-
delegate :unschedule, :scheduled?, :next_time, to: :job
|
|
224
|
-
|
|
225
|
-
def initialize(job)
|
|
226
|
-
@job = job
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def shutdown
|
|
230
|
-
job.unschedule
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
class DummyConnection
|
|
235
|
-
attr_reader :channel
|
|
236
|
-
delegate :server, :logger, :tenant, :transmit, :post_work, :_post_wrapped_work,
|
|
237
|
-
:report_work_error, to: :channel
|
|
238
|
-
delegate :event_loop, :pubsub, :worker_pool, to: :server
|
|
239
|
-
|
|
240
|
-
attr_reader :identifiers
|
|
241
|
-
|
|
242
|
-
def initialize(channel)
|
|
243
|
-
@channel = channel
|
|
244
|
-
@identifiers = []
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
end
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
module CableRoom
|
|
2
|
-
class ChannelTracker
|
|
3
|
-
def self.instance
|
|
4
|
-
@instance ||= new
|
|
5
|
-
end
|
|
6
|
-
|
|
7
|
-
BEAT_INTERVAL = 5.seconds
|
|
8
|
-
|
|
9
|
-
delegate :logger, :pubsub, :event_loop, :config, to: :cable_server
|
|
10
|
-
|
|
11
|
-
attr_reader :room_channels, :scheduler
|
|
12
|
-
|
|
13
|
-
def initialize
|
|
14
|
-
@room_channels = Set.new
|
|
15
|
-
@monitor = Monitor.new
|
|
16
|
-
|
|
17
|
-
@scheduler = Rufus::Scheduler.new
|
|
18
|
-
|
|
19
|
-
at_exit do
|
|
20
|
-
logger.info "Shutting down CableRoom"
|
|
21
|
-
shutdown!
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
scheduler.every(BEAT_INTERVAL) do
|
|
25
|
-
each_room_channel do |chan|
|
|
26
|
-
chan.beat
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def worker_pool
|
|
32
|
-
# TODO Pin Rooms to a specific thread so that they never have to worry about thread safety?
|
|
33
|
-
@worker_pool || @monitor.synchronize { @worker_pool ||= ThreadPool.new(max_size: config.worker_pool_size) }
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def track_room_channel(chan)
|
|
37
|
-
raise "Cannot add Room after shutdown" if @shutdown
|
|
38
|
-
@room_channels << chan
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def untrack_room_channel(chan)
|
|
42
|
-
@room_channels.delete(chan)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def shutdown?
|
|
46
|
-
@shutdown
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def shutdown!
|
|
50
|
-
@monitor.synchronize do
|
|
51
|
-
@shutdown = true
|
|
52
|
-
|
|
53
|
-
scheduler.shutdown
|
|
54
|
-
|
|
55
|
-
shutdown_rooms!
|
|
56
|
-
|
|
57
|
-
worker_pool.executor.shutdown
|
|
58
|
-
worker_pool.executor.wait_for_termination(5)
|
|
59
|
-
worker_pool.executor.kill
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def shutdown_rooms!
|
|
64
|
-
@monitor.synchronize do
|
|
65
|
-
each_room_channel do |chan|
|
|
66
|
-
chan.initiate_shutdown("Server shutting down")
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
count = 0
|
|
71
|
-
loop do
|
|
72
|
-
break if @room_channels.empty? || count >= 15
|
|
73
|
-
sleep 1
|
|
74
|
-
count += 1
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def each_room_channel(&blk)
|
|
79
|
-
@room_channels.dup.each(&blk)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
def cable_server
|
|
85
|
-
ActionCable.server
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
class ThreadPool < ActionCable::Server::Worker
|
|
89
|
-
set_callback :work, :around do |_, blk|
|
|
90
|
-
pconn = ActionCable::Server::Worker.connection
|
|
91
|
-
ActionCable::Server::Worker.connection = connection
|
|
92
|
-
blk.call
|
|
93
|
-
ensure
|
|
94
|
-
ActionCable::Server::Worker.connection = pconn
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# ActionCable's Worker#invoke reduces every exception to a log line and a no-argument
|
|
98
|
-
# `handle_exception` call, which discards the error itself. Rooms run all of their work
|
|
99
|
-
# through here, so report it properly instead.
|
|
100
|
-
def invoke(receiver, method, *args, connection:, &block)
|
|
101
|
-
work(connection) do
|
|
102
|
-
receiver.send method, *args, &block
|
|
103
|
-
rescue Exception => e
|
|
104
|
-
if connection.respond_to?(:report_work_error)
|
|
105
|
-
connection.report_work_error(e)
|
|
106
|
-
else
|
|
107
|
-
logger.error "There was an exception - #{e.class}(#{e.message})"
|
|
108
|
-
logger.error Array(e.backtrace).join("\n")
|
|
109
|
-
CableRoom.report_error(e, connection: connection)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def async_invoke(receiver, method, *args, connection: receiver, &block)
|
|
115
|
-
# Instead of posting directly to the global pool, post to a dedicated queue for the room/"connection".
|
|
116
|
-
# This makes each rooms so that they can be processed by at-most-one thread at a time, while still
|
|
117
|
-
# allowing multiple rooms to be processed by the same thread-pool.
|
|
118
|
-
|
|
119
|
-
# TODO Implement more of a round-robin approach so that no room can starve out others?
|
|
120
|
-
|
|
121
|
-
# "connection" here really references the Channel, since "Connections" in this context don't really exist
|
|
122
|
-
|
|
123
|
-
connection._post_wrapped_work(async: false) do
|
|
124
|
-
invoke(receiver, method, *args, connection: connection, &block)
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
end
|
|
130
|
-
end
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
module CableRoom
|
|
2
|
-
module Room
|
|
3
|
-
module ChannelAdapter
|
|
4
|
-
extend ActiveSupport::Concern
|
|
5
|
-
|
|
6
|
-
Channel = CableRoom::ChannelBase
|
|
7
|
-
|
|
8
|
-
class_methods do
|
|
9
|
-
def inherited(subclass)
|
|
10
|
-
# Descend from *this* class's Channel, so periodic timers and other channel-level
|
|
11
|
-
# configuration survive multiple levels of subclassing
|
|
12
|
-
subclass.const_set(:Channel, Class.new(self::Channel))
|
|
13
|
-
super
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|