Skip to content

Commit f61368e

Browse files
committed
Drain in-flight concurrent ActiveJob sends on launcher shutdown
The ShoryukenConcurrentSendAdapter enqueues by scheduling the SQS send on a background future and returning immediately, so jobs enqueued shortly before the process exits (e.g. by a worker during processing) could be silently dropped. PR #1018 added ShoryukenConcurrentSendAdapter#wait_for_pending_sends to drain them, but nothing called it during Shoryuken's own shutdown. Launcher#stop and #stop! now drain the configured ActiveJob adapter after the executor has shut down (so all worker-initiated sends have been issued), bounded by Shoryuken.options[:timeout]. The call is duck-typed and guarded: it is a no-op unless ActiveJob is loaded and its adapter responds to wait_for_pending_sends, and any error is swallowed so a draining hiccup can't break shutdown.
1 parent a663582 commit f61368e

2 files changed

Lines changed: 73 additions & 0 deletions

File tree

lib/shoryuken/launcher.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def stop!
4545

4646
shutdown_executor
4747

48+
drain_pending_active_job_sends
49+
4850
fire_event(:stopped)
4951
end
5052

@@ -62,6 +64,8 @@ def stop
6264

6365
shutdown_executor
6466

67+
drain_pending_active_job_sends
68+
6569
fire_event(:stopped)
6670
end
6771

@@ -103,6 +107,30 @@ def shutdown_executor
103107
executor.kill unless executor.wait_for_termination(Shoryuken.options[:timeout])
104108
end
105109

110+
# Drains in-flight asynchronous ActiveJob sends so jobs enqueued just before
111+
# shutdown (e.g. by a worker during processing) are flushed to SQS instead of
112+
# being dropped when the process exits.
113+
#
114+
# No-op unless ActiveJob is loaded and its configured adapter supports
115+
# draining (i.e. ShoryukenConcurrentSendAdapter). Bounded by the configured
116+
# timeout so it can never block shutdown indefinitely, and any error is
117+
# swallowed so a draining hiccup can't break the shutdown sequence.
118+
#
119+
# @return [void]
120+
def drain_pending_active_job_sends
121+
return unless defined?(::ActiveJob::Base)
122+
123+
adapter = ::ActiveJob::Base.queue_adapter
124+
return unless adapter.respond_to?(:wait_for_pending_sends)
125+
126+
logger.info { 'Draining in-flight ActiveJob sends' }
127+
128+
drained = adapter.wait_for_pending_sends(Shoryuken.options[:timeout])
129+
logger.warn { 'Timed out draining in-flight ActiveJob sends; some may not have been delivered' } unless drained
130+
rescue => e
131+
logger.warn { "Error draining in-flight ActiveJob sends: #{e.class}: #{e.message}" }
132+
end
133+
106134
# Returns the executor for running async operations
107135
#
108136
# Owns a dedicated executor rather than borrowing Concurrent.global_io_executor:

spec/lib/shoryuken/launcher_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,51 @@
118118
end
119119
end
120120

121+
describe 'draining in-flight ActiveJob sends on shutdown' do
122+
let(:adapter) { double('queue_adapter') }
123+
124+
before do
125+
allow(first_group_manager).to receive(:stop_new_dispatching)
126+
allow(first_group_manager).to receive(:await_dispatching_in_progress)
127+
allow(second_group_manager).to receive(:stop_new_dispatching)
128+
allow(second_group_manager).to receive(:await_dispatching_in_progress)
129+
130+
# Make ActiveJob present and point its adapter at our double, regardless of
131+
# whether active_job happens to be loaded by another spec.
132+
stub_const('ActiveJob::Base', Class.new)
133+
allow(ActiveJob::Base).to receive(:queue_adapter).and_return(adapter)
134+
end
135+
136+
it 'drains pending sends on graceful stop when the adapter supports it' do
137+
expect(adapter).to receive(:wait_for_pending_sends).with(Shoryuken.options[:timeout]).and_return(true)
138+
139+
subject.stop
140+
end
141+
142+
it 'drains pending sends on immediate stop! when the adapter supports it' do
143+
expect(adapter).to receive(:wait_for_pending_sends).with(Shoryuken.options[:timeout]).and_return(true)
144+
145+
subject.stop!
146+
end
147+
148+
it 'is a no-op when the adapter does not support draining' do
149+
# adapter is a plain double, so it does not respond to wait_for_pending_sends
150+
expect { subject.stop }.not_to raise_error
151+
end
152+
153+
it 'does not break shutdown when draining raises' do
154+
allow(adapter).to receive(:wait_for_pending_sends).and_raise('drain boom')
155+
156+
expect { subject.stop }.not_to raise_error
157+
end
158+
159+
it 'completes shutdown even if draining times out' do
160+
allow(adapter).to receive(:wait_for_pending_sends).and_return(false)
161+
162+
expect { subject.stop }.not_to raise_error
163+
end
164+
end
165+
121166
describe 'executor ownership' do
122167
context 'when no launcher_executor is configured' do
123168
before { allow(Shoryuken).to receive(:launcher_executor).and_return(nil) }

0 commit comments

Comments
 (0)