Skip to content

Commit 698707c

Browse files
committed
Add support for stop(cause:).
1 parent f30c2f8 commit 698707c

2 files changed

Lines changed: 48 additions & 11 deletions

File tree

lib/async/task.rb

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,24 @@
1818
module Async
1919
# Raised when a task is explicitly stopped.
2020
class Stop < Exception
21+
# Represents the source of the stop operation.
22+
class Cause < Exception
23+
end
24+
25+
# Create a new stop operation.
26+
def initialize(message = "Task was stopped")
27+
super(message)
28+
end
29+
2130
# Used to defer stopping the current task until later.
2231
class Later
2332
# Create a new stop later operation.
2433
#
2534
# @parameter task [Task] The task to stop later.
26-
def initialize(task)
35+
# @parameter cause [Exception] The cause of the stop operation.
36+
def initialize(task, cause = nil)
2737
@task = task
38+
@cause = cause
2839
end
2940

3041
# @returns [Boolean] Whether the task is alive.
@@ -34,7 +45,7 @@ def alive?
3445

3546
# Transfer control to the operation - this will stop the task.
3647
def transfer
37-
@task.stop
48+
@task.stop(false, cause: @cause)
3849
end
3950
end
4051
end
@@ -266,7 +277,14 @@ def wait
266277
# If `later` is false, it means that `stop` has been invoked directly. When `later` is true, it means that `stop` is invoked by `stop_children` or some other indirect mechanism. In that case, if we encounter the "current" fiber, we can't stop it right away, as it's currently performing `#stop`. Stopping it immediately would interrupt the current stop traversal, so we need to schedule the stop to occur later.
267278
#
268279
# @parameter later [Boolean] Whether to stop the task later, or immediately.
269-
def stop(later = false)
280+
# @parameter cause [Exception] The cause of the stop operation.
281+
def stop(later = false, cause: $!)
282+
# If no cause is given, we generate one from the current call stack:
283+
unless cause
284+
cause = Stop::Cause.new("Stop requested!")
285+
cause.set_backtrace(caller_locations(1..-1))
286+
end
287+
270288
if self.stopped?
271289
# If the task is already stopped, a `stop` state transition re-enters the same state which is a no-op. However, we will also attempt to stop any running children too. This can happen if the children did not stop correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry.
272290
return stopped!
@@ -280,27 +298,27 @@ def stop(later = false)
280298
# If we are deferring stop...
281299
if @defer_stop == false
282300
# Don't stop now... but update the state so we know we need to stop later.
283-
@defer_stop = true
301+
@defer_stop = cause
284302
return false
285303
end
286304

287305
if self.current?
288306
# If the fiber is current, and later is `true`, we need to schedule the fiber to be stopped later, as it's currently invoking `stop`:
289307
if later
290308
# If the fiber is the current fiber and we want to stop it later, schedule it:
291-
Fiber.scheduler.push(Stop::Later.new(self))
309+
Fiber.scheduler.push(Stop::Later.new(self, cause))
292310
else
293311
# Otherwise, raise the exception directly:
294-
raise Stop, "Stopping current task!"
312+
raise Stop, "Stopping current task!", cause: cause
295313
end
296314
else
297315
# If the fiber is not curent, we can raise the exception directly:
298316
begin
299317
# There is a chance that this will stop the fiber that originally called stop. If that happens, the exception handling in `#stopped` will rescue the exception and re-raise it later.
300-
Fiber.scheduler.raise(@fiber, Stop)
318+
Fiber.scheduler.raise(@fiber, Stop, cause: cause)
301319
rescue FiberError
302320
# In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later:
303-
Fiber.scheduler.push(Stop::Later.new(self))
321+
Fiber.scheduler.push(Stop::Later.new(self, cause))
304322
end
305323
end
306324
else
@@ -340,7 +358,7 @@ def defer_stop
340358

341359
# If we were asked to stop, we should do so now:
342360
if defer_stop
343-
raise Stop, "Stopping current task (was deferred)!"
361+
raise Stop, "Stopping current task (was deferred)!", cause: defer_stop
344362
end
345363
end
346364
else
@@ -351,7 +369,7 @@ def defer_stop
351369

352370
# @returns [Boolean] Whether stop has been deferred.
353371
def stop_deferred?
354-
@defer_stop
372+
!!@defer_stop
355373
end
356374

357375
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.

test/async/task.rb

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,25 @@
541541
expect(transient).to be(:running?)
542542
end.wait
543543
end
544+
545+
it "can stop a task and provide a cause" do
546+
cause = StandardError.new("boom")
547+
cause.set_backtrace(caller_locations)
548+
549+
task = reactor.async do |task|
550+
expect(task).to receive(:warn).with_options(have_keys(
551+
exception: be_a(StandardError),
552+
message: be == "boom",
553+
backtrace: be == cause.backtrace,
554+
)).and_return(nil)
555+
556+
task.stop(cause: cause)
557+
end
558+
559+
reactor.run
560+
561+
expect(task).to be(:stopped?)
562+
end
544563
end
545564

546565
with "#sleep" do
@@ -910,7 +929,7 @@ def sleep_forever
910929

911930
reactor.run_once(0)
912931

913-
expect(child_task.stop_deferred?).to be == nil
932+
expect(child_task.stop_deferred?).to be == false
914933
end
915934
end
916935

0 commit comments

Comments
 (0)