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.
- checksums.yaml +4 -4
- data/README.md +0 -1287
- data/cable_room.gemspec +7 -3
- data/lib/cable_room/bus.rb +45 -75
- data/lib/cable_room/cli.rb +105 -76
- data/lib/cable_room/config.rb +46 -80
- data/lib/cable_room/host/bus_inbound.rb +16 -11
- data/lib/cable_room/host/placement.rb +261 -0
- data/lib/cable_room/host/runner.rb +102 -390
- data/lib/cable_room/host/supervisor.rb +48 -22
- data/lib/cable_room/host/worker_pool.rb +27 -40
- data/lib/cable_room/host.rb +90 -320
- data/lib/cable_room/ports.rb +5 -5
- data/lib/cable_room/railtie.rb +14 -0
- data/lib/cable_room/room/base.rb +32 -28
- data/lib/cable_room/room/callbacks.rb +0 -22
- data/lib/cable_room/room/host_adapter.rb +1 -1
- data/lib/cable_room/room/lifecycle.rb +6 -23
- data/lib/cable_room/room/port_management.rb +0 -50
- data/lib/cable_room/room/reaping.rb +0 -33
- data/lib/cable_room/room/user_management.rb +0 -27
- data/lib/cable_room/room.rb +1 -2
- data/lib/cable_room/room_member.rb +112 -248
- data/lib/cable_room/room_proxy_channel.rb +2 -13
- data/lib/cable_room/version.rb +1 -1
- data/lib/cable_room.rb +50 -37
- metadata +10 -11
- data/CHANGELOG.md +0 -122
- data/lib/cable_room/broadcaster.rb +0 -116
- data/lib/cable_room/membership_store.rb +0 -105
- data/lib/cable_room/migration.rb +0 -586
- data/lib/cable_room/placement.rb +0 -260
- data/lib/cable_room/room/snapshotting.rb +0 -82
- data/lib/cable_room/room_harness.rb +0 -168
- data/lib/cable_room/snapshot.rb +0 -136
data/lib/cable_room.rb
CHANGED
|
@@ -4,28 +4,27 @@ require 'active_support/time'
|
|
|
4
4
|
require 'active_support/core_ext/module'
|
|
5
5
|
|
|
6
6
|
require 'action_cable'
|
|
7
|
+
require 'concurrent'
|
|
7
8
|
require 'redlock'
|
|
8
9
|
require 'rufus-scheduler'
|
|
9
10
|
|
|
10
|
-
require_relative 'cable_room/railtie'
|
|
11
11
|
require_relative 'cable_room/config'
|
|
12
|
-
require_relative 'cable_room/
|
|
12
|
+
require_relative 'cable_room/railtie'
|
|
13
13
|
|
|
14
14
|
require_relative 'cable_room/bus'
|
|
15
15
|
|
|
16
16
|
require_relative 'cable_room/periodic_timer'
|
|
17
|
-
require_relative 'cable_room/snapshot'
|
|
18
17
|
require_relative 'cable_room/host'
|
|
19
|
-
require_relative 'cable_room/placement'
|
|
20
|
-
require_relative 'cable_room/migration'
|
|
21
18
|
require_relative 'cable_room/ports'
|
|
22
|
-
require_relative 'cable_room/membership_store'
|
|
23
19
|
require_relative 'cable_room/room_member'
|
|
24
20
|
require_relative 'cable_room/room_proxy_channel'
|
|
25
21
|
require_relative 'cable_room/room/'
|
|
26
22
|
require_relative 'cable_room/version'
|
|
27
23
|
|
|
28
24
|
module CableRoom
|
|
25
|
+
BUS_LOCK = Mutex.new
|
|
26
|
+
private_constant :BUS_LOCK
|
|
27
|
+
|
|
29
28
|
class << self
|
|
30
29
|
# Called as `handler.call(error, context)` whenever work inside a Room raises. Rooms swallow
|
|
31
30
|
# exceptions so that one bad message can't kill the Room, which makes this the only way to
|
|
@@ -44,25 +43,29 @@ module CableRoom
|
|
|
44
43
|
warn "CableRoom.error_handler raised #{handler_error.class}: #{handler_error.message}"
|
|
45
44
|
end
|
|
46
45
|
|
|
47
|
-
# The
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
@config ||= Config.new.apply_env_overrides.validate!
|
|
46
|
+
# The Apartment tenant the calling thread is in, or nil in an app without Apartment
|
|
47
|
+
def current_tenant
|
|
48
|
+
Apartment::Tenant.current if defined?(Apartment)
|
|
51
49
|
end
|
|
52
50
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
def
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
51
|
+
# Run the block switched into `tenant`. Does nothing extra when there's no tenant, no
|
|
52
|
+
# Apartment, or the thread is already in it, so an :inline join (which starts its room in its
|
|
53
|
+
# own tenant) never switches at all.
|
|
54
|
+
def with_tenant(tenant, &blk)
|
|
55
|
+
return yield if tenant.nil? || !defined?(Apartment) || Apartment::Tenant.current == tenant
|
|
56
|
+
|
|
57
|
+
Apartment::Tenant.switch(tenant, &blk)
|
|
60
58
|
end
|
|
61
59
|
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
# Run the block inside the Rails app's executor (database connections, reloading, and so on),
|
|
61
|
+
# the way ActionCable wraps channel callbacks. Nesting is fine: the executor yields straight
|
|
62
|
+
# through when it's already active on this thread. Without Rails it just yields.
|
|
63
|
+
def with_app_executor(&blk)
|
|
64
|
+
if defined?(Rails) && Rails.application
|
|
65
|
+
Rails.application.executor.wrap(&blk)
|
|
66
|
+
else
|
|
67
|
+
yield
|
|
68
|
+
end
|
|
66
69
|
end
|
|
67
70
|
|
|
68
71
|
def redis_pool
|
|
@@ -80,30 +83,40 @@ module CableRoom
|
|
|
80
83
|
])
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
# The process-wide Redis bus (see CableRoom::Bus). Built on first use.
|
|
86
|
+
# The process-wide Redis bus (see CableRoom::Bus). Built on first use. Guarded so two threads
|
|
87
|
+
# racing to it can't each build one: a handler registered on the loser would be stranded.
|
|
84
88
|
def bus
|
|
85
|
-
@bus ||= Bus.new
|
|
89
|
+
@bus || BUS_LOCK.synchronize { @bus ||= Bus.new }
|
|
86
90
|
end
|
|
87
91
|
|
|
88
92
|
# 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
|
|
93
|
+
# `cable_room server --workers N` does it in every worker (see Host::Supervisor); an app that
|
|
94
|
+
# forks for its own reasons (Puma's `on_worker_boot` with `preload_app!`, a job runner) should
|
|
95
|
+
# call it there too.
|
|
96
|
+
#
|
|
97
|
+
# A forked child starts with copies of the parent's objects but only the forking thread, so
|
|
98
|
+
# anything that owns a thread or a socket is broken in the child: the Bus subscriber thread
|
|
99
|
+
# doesn't exist, a mutex may be held by a thread that's gone, and a Redis socket is shared
|
|
100
|
+
# with the parent, so both processes would read each other's replies. This drops the memos,
|
|
101
|
+
# so the next caller builds fresh ones:
|
|
90
102
|
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
# ActiveRecord, so those need nothing from us; this keeps the gem's rule simple: nothing
|
|
98
|
-
# Redis-backed survives a fork.
|
|
103
|
+
# * the Bus. The copy is only dropped, not shut down: its pub/sub socket is still the
|
|
104
|
+
# parent's, and anything sent on it (even a TLS close_notify) would land in the parent's
|
|
105
|
+
# session. GC closes the child's copy of the descriptor quietly.
|
|
106
|
+
# * the Redlock client, whose monitor may be held by a parent thread.
|
|
107
|
+
# * the Host, whose rooms, locks, and threads are the parent's. It's abandoned (see
|
|
108
|
+
# Host#abandon!) so its `at_exit` hook can't close the parent's rooms when the child exits.
|
|
99
109
|
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
110
|
+
# Pooled connections need nothing from us. `redis_pool` is RediConn's shared pool for the
|
|
111
|
+
# URL (the same object comes back if we rebuild it), and connection_pool drops the connections
|
|
112
|
+
# it inherited on its own (it hooks `Process._fork`); redis-client also reconnects any client
|
|
113
|
+
# it finds in a new pid. Rails does the same for ActiveRecord (`PoolConfig.discard_pools!`
|
|
114
|
+
# runs after every fork), and its pools reconnect lazily on first use.
|
|
102
115
|
def after_fork!
|
|
103
|
-
@bus = nil
|
|
116
|
+
BUS_LOCK.synchronize { @bus = nil }
|
|
104
117
|
@lock_manager = nil
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
Host.replace_current(nil)&.abandon!
|
|
119
|
+
nil
|
|
107
120
|
end
|
|
108
121
|
end
|
|
109
122
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cable_room
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0.beta2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ethan Knapp
|
|
@@ -77,14 +77,20 @@ dependencies:
|
|
|
77
77
|
requirements:
|
|
78
78
|
- - ">="
|
|
79
79
|
- !ruby/object:Gem::Version
|
|
80
|
-
version: '5
|
|
80
|
+
version: '5'
|
|
81
|
+
- - "<"
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '7'
|
|
81
84
|
type: :runtime
|
|
82
85
|
prerelease: false
|
|
83
86
|
version_requirements: !ruby/object:Gem::Requirement
|
|
84
87
|
requirements:
|
|
85
88
|
- - ">="
|
|
86
89
|
- !ruby/object:Gem::Version
|
|
87
|
-
version: '5
|
|
90
|
+
version: '5'
|
|
91
|
+
- - "<"
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '7'
|
|
88
94
|
- !ruby/object:Gem::Dependency
|
|
89
95
|
name: rspec
|
|
90
96
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -106,24 +112,20 @@ executables:
|
|
|
106
112
|
extensions: []
|
|
107
113
|
extra_rdoc_files: []
|
|
108
114
|
files:
|
|
109
|
-
- CHANGELOG.md
|
|
110
115
|
- README.md
|
|
111
116
|
- cable_room.gemspec
|
|
112
117
|
- exe/cable_room
|
|
113
118
|
- lib/cable_room.rb
|
|
114
|
-
- lib/cable_room/broadcaster.rb
|
|
115
119
|
- lib/cable_room/bus.rb
|
|
116
120
|
- lib/cable_room/cli.rb
|
|
117
121
|
- lib/cable_room/config.rb
|
|
118
122
|
- lib/cable_room/host.rb
|
|
119
123
|
- lib/cable_room/host/bus_inbound.rb
|
|
124
|
+
- lib/cable_room/host/placement.rb
|
|
120
125
|
- lib/cable_room/host/runner.rb
|
|
121
126
|
- lib/cable_room/host/supervisor.rb
|
|
122
127
|
- lib/cable_room/host/worker_pool.rb
|
|
123
|
-
- lib/cable_room/membership_store.rb
|
|
124
|
-
- lib/cable_room/migration.rb
|
|
125
128
|
- lib/cable_room/periodic_timer.rb
|
|
126
|
-
- lib/cable_room/placement.rb
|
|
127
129
|
- lib/cable_room/ports.rb
|
|
128
130
|
- lib/cable_room/railtie.rb
|
|
129
131
|
- lib/cable_room/room.rb
|
|
@@ -137,13 +139,10 @@ files:
|
|
|
137
139
|
- lib/cable_room/room/port_policies.rb
|
|
138
140
|
- lib/cable_room/room/port_scoping.rb
|
|
139
141
|
- lib/cable_room/room/reaping.rb
|
|
140
|
-
- lib/cable_room/room/snapshotting.rb
|
|
141
142
|
- lib/cable_room/room/threading.rb
|
|
142
143
|
- lib/cable_room/room/user_management.rb
|
|
143
|
-
- lib/cable_room/room_harness.rb
|
|
144
144
|
- lib/cable_room/room_member.rb
|
|
145
145
|
- lib/cable_room/room_proxy_channel.rb
|
|
146
|
-
- lib/cable_room/snapshot.rb
|
|
147
146
|
- lib/cable_room/version.rb
|
|
148
147
|
homepage: https://instructure.com
|
|
149
148
|
licenses: []
|
data/CHANGELOG.md
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
# Changelog.
|
|
2
|
-
|
|
3
|
-
## 1.0.0
|
|
4
|
-
|
|
5
|
-
Rooms can now run in their own process pool, and browsers can be served by anycable-go. Every
|
|
6
|
-
Room definition from 0.6 works unchanged. The channel and deployment contracts do change; the
|
|
7
|
-
README's "Upgrading from 0.6 to 1.0" section is the checklist.
|
|
8
|
-
|
|
9
|
-
### Breaking.
|
|
10
|
-
|
|
11
|
-
- **Clients must send `hello`.** A membership no longer announces itself to the Room when the
|
|
12
|
-
channel subscribes. The browser performs `hello` once ActionCable confirms the subscription
|
|
13
|
-
(`connected()` in the JS client), and only then does `port_connected` go out. There is no
|
|
14
|
-
fallback: a client that never says `hello` receives broadcasts on the shared stream, but the
|
|
15
|
-
Room never sees it and everything it sends is dropped. Browser tabs open across the deploy
|
|
16
|
-
speak the old handshake and stay deaf until reloaded.
|
|
17
|
-
- **`ChannelBase`, `ChannelTracker`, and `DummyConnection` are gone.** Rooms no longer run inside
|
|
18
|
-
a synthetic ActionCable channel. `CableRoom::Host` (one per process) and `Host::Runner` (one
|
|
19
|
-
per Room) own the work queue, worker pool, scheduler, Redlock renewal, watchdog, and shutdown.
|
|
20
|
-
Internal names apps may have reached for moved with them:
|
|
21
|
-
- inside a Room, `@cable_channel` is now `@runner` (a `Host::Runner`);
|
|
22
|
-
- the `CableRoom.error_handler` context key `channel:` is now `runner:` (with `room:`,
|
|
23
|
-
`room_class:`, and `room_key:` alongside it);
|
|
24
|
-
- `Room#params` is gone (it delegated to the synthetic channel, which had none);
|
|
25
|
-
- `ChannelTracker::BEAT_INTERVAL` is `Host::BEAT_INTERVAL`;
|
|
26
|
-
- `Room::ChannelAdapter` is `Room::HostAdapter`, and the `MyRoom::Channel` constant no longer
|
|
27
|
-
exists;
|
|
28
|
-
- `CableRoom::Room.locally_open_rooms` and `MyRoom.locally_running_instances` read from the
|
|
29
|
-
Host and return `[]` in a process that hosts no Rooms.
|
|
30
|
-
- **Provisioning happens over the bus.** `join_room(..., create: true)` never starts a Room in
|
|
31
|
-
the member's process. It publishes a `provision` request (at join and on every ping until the
|
|
32
|
-
Room acknowledges the port), and one of the hosts starts the Room after a load-weighted
|
|
33
|
-
delay. `Room::Base.ensure` still starts a Room directly, but only in a process that hosts
|
|
34
|
-
Rooms: in a `room_host = :remote` web process it raises `CableRoom::Host::NotHosting`.
|
|
35
|
-
- **Member→room traffic rides cable_room's own Redis.** Everything a member sends (`to_room`,
|
|
36
|
-
`port_connected`, `port_ping`, `port_disconnected`, custom ports) and `Room::Base.send_message`
|
|
37
|
-
publish on `CableRoom::Bus`, over the `CABLEROOM_*` connection. The Room side no longer
|
|
38
|
-
touches ActionCable pubsub for inbound. That connection must be reachable from every web
|
|
39
|
-
process and every rooms host.
|
|
40
|
-
- **`redis` is a runtime dependency** (`>= 5.0`). It was a development dependency; rediconn
|
|
41
|
-
needed it all along, and the bus uses it directly.
|
|
42
|
-
- **`PortManagement::PORT_TIMEOUT` is 45 seconds**, up from 30. A member that stops pinging
|
|
43
|
-
keeps its port 15 seconds longer.
|
|
44
|
-
- **`room_closed` always carries a reason.** In 0.6 a close the host decided (process exit,
|
|
45
|
-
watchdog timeout, lost lock) sent `reason: nil`; only `shutdown!` from inside the Room set one.
|
|
46
|
-
Now the runner sets it too: `"Server shutting down"`, `"Watchdog timeout"`, the signal name on
|
|
47
|
-
a `cable_room server` drain, and the failure on a migration that closed instead of moving.
|
|
48
|
-
- **Instrumentation payloads.** `error.cable_room` and the error handler get the new context
|
|
49
|
-
keys above. Every 0.6 event still fires with the same name.
|
|
50
|
-
|
|
51
|
-
### Added.
|
|
52
|
-
|
|
53
|
-
- `CableRoom.configure` / `CableRoom.config` (`CableRoom::Config`) with five knobs:
|
|
54
|
-
`room_host` (`:inline` | `:remote`), `broadcaster` (`:action_cable` | `:anycable`),
|
|
55
|
-
`provision_delay_ms` (20), `drain_timeout` (10 minutes), and `handoff_timeout` (10 seconds).
|
|
56
|
-
`CABLE_ROOM_HOST` and `CABLE_ROOM_BROADCASTER` override the first two, even when the block sets
|
|
57
|
-
them. Unknown values raise at boot and name the valid options. Defaults reproduce 0.6.
|
|
58
|
-
- `CableRoom::Broadcaster`: room→member traffic goes through `ActionCableBroadcaster`
|
|
59
|
-
(`ActionCable.server.broadcast`, the default) or `AnyCableBroadcaster` (`AnyCable.broadcast`).
|
|
60
|
-
Stream names are identical in both. `anycable-rails` is optional; selecting `:anycable`
|
|
61
|
-
without it raises `Broadcaster::MissingDependency` naming the gem.
|
|
62
|
-
- `CableRoom::Bus`: Redis pub/sub and the handoff storage on the `CABLEROOM_*` connection.
|
|
63
|
-
Channel names: `cr:{RoomClass:key}:in` (and `:in:{port}` for custom ports), `cr:provision`,
|
|
64
|
-
`cr:{RoomClass:key}:handoff`, `cr:{RoomClass:key}:snapshot`, `cr:{RoomClass:key}:adopted`.
|
|
65
|
-
`Room::Base.inbound_channel(key, port)` names a Room's inbound channel.
|
|
66
|
-
- `CableRoom::Host`: `Host.current` (nil when the process hosts nothing), `Host.instance`,
|
|
67
|
-
`Host.start!`, `Host#open_ports` (the load gauge to scale on), `Host#open_rooms_count`,
|
|
68
|
-
`Host#drain!`, `Host.after_drain`, `Host::NotHosting`.
|
|
69
|
-
- `cable_room server` (`exe/cable_room`, `CableRoom::CLI`): boots the Rails app in the current
|
|
70
|
-
directory (or `--require PATH`), refuses to run unless `room_host` is `:remote`, hosts Rooms
|
|
71
|
-
until SIGTERM or SIGINT, and migrates open Rooms to peers on the way out. `--workers N`
|
|
72
|
-
(default: core count) preloads the app and forks N single-GVL workers through
|
|
73
|
-
`Host::Supervisor`, which replaces a worker that dies (with a growing delay for one that dies
|
|
74
|
-
at boot), relays SIGTERM and SIGINT, and waits for every worker. `--version` prints the
|
|
75
|
-
version. `CableRoom.after_fork!` resets the bus, lock manager, Redis pool, and Host in a
|
|
76
|
-
forked child.
|
|
77
|
-
- Load-weighted provisioning (`CableRoom::Placement`): every host hears each `provision`
|
|
78
|
-
request, waits `provision_delay_ms × (open_rooms + jitter)`, then races for the Redlock.
|
|
79
|
-
Exactly one wins. A draining host never claims.
|
|
80
|
-
- Snapshot and restore: optional `snapshot_state` / `restore_state` hooks on a Room,
|
|
81
|
-
`restored?`, `CableRoom::Snapshot` (version 1, gem-owned ports, users, reaper deadlines, and
|
|
82
|
-
`app_state`), `Snapshot::NotSerializable` raised at snapshot time for non-JSON state, and
|
|
83
|
-
`Host#restore_room`. A restored Room runs `restore_state` instead of `startup` when it defines
|
|
84
|
-
it, and never broadcasts `room_opened`.
|
|
85
|
-
- Live migration on planned shutdown (`CableRoom::Migration`): freeze → snapshot → offer (Redis,
|
|
86
|
-
60 second TTL) → peer adopts → relay and replay with per-stream markers → let go. Members keep
|
|
87
|
-
their streams and lose nothing. A Room nobody adopts within `handoff_timeout` closes with
|
|
88
|
-
`room_closed`; `drain_timeout` forces whatever is still waiting through. New lifecycle states
|
|
89
|
-
`:freezing` and `:frozen`.
|
|
90
|
-
- Channel actions `hello` and `ping` on every `RoomMember` channel (and so on
|
|
91
|
-
`RoomProxyChannel`), plus the server-side `hello_room_memberships`, `RoomMembership#hello!`,
|
|
92
|
-
`#hello_received?`, `#accepts_input?`, and the protected `room_memberships` reader.
|
|
93
|
-
- AnyCable channel state: `CableRoom::MembershipStore` keeps each membership's identity in
|
|
94
|
-
anycable-rails' `state_attr_accessor`, so `hello`, `receive`, `ping`, and `unsubscribed` work
|
|
95
|
-
across anycable-rails' per-call channel instances. Under plain ActionCable nothing changes.
|
|
96
|
-
- New `ActiveSupport::Notifications` events: `hello_received.cable_room`,
|
|
97
|
-
`provision_requested.cable_room`, `provision_claimed.cable_room`, `room_snapshotted.cable_room`,
|
|
98
|
-
`room_restored.cable_room`, `host_draining.cable_room`, and `room_migrated.cable_room`.
|
|
99
|
-
- CI: an `e2e-anycable` job runs `spec/e2e` against a real `anycable/anycable-go:1.6` under both
|
|
100
|
-
Appraisals. Locally it runs only with `CABLE_ROOM_E2E=1`.
|
|
101
|
-
- `CHANGELOG.md` and `exe/` ship in the gem.
|
|
102
|
-
|
|
103
|
-
### Changed.
|
|
104
|
-
|
|
105
|
-
- `Room::Base.room_port_key` builds the stream name itself (`"RoomClass:key:port"`, records by
|
|
106
|
-
GlobalID). The format is the same one ActionCable produced in 0.6.
|
|
107
|
-
- The ActionCable `restart` hook (development code reload) stops Rooms through the Host.
|
|
108
|
-
- `RoomMembership#stream_port` takes `auto_close:`; the room-side `stream_port` takes `on_live:`.
|
|
109
|
-
- The `Railtie` no longer declares empty `rake_tasks`, `console`, and `runner` blocks.
|
|
110
|
-
- The README describes the 1.0 architecture: configuration, deployment modes, `cable_room
|
|
111
|
-
server`, provisioning, the hello handshake, AnyCable's browser contract, migration, every
|
|
112
|
-
instrumentation event, and the 0.6 → 1.0 upgrade guide.
|
|
113
|
-
|
|
114
|
-
### Fixed.
|
|
115
|
-
|
|
116
|
-
- The gemspec declares `redis`, which rediconn loads but never listed. An app that didn't have
|
|
117
|
-
`redis` in its own Gemfile could fail to boot with 0.6.
|
|
118
|
-
|
|
119
|
-
## 0.6.2.beta1
|
|
120
|
-
|
|
121
|
-
The last release before distributed rooms. Rooms ran inside a synthetic ActionCable channel in
|
|
122
|
-
every process that joined them.
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module CableRoom
|
|
4
|
-
# How a Room pushes messages out to its members. Every room→member message (`from_room`, tag,
|
|
5
|
-
# user, and `mtok` streams, plus `Room::Base.send_message`) goes through one of these, so
|
|
6
|
-
# swapping the transport is a config change, not a code change.
|
|
7
|
-
#
|
|
8
|
-
# `CableRoom.config.broadcaster` picks the implementation:
|
|
9
|
-
#
|
|
10
|
-
# :action_cable -> ActionCableBroadcaster (the default; today's behavior)
|
|
11
|
-
# :anycable -> AnyCableBroadcaster (needs the optional `anycable-rails` gem)
|
|
12
|
-
#
|
|
13
|
-
# Every broadcaster answers `broadcast(stream, payload)`. Specs can swap in a recording fake
|
|
14
|
-
# with `Broadcaster.current = fake`; `CableRoom.reset_config!` drops it again.
|
|
15
|
-
module Broadcaster
|
|
16
|
-
# Raised when the config asks for a broadcaster whose gem isn't in the bundle.
|
|
17
|
-
class MissingDependency < LoadError; end
|
|
18
|
-
|
|
19
|
-
class << self
|
|
20
|
-
# The broadcaster the current config selects. Built once per config object and reused,
|
|
21
|
-
# so `CableRoom.reset_config!` or `CableRoom.configure` picks up a fresh one on the next
|
|
22
|
-
# call without anyone having to remember to clear a cache.
|
|
23
|
-
def current
|
|
24
|
-
config = CableRoom.config
|
|
25
|
-
return @current if @current && @current_config.equal?(config)
|
|
26
|
-
|
|
27
|
-
@current_config = config
|
|
28
|
-
@current = build(config.broadcaster)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Replace the broadcaster for the life of the current config object. Meant for specs that
|
|
32
|
-
# need a fake without rspec-mocks; `CableRoom.reset_config!` builds a new config, which
|
|
33
|
-
# makes `current` rebuild from it and forget this one.
|
|
34
|
-
def current=(broadcaster)
|
|
35
|
-
@current_config = CableRoom.config
|
|
36
|
-
@current = broadcaster
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Builds the broadcaster named by `name`. Config already rejects unknown names, so the
|
|
40
|
-
# `else` branch only fires if someone bypasses config.
|
|
41
|
-
def build(name)
|
|
42
|
-
case name&.to_sym
|
|
43
|
-
when :action_cable
|
|
44
|
-
ActionCableBroadcaster.new
|
|
45
|
-
when :anycable
|
|
46
|
-
AnyCableBroadcaster.new
|
|
47
|
-
else
|
|
48
|
-
raise ArgumentError,
|
|
49
|
-
"CableRoom broadcaster must be one of #{Config::BROADCASTERS.map(&:inspect).join(', ')} " \
|
|
50
|
-
"(got #{name.inspect})"
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Publishes through the ActionCable server, which hands the message to whatever pubsub
|
|
56
|
-
# adapter the app configured (Redis, async, ...).
|
|
57
|
-
class ActionCableBroadcaster
|
|
58
|
-
def broadcast(stream, payload)
|
|
59
|
-
ActionCable.server.broadcast(stream, payload)
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Publishes through `AnyCable.broadcast`, so anycable-go fans the message out to sockets.
|
|
64
|
-
# Stream names are the same ones ActionCableBroadcaster uses, so a member subscribed through
|
|
65
|
-
# anycable-go sees the same traffic it would through ActionCable.
|
|
66
|
-
#
|
|
67
|
-
# `anycable-rails` is optional at runtime: it's only loaded when this broadcaster is built, and
|
|
68
|
-
# a missing gem raises MissingDependency naming what to add.
|
|
69
|
-
#
|
|
70
|
-
# Payload encoding: `ActionCable.server.broadcast` runs every message through its coder
|
|
71
|
-
# (ActiveSupport::JSON by default) and hands the pubsub adapter a JSON string; anycable-rails's
|
|
72
|
-
# own ActionCable adapter forwards that string to `AnyCable.broadcast` untouched, and
|
|
73
|
-
# anycable-go expects the `data` field to be a pre-encoded string. So we encode here with the
|
|
74
|
-
# same coder, and a member decodes the message exactly as it would from ActionCable.
|
|
75
|
-
class AnyCableBroadcaster
|
|
76
|
-
CODER = ActiveSupport::JSON
|
|
77
|
-
GEM_NAME = "anycable-rails"
|
|
78
|
-
|
|
79
|
-
class << self
|
|
80
|
-
# Loads AnyCable on first use. `anycable` (a dependency of anycable-rails) is what defines
|
|
81
|
-
# `AnyCable.broadcast`; requiring it rather than `anycable-rails` avoids re-running the
|
|
82
|
-
# Rails integration in a process that already booted without it.
|
|
83
|
-
def ensure_loaded!
|
|
84
|
-
return if defined?(::AnyCable) && ::AnyCable.respond_to?(:broadcast)
|
|
85
|
-
|
|
86
|
-
begin
|
|
87
|
-
require "anycable"
|
|
88
|
-
rescue LoadError
|
|
89
|
-
raise MissingDependency,
|
|
90
|
-
"CableRoom broadcaster :anycable needs the #{GEM_NAME} gem. " \
|
|
91
|
-
"Add `gem \"#{GEM_NAME}\"` to your Gemfile, or set broadcaster to :action_cable."
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def initialize
|
|
97
|
-
self.class.ensure_loaded!
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def broadcast(stream, payload)
|
|
101
|
-
publish(stream, encode(payload))
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
protected
|
|
105
|
-
|
|
106
|
-
# The one call that leaves the process. Fakes override this to record instead of publish.
|
|
107
|
-
def publish(stream, encoded_payload)
|
|
108
|
-
::AnyCable.broadcast(stream, encoded_payload)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def encode(payload)
|
|
112
|
-
CODER.encode(payload)
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
module CableRoom
|
|
2
|
-
# Where a channel keeps its RoomMemberships between calls. `RoomMember#_room_memberships` is one
|
|
3
|
-
# of these, and RoomMembership adds and removes itself from it.
|
|
4
|
-
#
|
|
5
|
-
# Under ActionCable the channel object lives as long as the socket, so an in-memory set is the
|
|
6
|
-
# whole story: this base class. Under anycable-rails every RPC call (subscribe, message,
|
|
7
|
-
# unsubscribe, disconnect) builds a fresh channel instance and never re-runs `subscribed`, so an
|
|
8
|
-
# in-memory set is empty by the time `hello` or `receive` arrive. The AnyCable subclass writes
|
|
9
|
-
# each membership's identity into the channel's AnyCable state, which anycable-go hands back on
|
|
10
|
-
# every call, and rebuilds the memberships from it on first use.
|
|
11
|
-
class MembershipStore
|
|
12
|
-
include Enumerable
|
|
13
|
-
|
|
14
|
-
def self.for(channel)
|
|
15
|
-
channel.anycable_channel? ? AnyCable.new(channel) : new(channel)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def initialize(channel)
|
|
19
|
-
@channel = channel
|
|
20
|
-
@memberships = Set.new
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def <<(membership)
|
|
24
|
-
memberships << membership
|
|
25
|
-
persist!
|
|
26
|
-
self
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def delete(membership)
|
|
30
|
-
memberships.delete(membership)
|
|
31
|
-
persist!
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def clear
|
|
35
|
-
memberships.clear
|
|
36
|
-
persist!
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def each(&blk)
|
|
40
|
-
memberships.each(&blk)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def to_a
|
|
44
|
-
memberships.to_a
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def empty?
|
|
48
|
-
memberships.empty?
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def size
|
|
52
|
-
memberships.size
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Write the memberships back to wherever they live. Call it after changing something about a
|
|
56
|
-
# membership that has to survive the call (RoomMembership#hello! does). In memory there is
|
|
57
|
-
# nothing to do.
|
|
58
|
-
def persist!; end
|
|
59
|
-
|
|
60
|
-
protected
|
|
61
|
-
|
|
62
|
-
def memberships
|
|
63
|
-
@memberships
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# The anycable-rails flavor: the memberships also live in the channel's AnyCable state
|
|
67
|
-
# (`state_attr_accessor`), as one JSON string of RoomMembership#persisted_identity records.
|
|
68
|
-
#
|
|
69
|
-
# One string rather than an array of hashes because anycable-rails runs every value in the
|
|
70
|
-
# state through its own serializer, which locates any GlobalID-looking string through the
|
|
71
|
-
# database. The `extra` a member joined with is ActiveJob-serialized and may well contain one,
|
|
72
|
-
# and the room is the only party that should deserialize it. Encoding the records ourselves
|
|
73
|
-
# keeps that serializer out of them.
|
|
74
|
-
class AnyCable < MembershipStore
|
|
75
|
-
STATE_ATTRIBUTE = :_cable_room_memberships
|
|
76
|
-
|
|
77
|
-
def persist!
|
|
78
|
-
# The accessor is private on the channel (see RoomMember) so it can't be performed as a
|
|
79
|
-
# channel action; `send` is deliberate.
|
|
80
|
-
@channel.send(:"#{STATE_ATTRIBUTE}=", ActiveSupport::JSON.encode(memberships.map(&:persisted_identity)))
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
protected
|
|
84
|
-
|
|
85
|
-
# Rebuild on first use, whatever the use is. The subscribe call that creates the first
|
|
86
|
-
# membership finds nothing to rebuild; every later call finds what subscribe (and hello)
|
|
87
|
-
# wrote. Restored memberships are added straight to the set: they exist already as far as
|
|
88
|
-
# anycable-go is concerned, so there is nothing to persist and no stream to open.
|
|
89
|
-
def memberships
|
|
90
|
-
restore! unless @restored
|
|
91
|
-
super
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def restore!
|
|
95
|
-
@restored = true
|
|
96
|
-
raw = @channel.send(STATE_ATTRIBUTE)
|
|
97
|
-
return if raw.blank?
|
|
98
|
-
|
|
99
|
-
ActiveSupport::JSON.decode(raw).each do |record|
|
|
100
|
-
@memberships << RoomMembership.restore(@channel, record)
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|