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.
- checksums.yaml +7 -0
- data/README.mediawiki +169 -0
- data/Rakefile +24 -0
- data/bin/console +11 -0
- data/bin/messagebus_swarm +77 -0
- data/lib/messagebus.rb +62 -0
- data/lib/messagebus/client.rb +166 -0
- data/lib/messagebus/cluster_map.rb +161 -0
- data/lib/messagebus/connection.rb +118 -0
- data/lib/messagebus/consumer.rb +447 -0
- data/lib/messagebus/custom_errors.rb +37 -0
- data/lib/messagebus/dottable_hash.rb +113 -0
- data/lib/messagebus/error_status.rb +42 -0
- data/lib/messagebus/logger.rb +45 -0
- data/lib/messagebus/message.rb +168 -0
- data/lib/messagebus/messagebus_types.rb +107 -0
- data/lib/messagebus/producer.rb +187 -0
- data/lib/messagebus/swarm.rb +49 -0
- data/lib/messagebus/swarm/controller.rb +296 -0
- data/lib/messagebus/swarm/drone.rb +195 -0
- data/lib/messagebus/swarm/drone/logging_worker.rb +53 -0
- data/lib/messagebus/validations.rb +68 -0
- data/lib/messagebus/version.rb +36 -0
- data/messagebus.gemspec +29 -0
- data/spec/messagebus/client_spec.rb +157 -0
- data/spec/messagebus/cluster_map_spec.rb +178 -0
- data/spec/messagebus/consumer_spec.rb +338 -0
- data/spec/messagebus/dottable_hash_spec.rb +137 -0
- data/spec/messagebus/message_spec.rb +93 -0
- data/spec/messagebus/producer_spec.rb +147 -0
- data/spec/messagebus/swarm/controller_spec.rb +73 -0
- data/spec/messagebus/validations_spec.rb +71 -0
- data/spec/spec_helper.rb +10 -0
- data/vendor/gems/stomp.rb +23 -0
- data/vendor/gems/stomp/client.rb +360 -0
- data/vendor/gems/stomp/connection.rb +583 -0
- data/vendor/gems/stomp/errors.rb +39 -0
- data/vendor/gems/stomp/ext/hash.rb +24 -0
- data/vendor/gems/stomp/message.rb +68 -0
- 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
|