cable_room 0.6.1 → 0.6.2.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 652ffcb10ea146215a77d64543cce7c60d1e67d45db773eaaae6487d50fb5340
4
- data.tar.gz: 5be41bc8fb4fbfa16575f5693c939d3b7877fd520f0daac373e4e44b505aeefa
3
+ metadata.gz: 34ff8028043d9407e2f5f9487c472873b8ad56c1cda136ad50038ca28be578ff
4
+ data.tar.gz: 401c6500ac0d1c065967a4901a4a2e3d0ed75b1a8ffc3101260e820b5fff5bc4
5
5
  SHA512:
6
- metadata.gz: be5a1a35fa28418a373b219c25b9f994291feea230840078584d9bd447b5d334ef31b205c542032570cd0148bb9ab42ced9543bd21eee2f50ea131f519ef3b94
7
- data.tar.gz: 65fd75d2bda566796cc84b495c7ff32c7a3de15331c8ec891f33c36a549acfa14289f3817ee84eb086694891bf970c4ba6b99337b68aafb5e5acc0e5dea3fc69
6
+ metadata.gz: 83df8225dca28fd3c4fa70434698fea93bfd05ab495c5cac8d8718cb5acfe7a8c5800548a2332d2200247efc1dd29cd900d161e1914a75b3f34c908c972c4d24
7
+ data.tar.gz: 25e0451d688da7390793a0cca348e488b618c89dc8e98b510b775e7babdaf4df912c583b1bb1b0d04b8567c899646075c5abb27062e9e71de13f2fd458eb7169
@@ -6,8 +6,18 @@ module CableRoom
6
6
  @ports_proxy ||= PortsProxy.new(self)
7
7
  end
8
8
 
9
- def stream_port(port, auto_close: true, &blk)
10
- @cable_channel.stream_from(room_port_key(port), coder: ActiveSupport::JSON, &blk)
9
+ # Subscribe the channel to `port`, delivering each decoded message to the block.
10
+ #
11
+ # `on_live`, when given, runs once the pubsub adapter has confirmed the subscription — the
12
+ # first moment a message published to the port is guaranteed to reach the block. Subscribing
13
+ # is asynchronous, so anything published before then can be lost.
14
+ def stream_port(port, auto_close: true, on_live: nil, &blk)
15
+ broadcasting = room_port_key(port)
16
+ if on_live
17
+ _stream_from_observing_liveness(broadcasting, on_live, &blk)
18
+ else
19
+ @cable_channel.stream_from(broadcasting, coder: ActiveSupport::JSON, &blk)
20
+ end
11
21
  _streamed_ports << port if auto_close
12
22
  end
13
23
 
@@ -32,6 +42,34 @@ module CableRoom
32
42
  @_streamed_ports ||= Set.new
33
43
  end
34
44
 
45
+ # ActionCable::Channel::Streams#stream_from confirms the channel subscription once every one of
46
+ # its streams is live, but offers no per-stream hook. This is its body — unchanged from Rails
47
+ # 6.1 through 8.1 — with `on_live` added to the adapter's success callback.
48
+ #
49
+ # When the channel has replaced `stream_from` (ActionCable's channel test stubs, recorders,
50
+ # `StubRoomChannel`) there is no subscription to observe: defer to it and report live at once.
51
+ def _stream_from_observing_liveness(broadcasting, on_live, &blk)
52
+ chan = @cable_channel
53
+ unless chan.method(:stream_from).owner == ActionCable::Channel::Streams
54
+ chan.stream_from(broadcasting, coder: ActiveSupport::JSON, &blk)
55
+ on_live.call
56
+ return
57
+ end
58
+ return if chan.respond_to?(:unsubscribed?, true) && chan.send(:unsubscribed?)
59
+
60
+ chan.send(:defer_subscription_confirmation!)
61
+ handler = chan.send(:worker_pool_stream_handler, broadcasting, blk, coder: ActiveSupport::JSON)
62
+ chan.send(:streams)[broadcasting] = handler
63
+
64
+ chan.connection.server.event_loop.post do
65
+ chan.send(:pubsub).subscribe(broadcasting, handler, lambda do
66
+ chan.send(:ensure_confirmation_sent)
67
+ chan.logger.info "#{chan.class.name} is streaming from #{broadcasting}"
68
+ on_live.call
69
+ end)
70
+ end
71
+ end
72
+
35
73
  class PortsProxy
36
74
  def initialize(ports_concern)
37
75
  @ports_concern = ports_concern
@@ -56,7 +56,7 @@ module CableRoom
56
56
  class_attribute :_system_message_types, instance_writer: false, default: Set.new
57
57
 
58
58
  set_callback(:receive_message, :before) do
59
- logger.debug "Received message: #{message.inspect}"
59
+ logger.debug { "Received message: #{message.inspect}" }
60
60
  end
61
61
 
62
62
  set_callback(:receive_message, :before) do
@@ -103,13 +103,13 @@ module CableRoom
103
103
  return if left?
104
104
  if @has_established
105
105
  port_transmit(room_class::ROOM_IN_CHANNEL, { type: 'port_ping' }, secure_context: true)
106
- else
107
- # The port_connected sent by initiate_connection can be lost: stream subscriptions are
108
- # asynchronous, so a room on another server can acknowledge before our private stream is
109
- # live and an unestablished membership silently drops everything the client sends
110
- # (see #<<). Until acknowledged, keep re-announcing instead of pinging: port_connected is
111
- # idempotent on the room side (the port is merged, user_joined fires only once), and the
112
- # re-acknowledgement lands once our subscription is up.
106
+ elsif @streams_live
107
+ # Announced but never acknowledged. Our streams were already live when we announced (see
108
+ # initiate_connection), so this is not the subscribe race the announcement or the
109
+ # acknowledgement was lost in transit, or the room did not exist yet. An unestablished
110
+ # membership silently drops everything the client sends (see #<<), so re-announce instead
111
+ # of pinging: port_connected is idempotent on the room side (the port is merged,
112
+ # user_joined fires only once).
113
113
  transmit_port_connected
114
114
  end
115
115
  @mutex.synchronize do
@@ -140,6 +140,23 @@ module CableRoom
140
140
 
141
141
  def key; @room_key; end
142
142
 
143
+ # Every stream this membership opens reports back once it is live; the last one to come up
144
+ # sends port_connected (see initiate_connection).
145
+ #
146
+ # The adapter runs the liveness callback on its own terms — the async and inline adapters call
147
+ # it while holding their subscriber-map lock, the Redis adapter on the event loop — so nothing
148
+ # may happen inside it except a hop to a worker thread: taking @mutex there deadlocks against
149
+ # an initiate_connection that is mid-broadcast, and broadcasting from it re-enters the lock.
150
+ def stream_port(port, on_live: nil, **kwargs, &blk)
151
+ generation = @stream_generation
152
+ @streams_pending += 1
153
+ connection = @cable_channel.connection
154
+ super(port, **kwargs, on_live: lambda do
155
+ connection.worker_pool.async_invoke(self, :_stream_became_live, generation, connection: connection)
156
+ on_live&.call
157
+ end, &blk)
158
+ end
159
+
143
160
  protected
144
161
 
145
162
  def port_transmit(port, data, secure_context: false)
@@ -170,7 +187,9 @@ module CableRoom
170
187
  @has_established = true
171
188
  @on_joined&.call(self) if first_acknowledgement
172
189
  when 'room_opened'
173
- transmit_port_connected
190
+ # If our streams are not all live yet, the pending announcement covers this (and an early
191
+ # one would be acknowledged into a stream nobody is listening to yet).
192
+ transmit_port_connected if @streams_live
174
193
  @on_room_opened&.call(self)
175
194
  when 'room_closed'
176
195
  leave!
@@ -192,6 +211,18 @@ module CableRoom
192
211
  @has_established = false
193
212
  @cable_channel._room_memberships << self
194
213
 
214
+ # port_connected must not go out until every stream below is live on the pubsub adapter.
215
+ # Subscribing is asynchronous and the room acknowledges on the private @token stream, so
216
+ # announcing first lets a fast room — in practice one on another server — reply into a
217
+ # stream nobody is listening to yet; the acknowledgement is lost and the membership never
218
+ # establishes. stream_port (overridden above) counts the streams it opens and the last one
219
+ # to come up announces (_stream_became_live). The generation discards confirmations from
220
+ # a previous connection that land after a rejoin!.
221
+ @stream_generation = (@stream_generation || 0) + 1
222
+ @streams_pending = 0
223
+ @streams_armed = false
224
+ @streams_live = false
225
+
195
226
  # Listen to public/broadcast channel
196
227
  stream_port(room_class::ROOM_OUT_CHANNEL) do |message|
197
228
  handle_received_message(message)
@@ -217,12 +248,33 @@ module CableRoom
217
248
 
218
249
  @preconfigure&.call(self)
219
250
 
220
- transmit_port_connected
251
+ # Streams whose adapter confirmed synchronously are already counted down; announce now if
252
+ # that was all of them, otherwise the last confirmation will.
253
+ @streams_armed = true
254
+ _announce_if_streams_live
221
255
 
222
256
  maybe_provision_room
223
257
  end
224
258
  end
225
259
 
260
+ def _stream_became_live(generation)
261
+ @mutex.synchronize do
262
+ return unless generation == @stream_generation
263
+
264
+ @streams_pending -= 1
265
+ _announce_if_streams_live
266
+ end
267
+ end
268
+
269
+ # Under @mutex. Sends port_connected once per connection, when every stream opened by
270
+ # initiate_connection has been confirmed live.
271
+ def _announce_if_streams_live
272
+ return if @streams_live || !@streams_armed || @streams_pending > 0 || left?
273
+
274
+ @streams_live = true
275
+ transmit_port_connected
276
+ end
277
+
226
278
  def transmit_port_connected
227
279
  msg = {
228
280
  type: 'port_connected',
@@ -1,3 +1,3 @@
1
1
  module CableRoom
2
- VERSION = "0.6.1".freeze
2
+ VERSION = "0.6.2.beta1".freeze
3
3
  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.6.1
4
+ version: 0.6.2.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ethan Knapp