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/config.rb
CHANGED
|
@@ -1,112 +1,78 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CableRoom
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ROOM_HOSTS = %i[inline remote].freeze
|
|
23
|
-
BROADCASTERS = %i[action_cable anycable].freeze
|
|
24
|
-
|
|
25
|
-
ENV_OVERRIDES = {
|
|
26
|
-
room_host: "CABLE_ROOM_HOST",
|
|
27
|
-
broadcaster: "CABLE_ROOM_BROADCASTER",
|
|
28
|
-
}.freeze
|
|
29
|
-
|
|
30
|
-
DEFAULTS = {
|
|
31
|
-
room_host: :inline,
|
|
32
|
-
broadcaster: :action_cable,
|
|
33
|
-
provision_delay_ms: 20,
|
|
34
|
-
drain_timeout: 10.minutes,
|
|
35
|
-
handoff_timeout: 10.seconds,
|
|
36
|
-
}.freeze
|
|
37
|
-
|
|
38
|
-
attr_reader :room_host, :broadcaster
|
|
39
|
-
attr_accessor :provision_delay_ms, :drain_timeout, :handoff_timeout
|
|
40
|
-
|
|
41
|
-
def initialize
|
|
42
|
-
DEFAULTS.each { |name, value| instance_variable_set("@#{name}", value) }
|
|
4
|
+
class ConfigError < ArgumentError; end
|
|
5
|
+
|
|
6
|
+
ROOM_HOSTS = %i[inline remote].freeze
|
|
7
|
+
ROOM_HOST_ENV = "CABLE_ROOM_HOST"
|
|
8
|
+
DEFAULT_PROVISION_DELAY_MS = 20
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Where rooms run. `:inline` (the default) keeps them in the web process; `:remote` moves
|
|
12
|
+
# them to a dedicated host pool. Set it in an initializer:
|
|
13
|
+
#
|
|
14
|
+
# CableRoom.room_host = :remote
|
|
15
|
+
#
|
|
16
|
+
# The CABLE_ROOM_HOST env var (`inline` or `remote`, any case) wins over the setter, so one
|
|
17
|
+
# build can run in either mode without a code change. The env var is read once, on first
|
|
18
|
+
# use; the Railtie reads it at boot so a bad value fails there instead of mid-request.
|
|
19
|
+
def room_host
|
|
20
|
+
@room_host_from_env = room_host_from_env unless defined?(@room_host_from_env)
|
|
21
|
+
@room_host_from_env || @room_host || :inline
|
|
43
22
|
end
|
|
44
23
|
|
|
45
|
-
#
|
|
46
|
-
# dedicated host pool. Accepts a Symbol or String; unknown values raise right away.
|
|
24
|
+
# Accepts a Symbol or String; unknown values raise CableRoom::ConfigError
|
|
47
25
|
def room_host=(value)
|
|
48
|
-
@room_host =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
# How a room pushes messages to members. Accepts a Symbol or String; unknown values raise.
|
|
52
|
-
def broadcaster=(value)
|
|
53
|
-
@broadcaster = validate_choice!(:broadcaster, value, BROADCASTERS)
|
|
26
|
+
@room_host = cast_room_host(value) do
|
|
27
|
+
"CableRoom.room_host must be one of #{ROOM_HOSTS.map(&:inspect).join(', ')} (got #{value.inspect})"
|
|
28
|
+
end
|
|
54
29
|
end
|
|
55
30
|
|
|
56
|
-
# Rooms run inside every process that joins them (the web process in a Rails app).
|
|
57
31
|
def inline?
|
|
58
32
|
room_host == :inline
|
|
59
33
|
end
|
|
60
34
|
|
|
61
|
-
# Rooms run only in `cable_room server` processes; a web process never hosts one.
|
|
62
35
|
def remote?
|
|
63
36
|
room_host == :remote
|
|
64
37
|
end
|
|
65
38
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
39
|
+
# How long a rooms Host waits, per room it already runs, before racing for a room a member
|
|
40
|
+
# asked for (see Host::Placement). The least-loaded Host wakes first and usually wins, so
|
|
41
|
+
# this is what spreads rooms across a pool. Milliseconds; defaults to 20. `nil` restores it.
|
|
42
|
+
def provision_delay_ms
|
|
43
|
+
@provision_delay_ms || DEFAULT_PROVISION_DELAY_MS
|
|
44
|
+
end
|
|
72
45
|
|
|
73
|
-
|
|
46
|
+
def provision_delay_ms=(value)
|
|
47
|
+
unless value.nil? || (value.is_a?(Numeric) && value >= 0)
|
|
48
|
+
raise ConfigError, "CableRoom.provision_delay_ms must be a number >= 0 (got #{value.inspect})"
|
|
74
49
|
end
|
|
75
|
-
self
|
|
76
|
-
end
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
# this mostly guards the timing knobs, which are plain accessors.
|
|
80
|
-
def validate!
|
|
81
|
-
validate_choice!(:room_host, room_host, ROOM_HOSTS)
|
|
82
|
-
validate_choice!(:broadcaster, broadcaster, BROADCASTERS)
|
|
83
|
-
validate_number!(:provision_delay_ms, provision_delay_ms, min: 0)
|
|
84
|
-
validate_number!(:drain_timeout, drain_timeout, min: 0, exclusive: true)
|
|
85
|
-
validate_number!(:handoff_timeout, handoff_timeout, min: 0, exclusive: true)
|
|
86
|
-
self
|
|
51
|
+
@provision_delay_ms = value
|
|
87
52
|
end
|
|
88
53
|
|
|
89
|
-
|
|
90
|
-
|
|
54
|
+
# Forgets both the setter and the env var, so the next read starts over. For specs.
|
|
55
|
+
def reset_room_host!
|
|
56
|
+
remove_instance_variable(:@room_host) if defined?(@room_host)
|
|
57
|
+
remove_instance_variable(:@room_host_from_env) if defined?(@room_host_from_env)
|
|
91
58
|
end
|
|
92
59
|
|
|
93
60
|
private
|
|
94
61
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
return
|
|
62
|
+
def room_host_from_env
|
|
63
|
+
value = ENV[ROOM_HOST_ENV]
|
|
64
|
+
return nil if value.nil? || value.strip.empty?
|
|
98
65
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
66
|
+
cast_room_host(value.strip.downcase) do
|
|
67
|
+
"#{ROOM_HOST_ENV} must be one of #{ROOM_HOSTS.join(', ')} (got #{value.inspect})"
|
|
68
|
+
end
|
|
102
69
|
end
|
|
103
70
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
return
|
|
71
|
+
def cast_room_host(value)
|
|
72
|
+
symbol = value.to_s.to_sym unless value.nil?
|
|
73
|
+
return symbol if ROOM_HOSTS.include?(symbol)
|
|
107
74
|
|
|
108
|
-
|
|
109
|
-
raise InvalidOption, "CableRoom config: #{name} must be a number #{bound} (got #{value.inspect})"
|
|
75
|
+
raise ConfigError, yield
|
|
110
76
|
end
|
|
111
77
|
end
|
|
112
78
|
end
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
module CableRoom
|
|
2
2
|
class Host
|
|
3
3
|
# How rooms receive messages: off the process-wide CableRoom::Bus (see `CableRoom.bus`), on
|
|
4
|
-
# the channels `
|
|
5
|
-
#
|
|
6
|
-
# difference is which process the Host lives in. Anything with these two methods can be handed
|
|
7
|
-
# to `Host.new(inbound:)`.
|
|
4
|
+
# the channels `Bus.inbound_channel` names. This is the only room-side code that touches the
|
|
5
|
+
# inbound transport; anything with these two methods can be handed to `Host.new(inbound:)`.
|
|
8
6
|
#
|
|
9
7
|
# `subscribe` returns a handle that `unsubscribe` needs back. Handlers get the decoded message
|
|
10
8
|
# on the Bus's single subscriber thread, in publish order, so they must be quick and hand the
|
|
@@ -12,22 +10,29 @@ module CableRoom
|
|
|
12
10
|
# Redis confirms the subscription, so `on_live` runs on the caller's thread right before it
|
|
13
11
|
# returns, and anything published after that is delivered.
|
|
14
12
|
class BusInbound
|
|
15
|
-
# `bus` defaults to `CableRoom.bus` (looked up lazily so specs can swap it)
|
|
16
|
-
|
|
13
|
+
# `bus` defaults to `CableRoom.bus` (looked up lazily so specs can swap it). `timeout` is how
|
|
14
|
+
# long `subscribe` and `unsubscribe` wait for Redis to confirm.
|
|
15
|
+
def initialize(bus: nil, timeout: Bus::DEFAULT_TIMEOUT)
|
|
17
16
|
@bus = bus
|
|
17
|
+
@timeout = timeout
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def subscribe(channel, on_live: nil, &on_message)
|
|
21
|
-
|
|
21
|
+
handle = ->(message, _channel) { on_message.call(message) }
|
|
22
|
+
bus.subscribe(channel, timeout: @timeout, &handle)
|
|
22
23
|
on_live&.call
|
|
23
|
-
|
|
24
|
+
handle
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
# The Bus keeps one handler per channel, so only let go of the channel while it's still
|
|
28
|
+
# ours. A runner undoing a late subscribe (see Runner#subscribe) must not drop the handler
|
|
29
|
+
# of the next runner for the same room.
|
|
30
|
+
def unsubscribe(channel, handle)
|
|
31
|
+
bus.unsubscribe(channel, handler: handle, timeout: @timeout)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
private
|
|
35
|
+
|
|
31
36
|
def bus
|
|
32
37
|
@bus || CableRoom.bus
|
|
33
38
|
end
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
require 'active_support/inflector'
|
|
2
|
+
|
|
3
|
+
module CableRoom
|
|
4
|
+
class Host
|
|
5
|
+
# The rooms side of provisioning. In :remote a `create: true` member can't start its room, so
|
|
6
|
+
# it publishes a provision request on `Bus.provision_channel` (see
|
|
7
|
+
# RoomMembership#request_remote_room). Every rooms Host runs one Placement, which hears every
|
|
8
|
+
# request and decides whether this Host should be the one to start the room.
|
|
9
|
+
#
|
|
10
|
+
# The decision is a race weighted by load. Each Host waits `CableRoom.provision_delay_ms` for
|
|
11
|
+
# every room it already runs, plus up to one room's worth of jitter to break ties, then tries
|
|
12
|
+
# the room's Redlock once through `Host#ensure_room`. The least-loaded Host wakes first and
|
|
13
|
+
# usually wins; the rest find the lock held and do nothing. Losing is normal and quiet.
|
|
14
|
+
#
|
|
15
|
+
# Requests arrive on the Bus subscriber thread, which must stay quick, and a Bus subscribe made
|
|
16
|
+
# from that thread doesn't wait for Redis to confirm it, so a room started there would take
|
|
17
|
+
# traffic before it's listening. So `handle` only validates and schedules; the wait and the
|
|
18
|
+
# claim run later on a small executor of Placement's own. It's bounded twice: CLAIM_THREADS
|
|
19
|
+
# claims run at once, and at most MAX_PENDING wait at a time (more are dropped; the member
|
|
20
|
+
# asks again on its next ping).
|
|
21
|
+
#
|
|
22
|
+
# The request carries the member's tenant, and the claim runs switched into it, so the lock
|
|
23
|
+
# key, the inbound channel, and the stream names come out the same as on the web side under a
|
|
24
|
+
# tenant-prefixing `broadcasting_for` (PandaPal's).
|
|
25
|
+
#
|
|
26
|
+
# Specs make the race deterministic with `jitter: -> { 0 }` (any callable returning 0..1).
|
|
27
|
+
class Placement
|
|
28
|
+
# A request this Host can't act on. Logged and dropped, never raised to the Bus.
|
|
29
|
+
class InvalidRequest < ArgumentError; end
|
|
30
|
+
|
|
31
|
+
REQUEST_TYPE = "provision".freeze
|
|
32
|
+
|
|
33
|
+
# How many claims run at once. A claim is mostly a Redlock call and, for the winner, room
|
|
34
|
+
# startup (a few Bus subscribes), so a couple of threads keep up with a burst.
|
|
35
|
+
CLAIM_THREADS = 2
|
|
36
|
+
|
|
37
|
+
# How many claims may wait on their timer at once. Past this, requests are dropped.
|
|
38
|
+
MAX_PENDING = 100
|
|
39
|
+
|
|
40
|
+
# Only something shaped like a constant name gets near `safe_constantize`
|
|
41
|
+
CLASS_NAME = /\A(::)?[A-Z]\w*(::[A-Z]\w*)*\z/
|
|
42
|
+
|
|
43
|
+
attr_reader :host, :channel
|
|
44
|
+
|
|
45
|
+
# `jitter` returns a Float in 0..1 (defaults to `rand`).
|
|
46
|
+
def initialize(host, jitter: nil)
|
|
47
|
+
@host = host
|
|
48
|
+
@jitter = jitter || -> { Kernel.rand }
|
|
49
|
+
@channel = Bus.provision_channel
|
|
50
|
+
|
|
51
|
+
# Everything below is guarded by @mutex, which is never held across a call into the Bus
|
|
52
|
+
@mutex = Mutex.new
|
|
53
|
+
@idle = ConditionVariable.new
|
|
54
|
+
@pending = {} # request identity => its scheduled claim, so one room never queues twice here
|
|
55
|
+
@stopped = true
|
|
56
|
+
@handle = nil
|
|
57
|
+
@executor = nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Subscribe to the provision channel on the Host's inbound transport. Blocks until Redis
|
|
61
|
+
# confirms, so a request published after this returns is heard. Calling it while running
|
|
62
|
+
# does nothing.
|
|
63
|
+
def start
|
|
64
|
+
@mutex.synchronize do
|
|
65
|
+
return self unless @stopped
|
|
66
|
+
|
|
67
|
+
@stopped = false
|
|
68
|
+
@executor = Concurrent::ThreadPoolExecutor.new(
|
|
69
|
+
name: "cable_room-placement",
|
|
70
|
+
min_threads: 0,
|
|
71
|
+
max_threads: CLAIM_THREADS,
|
|
72
|
+
max_queue: 0, # MAX_PENDING bounds what can be queued
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
handle = host.inbound.subscribe(channel) { |request| self.handle(request) }
|
|
77
|
+
@mutex.synchronize { @handle = handle }
|
|
78
|
+
self
|
|
79
|
+
rescue StandardError
|
|
80
|
+
stop
|
|
81
|
+
raise
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Stop hearing requests and drop every claim still waiting on its timer, then wait (up to
|
|
85
|
+
# `wait` seconds) for a claim already mid-attempt to finish, so nothing starts a room here
|
|
86
|
+
# after this returns.
|
|
87
|
+
def stop(wait: 5)
|
|
88
|
+
handle, executor = @mutex.synchronize do
|
|
89
|
+
return self if @stopped
|
|
90
|
+
|
|
91
|
+
@stopped = true
|
|
92
|
+
[@handle, @executor].tap do
|
|
93
|
+
@handle = nil
|
|
94
|
+
@executor = nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
host.inbound.unsubscribe(channel, handle) if handle
|
|
99
|
+
|
|
100
|
+
# A cancelled task never runs its block (and so never its `finish`), so forget it here
|
|
101
|
+
tasks = @mutex.synchronize { @pending.to_a }
|
|
102
|
+
tasks.each { |id, task| finish(id) if task.cancel }
|
|
103
|
+
wait_idle(timeout: wait)
|
|
104
|
+
executor&.shutdown
|
|
105
|
+
self
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def running?
|
|
109
|
+
@mutex.synchronize { !@stopped }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# How many claims are waiting on their timer or mid-attempt right now
|
|
113
|
+
def pending_count
|
|
114
|
+
@mutex.synchronize { @pending.size }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Block until every scheduled claim has finished (won, lost, or been cancelled). Returns
|
|
118
|
+
# false if `timeout` passes first.
|
|
119
|
+
def wait_idle(timeout: 5)
|
|
120
|
+
deadline = monotonic_now + timeout
|
|
121
|
+
@mutex.synchronize do
|
|
122
|
+
until @pending.empty?
|
|
123
|
+
remaining = deadline - monotonic_now
|
|
124
|
+
return false if remaining <= 0
|
|
125
|
+
@idle.wait(@mutex, remaining)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# One request off the Bus, on its subscriber thread. Only the cheap checks happen here; the
|
|
132
|
+
# delayed claim is scheduled on the executor. Returns the scheduled task, or nil when there
|
|
133
|
+
# is nothing to do.
|
|
134
|
+
def handle(request)
|
|
135
|
+
room_class, serialized_key, tenant = parse(request)
|
|
136
|
+
id = [room_class.name, serialized_key, tenant]
|
|
137
|
+
|
|
138
|
+
return skip(id, "host is shutting down") if host.shutdown?
|
|
139
|
+
|
|
140
|
+
load = host.open_rooms_count
|
|
141
|
+
delay = delay_for(load)
|
|
142
|
+
|
|
143
|
+
@mutex.synchronize do
|
|
144
|
+
return skip(id, "placement is stopped") if @stopped
|
|
145
|
+
return skip(id, "a claim is already pending here") if @pending.key?(id)
|
|
146
|
+
return skip(id, "#{MAX_PENDING} claims are already pending here") if @pending.size >= MAX_PENDING
|
|
147
|
+
|
|
148
|
+
task = Concurrent::ScheduledTask.new(delay, executor: @executor) do
|
|
149
|
+
claim(room_class, serialized_key, tenant, delay: delay, load: load)
|
|
150
|
+
ensure
|
|
151
|
+
finish(id)
|
|
152
|
+
end
|
|
153
|
+
@pending[id] = task
|
|
154
|
+
task.execute
|
|
155
|
+
end
|
|
156
|
+
rescue InvalidRequest => e
|
|
157
|
+
logger.warn "Dropping provision request: #{e.message}"
|
|
158
|
+
nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Seconds to wait before racing for a room, given how many rooms this Host already runs:
|
|
162
|
+
# `provision_delay_ms × (load + jitter)`, jitter in 0..1. A Host with more rooms always waits
|
|
163
|
+
# longer than one with fewer; the jitter only orders Hosts with the same load.
|
|
164
|
+
def delay_for(load)
|
|
165
|
+
CableRoom.provision_delay_ms / 1000.0 * (load + @jitter.call.to_f)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
# After the delay, on the executor. Re-checks what may have changed while we waited, then
|
|
171
|
+
# tries the room's lock once. The key is deserialized here, in the tenant, because a record
|
|
172
|
+
# key is a database lookup.
|
|
173
|
+
def claim(room_class, serialized_key, tenant, delay:, load:)
|
|
174
|
+
return if stopped? || host.shutdown?
|
|
175
|
+
|
|
176
|
+
CableRoom.with_app_executor do
|
|
177
|
+
CableRoom.with_tenant(tenant) do
|
|
178
|
+
key = deserialize_key(serialized_key)
|
|
179
|
+
next if running_here?(room_class, key, tenant)
|
|
180
|
+
|
|
181
|
+
payload = { host: host, room_class: room_class, room_key: key, tenant: tenant, delay: delay, open_rooms: load }
|
|
182
|
+
claimed = ActiveSupport::Notifications.instrument("provision_claimed.cable_room", payload) do |p|
|
|
183
|
+
p[:claimed] = host.ensure_room(room_class, key, tenant: tenant)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
name = "#{room_class.name}[#{key}]"
|
|
187
|
+
if claimed
|
|
188
|
+
logger.info "Claimed #{name} after #{(delay * 1000).round}ms with #{load} room(s) open"
|
|
189
|
+
else
|
|
190
|
+
logger.debug "Lost the race for #{name}: another host holds its lock"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
rescue InvalidRequest => e
|
|
195
|
+
logger.warn "Dropping provision request: #{e.message}"
|
|
196
|
+
rescue StandardError => e
|
|
197
|
+
logger.error "Failed to claim #{room_class.name}: #{e.class}: #{e.message}"
|
|
198
|
+
CableRoom.report_error(e, placement: self, room_class: room_class, room_key: serialized_key, tenant: tenant)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def finish(id)
|
|
202
|
+
@mutex.synchronize do
|
|
203
|
+
@pending.delete(id)
|
|
204
|
+
@idle.broadcast if @pending.empty?
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def skip(id, why)
|
|
209
|
+
logger.debug "Ignoring provision request for #{id.inspect}: #{why}"
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def stopped?
|
|
214
|
+
@mutex.synchronize { @stopped }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def running_here?(room_class, key, tenant)
|
|
218
|
+
host.runners.any? { |r| r.room_class == room_class && r.key == key && r.tenant == tenant }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Check a decoded Bus message's shape and find its Room class. The class name comes off the
|
|
222
|
+
# wire, so only a CableRoom::Room::Base subclass is accepted: `constantize` on an arbitrary
|
|
223
|
+
# string is not something a request should be able to do.
|
|
224
|
+
def parse(request)
|
|
225
|
+
raise InvalidRequest, "not a Hash: #{request.inspect.truncate(200)}" unless request.is_a?(Hash)
|
|
226
|
+
|
|
227
|
+
type = request["type"]
|
|
228
|
+
raise InvalidRequest, "expected a #{REQUEST_TYPE.inspect} request (got #{type.inspect.truncate(100)})" unless type == REQUEST_TYPE
|
|
229
|
+
|
|
230
|
+
name = request["room_class"]
|
|
231
|
+
klass = ActiveSupport::Inflector.safe_constantize(name) if name.is_a?(String) && name.match?(CLASS_NAME)
|
|
232
|
+
unless klass.is_a?(Class) && klass < Room::Base
|
|
233
|
+
raise InvalidRequest, "#{name.inspect.truncate(100)} is not a CableRoom::Room::Base subclass"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The key travels the way `extra` does (ActiveJob's argument serializer): a one-item list
|
|
237
|
+
key = request["room_key"]
|
|
238
|
+
raise InvalidRequest, "room_key must be a serialized one-item list (got #{key.inspect.truncate(100)})" unless key.is_a?(Array) && key.size == 1
|
|
239
|
+
|
|
240
|
+
tenant = request["tenant"]
|
|
241
|
+
raise InvalidRequest, "tenant must be a String or null (got #{tenant.inspect.truncate(100)})" unless tenant.nil? || tenant.is_a?(String)
|
|
242
|
+
|
|
243
|
+
[klass, key, tenant]
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def deserialize_key(serialized)
|
|
247
|
+
::ActiveJob::Arguments.deserialize(serialized).first
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
raise InvalidRequest, "room_key can't be deserialized: #{e.class}: #{e.message}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def logger
|
|
253
|
+
host.logger
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def monotonic_now
|
|
257
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|