Skip to content

Commit ab28e60

Browse files
committed
Backport Task#defer_stop.
1 parent fe8aa06 commit ab28e60

File tree

2 files changed

+103
-0
lines changed

2 files changed

+103
-0
lines changed

lib/async/task.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def initialize(reactor, parent = Task.current?, logger: nil, finished: nil, **op
8484
@logger = logger || @parent.logger
8585

8686
@fiber = make_fiber(&block)
87+
88+
@defer_stop = nil
8789
end
8890

8991
attr :logger
@@ -162,6 +164,13 @@ def stop(later = false)
162164
return
163165
end
164166

167+
# If we are deferring stop...
168+
if @defer_stop == false
169+
# Don't stop now... but update the state so we know we need to stop later.
170+
@defer_stop = true
171+
return false
172+
end
173+
165174
if self.running?
166175
if self.current?
167176
if later
@@ -182,6 +191,41 @@ def stop(later = false)
182191
end
183192
end
184193

194+
# Defer the handling of stop. During the execution of the given block, if a stop is requested, it will be deferred until the block exits. This is useful for ensuring graceful shutdown of servers and other long-running tasks. You should wrap the response handling code in a defer_stop block to ensure that the task is stopped when the response is complete but not before.
195+
#
196+
# You can nest calls to defer_stop, but the stop will only be deferred until the outermost block exits.
197+
#
198+
# If stop is invoked a second time, it will be immediately executed.
199+
#
200+
# @yields {} The block of code to execute.
201+
def defer_stop
202+
# Tri-state variable for controlling stop:
203+
# - nil: defer_stop has not been called.
204+
# - false: defer_stop has been called and we are not stopping.
205+
# - true: defer_stop has been called and we will stop when exiting the block.
206+
if @defer_stop.nil?
207+
# If we are not deferring stop already, we can defer it now:
208+
@defer_stop = false
209+
210+
begin
211+
yield
212+
rescue Stop
213+
# If we are exiting due to a stop, we shouldn't try to invoke stop again:
214+
@defer_stop = nil
215+
raise
216+
ensure
217+
# If we were asked to stop, we should do so now:
218+
if @defer_stop
219+
@defer_stop = nil
220+
self.stop
221+
end
222+
end
223+
else
224+
# If we are deferring stop already, entering it again is a no-op.
225+
yield
226+
end
227+
end
228+
185229
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
186230
# @return [Async::Task]
187231
# @raise [RuntimeError] if task was not {set!} for the current fiber.

spec/async/task_spec.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
require 'async'
2424
require 'async/clock'
25+
require 'async/notification'
2526

2627
RSpec.describe Async::Task do
2728
let(:reactor) {Async::Reactor.new}
@@ -496,4 +497,62 @@ def sleep_forever
496497
expect(apples_task.to_s).to include "complete"
497498
end
498499
end
500+
501+
describe '#defer_stop' do
502+
it "can defer stopping a task" do
503+
child_task = reactor.async do |task|
504+
task.defer_stop do
505+
task.sleep(10)
506+
end
507+
end
508+
509+
reactor.run_once(0)
510+
511+
child_task.stop
512+
expect(child_task).to be_running
513+
514+
child_task.stop
515+
expect(child_task).to be_stopped
516+
end
517+
518+
it "will stop the task if it was deferred" do
519+
condition = Async::Notification.new
520+
521+
child_task = reactor.async do |task|
522+
task.defer_stop do
523+
condition.wait
524+
end
525+
end
526+
527+
reactor.run_once(0)
528+
529+
child_task.stop(true)
530+
expect(child_task).to be_running
531+
532+
reactor.async do
533+
condition.signal
534+
end
535+
536+
reactor.run_once(0)
537+
expect(child_task).to be_stopped
538+
end
539+
540+
it "can defer stop in a deferred stop" do
541+
child_task = reactor.async do |task|
542+
task.defer_stop do
543+
task.defer_stop do
544+
task.sleep(10)
545+
end
546+
end
547+
end
548+
549+
reactor.run_once(0)
550+
551+
child_task.stop
552+
expect(child_task).to be_running
553+
554+
child_task.stop
555+
expect(child_task).to be_stopped
556+
end
557+
end
499558
end

0 commit comments

Comments
 (0)