cable_room 0.7.0.beta2 → 0.7.0.beta3
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/README.md +56 -25
- data/lib/cable_room/host/runner.rb +54 -5
- data/lib/cable_room/host/worker_pool.rb +40 -27
- data/lib/cable_room/room/callbacks.rb +22 -0
- data/lib/cable_room/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 196a159b658502db6be07f62ae5987283192a3f76580f5ed79b3790c8c387732
|
|
4
|
+
data.tar.gz: c2ae315fee416f4e823f51fb2bea539692e6161f4849dd590fe845ffa3fe5f6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3210f7c372f5085b2acf3de7fd8ee98f9e7d81a951b319d2476faa71266d57e48ba21b8018815504b969ca86e2431e907462bc08dedb14fb5fbbae2a6bc4ada
|
|
7
|
+
data.tar.gz: 03adad866d9348d12acd61b848ca0a5cfd45b4557b0dd978d43a9b8d8581fa863674fe2e993b2404b0d7db4376d134e7e0a9d1f1912c29b0784704bbd871ceb0
|
data/README.md
CHANGED
|
@@ -154,12 +154,22 @@ class MyRoom < CableRoom::Room::Base
|
|
|
154
154
|
after_startup { } # aliased as on_startup
|
|
155
155
|
before_shutdown { } # last chance to broadcast
|
|
156
156
|
after_shutdown { } # aliased as on_shutdown
|
|
157
|
+
|
|
158
|
+
before_work { } # about to run on a Host thread: a message/timer handler, startup, shutdown...
|
|
159
|
+
after_work { } # ...same set, on the way back out
|
|
160
|
+
around_work { |room, blk| blk.call } # wrap the whole thing (see Multi-tenancy)
|
|
157
161
|
end
|
|
158
162
|
```
|
|
159
163
|
|
|
160
164
|
You can also just define `startup` and `shutdown` methods; they run inside the corresponding
|
|
161
165
|
callback chain.
|
|
162
166
|
|
|
167
|
+
`before_work`/`after_work`/`around_work` wrap *every* piece of Room code that ever runs on a Host
|
|
168
|
+
thread — not just messages, but `startup`, `restore_state`, `snapshot_state`, and `shutdown` too
|
|
169
|
+
(see [Multi-tenancy](#multi-tenancy)). Define them on `MyRoom` for just this Room, or on your own
|
|
170
|
+
shared base Room class for all of them — ordinary callback inheritance, so a subclass's own
|
|
171
|
+
`around_work` nests inside whatever its ancestors already declared.
|
|
172
|
+
|
|
163
173
|
Out of the box a Room broadcasts `{ type: "room_opened" }` after startup and
|
|
164
174
|
`{ type: "room_closed", reason: ... }` before shutdown.
|
|
165
175
|
|
|
@@ -578,29 +588,38 @@ carries around for you, the same way `key` or `extra` are. **It never switches a
|
|
|
578
588
|
Whatever ends up on `Runner#tenant` still has to actually be applied before a Room's DB calls run,
|
|
579
589
|
the same way a request's tenant has to be applied before a controller action's do.
|
|
580
590
|
|
|
581
|
-
|
|
591
|
+
`around_work` (alongside `before_work`/`after_work`, see [Lifecycle](#lifecycle)) is the hook for
|
|
592
|
+
that. It wraps every piece of Room code that runs on a Host thread — `startup`/`restore_state`,
|
|
593
|
+
message and timer handlers, `snapshot_state`, and `shutdown` alike — not just message dispatch, so
|
|
594
|
+
there's exactly one place to apply a tenant no matter which of those runs first for a given Room.
|
|
595
|
+
Define it once on your own shared base Room class (every app Room already inherits from
|
|
596
|
+
`CableRoom::Room::Base`, directly or through one of your own) to cover every Room, or again on a
|
|
597
|
+
specific Room subclass for one that needs something different — ordinary callback inheritance,
|
|
598
|
+
nothing cable_room-specific:
|
|
582
599
|
|
|
583
600
|
```ruby
|
|
584
|
-
|
|
585
|
-
#
|
|
586
|
-
#
|
|
587
|
-
#
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
class ApplicationRoom < CableRoom::Room::Base
|
|
602
|
+
# Lazily switch a worker thread to the right tenant only when it's about to touch the DB.
|
|
603
|
+
# Actively switching up front checks out a connection and runs SET search_path even for a
|
|
604
|
+
# message that never queries anything, so this only stages the tenant (no DB call) and lets
|
|
605
|
+
# the connection pool's own checkout hook apply it the moment a connection is actually acquired.
|
|
606
|
+
around_work do |room, blk|
|
|
607
|
+
Thread.current[:cable_tenant] = {
|
|
608
|
+
adapter: Apartment::Tenant.adapter,
|
|
609
|
+
tenant: room.tenant,
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
# If this thread already holds a connection from earlier work, release it so the checkout
|
|
613
|
+
# hook below gets a fresh checkout to apply the schema to.
|
|
614
|
+
pool = Apartment.connection_class.connection_pool
|
|
615
|
+
pool.release_connection if pool.active_connection?
|
|
616
|
+
|
|
617
|
+
Apartment::Tenant.adapter.instance_variable_set(:@current, room.tenant)
|
|
618
|
+
|
|
619
|
+
blk.call
|
|
620
|
+
ensure
|
|
621
|
+
Thread.current[:cable_tenant] = nil
|
|
622
|
+
end
|
|
604
623
|
end
|
|
605
624
|
|
|
606
625
|
ActiveSupport.on_load(:active_record) do
|
|
@@ -616,10 +635,22 @@ ActiveSupport.on_load(:active_record) do
|
|
|
616
635
|
end
|
|
617
636
|
```
|
|
618
637
|
|
|
619
|
-
`
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
638
|
+
`room` is the Room instance itself — `room.tenant` (delegated to its `Host::Runner`) is read fresh
|
|
639
|
+
on every call, so a Room only ever sees its own tenant even though many Rooms for many orgs share
|
|
640
|
+
the same small pool of Host worker threads. `around_work` never has to guard against running
|
|
641
|
+
twice: cable_room only ever enters it once per thread, even when one piece of Room code calls
|
|
642
|
+
another (a message handler that shuts the Room down, say) — the inner call just runs inside the
|
|
643
|
+
outer one's context.
|
|
644
|
+
|
|
645
|
+
This replaces reaching for `ActionCable::Server::Worker.set_callback :work, :around` the way
|
|
646
|
+
PandaPal does for ordinary channels — that hook only ever fired for message dispatch, and never
|
|
647
|
+
for a Room's `startup`, `snapshot_state`, or `shutdown`, which run directly on a Host thread
|
|
648
|
+
instead. It's structural, not just a convention: `Host::WorkerPool` isn't an
|
|
649
|
+
`ActionCable::Server::Worker` subclass, so it doesn't share ActionCable's `:work` callback chain at
|
|
650
|
+
all. If PandaPal (or anything else) already has a `:work` hook installed, it's harmless to leave in
|
|
651
|
+
place — it simply has nothing to attach to for Room work, so it can neither conflict with,
|
|
652
|
+
double-apply with, nor be relied on in place of `around_work`. Define `around_work` and that's
|
|
653
|
+
the one thing actually switching a Room's tenant.
|
|
623
654
|
|
|
624
655
|
### The hello handshake.
|
|
625
656
|
|
|
@@ -18,6 +18,11 @@ module CableRoom
|
|
|
18
18
|
class Runner
|
|
19
19
|
FROZEN_STATES = %i[freezing frozen].freeze
|
|
20
20
|
|
|
21
|
+
# Thread-local flag guarding re-entry into a Room's `:work` callbacks (see
|
|
22
|
+
# `with_room_context`). Namespaced so it can never collide with a key an app's own callback
|
|
23
|
+
# (PandaPal's, or anything else touching `Thread.current`) happens to use for its own purposes.
|
|
24
|
+
APP_WORK_KEY = :"cable_room.in_room_work_callbacks"
|
|
25
|
+
|
|
21
26
|
attr_reader :host, :room, :room_class, :key, :uuid, :tenant, :logger
|
|
22
27
|
|
|
23
28
|
# Monotonic time this runner was built. `Host#drain!` migrates rooms oldest first.
|
|
@@ -122,7 +127,7 @@ module CableRoom
|
|
|
122
127
|
room.send(:_shutdown_reason=, reason) unless reason.nil?
|
|
123
128
|
unsubscribe_all
|
|
124
129
|
begin
|
|
125
|
-
with_executor { room.send(:_shutdown) }
|
|
130
|
+
with_executor { with_room_context { room.send(:_shutdown) } }
|
|
126
131
|
ensure
|
|
127
132
|
terminate!
|
|
128
133
|
end
|
|
@@ -281,7 +286,7 @@ module CableRoom
|
|
|
281
286
|
def snapshot
|
|
282
287
|
raise "#{room_class.name}[#{key}] must be frozen before it can be snapshotted (state: #{state})" unless state == :frozen
|
|
283
288
|
|
|
284
|
-
with_executor { Snapshot.take(room) }
|
|
289
|
+
with_executor { with_room_context { Snapshot.take(room) } }
|
|
285
290
|
end
|
|
286
291
|
|
|
287
292
|
# Give the room's lock up while staying alive, so another host can claim the room and this
|
|
@@ -365,7 +370,7 @@ module CableRoom
|
|
|
365
370
|
# message can't take the room down with it.
|
|
366
371
|
def post_work(async: false, silent: false, &blk)
|
|
367
372
|
work = proc do
|
|
368
|
-
worker_pool.invoke(blk, :call, connection: self)
|
|
373
|
+
worker_pool.invoke(-> { with_room_context(&blk) }, :call, connection: self)
|
|
369
374
|
rescue => e
|
|
370
375
|
report_work_error(e)
|
|
371
376
|
end
|
|
@@ -454,8 +459,10 @@ module CableRoom
|
|
|
454
459
|
def start_with(final_state: :started)
|
|
455
460
|
@current_state = :starting
|
|
456
461
|
with_executor do
|
|
457
|
-
|
|
458
|
-
|
|
462
|
+
with_room_context do
|
|
463
|
+
yield
|
|
464
|
+
start_periodic_timers unless final_state == :frozen
|
|
465
|
+
end
|
|
459
466
|
end
|
|
460
467
|
@current_state = final_state
|
|
461
468
|
rescue => e
|
|
@@ -572,6 +579,48 @@ module CableRoom
|
|
|
572
579
|
yield
|
|
573
580
|
end
|
|
574
581
|
end
|
|
582
|
+
|
|
583
|
+
# The one seam a Room hooks to run its own around-work logic -- Apartment switching,
|
|
584
|
+
# tracing, whatever -- instead of reaching for ActionCable::Server::Worker's `:work`
|
|
585
|
+
# callback the way PandaPal does (see README's Multi-tenancy section). Deliberately the
|
|
586
|
+
# Room's own `before_work`/`after_work`/`around_work` (see Room::Callbacks), not a second,
|
|
587
|
+
# separately-configured extension point: a Room class already is cable_room's own
|
|
588
|
+
# (non-shared, non-leaky) namespace, ordinary Ruby inheritance already gives "every Room"
|
|
589
|
+
# (define it on your own base Room class, or reopen CableRoom::Room::Base itself) and
|
|
590
|
+
# "just this one" (define it again on a specific subclass) for free, and there's no reason
|
|
591
|
+
# to make an app choose between two different mechanisms for the same thing. Still supports
|
|
592
|
+
# PandaPal's lazy pattern: stage a thread-local here, apply it from an ActiveRecord
|
|
593
|
+
# `checkout` hook, so a message that never queries anything never pays for a schema switch.
|
|
594
|
+
#
|
|
595
|
+
# Every place this Runner touches Room code goes through here: `post_work`'s dispatched
|
|
596
|
+
# work, and the lifecycle methods (`start_with`, `stop!`, `snapshot`) that -- unlike
|
|
597
|
+
# `post_work` -- run directly on the calling thread rather than through the worker pool.
|
|
598
|
+
# A Room's `around_work` never has to know which of those it's wrapping, or guard against
|
|
599
|
+
# being entered twice: called from inside work that's already running (`Room::Lifecycle#stop!`
|
|
600
|
+
# from a message handler, say, or the watchdog's own `stop!`), this just yields through.
|
|
601
|
+
#
|
|
602
|
+
# AR query-log tagging (tag the log with this Room's own tags, the way
|
|
603
|
+
# `ActiveRecordConnectionManagement` did when WorkerPool was still a Worker subclass) wraps
|
|
604
|
+
# every call here regardless of re-entry -- tagging nests safely, unlike `:work` callbacks
|
|
605
|
+
# that stage-and-clear a thread-local.
|
|
606
|
+
def with_room_context(&blk)
|
|
607
|
+
with_ar_log_tagging do
|
|
608
|
+
next yield if Thread.current[APP_WORK_KEY]
|
|
609
|
+
|
|
610
|
+
Thread.current[APP_WORK_KEY] = true
|
|
611
|
+
begin
|
|
612
|
+
room.send(:run_callbacks, :work, &blk)
|
|
613
|
+
ensure
|
|
614
|
+
Thread.current[APP_WORK_KEY] = false
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def with_ar_log_tagging(&blk)
|
|
620
|
+
return yield unless defined?(ActiveRecord::Base)
|
|
621
|
+
|
|
622
|
+
logger.tag(ActiveRecord::Base.logger, &blk)
|
|
623
|
+
end
|
|
575
624
|
end
|
|
576
625
|
end
|
|
577
626
|
end
|
|
@@ -1,37 +1,50 @@
|
|
|
1
|
+
require 'concurrent'
|
|
2
|
+
|
|
1
3
|
module CableRoom
|
|
2
4
|
class Host
|
|
3
|
-
# The thread pool every room's work runs on.
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
# The thread pool every room's work runs on. Deliberately *not* an ActionCable::Server::Worker
|
|
6
|
+
# subclass: that would put Room work on the same shared `:work` callback chain as ordinary
|
|
7
|
+
# ActionCable connections, and that chain is leaky by construction -- `ActiveSupport::Callbacks`
|
|
8
|
+
# re-injects a callback added to the base class into every existing descendant regardless of
|
|
9
|
+
# load order, so an app's own tenant-switching hook (PandaPal's, say, registered from a Rails
|
|
10
|
+
# initializer well after this class is defined) ends up wrapping Room work too, whether we want
|
|
11
|
+
# it to or not. Owning a distinct namespace here avoids that category of problem entirely:
|
|
12
|
+
# nothing outside cable_room can attach to Room work by surprise. The one seam an app gets is
|
|
13
|
+
# a Room's own `before_work`/`after_work`/`around_work` (see Room::Callbacks and
|
|
14
|
+
# Host::Runner#with_room_context).
|
|
15
|
+
class WorkerPool
|
|
16
|
+
attr_reader :executor
|
|
17
|
+
|
|
18
|
+
def initialize(max_size: 5)
|
|
19
|
+
@executor = Concurrent::ThreadPoolExecutor.new(
|
|
20
|
+
name: "CableRoom",
|
|
21
|
+
min_threads: 1,
|
|
22
|
+
max_threads: max_size,
|
|
23
|
+
max_queue: 0,
|
|
24
|
+
)
|
|
17
25
|
end
|
|
18
26
|
|
|
19
|
-
#
|
|
20
|
-
# `handle_exception
|
|
21
|
-
# through here, so report it properly
|
|
27
|
+
# Reduces every exception to a proper report instead of ActionCable's Worker#invoke, which
|
|
28
|
+
# logs a line and calls a no-argument `handle_exception`, discarding the error itself. Rooms
|
|
29
|
+
# run all of their work through here, so report it properly. `connection:` is always a
|
|
30
|
+
# Host::Runner in practice (see Runner#post_work); the else branch is a defensive fallback.
|
|
22
31
|
def invoke(receiver, method, *args, connection:, &block)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
CableRoom.report_error(e, connection: connection)
|
|
32
|
-
end
|
|
32
|
+
receiver.send method, *args, &block
|
|
33
|
+
rescue Exception => e
|
|
34
|
+
if connection.respond_to?(:report_work_error)
|
|
35
|
+
connection.report_work_error(e)
|
|
36
|
+
else
|
|
37
|
+
logger.error "There was an exception - #{e.class}(#{e.message})"
|
|
38
|
+
logger.error Array(e.backtrace).join("\n")
|
|
39
|
+
CableRoom.report_error(e, connection: connection)
|
|
33
40
|
end
|
|
34
41
|
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def logger
|
|
46
|
+
ActionCable.server.logger
|
|
47
|
+
end
|
|
35
48
|
end
|
|
36
49
|
end
|
|
37
50
|
end
|
|
@@ -7,6 +7,7 @@ module CableRoom
|
|
|
7
7
|
included do
|
|
8
8
|
define_callbacks :startup
|
|
9
9
|
define_callbacks :shutdown
|
|
10
|
+
define_callbacks :work
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
class_methods do
|
|
@@ -27,6 +28,27 @@ module CableRoom
|
|
|
27
28
|
set_callback(:shutdown, :after, *methods, &block)
|
|
28
29
|
end
|
|
29
30
|
alias_method :on_shutdown, :after_shutdown
|
|
31
|
+
|
|
32
|
+
# Wraps every piece of Room code that runs on a Host thread: startup/restore, message and
|
|
33
|
+
# timer handlers, snapshot_state, and shutdown alike (see Host::Runner#with_room_context,
|
|
34
|
+
# the one thing that ever triggers the :work callback chain). This is the seam for
|
|
35
|
+
# anything that has to be true before a Room's own code runs on a given thread -- Apartment
|
|
36
|
+
# switching first among them (see README's Multi-tenancy section).
|
|
37
|
+
#
|
|
38
|
+
# Define it once on your own shared base Room class (or reopen CableRoom::Room::Base
|
|
39
|
+
# itself) to cover every Room; define it again on a specific Room subclass for one that
|
|
40
|
+
# needs something different -- ordinary callback inheritance, nothing cable_room-specific.
|
|
41
|
+
def before_work(*methods, &block)
|
|
42
|
+
set_callback(:work, :before, *methods, &block)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def after_work(*methods, &block)
|
|
46
|
+
set_callback(:work, :after, *methods, &block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def around_work(*methods, &block)
|
|
50
|
+
set_callback(:work, :around, *methods, &block)
|
|
51
|
+
end
|
|
30
52
|
end
|
|
31
53
|
end
|
|
32
54
|
end
|
data/lib/cable_room/version.rb
CHANGED