8000 Add support for `stop(cause:)`. · socketry/async@53670d1 · GitHub
[go: up one dir, main page]

Skip to content

Commit 53670d1

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

File tree

2 files changed

+69
-11
lines changed

2 files changed

+69
-11
lines changed

lib/async/task.rb

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,45 @@
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+
if RUBY_VERSION >= "3.4"
24+
# @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller.
25+
def self.backtrace
26+
caller_locations(2..-1)
27+
end
28+
else
29+
# @returns [Array(String)] The backtrace of the caller.
30+
def self.backtrace
31+
caller(2..-1)
32+
end
33+
end
34+
35+
# Create a new cause of the stop operation, with the given message.
36+
#
37+
# @parameter message [String] The error message.
38+
# @returns [Cause] The cause of the stop operation.
39+
def self.for(message = "Task was stopped")
40+
instance = self.new(message)
41+
instance.set_backtrace(self.backtrace)
42+
return instance
43+
end
44+
end
45+
46+
# Create a new stop operation.
47+
def initialize(message = "Task was stopped")
48+
super(message)
49+
end
50+
2151
# Used to defer stopping the current task until later.
2252
class Later
2353
# Create a new stop later operation.
2454
#
2555
# @parameter task [Task] The task to stop later.
26-
def initialize(task)
56+
# @parameter cause [Exception] The cause of the stop operation.
57+
def initialize(task, cause = nil)
2758
@task = task
59+
@cause = cause
2860
end
2961

3062
# @returns [Boolean] Whether the task is alive.
@@ -34,7 +66,7 @@ def alive?
3466

3567
# Transfer control to the operation - this will stop the task.
3668
def transfer
37-
@task.stop
69+
@task.stop(false, cause: @cause)
3870
end
3971
end
4072
end
@@ -266,7 +298,13 @@ def wait
266298
# 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.
267299
#
268300
# @parameter later [Boolean] Whether to stop the task later, or immediately.
269-
def stop(later = false)
301+
# @parameter cause [Exception] The cause of the stop operation.
302+
def stop(later = false, cause: $!)
303+
# If no cause is given, we generate one from the current call stack:
304+
unless cause
305+
cause = Stop::Cause.for("Stopping task!")
306+
end
307+
270308
if self.stopped?
271309
# 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.
272310
return stopped!
@@ -280,27 +318,27 @@ def stop(later = false)
280318
# If we are deferring stop...
281319
if @defer_stop == false
282320
# Don't stop now... but update the state so we know we need to stop later.
283-
@defer_stop = true
321+
@defer_stop = cause
284322
return false
285323
end
286324

287325
if self.current?
288326
# 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`:
289327
if later
290328
# If the fiber is the current fiber and we want to stop it later, schedule it:
291-
Fiber.scheduler.push(Stop::Later.new(self))
329+
Fiber.scheduler.push(Stop::Later.new(self, cause))
292330
else
293331
# Otherwise, raise the exception directly:
294-
raise Stop, "Stopping current task!"
332+
raise Stop, "Stopping current task!", cause: cause
295333
end
296334
else
297335
# If the fiber is not curent, we can raise the exception directly:
298336
begin
299337
# 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)
338+
Fiber.scheduler.raise(@fiber, Stop, cause: cause)
301339
rescue FiberError
302340
# 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))
341+
Fiber.scheduler.push(Stop::Later.new(self, cause))
304342
end
305343
end
306344
else
@@ -340,7 +378,7 @@ def defer_stop
340378

341379
# If we were asked to stop, we should do so now:
342380
if defer_stop
343-
raise Stop, "Stopping current task (was deferred)!"
381+
raise Stop, "Stopping current task (was deferred)!", cause: defer_stop
344382
end
345383
end
346384
else
@@ -351,7 +389,7 @@ def defer_stop
351389

352390
# @returns [Boolean] Whether stop has been deferred.
353391
def stop_deferred?
354-
@defer_stop
392+
!!@defer_stop
355393
end
356394

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

test/async/task.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,26 @@
541541
expect(transient).to be(:running?)
542542
end.wait
543543
end
544+
545+
it "can stop a task and provide a cause" do
546+
error = nil
547+
548+
cause = Async::Stop::Cause.for("boom")
549+
550+
task = reactor.async do |task|
551+
begin
552+
task.stop(cause: cause)
553+
rescue Async::Stop => error
554+
raise
555+
end
556+
end
557+
558+
reactor.run
559+
560+
expect(task).to be(:stopped?)
561+
expect(error).to be_a(Async::Stop)
562+
expect(error.cause).to be == cause
563+
end
544564
end
545565

546566
with "#sleep" do
@@ -910,7 +930,7 @@ def sleep_forever
910930

911931
reactor.run_once(0)
912932

913-
expect(child_task.stop_deferred?).to be == nil
933+
expect(child_task.stop_deferred?).to be == false
914934
end
915935
end
916936

0 commit comments

Comments
 (0)
0