celluloid_pubsub 1.0.2 → 2.0.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.
@@ -1,17 +1,22 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
+
3
4
  require 'reel/spy'
4
5
  Reel::Spy::Colors.class_eval do
5
6
  alias_method :original_colorize, :colorize
6
7
 
7
- def colorize(_n, str)
8
+ # :nocov:
9
+ def colorize(_var, str)
8
10
  force_utf8_encoding(str)
9
11
  end
12
+ # :nocov:
10
13
 
11
14
  # Returns utf8 encoding of the msg
12
15
  # @param [String] msg
13
16
  # @return [String] ReturnsReturns utf8 encoding of the msg
17
+ # :nocov:
14
18
  def force_utf8_encoding(msg)
15
19
  msg.respond_to?(:force_encoding) && msg.encoding.name != 'UTF-8' ? msg.force_encoding('UTF-8') : msg
16
20
  end
21
+ # :nocov:
17
22
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
+
3
4
  require_relative './registry'
4
5
  require_relative './helper'
5
6
  module CelluloidPubsub
@@ -18,7 +19,7 @@ module CelluloidPubsub
18
19
  include CelluloidPubsub::BaseActor
19
20
 
20
21
  # available actions that can be delegated
21
- AVAILABLE_ACTIONS = %w(unsubscribe_clients unsubscribe subscribe publish unsubscribe_all).freeze
22
+ AVAILABLE_ACTIONS = %w[unsubscribe_clients unsubscribe subscribe publish unsubscribe_all].freeze
22
23
 
23
24
  # The websocket connection received from the server
24
25
  # @return [Reel::WebSocket] websocket connection
@@ -32,7 +33,13 @@ module CelluloidPubsub
32
33
  # @return [Array] array of channels to which the current reactor has subscribed to
33
34
  attr_accessor :channels
34
35
 
36
+ # The same options passed to the server are available on the reactor too
37
+ # @return [Hash] Hash with all the options passed to the server
38
+ attr_reader :options
39
+
35
40
  finalizer :shutdown
41
+ trap_exit :actor_died
42
+
36
43
  # rececives a new socket connection from the server
37
44
  # and listens for messages
38
45
  #
@@ -42,11 +49,68 @@ module CelluloidPubsub
42
49
  #
43
50
  # @api public
44
51
  def work(websocket, server)
52
+ initialize_data(websocket, server)
53
+ server.reactors << Actor.current
54
+ async.run
55
+ end
56
+
57
+ # initializes the actor
58
+ #
59
+ # @param [Reel::WebSocket] websocket
60
+ # @param [CelluloidPubsub::WebServer] server
61
+ #
62
+ # @return [Celluloid::Actor] returns the actor
63
+ #
64
+ # @api public
65
+ def initialize_data(websocket, server)
66
+ @websocket = websocket
45
67
  @server = server
68
+ @options = @server.server_options
46
69
  @channels = []
47
- @websocket = websocket
48
- log_debug "#{self.class} Streaming changes for #{websocket.url}"
49
- async.run
70
+ @shutting_down = false
71
+ setup_celluloid_logger
72
+ log_debug "#{self.class} Streaming changes for #{websocket.url} #{websocket.class.name}"
73
+ yield(websocket, server) if block_given?
74
+ cell_actor
75
+ end
76
+
77
+ # the method will return the file path of the log file where debug messages will be printed
78
+ #
79
+ #
80
+ # @return [String] returns the file path of the log file where debug messages will be printed
81
+ #
82
+ # @api public
83
+ def log_file_path
84
+ @log_file_path ||= options.fetch('log_file_path', nil)
85
+ end
86
+
87
+ # the method will return the log level of the logger
88
+ #
89
+ # @return [Integer, nil] return the log level used by the logger ( default is 1 - info)
90
+ #
91
+ # @api public
92
+ def log_level
93
+ @log_level ||= options['log_level'] || ::Logger::Severity::INFO
94
+ end
95
+
96
+ # the method will return options needed when configuring an adapter
97
+ # @see celluloid_pubsub_redis_adapter for more information
98
+ #
99
+ # @return [Hash] returns options needed by the adapter
100
+ #
101
+ # @api public
102
+ def adapter_options
103
+ @adapter_options ||= options['adapter_options'] || {}
104
+ end
105
+
106
+ # the method will return true if the actor is shutting down
107
+ #
108
+ #
109
+ # @return [Boolean] returns true if the actor is shutting down
110
+ #
111
+ # @api public
112
+ def shutting_down?
113
+ @shutting_down == true
50
114
  end
51
115
 
52
116
  # the method will return true if debug is enabled
@@ -56,7 +120,8 @@ module CelluloidPubsub
56
120
  #
57
121
  # @api public
58
122
  def debug_enabled?
59
- !@server.dead? && @server.debug_enabled?
123
+ @debug_enabled = options.fetch('enable_debug', false)
124
+ @debug_enabled == true
60
125
  end
61
126
 
62
127
  # reads from the socket the message
@@ -70,11 +135,12 @@ module CelluloidPubsub
70
135
  # :nocov:
71
136
  def run
72
137
  loop do
73
- break if Actor.current.dead? || @websocket.closed? || @server.dead?
138
+ break if shutting_down? || actor_dead?(Actor.current) || @websocket.closed? || actor_dead?(@server)
74
139
  message = try_read_websocket
75
140
  handle_websocket_message(message) if message.present?
76
141
  end
77
142
  end
143
+ # :nocov:
78
144
 
79
145
  # will try to read the message from the websocket
80
146
  # and if it fails will log the exception if debug is enabled
@@ -83,10 +149,9 @@ module CelluloidPubsub
83
149
  #
84
150
  # @api public
85
151
  #
86
- # :nocov:
87
152
  def try_read_websocket
88
153
  @websocket.closed? ? nil : @websocket.read
89
- rescue
154
+ rescue StandardError
90
155
  nil
91
156
  end
92
157
 
@@ -108,9 +173,10 @@ module CelluloidPubsub
108
173
  #
109
174
  # @api public
110
175
  def parse_json_data(message)
176
+ log_debug "#{reactor_class} received #{message}"
111
177
  JSON.parse(message)
112
- rescue => exception
113
- log_debug "#{reactor_class} could not parse #{message} because of #{exception.inspect}"
178
+ rescue StandardError => e
179
+ log_debug "#{reactor_class} could not parse #{message} because of #{e.inspect}"
114
180
  message
115
181
  end
116
182
 
@@ -147,7 +213,7 @@ module CelluloidPubsub
147
213
  #
148
214
  # @api public
149
215
  def handle_parsed_websocket_message(json_data)
150
- data = json_data.is_a?(Hash) ? json_data.stringify_keys : {}
216
+ data = json_data.is_a?(Hash) ? json_data.stringify_keys : {}
151
217
  if CelluloidPubsub::Reactor::AVAILABLE_ACTIONS.include?(data['client_action'].to_s)
152
218
  log_debug "#{self.class} finds actions for #{json_data}"
153
219
  delegate_action(data) if data['client_action'].present?
@@ -183,7 +249,7 @@ module CelluloidPubsub
183
249
  end
184
250
 
185
251
  # the method will delegate the message to the server in an asyncronous way by sending the current actor and the message
186
- # @see {CelluloidPubsub::WebServer#handle_dispatched_message}
252
+ # @see CelluloidPubsub::WebServer#handle_dispatched_message
187
253
  #
188
254
  # @param [Hash] json_data
189
255
  #
@@ -262,7 +328,8 @@ module CelluloidPubsub
262
328
  #
263
329
  # @api public
264
330
  def shutdown
265
- debug "#{self.class} tries to 'shudown'"
331
+ @shutting_down = true
332
+ log_debug "#{self.class} tries to 'shutdown'"
266
333
  @websocket.close if @websocket.present? && !@websocket.closed?
267
334
  terminate
268
335
  end
@@ -285,6 +352,42 @@ module CelluloidPubsub
285
352
  @websocket << message.merge('client_action' => 'successful_subscription', 'channel' => channel).to_json if @server.adapter == CelluloidPubsub::WebServer::CLASSIC_ADAPTER
286
353
  end
287
354
 
355
+ # this method will write to the socket all messages that were published
356
+ # to that channel before the actor subscribed
357
+ #
358
+ # @param [String] channel
359
+ # @return [void]
360
+ #
361
+ # @api public
362
+ def send_unpublished(channel)
363
+ return if (messages = unpublished_messages(channel)).blank?
364
+ messages.each do |msg|
365
+ @websocket << msg.to_json
366
+ end
367
+ end
368
+
369
+ # the method clears all the messages left unpublished in a channel
370
+ #
371
+ # @param [String] channel
372
+ #
373
+ # @return [void]
374
+ #
375
+ # @api public
376
+ def clear_unpublished_messages(channel)
377
+ CelluloidPubsub::Registry.messages[channel] = []
378
+ end
379
+
380
+ # the method will return a list of all unpublished messages in a channel
381
+ #
382
+ # @param [String] channel
383
+ #
384
+ # @return [Array] the list of messages that were not published
385
+ #
386
+ # @api public
387
+ def unpublished_messages(channel)
388
+ (messages = CelluloidPubsub::Registry.messages[channel]).present? ? messages : []
389
+ end
390
+
288
391
  # this method will return a list of all subscribers to a particular channel or a empty array
289
392
  #
290
393
  #
@@ -326,9 +429,9 @@ module CelluloidPubsub
326
429
  def publish(current_topic, json_data)
327
430
  message = json_data['data'].to_json
328
431
  return if current_topic.blank? || message.blank?
329
- server_pusblish_event(current_topic, message)
330
- rescue => exception
331
- log_debug("could not publish message #{message} into topic #{current_topic} because of #{exception.inspect}")
432
+ server_publish_event(current_topic, message)
433
+ rescue StandardError => e
434
+ log_debug("could not publish message #{message} into topic #{current_topic} because of #{e.inspect}")
332
435
  end
333
436
 
334
437
  # the method will publish to all subsribers of a channel a message
@@ -339,18 +442,34 @@ module CelluloidPubsub
339
442
  # @return [void]
340
443
  #
341
444
  # @api public
342
- def server_pusblish_event(current_topic, message)
343
- @server.mutex.synchronize do
344
- (@server.subscribers[current_topic].dup || []).pmap do |hash|
445
+ def server_publish_event(current_topic, message)
446
+ if (subscribers = @server.subscribers[current_topic]).present?
447
+ subscribers.dup.pmap do |hash|
345
448
  hash[:reactor].websocket << message
346
449
  end
450
+ else
451
+ save_unpublished_message(current_topic, message)
452
+ end
453
+ end
454
+
455
+ # the method save the message for a specific channel if there are no subscribers
456
+ #
457
+ # @param [String] current_topic
458
+ # @param [#to_s] message
459
+ #
460
+ # @return [void]
461
+ #
462
+ # @api public
463
+ def save_unpublished_message(current_topic, message)
464
+ @server.timers_mutex.synchronize do
465
+ (CelluloidPubsub::Registry.messages[current_topic] ||= []) << message
347
466
  end
348
467
  end
349
468
 
350
- # unsubscribes all actors from all channels and terminates the curent actor
469
+ # unsubscribes all actors from all channels and terminates the current actor
351
470
  #
352
471
  # @param [String] _channel NOT USED - needed to maintain compatibility with the other methods
353
- # @param [Object] _json_data NOT USED - needed to maintain compatibility with the other methods
472
+ # @param [Object] json_data NOT USED - needed to maintain compatibility with the other methods
354
473
  #
355
474
  # @return [void]
356
475
  #
@@ -383,12 +502,25 @@ module CelluloidPubsub
383
502
  # @api public
384
503
  def server_kill_reactors(channel)
385
504
  @server.mutex.synchronize do
386
- (@server.subscribers[channel].dup || []).pmap do |hash|
505
+ (@server.subscribers[channel] || []).dup.pmap do |hash|
387
506
  reactor = hash[:reactor]
388
507
  reactor.websocket.close
389
508
  Celluloid::Actor.kill(reactor)
390
509
  end
391
510
  end
392
511
  end
512
+
513
+ # method called when the actor is exiting
514
+ #
515
+ # @param [actor] actor - the current actor
516
+ # @param [Hash] reason - the reason it crashed
517
+ #
518
+ # @return [void]
519
+ #
520
+ # @api public
521
+ def actor_died(actor, reason)
522
+ @shutting_down = true
523
+ log_debug "Oh no! #{actor.inspect} has died because of a #{reason.class}"
524
+ end
393
525
  end
394
526
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
+
3
4
  module CelluloidPubsub
4
5
  # class used to register new channels and save them in memory
5
6
  # @attr channels
@@ -8,8 +9,33 @@ module CelluloidPubsub
8
9
  class << self
9
10
  # The channels that the server can handle
10
11
  # @return [Array] array of channels to which actors have subscribed to
11
- attr_accessor :channels
12
+ attr_writer :channels
13
+
14
+ # Messages that are published before any clients being subscribed to those channels
15
+ # will be kept here until a client subscribes to that channel
16
+ # @return [Hash] key-value pairs containing the channel and the messages that were published
17
+ attr_writer :messages
18
+
19
+ # holds a list of all messages sent by clients that were not published
20
+ # to a channel because there were no subscribers at that time
21
+ #
22
+ # The keys are the channel names and the values are arrays of messages
23
+ #
24
+ # @return [Hash<String, Array<Hash>>]
25
+ #
26
+ # @api private
27
+ def messages
28
+ @messages ||= {}
29
+ end
30
+
31
+ # holds a list of all known channels
32
+ #
33
+ # @return [Array<String>]
34
+ #
35
+ # @api private
36
+ def channels
37
+ @channels ||= []
38
+ end
12
39
  end
13
- @channels = []
14
40
  end
15
41
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
+
3
4
  # Returns the version of the gem as a <tt>Gem::Version</tt>
4
5
  module CelluloidPubsub
5
6
  # it prints the gem version as a string
@@ -7,23 +8,40 @@ module CelluloidPubsub
7
8
  # @return [String]
8
9
  #
9
10
  # @api public
11
+ # :nocov:
10
12
  def self.gem_version
11
13
  Gem::Version.new VERSION::STRING
12
14
  end
15
+ # :nocov:
13
16
 
14
17
  # module used to generate the version string
15
18
  # provides a easy way of getting the major, minor and tiny
19
+ # :nocov:
16
20
  module VERSION
17
21
  # major release version
18
- MAJOR = 1
22
+ # :nocov:
23
+ MAJOR = 2
24
+ # :nocov:
25
+
19
26
  # minor release version
27
+ # :nocov:
20
28
  MINOR = 0
29
+ # :nocov:
30
+
21
31
  # tiny release version
22
- TINY = 2
32
+ # :nocov:
33
+ TINY = 0
34
+ # :nocov:
35
+
23
36
  # prelease version ( set this only if it is a prelease)
37
+ # :nocov:
24
38
  PRE = nil
39
+ # :nocov:
25
40
 
26
41
  # generates the version string
42
+ # :nocov:
27
43
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
44
+ # :nocov:
28
45
  end
46
+ # :nocov:
29
47
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: utf-8
2
2
  # frozen_string_literal: true
3
+
3
4
  require_relative './reactor'
4
5
  require_relative './helper'
5
6
  module CelluloidPubsub
@@ -17,6 +18,8 @@ module CelluloidPubsub
17
18
  # @return [Hash] The hostname on which the webserver runs on
18
19
  # @attr mutex
19
20
  # @return [Mutex] The mutex that will synchronize actions on subscribers
21
+ # @attr timers_mutex
22
+ # @return [Mutex] The mutex that will synchronize actions on registry messages
20
23
  class WebServer < Reel::Server::HTTP
21
24
  include CelluloidPubsub::BaseActor
22
25
 
@@ -27,8 +30,12 @@ module CelluloidPubsub
27
30
  # The name of the default adapter
28
31
  CLASSIC_ADAPTER = 'classic'
29
32
 
30
- attr_accessor :server_options, :subscribers, :mutex
33
+ attr_accessor :server_options, :subscribers, :mutex, :timers_mutex
34
+
35
+ attr_reader :reactors
36
+
31
37
  finalizer :shutdown
38
+ trap_exit :actor_died
32
39
  # receives a list of options that are used to configure the webserver
33
40
  #
34
41
  # @param [Hash] options the options that can be used to connect to webser and send additional data
@@ -47,8 +54,11 @@ module CelluloidPubsub
47
54
  @server_options = parse_options(options)
48
55
  @subscribers = {}
49
56
  @mutex = Mutex.new
57
+ @timers_mutex = Mutex.new
58
+ @shutting_down = false
59
+ @reactors = []
50
60
  setup_celluloid_logger
51
- debug "CelluloidPubsub::WebServer example starting on #{hostname}:#{port}"
61
+ log_debug "CelluloidPubsub::WebServer example starting on #{hostname}:#{port}"
52
62
  super(hostname, port, { spy: spy, backlog: backlog }, &method(:on_connection))
53
63
  end
54
64
 
@@ -80,7 +90,7 @@ module CelluloidPubsub
80
90
  # @return [Hash] return the socket families available as keys in the hash
81
91
  #
82
92
  # @api public
83
- # rubocop:disable ClassVars
93
+ # rubocop:disable Style/ClassVars
84
94
  def self.socket_families
85
95
  @@socket_families ||= Hash[*socket_infos.map { |af, *_| af }.uniq.zip([]).flatten]
86
96
  end
@@ -99,7 +109,7 @@ module CelluloidPubsub
99
109
  port
100
110
  end
101
111
  end
102
- # rubocop:enable ClassVars
112
+ # rubocop:enable Style/ClassVars
103
113
 
104
114
  # this method is overriden from the Reel::Server::HTTP in order to set the spy to the celluloid logger
105
115
  # before the connection is accepted.
@@ -107,9 +117,24 @@ module CelluloidPubsub
107
117
  # @api public
108
118
  def run
109
119
  @spy = Celluloid.logger if spy
120
+ async.bind_timers
110
121
  loop { async.handle_connection @server.accept }
111
122
  end
112
123
 
124
+ # the method will run indefinitely and will check if are there
125
+ # any unpublished messages that can be send to new subscribers
126
+ #
127
+ # @param [Boolean] run FLag to control if the server should try checking
128
+ # if there are any unpublished messages that need to be sent
129
+ #
130
+ # @return [void]
131
+ #
132
+ # @api public
133
+ def bind_timers(run = false)
134
+ try_sending_unpublished if run
135
+ after(0.1) { bind_timers(true) }
136
+ end
137
+
113
138
  # the method will return true if redis can be used otherwise false
114
139
  #
115
140
  #
@@ -121,6 +146,16 @@ module CelluloidPubsub
121
146
  @adapter.present? ? @adapter : CelluloidPubsub::WebServer::CLASSIC_ADAPTER
122
147
  end
123
148
 
149
+ # the method will return true if the actor is shutting down
150
+ #
151
+ #
152
+ # @return [Boolean] returns true if the actor is shutting down
153
+ #
154
+ # @api public
155
+ def shutting_down?
156
+ @shutting_down == true
157
+ end
158
+
124
159
  # the method will return true if debug is enabled otherwise false
125
160
  #
126
161
  #
@@ -139,7 +174,11 @@ module CelluloidPubsub
139
174
  #
140
175
  # @api public
141
176
  def shutdown
142
- debug "#{self.class} tries to 'shudown'"
177
+ @shutting_down = true
178
+ log_debug "#{self.class} tries to 'shutdown'"
179
+ reactors.each do |reactor|
180
+ reactor.terminate unless actor_dead?(reactor)
181
+ end
143
182
  terminate
144
183
  end
145
184
 
@@ -153,6 +192,15 @@ module CelluloidPubsub
153
192
  @log_file_path = @server_options.fetch('log_file_path', nil)
154
193
  end
155
194
 
195
+ # the method will return the log level of the logger
196
+ #
197
+ # @return [Integer, nil] return the log level used by the logger ( default is 1 - info)
198
+ #
199
+ # @api public
200
+ def log_level
201
+ @log_level ||= @server_options['log_level'] || ::Logger::Severity::INFO
202
+ end
203
+
156
204
  # the method will return the hostname on which the server is running on
157
205
  #
158
206
  #
@@ -219,7 +267,7 @@ module CelluloidPubsub
219
267
  def on_connection(connection)
220
268
  while request = connection.request
221
269
  if request.websocket?
222
- log_debug "#{self.class} Received a WebSocket connection"
270
+ log_debug "#{self.class} Received a WebSocket connection #{request.websocket.url}"
223
271
 
224
272
  # We're going to hand off this connection to another actor (Writer/Reader)
225
273
  # However, initially Reel::Connections are "attached" to the
@@ -294,6 +342,23 @@ module CelluloidPubsub
294
342
  end
295
343
  end
296
344
 
345
+ # this method will know when a client has successfully registered
346
+ # and will write to the socket all messages that were published
347
+ # to that channel before the actor subscribed
348
+ #
349
+ # @return [void]
350
+ #
351
+ # @api publicsCelluloidPubsub::Registry.messages
352
+ def try_sending_unpublished
353
+ CelluloidPubsub::Registry.messages.each_key do |channel|
354
+ next if (clients = subscribers[channel]).blank?
355
+ clients.dup.pmap do |hash|
356
+ hash[:reactor].send_unpublished(channel)
357
+ end
358
+ clients.last[:reactor].clear_unpublished_messages(channel)
359
+ end
360
+ end
361
+
297
362
  # If the message can be parsed into a Hash it will respond to the reactor's websocket connection with the same message in JSON format
298
363
  # otherwise will try send the message how it is and escaped into JSON format
299
364
  #
@@ -309,5 +374,18 @@ module CelluloidPubsub
309
374
  final_data = message.present? && message.is_a?(Hash) ? message.to_json : data.to_json
310
375
  reactor.websocket << final_data
311
376
  end
377
+
378
+ # method called when the actor is exiting
379
+ #
380
+ # @param [actor] actor - the current actor
381
+ # @param [Hash] reason - the reason it crashed
382
+ #
383
+ # @return [void]
384
+ #
385
+ # @api public
386
+ def actor_died(actor, reason)
387
+ @shutting_down = true
388
+ log_debug "Oh no! #{actor.inspect} has died because of a #{reason.class}"
389
+ end
312
390
  end
313
391
  end