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
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
module CableRoom
|
|
2
|
-
# Raised at subscribe time when `join_room` asks for something AnyCable can't deliver
|
|
3
|
-
# (see RoomMember#refuse_unsupported_under_anycable!).
|
|
4
|
-
class AnyCableUnsupported < ArgumentError; end
|
|
5
|
-
|
|
6
2
|
module RoomMember
|
|
7
3
|
extend ActiveSupport::Concern
|
|
8
4
|
|
|
@@ -13,19 +9,18 @@ module CableRoom
|
|
|
13
9
|
# ~30% of all room messages at 10 s with no skipping.
|
|
14
10
|
PING_INTERVAL = 15.seconds
|
|
15
11
|
|
|
12
|
+
# A dropped announcement is invisible to the port. The room hears members on the Bus, and its
|
|
13
|
+
# subscription is live once the room has started, but the room may not have started yet, may
|
|
14
|
+
# live in another process, or the Bus may be reconnecting; Redis pub/sub doesn't queue, so a
|
|
15
|
+
# port_connected sent before the room is listening is discarded with no error. Re-announce on
|
|
16
|
+
# a doubling backoff from here until the room acknowledges, rather than waiting out a whole
|
|
17
|
+
# PING_INTERVAL. port_connected is idempotent on the room side (the port is merged,
|
|
18
|
+
# user_joined fires only once), so a redundant re-announcement costs one message.
|
|
19
|
+
REANNOUNCE_INITIAL_DELAY = 0.1.seconds
|
|
20
|
+
|
|
16
21
|
included do
|
|
17
22
|
periodically :ping_room_memberships, every: PING_INTERVAL
|
|
18
23
|
|
|
19
|
-
# Where MembershipStore::AnyCable keeps the memberships between anycable-rails' per-call
|
|
20
|
-
# channel instances. `state_attr_accessor` only exists once anycable-rails is loaded; without
|
|
21
|
-
# it the channel lives for the socket and the in-memory store is all there is. Private
|
|
22
|
-
# because every public method of a channel is an action a client can perform, and a client
|
|
23
|
-
# that could write this could claim another member's token.
|
|
24
|
-
if respond_to?(:state_attr_accessor)
|
|
25
|
-
state_attr_accessor MembershipStore::AnyCable::STATE_ATTRIBUTE
|
|
26
|
-
private MembershipStore::AnyCable::STATE_ATTRIBUTE, :"#{MembershipStore::AnyCable::STATE_ATTRIBUTE}="
|
|
27
|
-
end
|
|
28
|
-
|
|
29
24
|
after_unsubscribe do
|
|
30
25
|
to_close = _room_memberships.to_a
|
|
31
26
|
_room_memberships.clear
|
|
@@ -33,51 +28,13 @@ module CableRoom
|
|
|
33
28
|
end
|
|
34
29
|
end
|
|
35
30
|
|
|
36
|
-
# The channel's memberships (a MembershipStore). RoomMembership registers itself here, so this
|
|
37
|
-
# has to stay public; treat it as internal and read `room_memberships` instead.
|
|
38
31
|
def _room_memberships
|
|
39
|
-
@_room_memberships ||=
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# True when anycable-rails is handling this channel: the connection carries an AnyCable socket,
|
|
43
|
-
# this instance exists for one RPC call only, its streams are held by anycable-go, and its
|
|
44
|
-
# timers never run. `anycabled?` is what anycable-rails itself checks; it only exists once the
|
|
45
|
-
# gem is loaded. (Public methods are actions; performing this one is harmless.)
|
|
46
|
-
def anycable_channel?
|
|
47
|
-
connection.respond_to?(:anycabled?) && !!connection.anycabled?
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# The `hello` channel action. The browser performs it once ActionCable has confirmed the
|
|
51
|
-
# subscription (`connected()` in the JS client), which is the first moment every stream this
|
|
52
|
-
# channel opened is guaranteed live. Each membership announces itself to its room in response;
|
|
53
|
-
# nothing is announced before then. A channel with no memberships ignores it.
|
|
54
|
-
#
|
|
55
|
-
# Public methods on a channel are its actions, so every RoomMember channel accepts
|
|
56
|
-
# `perform("hello")` without further wiring.
|
|
57
|
-
def hello(_data = nil)
|
|
58
|
-
hello_room_memberships
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# The `ping` channel action. Under ActionCable the channel's own timer pings, so a client that
|
|
62
|
-
# performs this is doing harmless extra work. Under AnyCable channel timers never run (there is
|
|
63
|
-
# no long-lived channel object to run them on), so the browser performs this every
|
|
64
|
-
# PING_INTERVAL and each membership turns it into port_ping. See README "Using AnyCable".
|
|
65
|
-
def ping(_data = nil)
|
|
66
|
-
ping_room_memberships
|
|
32
|
+
@_room_memberships ||= Set.new
|
|
67
33
|
end
|
|
68
34
|
|
|
69
35
|
protected
|
|
70
36
|
|
|
71
|
-
# The memberships `join_room` created on this channel. Under AnyCable the instance variable
|
|
72
|
-
# `subscribed` assigned is gone by the next call; this still has them, rebuilt from the
|
|
73
|
-
# channel state.
|
|
74
|
-
def room_memberships
|
|
75
|
-
_room_memberships
|
|
76
|
-
end
|
|
77
|
-
|
|
78
37
|
def join_room(room_class, room_key = nil, as: :not_given, forward: false, **kwargs, &blk)
|
|
79
|
-
refuse_unsupported_under_anycable!(forward: forward, callbacks: kwargs, block: blk)
|
|
80
|
-
|
|
81
38
|
if forward
|
|
82
39
|
# raise ArgumentError, "Cannot specify both `forward: true` and `on_message:`" if kwargs[:on_message]
|
|
83
40
|
original_on_message = kwargs[:on_message]
|
|
@@ -101,33 +58,13 @@ module CableRoom
|
|
|
101
58
|
_room_memberships.each(&:ping!)
|
|
102
59
|
end
|
|
103
60
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def refuse_unsupported_under_anycable!(forward:, callbacks:, block:)
|
|
113
|
-
return unless anycable_channel?
|
|
114
|
-
|
|
115
|
-
unsupported = []
|
|
116
|
-
unsupported << "forward: false" unless forward
|
|
117
|
-
unsupported.concat(UNSUPPORTED_UNDER_ANYCABLE_CALLBACKS.select { |cb| callbacks[cb] }.map { |cb| "#{cb}:" })
|
|
118
|
-
unsupported << "a preconfigure block" if block
|
|
119
|
-
return if unsupported.empty?
|
|
120
|
-
|
|
121
|
-
raise AnyCableUnsupported,
|
|
122
|
-
"join_room used #{unsupported.join(', ')} on an AnyCable-backed channel (#{self.class.name}). " \
|
|
123
|
-
"Under AnyCable room messages never pass through this process, so these can't work; " \
|
|
124
|
-
"use forward: true without callbacks (see README, \"Using AnyCable\")."
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# Server-side entry point for the hello signal, for channels that learn the client is ready
|
|
128
|
-
# some other way than the `hello` action (a custom handshake message, for instance).
|
|
129
|
-
def hello_room_memberships
|
|
130
|
-
_room_memberships.each(&:hello!)
|
|
61
|
+
def transmit_subscription_confirmation(...)
|
|
62
|
+
super
|
|
63
|
+
memberships = _room_memberships.to_a
|
|
64
|
+
memberships.each(&:streams_live!)
|
|
65
|
+
connection.worker_pool.async_exec(self, connection: connection) do
|
|
66
|
+
memberships.each(&:transmit_port_connected)
|
|
67
|
+
end
|
|
131
68
|
end
|
|
132
69
|
end
|
|
133
70
|
|
|
@@ -148,28 +85,21 @@ module CableRoom
|
|
|
148
85
|
on_left: nil,
|
|
149
86
|
tags: [],
|
|
150
87
|
extra: nil,
|
|
151
|
-
tenant: nil,
|
|
152
88
|
&preconfigure
|
|
153
89
|
)
|
|
154
90
|
@mutex = Monitor.new
|
|
155
91
|
|
|
156
92
|
@has_left = false
|
|
157
93
|
@has_established = false
|
|
158
|
-
@
|
|
94
|
+
@streams_live = false
|
|
95
|
+
@reannounce_delay = nil
|
|
96
|
+
@reannounce_job = nil
|
|
159
97
|
|
|
160
98
|
@cable_channel = cable_channel
|
|
161
99
|
@room_class = room_class
|
|
162
100
|
@room_key = room_key
|
|
163
101
|
@allow_create = create
|
|
164
102
|
|
|
165
|
-
# Defaults to the ambient tenant right here, at join_room's own call site -- the member's
|
|
166
|
-
# own request thread, the last point before this room's work moves to a Host thread with no
|
|
167
|
-
# request of its own to derive one from (see Runner#initialize). An app only needs to pass
|
|
168
|
-
# `tenant:` explicitly to ask for something other than "whatever tenant this join is
|
|
169
|
-
# happening under", which is the common case. nil isn't a real tenant either way, so it
|
|
170
|
-
# doubles as "not given".
|
|
171
|
-
@tenant = tenant || (Apartment::Tenant.current if defined?(Apartment))
|
|
172
|
-
|
|
173
103
|
@on_room_opened = on_room_opened
|
|
174
104
|
@on_joined = on_joined
|
|
175
105
|
@on_message = on_message
|
|
@@ -197,95 +127,11 @@ module CableRoom
|
|
|
197
127
|
@mutex.synchronize { @has_left }
|
|
198
128
|
end
|
|
199
129
|
|
|
200
|
-
def hello_received?
|
|
201
|
-
@hello_received
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# Whether room→member traffic reaches this object. Under ActionCable the streams opened in
|
|
205
|
-
# initiate_connection dispatch into handle_received_message. Under AnyCable anycable-go holds
|
|
206
|
-
# the streams and delivers them straight to the socket, so this process never sees a room
|
|
207
|
-
# message: acknowledgements, room_opened, and room_closed are invisible here, and none of the
|
|
208
|
-
# on_* callbacks can fire.
|
|
209
|
-
def hears_room?
|
|
210
|
-
!@cable_channel.anycable_channel?
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Whether the room has acknowledged this port, as far as this process can tell. Under
|
|
214
|
-
# ActionCable that is the acknowledgement itself. Under AnyCable the acknowledgement went to
|
|
215
|
-
# the socket, so the most this process knows is that hello went out; the browser, which does
|
|
216
|
-
# see port_acknowledged, owns the retry (README "Using AnyCable").
|
|
217
|
-
def presumed_established?
|
|
218
|
-
hears_room? ? @has_established : @hello_received
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Whether client input can be handed to the room. Under ActionCable input waits for the
|
|
222
|
-
# acknowledgement, because until then the room has no port to attribute it to. Under AnyCable
|
|
223
|
-
# it goes as soon as hello has: hello's port_connected and the input travel the same Bus
|
|
224
|
-
# channel from the same process, so the room sees them in that order, and a room that doesn't
|
|
225
|
-
# exist yet drops both the way ActionCable would have dropped the input.
|
|
226
|
-
def accepts_input?
|
|
227
|
-
!left? && presumed_established?
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
# What MembershipStore::AnyCable writes into the channel state: enough to rebuild this
|
|
231
|
-
# membership on a later RPC call (see .restore). Callbacks are procs and can't go; `extra`
|
|
232
|
-
# travels in the ActiveJob-serialized form the room deserializes, which needs no lookup here.
|
|
233
|
-
def persisted_identity
|
|
234
|
-
{
|
|
235
|
-
"token" => @token,
|
|
236
|
-
"room_class" => room_class.name,
|
|
237
|
-
"room_key" => @room_key,
|
|
238
|
-
"tags" => @tags,
|
|
239
|
-
"extra" => serialized_extra,
|
|
240
|
-
"create" => @allow_create,
|
|
241
|
-
"hello_received" => @hello_received,
|
|
242
|
-
"tenant" => @tenant,
|
|
243
|
-
}
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# The inverse of #persisted_identity, for a channel instance anycable-rails built for one RPC
|
|
247
|
-
# call. The result can hello!, ping!, leave!, and forward input with the token the subscribe
|
|
248
|
-
# call created. It does not open streams (anycable-go still holds the ones subscribe opened)
|
|
249
|
-
# and does not announce itself (only hello! does); it carries no callbacks.
|
|
250
|
-
def self.restore(cable_channel, record)
|
|
251
|
-
membership = allocate
|
|
252
|
-
membership.send(:restore_from, cable_channel, record)
|
|
253
|
-
membership
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# The client has said hello: its subscription is confirmed, so every stream this membership
|
|
257
|
-
# opened is live and the room's acknowledgement has somewhere to land. This is the only thing
|
|
258
|
-
# that lets port_connected go out — a membership that never hears hello never announces.
|
|
259
|
-
#
|
|
260
|
-
# Hello is remembered for the life of the membership, across rejoin!: the browser says it
|
|
261
|
-
# once per subscription, and a rejoin happens server-side without the browser knowing.
|
|
262
|
-
def hello!
|
|
263
|
-
return if left?
|
|
264
|
-
|
|
265
|
-
ActiveSupport::Notifications.instrument(
|
|
266
|
-
"hello_received.cable_room",
|
|
267
|
-
{ membership: self, room_class: room_class, room_key: @room_key, channel: @cable_channel }
|
|
268
|
-
) do
|
|
269
|
-
@hello_received = true
|
|
270
|
-
# Under AnyCable the next call starts from a fresh channel; it has to know hello happened.
|
|
271
|
-
@cable_channel._room_memberships.persist!
|
|
272
|
-
# A repeated hello re-announces, which is harmless: port_connected is idempotent room-side.
|
|
273
|
-
transmit_port_connected
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
130
|
def ping!
|
|
278
131
|
return if left?
|
|
279
|
-
if
|
|
280
|
-
# The room already heard from us if we sent anything since the last interval; the next
|
|
281
|
-
# ping lands within 2 x PING_INTERVAL of that, well inside PORT_TIMEOUT.
|
|
132
|
+
if @has_established
|
|
282
133
|
port_transmit(room_class::ROOM_IN_CHANNEL, { type: 'port_ping' }, secure_context: true) unless recently_transmitted?
|
|
283
|
-
|
|
284
|
-
# Announced but never acknowledged: the announcement or the acknowledgement was lost in
|
|
285
|
-
# transit, or the room did not exist yet. An unestablished membership silently drops
|
|
286
|
-
# everything the client sends (see RoomProxyChannel#receive), so re-announce instead of
|
|
287
|
-
# pinging: port_connected is idempotent on the room side (the port is merged, user_joined
|
|
288
|
-
# fires only once). Before hello there is nothing to heal — the client isn't ready.
|
|
134
|
+
else
|
|
289
135
|
transmit_port_connected
|
|
290
136
|
end
|
|
291
137
|
@mutex.synchronize do
|
|
@@ -297,6 +143,7 @@ module CableRoom
|
|
|
297
143
|
@mutex.synchronize do
|
|
298
144
|
return if left?
|
|
299
145
|
|
|
146
|
+
stop_reannouncing
|
|
300
147
|
close_streamed_ports!
|
|
301
148
|
@cable_channel._room_memberships.delete(self)
|
|
302
149
|
port_transmit(room_class::ROOM_IN_CHANNEL, { type: 'port_disconnected' }, secure_context: true)
|
|
@@ -316,6 +163,22 @@ module CableRoom
|
|
|
316
163
|
|
|
317
164
|
def key; @room_key; end
|
|
318
165
|
|
|
166
|
+
# @internal
|
|
167
|
+
def streams_live!
|
|
168
|
+
@streams_live = true
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# @internal
|
|
172
|
+
def transmit_port_connected
|
|
173
|
+
msg = {
|
|
174
|
+
type: 'port_connected',
|
|
175
|
+
tags: @tags,
|
|
176
|
+
}
|
|
177
|
+
msg[:extra] = ::ActiveJob::Arguments.serialize([@extra]) if @extra
|
|
178
|
+
port_transmit(room_class::ROOM_IN_CHANNEL, msg, secure_context: true)
|
|
179
|
+
schedule_reannounce
|
|
180
|
+
end
|
|
181
|
+
|
|
319
182
|
# -- The member side of Ports ---------------------------------------------------------------
|
|
320
183
|
|
|
321
184
|
# Everything a member sends goes to its room, so it's published on the room's Bus channel for
|
|
@@ -337,13 +200,8 @@ module CableRoom
|
|
|
337
200
|
@last_transmit_at = monotonic_now unless type == :port_ping
|
|
338
201
|
end
|
|
339
202
|
|
|
340
|
-
# Room→member traffic arrives on ActionCable streams (the
|
|
203
|
+
# Room→member traffic arrives on ActionCable streams (the room broadcasts to them), so
|
|
341
204
|
# listening on a port is a plain stream_from on the member's channel.
|
|
342
|
-
#
|
|
343
|
-
# Subscribing is asynchronous: anything published to the port before the pubsub adapter
|
|
344
|
-
# confirms the subscription can be lost. ActionCable confirms the channel subscription to the
|
|
345
|
-
# client only after every stream is live, which is why members wait for the client's `hello`
|
|
346
|
-
# (see #hello!) rather than announcing themselves from here.
|
|
347
205
|
def stream_port(port, auto_close: true, &blk)
|
|
348
206
|
@cable_channel.stream_from(room_port_key(port), coder: ActiveSupport::JSON, &blk)
|
|
349
207
|
_streamed_ports << port if auto_close
|
|
@@ -358,6 +216,48 @@ module CableRoom
|
|
|
358
216
|
|
|
359
217
|
protected
|
|
360
218
|
|
|
219
|
+
# Re-announce until the room acknowledges the port, doubling the wait each time. Handing off
|
|
220
|
+
# to ping! at PING_INTERVAL keeps a stuck membership from holding a timer open forever.
|
|
221
|
+
#
|
|
222
|
+
# The timer is a plain Concurrent::ScheduledTask, not the Host's scheduler: in :remote a web
|
|
223
|
+
# process has no Host, and the member side must never need one.
|
|
224
|
+
def schedule_reannounce
|
|
225
|
+
@mutex.synchronize do
|
|
226
|
+
cancel_reannounce
|
|
227
|
+
return if @has_established || @has_left
|
|
228
|
+
return if Host.current&.shutdown?
|
|
229
|
+
|
|
230
|
+
next_delay = @reannounce_delay ? @reannounce_delay * 2 : RoomMember::REANNOUNCE_INITIAL_DELAY
|
|
231
|
+
return if next_delay > RoomMember::PING_INTERVAL
|
|
232
|
+
|
|
233
|
+
@reannounce_delay = next_delay
|
|
234
|
+
|
|
235
|
+
# async_exec runs the block with `instance_exec` against the channel, so hold onto the
|
|
236
|
+
# membership rather than relying on `self`.
|
|
237
|
+
membership = self
|
|
238
|
+
channel = @cable_channel
|
|
239
|
+
@reannounce_job = Concurrent::ScheduledTask.execute(@reannounce_delay.to_f) do
|
|
240
|
+
channel.connection.worker_pool.async_exec(channel, connection: channel.connection) do
|
|
241
|
+
membership.transmit_port_connected unless membership.connected? || membership.left?
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def cancel_reannounce
|
|
248
|
+
@mutex.synchronize do
|
|
249
|
+
@reannounce_job&.cancel
|
|
250
|
+
@reannounce_job = nil
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def stop_reannouncing
|
|
255
|
+
@mutex.synchronize do
|
|
256
|
+
cancel_reannounce
|
|
257
|
+
@reannounce_delay = nil
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
361
261
|
def recently_transmitted?
|
|
362
262
|
@last_transmit_at && (monotonic_now - @last_transmit_at) < RoomMember::PING_INTERVAL
|
|
363
263
|
end
|
|
@@ -378,12 +278,13 @@ module CableRoom
|
|
|
378
278
|
# A re-announced port (see ping!) is re-acknowledged; only the first one is a join.
|
|
379
279
|
first_acknowledgement = !@has_established
|
|
380
280
|
@has_established = true
|
|
281
|
+
stop_reannouncing
|
|
381
282
|
@on_joined&.call(self) if first_acknowledgement
|
|
382
283
|
when 'room_opened'
|
|
383
|
-
#
|
|
384
|
-
#
|
|
385
|
-
#
|
|
386
|
-
transmit_port_connected if @
|
|
284
|
+
# Only safe once our own streams are confirmed; otherwise the room may acknowledge on the
|
|
285
|
+
# private port before anyone is listening. If we are not live yet, the subscription
|
|
286
|
+
# confirmation hook will announce for us.
|
|
287
|
+
transmit_port_connected if @streams_live
|
|
387
288
|
@on_room_opened&.call(self)
|
|
388
289
|
when 'room_closed'
|
|
389
290
|
leave!
|
|
@@ -403,14 +304,9 @@ module CableRoom
|
|
|
403
304
|
|
|
404
305
|
@token = SecureRandom.hex(16)
|
|
405
306
|
@has_established = false
|
|
307
|
+
stop_reannouncing
|
|
406
308
|
@cable_channel._room_memberships << self
|
|
407
309
|
|
|
408
|
-
# Subscribing is asynchronous, and the room acknowledges on the private @token stream. We
|
|
409
|
-
# never announce here: port_connected waits for the client's hello (see hello!), which
|
|
410
|
-
# only arrives after ActionCable has confirmed the subscription — that is, after every one
|
|
411
|
-
# of these streams is live. A rejoin! is the exception: hello already happened for this
|
|
412
|
-
# socket, so announce now and let ping! repeat it if the room misses it.
|
|
413
|
-
|
|
414
310
|
# Listen to public/broadcast channel
|
|
415
311
|
stream_port(room_class::ROOM_OUT_CHANNEL) do |message|
|
|
416
312
|
handle_received_message(message)
|
|
@@ -436,81 +332,49 @@ module CableRoom
|
|
|
436
332
|
|
|
437
333
|
@preconfigure&.call(self)
|
|
438
334
|
|
|
439
|
-
|
|
335
|
+
# Announcing here races the async stream subscriptions: the room can acknowledge on the
|
|
336
|
+
# private port before it is live and the ack is lost. transmit_subscription_confirmation
|
|
337
|
+
# announces instead, once every stream is confirmed.
|
|
440
338
|
|
|
441
339
|
maybe_provision_room
|
|
442
340
|
end
|
|
443
341
|
end
|
|
444
342
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
@has_left = false
|
|
448
|
-
# Never learned here (see #hears_room?); the browser sees the acknowledgement instead.
|
|
449
|
-
@has_established = false
|
|
450
|
-
|
|
451
|
-
@cable_channel = cable_channel
|
|
452
|
-
@room_class = record.fetch("room_class").constantize
|
|
453
|
-
@room_key = record.fetch("room_key")
|
|
454
|
-
@token = record.fetch("token")
|
|
455
|
-
@tags = Array(record["tags"]).map(&:to_sym)
|
|
456
|
-
@serialized_extra = record["extra"]
|
|
457
|
-
@allow_create = record["create"] == true
|
|
458
|
-
@hello_received = record["hello_received"] == true
|
|
459
|
-
@tenant = record["tenant"]
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
def transmit_port_connected
|
|
463
|
-
msg = {
|
|
464
|
-
type: 'port_connected',
|
|
465
|
-
tags: @tags,
|
|
466
|
-
}
|
|
467
|
-
msg[:extra] = serialized_extra if serialized_extra
|
|
468
|
-
port_transmit(room_class::ROOM_IN_CHANNEL, msg, secure_context: true)
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
# `extra` as the room receives it. Serialized once: a restored membership only ever has this
|
|
472
|
-
# form (see #persisted_identity), a fresh one builds it from what join_room was given.
|
|
473
|
-
def serialized_extra
|
|
474
|
-
return @serialized_extra if defined?(@serialized_extra)
|
|
475
|
-
|
|
476
|
-
@serialized_extra = @extra ? ::ActiveJob::Arguments.serialize([@extra]) : nil
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
# Ask the rooms hosts to start our room if `create: true` asked for it and nobody has
|
|
480
|
-
# acknowledged us yet. This never starts a room in the member's process: it publishes a
|
|
481
|
-
# `provision` request on the Bus, and every Host (the web process's own in :inline, the
|
|
482
|
-
# `cable_room server` fleet in :remote) hears it and races for the room's lock, the least
|
|
483
|
-
# loaded one first (see CableRoom::Placement). One code path, both modes.
|
|
484
|
-
#
|
|
485
|
-
# Called at join and again on every ping until the room acknowledges the port, so a lost
|
|
486
|
-
# request costs at most one ping interval, and a room whose host died comes back on the next
|
|
487
|
-
# ping from any member. Once acknowledged the room plainly exists, so we stop asking.
|
|
343
|
+
# Start the room if `create: true` asked for it and nobody runs it yet. Called at join and
|
|
344
|
+
# again on every ping, so a room whose host died comes back.
|
|
488
345
|
def maybe_provision_room
|
|
489
346
|
return if left?
|
|
490
347
|
return unless @allow_create
|
|
491
|
-
return if
|
|
348
|
+
return request_remote_room if CableRoom.remote?
|
|
349
|
+
return if Host.current&.shutdown?
|
|
492
350
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
# before asking; otherwise the very first request in a fresh process would go unheard.
|
|
496
|
-
return if Host.instance.shutdown?
|
|
497
|
-
end
|
|
351
|
+
@room_class.ensure(@room_key)
|
|
352
|
+
end
|
|
498
353
|
|
|
354
|
+
# In :remote this process never hosts rooms, so `create: true` asks the rooms Hosts to start
|
|
355
|
+
# it: a provision request on the Bus, which every rooms Host's Placement hears and races for,
|
|
356
|
+
# the least-loaded first (see Host::Placement). Called at join and on every ping, even once the
|
|
357
|
+
# room has acknowledged the port, the same as `ensure` on every ping in :inline: a rooms
|
|
358
|
+
# process that dies sends no room_closed, and this is what brings its rooms back within one
|
|
359
|
+
# ping. While the room runs it costs one publish per member per ping, and a single-attempt lock
|
|
360
|
+
# try on each rooms Host that isn't running it (the one that is skips the request).
|
|
361
|
+
#
|
|
362
|
+
# The request names our tenant, since a rooms process has no request tenant of its own: the
|
|
363
|
+
# Host that claims the room starts it there, so its lock, inbound channel, and stream names
|
|
364
|
+
# match the ones this member uses.
|
|
365
|
+
def request_remote_room
|
|
499
366
|
request = {
|
|
500
|
-
type:
|
|
367
|
+
type: Host::Placement::REQUEST_TYPE,
|
|
501
368
|
room_class: room_class.name,
|
|
502
369
|
# Keys travel the way `extra` does, so a record key arrives on the host as the record
|
|
503
370
|
room_key: ::ActiveJob::Arguments.serialize([@room_key]),
|
|
371
|
+
tenant: CableRoom.current_tenant,
|
|
504
372
|
requested_at: Time.current,
|
|
505
|
-
# Whatever `join_room` was given for `tenant:` (nil for a single-tenant app). The Host has
|
|
506
|
-
# no request of its own to derive this from, so a multi-tenant app has to hand it over
|
|
507
|
-
# explicitly here rather than have the Runner guess at ambient state (see Runner#initialize).
|
|
508
|
-
tenant: @tenant,
|
|
509
373
|
}
|
|
510
374
|
|
|
511
375
|
ActiveSupport::Notifications.instrument(
|
|
512
376
|
"provision_requested.cable_room",
|
|
513
|
-
{ membership: self, room_class: room_class, room_key: @room_key,
|
|
377
|
+
{ membership: self, room_class: room_class, room_key: @room_key, tenant: request[:tenant], request: request }
|
|
514
378
|
) do
|
|
515
379
|
CableRoom.bus.publish(Bus.provision_channel, request)
|
|
516
380
|
end
|
|
@@ -13,17 +13,12 @@ module CableRoom
|
|
|
13
13
|
@room_membership = subscribe_to_room
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
# The channel's actions are `hello` and `ping` (inherited from RoomMember: the client performs
|
|
17
|
-
# hello once its subscription is confirmed, and the membership announces itself to the room)
|
|
18
|
-
# and `receive`, which pipes everything else the client sends into the room. Until the room
|
|
19
|
-
# has acknowledged the membership (or, under AnyCable, until hello has gone out — see
|
|
20
|
-
# RoomMembership#accepts_input?) there is nowhere for input to go, so it is dropped.
|
|
21
16
|
def receive(data)
|
|
22
|
-
room_membership << data if room_membership&.
|
|
17
|
+
@room_membership << data if @room_membership&.connected?
|
|
23
18
|
end
|
|
24
19
|
|
|
25
20
|
def unsubscribed
|
|
26
|
-
room_membership&.leave!
|
|
21
|
+
@room_membership&.leave!
|
|
27
22
|
end
|
|
28
23
|
|
|
29
24
|
protected
|
|
@@ -32,12 +27,6 @@ module CableRoom
|
|
|
32
27
|
raise NotImplementedError
|
|
33
28
|
end
|
|
34
29
|
|
|
35
|
-
# The membership `subscribed` created. Under AnyCable this channel instance may not be the one
|
|
36
|
-
# that ran `subscribed`, so fall back to the memberships rebuilt from the channel state.
|
|
37
|
-
def room_membership
|
|
38
|
-
@room_membership ||= room_memberships.first
|
|
39
|
-
end
|
|
40
|
-
|
|
41
30
|
def join_room(*args, **kwargs, &blk)
|
|
42
31
|
kwargs[:forward] = true
|
|
43
32
|
super
|
data/lib/cable_room/version.rb
CHANGED