shoryuken 2.1.1 → 3.1.3

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +5 -8
  3. data/.rubocop.yml +8 -2
  4. data/.travis.yml +6 -0
  5. data/CHANGELOG.md +167 -0
  6. data/Gemfile +2 -0
  7. data/README.md +16 -155
  8. data/Rakefile +0 -1
  9. data/bin/cli/base.rb +40 -0
  10. data/bin/cli/sqs.rb +204 -0
  11. data/bin/shoryuken +55 -9
  12. data/examples/default_worker.rb +1 -1
  13. data/lib/shoryuken/client.rb +3 -15
  14. data/lib/shoryuken/default_worker_registry.rb +10 -6
  15. data/lib/shoryuken/environment_loader.rb +60 -57
  16. data/lib/shoryuken/fetcher.rb +21 -51
  17. data/lib/shoryuken/launcher.rb +77 -23
  18. data/lib/shoryuken/logging.rb +0 -5
  19. data/lib/shoryuken/manager.rb +54 -205
  20. data/lib/shoryuken/message.rb +4 -13
  21. data/lib/shoryuken/middleware/chain.rb +1 -18
  22. data/lib/shoryuken/middleware/server/auto_delete.rb +3 -8
  23. data/lib/shoryuken/middleware/server/auto_extend_visibility.rb +16 -17
  24. data/lib/shoryuken/middleware/server/exponential_backoff_retry.rb +37 -22
  25. data/lib/shoryuken/middleware/server/timing.rb +2 -2
  26. data/lib/shoryuken/options.rb +196 -0
  27. data/lib/shoryuken/polling/base.rb +67 -0
  28. data/lib/shoryuken/polling/strict_priority.rb +77 -0
  29. data/lib/shoryuken/polling/weighted_round_robin.rb +66 -0
  30. data/lib/shoryuken/processor.rb +27 -25
  31. data/lib/shoryuken/queue.rb +28 -9
  32. data/lib/shoryuken/runner.rb +131 -0
  33. data/lib/shoryuken/util.rb +1 -9
  34. data/lib/shoryuken/version.rb +1 -1
  35. data/lib/shoryuken/worker.rb +9 -1
  36. data/lib/shoryuken.rb +47 -157
  37. data/shoryuken.gemspec +7 -7
  38. data/spec/integration/launcher_spec.rb +15 -8
  39. data/spec/shoryuken/client_spec.rb +2 -45
  40. data/spec/shoryuken/default_worker_registry_spec.rb +12 -10
  41. data/spec/shoryuken/environment_loader_spec.rb +54 -0
  42. data/spec/shoryuken/fetcher_spec.rb +27 -46
  43. data/spec/shoryuken/manager_spec.rb +80 -93
  44. data/spec/shoryuken/middleware/chain_spec.rb +0 -24
  45. data/spec/shoryuken/middleware/server/auto_delete_spec.rb +2 -2
  46. data/spec/shoryuken/middleware/server/auto_extend_visibility_spec.rb +7 -3
  47. data/spec/shoryuken/middleware/server/exponential_backoff_retry_spec.rb +56 -33
  48. data/spec/shoryuken/middleware/server/timing_spec.rb +5 -3
  49. data/spec/shoryuken/options_spec.rb +100 -0
  50. data/spec/shoryuken/polling/strict_priority_spec.rb +140 -0
  51. data/spec/shoryuken/polling/weighted_round_robin_spec.rb +99 -0
  52. data/spec/shoryuken/processor_spec.rb +20 -37
  53. data/spec/shoryuken/queue_spec.rb +72 -26
  54. data/spec/shoryuken/{cli_spec.rb → runner_spec.rb} +11 -26
  55. data/spec/shoryuken_spec.rb +1 -48
  56. data/spec/spec_helper.rb +14 -20
  57. data/test_workers/endless_interruptive_worker.rb +41 -0
  58. data/test_workers/endless_uninterruptive_worker.rb +44 -0
  59. metadata +39 -34
  60. data/lib/shoryuken/aws_config.rb +0 -64
  61. data/lib/shoryuken/cli.rb +0 -215
  62. data/lib/shoryuken/sns_arn.rb +0 -27
  63. data/lib/shoryuken/topic.rb +0 -17
  64. data/spec/shoryuken/sns_arn_spec.rb +0 -42
  65. data/spec/shoryuken/topic_spec.rb +0 -32
  66. data/spec/shoryuken_endpoint.yml +0 -6
@@ -20,9 +20,9 @@ module Shoryuken
20
20
  end
21
21
 
22
22
  logger.info { "completed in: #{total_time} ms" }
23
- rescue => e
23
+ rescue
24
24
  logger.info { "failed in: #{elapsed(started_at)} ms" }
25
- raise e
25
+ raise
26
26
  end
27
27
  end
28
28
  end
@@ -0,0 +1,196 @@
1
+ module Shoryuken
2
+ class Options
3
+ DEFAULTS = {
4
+ concurrency: 25,
5
+ queues: [],
6
+ aws: {},
7
+ delay: 0,
8
+ timeout: 8,
9
+ lifecycle_events: {
10
+ startup: [],
11
+ dispatch: [],
12
+ quiet: [],
13
+ shutdown: []
14
+ }
15
+ }.freeze
16
+
17
+ @@groups = {}
18
+ @@worker_registry = DefaultWorkerRegistry.new
19
+ @@active_job_queue_name_prefixing = false
20
+ @@sqs_client = nil
21
+ @@sqs_client_receive_message_opts = {}
22
+ @@start_callback = nil
23
+ @@stop_callback = nil
24
+
25
+ class << self
26
+ def active_job?
27
+ defined?(::ActiveJob)
28
+ end
29
+
30
+ def add_group(group, concurrency)
31
+ groups[group] ||= {
32
+ concurrency: concurrency,
33
+ queues: []
34
+ }
35
+ end
36
+
37
+ def groups
38
+ @@groups
39
+ end
40
+
41
+ def add_queue(queue, weight, group)
42
+ weight.times do
43
+ groups[group][:queues] << queue
44
+ end
45
+ end
46
+
47
+ def ungrouped_queues
48
+ groups.values.flat_map { |options| options[:queues] }
49
+ end
50
+
51
+ def worker_registry
52
+ @@worker_registry
53
+ end
54
+
55
+ def worker_registry=(worker_registry)
56
+ @@worker_registry = worker_registry
57
+ end
58
+
59
+ def polling_strategy(group)
60
+ options[group].to_h.fetch(:polling_strategy, Polling::WeightedRoundRobin)
61
+ end
62
+
63
+ def start_callback
64
+ @@start_callback
65
+ end
66
+
67
+ def start_callback=(start_callback)
68
+ @@start_callback = start_callback
69
+ end
70
+
71
+ def stop_callback
72
+ @@stop_callback
73
+ end
74
+
75
+ def stop_callback=(stop_callback)
76
+ @@stop_callback = stop_callback
77
+ end
78
+
79
+ def active_job_queue_name_prefixing
80
+ @@active_job_queue_name_prefixing
81
+ end
82
+
83
+ def active_job_queue_name_prefixing=(active_job_queue_name_prefixing)
84
+ @@active_job_queue_name_prefixing = active_job_queue_name_prefixing
85
+ end
86
+
87
+ def sqs_client
88
+ @@sqs_client ||= Aws::SQS::Client.new
89
+ end
90
+
91
+ def sqs_client=(sqs_client)
92
+ @@sqs_client = sqs_client
93
+ end
94
+
95
+ def sqs_client_receive_message_opts
96
+ @@sqs_client_receive_message_opts
97
+ end
98
+
99
+ def sqs_client_receive_message_opts=(sqs_client_receive_message_opts)
100
+ @@sqs_client_receive_message_opts['default'] = sqs_client_receive_message_opts
101
+ end
102
+
103
+ def options
104
+ @@options ||= DEFAULTS.dup
105
+ end
106
+
107
+ def logger
108
+ Shoryuken::Logging.logger
109
+ end
110
+
111
+ def register_worker(*args)
112
+ @@worker_registry.register_worker(*args)
113
+ end
114
+
115
+ def configure_server
116
+ yield self if server?
117
+ end
118
+
119
+ def server_middleware
120
+ @@server_chain ||= default_server_middleware
121
+ yield @@server_chain if block_given?
122
+ @@server_chain
123
+ end
124
+
125
+ def configure_client
126
+ yield self unless server?
127
+ end
128
+
129
+ def client_middleware
130
+ @@client_chain ||= default_client_middleware
131
+ yield @@client_chain if block_given?
132
+ @@client_chain
133
+ end
134
+
135
+ def default_worker_options
136
+ @@default_worker_options ||= {
137
+ 'queue' => 'default',
138
+ 'delete' => false,
139
+ 'auto_delete' => false,
140
+ 'auto_visibility_timeout' => false,
141
+ 'retry_intervals' => nil,
142
+ 'batch' => false
143
+ }
144
+ end
145
+
146
+ def default_worker_options=(default_worker_options)
147
+ @@default_worker_options = default_worker_options
148
+ end
149
+
150
+ def on_start(&block)
151
+ @@start_callback = block
152
+ end
153
+
154
+ def on_stop(&block)
155
+ @@stop_callback = block
156
+ end
157
+
158
+ # Register a block to run at a point in the Shoryuken lifecycle.
159
+ # :startup, :quiet or :shutdown are valid events.
160
+ #
161
+ # Shoryuken.configure_server do |config|
162
+ # config.on(:shutdown) do
163
+ # puts "Goodbye cruel world!"
164
+ # end
165
+ # end
166
+ def on(event, &block)
167
+ fail ArgumentError, "Symbols only please: #{event}" unless event.is_a?(Symbol)
168
+ fail ArgumentError, "Invalid event name: #{event}" unless options[:lifecycle_events].key?(event)
169
+ options[:lifecycle_events][event] << block
170
+ end
171
+
172
+ private
173
+
174
+ def default_server_middleware
175
+ Middleware::Chain.new do |m|
176
+ m.add Middleware::Server::Timing
177
+ m.add Middleware::Server::ExponentialBackoffRetry
178
+ m.add Middleware::Server::AutoDelete
179
+ m.add Middleware::Server::AutoExtendVisibility
180
+ if defined?(::ActiveRecord::Base)
181
+ require 'shoryuken/middleware/server/active_record'
182
+ m.add Middleware::Server::ActiveRecord
183
+ end
184
+ end
185
+ end
186
+
187
+ def default_client_middleware
188
+ Middleware::Chain.new
189
+ end
190
+
191
+ def server?
192
+ defined?(Shoryuken::CLI)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,67 @@
1
+ module Shoryuken
2
+ module Polling
3
+ QueueConfiguration = Struct.new(:name, :options) do
4
+ def hash
5
+ name.hash
6
+ end
7
+
8
+ def ==(other)
9
+ case other
10
+ when String
11
+ if options.empty?
12
+ name == other
13
+ else
14
+ false
15
+ end
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ alias_method :eql?, :==
22
+
23
+ def to_s
24
+ if options.empty?
25
+ name
26
+ else
27
+ "#<QueueConfiguration #{name} options=#{options.inspect}>"
28
+ end
29
+ end
30
+ end
31
+
32
+ class BaseStrategy
33
+ include Util
34
+
35
+ def next_queue
36
+ fail NotImplementedError
37
+ end
38
+
39
+ def messages_found(queue, messages_found)
40
+ fail NotImplementedError
41
+ end
42
+
43
+ def active_queues
44
+ fail NotImplementedError
45
+ end
46
+
47
+ def ==(other)
48
+ case other
49
+ when Array
50
+ @queues == other
51
+ else
52
+ if other.respond_to?(:active_queues)
53
+ active_queues == other.active_queues
54
+ else
55
+ false
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def delay
63
+ Shoryuken.options[:delay].to_f
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,77 @@
1
+ module Shoryuken
2
+ module Polling
3
+ class StrictPriority < BaseStrategy
4
+ def initialize(queues)
5
+ # Priority ordering of the queues, highest priority first
6
+ @queues = queues
7
+ .group_by { |q| q }
8
+ .sort_by { |_, qs| -qs.count }
9
+ .map(&:first)
10
+
11
+ # Pause status of the queues, default to past time (unpaused)
12
+ @paused_until = queues
13
+ .each_with_object(Hash.new) { |queue, h| h[queue] = Time.at(0) }
14
+
15
+ # Start queues at 0
16
+ reset_next_queue
17
+ end
18
+
19
+ def next_queue
20
+ next_queue = next_active_queue
21
+ next_queue.nil? ? nil : QueueConfiguration.new(next_queue, {})
22
+ end
23
+
24
+ def messages_found(queue, messages_found)
25
+ if messages_found == 0
26
+ pause(queue)
27
+ else
28
+ reset_next_queue
29
+ end
30
+ end
31
+
32
+ def active_queues
33
+ @queues
34
+ .reverse
35
+ .map.with_index(1)
36
+ .reject { |q, _| queue_paused?(q) }
37
+ .reverse
38
+ end
39
+
40
+ private
41
+
42
+ def next_active_queue
43
+ reset_next_queue if queues_unpaused_since?
44
+
45
+ size = @queues.length
46
+ size.times do
47
+ queue = @queues[@next_queue_index]
48
+ @next_queue_index = (@next_queue_index + 1) % size
49
+ return queue unless queue_paused?(queue)
50
+ end
51
+
52
+ nil
53
+ end
54
+
55
+ def queues_unpaused_since?
56
+ last = @last_unpause_check
57
+ now = @last_unpause_check = Time.now
58
+
59
+ last && @paused_until.values.any? { |t| t > last && t <= now }
60
+ end
61
+
62
+ def reset_next_queue
63
+ @next_queue_index = 0
64
+ end
65
+
66
+ def queue_paused?(queue)
67
+ @paused_until[queue] > Time.now
68
+ end
69
+
70
+ def pause(queue)
71
+ return unless delay > 0
72
+ @paused_until[queue] = Time.now + delay
73
+ logger.debug "Paused #{queue}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,66 @@
1
+ module Shoryuken
2
+ module Polling
3
+ class WeightedRoundRobin < BaseStrategy
4
+ def initialize(queues)
5
+ @initial_queues = queues
6
+ @queues = queues.dup.uniq
7
+ @paused_queues = []
8
+ end
9
+
10
+ def next_queue
11
+ unpause_queues
12
+ queue = @queues.shift
13
+ return nil if queue.nil?
14
+
15
+ @queues << queue
16
+ QueueConfiguration.new(queue, {})
17
+ end
18
+
19
+ def messages_found(queue, messages_found)
20
+ if messages_found == 0
21
+ pause(queue)
22
+ return
23
+ end
24
+
25
+ maximum_weight = maximum_queue_weight(queue)
26
+ current_weight = current_queue_weight(queue)
27
+ if maximum_weight > current_weight
28
+ logger.info { "Increasing #{queue} weight to #{current_weight + 1}, max: #{maximum_weight}" }
29
+ @queues << queue
30
+ end
31
+ end
32
+
33
+ def active_queues
34
+ unparse_queues(@queues)
35
+ end
36
+
37
+ private
38
+
39
+ def pause(queue)
40
+ return unless @queues.delete(queue)
41
+ @paused_queues << [Time.now + delay, queue]
42
+ logger.debug "Paused #{queue}"
43
+ end
44
+
45
+ def unpause_queues
46
+ return if @paused_queues.empty?
47
+ return if Time.now < @paused_queues.first[0]
48
+ pause = @paused_queues.shift
49
+ @queues << pause[1]
50
+ logger.debug "Unpaused #{pause[1]}"
51
+ end
52
+
53
+ def current_queue_weight(queue)
54
+ queue_weight(@queues, queue)
55
+ end
56
+
57
+ def maximum_queue_weight(queue)
58
+ queue_weight(@initial_queues, queue)
59
+ end
60
+
61
+ def queue_weight(queues, queue)
62
+ queues.count { |q| q == queue }
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,41 +1,46 @@
1
- require 'json'
2
-
3
1
  module Shoryuken
4
2
  class Processor
5
- include Celluloid
6
3
  include Util
7
4
 
8
- def initialize(manager)
9
- @manager = manager
5
+ attr_reader :queue, :sqs_msg
6
+
7
+ def self.process(queue, sqs_msg)
8
+ new(queue, sqs_msg).process
10
9
  end
11
10
 
12
- attr_accessor :proxy_id
11
+ def initialize(queue, sqs_msg)
12
+ @queue = queue
13
+ @sqs_msg = sqs_msg
14
+ end
13
15
 
14
- def process(queue, sqs_msg)
15
- @manager.async.real_thread(proxy_id, Thread.current)
16
+ def process
17
+ return logger.error { "No worker found for #{queue}" } unless worker
16
18
 
17
- worker = Shoryuken.worker_registry.fetch_worker(queue, sqs_msg)
19
+ worker.class.server_middleware.invoke(worker, queue, sqs_msg, body) do
20
+ worker.perform(sqs_msg, body)
21
+ end
22
+ rescue Exception => ex
23
+ logger.error { "Processor failed: #{ex.message}" }
24
+ logger.error { ex.backtrace.join("\n") } unless ex.backtrace.nil?
18
25
 
19
- body = get_body(worker.class, sqs_msg)
26
+ raise
27
+ end
20
28
 
21
- worker.class.server_middleware.invoke(worker, queue, sqs_msg, body) do
22
- worker.perform(sqs_msg, body)
23
- end
29
+ private
24
30
 
25
- @manager.async.processor_done(queue, current_actor)
31
+ def worker
32
+ @_worker ||= Shoryuken.worker_registry.fetch_worker(queue, sqs_msg)
26
33
  end
27
34
 
28
- private
35
+ def worker_class
36
+ worker.class
37
+ end
29
38
 
30
- def get_body(worker_class, sqs_msg)
31
- if sqs_msg.is_a? Array
32
- sqs_msg.map { |m| parse_body(worker_class, m) }
33
- else
34
- parse_body(worker_class, sqs_msg)
35
- end
39
+ def body
40
+ @_body ||= sqs_msg.is_a?(Array) ? sqs_msg.map(&method(:parse_body)) : parse_body(sqs_msg)
36
41
  end
37
42
 
38
- def parse_body(worker_class, sqs_msg)
43
+ def parse_body(sqs_msg)
39
44
  body_parser = worker_class.get_shoryuken_options['body_parser']
40
45
 
41
46
  case body_parser
@@ -55,9 +60,6 @@ module Shoryuken
55
60
  body_parser.load(sqs_msg.body)
56
61
  end
57
62
  end
58
- rescue => e
59
- logger.error { "Error parsing the message body: #{e.message}\nbody_parser: #{body_parser}\nsqs_msg.body: #{sqs_msg.body}" }
60
- raise
61
63
  end
62
64
  end
63
65
  end
@@ -1,17 +1,16 @@
1
1
  module Shoryuken
2
2
  class Queue
3
+ include Util
4
+
3
5
  FIFO_ATTR = 'FifoQueue'
4
6
  MESSAGE_GROUP_ID = 'ShoryukenMessage'
5
7
  VISIBILITY_TIMEOUT_ATTR = 'VisibilityTimeout'
6
8
 
7
9
  attr_accessor :name, :client, :url
8
10
 
9
- def initialize(client, name)
10
- self.name = name
11
+ def initialize(client, name_or_url)
11
12
  self.client = client
12
- self.url = client.get_queue_url(queue_name: name).queue_url
13
- rescue Aws::SQS::Errors::NonExistentQueue => e
14
- raise e, "The specified queue '#{name}' does not exist."
13
+ set_name_and_url(name_or_url)
15
14
  end
16
15
 
17
16
  def visibility_timeout
@@ -19,7 +18,13 @@ module Shoryuken
19
18
  end
20
19
 
21
20
  def delete_messages(options)
22
- client.delete_message_batch(options.merge(queue_url: url))
21
+ client.delete_message_batch(
22
+ options.merge(queue_url: url)
23
+ ).failed.any? do |failure|
24
+ logger.error do
25
+ "Could not delete #{failure.id}, code: '#{failure.code}', message: '#{failure.message}', sender_fault: #{failure.sender_fault}"
26
+ end
27
+ end
23
28
  end
24
29
 
25
30
  def send_message(options)
@@ -35,9 +40,7 @@ module Shoryuken
35
40
  end
36
41
 
37
42
  def receive_messages(options)
38
- client.receive_message(options.merge(queue_url: url)).
39
- messages.
40
- map { |m| Message.new(client, self, m) }
43
+ client.receive_message(options.merge(queue_url: url)).messages.map { |m| Message.new(client, self, m) }
41
44
  end
42
45
 
43
46
  def fifo?
@@ -46,6 +49,22 @@ module Shoryuken
46
49
 
47
50
  private
48
51
 
52
+ def set_name(name)
53
+ self.name = name
54
+ self.url = client.get_queue_url(queue_name: name).queue_url
55
+ rescue Aws::Errors::NoSuchEndpointError, Aws::SQS::Errors::NonExistentQueue => ex
56
+ raise ex, "The specified queue #{name} does not exist."
57
+ end
58
+
59
+ def set_url(url)
60
+ self.name = url.split('/').last
61
+ self.url = url
62
+ end
63
+
64
+ def set_name_and_url(name_or_url)
65
+ name_or_url.start_with?('https://sqs.') ? set_url(name_or_url) : set_name(name_or_url)
66
+ end
67
+
49
68
  def queue_attributes
50
69
  # Note: Retrieving all queue attributes as requesting `FifoQueue` on non-FIFO queue raises error.
51
70
  # See issue: https://github.com/aws/aws-sdk-ruby/issues/1350
@@ -0,0 +1,131 @@
1
+ $stdout.sync = true
2
+
3
+ require 'singleton'
4
+ require 'optparse'
5
+ require 'erb'
6
+
7
+ require 'shoryuken'
8
+
9
+ module Shoryuken
10
+ # rubocop:disable Lint/InheritException
11
+ # rubocop:disable Metrics/AbcSize
12
+ # See: https://github.com/mperham/sidekiq/blob/33f5d6b2b6c0dfaab11e5d39688cab7ebadc83ae/lib/sidekiq/cli.rb#L20
13
+ class Shutdown < Interrupt; end
14
+
15
+ class Runner
16
+ include Util
17
+ include Singleton
18
+
19
+ def run(options)
20
+ self_read, self_write = IO.pipe
21
+
22
+ %w(INT TERM USR1 TTIN).each do |sig|
23
+ begin
24
+ trap sig do
25
+ self_write.puts(sig)
26
+ end
27
+ rescue ArgumentError
28
+ puts "Signal #{sig} not supported"
29
+ end
30
+ end
31
+
32
+ loader = EnvironmentLoader.setup_options(options)
33
+
34
+ # When cli args exist, override options in config file
35
+ Shoryuken.options.merge!(options)
36
+
37
+ daemonize(Shoryuken.options)
38
+ write_pid(Shoryuken.options)
39
+
40
+ loader.load
41
+
42
+ initialize_concurrent_logger
43
+
44
+ @launcher = Shoryuken::Launcher.new
45
+
46
+ begin
47
+ @launcher.start
48
+
49
+ while (readable_io = IO.select([self_read]))
50
+ signal = readable_io.first[0].gets.strip
51
+ handle_signal(signal)
52
+ end
53
+ rescue Interrupt
54
+ @launcher.stop!
55
+ exit 0
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def initialize_concurrent_logger
62
+ return unless Shoryuken.logger
63
+
64
+ Concurrent.global_logger = lambda do |level, progname, msg = nil, &block|
65
+ Shoryuken.logger.log(level, msg, progname, &block)
66
+ end
67
+ end
68
+
69
+ def daemonize(options)
70
+ return unless options[:daemon]
71
+
72
+ files_to_reopen = []
73
+ ObjectSpace.each_object(File) do |file|
74
+ files_to_reopen << file unless file.closed?
75
+ end
76
+
77
+ Process.daemon(true, true)
78
+
79
+ files_to_reopen.each do |file|
80
+ begin
81
+ file.reopen file.path, 'a+'
82
+ file.sync = true
83
+ rescue ::Exception
84
+ end
85
+ end
86
+
87
+ [$stdout, $stderr].each do |io|
88
+ File.open(options[:logfile], 'ab') do |f|
89
+ io.reopen(f)
90
+ end
91
+ io.sync = true
92
+ end
93
+ $stdin.reopen('/dev/null')
94
+ end
95
+
96
+ def write_pid(options)
97
+ return unless (path = options[:pidfile])
98
+
99
+ File.open(path, 'w') { |f| f.puts(Process.pid) }
100
+ end
101
+
102
+ def execute_soft_shutdown
103
+ logger.info { 'Received USR1, will soft shutdown down' }
104
+
105
+ @launcher.stop
106
+ exit 0
107
+ end
108
+
109
+ def print_threads_backtrace
110
+ Thread.list.each do |thread|
111
+ logger.info { "Thread TID-#{thread.object_id.to_s(36)} #{thread['label']}" }
112
+ if thread.backtrace
113
+ logger.info { thread.backtrace.join("\n") }
114
+ else
115
+ logger.info { '<no backtrace available>' }
116
+ end
117
+ end
118
+ end
119
+
120
+ def handle_signal(sig)
121
+ case sig
122
+ when 'USR1' then execute_soft_shutdown
123
+ when 'TTIN' then print_threads_backtrace
124
+ else
125
+ logger.info { "Received #{sig}, will shutdown down" }
126
+
127
+ raise Interrupt
128
+ end
129
+ end
130
+ end
131
+ end