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/room/base.rb
CHANGED
|
@@ -10,48 +10,57 @@ module CableRoom
|
|
|
10
10
|
class << self
|
|
11
11
|
# Start this room in the current process if nobody else is running it. Takes the room's
|
|
12
12
|
# Redlock first, so exactly one process wins; returns false when the lock is held.
|
|
13
|
+
# `tenant` defaults to the caller's (see Host#ensure_room).
|
|
13
14
|
#
|
|
14
15
|
# Only a process that hosts rooms can do this: every process in :inline, and only
|
|
15
16
|
# `cable_room server` in :remote. Anywhere else `Host.instance` raises Host::NotHosting
|
|
16
17
|
# rather than letting a room start where it doesn't belong.
|
|
17
|
-
def ensure(key = nil)
|
|
18
|
-
Host.instance.ensure_room(self, key)
|
|
18
|
+
def ensure(key = nil, tenant: CableRoom.current_tenant)
|
|
19
|
+
Host.instance.ensure_room(self, key, tenant: tenant)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
# The stream name for a room, or for one of its ports: "RoomClass:key" or
|
|
22
|
-
# "RoomClass:key:port". Members and rooms both use this, so it has to
|
|
23
|
+
# "RoomClass:key:port". Members and rooms both use this, so it has to stay stable. These
|
|
24
|
+
# are the names rooms had as ActionCable channels (`broadcasting_for`), so a rolling deploy
|
|
25
|
+
# and the browser side see no change. It's also the room's Redlock key.
|
|
23
26
|
def room_port_key(room_key, port = nil)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
full_key = [room_key]
|
|
28
|
+
full_key << port if port
|
|
29
|
+
stream_namer.broadcasting_for(full_key)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
|
-
# The Bus channel members publish on to reach a room's inbound
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
# the design's `cr:{room_key}:in` and a custom port on `cr:{room_key}:in:{port}`.
|
|
32
|
+
# The Bus channel members publish on to reach a room's inbound `port`, and the one the
|
|
33
|
+
# room's Host subscribes to for it: `cr:{room_port_key}:in` for the main port and
|
|
34
|
+
# `cr:{room_port_key}:in:{port}` for a custom one (see Bus.inbound_channel).
|
|
33
35
|
def inbound_channel(room_key, port = ROOM_IN_CHANNEL)
|
|
34
|
-
port = port.to_s == ROOM_IN_CHANNEL.to_s ? nil :
|
|
35
|
-
Bus.inbound_channel(
|
|
36
|
+
port = port.to_s == ROOM_IN_CHANNEL.to_s ? nil : port.to_s
|
|
37
|
+
Bus.inbound_channel(self, room_key, port)
|
|
36
38
|
end
|
|
37
39
|
|
|
38
|
-
# Send a message to a room from anywhere in the app: same Bus channel a member uses
|
|
40
|
+
# Send a message to a room from anywhere in the app: the same Bus channel a member uses
|
|
39
41
|
def send_message(room_key, data, port: ROOM_IN_CHANNEL)
|
|
40
42
|
CableRoom.bus.publish(inbound_channel(room_key, port), data)
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Every instance of this room class running in this process. Empty when the process
|
|
44
|
-
# hosts no rooms
|
|
46
|
+
# hosts no rooms (a web process in :remote).
|
|
45
47
|
def locally_running_instances
|
|
46
48
|
Room.locally_open_rooms.select { |room| room.class == self }
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
private
|
|
50
52
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
# Names this room's streams the way it did when it was an ActionCable channel: through
|
|
54
|
+
# that channel class's `broadcasting_for`, so an app's override of it still applies.
|
|
55
|
+
# PandaPal's puts the Apartment tenant first, which keeps two tenants' rooms with the
|
|
56
|
+
# same key (and their Redlocks) apart. Never instantiated; it only answers `channel_name`.
|
|
57
|
+
def stream_namer
|
|
58
|
+
@stream_namer ||= begin
|
|
59
|
+
room_class = self
|
|
60
|
+
Class.new(ActionCable::Channel::Base) do
|
|
61
|
+
define_singleton_method(:channel_name) { room_class.name }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
55
64
|
end
|
|
56
65
|
end
|
|
57
66
|
|
|
@@ -62,7 +71,6 @@ module CableRoom
|
|
|
62
71
|
include HostAdapter
|
|
63
72
|
|
|
64
73
|
include Lifecycle
|
|
65
|
-
include Snapshotting
|
|
66
74
|
include Reaping
|
|
67
75
|
include InputHandling
|
|
68
76
|
|
|
@@ -73,7 +81,7 @@ module CableRoom
|
|
|
73
81
|
|
|
74
82
|
attr_reader :key
|
|
75
83
|
|
|
76
|
-
delegate :logger,
|
|
84
|
+
delegate :logger, to: :@runner
|
|
77
85
|
|
|
78
86
|
def initialize(runner, key = nil)
|
|
79
87
|
super()
|
|
@@ -85,11 +93,11 @@ module CableRoom
|
|
|
85
93
|
port_transmit(ROOM_OUT_CHANNEL, data)
|
|
86
94
|
end
|
|
87
95
|
|
|
88
|
-
# The room's sending side of Ports: room→member messages go out
|
|
89
|
-
#
|
|
90
|
-
#
|
|
96
|
+
# The room's sending side of Ports: room→member messages go out as an ActionCable
|
|
97
|
+
# broadcast on the stream named by `room_port_key(port)`. (The listening side is in
|
|
98
|
+
# Room::HostAdapter.) Public because `ports[:x] << msg` reaches it through a PortProxy.
|
|
91
99
|
def port_transmit(port, data)
|
|
92
|
-
|
|
100
|
+
ActionCable.server.broadcast(room_port_key(port), data)
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
protected
|
|
@@ -101,10 +109,6 @@ module CableRoom
|
|
|
101
109
|
def room_port_key(sub_channel = nil)
|
|
102
110
|
self.class.room_port_key(key, sub_channel)
|
|
103
111
|
end
|
|
104
|
-
|
|
105
|
-
def broadcaster
|
|
106
|
-
Broadcaster.current
|
|
107
|
-
end
|
|
108
112
|
end
|
|
109
113
|
end
|
|
110
114
|
end
|
|
@@ -7,7 +7,6 @@ module CableRoom
|
|
|
7
7
|
included do
|
|
8
8
|
define_callbacks :startup
|
|
9
9
|
define_callbacks :shutdown
|
|
10
|
-
define_callbacks :work
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
class_methods do
|
|
@@ -28,27 +27,6 @@ module CableRoom
|
|
|
28
27
|
set_callback(:shutdown, :after, *methods, &block)
|
|
29
28
|
end
|
|
30
29
|
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
|
|
52
30
|
end
|
|
53
31
|
end
|
|
54
32
|
end
|
|
@@ -25,7 +25,7 @@ module CableRoom
|
|
|
25
25
|
|
|
26
26
|
# The room's listening side of Ports: subscribe, through the Host, to the Bus channel
|
|
27
27
|
# members publish on for `port` (see Room::Base.inbound_channel). `on_live` runs once the
|
|
28
|
-
# subscription is confirmed
|
|
28
|
+
# subscription is confirmed.
|
|
29
29
|
def stream_port(port, auto_close: true, on_live: nil, &blk)
|
|
30
30
|
@runner.subscribe(inbound_channel(port), on_live: on_live, &blk)
|
|
31
31
|
_streamed_ports << port if auto_close
|
|
@@ -5,10 +5,8 @@ module CableRoom
|
|
|
5
5
|
|
|
6
6
|
included do
|
|
7
7
|
after_startup do
|
|
8
|
-
logger.info
|
|
9
|
-
|
|
10
|
-
# would make every one of them re-announce for nothing.
|
|
11
|
-
self << { type: 'room_opened' } unless restored?
|
|
8
|
+
logger.info "Started"
|
|
9
|
+
self << { type: 'room_opened' }
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
before_shutdown do
|
|
@@ -27,8 +25,7 @@ module CableRoom
|
|
|
27
25
|
@runner.state
|
|
28
26
|
end
|
|
29
27
|
|
|
30
|
-
# Requests that the Room shut down gracefully, processing any pending messages
|
|
31
|
-
# room (mid-migration) shuts down the same way; that's the "no peer took it" path.
|
|
28
|
+
# Requests that the Room shut down gracefully, processing any pending messages
|
|
32
29
|
def shutdown!(reason = "Room requested shutdown")
|
|
33
30
|
@shutdown_reason = reason
|
|
34
31
|
logger.info "Shutdown requested: #{reason}"
|
|
@@ -42,24 +39,10 @@ module CableRoom
|
|
|
42
39
|
|
|
43
40
|
private
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _shutdown_reason=(reason)
|
|
48
|
-
@shutdown_reason = reason
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
# The startup chain. A restore (see Snapshotting#_restore) runs the same callbacks — they
|
|
52
|
-
# are what subscribe the inbound port and start the reapers — but swaps `startup` for
|
|
53
|
-
# `restore_state(app_state)` when the room defines it.
|
|
54
|
-
def _startup(restoring: false, app_state: nil)
|
|
55
|
-
event = restoring ? "room_restored.cable_room" : "room_opened.cable_room"
|
|
56
|
-
ActiveSupport::Notifications.instrument(event, { room: self }) do
|
|
42
|
+
def _startup
|
|
43
|
+
ActiveSupport::Notifications.instrument("room_opened.cable_room", { room: self }) do
|
|
57
44
|
run_callbacks :startup do
|
|
58
|
-
if
|
|
59
|
-
restore_state(app_state)
|
|
60
|
-
else
|
|
61
|
-
startup if respond_to?(:startup)
|
|
62
|
-
end
|
|
45
|
+
startup if respond_to?(:startup)
|
|
63
46
|
end
|
|
64
47
|
end
|
|
65
48
|
end
|
|
@@ -158,29 +158,6 @@ module CableRoom
|
|
|
158
158
|
end
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
-
# -- Snapshot and restore (see CableRoom::Snapshot) ----------------------------------------
|
|
162
|
-
|
|
163
|
-
def _snapshot_port_clients
|
|
164
|
-
@_port_clients.values.map(&:to_snapshot)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
# Re-create every port exactly as it was, without running port_connected callbacks: the
|
|
168
|
-
# members are still there and were already acknowledged. A port whose user can't be found
|
|
169
|
-
# any more (its record was deleted) is dropped rather than failing the whole room; that
|
|
170
|
-
# member will be re-acknowledged when it next pings.
|
|
171
|
-
def _restore_port_clients(entries)
|
|
172
|
-
Array(entries).each do |entry|
|
|
173
|
-
client = PortClient.new(self, entry["token"])
|
|
174
|
-
begin
|
|
175
|
-
client.restore!(entry)
|
|
176
|
-
rescue ::ActiveJob::DeserializationError => e
|
|
177
|
-
logger.warn "Dropping port #{entry['token']} from the snapshot: #{e.message}"
|
|
178
|
-
next
|
|
179
|
-
end
|
|
180
|
-
@_port_clients[client.token] = client
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
161
|
def check_port_inactivity
|
|
185
162
|
return unless @_port_clients
|
|
186
163
|
|
|
@@ -238,33 +215,6 @@ module CableRoom
|
|
|
238
215
|
def tag!(*tags)
|
|
239
216
|
self[:tags].merge(tags.flatten.map(&:to_sym))
|
|
240
217
|
end
|
|
241
|
-
|
|
242
|
-
SNAPSHOT_OWN_KEYS = %w[tags as last_seen_at].freeze
|
|
243
|
-
|
|
244
|
-
# This port as one `port_clients` entry of a CableRoom::Snapshot. `as` goes through the
|
|
245
|
-
# same serializer the member used to send it (a GlobalID for a record). Everything else on
|
|
246
|
-
# the port — what the member's `extra:` merged in, plus anything the room stored with
|
|
247
|
-
# `message_origin[:x] = ...` — travels under `metadata`, the same way, so records survive.
|
|
248
|
-
def to_snapshot
|
|
249
|
-
metadata = @metadata.to_h.except(*SNAPSHOT_OWN_KEYS)
|
|
250
|
-
{
|
|
251
|
-
token: token,
|
|
252
|
-
tags: self[:tags].map(&:to_s),
|
|
253
|
-
as: Snapshot.serialize_argument(self[:as]),
|
|
254
|
-
last_seen_at: Snapshot.encode_time(self[:last_seen_at]),
|
|
255
|
-
metadata: Snapshot.serialize_argument(metadata),
|
|
256
|
-
}
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
# The inverse of `to_snapshot`. `last_seen_at` is kept, not reset: the port is exactly as
|
|
260
|
-
# old as it was, and its member's next ping refreshes it as usual.
|
|
261
|
-
def restore!(entry)
|
|
262
|
-
merge!(Snapshot.deserialize_argument(entry["metadata"]))
|
|
263
|
-
tag!(entry["tags"])
|
|
264
|
-
self[:as] = Snapshot.deserialize_argument(entry["as"])
|
|
265
|
-
self[:last_seen_at] = Snapshot.decode_time(entry["last_seen_at"]) || self[:last_seen_at]
|
|
266
|
-
self
|
|
267
|
-
end
|
|
268
218
|
end
|
|
269
219
|
end
|
|
270
220
|
end
|
|
@@ -74,39 +74,6 @@ module CableRoom
|
|
|
74
74
|
ping_watchdog
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
# -- Snapshot and restore (see CableRoom::Snapshot) ----------------------------------------
|
|
78
|
-
|
|
79
|
-
# One entry per `reap_when`, in declaration order. `last_keep_at` is the wall-clock moment
|
|
80
|
-
# the reaper's grace period started counting from (nil until it has run once), so the
|
|
81
|
-
# deadline — `last_keep_at + grace` — is the same on whichever host restores it.
|
|
82
|
-
def _snapshot_reaper_state
|
|
83
|
-
self.class.reaper_checkers.each_with_index.map do |cfg, index|
|
|
84
|
-
state = _reaper_states[cfg] || {}
|
|
85
|
-
{ key: cfg[:key]&.to_s, index: index, last_keep_at: Snapshot.encode_time(state[:last_keep_at]) }
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Runs before the startup callbacks, which add the timer to each state and leave the rest
|
|
90
|
-
# alone. A named reaper matches by name and an anonymous one by position, so a Room whose
|
|
91
|
-
# reapers changed between the two hosts' code versions keeps what still lines up and drops
|
|
92
|
-
# the rest.
|
|
93
|
-
def _restore_reaper_state(entries)
|
|
94
|
-
checkers = self.class.reaper_checkers
|
|
95
|
-
Array(entries).each do |entry|
|
|
96
|
-
cfg =
|
|
97
|
-
if entry["key"].present?
|
|
98
|
-
checkers.find { |c| c[:key].to_s == entry["key"] }
|
|
99
|
-
else
|
|
100
|
-
candidate = checkers[entry["index"].to_i]
|
|
101
|
-
candidate if candidate && candidate[:key].nil?
|
|
102
|
-
end
|
|
103
|
-
next unless cfg
|
|
104
|
-
|
|
105
|
-
last_keep_at = Snapshot.decode_time(entry["last_keep_at"])
|
|
106
|
-
(_reaper_states[cfg] ||= {})[:last_keep_at] = last_keep_at if last_keep_at
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
77
|
private
|
|
111
78
|
|
|
112
79
|
def _reaper_states
|
|
@@ -98,33 +98,6 @@ module CableRoom
|
|
|
98
98
|
tags
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
-
# -- Snapshot and restore (see CableRoom::Snapshot) ----------------------------------------
|
|
102
|
-
|
|
103
|
-
# The user map is carried on its own rather than rebuilt from the ports: a port whose join
|
|
104
|
-
# the tag policy refused has no user entry, and a rebuild would invent one.
|
|
105
|
-
def _snapshot_user_state
|
|
106
|
-
@_user_map_mutex.synchronize do
|
|
107
|
-
@_user_state_map.map do |user, usm|
|
|
108
|
-
{ user: Snapshot.serialize_argument(user), port_tokens: usm[:port_tokens].to_a }
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# No user_joined callbacks here: these users joined long ago, on the previous host.
|
|
114
|
-
def _restore_user_state(entries)
|
|
115
|
-
@_user_map_mutex.synchronize do
|
|
116
|
-
Array(entries).each do |entry|
|
|
117
|
-
begin
|
|
118
|
-
user = Snapshot.deserialize_argument(entry["user"])
|
|
119
|
-
rescue ::ActiveJob::DeserializationError => e
|
|
120
|
-
logger.warn "Dropping a user from the snapshot: #{e.message}"
|
|
121
|
-
next
|
|
122
|
-
end
|
|
123
|
-
@_user_state_map[user] = { port_tokens: Set.new(entry["port_tokens"]) }
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
101
|
def _apply_port_scope(user: nil, tag: nil, **kwargs)
|
|
129
102
|
if user && tag
|
|
130
103
|
user_tags = all_user_tags(user) || []
|
data/lib/cable_room/room.rb
CHANGED
|
@@ -10,7 +10,6 @@ module CableRoom
|
|
|
10
10
|
autoload :HostAdapter
|
|
11
11
|
|
|
12
12
|
autoload :Lifecycle
|
|
13
|
-
autoload :Snapshotting
|
|
14
13
|
autoload :Reaping
|
|
15
14
|
autoload :InputHandling
|
|
16
15
|
|
|
@@ -22,7 +21,7 @@ module CableRoom
|
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
# Every room running in this process. Asking never creates a Host, so a process that hosts
|
|
25
|
-
# no rooms (a web process in :remote)
|
|
24
|
+
# no rooms (a web process in :remote) gets an empty list.
|
|
26
25
|
def self.locally_open_rooms
|
|
27
26
|
Host.current&.rooms || []
|
|
28
27
|
end
|