cable_room 0.1.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 841a40a6a2460c6be5811f2fe24e6fffc1bb7291446029bf1e62497df24ee507
4
- data.tar.gz: fff54424d08af52f0a35d23089f7437054b6d2e9a63ae66d620d7678edeadfdf
3
+ metadata.gz: 319532a2546c5bbd96b89ca34c9a44ba963a5bc97bc62e5faacf8417cb815783
4
+ data.tar.gz: 2d1c92b8e36af65a69182537ba25b2ba5652569e21d398f9bccbd91011e6e411
5
5
  SHA512:
6
- metadata.gz: d44fa0d6f66a5905e20ad96b3df974d9d44e43e9c057a8de6316def16f3d577c8f3270254c27cc5c0e1fc4a3fab0be2307566f55b77ada20a53c04f946c1834b
7
- data.tar.gz: ac558498e04bf2d82a9048a7ff1cab21324cc14c36fbcf9073311c3d7bc62cd1094e7df2a17b129cdd5e6bdb06e4befbec49bb6e57f310e4323aab4b2217d4e7
6
+ metadata.gz: 25afc8414cc55c14d41a72c2d8898c026d442f204744a1a67721e2fd6e6b6b9ea9013bfe727ba98984475c504b00eb95bda324fdf2c60a6e2b9269d5e3fb4e17
7
+ data.tar.gz: 903fa66437dec42ba12b87c49a0864b561b0b8b2f302de05f80482d8a086a1e7971ba5f03e04146e4905b6854f017c2d311e222393ba977cca9936d5b656f306
@@ -67,8 +67,11 @@ module CableRoom
67
67
  @mutex.synchronize do
68
68
  @current_state = :shutting_down
69
69
  stop_all_streams
70
- @room.send(:_shutdown)
71
- terminate!
70
+ begin
71
+ @room.send(:_shutdown)
72
+ ensure
73
+ terminate!
74
+ end
72
75
  end
73
76
  end
74
77
 
@@ -80,7 +83,9 @@ module CableRoom
80
83
  end
81
84
 
82
85
  def check_room_watchdog
83
- return if state == :dead || state == :shutting_down
86
+ @mutex.synchronize do
87
+ return if state == :dead || state == :shutting_down
88
+ end
84
89
 
85
90
  relock = CableRoom.lock_manager.lock(@lock_info[:resource], @lock_duration.in_milliseconds, extend: @lock_info)
86
91
  unless relock
@@ -121,7 +126,7 @@ module CableRoom
121
126
  end
122
127
  end
123
128
 
124
- def post_work(async: false, silent: false, &blk)
129
+ def _post_wrapped_work(async: false, silent: false, &blk)
125
130
  if async
126
131
  # Async stuff is mostly untracked - we just post it to the worker pool and forget about it
127
132
  worker_pool.executor.post(&blk)
@@ -137,6 +142,14 @@ module CableRoom
137
142
  end
138
143
  end
139
144
 
145
+ def post_work(**kwargs, &blk)
146
+ _post_wrapped_work(**kwargs) do
147
+ worker_pool.invoke(self, :instance_exec, connection: self, &blk)
148
+ rescue => e
149
+ logger.error "Error during work execution: #{e.class.name}: #{e.message}"
150
+ end
151
+ end
152
+
140
153
  def beat
141
154
  post_work(async: true) do
142
155
  check_room_watchdog
@@ -184,7 +197,7 @@ module CableRoom
184
197
 
185
198
  class DummyConnection
186
199
  attr_reader :channel
187
- delegate :server, :logger, :tenant, :transmit, :post_work, to: :channel
200
+ delegate :server, :logger, :tenant, :transmit, :post_work, :_post_wrapped_work, to: :channel
188
201
  delegate :event_loop, :pubsub, :worker_pool, to: :server
189
202
 
190
203
  attr_reader :identifiers
@@ -96,7 +96,7 @@ module CableRoom
96
96
  ActionCable::Server::Worker.connection = pconn
97
97
  end
98
98
 
99
- def async_invoke(receiver, method, *args, connection: receiver, async: nil, silent: false, &block)
99
+ def async_invoke(receiver, method, *args, connection: receiver, &block)
100
100
  # Instead of posting directly to the global pool, post to a dedicated queue for the room/"connection".
101
101
  # This makes each rooms so that they can be processed by at-most-one thread at a time, while still
102
102
  # allowing multiple rooms to be processed by the same thread-pool.
@@ -105,7 +105,7 @@ module CableRoom
105
105
 
106
106
  # "connection" here really references the Channel, since "Connections" in this context don't really exist
107
107
 
108
- connection.post_work(async:, silent:) do
108
+ connection._post_wrapped_work(async: false) do
109
109
  invoke(receiver, method, *args, connection: connection, &block)
110
110
  end
111
111
  end
@@ -50,15 +50,19 @@ module CableRoom
50
50
  end
51
51
 
52
52
  include Ports
53
+
53
54
  include Callbacks
54
55
  include Threading
55
56
  include ChannelAdapter
56
- include InputHandling
57
+
57
58
  include Lifecycle
58
59
  include Reaping
60
+ include InputHandling
59
61
 
60
- include MemberManagement
62
+ include PortScoping
63
+ include PortManagement
61
64
  include UserManagement
65
+ include Broadcasting
62
66
 
63
67
  attr_reader :key
64
68
 
@@ -0,0 +1,13 @@
1
+ module CableRoom
2
+ module Room
3
+ module Broadcasting
4
+ extend ActiveSupport::Concern
5
+
6
+ def broadcast(message, **kwargs)
7
+ with_port_scope!(**kwargs) do |client_port: nil|
8
+ ports[client_port || self.class::ROOM_OUT_CHANNEL] << message
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,16 +1,58 @@
1
1
  module CableRoom
2
2
  module Room
3
+ thread_mattr_accessor :current_message
4
+
3
5
  module InputHandling
4
6
  extend ActiveSupport::Concern
7
+ include ActiveSupport::Callbacks
8
+
9
+ class_methods do
10
+ def authorize_inbound(symbol_or_proc = nil, only: nil, except: nil, &blk)
11
+ raise ArgumentError, "Must provide either a symbol, proc, or block" unless symbol_or_proc ^ blk
12
+
13
+ only = Set.new(Array(only).map(&:to_sym)) if only
14
+ except = Set.new(Array(except).map(&:to_sym)) if except
15
+
16
+ blk = symbol_or_proc if symbol_or_proc.is_a?(Proc)
17
+ blk = -> { send(symbol_or_proc) } if symbol_or_proc.is_a?(Symbol)
18
+
19
+ set_callback(:receive_message, :before) do
20
+ type = message['type'].to_s.underscore.to_sym
21
+ if (only && !only.include?(type)) || (except && except.include?(type))
22
+ next
23
+ end
24
+
25
+ unless instance_exec(message, &blk)
26
+ logger.warn "Dropping unauthorized message: #{message.inspect}"
27
+ throw :abort
28
+ end
29
+ end
30
+ end
31
+ end
5
32
 
6
33
  included do
34
+ define_callbacks :receive_message
35
+
36
+ set_callback(:receive_message, :before) do
37
+ logger.debug "Received message: #{message.inspect}"
38
+ end
39
+
40
+ set_callback(:receive_message, :before) do
41
+ if message == "KILL"
42
+ shutdown!
43
+ throw :abort
44
+ end
45
+ end
46
+
7
47
  before_startup do
8
48
  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)
49
+ begin
50
+ Room.current_message = message
51
+ run_callbacks :receive_message do
52
+ handle_received_message(message)
53
+ end
54
+ ensure
55
+ Room.current_message = nil
14
56
  end
15
57
  end
16
58
  end
@@ -18,6 +60,10 @@ module CableRoom
18
60
 
19
61
  protected
20
62
 
63
+ def message
64
+ Room.current_message
65
+ end
66
+
21
67
  def handle_received_message(message)
22
68
  sym = :"on_#{message['type'].underscore.to_s.downcase}"
23
69
  if respond_to?(sym, true)
@@ -0,0 +1,295 @@
1
+ module CableRoom
2
+ module Room
3
+ module PortManagement
4
+ extend ActiveSupport::Concern
5
+
6
+ PORT_TIMEOUT = 30.seconds
7
+
8
+ class_methods do
9
+ def inbound_require_tag(tag, **kwargs)
10
+ tag_array = Array(tag).map(&:to_sym)
11
+ authorize_inbound(**kwargs) do |message|
12
+ (tag_array - origin_tags).empty?
13
+ end
14
+ end
15
+
16
+ def inbound_tag_policy(**kwargs, &blk)
17
+ if blk
18
+ self._port_policy_blocks = [*(_port_policy_blocks || []), {
19
+ **kwargs,
20
+ block: blk,
21
+ }]
22
+ else
23
+ @_tag_policy ||= TagPolicy.new().tap do |pol|
24
+ (_port_policy_blocks || []).each do |p|
25
+ pol.evaluate(**p.except(:block), &p[:block])
26
+ end
27
+ end
28
+ @_tag_policy
29
+ end
30
+ end
31
+
32
+ def on_port_connected(...)
33
+ set_callback(:port_connected, :before, ...)
34
+ end
35
+
36
+ def on_port_disconnected(...)
37
+ set_callback(:port_disconnected, :after, ...)
38
+ end
39
+ end
40
+
41
+ included do
42
+ class_attribute :_port_policy_blocks, instance_writer: false, default: {}
43
+
44
+ periodically :check_port_inactivity, every: PORT_TIMEOUT
45
+
46
+ define_callbacks :port_connected, :port_disconnected
47
+
48
+ set_callback(:receive_message, :around) do |_, blk|
49
+ previous_mtok = @current_message_origin
50
+ begin
51
+ mtok = message['mtok']
52
+ @current_message_origin = mtok
53
+ blk.call
54
+ ensure
55
+ @current_message_origin = previous_mtok
56
+ end
57
+ end
58
+
59
+ authorize_inbound do |message|
60
+ msg_type = message['type'].to_s.underscore.to_sym
61
+ next true if %i[port_connected port_disconnected].include?(msg_type)
62
+ policy_allows?(msg_type)
63
+ end
64
+
65
+ inbound_tag_policy(priority: -10) do
66
+ allow :*, :connect
67
+ end
68
+ end
69
+
70
+ def initialize(...)
71
+ super
72
+ @port_data = {}
73
+ end
74
+
75
+ def _apply_port_scope(client_port: nil, tag: nil, **kwargs)
76
+ if client_port && tag
77
+ throw :abort unless port_data(client_port)[:tags]&.include?(tag)
78
+ end
79
+
80
+ { client_port: client_port || tag, **kwargs }
81
+ end
82
+
83
+ def reply(message = nil, **kwargs)
84
+ raise ArgumentError, "Can only use reply when handling a message" unless origin_port
85
+ raise ArgumentError, "Must provide message or block" unless message || block_given?
86
+ raise ArgumentError, "Cannot specify client_port: when using reply" if kwargs.key?(:client_port)
87
+
88
+ if block_given?
89
+ with_port_scope(client_port: origin_port) do
90
+ yield
91
+ end
92
+ else
93
+ broadcast(message, **kwargs, client_port: origin_port)
94
+ end
95
+ end
96
+
97
+ protected
98
+
99
+ def policy_allows?(method)
100
+ self.class.inbound_tag_policy.pass?(method, origin_tags)
101
+ end
102
+
103
+ def on_port_disconnected(); end
104
+
105
+ def on_port_connected(); end
106
+
107
+ def touch_port_activity(mtok = nil)
108
+ port_data(mtok)[:last_seen_at] = Time.now
109
+ end
110
+
111
+ def port_data(mtok = nil)
112
+ mtok ||= @current_message_origin
113
+ @port_data[mtok] ||= HashWithIndifferentAccess.new
114
+ end
115
+
116
+ def origin_port
117
+ @current_message_origin
118
+ end
119
+
120
+ def origin_tags
121
+ port_data[:tags] || []
122
+ end
123
+
124
+ def origin_tagged?(tag)
125
+ origin_tags.include?(tag)
126
+ end
127
+
128
+ def tag_origin!(*tags)
129
+ port_data[:tags] ||= Set.new
130
+ port_data[:tags].merge(tags.flatten.map(&:to_sym))
131
+ end
132
+
133
+ def handle_received_message(message)
134
+ case message['type']
135
+ when 'port_connected'
136
+ mdata = port_data
137
+ mdata[:last_seen_at] = Time.now
138
+ touch_port_activity
139
+
140
+ mdata.merge! ::ActiveJob::Arguments.deserialize(message['extra'])[0] if message['extra'].present?
141
+ tag_origin!(message['tags'])
142
+
143
+ if self.class.inbound_tag_policy.pass?(message['type'].to_sym, origin_tags)
144
+ run_callbacks :port_connected do
145
+ on_port_connected
146
+ end
147
+ else
148
+ @port_data.delete(origin_port)
149
+ end
150
+ when 'port_disconnected'
151
+ if self.class.inbound_tag_policy.pass?(message['type'].to_sym, origin_tags)
152
+ run_callbacks :port_disconnected do
153
+ on_port_disconnected
154
+ end
155
+ end
156
+ @port_data.delete(origin_port)
157
+ when 'port_ping'
158
+ touch_port_activity
159
+ else
160
+ super
161
+ end
162
+ end
163
+
164
+ def check_port_inactivity
165
+ return unless @port_data
166
+
167
+ threshold = PORT_TIMEOUT.ago
168
+ @port_data.each do |mtok, data|
169
+ next unless data[:last_seen_at] && data[:last_seen_at] < threshold
170
+
171
+ run_callbacks :port_disconnected do
172
+ on_port_disconnected
173
+ end
174
+ @port_data.delete(mtok)
175
+ end
176
+ end
177
+
178
+ class TagPolicy
179
+ ALIASES = {}
180
+
181
+ def self.define_tag_alias(key, implies)
182
+ Array(implies).each do |i|
183
+ ALIASES[i] ||= Set.new
184
+ ALIASES[i] << key
185
+ end
186
+ end
187
+
188
+ define_tag_alias :connect, [:port_connected, :port_ping, :port_disconnected]
189
+
190
+ def initialize()
191
+ @policy_rules = []
192
+ @fallback_rule = { action: :allow, priority: -100, tag: :*, methods: :* }
193
+ append_rule(@fallback_rule)
194
+ end
195
+
196
+ def pass?(method, user_tags)
197
+ user_tags = Set.new(Array(user_tags).map(&:to_sym))
198
+ find_rules_for(method).each do |r|
199
+ if r[:tag] == :* || user_tags.include?(r[:tag])
200
+ return r[:action] == :allow
201
+ end
202
+ end
203
+
204
+ false
205
+ end
206
+
207
+ def evaluate(priority: 10, &blk)
208
+ @fallback_rule[:action] = :deny if priority > 0
209
+
210
+ p = @rule_priority
211
+ @rule_priority = priority
212
+ instance_exec(&blk)
213
+ ensure
214
+ @rule_priority = p
215
+ end
216
+
217
+ protected
218
+
219
+ def allow(tag = :*, methods = :*)
220
+ append_rule({
221
+ action: :allow,
222
+ tag: tag,
223
+ methods: methods,
224
+ })
225
+ end
226
+
227
+ def deny(tag = :*, methods = :*)
228
+ append_rule({
229
+ action: :deny,
230
+ tag: tag,
231
+ methods: methods,
232
+ })
233
+ end
234
+
235
+ private
236
+
237
+ def append_rule(rule)
238
+ rule[:methods] = Set.new(Array(rule[:methods]).map(&:to_sym))
239
+ rule[:priority] ||= @rule_priority || 10
240
+ @policy_rules << rule
241
+ end
242
+
243
+ def find_rules_for(method)
244
+ rules ||= select_ruleset(@policy_rules, method).presence
245
+ rules ||= []
246
+
247
+ rules.sort_by! { |r| -r[:priority] }
248
+
249
+ rules
250
+ end
251
+
252
+ def select_ruleset(set, method)
253
+ all_aliases = Set.new
254
+ all_aliases << method
255
+
256
+ bfs(method) do |m|
257
+ all_aliases.merge(ALIASES[m] || [])
258
+ ALIASES[m]
259
+ end
260
+
261
+ all_aliases << :*
262
+
263
+ rules = set.select { |r| (Set.new(r[:methods]) & all_aliases).count > 0 }.sort_by { |r| -r[:priority] }
264
+
265
+ rules
266
+ end
267
+
268
+ def bfs(base, children = nil)
269
+ q = [base]
270
+ seen = Set.new
271
+ until q.empty?
272
+ n = q.shift
273
+ next if seen.include?(n)
274
+ seen << n
275
+
276
+ catch :end_branch do
277
+ value = catch :found do
278
+ result = yield n
279
+ next_set = children ? children.call(n) : Array(result)
280
+ (next_set || []).each do |c|
281
+ q << c unless seen.include?(c)
282
+ end
283
+ throw :end_branch
284
+ end
285
+
286
+ return value || n
287
+ end
288
+ end
289
+
290
+ nil
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,40 @@
1
+ module CableRoom
2
+ module Room
3
+ thread_mattr_accessor :current_port_scope
4
+
5
+ module PortScoping
6
+ extend ActiveSupport::Concern
7
+
8
+ def without_port_scope(&blk)
9
+ prev = Room.current_port_scope
10
+ Room.current_port_scope = nil
11
+ yield
12
+ ensure
13
+ Room.current_port_scope = prev
14
+ end
15
+
16
+ # Wrap the passed block with a specific messaging scope
17
+ def with_port_scope(**kwargs, &blk)
18
+ prev = Room.current_port_scope
19
+ Room.current_port_scope = prev ? prev.merge(kwargs) : kwargs
20
+ yield
21
+ ensure
22
+ Room.current_port_scope = prev
23
+ end
24
+
25
+ # Similar to with_port_scope, but won't call the block if no ports match
26
+ def with_port_scope!(**kwargs, &blk)
27
+ if kwargs.empty?
28
+ catch :abort do
29
+ remaining = _apply_port_scope(**Room.current_port_scope || {})
30
+ blk.call(**remaining)
31
+ end
32
+ else
33
+ with_port_scope(**kwargs) do
34
+ with_port_scope!(&blk)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,65 +3,104 @@ module CableRoom
3
3
  module UserManagement
4
4
  extend ActiveSupport::Concern
5
5
 
6
- def initialize(...)
7
- @_user_state_map = {}
8
- @_user_map_mutex = Monitor.new
9
- super
6
+ class_methods do
7
+ def on_user_joined(...)
8
+ set_callback(:user_joined, :before, ...)
9
+ end
10
+
11
+ def on_user_left(...)
12
+ set_callback(:user_left, :after, ...)
13
+ end
10
14
  end
11
15
 
12
- def on_user_left(user); end
16
+ included do
17
+ set_callback(:port_connected, :before, :_handle_user_setup_during_connection)
18
+ set_callback(:port_disconnected, :after, :_handle_user_cleanup_after_disconnection)
13
19
 
14
- def on_member_left(mtok)
15
- _user_state_transaction do |usm, u|
16
- next unless usm.present?
17
- usm[:member_tokens].delete(mtok)
18
- if usm[:member_tokens].empty?
19
- @_user_state_map.delete(u)
20
- on_user_left(u)
21
- end
20
+ define_callbacks :user_joined, :user_left
21
+
22
+ inbound_tag_policy(priority: -10) do
23
+ allow :*, :join
22
24
  end
23
25
 
26
+ PortManagement::TagPolicy.define_tag_alias :join, [:connect, :user_joined, :user_left]
27
+ end
28
+
29
+ def initialize(...)
30
+ @_user_state_map = {}
31
+ @_user_map_mutex = Monitor.new
24
32
  super
25
33
  end
26
34
 
27
- def on_user_joined(user); end
35
+ def on_user_left; end
36
+
37
+ def on_user_joined; end
28
38
 
29
- def on_member_joined(mtok)
39
+ protected
40
+
41
+ def _handle_user_setup_during_connection
30
42
  _user_state_transaction do |usm, u|
31
43
  if usm.nil?
32
44
  usm = @_user_state_map[u] = {
33
- member_tokens: Set.new,
45
+ port_tokens: Set.new,
34
46
  }
35
47
  is_new = true
36
48
  end
37
- usm[:member_tokens] << mtok
38
- on_user_joined(u) if is_new
49
+ usm[:port_tokens] << origin_port
50
+ if is_new
51
+ run_callbacks :user_joined do
52
+ on_user_joined
53
+ end
54
+ end
39
55
  end
56
+ end
40
57
 
41
- super
58
+ def _handle_user_cleanup_after_disconnection
59
+ _user_state_transaction do |usm, u|
60
+ next unless usm.present?
61
+ usm[:port_tokens].delete(origin_port)
62
+ if usm[:port_tokens].empty?
63
+ run_callbacks :user_left do
64
+ on_user_left
65
+ end
66
+ @_user_state_map.delete(u)
67
+ end
68
+ end
42
69
  end
43
70
 
44
71
  def connected_users
45
- _user_state_map.keys
72
+ @_user_state_map.keys
46
73
  end
47
74
 
48
- def transmit_to_user(msg, user: nil)
49
- raise "No user specified" if user.nil? && current_user.nil?
50
- user ||= current_user
75
+ def origin_user
76
+ port_data[:as]
77
+ end
51
78
 
52
- _user_state_map[user][:member_tokens].each do |mtok|
53
- ports[mtok] << msg
79
+ def all_user_tags(user = nil)
80
+ user ||= origin_user
81
+ raw_ports = @_user_state_map[user]&.[](:port_tokens) || []
82
+ tags = Set.new
83
+ raw_ports.each do |ptok|
84
+ tags.merge(port_data(ptok)&.[](:tags) || [])
54
85
  end
86
+ tags
55
87
  end
56
88
 
57
- def current_user
58
- member_data[:as]
89
+ def _apply_port_scope(user: nil, tag: nil, **kwargs)
90
+ if user && tag
91
+ user_tags = all_user_tags(user) || []
92
+ throw :abort unless user_tags.include?(tag)
93
+ end
94
+
95
+ super(tag: user || tag, **kwargs)
59
96
  end
60
97
 
61
98
  private
62
99
 
63
- def _user_state_transaction(user = current_user)
100
+ def _user_state_transaction(user = origin_user)
64
101
  return unless user.present?
102
+ return unless policy_allows?(:join)
103
+
65
104
  @_user_map_mutex.synchronize do
66
105
  usm = @_user_state_map[user]
67
106
  yield usm, user
@@ -5,16 +5,18 @@ module CableRoom
5
5
  eager_autoload do
6
6
  autoload :Base
7
7
 
8
- # autoload :Ports
9
8
  autoload :Callbacks
10
9
  autoload :Threading
11
10
  autoload :ChannelAdapter
12
- autoload :InputHandling
11
+
13
12
  autoload :Lifecycle
14
13
  autoload :Reaping
14
+ autoload :InputHandling
15
15
 
16
- autoload :MemberManagement
16
+ autoload :PortScoping
17
+ autoload :PortManagement
17
18
  autoload :UserManagement
19
+ autoload :Broadcasting
18
20
  end
19
21
  end
20
22
  end