mbus 1.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.mediawiki +169 -0
  3. data/Rakefile +24 -0
  4. data/bin/console +11 -0
  5. data/bin/messagebus_swarm +77 -0
  6. data/lib/messagebus.rb +62 -0
  7. data/lib/messagebus/client.rb +166 -0
  8. data/lib/messagebus/cluster_map.rb +161 -0
  9. data/lib/messagebus/connection.rb +118 -0
  10. data/lib/messagebus/consumer.rb +447 -0
  11. data/lib/messagebus/custom_errors.rb +37 -0
  12. data/lib/messagebus/dottable_hash.rb +113 -0
  13. data/lib/messagebus/error_status.rb +42 -0
  14. data/lib/messagebus/logger.rb +45 -0
  15. data/lib/messagebus/message.rb +168 -0
  16. data/lib/messagebus/messagebus_types.rb +107 -0
  17. data/lib/messagebus/producer.rb +187 -0
  18. data/lib/messagebus/swarm.rb +49 -0
  19. data/lib/messagebus/swarm/controller.rb +296 -0
  20. data/lib/messagebus/swarm/drone.rb +195 -0
  21. data/lib/messagebus/swarm/drone/logging_worker.rb +53 -0
  22. data/lib/messagebus/validations.rb +68 -0
  23. data/lib/messagebus/version.rb +36 -0
  24. data/messagebus.gemspec +29 -0
  25. data/spec/messagebus/client_spec.rb +157 -0
  26. data/spec/messagebus/cluster_map_spec.rb +178 -0
  27. data/spec/messagebus/consumer_spec.rb +338 -0
  28. data/spec/messagebus/dottable_hash_spec.rb +137 -0
  29. data/spec/messagebus/message_spec.rb +93 -0
  30. data/spec/messagebus/producer_spec.rb +147 -0
  31. data/spec/messagebus/swarm/controller_spec.rb +73 -0
  32. data/spec/messagebus/validations_spec.rb +71 -0
  33. data/spec/spec_helper.rb +10 -0
  34. data/vendor/gems/stomp.rb +23 -0
  35. data/vendor/gems/stomp/client.rb +360 -0
  36. data/vendor/gems/stomp/connection.rb +583 -0
  37. data/vendor/gems/stomp/errors.rb +39 -0
  38. data/vendor/gems/stomp/ext/hash.rb +24 -0
  39. data/vendor/gems/stomp/message.rb +68 -0
  40. metadata +138 -0
@@ -0,0 +1,187 @@
1
+ # Copyright (c) 2012, Groupon, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions
6
+ # are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # Neither the name of GROUPON nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require 'thread'
32
+ require 'net/http'
33
+ require "messagebus/dottable_hash"
34
+
35
+ module Messagebus
36
+ # Producr client class. Provides simple API to publish events. Refresh connections every connectionLifetime user can specify a load-balancer
37
+ # and the producer will connect with different server each connectionLifetime interval effectively rotating publish load to all machines.
38
+ #
39
+ # parameters:
40
+ # dest (String, required value, name of the queue/topic)
41
+ # host_params (list<string>, required value, eg. '[localhost:61613]')
42
+ # options : A hash map for optional values.
43
+ # user (String, default : '')
44
+ # passwd (String, default : '')
45
+ # conn_lifetime_sec (Int, default:300 secs)
46
+ # receipt_wait_timeout_ms (Int, optoional value, default: 5 seconds)
47
+ #
48
+ class Producer < Connection
49
+ attr_accessor :state
50
+ PUBLISH_HEADERS = { :suppress_content_length => true, :persistent => true }
51
+ SCHEDULED_DELIVERY_TIME_MS_HEADER = 'scheduled_delivery_time'
52
+
53
+ def initialize(host_params, options={})
54
+ options = DottableHash.new(options)
55
+ super(host_params, options)
56
+ validate_connection_config(@host_params, options)
57
+ end
58
+
59
+ # Start the producer client
60
+ def start
61
+ @state = STARTED
62
+ logger.info "Starting producer with host_params:#{@host_params}"
63
+ @connection_start_time = Time.new
64
+ @stomp = start_server(@host_params, @options.user, @options.passwd)
65
+ rescue => e
66
+ logger.error "Error occurred while starting a connection: #{e}\n #{e.backtrace.join("\n")}"
67
+ end
68
+
69
+ # Close the producer client
70
+ def stop
71
+ @state = STOPPED
72
+ logger.info "Stopping producer with host_params:#{@host_params}"
73
+ stop_server(@stomp)
74
+ rescue => e
75
+ logger.error "Error occurred while stopping a connection: #{e}\n #{e.backtrace.join("\n")}"
76
+ end
77
+
78
+ # This is implemented with a *args to workaround the historical api
79
+ # requiring a dest_type parameter. That parameter has been removed, but the
80
+ # api has been kept backwards compatible for now.
81
+ #
82
+ # Historical version
83
+ # def publish(dest, dest_type, message, connect_headers={}, safe=true)
84
+ # For the current version see actual_publish.
85
+ def publish(*args)
86
+ if args.size > 2 && args[1].is_a?(String) && (args[1].downcase == 'topic' || args[1].downcase == 'queue')
87
+ logger.warn "Passing dest_type to Producer#publish is deprecated (it isn't needed). Please update your usage."
88
+ args.delete_at(1)
89
+ end
90
+ actual_publish(*args)
91
+ end
92
+
93
+ # This is the actual publish method. See publish for why this is designed this way.
94
+ def actual_publish(dest, message, connect_headers={}, safe=true)
95
+ if !started?
96
+ logger.error "Cannot publish without first starting the producer. Current state is '#{@state}'"
97
+ return
98
+ end
99
+ validate_destination_config(dest)
100
+ publish_internal(dest, message, connect_headers, safe)
101
+ rescue => e
102
+ logger.error "Error occurred while publishing the message: #{e}\n #{e.backtrace.join("\n")}"
103
+ return false
104
+ end
105
+
106
+ private
107
+
108
+ def publish_internal(dest, message, connect_headers, safe_mode=false)
109
+ if not message.is_a?(Messagebus::Message)
110
+ raise "ERROR: message should be a Messagebus::Message type."
111
+ end
112
+
113
+ check_and_refresh_connection
114
+ logger.debug "publishing message with message Id:#{message.message_id} safe_mode:#{safe_mode}"
115
+ logger.debug { "message: #{message.inspect}" }
116
+
117
+ connect_headers = connect_headers.merge(PUBLISH_HEADERS)
118
+ if not safe_mode
119
+ @stomp.publish(dest, message.to_thrift_binary, connect_headers)
120
+ return true
121
+ else
122
+ receipt_received = false
123
+ errors_received = nil
124
+ @stomp.publish(dest, message.to_thrift_binary, connect_headers) do |msg|
125
+ if msg.command == 'ERROR'
126
+ errors_received = msg
127
+ raise "Failed to publish the message while publishing to #{dest} to the server with Error: #{msg.body.to_s} #{caller}"
128
+ else
129
+ receipt_received = true
130
+ end
131
+ end
132
+
133
+ # wait for receipt up to given timeout.
134
+ do_with_timeout(@options.receipt_wait_timeout_ms) do
135
+ if errors_received
136
+ raise "Failed to publish the message while publishing to #{dest} to the server with Error:\n" + errors_received.body.to_s
137
+ end
138
+
139
+ if not receipt_received
140
+ sleep 0.005
141
+ else
142
+ return true
143
+ end
144
+ end
145
+
146
+ logger.error "Publish to #{dest} may have failed, publish_safe() timeout while waiting for receipt"
147
+ raise "publish_safe() timeout while waiting for receipt"
148
+ end
149
+ end
150
+
151
+ def check_refresh_required
152
+ logger.debug("Checking if we need to refresh connections....")
153
+ stale_connection = false
154
+ current_time = Time.new()
155
+ conn_time = current_time - @connection_start_time
156
+ refresh_connection = false
157
+ if conn_time > @options.conn_lifetime_sec
158
+ stale_connection = true
159
+ logger.info("Stale connection found, need to refresh connection.")
160
+ end
161
+
162
+ broken_connection = false
163
+ if @stomp == nil || !@stomp.respond_to?("running") || !@stomp.running
164
+ broken_connection = true
165
+ end
166
+
167
+
168
+ if @state == STOPPED || stale_connection || broken_connection
169
+ refresh_connection = true
170
+ end
171
+ refresh_connection
172
+ end
173
+
174
+ def check_and_refresh_connection
175
+ if check_refresh_required
176
+ logger.debug("Connection status = #{@state}")
177
+ logger.debug("Refreshing connection...")
178
+
179
+ if @state != STOPPED
180
+ stop
181
+ end
182
+ start
183
+ end
184
+ end
185
+
186
+ end
187
+ end
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2012, Groupon, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions
6
+ # are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # Neither the name of GROUPON nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ module Messagebus
32
+ module Swarm
33
+ def self.logger=(logger)
34
+ @logger = logger
35
+ end
36
+
37
+ def self.logger
38
+ @logger
39
+ end
40
+
41
+ def self.default_configuration=(default_configuration)
42
+ @default_configuration = default_configuration
43
+ end
44
+
45
+ def self.default_configuration
46
+ @default_configuration
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,296 @@
1
+ # Copyright (c) 2012, Groupon, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions
6
+ # are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # Neither the name of GROUPON nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require 'messagebus/consumer'
32
+ require 'messagebus/swarm/drone'
33
+
34
+ require 'yaml'
35
+
36
+ module Messagebus
37
+ module Swarm
38
+ ##
39
+ # The controller for a set of drone workers.
40
+ class Controller
41
+ ConfigurationSource = Struct.new(:configuration_type, :value) do
42
+ def configuration_hash
43
+ case configuration_type
44
+ when :path
45
+ YAML.load_file(value)
46
+ when :eval
47
+ eval(value)
48
+ when :default
49
+ Messagebus::Swarm.default_configuration
50
+ else
51
+ raise "Unsupported configuration type #{configuration_type}"
52
+ end
53
+ end
54
+ end
55
+
56
+ class BadConfigurationError < RuntimeError; end
57
+
58
+ # It's important this a different logger instance than the one used for
59
+ # the drones/consumers/other things to avoid deadlocking issues. It's ok
60
+ # for it to use the same file, just not be the same instance of a logger.
61
+ #
62
+ # This logger will be used in a signal handler, and logging involves
63
+ # mutexes, so we need/want to be sure the logger isn't being used by
64
+ # any other code outside the signal handler.
65
+ def self.swarm_control_logger=(swarm_control_logger)
66
+ @swarm_control_logger = swarm_control_logger
67
+ end
68
+ def self.swarm_control_logger
69
+ @swarm_control_logger ||= Logger.new($stdout)
70
+ end
71
+
72
+ def self.after_fork(&block)
73
+ after_fork_procs << block
74
+ end
75
+
76
+ def self.after_fork_procs
77
+ @after_fork_procs ||= []
78
+ end
79
+
80
+ ##
81
+ # Starts up the swarm based on the given config. This method does not
82
+ # return until the swarm is stopped down.
83
+ #
84
+ # If the config has config[:swarm][:fork]=true, it will boot the drones
85
+ # in subprocesses, otherwise it will use threads.
86
+ #
87
+ # [destination_name] limit booting drones to only ones acting on the given
88
+ # destination
89
+ # [drone_count] override the number of drones to run
90
+ def self.start(configuration_source, drone_logger, destination_name=nil, drone_count=nil)
91
+ config = if configuration_source.is_a?(ConfigurationSource)
92
+ configuration_source.configuration_hash
93
+ else
94
+ configuration_source
95
+ end
96
+ raise BadConfigurationError.new("#{configuration_source.inspect} didn't evaluate to a configuration") if config.nil?
97
+ config = Messagebus::DottableHash.new(config)
98
+ relevant_worker_configs = config.workers
99
+
100
+ # apply any applicable destination_name or drone_count settings
101
+ relevant_worker_configs = relevant_worker_configs.select { |worker_config| worker_config[:destination] == destination_name } if destination_name
102
+ relevant_worker_configs = relevant_worker_configs.map { |worker_config| worker_config.merge(:drones => drone_count) } if drone_count
103
+
104
+ # The || is for backwards compatibility
105
+ default_cluster_config = config.cluster_defaults || config
106
+
107
+ drones = build_drones(relevant_worker_configs, default_cluster_config, config.clusters, swarm_control_logger, drone_logger)
108
+ booter = start_drones(swarm_control_logger, config.swarm_config && config.swarm_config.fork, drones)
109
+
110
+ booter.wait
111
+ end
112
+
113
+ ##
114
+ # Shut down a previously started swarm
115
+ def self.stop(pid)
116
+ stop_drones(pid)
117
+ end
118
+
119
+ def self.require_files(files)
120
+ files.each { |file_to_require| require file_to_require }
121
+ end
122
+
123
+ def self.write_pid(pid_file)
124
+ File.open(pid_file, "w") { |f| f.print(Process.pid) }
125
+ end
126
+
127
+ def self.delete_pid(pid_file)
128
+ File.delete(pid_file) if File.exist?(pid_file)
129
+ end
130
+
131
+ # :nodoc: all
132
+ module ProcessManagementConcerns
133
+ # We capture INT (ctrl-c) and TERM (default signal kill sends).
134
+ STOP_PARENT_PROCESSING_SIGNALS = %w(INT TERM)
135
+ STOP_SUBPROCESS_SIGNAL = 'TERM'
136
+
137
+ class DroneForker
138
+ def initialize(drones)
139
+ @drones = drones
140
+ end
141
+
142
+ def start
143
+ # Intentionally not doing this with a map so if we get a signal
144
+ # while building this list up, we'll still have some/most of the
145
+ # pids. This still isn't perfect. Sure there's a perfect way to
146
+ # do this.
147
+ @pids = []
148
+ parent_pid = Process.pid
149
+
150
+ @drones.map do |drone|
151
+ @pids << fork do
152
+ $0 = "ruby messagebus_swarm worker for #{parent_pid}-#{drone.id}"
153
+ Messagebus::Swarm::Controller.after_fork_procs.each{|after_fork_proc| after_fork_proc.call }
154
+ register_stop_signal_handler(drone)
155
+ drone.processing_loop
156
+ end
157
+ end
158
+ end
159
+
160
+ def wait
161
+ Process.wait
162
+ end
163
+
164
+ def stop
165
+ @pids.each do |pid|
166
+ Process.kill(STOP_SUBPROCESS_SIGNAL, pid)
167
+ end
168
+ end
169
+
170
+ private
171
+ def register_stop_signal_handler(drone)
172
+ Signal.trap(STOP_SUBPROCESS_SIGNAL) do
173
+ # The drone is stopped in a separate thread so that when our
174
+ # signal handler triggers, it'll be unblocking a thread that is
175
+ # not itself. Without this, ruby's queue impl seems to get stuck.
176
+ # This most likely has something to do with a thread unblocking
177
+ # itself, which isn't something that could ever happen outside
178
+ # of a signal.
179
+ # tldr: this is black magic, and seems to work
180
+ Thread.new do
181
+ drone.stop
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ class DroneThreader
188
+ def initialize(drones)
189
+ @drones = drones
190
+ end
191
+
192
+ def start
193
+ @threads = @drones.map do |drone|
194
+ Thread.new do
195
+ drone.processing_loop
196
+ end
197
+ end
198
+ end
199
+
200
+ def wait
201
+ @threads.each(&:join)
202
+ end
203
+
204
+ def stop
205
+ @drones.each(&:stop)
206
+ end
207
+ end
208
+
209
+ def stop_drones(pid)
210
+ Process.kill(STOP_PARENT_PROCESSING_SIGNALS[0], pid)
211
+ end
212
+
213
+ def start_drones(swarm_control_logger, fork_or_not, drones)
214
+ swarm_control_logger.info("Booting drones fork=#{!!fork_or_not}")
215
+ booter = if fork_or_not
216
+ DroneForker.new(drones)
217
+ else
218
+ DroneThreader.new(drones)
219
+ end
220
+
221
+ STOP_PARENT_PROCESSING_SIGNALS.each do |signal|
222
+ Signal.trap(signal) do
223
+ swarm_control_logger.info("Stopping drones")
224
+ booter.stop
225
+ end
226
+ end
227
+ booter.start
228
+ booter
229
+ end
230
+ end
231
+ extend ProcessManagementConcerns
232
+
233
+ # :nodoc: all
234
+ module DroneFactoryConcerns
235
+ def build_drones(worker_configs, default_cluster_config, cluster_configs, swarm_control_logger, drone_logger)
236
+ drones = worker_configs.map do |worker_config|
237
+ destination_name = worker_config.destination
238
+ relevant_clusters = cluster_configs.select do |cluster_config|
239
+ destinations = cluster_config.destinations
240
+ destinations.include?(destination_name)
241
+ end
242
+
243
+ if relevant_clusters.empty?
244
+ swarm_control_logger.warn "couldn't find any clusters to process #{destination_name}"
245
+ end
246
+
247
+ consumers = build_consumers(worker_config, relevant_clusters, destination_name, default_cluster_config, swarm_control_logger)
248
+ drones = build_drone_instances(worker_config, consumers, destination_name, drone_logger)
249
+ end.flatten
250
+
251
+ drones
252
+ end
253
+
254
+ private
255
+ def build_consumers(worker_config, relevant_clusters, destination_name, default_cluster_config, swarm_control_logger)
256
+ relevant_clusters.map do |cluster_config|
257
+ full_cluster_config = default_cluster_config.merge(cluster_config)
258
+ consumer_address = full_cluster_config.consumer_address
259
+
260
+ swarm_control_logger.debug { "creating a consumer for #{destination_name}@#{consumer_address} with settings: #{full_cluster_config.inspect}" }
261
+
262
+ # Build worker_count consumers because we should have 1 consumer per drone
263
+ (worker_config.drones || 1).times.map do
264
+ Messagebus::Consumer.new([consumer_address], full_cluster_config.merge!(
265
+ :destination_name => destination_name,
266
+ :ack_type => Messagebus::ACK_TYPE_CLIENT,
267
+ :subscription_id => worker_config.subscription_id
268
+ ))
269
+ end
270
+ end.flatten
271
+ end
272
+
273
+ def build_drone_instances(worker_config, consumers, destination_name, drone_logger)
274
+ consumers.map do |consumer|
275
+ worker_class = constantize(worker_config.worker)
276
+
277
+ # arbitrary id, but seems like having a unique identifier for the drone could come in handy
278
+ drone_id = rand(1_000_000_000).to_s
279
+ drone = Drone.new(:consumer => consumer,
280
+ :destination_name => destination_name,
281
+ :id => drone_id,
282
+ :logger => drone_logger,
283
+ :worker_class => worker_class)
284
+ end
285
+ end
286
+
287
+ def constantize(worker_class_name)
288
+ worker_class_name.split("::").inject(Object) do |base_response, class_string|
289
+ base_response = base_response.const_get(class_string)
290
+ end
291
+ end
292
+ end
293
+ extend DroneFactoryConcerns
294
+ end
295
+ end
296
+ end