carnivore 0.2.0 → 0.2.2

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.
@@ -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