cable_room 0.7.0.beta3 → 0.8.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.
@@ -1,8 +1,8 @@
1
- require 'socket'
2
1
  require_relative 'host/worker_pool'
3
2
  require_relative 'host/bus_inbound'
4
3
  require_relative 'host/runner'
5
4
  require_relative 'host/supervisor'
5
+ require_relative 'host/placement'
6
6
 
7
7
  module CableRoom
8
8
  # One Host per process. It owns every Room running here: the worker pool their work runs on,
@@ -20,8 +20,8 @@ module CableRoom
20
20
  class NotHosting < StandardError; end
21
21
 
22
22
  NOT_HOSTING_MESSAGE =
23
- "This process doesn't host rooms: CableRoom.config.room_host is :remote, so rooms run " \
24
- "under `cable_room server`. Start rooms there (or call CableRoom::Host.start! first if this " \
23
+ "This process doesn't host rooms: CableRoom.room_host is :remote, so rooms run under " \
24
+ "`cable_room server`. Start rooms there (or call CableRoom::Host.start! first if this " \
25
25
  "process really should host them).".freeze
26
26
 
27
27
  @start_mutex = Mutex.new
@@ -39,88 +39,53 @@ module CableRoom
39
39
  # (`cable_room server`) has one; anyone else gets NotHosting, so a room can never quietly
40
40
  # start in the web tier because some code path reached for the Host.
41
41
  def instance
42
- current || (CableRoom.config.inline? ? start! : raise(NotHosting, NOT_HOSTING_MESSAGE))
43
- end
44
-
45
- # Make this process a rooms host. Builds the Host once and starts its provisioning listener,
46
- # so this process hears `create: true` requests from then on; later calls return the same
47
- # Host. `cable_room server` calls this explicitly; in :inline `instance` calls it for you.
48
- def start!
49
- @start_mutex.synchronize { @current ||= new.tap(&:start_placement) }
42
+ current || (CableRoom.inline? ? start! : raise(NotHosting, NOT_HOSTING_MESSAGE))
43
+ end
44
+
45
+ # Make this process a rooms host. Builds the Host once, with `options` (see `new`); later
46
+ # calls return the same one and ignore them. `cable_room server` calls this explicitly; in
47
+ # :inline `instance` calls it for you.
48
+ #
49
+ # In :remote it also starts the Host's Placement, so from then on this process hears
50
+ # `create: true` members' provision requests and races its peers for their rooms. In :inline
51
+ # nobody sends those (a member starts its room itself), so there's nothing to listen for.
52
+ def start!(**options)
53
+ @start_mutex.synchronize do
54
+ @current ||= begin
55
+ host = new(**options)
56
+ host.start_placement if CableRoom.remote?
57
+ host
58
+ rescue StandardError
59
+ host&.shutdown!
60
+ raise
61
+ end
62
+ end
50
63
  end
51
64
 
52
65
  # Swap this process's Host for `host` (nil allowed) and return the previous one. Only specs
53
- # that play two process roles in one Ruby process should need this (see RoomHostHarness):
54
- # the "web" side has to see no Host while another Host runs the rooms.
66
+ # that play both process roles in one Ruby process need this: the web side has to see no
67
+ # Host while another Host runs the rooms.
55
68
  def replace_current(host)
56
69
  @start_mutex.synchronize { @current.tap { @current = host } }
57
70
  end
58
71
  end
59
72
 
60
- # How many rooms `drain!` hands off at once before `drain_timeout`. Each handoff costs the
61
- # adopting peer a full room restore (its startup chain, often database reads), so a host with
62
- # hundreds of rooms must not offer them all in the same second; and each one holds a thread
63
- # here for up to `handoff_timeout`. Four keeps a big host draining in well under the default
64
- # ten minutes (a handoff normally takes well under a second) without swamping the fleet.
65
- DRAIN_CONCURRENCY = 4
66
-
67
- # What `drain!` returns: how the rooms ended up, and how long it took. `migrated` and
68
- # `closed` are Migration objects (see their `state` and `room`).
69
- DrainResult = Struct.new(:reason, :migrated, :closed, :duration, keyword_init: true) do
70
- def rooms
71
- migrated.size + closed.size
72
- end
73
- end
74
-
75
73
  attr_reader :scheduler, :inbound, :placement
76
74
 
77
- # True once this host has started shutting down for good and is handing its rooms off (see
78
- # `drain!`). A draining host never claims a provision request, so it can't take on a room it
79
- # would only have to hand off again.
80
- attr_writer :draining
81
-
82
75
  delegate :logger, to: :cable_server
83
76
 
84
- class << self
85
- # Register a block to run when a `drain!` finishes, with the Host and its DrainResult. An
86
- # app that scales in through an ASG termination lifecycle hook completes the hook here
87
- # (`complete-lifecycle-action`), once every room has moved or closed. Blocks run on the
88
- # draining thread before `drain!` returns; one that raises is reported and doesn't stop
89
- # the others.
90
- def after_drain(&blk)
91
- raise ArgumentError, "after_drain needs a block" unless blk
92
- after_drain_callbacks << blk
93
- blk
94
- end
95
-
96
- def after_drain_callbacks
97
- @after_drain_callbacks ||= []
98
- end
99
- end
100
-
101
- # `inbound` is how rooms receive messages: by default the process-wide Bus, so a room hears
102
- # its members the same way whether it runs inside the web process or on a remote host.
103
- def initialize(inbound: BusInbound.new)
77
+ # `inbound` is how rooms receive messages: by default the process-wide Bus, waiting up to
78
+ # `subscribe_timeout` seconds for Redis to confirm each room's subscriptions (a room's startup
79
+ # blocks on that). Anything with BusInbound's two methods works.
80
+ def initialize(inbound: nil, subscribe_timeout: Bus::DEFAULT_TIMEOUT)
104
81
  @runners = Set.new
105
82
  @monitor = Monitor.new
106
- @inbound = inbound
107
- @draining = false
108
- @drain_result = nil
109
- @drain_thread = nil
110
- @migrations = []
83
+ @inbound = inbound || BusInbound.new(timeout: subscribe_timeout)
111
84
  @scheduler = Rufus::Scheduler.new
112
85
 
113
- # A rooms host that leaves with rooms still open (a crash on the main thread, say; on a
114
- # SIGTERM `cable_room server` has already drained by now, so this is a no-op) gives them
115
- # the same chance to move as a planned shutdown would. In :inline there is no fleet of
116
- # peers by design (see `drain!`), so exit closes rooms the way it always has.
117
86
  at_exit do
118
87
  logger.info "Shutting down CableRoom"
119
- if CableRoom.config.remote? && !shutdown? && runners.any?
120
- drain!(reason: "process exit")
121
- else
122
- shutdown!
123
- end
88
+ shutdown!
124
89
  end
125
90
 
126
91
  # Every few seconds each room renews its Redlock and checks its watchdog
@@ -130,118 +95,56 @@ module CableRoom
130
95
  end
131
96
 
132
97
  def worker_pool
98
+ # TODO Pin Rooms to a specific thread so that they never have to worry about thread safety?
133
99
  @worker_pool || @monitor.synchronize do
134
100
  @worker_pool ||= WorkerPool.new(max_size: cable_server.config.worker_pool_size)
135
101
  end
136
102
  end
137
103
 
138
104
  # Start `room_class`'s room for `key` on this Host if nobody else is running it. Takes the
139
- # room's Redlock first, so exactly one process cluster-wide wins; returns false when the lock
140
- # is already held. This is the host-side way to bring a room up: `Room::Base.ensure` calls it
141
- # on the process Host, Placement calls it for a provision request, and `cable_room server`
142
- # (or a spec harness) can call it on a Host it owns without any member asking.
105
+ # room's Redlock first, so exactly one process wins; returns false when the lock is held.
106
+ # `Room::Base.ensure` calls this on the process Host; `cable_room server` (or a spec playing
107
+ # one) can call it on a Host it owns.
143
108
  #
144
- # If a snapshot of the room is on offer (another host is handing it off, see
145
- # CableRoom::Migration), winning the lock means adopting it: the room comes back here with
146
- # its members and state rather than starting fresh underneath the old host. The check is
147
- # made after the lock is won, so it can't be raced. `handoff_only: true` (a handoff request)
148
- # starts nothing when there is no snapshot to adopt — it has expired, or the old host took
149
- # the room back — and lets the lock go.
150
- def ensure_room(room_class, key = nil, handoff_only: false, tenant: nil)
151
- lock_key = room_class.room_port_key(key)
152
- lock_info = CableRoom.lock_manager.lock(lock_key, room_class::LOCK_DURATION.in_milliseconds)
153
- return false unless lock_info
154
-
155
- snapshot = bus.get(Bus.snapshot_key(lock_key))
156
- return Migration.adopt(self, room_class, key, lock_info: lock_info, snapshot: snapshot) if snapshot
157
-
158
- if handoff_only
159
- logger.info "No snapshot to adopt for #{lock_key}; letting the lock go"
160
- CableRoom.lock_manager.unlock(lock_info)
161
- return false
162
- end
109
+ # `tenant` is the Apartment tenant the room runs in. It defaults to the caller's, which is
110
+ # right for a member's own request thread (:inline). A rooms process has no request tenant,
111
+ # so whatever starts a room there must pass it. The lock key, the room's stream names, and its
112
+ # startup all depend on it, so the whole start runs switched into it.
113
+ def ensure_room(room_class, key = nil, tenant: CableRoom.current_tenant)
114
+ CableRoom.with_tenant(tenant) do
115
+ lock_key = room_class.room_port_key(key)
116
+ # One attempt: a held lock means the room is already running, and retrying (Redlock's
117
+ # default is 3 retries, 200-250 ms apart) would park the caller's ActionCable worker for
118
+ # ~0.7 s on every `create: true` member's ping.
119
+ lock_info = CableRoom.lock_manager.lock(lock_key, room_class::LOCK_DURATION.in_milliseconds, retry_count: 0)
120
+ next false unless lock_info
121
+
122
+ begin
123
+ start_room(
124
+ room_class,
125
+ key,
126
+ lock_info,
127
+ watchdog_interval: room_class::WATCH_DOG_INTERVAL,
128
+ lock_duration: room_class::LOCK_DURATION,
129
+ tenant: tenant,
130
+ )
131
+ rescue => e
132
+ CableRoom.lock_manager.unlock(lock_info)
133
+ raise e
134
+ end
163
135
 
164
- start_room(
165
- room_class,
166
- key,
167
- lock_info,
168
- watchdog_interval: room_class::WATCH_DOG_INTERVAL,
169
- lock_duration: room_class::LOCK_DURATION,
170
- tenant: tenant,
171
- )
172
-
173
- true
174
- rescue => e
175
- CableRoom.lock_manager.unlock(lock_info) if lock_info
176
- raise e
136
+ true
137
+ end
177
138
  end
178
139
 
179
140
  # Start running a room here. The caller (`ensure_room`) has already taken the room's Redlock;
180
- # the runner renews it from now on and releases it when the room dies. `tenant:` comes from the
181
- # member's provision request (see RoomMembership#initialize / #maybe_provision_room and
182
- # Placement#claim), already defaulted there to the ambient tenant if the app didn't pass one
183
- # explicitly -- nil past that point just means no Apartment multi-tenancy is in play.
184
- def start_room(room_class, key, lock_info, watchdog_interval:, lock_duration:, tenant: nil)
141
+ # the runner renews it from now on and releases it when the room dies.
142
+ def start_room(room_class, key, lock_info, watchdog_interval:, lock_duration:, tenant: CableRoom.current_tenant)
185
143
  runner = Runner.new(self, room_class, key, lock_info, watchdog_interval:, lock_duration:, tenant:)
186
144
  runner.start!
187
145
  runner
188
146
  end
189
147
 
190
- # Bring a room back from a CableRoom::Snapshot on this Host, the way a migration's adopter
191
- # does. With no `lock_info`, takes the room's Redlock first and returns false if someone else
192
- # holds it — the same rule as `ensure_room`. With one, the caller has already claimed the lock.
193
- # Raises Snapshot::UnknownVersion or Snapshot::UnknownRoomClass before anything is built if
194
- # the snapshot can't be used here. `hold_inbound: true` brings the room up frozen, holding
195
- # its inbound until `Runner#thaw!` (see Runner#restore!).
196
- #
197
- # If the room fails to come up, the lock is released either way — the caller's too. A room
198
- # that can't start here must not keep its key locked: the migration that handed it over
199
- # takes the lock back and restarts it as soon as the lock is free.
200
- def restore_room(snapshot, lock_info = nil, hold_inbound: false)
201
- snapshot = Snapshot.validate!(snapshot)
202
- room_class = Snapshot.room_class_for(snapshot)
203
- key = Snapshot.key_for(snapshot)
204
- lock_key = room_class.room_port_key(key)
205
-
206
- if lock_info
207
- unless lock_info[:resource] == lock_key
208
- raise ArgumentError, "lock_info is for #{lock_info[:resource].inspect}, but the snapshot is #{lock_key}"
209
- end
210
- else
211
- lock_info = CableRoom.lock_manager.lock(lock_key, room_class::LOCK_DURATION.in_milliseconds)
212
- return false unless lock_info
213
- end
214
-
215
- runner = Runner.new(
216
- self,
217
- room_class,
218
- key,
219
- lock_info,
220
- watchdog_interval: room_class::WATCH_DOG_INTERVAL,
221
- lock_duration: room_class::LOCK_DURATION,
222
- tenant: snapshot["tenant"],
223
- )
224
- runner.restore!(snapshot, hold_inbound: hold_inbound)
225
- runner
226
- rescue => e
227
- # `restore!` releases the lock itself when the room fails to come up; this covers a failure
228
- # before the runner exists
229
- CableRoom.lock_manager.unlock(lock_info) if lock_info && runner.nil?
230
- raise e
231
- end
232
-
233
- # A name for this host in migration traffic and instrumentation (`from_host`, `to_host`):
234
- # machine, pid, and a few random characters, so two hosts in one process (a spec) differ too.
235
- def id
236
- @id ||= "#{Socket.gethostname}:#{Process.pid}:#{SecureRandom.hex(3)}"
237
- end
238
-
239
- # The Bus this host's rooms listen on, for the migration's list and key operations. Falls
240
- # back to the process-wide Bus when `inbound` isn't Bus-backed.
241
- def bus
242
- inbound.respond_to?(:bus) ? inbound.bus : CableRoom.bus
243
- end
244
-
245
148
  def track(runner)
246
149
  @monitor.synchronize do
247
150
  raise "Cannot add Room after shutdown" if @shutdown
@@ -262,128 +165,37 @@ module CableRoom
262
165
  runners.map(&:room)
263
166
  end
264
167
 
265
- # How many member ports are attached to rooms on this Host right now. This is the load
266
- # number the design scales the rooms pool on, so an app can publish it as a gauge. It reads
267
- # each room's port table from outside the room's thread; a count that's a moment stale is
268
- # fine for a metric.
269
- def open_ports
270
- rooms.sum(&:open_port_count)
271
- end
272
-
273
- # How many rooms this Host runs right now. This is the load number provisioning weights its
274
- # delay by: a host with more rooms waits longer before racing for a new one.
275
- def open_rooms_count
276
- @monitor.synchronize { @runners.size }
277
- end
278
-
279
168
  def each_runner(&blk)
280
169
  runners.each(&blk)
281
170
  end
282
171
 
283
- def draining?
284
- @draining
285
- end
286
-
287
- # True once `drain!` has finished (every room moved or closed, and the host is shut down).
288
- def drained?
289
- !@drain_result.nil?
290
- end
291
-
292
- # The result of the drain, or nil until `drain!` has finished.
293
- def drain_result
294
- @drain_result
295
- end
296
-
297
- # Every CableRoom::Migration this host's drain has started, oldest room first. Each one's
298
- # `state` says where it is; the list is complete once `drained?`.
299
- def migrations
300
- @monitor.synchronize { @migrations.dup }
172
+ # How many rooms this Host runs right now. Cheap enough to publish as a gauge.
173
+ def open_rooms_count
174
+ @monitor.synchronize { @runners.size }
301
175
  end
302
176
 
303
- # Planned shutdown: move every room here to a peer host, then shut down. This is what SIGTERM
304
- # means to `cable_room server` (see CableRoom::Migration for the per-room protocol):
305
- #
306
- # 1. Mark the host draining, so its Placement claims nothing more (a room it took now would
307
- # only have to move again), and instrument `host_draining.cable_room`.
308
- # 2. Hand rooms off oldest first, DRAIN_CONCURRENCY at a time. A room nobody adopts within
309
- # `handoff_timeout` closes with `room_closed`; so does one that can't be snapshotted.
310
- # 3. When `drain_timeout` passes with rooms still waiting their turn, offer all of them at
311
- # once — the same path, just no longer throttled — so the drain ends within about one
312
- # more `handoff_timeout` whatever the fleet does.
313
- # 4. `shutdown!`, run the `Host.after_drain` callbacks, and return a DrainResult.
314
- #
315
- # Blocks until done. Runs once: a second call (from another thread, or `at_exit` after
316
- # `cable_room server` has already drained) waits for the first and returns its result.
317
- # `drain_timeout` and `handoff_timeout` default to the config's; specs pass short ones.
318
- #
319
- # In :inline the peers are the other web processes hosting rooms, if any. The design gives
320
- # :inline no fleet of its own, so the gem doesn't drain there on exit (see `at_exit`); an app
321
- # running several inline processes may still call this from its own signal handling.
322
- def drain!(reason:, drain_timeout: nil, handoff_timeout: nil, concurrency: DRAIN_CONCURRENCY)
323
- other = @monitor.synchronize do
324
- return @drain_result if @drain_result
325
- @drain_thread.tap { @drain_thread ||= Thread.current }
326
- end
327
- if other
328
- other.join unless other == Thread.current
329
- return @drain_result
330
- end
331
-
332
- drain_timeout ||= CableRoom.config.drain_timeout
333
- handoff_timeout ||= CableRoom.config.handoff_timeout
334
- started = monotonic_now
335
- self.draining = true
336
-
337
- queued = runners.sort_by(&:started_at)
338
- migrations = queued.map { |runner| Migration.new(self, runner, reason: reason, handoff_timeout: handoff_timeout) }
339
- @monitor.synchronize { @migrations = migrations }
340
- logger.info "Draining #{migrations.size} room(s): #{reason}"
341
-
342
- payload = { host: self, rooms: queued.map(&:room), reason: reason }
343
- ActiveSupport::Notifications.instrument("host_draining.cable_room", payload) do |event|
344
- run_migrations(migrations, concurrency: concurrency, deadline: started + drain_timeout.to_f)
345
- event[:migrated] = migrations.count(&:migrated?)
346
- event[:closed] = migrations.count { |m| !m.migrated? }
347
- end
348
-
349
- shutdown!
350
-
351
- result = DrainResult.new(
352
- reason: reason,
353
- migrated: migrations.select(&:migrated?),
354
- closed: migrations.reject(&:migrated?),
355
- duration: monotonic_now - started,
356
- )
357
- logger.info "Drained: #{result.migrated.size} room(s) migrated, #{result.closed.size} closed, in #{result.duration.round(2)}s"
358
- @monitor.synchronize { @drain_result = result }
359
-
360
- self.class.after_drain_callbacks.each do |callback|
361
- callback.call(self, result)
362
- rescue => e
363
- CableRoom.report_error(e, host: self, drain_result: result)
364
- end
365
-
366
- result
177
+ # How many member ports are attached across every room here. It reads each room's port table
178
+ # from outside the room's thread; a count that's a moment stale is fine for a metric.
179
+ def open_ports
180
+ rooms.sum(&:open_port_count)
367
181
  end
368
182
 
369
183
  def shutdown?
370
184
  @shutdown
371
185
  end
372
186
 
373
- # Start listening for provision requests (see CableRoom::Placement). `Host.start!` does this
374
- # for the process Host; a Host built with `new` (a spec harness) calls it itself. Pass a
375
- # Placement to use one with non-default settings; calling it again re-arms the current one.
376
- def start_placement(placement = nil)
377
- previous = @monitor.synchronize do
187
+ # Start hearing provision requests (see Host::Placement). `Host.start!` does this for a :remote
188
+ # rooms process; a Host built with `new` (a spec playing one) calls it itself. `options` go to
189
+ # Placement.new the first time; later calls restart the same Placement.
190
+ def start_placement(**options)
191
+ placement = @monitor.synchronize do
378
192
  raise "Cannot start placement after shutdown" if @shutdown
379
- @placement.tap { @placement = placement || @placement || Placement.new(self) }
193
+ @placement ||= Placement.new(self, **options)
380
194
  end
381
- previous.stop if previous && previous != @placement
382
- @placement.start
383
- @placement
195
+ placement.start
384
196
  end
385
197
 
386
- # Stop listening and drop any claim still waiting. Nothing starts a room here after this.
198
+ # Stop hearing requests and drop any claim still waiting. Nothing starts a room here after this.
387
199
  def stop_placement
388
200
  @placement&.stop
389
201
  end
@@ -405,6 +217,15 @@ module CableRoom
405
217
  worker_pool.executor.kill
406
218
  end
407
219
 
220
+ # Give this Host up without touching anything it holds. For a copy of a Host that a forked
221
+ # child inherited from its parent (see `CableRoom.after_fork!`): its rooms, locks, and threads
222
+ # belong to the parent, so the child must neither close those rooms nor let the `at_exit`
223
+ # hook above do it when the child exits. Marks it shut down so both are no-ops.
224
+ def abandon!
225
+ @shutdown = true
226
+ self
227
+ end
228
+
408
229
  # Ask every room to finish what it has queued and shut down, then give them up to
409
230
  # `wait` seconds to do it
410
231
  def shutdown_rooms!(wait: 15)
@@ -424,57 +245,6 @@ module CableRoom
424
245
 
425
246
  private
426
247
 
427
- # Run the migrations `concurrency` at a time until `deadline`, then everything still waiting
428
- # at once. Each runs on a thread of its own rather than the worker pool: a handoff blocks for
429
- # up to `handoff_timeout` waiting on a peer, and the rooms still need the pool to run.
430
- def run_migrations(migrations, concurrency:, deadline:)
431
- queue = Queue.new
432
- migrations.each { |migration| queue << migration }
433
- next_migration = -> { queue.pop(true) rescue nil } # non-blocking: nil once the queue is empty
434
-
435
- workers = Array.new([concurrency, migrations.size].min) do |i|
436
- migration_thread("cable_room-drain-#{i}") do
437
- while (migration = next_migration.call)
438
- migration.run
439
- end
440
- end
441
- end
442
-
443
- workers.each do |thread|
444
- remaining = deadline - monotonic_now
445
- break if remaining <= 0
446
- thread.join(remaining)
447
- end
448
-
449
- forced = []
450
- while (migration = next_migration.call)
451
- forced << migration
452
- end
453
- if forced.any?
454
- logger.warn "drain_timeout passed with #{forced.size} room(s) still waiting; handing them all off now"
455
- end
456
- forced_threads = forced.map { |migration| migration_thread("cable_room-drain-forced") { migration.run } }
457
-
458
- (workers + forced_threads).each(&:join)
459
- end
460
-
461
- def migration_thread(name)
462
- thread = Thread.new do
463
- yield
464
- rescue => e
465
- # Migration#run handles its own failures; this is for a bug in the loop itself, which
466
- # must not take the whole drain down with it
467
- CableRoom.report_error(e, host: self)
468
- end
469
- thread.name = name
470
- thread.report_on_exception = false
471
- thread
472
- end
473
-
474
- def monotonic_now
475
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
476
- end
477
-
478
248
  def cable_server
479
249
  ActionCable.server
480
250
  end
@@ -4,14 +4,14 @@ module CableRoom
4
4
  # listens on it. Which transport that touches depends on which side you're on, and each side
5
5
  # implements the three methods below:
6
6
  #
7
- # * Member side (RoomMembership): sending publishes on the Bus channel for the room's port
8
- # (member→room), listening is an ActionCable stream (room→member, fed by the broadcaster).
9
- # * Room side (Room::Base and Room::HostAdapter): sending goes through the configured
10
- # CableRoom::Broadcaster, listening subscribes on the Bus through the room's Host.
7
+ # * Member side (RoomMembership): sending publishes on the room's Bus channel for the port
8
+ # (member→room); listening is an ActionCable stream (room→member).
9
+ # * Room side (Room::Base and Room::HostAdapter): sending is an ActionCable broadcast on the
10
+ # port's stream; listening subscribes on the Bus through the room's Host.
11
11
  #
12
12
  # So a member's `ports[:custom] << msg` and a room's `ports[:custom].stream` meet on the Bus,
13
13
  # and a room's `ports[:custom] << msg` and a member's `ports[:custom].stream` meet on the
14
- # broadcaster's streams. Neither side needs to know that.
14
+ # ActionCable stream named by `room_port_key`. Neither side needs to know that.
15
15
  module Ports
16
16
  extend ActiveSupport::Concern
17
17
 
@@ -5,6 +5,20 @@ require "active_model/railtie"
5
5
 
6
6
  module CableRoom
7
7
  class Railtie < Rails::Railtie # :nodoc:
8
+ rake_tasks do
9
+ end
10
+
11
+ console do |app|
12
+ end
13
+
14
+ runner do
15
+ end
16
+
17
+ # Read CABLE_ROOM_HOST now so a bad value fails the boot, not the first room
18
+ initializer "cable_room.room_host" do
19
+ CableRoom.room_host
20
+ end
21
+
8
22
  initializer "cable_room.hook_action_cable_restart" do
9
23
  module ActionCableServerExtensions
10
24
  # ActionCable restarts when the app reloads; stop every room with it. A process that