celluloid_pubsub 1.1.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
20
- MINOR = 1
27
+ # :nocov:
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