From 225ab4f8bc90ffb595665c17b6d95e68344c6c78 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 8 Mar 2026 11:05:29 +1300 Subject: [PATCH 1/2] Rename `Stop` -> `Cancel` and related interfaces. Retain backwards compatibility with appropriate aliases. --- lib/async/barrier.rb | 14 +++- lib/async/cancel.rb | 80 ++++++++++++++++++++ lib/async/node.rb | 24 ++++-- lib/async/scheduler.rb | 13 +++- lib/async/stop.rb | 78 +------------------ lib/async/task.rb | 164 ++++++++++++++++++++++------------------ releases.md | 4 + test/async/condition.rb | 4 +- test/async/node.rb | 6 +- test/async/reactor.rb | 6 +- test/async/task.rb | 12 +-- 11 files changed, 230 insertions(+), 175 deletions(-) create mode 100644 lib/async/cancel.rb diff --git a/lib/async/barrier.rb b/lib/async/barrier.rb index cb353b0c..769b19ac 100644 --- a/lib/async/barrier.rb +++ b/lib/async/barrier.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2019-2025, by Samuel Williams. +# Copyright, 2019-2026, by Samuel Williams. require_relative "list" require_relative "task" @@ -88,14 +88,20 @@ def wait end end - # Stop all tasks held by the barrier. + # Cancel all tasks held by the barrier. # @asynchronous May wait for tasks to finish executing. - def stop + def cancel @tasks.each do |waiting| - waiting.task.stop + waiting.task.cancel end @finished.close end + + # Backward compatibility alias for {#cancel}. + # @deprecated Use {#cancel} instead. + def stop + cancel + end end end diff --git a/lib/async/cancel.rb b/lib/async/cancel.rb new file mode 100644 index 00000000..d06b3bdb --- /dev/null +++ b/lib/async/cancel.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2026, by Samuel Williams. + +module Async + # Raised when a task is explicitly cancelled. + class Cancel < Exception + # Represents the source of the cancel operation. + class Cause < Exception + if RUBY_VERSION >= "3.4" + # @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller. + def self.backtrace + caller_locations(2..-1) + end + else + # @returns [Array(String)] The backtrace of the caller. + def self.backtrace + caller(2..-1) + end + end + + # Create a new cause of the cancel operation, with the given message. + # + # @parameter message [String] The error message. + # @returns [Cause] The cause of the cancel operation. + def self.for(message = "Task was cancelled!") + instance = self.new(message) + instance.set_backtrace(self.backtrace) + return instance + end + end + + if RUBY_VERSION < "3.5" + # Create a new cancel operation. + # + # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise} + # + # @parameter message [String | Hash] The error message or a hash containing the cause. + def initialize(message = "Task was cancelled") + + if message.is_a?(Hash) + @cause = message[:cause] + message = "Task was cancelled" + end + + super(message) + end + + # @returns [Exception] The cause of the cancel operation. + # + # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}, we explicitly capture the cause here. + def cause + super || @cause + end + end + + # Used to defer cancelling the current task until later. + class Later + # Create a new cancel later operation. + # + # @parameter task [Task] The task to cancel later. + # @parameter cause [Exception] The cause of the cancel operation. + def initialize(task, cause = nil) + @task = task + @cause = cause + end + + # @returns [Boolean] Whether the task is alive. + def alive? + true + end + + # Transfer control to the operation - this will cancel the task. + def transfer + @task.cancel(false, cause: @cause) + end + end + end +end diff --git a/lib/async/node.rb b/lib/async/node.rb index 113990c7..6a6e3196 100644 --- a/lib/async/node.rb +++ b/lib/async/node.rb @@ -280,18 +280,24 @@ def terminate return @children.nil? end - # Attempt to stop the current node immediately, including all non-transient children. Invokes {#stop_children} to stop all children. + # Attempt to cancel the current node immediately, including all non-transient children. Invokes {#stop_children} to cancel all children. # - # @parameter later [Boolean] Whether to defer stopping until some point in the future. - def stop(later = false) + # @parameter later [Boolean] Whether to defer cancelling until some point in the future. + def cancel(later = false) # The implementation of this method may defer calling `stop_children`. stop_children(later) end + # Backward compatibility alias for {#cancel}. + # @deprecated Use {#cancel} instead. + def stop(...) + cancel(...) + end + # Attempt to stop all non-transient children. private def stop_children(later = false) @children&.each do |child| - child.stop(later) unless child.transient? + child.cancel(later) unless child.transient? end end @@ -301,11 +307,17 @@ def wait nil end - # Whether the node has been stopped. - def stopped? + # Whether the node has been cancelled. + def cancelled? @children.nil? end + # Backward compatibility alias for {#cancelled?}. + # @deprecated Use {#cancelled?} instead. + def stopped? + cancelled? + end + # Print the hierarchy of the task tree from the given node. # # @parameter out [IO] The output stream to write to. diff --git a/lib/async/scheduler.rb b/lib/async/scheduler.rb index 956bb5b7..68cc8bd1 100644 --- a/lib/async/scheduler.rb +++ b/lib/async/scheduler.rb @@ -179,7 +179,7 @@ def closed? # @returns [String] A description of the scheduler. def to_s - "\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>" + "\#<#{self.description} #{@children&.size || 0} children (#{cancelled? ? 'cancelled' : 'running'})>" end # Interrupt the event loop and cause it to exit. @@ -511,15 +511,20 @@ def run_once(timeout = nil) return false end - # Stop all children, including transient children. + # Cancel all children, including transient children. # # @public Since *Async v1*. - def stop + def cancel @children&.each do |child| - child.stop + child.cancel end end + # Backward compatibility alias for cancel. + def stop + cancel + end + private def run_loop(&block) interrupt = nil diff --git a/lib/async/stop.rb b/lib/async/stop.rb index f83f2e5a..6daf9733 100644 --- a/lib/async/stop.rb +++ b/lib/async/stop.rb @@ -1,82 +1,10 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2025, by Samuel Williams. +# Copyright, 2025-2026, by Samuel Williams. -require "fiber" -require "console" +require_relative "cancel" module Async - # Raised when a task is explicitly stopped. - class Stop < Exception - # Represents the source of the stop operation. - class Cause < Exception - if RUBY_VERSION >= "3.4" - # @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller. - def self.backtrace - caller_locations(2..-1) - end - else - # @returns [Array(String)] The backtrace of the caller. - def self.backtrace - caller(2..-1) - end - end - - # Create a new cause of the stop operation, with the given message. - # - # @parameter message [String] The error message. - # @returns [Cause] The cause of the stop operation. - def self.for(message = "Task was stopped") - instance = self.new(message) - instance.set_backtrace(self.backtrace) - return instance - end - end - - if RUBY_VERSION < "3.5" - # Create a new stop operation. - # - # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise} - # - # @parameter message [String | Hash] The error message or a hash containing the cause. - def initialize(message = "Task was stopped") - if message.is_a?(Hash) - @cause = message[:cause] - message = "Task was stopped" - end - - super(message) - end - - # @returns [Exception] The cause of the stop operation. - # - # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}, we explicitly capture the cause here. - def cause - super || @cause - end - end - - # Used to defer stopping the current task until later. - class Later - # Create a new stop later operation. - # - # @parameter task [Task] The task to stop later. - # @parameter cause [Exception] The cause of the stop operation. - def initialize(task, cause = nil) - @task = task - @cause = cause - end - - # @returns [Boolean] Whether the task is alive. - def alive? - true - end - - # Transfer control to the operation - this will stop the task. - def transfer - @task.stop(false, cause: @cause) - end - end - end + Stop = Cancel end diff --git a/lib/async/task.rb b/lib/async/task.rb index f192f70a..5a848e16 100644 --- a/lib/async/task.rb +++ b/lib/async/task.rb @@ -21,7 +21,7 @@ Fiber.attr_accessor :async_task module Async - # Represents a sequential unit of work, defined by a block, which is executed concurrently with other tasks. A task can be in one of the following states: `initialized`, `running`, `completed`, `failed`, `cancelled` or `stopped`. + # Represents a sequential unit of work, defined by a block, which is executed concurrently with other tasks. A task can be in one of the following states: `initialized`, `running`, `completed`, `failed`, or `cancelled`. # # ```mermaid # stateDiagram-v2 @@ -34,11 +34,11 @@ module Async # Completed --> [*] # Failed --> [*] # - # Running --> Stopped : Stop - # Stopped --> [*] - # Completed --> Stopped : Stop - # Failed --> Stopped : Stop - # Initialized --> Stopped : Stop + # Running --> Cancelled : Cancel + # Cancelled --> [*] + # Completed --> Cancelled : Cancel + # Failed --> Cancelled : Cancel + # Initialized --> Cancelled : Cancel # ``` # # @example Creating a task that sleeps for 1 second. @@ -98,7 +98,7 @@ def initialize(parent = Task.current?, finished: nil, **options, &block) warn("finished: argument with non-false value is deprecated and will be removed.", uplevel: 1, category: :deprecated) if $VERBOSE end - @defer_stop = nil + @defer_cancel = nil # Call this after all state is initialized, as it may call `add_child` which will set the parent and make it visible to the scheduler. super(parent, **options) @@ -183,8 +183,8 @@ def failed? @promise.failed? end - # @returns [Boolean] Whether the task has been stopped. - def stopped? + # @returns [Boolean] Whether the task has been cancelled. + def cancelled? @promise.cancelled? end @@ -198,11 +198,11 @@ def complete? self.completed? end - # @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`. + # @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:cancelled` or `:failed`. def status case @promise.resolved when :cancelled - :stopped + :cancelled when :failed :failed when :completed @@ -260,7 +260,7 @@ def async(*arguments, **options, &block) return task end - # Retrieve the current result of the task. Will cause the caller to wait until result is available. If the task resulted in an unhandled error (derived from `StandardError`), this will be raised. If the task was stopped, this will return `nil`. + # Retrieve the current result of the task. Will cause the caller to wait until result is available. If the task resulted in an unhandled error (derived from `StandardError`), this will be raised. If the task was cancelled, this will return `nil`. # # Conceptually speaking, waiting on a task should return a result, and if it throws an exception, this is certainly an exceptional case that should represent a failure in your program, not an expected outcome. In other words, you should not design your programs to expect exceptions from `#wait` as a normal flow control, and prefer to catch known exceptions within the task itself and return a result that captures the intention of the failure, e.g. a `TimeoutError` might simply return `nil` or `false` to indicate that the operation did not generate a valid result (as a timeout was an expected outcome of the internal operation in this case). # @@ -274,7 +274,7 @@ def wait begin @promise.wait rescue Promise::Cancel - # For backward compatibility, stopped tasks return nil: + # For backward compatibility, cancelled tasks return nil: return nil end end @@ -315,7 +315,7 @@ def wait_all def result value = @promise.value - # For backward compatibility, return nil for stopped tasks: + # For backward compatibility, return nil for cancelled tasks: if @promise.cancelled? nil else @@ -323,103 +323,115 @@ def result end end - # Stop the task and all of its children. + # Cancel the task and all of its children. # - # 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. + # If `later` is false, it means that `cancel` has been invoked directly. When `later` is true, it means that `cancel` is invoked by `stop_children` or some other indirect mechanism. In that case, if we encounter the "current" fiber, we can't cancel it right away, as it's currently performing `#cancel`. Cancelling it immediately would interrupt the current cancel traversal, so we need to schedule the cancel to occur later. # - # @parameter later [Boolean] Whether to stop the task later, or immediately. - # @parameter cause [Exception] The cause of the stop operation. - def stop(later = false, cause: $!) + # @parameter later [Boolean] Whether to cancel the task later, or immediately. + # @parameter cause [Exception] The cause of the cancel operation. + def cancel(later = false, cause: $!) # If no cause is given, we generate one from the current call stack: unless cause - cause = Stop::Cause.for("Stopping task!") + cause = Cancel::Cause.for("Cancelling task!") end - if self.stopped? - # 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. - return stopped! + if self.cancelled? + # If the task is already cancelled, a `cancel` state transition re-enters the same state which is a no-op. However, we will also attempt to cancel any running children too. This can happen if the children did not cancel correctly the first time around. Doing this should probably be considered a bug, but it's better to be safe than sorry. + return cancelled! end - # If the fiber is alive, we need to stop it: + # If the fiber is alive, we need to cancel it: if @fiber&.alive? # As the task is now exiting, we want to ensure the event loop continues to execute until the task finishes. self.transient = false - # If we are deferring stop... - if @defer_stop == false - # Don't stop now... but update the state so we know we need to stop later. - @defer_stop = cause + # If we are deferring cancel... + if @defer_cancel == false + # Don't cancel now... but update the state so we know we need to cancel later. + @defer_cancel = cause return false end if self.current? - # 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`: + # If the fiber is current, and later is `true`, we need to schedule the fiber to be cancelled later, as it's currently invoking `cancel`: if later - # If the fiber is the current fiber and we want to stop it later, schedule it: - Fiber.scheduler.push(Stop::Later.new(self, cause)) + # If the fiber is the current fiber and we want to cancel it later, schedule it: + Fiber.scheduler.push(Cancel::Later.new(self, cause)) else # Otherwise, raise the exception directly: - raise Stop, "Stopping current task!", cause: cause + raise Cancel, "Cancelling current task!", cause: cause end else # If the fiber is not curent, we can raise the exception directly: begin - # 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. - Fiber.scheduler.raise(@fiber, Stop, cause: cause) + # There is a chance that this will cancel the fiber that originally called cancel. If that happens, the exception handling in `#cancelled` will rescue the exception and re-raise it later. + Fiber.scheduler.raise(@fiber, Cancel, cause: cause) rescue FiberError - # In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be stopped later: - Fiber.scheduler.push(Stop::Later.new(self, cause)) + # In some cases, this can cause a FiberError (it might be resumed already), so we schedule it to be cancelled later: + Fiber.scheduler.push(Cancel::Later.new(self, cause)) end end else - # We are not running, but children might be, so transition directly into stopped state: - stop! + # We are not running, but children might be, so transition directly into cancelled state: + cancel! end end - # 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. + # Defer the handling of cancel. During the execution of the given block, if a cancel 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_cancel block to ensure that the task is cancelled when the response is complete but not before. # - # You can nest calls to defer_stop, but the stop will only be deferred until the outermost block exits. + # You can nest calls to defer_cancel, but the cancel will only be deferred until the outermost block exits. # - # If stop is invoked a second time, it will be immediately executed. + # If cancel is invoked a second time, it will be immediately executed. # # @yields {} The block of code to execute. # @public Since *Async v1*. - def defer_stop - # Tri-state variable for controlling stop: - # - nil: defer_stop has not been called. - # - false: defer_stop has been called and we are not stopping. - # - true: defer_stop has been called and we will stop when exiting the block. - if @defer_stop.nil? + def defer_cancel + # Tri-state variable for controlling cancel: + # - nil: defer_cancel has not been called. + # - false: defer_cancel has been called and we are not cancelling. + # - true: defer_cancel has been called and we will cancel when exiting the block. + if @defer_cancel.nil? begin - # If we are not deferring stop already, we can defer it now: - @defer_stop = false + # If we are not deferring cancel already, we can defer it now: + @defer_cancel = false yield - rescue Stop - # If we are exiting due to a stop, we shouldn't try to invoke stop again: - @defer_stop = nil + rescue Cancel + # If we are exiting due to a cancel, we shouldn't try to invoke cancel again: + @defer_cancel = nil raise ensure - defer_stop = @defer_stop + defer_cancel = @defer_cancel # We need to ensure the state is reset before we exit the block: - @defer_stop = nil + @defer_cancel = nil - # If we were asked to stop, we should do so now: - if defer_stop - raise Stop, "Stopping current task (was deferred)!", cause: defer_stop + # If we were asked to cancel, we should do so now: + if defer_cancel + raise Cancel, "Cancelling current task (was deferred)!", cause: defer_cancel end end else - # If we are deferring stop already, entering it again is a no-op. + # If we are deferring cancel already, entering it again is a no-op. yield end end - # @returns [Boolean] Whether stop has been deferred. + # Backward compatibility alias for {#defer_cancel}. + # @deprecated Use {#defer_cancel} instead. + def defer_stop(&block) + defer_cancel(&block) + end + + # @returns [Boolean] Whether cancel has been deferred. + def cancel_deferred? + !!@defer_cancel + end + + # Backward compatibility alias for {#cancel_deferred?}. + # @deprecated Use {#cancel_deferred?} instead. def stop_deferred? - !!@defer_stop + cancel_deferred? end # Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available. @@ -468,40 +480,48 @@ def failed!(exception = false) @promise.reject(exception) end - def stopped! - # Console.info(self, status:) {"Task #{self} was stopped with #{@children&.size.inspect} children!"} + def cancelled! + # Console.info(self, status:) {"Task #{self} was cancelled with #{@children&.size.inspect} children!"} # Cancel the promise: @promise.cancel - stopped = false + cancelled = false begin # We are not running, but children might be so we should stop them: stop_children(true) - rescue Stop - stopped = true - # If we are stopping children, and one of them tries to stop the current task, we should ignore it. We will be stopped later. + rescue Cancel + cancelled = true + # If we are cancelling children, and one of them tries to cancel the current task, we should ignore it. We will be cancelled later. retry end - if stopped - raise Stop, "Stopping current task!" + if cancelled + raise Cancel, "Cancelling current task!" end end - def stop! - stopped! + def stopped! + cancelled! + end + + def cancel! + cancelled! finish! end + def stop! + cancel! + end + def schedule(&block) @fiber = Fiber.new(annotation: self.annotation) do begin completed!(yield) - rescue Stop - stopped! + rescue Cancel + cancelled! rescue StandardError => error failed!(error) rescue Exception => exception diff --git a/releases.md b/releases.md index 2daa3ede..d2aac0cd 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Rename `Task#stop` to `Task#cancel` for better clarity and consistency with common concurrency terminology. The old `stop` method is still available as an alias for backward compatibility, but it is recommended to use `cancel` going forward. + ## v2.37.0 - Introduce `Async::Loop` for robust, time-aligned loops. diff --git a/test/async/condition.rb b/test/async/condition.rb index 46498770..04eebd35 100644 --- a/test/async/condition.rb +++ b/test/async/condition.rb @@ -2,7 +2,7 @@ # Released under the MIT License. # Copyright, 2017, by Kent Gruber. -# Copyright, 2017-2025, by Samuel Williams. +# Copyright, 2017-2026, by Samuel Williams. require "sus/fixtures/async" require "async/condition" @@ -49,7 +49,7 @@ consumer.wait producer.wait - expect(producer.status).to be == :stopped + expect(producer).to be(:cancelled?) expect(consumer.status).to be == :completed end diff --git a/test/async/node.rb b/test/async/node.rb index fc50094e..ebec0662 100644 --- a/test/async/node.rb +++ b/test/async/node.rb @@ -283,10 +283,10 @@ expect(middle.children).to be(:transients?) - expect(child1).not.to receive(:stop) - expect(child2).to receive(:stop) + expect(child1).not.to receive(:cancel) + expect(child2).to receive(:cancel) - node.stop + node.cancel end end diff --git a/test/async/reactor.rb b/test/async/reactor.rb index b2b88092..9c319b7c 100644 --- a/test/async/reactor.rb +++ b/test/async/reactor.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Released under the MIT License. -# Copyright, 2017-2025, by Samuel Williams. +# Copyright, 2017-2026, by Samuel Williams. # Copyright, 2017, by Devin Christensen. require "async" @@ -267,8 +267,8 @@ end with "#to_s" do - it "shows stopped" do - expect(reactor.to_s).to be =~ /stopped/ + it "shows cancelled" do + expect(reactor.to_s).to be =~ /cancelled/ end end diff --git a/test/async/task.rb b/test/async/task.rb index a349769f..37ac33ee 100644 --- a/test/async/task.rb +++ b/test/async/task.rb @@ -237,7 +237,7 @@ task.stop - expect(task.status).to be == :stopped + expect(task).to be(:cancelled?) expect(reactor.children).to be(:empty?) end @@ -255,8 +255,8 @@ parent.stop - expect(parent.status).to be == :stopped - expect(child.status).to be == :stopped + expect(parent).to be(:cancelled?) + expect(child).to be(:cancelled?) expect(reactor.children).to be(:empty?) end @@ -295,7 +295,7 @@ end task.stop - expect(task.status).to be == :stopped + expect(task).to be(:cancelled?) end it "reports failed status for failed tasks" do @@ -379,10 +379,10 @@ parent.stop parent.wait - expect(parent.status).to be == :stopped + expect(parent).to be(:cancelled?) child.wait - expect(child.status).to be == :stopped + expect(child).to be(:cancelled?) end end From ec584b4b4ba8997f3b5a9efe04f16b7f9ca5cf9c Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Sun, 8 Mar 2026 19:18:08 +1300 Subject: [PATCH 2/2] Update documentation. --- context/scheduler.md | 6 ++-- context/tasks.md | 56 +++++++++++++++++++------------------- guides/scheduler/readme.md | 6 ++-- guides/tasks/readme.md | 56 +++++++++++++++++++------------------- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/context/scheduler.md b/context/scheduler.md index 43154083..d88abf30 100644 --- a/context/scheduler.md +++ b/context/scheduler.md @@ -38,7 +38,7 @@ Async do |task| task.print_hierarchy($stderr) # Kill the subtask - subtask.stop + subtask.cancel end ~~~ @@ -69,9 +69,9 @@ end You can use this approach to embed the reactor in another event loop. For some integrations, you may want to specify the maximum time to wait to {ruby Async::Scheduler#run_once}. -### Stopping a Scheduler +### Cancelling a Scheduler -{ruby Async::Scheduler#stop} will stop the current scheduler and all children tasks. +{ruby Async::Scheduler#cancel} will cancel the current scheduler and all children tasks. ### Fiber Scheduler Integration diff --git a/context/tasks.md b/context/tasks.md index 4c048bc4..80d39fae 100644 --- a/context/tasks.md +++ b/context/tasks.md @@ -4,7 +4,7 @@ This guide explains how asynchronous tasks work and how to use them. ## Overview -Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is stopped, it will also stop all its children tasks. The reactor always starts with one root task. +Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is cancelled, it will also cancel all its children tasks. The reactor always starts with one root task. ```mermaid graph LR @@ -23,11 +23,11 @@ graph LR A fiber is a lightweight unit of execution that can be suspended and resumed at specific points. After a fiber is suspended, it can be resumed later at the same point with the same execution state. Because only one fiber can execute at a time, they are often referred to as a mechanism for cooperative concurrency. -A task provides extra functionality on top of fibers. A task behaves like a promise: it either succeeds with a value or fails with an exception. Tasks keep track of their parent-child relationships, and when a parent task is stopped, it will also stop all its children tasks. This makes it easier to create complex programs with many concurrent tasks. +A task provides extra functionality on top of fibers. A task behaves like a promise: it either succeeds with a value or fails with an exception. Tasks keep track of their parent-child relationships, and when a parent task is cancelled, it will also cancel all its children tasks. This makes it easier to create complex programs with many concurrent tasks. ### Why does Async manipulate tasks and not fibers? -The {ruby Async::Scheduler} actually works directly with fibers for most operations and isn't aware of tasks. However, the reactor does maintain a tree of tasks for the purpose of managing task and reactor life-cycle. For example, stopping a parent task will stop all its children tasks, and the reactor will exit when all tasks are finished. +The {ruby Async::Scheduler} actually works directly with fibers for most operations and isn't aware of tasks. However, the reactor does maintain a tree of tasks for the purpose of managing task and reactor life-cycle. For example, cancelling a parent task will cancel all its children tasks, and the reactor will exit when all tasks are finished. ## Task Lifecycle @@ -40,20 +40,20 @@ stateDiagram-v2 running --> failed : unhandled StandardError-derived exception running --> complete : user code finished - running --> stopped : stop + running --> cancelled : cancel - initialized --> stopped : stop + initialized --> cancelled : cancel failed --> [*] complete --> [*] - stopped --> [*] + cancelled --> [*] ``` -Tasks are created in the `initialized` state, and are run by the reactor. During the execution, a task can either `complete` successfully, become `failed` with an unhandled `StandardError`-derived exception, or be explicitly `stopped`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}. +Tasks are created in the `initialized` state, and are run by the reactor. During the execution, a task can either `complete` successfully, become `failed` with an unhandled `StandardError`-derived exception, or be explicitly `cancelled`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}. 1. In the case the task successfully completed, the result will be whatever value was generated by the last expression in the task. 2. In the case the task failed with an unhandled `StandardError`-derived exception, waiting on the task will re-raise the exception. -3. In the case the task was stopped, the result will be `nil`. +3. In the case the task was cancelled, the result will be `nil`. ## Starting A Task @@ -175,8 +175,8 @@ Async do break if done.size >= 2 end ensure - # The remainder of the tasks will be stopped: - barrier.stop + # The remainder of the tasks will be cancelled: + barrier.cancel end end ``` @@ -199,18 +199,18 @@ begin # Wait until all jobs are done: barrier.wait ensure - # Stop any remaining jobs: - barrier.stop + # Cancel any remaining jobs: + barrier.cancel end ~~~ -## Stopping a Task +## Cancelling a Task When a task completes execution, it will enter the `complete` state (or the `failed` state if it raises an unhandled exception). -There are various situations where you may want to stop a task ({ruby Async::Task#stop}) before it completes. The most common case is shutting down a server. A more complex example is this: you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then stop (terminate/cancel) the remaining operations. +There are various situations where you may want to cancel a task ({ruby Async::Task#cancel}) before it completes. The most common case is shutting down a server. A more complex example is this: you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then cancel the remaining operations. -Using the above program as an example, let's stop all the tasks just after the first one completes. +Using the above program as an example, let's cancel all the tasks just after the first one completes. ```ruby Async do @@ -221,14 +221,14 @@ Async do end end - # Stop all the above tasks: - tasks.each(&:stop) + # Cancel all the above tasks: + tasks.each(&:cancel) end ``` -### Stopping all Tasks held in a Barrier +### Cancelling all Tasks held in a Barrier -To stop (terminate/cancel) all the tasks held in a barrier: +To cancel all the tasks held in a barrier: ```ruby barrier = Async::Barrier.new @@ -241,11 +241,11 @@ Async do end end - barrier.stop + barrier.cancel end ``` -Unless your tasks all rescue and suppresses `StandardError`-derived exceptions, be sure to call ({ruby Async::Barrier#stop}) to stop the remaining tasks: +Unless your tasks all rescue and suppresses `StandardError`-derived exceptions, be sure to call ({ruby Async::Barrier#cancel}) to cancel the remaining tasks: ```ruby barrier = Async::Barrier.new @@ -261,7 +261,7 @@ Async do begin barrier.wait ensure - barrier.stop + barrier.cancel end end ``` @@ -273,10 +273,10 @@ In order to ensure your resources are cleaned up correctly, make sure you wrap r ~~~ ruby Async do begin - socket = connect(remote_address) # May raise Async::Stop + socket = connect(remote_address) # May raise Async::Cancel - socket.write(...) # May raise Async::Stop - socket.read(...) # May raise Async::Stop + socket.write(...) # May raise Async::Cancel + socket.read(...) # May raise Async::Cancel ensure socket.close if socket end @@ -398,9 +398,9 @@ end Transient tasks are similar to normal tasks, except for the following differences: -1. They are not considered by {ruby Async::Task#finished?}, so they will not keep the reactor alive. Instead, they are stopped (with a {ruby Async::Stop} exception) when all other (non-transient) tasks are finished. +1. They are not considered by {ruby Async::Task#finished?}, so they will not keep the reactor alive. Instead, they are cancelled (with a {ruby Async::Cancel} exception) when all other (non-transient) tasks are finished. 2. As soon as a parent task is finished, any transient child tasks will be moved up to be children of the parent's parent. This ensures that they never keep a sub-tree alive. -3. Similarly, if you `stop` a task, any transient child tasks will be moved up the tree as above rather than being stopped. +3. Similarly, if you `cancel` a task, any transient child tasks will be moved up the tree as above rather than being cancelled. The purpose of transient tasks is when a task is an implementation detail of an object or instance, rather than a concurrency process. Some examples of transient tasks: @@ -439,7 +439,7 @@ class TimeStringCache sleep(1) end ensure - # When the reactor terminates all tasks, `Async::Stop` will be raised from `sleep` and this code will be invoked. By clearing `@refresh`, we ensure that the task will be recreated if needed again: + # When the reactor terminates all tasks, `Async::Cancel` will be raised from `sleep` and this code will be invoked. By clearing `@refresh`, we ensure that the task will be recreated if needed again: @refresh = nil end end diff --git a/guides/scheduler/readme.md b/guides/scheduler/readme.md index 43154083..d88abf30 100644 --- a/guides/scheduler/readme.md +++ b/guides/scheduler/readme.md @@ -38,7 +38,7 @@ Async do |task| task.print_hierarchy($stderr) # Kill the subtask - subtask.stop + subtask.cancel end ~~~ @@ -69,9 +69,9 @@ end You can use this approach to embed the reactor in another event loop. For some integrations, you may want to specify the maximum time to wait to {ruby Async::Scheduler#run_once}. -### Stopping a Scheduler +### Cancelling a Scheduler -{ruby Async::Scheduler#stop} will stop the current scheduler and all children tasks. +{ruby Async::Scheduler#cancel} will cancel the current scheduler and all children tasks. ### Fiber Scheduler Integration diff --git a/guides/tasks/readme.md b/guides/tasks/readme.md index 4c048bc4..80d39fae 100644 --- a/guides/tasks/readme.md +++ b/guides/tasks/readme.md @@ -4,7 +4,7 @@ This guide explains how asynchronous tasks work and how to use them. ## Overview -Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is stopped, it will also stop all its children tasks. The reactor always starts with one root task. +Tasks are the smallest unit of sequential code execution in {ruby Async}. Tasks can create other tasks, and Async tracks the parent-child relationship between tasks. When a parent task is cancelled, it will also cancel all its children tasks. The reactor always starts with one root task. ```mermaid graph LR @@ -23,11 +23,11 @@ graph LR A fiber is a lightweight unit of execution that can be suspended and resumed at specific points. After a fiber is suspended, it can be resumed later at the same point with the same execution state. Because only one fiber can execute at a time, they are often referred to as a mechanism for cooperative concurrency. -A task provides extra functionality on top of fibers. A task behaves like a promise: it either succeeds with a value or fails with an exception. Tasks keep track of their parent-child relationships, and when a parent task is stopped, it will also stop all its children tasks. This makes it easier to create complex programs with many concurrent tasks. +A task provides extra functionality on top of fibers. A task behaves like a promise: it either succeeds with a value or fails with an exception. Tasks keep track of their parent-child relationships, and when a parent task is cancelled, it will also cancel all its children tasks. This makes it easier to create complex programs with many concurrent tasks. ### Why does Async manipulate tasks and not fibers? -The {ruby Async::Scheduler} actually works directly with fibers for most operations and isn't aware of tasks. However, the reactor does maintain a tree of tasks for the purpose of managing task and reactor life-cycle. For example, stopping a parent task will stop all its children tasks, and the reactor will exit when all tasks are finished. +The {ruby Async::Scheduler} actually works directly with fibers for most operations and isn't aware of tasks. However, the reactor does maintain a tree of tasks for the purpose of managing task and reactor life-cycle. For example, cancelling a parent task will cancel all its children tasks, and the reactor will exit when all tasks are finished. ## Task Lifecycle @@ -40,20 +40,20 @@ stateDiagram-v2 running --> failed : unhandled StandardError-derived exception running --> complete : user code finished - running --> stopped : stop + running --> cancelled : cancel - initialized --> stopped : stop + initialized --> cancelled : cancel failed --> [*] complete --> [*] - stopped --> [*] + cancelled --> [*] ``` -Tasks are created in the `initialized` state, and are run by the reactor. During the execution, a task can either `complete` successfully, become `failed` with an unhandled `StandardError`-derived exception, or be explicitly `stopped`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}. +Tasks are created in the `initialized` state, and are run by the reactor. During the execution, a task can either `complete` successfully, become `failed` with an unhandled `StandardError`-derived exception, or be explicitly `cancelled`. In all of these cases, you can wait for a task to complete by using {ruby Async::Task#wait}. 1. In the case the task successfully completed, the result will be whatever value was generated by the last expression in the task. 2. In the case the task failed with an unhandled `StandardError`-derived exception, waiting on the task will re-raise the exception. -3. In the case the task was stopped, the result will be `nil`. +3. In the case the task was cancelled, the result will be `nil`. ## Starting A Task @@ -175,8 +175,8 @@ Async do break if done.size >= 2 end ensure - # The remainder of the tasks will be stopped: - barrier.stop + # The remainder of the tasks will be cancelled: + barrier.cancel end end ``` @@ -199,18 +199,18 @@ begin # Wait until all jobs are done: barrier.wait ensure - # Stop any remaining jobs: - barrier.stop + # Cancel any remaining jobs: + barrier.cancel end ~~~ -## Stopping a Task +## Cancelling a Task When a task completes execution, it will enter the `complete` state (or the `failed` state if it raises an unhandled exception). -There are various situations where you may want to stop a task ({ruby Async::Task#stop}) before it completes. The most common case is shutting down a server. A more complex example is this: you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then stop (terminate/cancel) the remaining operations. +There are various situations where you may want to cancel a task ({ruby Async::Task#cancel}) before it completes. The most common case is shutting down a server. A more complex example is this: you may fan out multiple (10s, 100s) of requests, wait for a subset to complete (e.g. the first 5 or all those that complete within a given deadline), and then cancel the remaining operations. -Using the above program as an example, let's stop all the tasks just after the first one completes. +Using the above program as an example, let's cancel all the tasks just after the first one completes. ```ruby Async do @@ -221,14 +221,14 @@ Async do end end - # Stop all the above tasks: - tasks.each(&:stop) + # Cancel all the above tasks: + tasks.each(&:cancel) end ``` -### Stopping all Tasks held in a Barrier +### Cancelling all Tasks held in a Barrier -To stop (terminate/cancel) all the tasks held in a barrier: +To cancel all the tasks held in a barrier: ```ruby barrier = Async::Barrier.new @@ -241,11 +241,11 @@ Async do end end - barrier.stop + barrier.cancel end ``` -Unless your tasks all rescue and suppresses `StandardError`-derived exceptions, be sure to call ({ruby Async::Barrier#stop}) to stop the remaining tasks: +Unless your tasks all rescue and suppresses `StandardError`-derived exceptions, be sure to call ({ruby Async::Barrier#cancel}) to cancel the remaining tasks: ```ruby barrier = Async::Barrier.new @@ -261,7 +261,7 @@ Async do begin barrier.wait ensure - barrier.stop + barrier.cancel end end ``` @@ -273,10 +273,10 @@ In order to ensure your resources are cleaned up correctly, make sure you wrap r ~~~ ruby Async do begin - socket = connect(remote_address) # May raise Async::Stop + socket = connect(remote_address) # May raise Async::Cancel - socket.write(...) # May raise Async::Stop - socket.read(...) # May raise Async::Stop + socket.write(...) # May raise Async::Cancel + socket.read(...) # May raise Async::Cancel ensure socket.close if socket end @@ -398,9 +398,9 @@ end Transient tasks are similar to normal tasks, except for the following differences: -1. They are not considered by {ruby Async::Task#finished?}, so they will not keep the reactor alive. Instead, they are stopped (with a {ruby Async::Stop} exception) when all other (non-transient) tasks are finished. +1. They are not considered by {ruby Async::Task#finished?}, so they will not keep the reactor alive. Instead, they are cancelled (with a {ruby Async::Cancel} exception) when all other (non-transient) tasks are finished. 2. As soon as a parent task is finished, any transient child tasks will be moved up to be children of the parent's parent. This ensures that they never keep a sub-tree alive. -3. Similarly, if you `stop` a task, any transient child tasks will be moved up the tree as above rather than being stopped. +3. Similarly, if you `cancel` a task, any transient child tasks will be moved up the tree as above rather than being cancelled. The purpose of transient tasks is when a task is an implementation detail of an object or instance, rather than a concurrency process. Some examples of transient tasks: @@ -439,7 +439,7 @@ class TimeStringCache sleep(1) end ensure - # When the reactor terminates all tasks, `Async::Stop` will be raised from `sleep` and this code will be invoked. By clearing `@refresh`, we ensure that the task will be recreated if needed again: + # When the reactor terminates all tasks, `Async::Cancel` will be raised from `sleep` and this code will be invoked. By clearing `@refresh`, we ensure that the task will be recreated if needed again: @refresh = nil end end