carnivore 0.2.0 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,20 +3,24 @@ require 'celluloid'
3
3
  require 'carnivore'
4
4
 
5
5
  module Carnivore
6
+ # Message source
7
+ # @abstract
6
8
  class Source
7
9
 
8
10
  autoload :SourceContainer, 'carnivore/source_container'
9
11
 
10
12
  class << self
11
13
 
12
- # args:: Hash
13
- # :type -> Source type
14
- # :args -> arguments for `Source` instance
15
- # Builds a source container of `:type`
14
+ # Builds a source container
15
+ #
16
+ # @param args [Hash] source configuration
17
+ # @option args [String, Symbol] :type type of source to build
18
+ # @option args [Hash] :args configuration hash for source initialization
19
+ # @return [SourceContainer]
16
20
  def build(args={})
17
21
  [:args, :type].each do |key|
18
22
  unless(args.has_key?(key))
19
- raise ArgumentError.new "Missing required parameter `:#{key}`"
23
+ abort ArgumentError.new "Missing required parameter `:#{key}`"
20
24
  end
21
25
  end
22
26
  require Source.require_path(args[:type]) || "carnivore/source/#{args[:type]}"
@@ -28,46 +32,56 @@ module Carnivore
28
32
  inst
29
33
  end
30
34
 
31
- # type:: Symbol of type of source
32
- # require_path:: Path to feed to `require`
33
- # Registers a source
35
+ # Register a new source type
36
+ #
37
+ # @param type [Symbol] name of source type
38
+ # @param require_path [String] path to require when requested
39
+ # @return [TrueClass]
34
40
  def provide(type, require_path)
35
- @source_klass ||= {}
36
- @source_klass[type.to_sym] = require_path
41
+ @source_klass ||= Smash.new
42
+ @source_klass[type] = require_path
37
43
  true
38
44
  end
39
45
 
40
- # type: Symbol of source type
41
- # Returns register path for given type of source
46
+ # Registered path for given source type
47
+ #
48
+ # @param type [String, Symbol] name of source type
49
+ # @return [String, NilClass]
42
50
  def require_path(type)
43
- @source_klass ||= {}
44
- @source_klass[type.to_sym]
51
+ @source_klass ||= Smash.new
52
+ @source_klass[type]
45
53
  end
46
54
 
47
- # name:: Name of source
48
- # inst:: SourceContainer
49
55
  # Register the container
56
+ #
57
+ # @param name [String, Symbol] name of source
58
+ # @param inst [SourceContainer]
59
+ # @return [TrueClass]
50
60
  def register(name, inst)
51
- @sources ||= {}
52
- @sources[name.to_sym] = inst
61
+ @sources ||= Smash.new
62
+ @sources[name] = inst
53
63
  true
54
64
  end
55
65
 
56
- # name:: Name of registered source
57
- # Return source container
66
+ # Source container with given name
67
+ #
68
+ # @param name [String, Symbol] name of source
69
+ # @return [SourceContainer]
58
70
  def source(name)
59
71
  if(@sources && @sources[name.to_sym])
60
72
  @sources[name.to_sym]
61
73
  else
62
- raise KeyError.new("Requested named source is not registered: #{name}")
74
+ Celluloid.logger.error "Source lookup failed (name: #{name})"
75
+ abort KeyError.new("Requested named source is not registered: #{name}")
63
76
  end
64
77
  end
65
78
 
66
- # Registered containers
79
+ # @return [Array<SourceContainer>] registered source containers
67
80
  def sources
68
81
  @sources ? @sources.values : []
69
82
  end
70
83
 
84
+ # Reset communication methods within class
71
85
  def reset_comms!
72
86
  self.class_eval do
73
87
  unless(method_defined?(:reset_communications?))
@@ -84,25 +98,56 @@ module Carnivore
84
98
 
85
99
  include Celluloid
86
100
  include Utils::Logging
101
+ # @!parse include Carnivore::Utils::Logging
87
102
 
103
+ finalizer :teardown_cleanup
104
+
105
+ # @return [String, Symbol] name of source
88
106
  attr_reader :name
107
+ # @return [Array<Callback>] registered callbacks
89
108
  attr_reader :callbacks
109
+ # @return [TrueClass, FalseClass] auto confirm received messages
90
110
  attr_reader :auto_confirm
111
+ # @return [TrueClass, FalseClass] start source processing on initialization
91
112
  attr_reader :auto_process
113
+ # @return [TrueClass, FalseClass] message processing control switch
92
114
  attr_reader :run_process
115
+ # @return [Carnivore::Supervisor] supervisor maintaining callback instances
93
116
  attr_reader :callback_supervisor
117
+ # @return [Hash] registry of processed messages
94
118
  attr_reader :message_registry
119
+ # @return [Queue] local loop message queue
95
120
  attr_reader :message_loop
121
+ # @return [Queue] remote message queue
122
+ attr_reader :message_remote
123
+ # @return [TrueClass, FalseClass] currently processing a message
96
124
  attr_reader :processing
97
125
 
126
+ # Create new Source
127
+ #
128
+ # @param args [Hash]
129
+ # @option args [String, Symbol] :name name of source
130
+ # @option args [TrueClass, FalseClass] :auto_process start processing on initialization
131
+ # @option args [TrueClass, FalseClass] :auto_confirm confirm messages automatically on receive
132
+ # @option args [Proc] :orphan_callback execute block when no callbacks are valid for message
133
+ # @option args [TrueClass, FalseClass] :prevent_duplicates setup and use message registry
134
+ # @option args [Array<Callback>] :callbacks callbacks to register on this source
98
135
  def initialize(args={})
136
+ @args = Smash.new(args)
99
137
  @callbacks = []
100
- @message_loop = []
138
+ @message_loop = Queue.new
139
+ @message_remote = Queue.new
101
140
  @callback_names = {}
102
- @auto_process = args.fetch(:auto_process, true)
141
+ @auto_process = !!args.fetch(:auto_process, true)
103
142
  @run_process = true
104
143
  @auto_confirm = !!args[:auto_confirm]
105
144
  @callback_supervisor = Carnivore::Supervisor.create!.last
145
+ if(args[:orphan_callback])
146
+ unless(args[:orphan_callback].is_a?(Proc))
147
+ raise TypeError.new("Expected `Proc` type for `orphan_callback` but received `#{args[:orphan_callback].class}`")
148
+ end
149
+ define_singleton_method(:orphan_callback, &args[:orphan_callback])
150
+ end
106
151
  if(args[:prevent_duplicates])
107
152
  init_registry
108
153
  end
@@ -124,61 +169,70 @@ module Carnivore
124
169
  end
125
170
 
126
171
  # Ensure we cleanup our internal supervisor before bailing out
127
- def terminate
172
+ def teardown_cleanup
173
+ warn 'Termination request received. Tearing down!'
128
174
  callback_supervisor.terminate
129
- super
130
175
  end
131
176
 
132
- # Automatically confirm messages after dispatch
177
+ # @return [TrueClass, FalseClass] automatic message confirmation enabled
133
178
  def auto_confirm?
134
179
  @auto_confirm
135
180
  end
136
181
 
137
- # Return string for inspection
182
+ # @return [String] inspection formatted string
138
183
  def inspect
139
184
  "<#{self.class.name}:#{object_id} @name=#{name} @callbacks=#{Hash[*callbacks.map{|k,v| [k,v.object_id]}.flatten]}>"
140
185
  end
141
186
 
142
- # Return string of instance
187
+ # @return [String] stringified instance
143
188
  def to_s
144
189
  "<#{self.class.name}:#{object_id} @name=#{name}>"
145
190
  end
146
191
 
147
- # args:: Argument hash used to initialize instance
148
- # Setup called during initialization for child sources to override
192
+ # Setup hook for source requiring customized setup
193
+ #
194
+ # @param args [Hash] initialization hash
149
195
  def setup(args={})
150
196
  debug 'No custom setup declared'
151
197
  end
152
198
 
153
- # args:: Argument hash
154
- # Connection method to be overridden in child sources
155
- def connect(args={})
199
+ # Connection hook for sources requiring customized connect
200
+ #
201
+ # @param args [Hash] initialization hash
202
+ def connect
156
203
  debug 'No custom connect declared'
157
204
  end
158
205
 
159
- # args:: number of messages to read
160
- # Returns messages from source
206
+ # Receive messages from source
207
+ # @abstract
208
+ #
209
+ # @param n [Integer] number of messages
210
+ # @return [Object, Array<Object>] payload or array of payloads
161
211
  def receive(n=1)
162
- raise NoMethodError.new('Abstract method not valid for runtime')
212
+ raise NotImplementedError.new('Abstract method not valid for runtime')
163
213
  end
164
214
 
165
- # message:: Payload to transmit
166
- # original_message:: Original `Carnivore::Message`
167
- # args:: Custom arguments
168
- # Transmit message on source
215
+ # Send payload to source
216
+ #
217
+ # @param message [Object] payload
218
+ # @param original_message [Carnviore::Message] original message if reply to extract optional metadata
219
+ # @param args [Hash] optional extra arguments
169
220
  def transmit(message, original_message=nil, args={})
170
- raise NoMethodError.new('Abstract method not valid for runtime')
221
+ raise NotImplemented.new('Abstract method not valid for runtime')
171
222
  end
172
223
 
173
- # message:: Carnivore::Message
174
224
  # Confirm receipt of the message on source
225
+ #
226
+ # @param message [Carnivore::Message]
175
227
  def confirm(message)
176
228
  debug 'No custom confirm declared'
177
229
  end
178
230
 
179
- # callback_name:: Name of callback
180
- # block_or_class:: Carnivore::Callback class or a block
181
231
  # Adds the given callback to the source for message processing
232
+ #
233
+ # @param callback_name [String, Symbol] name of callback
234
+ # @param block_or_class [Carnivore::Callback, Proc]
235
+ # @return [self]
182
236
  def add_callback(callback_name, block_or_class)
183
237
  name = "#{self.name}:#{callback_name}"
184
238
  if(block_or_class.is_a?(Class))
@@ -188,32 +242,36 @@ module Carnivore
188
242
  return self
189
243
  elsif(size == 1)
190
244
  debug "Adding callback class (#{block_or_class}) under supervision. Name: #{callback_name(name)}"
191
- callback_supervisor.supervise_as callback_name(name), block_or_class, name
245
+ callback_supervisor.supervise_as callback_name(name), block_or_class, name, current_actor
192
246
  else
193
247
  debug "Adding callback class (#{block_or_class}) under supervision pool (#{size} workers). Name: #{callback_name(name)}"
194
- callback_supervisor.pool block_or_class, as: callback_name(name), size: size, args: [name]
248
+ callback_supervisor.pool block_or_class, as: callback_name(name), size: size, args: [name, current_actor]
195
249
  end
196
250
  else
197
251
  debug "Adding custom callback class from block (#{block_or_class}) under supervision. Name: #{callback_name(name)}"
198
- callback_supervisor.supervise_as callback_name(name), Callback, name, block_or_class
252
+ callback_supervisor.supervise_as callback_name(name), Callback, name, current_actor, block_or_class
199
253
  end
200
254
  callbacks.push(name).uniq!
201
255
  self
202
256
  end
203
257
 
204
- # name:: Name of callback
205
258
  # Remove the named callback from the source
259
+ #
260
+ # @param name [String, Symbol]
261
+ # @return [self]
206
262
  def remove_callback(name)
207
263
  unless(@callbacks.include?(callback_name(name)))
208
- raise NameError.new("Failed to locate callback named: #{name}")
264
+ abort NameError.new("Failed to locate callback named: #{name}")
209
265
  end
210
266
  actors[callback_name(name)].terminate
211
267
  @callbacks.delete(name)
212
268
  self
213
269
  end
214
270
 
215
- # name:: Name of callback
216
271
  # Returns namespaced name (prefixed with source name and instance id)
272
+ #
273
+ # @param name [String, Symbol] name of callback
274
+ # @return [Carnivore::Callback, NilClass]
217
275
  def callback_name(name)
218
276
  unless(@callback_names[name])
219
277
  @callback_names[name] = [@name, self.object_id, name].join(':').to_sym
@@ -221,22 +279,28 @@ module Carnivore
221
279
  @callback_names[name]
222
280
  end
223
281
 
224
- # msg:: New message received from source
225
- # Returns formatted Carnivore::Message
282
+ # Create new Message from received payload
283
+ #
284
+ # @param msg [Object] received payload
285
+ # @return [Carnivore::Message]
226
286
  def format(msg)
227
287
  actor = Carnivore::Supervisor.supervisor[name]
228
288
  if(actor)
229
289
  Message.new(
230
290
  :message => msg,
231
- :source => actor
291
+ :source => actor.current_actor
232
292
  )
233
293
  else
234
294
  abort "Failed to locate self in registry (#{name})"
235
295
  end
236
296
  end
237
297
 
238
- # m:: Carnivore::Message
239
- # Returns true if message is valid to be processed
298
+ # Validate message is allowed before processing. This is currently
299
+ # only used when the message registry is enabled to prevent
300
+ # duplicate message processing.
301
+ #
302
+ # @param m [Carnivore::Message]
303
+ # @return [TrueClass, FalseClass]
240
304
  def valid_message?(m)
241
305
  if(message_registry)
242
306
  if(message_registry.valid?(m))
@@ -250,66 +314,104 @@ module Carnivore
250
314
  end
251
315
  end
252
316
 
253
- # args:: Arguments
254
- # Start processing messages from source
317
+ # Process incoming messages from this source
318
+ #
319
+ # @param args [Object] list of arguments
320
+ # @return [TrueClass]
255
321
  def process(*args)
256
322
  begin
257
323
  while(run_process && !callbacks.empty?)
258
324
  @processing = true
259
- loop_msgs = future.loop_receive unless loop_msgs
260
- remote_msgs = future.receive unless remote_msgs
261
- if(loop_msgs.ready?)
262
- msgs = loop_msgs.value
263
- loop_msgs = nil
264
- elsif(remote_msgs.ready?)
265
- msgs = remote_msgs.value
266
- remote_msgs = nil
267
- else
268
- msgs = []
325
+ async.receive_messages
326
+ if(message_loop.empty? && message_remote.empty?)
327
+ wait(:messages_available)
269
328
  end
329
+ msgs = []
330
+ msgs.push message_loop.pop unless message_loop.empty?
331
+ msgs.push message_remote.pop unless message_remote.empty?
270
332
  msgs = [msgs].flatten.compact.map do |m|
271
333
  if(valid_message?(m))
272
334
  format(m)
273
335
  end
274
336
  end.compact
275
337
  msgs.each do |msg|
276
- @callbacks.each do |name|
338
+ if(respond_to?(:orphan_callback))
339
+ valid_callbacks = callbacks.find_all do |name|
340
+ callback_supervisor[callback_name(name)].valid?(msg)
341
+ end
342
+ else
343
+ valid_callbacks = callbacks
344
+ end
345
+ valid_callbacks.each do |name|
277
346
  debug "Dispatching message<#{msg[:message].object_id}> to callback<#{name} (#{callback_name(name)})>"
278
347
  callback_supervisor[callback_name(name)].async.call(msg)
279
348
  end
349
+ if(valid_callbacks.empty?)
350
+ warn "Received message was not processed through any callbacks on this source: #{msg}"
351
+ orphan_callback(current_actor, msg) if respond_to?(:orphan_callback)
352
+ end
280
353
  end
281
- sleep(1) if msgs.empty?
282
354
  end
283
355
  ensure
284
356
  @processing = false
285
357
  end
358
+ true
286
359
  end
287
360
 
288
- # args:: unused
289
- # Return queued message from internal loop
361
+ # Receive messages from source
362
+ # @return [TrueClass]
363
+ def receive_messages
364
+ loop do
365
+ message_remote.push receive
366
+ signal(:messages_available)
367
+ end
368
+ true
369
+ end
370
+
371
+ # Get received message on local loopback
372
+ #
373
+ # @param args [Object] argument list (unused)
374
+ # @return [Carnivore::Message, NilClass]
290
375
  def loop_receive(*args)
291
- @message_loop.shift
376
+ message_loop.shift
292
377
  end
293
378
 
294
- # message:: Message for delivery
295
- # original_message:: unused
296
- # args:: unused
297
379
  # Push message onto internal loop queue
380
+ #
381
+ # @param message [Carnivore::Message]
382
+ # @param original_message [Object] unused
383
+ # @param args [Hash] unused
384
+ # @return [TrueClass]
298
385
  def loop_transmit(message, original_message=nil, args={})
299
- @message_loop.push message
386
+ message_loop.push message
387
+ signal(:messages_available)
388
+ true
300
389
  end
301
390
 
302
- # args:: transmit args
303
391
  # Send to local loop if processing otherwise use regular transmit
392
+ #
393
+ # @param args [Object] argument list
394
+ # @return [TrueClass]
304
395
  def _transmit(*args)
305
- if(processing)
396
+ if(loop_enabled? && processing)
306
397
  loop_transmit(*args)
307
398
  else
308
399
  custom_transmit(*args)
309
400
  end
401
+ true
402
+ end
403
+
404
+ # Local message loopback is enabled. Custom sources should
405
+ # override this method to allow loopback delivery if desired
406
+ #
407
+ # @return [TrueClass, FalseClass]
408
+ def loop_enabled?
409
+ false
310
410
  end
311
411
 
312
412
  # Load and initialize the message registry
413
+ #
414
+ # @return [MessageRegistry] new registry
313
415
  def init_registry
314
416
  require 'carnivore/message_registry'
315
417
  @message_registry = MessageRegistry.new
@@ -7,20 +7,30 @@ module Carnivore
7
7
  # occur prior to the supervisor actually starting the sources
8
8
  class SourceContainer
9
9
 
10
+ # @return [Class] class of Source
10
11
  attr_reader :klass
12
+ # @return [Hash] configuration hash for Source
11
13
  attr_reader :source_hash
12
14
 
13
- # class_name:: Name of source class
14
- # args:: argument hash to pass to source instance
15
+ # Create a new source container
16
+ #
17
+ # @param class_name [Class] class of Source
18
+ # @param args [Hash] configuration hash for source
15
19
  def initialize(class_name, args={})
16
20
  @klass = class_name
17
- @source_hash = args || {}
18
- @source_hash[:callbacks] = {}
21
+ @source_hash = Smash.new(args || {})
22
+ @source_hash[:callbacks] = Smash.new
19
23
  end
20
24
 
21
25
  # name:: Name of callback
22
26
  # klass:: Class of callback (optional)
23
27
  # Add a callback to a source via Class or block
28
+ #
29
+ # @param name [String, Symbol] name of callback
30
+ # @param klass [Class] class of callback
31
+ # @yield callback block
32
+ # @yieldparam message [Carnivore::Message] message to process
33
+ # @return [Class, Proc] callback registered
24
34
  def add_callback(name, klass=nil, &block)
25
35
  @source_hash[:callbacks][name] = klass || block
26
36
  end
@@ -19,18 +19,36 @@ MiniTest::Spec.before do
19
19
  end
20
20
 
21
21
  # Simple waiter method to stall testing
22
+ #
23
+ # @param name [String, Symbol] fetch wait time from environment variable
24
+ # @return [Numeric] seconds sleeping
22
25
  def source_wait(name='wait')
23
- sleep(ENV.fetch("CARNIVORE_SOURCE_#{name.to_s.upcase}", 0.2).to_f)
26
+ total = ENV.fetch("CARNIVORE_SOURCE_#{name.to_s.upcase}", 1.0).to_f
27
+ if(block_given?)
28
+ elapsed = 0.0
29
+ until(yield || elapsed >= total)
30
+ sleep(0.1)
31
+ elapsed += 0.1
32
+ end
33
+ elapsed
34
+ else
35
+ sleep(total)
36
+ total
37
+ end
24
38
  end
25
39
 
26
- # dummy store that should never be used for anything real
40
+ # Dummy message store used for testing
27
41
  class MessageStore
28
42
  class << self
29
43
 
44
+ # Initialize message storage
45
+ #
46
+ # @return [Array]
30
47
  def init
31
48
  @messages = []
32
49
  end
33
50
 
51
+ # @return [Array] messages
34
52
  def messages
35
53
  @messages
36
54
  end
@@ -38,21 +56,65 @@ class MessageStore
38
56
  end
39
57
  end
40
58
 
41
- # dummy source to hold final tranmission and stuff payload in store
42
59
  module Carnivore
43
60
  class Source
61
+ # Dummy source for testing used to capture payloads for inspection
44
62
  class Spec < Source
63
+
64
+ # @return [Array] messages confirmed
65
+ attr_reader :confirmed
66
+
67
+ # Creates new spec source
68
+ #
69
+ # @param args [Object] argument list (passed to Source)
70
+ # @yield source block (passed to Source)
71
+ def initialize(*args, &block)
72
+ super
73
+ @confirmed = []
74
+ end
75
+
76
+ # Setup the message store for payload storage
77
+ #
78
+ # @return [Array] message storage
45
79
  def setup(*args)
46
80
  MessageStore.init
47
81
  end
48
82
 
83
+ # Dummy receiver
49
84
  def receive(*args)
50
85
  wait(:forever)
51
86
  end
52
87
 
88
+ # Capture messages transmitted
89
+ #
90
+ # @param args [Object] argument list
91
+ # @return [TrueClass]
53
92
  def transmit(*args)
54
93
  MessageStore.messages << args.first
94
+ true
55
95
  end
96
+
97
+ # Format the message
98
+ #
99
+ # @param msg [Object] message payload
100
+ # @return [Carnivore::Message]
101
+ def format(msg)
102
+ Message.new(
103
+ :message => msg,
104
+ :source => self
105
+ )
106
+ end
107
+
108
+ # Capture confirmed messages
109
+ #
110
+ # @param payload [Object] payload of message
111
+ # @param args [Object] argument list (unused)
112
+ # @return [TrueClass]
113
+ def confirm(payload, *args)
114
+ confirmed << payload
115
+ true
116
+ end
117
+
56
118
  end
57
119
  end
58
120
  end
@@ -6,27 +6,51 @@ module Carnivore
6
6
 
7
7
  class << self
8
8
 
9
- attr_reader :registry, :supervisor
10
-
11
9
  # Build a new supervisor
10
+ #
11
+ # @return [Carinvore::Supervisor]
12
12
  def build!
13
- @registry, @supervisor = create!
14
- @supervisor
13
+ _, s = create!
14
+ supervisor(s)
15
15
  end
16
16
 
17
17
  # Create a new supervisor
18
- # Returns [registry,supervisor]
18
+ #
19
+ # @return [Array<[Celluloid::Registry, Carnivore::Supervisor]>]
19
20
  def create!
20
21
  registry = Celluloid::Registry.new
21
22
  [registry, run!(registry)]
22
23
  end
23
24
 
24
- # Destroy the registered supervisor
25
+ # Get/set the default supervisor
26
+ #
27
+ # @param sup [Carnivore::Supervisor]
28
+ # @return [Carnivore::Supervisor]
29
+ def supervisor(sup=nil)
30
+ if(sup)
31
+ Celluloid::Actor[:carnivore_supervisor] = sup
32
+ end
33
+ Celluloid::Actor[:carnivore_supervisor]
34
+ end
35
+
36
+ # Get the registry of the default supervisor
37
+ #
38
+ # @return [Celluloid::Registry, NilClass]
39
+ def registry
40
+ if(supervisor)
41
+ supervisor.registry
42
+ end
43
+ end
44
+
45
+ # Destroy the registered default supervisor
46
+ #
47
+ # @return [TrueClass]
25
48
  def terminate!
26
49
  if(supervisor)
27
50
  begin
28
51
  supervisor.terminate
29
- rescue Celluloid::DeadActorError
52
+ rescue Celluloid::DeadActorError => e
53
+ Celluloid::Logger.warn "Default supervisor is already in dead state (#{e.class}: #{e})"
30
54
  end
31
55
  @supervisor = nil
32
56
  @registry = nil
@@ -36,21 +60,15 @@ module Carnivore
36
60
 
37
61
  end
38
62
 
39
- # name:: Name of source
40
- # Return source
41
- def [](name)
42
- instance = @registry[name]
43
- unless(instance)
44
- if(member = @members.detect{|m| m && m.name.to_s == name.to_s})
45
- Celluloid::Logger.warn "Found missing actor in member list. Attempting to restart manually."
46
- restart_actor(member.actor, true)
47
- instance = @registry[name]
48
- unless(instance)
49
- Celluloid::Logger.error "Actor restart failed to make it available in the registry! (#{name})"
50
- end
51
- end
52
- end
53
- instance
63
+ # @return [Celluloid::Registry]
64
+ attr_reader :registry
65
+
66
+ # Fetch actor from registry
67
+ #
68
+ # @param k [String, Symbol] identifier
69
+ # @return [Celluloid::Actor, NilClass]
70
+ def [](k)
71
+ registry[k]
54
72
  end
55
73
 
56
74
  end