cable_room 0.1.0
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 +7 -0
- data/README.md +1 -0
- data/cable_room.gemspec +32 -0
- data/lib/cable_room/channel_base.rb +194 -0
- data/lib/cable_room/channel_tracker.rb +118 -0
- data/lib/cable_room/ports.rb +62 -0
- data/lib/cable_room/railtie.rb +53 -0
- data/lib/cable_room/room/base.rb +92 -0
- data/lib/cable_room/room/callbacks.rb +33 -0
- data/lib/cable_room/room/channel_adapter.rb +20 -0
- data/lib/cable_room/room/input_handling.rb +31 -0
- data/lib/cable_room/room/lifecycle.rb +53 -0
- data/lib/cable_room/room/member_management.rb +67 -0
- data/lib/cable_room/room/reaping.rb +84 -0
- data/lib/cable_room/room/threading.rb +13 -0
- data/lib/cable_room/room/user_management.rb +72 -0
- data/lib/cable_room/room.rb +20 -0
- data/lib/cable_room/room_member.rb +170 -0
- data/lib/cable_room/version.rb +3 -0
- data/lib/cable_room.rb +36 -0
- data/spec/cable_room/room_member_spec.rb +113 -0
- data/spec/internal/config/cable.yml +2 -0
- data/spec/internal/config/database.yml +5 -0
- data/spec/internal/config/routes.rb +5 -0
- data/spec/internal/config/storage.yml +3 -0
- data/spec/internal/db/schema.rb +6 -0
- data/spec/internal/log/test.log +720 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/spec_helper.rb +20 -0
- metadata +165 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bc7b6f7f0f2c70e5db315cd4dc11ff01e7b0d8dcd56ba5d54159d2c0f3b6d56e
|
|
4
|
+
data.tar.gz: b8a9866e23a63d93cd52c88ce9008d194f2ed3cea2060b0b533c5236235fe244
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c13aee8af8dba031b0e7333539bde585f8e898a8afbf87657a0fc0128e21c32580149b866107c976c4742df690f8c838d0e094b63e96d140a1d6fb6731a8d652
|
|
7
|
+
data.tar.gz: 2ca7afb01a2bf5b2c0096045f6f486ecf8a744b63017497fcf9dd835d770d1fd99b37d63a8ff8c442e1aacd7ed5e0167725f08077c93a6de3fde13a94805c4bd
|
data/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# RediConn
|
data/cable_room.gemspec
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
|
|
5
|
+
begin
|
|
6
|
+
require "cable_room/version"
|
|
7
|
+
version = CableRoom::VERSION
|
|
8
|
+
rescue LoadError
|
|
9
|
+
version = "0.0.0.docker"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Gem::Specification.new do |spec|
|
|
13
|
+
spec.name = "cable_room"
|
|
14
|
+
spec.version = version
|
|
15
|
+
spec.authors = ["Ethan Knapp"]
|
|
16
|
+
spec.email = ["eknapp@instructure.com"]
|
|
17
|
+
|
|
18
|
+
spec.summary = "Build live Rooms on top of ActionCable"
|
|
19
|
+
spec.homepage = "https://instructure.com"
|
|
20
|
+
|
|
21
|
+
spec.files = Dir["{app,config,db,lib}/**/*", "README.md", "*.gemspec"]
|
|
22
|
+
spec.test_files = Dir["spec/**/*"]
|
|
23
|
+
spec.require_paths = ['lib']
|
|
24
|
+
|
|
25
|
+
spec.add_dependency "rails", ">= 7.2", "< 9.0"
|
|
26
|
+
spec.add_dependency "rufus-scheduler", "~> 3.6"
|
|
27
|
+
spec.add_dependency "redlock", "~> 2.0"
|
|
28
|
+
spec.add_dependency "rediconn", "~> 0.1.0"
|
|
29
|
+
|
|
30
|
+
spec.add_development_dependency "redis"
|
|
31
|
+
spec.add_development_dependency 'rspec', '~> 3'
|
|
32
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
class ChannelBase < ActionCable::Channel::Base
|
|
3
|
+
attr_reader :room, :tenant
|
|
4
|
+
attr_reader :server, :logger
|
|
5
|
+
delegate :event_loop, :worker_pool, :pubsub, to: :server
|
|
6
|
+
|
|
7
|
+
def initialize(lock_info, room_class, key, config)
|
|
8
|
+
# We don't really have a "connection" in the ActionCable sense, so
|
|
9
|
+
# stuff in something that half looks like one
|
|
10
|
+
super(DummyConnection.new(self), "Room[]", {})
|
|
11
|
+
|
|
12
|
+
# Used mainly for logs and being able to follow a specific Room instance
|
|
13
|
+
@uuid = SecureRandom.hex(6)
|
|
14
|
+
|
|
15
|
+
@mutex = Monitor.new
|
|
16
|
+
@lock_info = lock_info
|
|
17
|
+
@current_state = :initializing
|
|
18
|
+
|
|
19
|
+
@tenant = Apartment::Tenant.current if defined?(Apartment)
|
|
20
|
+
|
|
21
|
+
@server = ChannelTracker.instance
|
|
22
|
+
@logger = ActionCable::Connection::TaggedLoggerProxy.new(
|
|
23
|
+
@server.logger,
|
|
24
|
+
tags: ["#{room_class.name} #{@uuid}"]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
logger.info "Initializing new #{room_class.name}"
|
|
28
|
+
logger.info " UUID: #{@uuid}"
|
|
29
|
+
logger.info " Key: #{room_class.room_port_key(key)}"
|
|
30
|
+
|
|
31
|
+
@watchdog_interval = config[:watchdog_interval]
|
|
32
|
+
@lock_duration = config[:lock_duration]
|
|
33
|
+
|
|
34
|
+
@processing_work = false
|
|
35
|
+
@work_queue = []
|
|
36
|
+
|
|
37
|
+
@room = room_class.new(self, key)
|
|
38
|
+
@server.track_room_channel self
|
|
39
|
+
ping_watchdog
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.channel_name
|
|
43
|
+
module_parent.name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def stream_from(...)
|
|
47
|
+
raise ArgumentError, "Block required" unless block_given?
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def state
|
|
52
|
+
@current_state
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
after_subscribe do
|
|
56
|
+
begin
|
|
57
|
+
@current_state = :starting
|
|
58
|
+
@room.send(:_startup)
|
|
59
|
+
@current_state = :started
|
|
60
|
+
rescue => e
|
|
61
|
+
terminate!
|
|
62
|
+
raise e
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
after_unsubscribe do
|
|
67
|
+
@mutex.synchronize do
|
|
68
|
+
@current_state = :shutting_down
|
|
69
|
+
stop_all_streams
|
|
70
|
+
@room.send(:_shutdown)
|
|
71
|
+
terminate!
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def ping_watchdog
|
|
76
|
+
return if state == :dead
|
|
77
|
+
|
|
78
|
+
logger.debug "Ping watchdog"
|
|
79
|
+
@last_watchdog_ping_at = Time.current
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def check_room_watchdog
|
|
83
|
+
return if state == :dead || state == :shutting_down
|
|
84
|
+
|
|
85
|
+
relock = CableRoom.lock_manager.lock(@lock_info[:resource], @lock_duration.in_milliseconds, extend: @lock_info)
|
|
86
|
+
unless relock
|
|
87
|
+
logger.warn "Lost lock, shutting down"
|
|
88
|
+
unsubscribe_from_channel
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
unless @last_watchdog_ping_at && @last_watchdog_ping_at > @watchdog_interval.ago
|
|
93
|
+
logger.warn "Watchdog timeout for room #{@room.class.name}[#{@room.key}], shutting down"
|
|
94
|
+
initiate_shutdown("Watchdog timeout")
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def initiate_shutdown(reason)
|
|
100
|
+
@mutex.synchronize do
|
|
101
|
+
return if @current_state == :dead || @current_state == :shutting_down
|
|
102
|
+
|
|
103
|
+
# Stop streams immediately to prevent further messages from being added
|
|
104
|
+
stop_all_streams
|
|
105
|
+
|
|
106
|
+
# Append the final unsubscribe to the work queue so we can process remaining messages first
|
|
107
|
+
post_work(async: false) do
|
|
108
|
+
unsubscribe_from_channel
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@current_state = :shutting_down
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def terminate!
|
|
116
|
+
@mutex.synchronize do
|
|
117
|
+
stop_all_streams
|
|
118
|
+
@current_state = :dead
|
|
119
|
+
CableRoom.lock_manager.unlock(@lock_info) if @lock_info
|
|
120
|
+
server.untrack_room_channel self
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def post_work(async: false, &blk)
|
|
125
|
+
if async
|
|
126
|
+
# Async stuff is mostly untracked - we just post it to the worker pool and forget about it
|
|
127
|
+
worker_pool.executor.post(&blk)
|
|
128
|
+
else
|
|
129
|
+
@mutex.synchronize do
|
|
130
|
+
raise "Attempt to post work to dead or shutting down room" if @current_state == :dead || @current_state == :shutting_down
|
|
131
|
+
@work_queue << blk
|
|
132
|
+
end
|
|
133
|
+
schedule_work
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def beat
|
|
138
|
+
post_work(async: true) do
|
|
139
|
+
check_room_watchdog
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def transmit(*args)
|
|
144
|
+
logger.info("Channel.transmit called, ignoring: #{args.inspect}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
protected
|
|
148
|
+
|
|
149
|
+
def start_periodic_timer(callback, every:)
|
|
150
|
+
raise "Attempt to start periodic timer on a dead room" if state == :dead || state == :shutting_down
|
|
151
|
+
|
|
152
|
+
connection.server.scheduler.schedule_every(every) do
|
|
153
|
+
post_work(async: false) do
|
|
154
|
+
instance_exec(&callback)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def schedule_work
|
|
160
|
+
@mutex.synchronize do
|
|
161
|
+
return if @processing_work
|
|
162
|
+
|
|
163
|
+
work = @work_queue.shift
|
|
164
|
+
return unless work
|
|
165
|
+
|
|
166
|
+
@processing_work = true
|
|
167
|
+
|
|
168
|
+
worker_pool.executor.post do
|
|
169
|
+
begin
|
|
170
|
+
work.call
|
|
171
|
+
ensure
|
|
172
|
+
@mutex.synchronize do
|
|
173
|
+
@processing_work = false
|
|
174
|
+
end
|
|
175
|
+
schedule_work
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class DummyConnection
|
|
183
|
+
attr_reader :channel
|
|
184
|
+
delegate :server, :logger, :tenant, :transmit, :post_work, to: :channel
|
|
185
|
+
delegate :event_loop, :pubsub, :worker_pool, to: :server
|
|
186
|
+
|
|
187
|
+
attr_reader :identifiers
|
|
188
|
+
|
|
189
|
+
def initialize(channel)
|
|
190
|
+
@channel = channel
|
|
191
|
+
@identifiers = []
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
class ChannelTracker
|
|
3
|
+
def self.instance
|
|
4
|
+
@instance ||= new
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
BEAT_INTERVAL = 5.seconds
|
|
8
|
+
|
|
9
|
+
delegate :logger, :pubsub, :event_loop, :config, to: :cable_server
|
|
10
|
+
|
|
11
|
+
attr_reader :room_channels, :scheduler
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@room_channels = Set.new
|
|
15
|
+
@monitor = Monitor.new
|
|
16
|
+
|
|
17
|
+
@scheduler = Rufus::Scheduler.new
|
|
18
|
+
|
|
19
|
+
at_exit do
|
|
20
|
+
logger.info "Shutting down CableRoom"
|
|
21
|
+
shutdown!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
scheduler.every(BEAT_INTERVAL) do
|
|
25
|
+
each_room_channel do |chan|
|
|
26
|
+
worker_pool.async_invoke(chan, :beat)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def worker_pool
|
|
32
|
+
# TODO Pin Rooms to a specific thread so that they never have to worry about thread safety?
|
|
33
|
+
@worker_pool || @monitor.synchronize { @worker_pool ||= ThreadPool.new(max_size: config.worker_pool_size) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def track_room_channel(chan)
|
|
37
|
+
raise "Cannot add Room after shutdown" if @shutdown
|
|
38
|
+
@room_channels << chan
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def untrack_room_channel(chan)
|
|
42
|
+
@room_channels.delete(chan)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def shutdown?
|
|
46
|
+
@shutdown
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def shutdown!
|
|
50
|
+
@monitor.synchronize do
|
|
51
|
+
@shutdown = true
|
|
52
|
+
|
|
53
|
+
scheduler.shutdown
|
|
54
|
+
|
|
55
|
+
shutdown_rooms!
|
|
56
|
+
|
|
57
|
+
worker_pool.executor.shutdown
|
|
58
|
+
worker_pool.executor.wait_for_termination(5)
|
|
59
|
+
worker_pool.executor.kill
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def shutdown_rooms!
|
|
64
|
+
@monitor.synchronize do
|
|
65
|
+
each_room_channel do |chan|
|
|
66
|
+
chan.initiate_shutdown("Server shutting down")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
count = 0
|
|
71
|
+
loop do
|
|
72
|
+
break if @room_channels.empty? || count >= 15
|
|
73
|
+
sleep 1
|
|
74
|
+
count += 1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
protected
|
|
79
|
+
|
|
80
|
+
def each_room_channel(&blk)
|
|
81
|
+
@room_channels.dup.each(&blk)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def cable_server
|
|
87
|
+
ActionCable.server
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class ThreadPool < ActionCable::Server::Worker
|
|
91
|
+
set_callback :work, :around do |_, blk|
|
|
92
|
+
pconn = ActionCable::Server::Worker.connection
|
|
93
|
+
ActionCable::Server::Worker.connection = connection
|
|
94
|
+
blk.call
|
|
95
|
+
ensure
|
|
96
|
+
ActionCable::Server::Worker.connection = pconn
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def async_invoke(receiver, method, *args, connection: receiver, async: nil, &block)
|
|
100
|
+
# Instead of posting directly to the global pool, post to a dedicated queue for the room/"connection".
|
|
101
|
+
# This makes each rooms so that they can be processed by at-most-one thread at a time, while still
|
|
102
|
+
# allowing multiple rooms to be processed by the same thread-pool.
|
|
103
|
+
|
|
104
|
+
# TODO Implement more of a round-robin approach so that no room can starve out others?
|
|
105
|
+
|
|
106
|
+
# "connection" here really references the Channel, since "Connections" in this context don't really exist
|
|
107
|
+
|
|
108
|
+
connection.post_work(async:) do
|
|
109
|
+
invoke(receiver, method, *args, connection: connection, &block)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
Rufus::Scheduler::Job.class_eval do
|
|
115
|
+
alias_method :shutdown, :unschedule
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
module Ports
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
def ports
|
|
6
|
+
@ports_proxy ||= PortsProxy.new(self)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def stream_port(port, auto_close: true, &blk)
|
|
10
|
+
@cable_channel.stream_from(room_port_key(port), coder: ActiveSupport::JSON, &blk)
|
|
11
|
+
_streamed_ports << port if auto_close
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def close_streamed_ports!
|
|
15
|
+
_streamed_ports.each do |port|
|
|
16
|
+
@cable_channel.stop_stream_from(room_port_key(port))
|
|
17
|
+
end
|
|
18
|
+
_streamed_ports.clear
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def port_transmit(port, data)
|
|
22
|
+
ActionCable.server.broadcast(room_port_key(port), data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def _room_channel_class
|
|
28
|
+
room_class.channel_class
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def _streamed_ports
|
|
32
|
+
@_streamed_ports ||= Set.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class PortsProxy
|
|
36
|
+
def initialize(ports_concern)
|
|
37
|
+
@ports_concern = ports_concern
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def [](port)
|
|
41
|
+
PortProxy.new(@ports_concern, port)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class PortProxy
|
|
46
|
+
def initialize(ports_concern, port)
|
|
47
|
+
@ports_concern = ports_concern
|
|
48
|
+
@port = port
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def transmit(data)
|
|
52
|
+
@ports_concern.port_transmit(@port, data)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
alias_method :<<, :transmit
|
|
56
|
+
|
|
57
|
+
def stream(&blk)
|
|
58
|
+
@ports_concern.stream_port(@port, &blk)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
require "active_model/railtie"
|
|
5
|
+
|
|
6
|
+
module CableRoom
|
|
7
|
+
class Railtie < Rails::Railtie # :nodoc:
|
|
8
|
+
rake_tasks do
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
console do |app|
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
runner do
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
initializer "cable_room.shutdown" do
|
|
18
|
+
Rails.application.reloader.before_class_unload do
|
|
19
|
+
CableRoom::ChannelTracker.instance.shutdown_rooms!
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
initializer "cable_room.monkey_patch_redlock" do
|
|
24
|
+
require 'redlock'
|
|
25
|
+
|
|
26
|
+
module ::Redlock
|
|
27
|
+
class Client
|
|
28
|
+
private
|
|
29
|
+
class RedisInstance
|
|
30
|
+
private
|
|
31
|
+
def recover_from_script_flush
|
|
32
|
+
retry_on_noscript = true
|
|
33
|
+
begin
|
|
34
|
+
yield
|
|
35
|
+
rescue RedisClient::CommandError, Redis::CommandError => e # <= ADDED Redis::CommandError
|
|
36
|
+
# When somebody has flushed the Redis instance's script cache, we might
|
|
37
|
+
# want to reload our scripts. Only attempt this once, though, to avoid
|
|
38
|
+
# going into an infinite loop.
|
|
39
|
+
if retry_on_noscript && e.message.include?('NOSCRIPT')
|
|
40
|
+
load_scripts
|
|
41
|
+
retry_on_noscript = false
|
|
42
|
+
retry
|
|
43
|
+
else
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
module Room
|
|
3
|
+
class Base
|
|
4
|
+
ROOM_OUT_CHANNEL = :from_room # Many-to-one channel for messages to the room
|
|
5
|
+
ROOM_IN_CHANNEL = :to_room # One-to-many channel for messages from the room
|
|
6
|
+
|
|
7
|
+
LOCK_DURATION = 15.seconds
|
|
8
|
+
WATCH_DOG_INTERVAL = 15.seconds
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def ensure(key = nil)
|
|
12
|
+
lock_key = room_port_key(key)
|
|
13
|
+
lock_info = CableRoom.lock_manager.lock(lock_key, self::LOCK_DURATION.in_milliseconds)
|
|
14
|
+
return false unless lock_info
|
|
15
|
+
|
|
16
|
+
vchannel = channel_class.new(
|
|
17
|
+
lock_info,
|
|
18
|
+
self,
|
|
19
|
+
key,
|
|
20
|
+
{
|
|
21
|
+
watchdog_interval: self::WATCH_DOG_INTERVAL,
|
|
22
|
+
lock_duration: self::LOCK_DURATION,
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
vchannel.subscribe_to_channel
|
|
26
|
+
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def room_port_key(room_key, port = nil)
|
|
31
|
+
full_key = [room_key]
|
|
32
|
+
full_key << port if port
|
|
33
|
+
self::Channel.broadcasting_for(full_key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def send_message(room_key, data, port: ROOM_IN_CHANNEL)
|
|
37
|
+
ActionCable.server.broadcast(room_port_key(room_key, port), data)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
protected
|
|
41
|
+
|
|
42
|
+
def periodically(method, every:, &blk)
|
|
43
|
+
blk ||= method
|
|
44
|
+
blk = -> { send(method) } unless blk.is_a?(Proc)
|
|
45
|
+
# TODO Looking at the docs, this creates a thread for each timer - that seems expensive
|
|
46
|
+
channel_class.periodically(every:) do
|
|
47
|
+
room.instance_exec(&blk)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
include Ports
|
|
53
|
+
include Callbacks
|
|
54
|
+
include Threading
|
|
55
|
+
include ChannelAdapter
|
|
56
|
+
include InputHandling
|
|
57
|
+
include Lifecycle
|
|
58
|
+
include Reaping
|
|
59
|
+
|
|
60
|
+
include MemberManagement
|
|
61
|
+
include UserManagement
|
|
62
|
+
|
|
63
|
+
attr_reader :key
|
|
64
|
+
|
|
65
|
+
delegate :logger, :params, to: :@cable_channel
|
|
66
|
+
|
|
67
|
+
def initialize(vchannel, key = nil)
|
|
68
|
+
super()
|
|
69
|
+
@key = key
|
|
70
|
+
@cable_channel = vchannel
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def <<(data)
|
|
74
|
+
port_transmit(ROOM_OUT_CHANNEL, data)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
def room_class
|
|
80
|
+
self.class
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def room_port_key(sub_channel = nil)
|
|
84
|
+
self.class.room_port_key(key, sub_channel)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def start_periodic_timer(callback, every:)
|
|
88
|
+
@cable_channel.send(:start_periodic_timer, -> { room.instance_exec(&callback) }, every:)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
module Room
|
|
3
|
+
module Callbacks
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
include ActiveSupport::Callbacks
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
define_callbacks :startup
|
|
9
|
+
define_callbacks :shutdown
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
def before_startup(*methods, &block)
|
|
14
|
+
set_callback(:startup, :before, *methods, &block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def after_startup(*methods, &block)
|
|
18
|
+
set_callback(:startup, :after, *methods, &block)
|
|
19
|
+
end
|
|
20
|
+
alias_method :on_startup, :after_startup
|
|
21
|
+
|
|
22
|
+
def before_shutdown(*methods, &block)
|
|
23
|
+
set_callback(:shutdown, :before, *methods, &block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def after_shutdown(*methods, &block)
|
|
27
|
+
set_callback(:shutdown, :after, *methods, &block)
|
|
28
|
+
end
|
|
29
|
+
alias_method :on_shutdown, :after_shutdown
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
module Room
|
|
3
|
+
module ChannelAdapter
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
Channel = CableRoom::ChannelBase
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def inherited(subclass)
|
|
10
|
+
subclass.const_set(:Channel, Class.new(channel_class))
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def channel_class
|
|
15
|
+
self::Channel
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
module Room
|
|
3
|
+
module InputHandling
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
before_startup do
|
|
8
|
+
ports[self.class::ROOM_IN_CHANNEL].stream do |message|
|
|
9
|
+
logger.debug "Received message: #{message.inspect}"
|
|
10
|
+
if message == "KILL"
|
|
11
|
+
shutdown!
|
|
12
|
+
else
|
|
13
|
+
handle_received_message(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def handle_received_message(message)
|
|
22
|
+
sym = :"on_#{message['type'].underscore.to_s.downcase}"
|
|
23
|
+
if respond_to?(sym, true)
|
|
24
|
+
send(sym, message)
|
|
25
|
+
else
|
|
26
|
+
logger.warn "Unknown room message type: #{message['type'].inspect}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module CableRoom
|
|
2
|
+
module Room
|
|
3
|
+
module Lifecycle
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
after_startup do
|
|
8
|
+
logger.info "Started"
|
|
9
|
+
self << { type: 'room_opened' }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before_shutdown do
|
|
13
|
+
self << { type: 'room_closed' }
|
|
14
|
+
logger.info "Shutting down"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
after_shutdown do
|
|
18
|
+
logger.info "Shutdown complete"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
protected
|
|
23
|
+
|
|
24
|
+
def lifecycle_state
|
|
25
|
+
@cable_channel.state
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Requests that the Room shut down gracefully, processing any pending messages
|
|
29
|
+
def shutdown!(reason = "Room requested shutdown")
|
|
30
|
+
@cable_channel.initiate_shutdown(reason)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Stops the room synchronously, ignoring remaining messages
|
|
34
|
+
def stop!
|
|
35
|
+
@cable_channel.unsubscribe_from_channel
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def _startup
|
|
41
|
+
run_callbacks :startup do
|
|
42
|
+
startup if respond_to?(:startup)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def _shutdown
|
|
47
|
+
run_callbacks :shutdown do
|
|
48
|
+
shutdown if respond_to?(:shutdown)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|