This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2017-2024, by Samuel Williams.
|
||||
# Copyright, 2020, by Salim Semaoune.
|
||||
|
||||
require_relative "async/version"
|
||||
require_relative "async/reactor"
|
||||
|
||||
require_relative "kernel/async"
|
||||
require_relative "kernel/sync"
|
||||
|
||||
# Asynchronous programming framework.
|
||||
module Async
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
A synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
~~~ ruby
|
||||
require 'async'
|
||||
require 'async/barrier'
|
||||
|
||||
barrier = Async::Barrier.new
|
||||
Sync do
|
||||
Console.info("Barrier Example: sleep sort.")
|
||||
|
||||
# Generate an array of 10 numbers:
|
||||
numbers = 10.times.map{rand(10)}
|
||||
sorted = []
|
||||
|
||||
# Sleep sort the numbers:
|
||||
numbers.each do |number|
|
||||
barrier.async do |task|
|
||||
sleep(number)
|
||||
sorted << number
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for all the numbers to be sorted:
|
||||
barrier.wait
|
||||
|
||||
Console.info("Sorted", sorted)
|
||||
ensure
|
||||
# Ensure all the tasks are stopped when we exit:
|
||||
barrier.stop
|
||||
end
|
||||
~~~
|
||||
|
||||
### Output
|
||||
|
||||
~~~
|
||||
0.0s info: Barrier Example: sleep sort.
|
||||
9.0s info: Sorted
|
||||
| [3, 3, 3, 4, 4, 5, 5, 5, 8, 9]
|
||||
~~~
|
||||
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2019-2024, by Samuel Williams.
|
||||
|
||||
require_relative "list"
|
||||
require_relative "task"
|
||||
|
||||
module Async
|
||||
# A general purpose synchronisation primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
class Barrier
|
||||
# Initialize the barrier.
|
||||
# @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
|
||||
# @public Since *Async v1*.
|
||||
def initialize(parent: nil)
|
||||
@tasks = List.new
|
||||
|
||||
@parent = parent
|
||||
end
|
||||
|
||||
class TaskNode < List::Node
|
||||
def initialize(task)
|
||||
@task = task
|
||||
end
|
||||
|
||||
attr :task
|
||||
end
|
||||
|
||||
private_constant :TaskNode
|
||||
|
||||
# Number of tasks being held by the barrier.
|
||||
def size
|
||||
@tasks.size
|
||||
end
|
||||
|
||||
# All tasks which have been invoked into the barrier.
|
||||
attr :tasks
|
||||
|
||||
# Execute a child task and add it to the barrier.
|
||||
# @asynchronous Executes the given block concurrently.
|
||||
def async(*arguments, parent: (@parent or Task.current), **options, &block)
|
||||
task = parent.async(*arguments, **options, &block)
|
||||
|
||||
@tasks.append(TaskNode.new(task))
|
||||
|
||||
return task
|
||||
end
|
||||
|
||||
# Whether there are any tasks being held by the barrier.
|
||||
# @returns [Boolean]
|
||||
def empty?
|
||||
@tasks.empty?
|
||||
end
|
||||
|
||||
# Wait for all tasks to complete by invoking {Task#wait} on each waiting task, which may raise an error. As long as the task has completed, it will be removed from the barrier.
|
||||
# @asynchronous Will wait for tasks to finish executing.
|
||||
def wait
|
||||
@tasks.each do |waiting|
|
||||
task = waiting.task
|
||||
begin
|
||||
task.wait
|
||||
ensure
|
||||
@tasks.remove?(waiting) unless task.alive?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Stop all tasks held by the barrier.
|
||||
# @asynchronous May wait for tasks to finish executing.
|
||||
def stop
|
||||
@tasks.each do |waiting|
|
||||
waiting.task.stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2018-2022, by Samuel Williams.
|
||||
|
||||
module Async
|
||||
# A convenient wrapper around the internal monotonic clock.
|
||||
# @public Since *Async v1*.
|
||||
class Clock
|
||||
# Get the current elapsed monotonic time.
|
||||
def self.now
|
||||
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
|
||||
# Measure the execution of a block of code.
|
||||
# @yields {...} The block to execute.
|
||||
# @returns [Numeric] The total execution time.
|
||||
def self.measure
|
||||
start_time = self.now
|
||||
|
||||
yield
|
||||
|
||||
return self.now - start_time
|
||||
end
|
||||
|
||||
# Start measuring elapsed time from now.
|
||||
# @returns [Clock]
|
||||
def self.start
|
||||
self.new.tap(&:start!)
|
||||
end
|
||||
|
||||
# Create a new clock with the initial total time.
|
||||
# @parameter total [Numeric] The initial clock duration.
|
||||
def initialize(total = 0)
|
||||
@total = total
|
||||
@started = nil
|
||||
end
|
||||
|
||||
# Start measuring a duration.
|
||||
def start!
|
||||
@started ||= Clock.now
|
||||
end
|
||||
|
||||
# Stop measuring a duration and append the duration to the current total.
|
||||
def stop!
|
||||
if @started
|
||||
@total += (Clock.now - @started)
|
||||
@started = nil
|
||||
end
|
||||
|
||||
return @total
|
||||
end
|
||||
|
||||
# The total elapsed time including any current duration.
|
||||
def total
|
||||
total = @total
|
||||
|
||||
if @started
|
||||
total += (Clock.now - @started)
|
||||
end
|
||||
|
||||
return total
|
||||
end
|
||||
|
||||
# Reset the total elapsed time. If the clock is currently running, reset the start time to now.
|
||||
def reset!
|
||||
@total = 0
|
||||
|
||||
if @started
|
||||
@started = Clock.now
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered. Zero or more fibers can wait on a condition. When the condition is signalled, the fibers will be resumed in order.
|
||||
|
||||
## Example
|
||||
|
||||
~~~ ruby
|
||||
require 'async'
|
||||
|
||||
Sync do
|
||||
condition = Async::Condition.new
|
||||
|
||||
Async do
|
||||
Console.info "Waiting for condition..."
|
||||
value = condition.wait
|
||||
Console.info "Condition was signalled: #{value}"
|
||||
end
|
||||
|
||||
Async do |task|
|
||||
sleep(1)
|
||||
Console.info "Signalling condition..."
|
||||
condition.signal("Hello World")
|
||||
end
|
||||
end
|
||||
~~~
|
||||
|
||||
### Output
|
||||
|
||||
~~~
|
||||
0.0s info: Waiting for condition...
|
||||
1.0s info: Signalling condition...
|
||||
1.0s info: Condition was signalled: Hello World
|
||||
~~~
|
||||
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2017-2024, by Samuel Williams.
|
||||
# Copyright, 2017, by Kent Gruber.
|
||||
|
||||
require "fiber"
|
||||
require_relative "list"
|
||||
|
||||
module Async
|
||||
# A synchronization primitive, which allows fibers to wait until a particular condition is (edge) triggered.
|
||||
# @public Since *Async v1*.
|
||||
class Condition
|
||||
# Create a new condition.
|
||||
def initialize
|
||||
@waiting = List.new
|
||||
end
|
||||
|
||||
class FiberNode < List::Node
|
||||
def initialize(fiber)
|
||||
@fiber = fiber
|
||||
end
|
||||
|
||||
def transfer(*arguments)
|
||||
@fiber.transfer(*arguments)
|
||||
end
|
||||
|
||||
def alive?
|
||||
@fiber.alive?
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :FiberNode
|
||||
|
||||
# Queue up the current fiber and wait on yielding the task.
|
||||
# @returns [Object]
|
||||
def wait
|
||||
@waiting.stack(FiberNode.new(Fiber.current)) do
|
||||
Fiber.scheduler.transfer
|
||||
end
|
||||
end
|
||||
|
||||
# @deprecated Replaced by {#waiting?}
|
||||
def empty?
|
||||
@waiting.empty?
|
||||
end
|
||||
|
||||
# @returns [Boolean] Is any fiber waiting on this notification?
|
||||
def waiting?
|
||||
@waiting.size > 0
|
||||
end
|
||||
|
||||
# Signal to a given task that it should resume operations.
|
||||
# @parameter value [Object | Nil] The value to return to the waiting fibers.
|
||||
def signal(value = nil)
|
||||
return if @waiting.empty?
|
||||
|
||||
waiting = self.exchange
|
||||
|
||||
waiting.each do |fiber|
|
||||
Fiber.scheduler.resume(fiber, value) if fiber.alive?
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def exchange
|
||||
waiting = @waiting
|
||||
@waiting = List.new
|
||||
return waiting
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
module Async
|
||||
# Shims for the console gem, redirecting warnings and above to `Kernel#warn`.
|
||||
#
|
||||
# If you require this file, the `async` library will not depend on the `console` gem.
|
||||
#
|
||||
# That includes any gems that sit within the `Async` namespace.
|
||||
#
|
||||
# This is an experimental feature.
|
||||
module Console
|
||||
# Log a message at the debug level. The shim is silent.
|
||||
def self.debug(...)
|
||||
end
|
||||
|
||||
# Log a message at the info level. The shim is silent.
|
||||
def self.info(...)
|
||||
end
|
||||
|
||||
# Log a message at the warn level. The shim redirects to `Kernel#warn`.
|
||||
def self.warn(*arguments, exception: nil, **options)
|
||||
if exception
|
||||
super(*arguments, exception.full_message, **options)
|
||||
else
|
||||
super(*arguments, **options)
|
||||
end
|
||||
end
|
||||
|
||||
# Log a message at the error level. The shim redirects to `Kernel#warn`.
|
||||
def self.error(...)
|
||||
self.warn(...)
|
||||
end
|
||||
|
||||
# Log a message at the fatal level. The shim redirects to `Kernel#warn`.
|
||||
def self.fatal(...)
|
||||
self.warn(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
module Async
|
||||
# A load balancing mechanism that can be used process work when the system is idle.
|
||||
class Idler
|
||||
# Create a new idler.
|
||||
#
|
||||
# @public Since *Async v2*.
|
||||
#
|
||||
# @parameter maximum_load [Numeric] The maximum load before we start shedding work.
|
||||
# @parameter backoff [Numeric] The initial backoff time, used for delaying work.
|
||||
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
||||
def initialize(maximum_load = 0.8, backoff: 0.01, parent: nil)
|
||||
@maximum_load = maximum_load
|
||||
@backoff = backoff
|
||||
@parent = parent
|
||||
end
|
||||
|
||||
# Wait until the system is idle, then execute the given block in a new task.
|
||||
#
|
||||
# @asynchronous Executes the given block concurrently.
|
||||
#
|
||||
# @parameter arguments [Array] The arguments to pass to the block.
|
||||
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
||||
# @parameter options [Hash] The options to pass to the task.
|
||||
# @yields {|task| ...} When the system is idle, the block will be executed in a new task.
|
||||
def async(*arguments, parent: (@parent or Task.current), **options, &block)
|
||||
wait
|
||||
|
||||
# It is crucial that we optimistically execute the child task, so that we prevent a tight loop invoking this method from consuming all available resources.
|
||||
parent.async(*arguments, **options, &block)
|
||||
end
|
||||
|
||||
# Wait until the system is idle, according to the maximum load specified.
|
||||
#
|
||||
# If the scheduler is overloaded, this method will sleep for an exponentially increasing amount of time.
|
||||
def wait
|
||||
scheduler = Fiber.scheduler
|
||||
backoff = nil
|
||||
|
||||
while true
|
||||
load = scheduler.load
|
||||
|
||||
break if load < @maximum_load
|
||||
|
||||
if backoff
|
||||
sleep(backoff)
|
||||
backoff *= 2.0
|
||||
else
|
||||
scheduler.yield
|
||||
backoff = @backoff
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
# The implementation lives in `queue.rb` but later we may move it here for better autoload/inference.
|
||||
require_relative "queue"
|
||||
@@ -0,0 +1,311 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2022-2024, by Samuel Williams.
|
||||
|
||||
module Async
|
||||
# A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
|
||||
class List
|
||||
# Initialize a new, empty, list.
|
||||
def initialize
|
||||
@head = self
|
||||
@tail = self
|
||||
@size = 0
|
||||
end
|
||||
|
||||
# @returns [String] A short summary of the list.
|
||||
def to_s
|
||||
sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
|
||||
end
|
||||
|
||||
alias inspect to_s
|
||||
|
||||
# Fast, safe, unbounded accumulation of children.
|
||||
def to_a
|
||||
items = []
|
||||
current = self
|
||||
|
||||
while current.tail != self
|
||||
unless current.tail.is_a?(Iterator)
|
||||
items << current.tail
|
||||
end
|
||||
|
||||
current = current.tail
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
# @attribute [Node | Nil] Points at the end of the list.
|
||||
attr_accessor :head
|
||||
|
||||
# @attribute [Node | Nil] Points at the start of the list.
|
||||
attr_accessor :tail
|
||||
|
||||
# @attribute [Integer] The number of nodes in the list.
|
||||
attr :size
|
||||
|
||||
# A callback that is invoked when an item is added to the list.
|
||||
def added(node)
|
||||
@size += 1
|
||||
return node
|
||||
end
|
||||
|
||||
# Append a node to the end of the list.
|
||||
def append(node)
|
||||
if node.head
|
||||
raise ArgumentError, "Node is already in a list!"
|
||||
end
|
||||
|
||||
node.tail = self
|
||||
@head.tail = node
|
||||
node.head = @head
|
||||
@head = node
|
||||
|
||||
return added(node)
|
||||
end
|
||||
|
||||
# Prepend a node to the start of the list.
|
||||
def prepend(node)
|
||||
if node.head
|
||||
raise ArgumentError, "Node is already in a list!"
|
||||
end
|
||||
|
||||
node.head = self
|
||||
@tail.head = node
|
||||
node.tail = @tail
|
||||
@tail = node
|
||||
|
||||
return added(node)
|
||||
end
|
||||
|
||||
# Add the node, yield, and the remove the node.
|
||||
# @yields {|node| ...} Yields the node.
|
||||
# @returns [Object] Returns the result of the block.
|
||||
def stack(node, &block)
|
||||
append(node)
|
||||
return yield(node)
|
||||
ensure
|
||||
remove!(node)
|
||||
end
|
||||
|
||||
# A callback that is invoked when an item is removed from the list.
|
||||
def removed(node)
|
||||
@size -= 1
|
||||
return node
|
||||
end
|
||||
|
||||
# Remove the node if it is in a list.
|
||||
#
|
||||
# You should be careful to only remove nodes that are part of this list.
|
||||
#
|
||||
# @returns [Node] Returns the node if it was removed, otherwise nil.
|
||||
def remove?(node)
|
||||
if node.head
|
||||
return remove!(node)
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
# Remove the node. If it was already removed, this will raise an error.
|
||||
#
|
||||
# You should be careful to only remove nodes that are part of this list.
|
||||
#
|
||||
# @raises [ArgumentError] If the node is not part of this list.
|
||||
# @returns [Node] Returns the node if it was removed, otherwise nil.
|
||||
def remove(node)
|
||||
# One downside of this interface is we don't actually check if the node is part of the list defined by `self`. This means that there is a potential for a node to be removed from a different list using this method, which in can throw off book-keeping when lists track size, etc.
|
||||
unless node.head
|
||||
raise ArgumentError, "Node is not in a list!"
|
||||
end
|
||||
|
||||
remove!(node)
|
||||
end
|
||||
|
||||
private def remove!(node)
|
||||
node.head.tail = node.tail
|
||||
node.tail.head = node.head
|
||||
|
||||
# This marks the node as being removed, and causes remove to fail if called a 2nd time.
|
||||
node.head = nil
|
||||
# node.tail = nil
|
||||
|
||||
return removed(node)
|
||||
end
|
||||
|
||||
# @returns [Boolean] Returns true if the list is empty.
|
||||
def empty?
|
||||
@size == 0
|
||||
end
|
||||
|
||||
# def validate!(node = nil)
|
||||
# previous = self
|
||||
# current = @tail
|
||||
# found = node.equal?(self)
|
||||
|
||||
# while true
|
||||
# break if current.equal?(self)
|
||||
|
||||
# if current.head != previous
|
||||
# raise "Invalid previous linked list node!"
|
||||
# end
|
||||
|
||||
# if current.is_a?(List) and !current.equal?(self)
|
||||
# raise "Invalid list in list node!"
|
||||
# end
|
||||
|
||||
# if node
|
||||
# found ||= current.equal?(node)
|
||||
# end
|
||||
|
||||
# previous = current
|
||||
# current = current.tail
|
||||
# end
|
||||
|
||||
# if node and !found
|
||||
# raise "Node not found in list!"
|
||||
# end
|
||||
# end
|
||||
|
||||
# Iterate over each node in the linked list. It is generally safe to remove the current node, any previous node or any future node during iteration.
|
||||
#
|
||||
# @yields {|node| ...} Yields each node in the list.
|
||||
# @returns [List] Returns self.
|
||||
def each(&block)
|
||||
return to_enum unless block_given?
|
||||
|
||||
Iterator.each(self, &block)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
# Determine whether the given node is included in the list.
|
||||
#
|
||||
# @parameter needle [Node] The node to search for.
|
||||
# @returns [Boolean] Returns true if the node is in the list.
|
||||
def include?(needle)
|
||||
self.each do |item|
|
||||
return true if needle.equal?(item)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
# @returns [Node] Returns the first node in the list, if it is not empty.
|
||||
def first
|
||||
# validate!
|
||||
|
||||
current = @tail
|
||||
|
||||
while !current.equal?(self)
|
||||
if current.is_a?(Iterator)
|
||||
current = current.tail
|
||||
else
|
||||
return current
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
# @returns [Node] Returns the last node in the list, if it is not empty.
|
||||
def last
|
||||
# validate!
|
||||
|
||||
current = @head
|
||||
|
||||
while !current.equal?(self)
|
||||
if current.is_a?(Iterator)
|
||||
current = current.head
|
||||
else
|
||||
return current
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
# Shift the first node off the list, if it is not empty.
|
||||
def shift
|
||||
if node = first
|
||||
remove!(node)
|
||||
end
|
||||
end
|
||||
|
||||
# A linked list Node.
|
||||
class Node
|
||||
attr_accessor :head
|
||||
attr_accessor :tail
|
||||
|
||||
alias inspect to_s
|
||||
end
|
||||
|
||||
class Iterator < Node
|
||||
def initialize(list)
|
||||
@list = list
|
||||
|
||||
# Insert the iterator as the first item in the list:
|
||||
@tail = list.tail
|
||||
@tail.head = self
|
||||
list.tail = self
|
||||
@head = list
|
||||
end
|
||||
|
||||
def remove!
|
||||
@head.tail = @tail
|
||||
@tail.head = @head
|
||||
@head = nil
|
||||
@tail = nil
|
||||
@list = nil
|
||||
end
|
||||
|
||||
def move_next
|
||||
# Move to the next item (which could be an iterator or the end):
|
||||
@tail.head = @head
|
||||
@head.tail = @tail
|
||||
@head = @tail
|
||||
@tail = @tail.tail
|
||||
@head.tail = self
|
||||
@tail.head = self
|
||||
end
|
||||
|
||||
def move_current
|
||||
while true
|
||||
# Are we at the end of the list?
|
||||
if @tail.equal?(@list)
|
||||
return nil
|
||||
end
|
||||
|
||||
if @tail.is_a?(Iterator)
|
||||
move_next
|
||||
else
|
||||
return @tail
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def each
|
||||
while current = move_current
|
||||
yield current
|
||||
|
||||
if current.equal?(@tail)
|
||||
move_next
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.each(list, &block)
|
||||
return if list.empty?
|
||||
|
||||
iterator = Iterator.new(list)
|
||||
|
||||
iterator.each(&block)
|
||||
ensure
|
||||
iterator&.remove!
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Iterator
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,324 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2017-2024, by Samuel Williams.
|
||||
# Copyright, 2017, by Kent Gruber.
|
||||
# Copyright, 2022, by Shannon Skipper.
|
||||
|
||||
require "fiber/annotation"
|
||||
|
||||
require_relative "list"
|
||||
|
||||
module Async
|
||||
# A list of children tasks.
|
||||
class Children < List
|
||||
# Create an empty list of children tasks.
|
||||
def initialize
|
||||
super
|
||||
@transient_count = 0
|
||||
end
|
||||
|
||||
# Some children may be marked as transient. Transient children do not prevent the parent from finishing.
|
||||
# @returns [Boolean] Whether the node has transient children.
|
||||
def transients?
|
||||
@transient_count > 0
|
||||
end
|
||||
|
||||
# Whether all children are considered finished. Ignores transient children.
|
||||
def finished?
|
||||
@size == @transient_count
|
||||
end
|
||||
|
||||
# Whether the children is empty, preserved for compatibility.
|
||||
def nil?
|
||||
empty?
|
||||
end
|
||||
|
||||
# Adjust the number of transient children, assuming it has changed.
|
||||
#
|
||||
# Despite being public, this is not intended to be called directly. It is used internally by {Node#transient=}.
|
||||
#
|
||||
# @parameter transient [Boolean] Whether to increment or decrement the transient count.
|
||||
def adjust_transient_count(transient)
|
||||
if transient
|
||||
@transient_count += 1
|
||||
else
|
||||
@transient_count -= 1
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def added(node)
|
||||
if node.transient?
|
||||
@transient_count += 1
|
||||
end
|
||||
|
||||
return super
|
||||
end
|
||||
|
||||
def removed(node)
|
||||
if node.transient?
|
||||
@transient_count -= 1
|
||||
end
|
||||
|
||||
return super
|
||||
end
|
||||
end
|
||||
|
||||
# A node in a tree, used for implementing the task hierarchy.
|
||||
class Node
|
||||
# Create a new node in the tree.
|
||||
# @parameter parent [Node | Nil] This node will attach to the given parent.
|
||||
def initialize(parent = nil, annotation: nil, transient: false)
|
||||
@parent = nil
|
||||
@children = nil
|
||||
|
||||
@annotation = annotation
|
||||
@object_name = nil
|
||||
|
||||
@transient = transient
|
||||
|
||||
@head = nil
|
||||
@tail = nil
|
||||
|
||||
if parent
|
||||
parent.add_child(self)
|
||||
end
|
||||
end
|
||||
|
||||
# @returns [Node] The root node in the hierarchy.
|
||||
def root
|
||||
@parent&.root || self
|
||||
end
|
||||
|
||||
# @private
|
||||
attr_accessor :head
|
||||
|
||||
# @private
|
||||
attr_accessor :tail
|
||||
|
||||
# @attribute [Node] The parent node.
|
||||
attr :parent
|
||||
|
||||
# @attribute [Children | Nil] Optional list of children.
|
||||
attr :children
|
||||
|
||||
# @attribute [String | Nil] A useful identifier for the current node.
|
||||
attr :annotation
|
||||
|
||||
# Whether this node has any children.
|
||||
# @returns [Boolean]
|
||||
def children?
|
||||
@children && !@children.empty?
|
||||
end
|
||||
|
||||
# Represents whether a node is transient. Transient nodes are not considered
|
||||
# when determining if a node is finished. This is useful for tasks which are
|
||||
# internal to an object rather than explicit user concurrency. For example,
|
||||
# a child task which is pruning a connection pool is transient, because it
|
||||
# is not directly related to the parent task, and should not prevent the
|
||||
# parent task from finishing.
|
||||
def transient?
|
||||
@transient
|
||||
end
|
||||
|
||||
# Change the transient state of the node.
|
||||
#
|
||||
# A transient node is not considered when determining if a node is finished, and propagates up if the parent is consumed.
|
||||
#
|
||||
# @parameter value [Boolean] Whether the node is transient.
|
||||
def transient=(value)
|
||||
if @transient != value
|
||||
@transient = value
|
||||
|
||||
@parent&.children&.adjust_transient_count(value)
|
||||
end
|
||||
end
|
||||
|
||||
# Annotate the node with a description.
|
||||
#
|
||||
# @parameter annotation [String] The description to annotate the node with.
|
||||
def annotate(annotation)
|
||||
if block_given?
|
||||
begin
|
||||
current_annotation = @annotation
|
||||
@annotation = annotation
|
||||
return yield
|
||||
ensure
|
||||
@annotation = current_annotation
|
||||
end
|
||||
else
|
||||
@annotation = annotation
|
||||
end
|
||||
end
|
||||
|
||||
# A description of the node, including the annotation and object name.
|
||||
#
|
||||
# @returns [String] The description of the node.
|
||||
def description
|
||||
@object_name ||= "#{self.class}:#{format '%#018x', object_id}#{@transient ? ' transient' : nil}"
|
||||
|
||||
if annotation = self.annotation
|
||||
"#{@object_name} #{annotation}"
|
||||
elsif line = self.backtrace(0, 1)&.first
|
||||
"#{@object_name} #{line}"
|
||||
else
|
||||
@object_name
|
||||
end
|
||||
end
|
||||
|
||||
# Provides a backtrace for nodes that have an active execution context.
|
||||
#
|
||||
# @returns [Array(Thread::Backtrace::Locations) | Nil] The backtrace of the node, if available.
|
||||
def backtrace(*arguments)
|
||||
nil
|
||||
end
|
||||
|
||||
# @returns [String] A description of the node.
|
||||
def to_s
|
||||
"\#<#{self.description}>"
|
||||
end
|
||||
|
||||
alias inspect to_s
|
||||
|
||||
# Change the parent of this node.
|
||||
#
|
||||
# @parameter parent [Node | Nil] The parent to attach to, or nil to detach.
|
||||
# @returns [Node] Itself.
|
||||
def parent=(parent)
|
||||
return if @parent.equal?(parent)
|
||||
|
||||
if @parent
|
||||
@parent.remove_child(self)
|
||||
@parent = nil
|
||||
end
|
||||
|
||||
if parent
|
||||
parent.add_child(self)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
protected def set_parent(parent)
|
||||
@parent = parent
|
||||
end
|
||||
|
||||
protected def add_child(child)
|
||||
@children ||= Children.new
|
||||
@children.append(child)
|
||||
child.set_parent(self)
|
||||
end
|
||||
|
||||
protected def remove_child(child)
|
||||
@children.remove(child)
|
||||
child.set_parent(nil)
|
||||
end
|
||||
|
||||
# Whether the node can be consumed (deleted) safely. By default, checks if the children set is empty.
|
||||
#
|
||||
# @returns [Boolean]
|
||||
def finished?
|
||||
@children.nil? || @children.finished?
|
||||
end
|
||||
|
||||
# If the node has a parent, and is {finished?}, then remove this node from
|
||||
# the parent.
|
||||
def consume
|
||||
if parent = @parent and finished?
|
||||
parent.remove_child(self)
|
||||
|
||||
# If we have children, then we need to move them to our the parent if they are not finished:
|
||||
if @children
|
||||
while child = @children.shift
|
||||
if child.finished?
|
||||
child.set_parent(nil)
|
||||
else
|
||||
parent.add_child(child)
|
||||
end
|
||||
end
|
||||
|
||||
@children = nil
|
||||
end
|
||||
|
||||
parent.consume
|
||||
end
|
||||
end
|
||||
|
||||
# Traverse the task tree.
|
||||
#
|
||||
# @returns [Enumerator] An enumerator which will traverse the tree if no block is given.
|
||||
# @yields {|node, level| ...} The node and the level relative to the given root.
|
||||
def traverse(&block)
|
||||
return enum_for(:traverse) unless block_given?
|
||||
|
||||
self.traverse_recurse(&block)
|
||||
end
|
||||
|
||||
protected def traverse_recurse(level = 0, &block)
|
||||
yield self, level
|
||||
|
||||
@children&.each do |child|
|
||||
child.traverse_recurse(level + 1, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# Immediately terminate all children tasks, including transient tasks. Internally invokes `stop(false)` on all children. This should be considered a last ditch effort and is used when closing the scheduler.
|
||||
def terminate
|
||||
# Attempt to stop the current task immediately, and all children:
|
||||
stop(false)
|
||||
|
||||
# If that doesn't work, take more serious action:
|
||||
@children&.each do |child|
|
||||
child.terminate
|
||||
end
|
||||
|
||||
return @children.nil?
|
||||
end
|
||||
|
||||
# Attempt to stop the current node immediately, including all non-transient children. Invokes {#stop_children} to stop all children.
|
||||
#
|
||||
# @parameter later [Boolean] Whether to defer stopping until some point in the future.
|
||||
def stop(later = false)
|
||||
# The implementation of this method may defer calling `stop_children`.
|
||||
stop_children(later)
|
||||
end
|
||||
|
||||
# Attempt to stop all non-transient children.
|
||||
private def stop_children(later = false)
|
||||
@children&.each do |child|
|
||||
child.stop(later) unless child.transient?
|
||||
end
|
||||
end
|
||||
|
||||
# Whether the node has been stopped.
|
||||
def stopped?
|
||||
@children.nil?
|
||||
end
|
||||
|
||||
# Print the hierarchy of the task tree from the given node.
|
||||
#
|
||||
# @parameter out [IO] The output stream to write to.
|
||||
# @parameter backtrace [Boolean] Whether to print the backtrace of each node.
|
||||
def print_hierarchy(out = $stdout, backtrace: true)
|
||||
self.traverse do |node, level|
|
||||
indent = "\t" * level
|
||||
|
||||
out.puts "#{indent}#{node}"
|
||||
|
||||
print_backtrace(out, indent, node) if backtrace
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def print_backtrace(out, indent, node)
|
||||
if backtrace = node.backtrace
|
||||
backtrace.each_with_index do |line, index|
|
||||
out.puts "#{indent}#{index.zero? ? "→ " : " "}#{line}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2018-2024, by Samuel Williams.
|
||||
|
||||
require_relative "condition"
|
||||
|
||||
module Async
|
||||
# A synchronization primitive, which allows fibers to wait until a notification is received. Does not block the task which signals the notification. Waiting tasks are resumed on next iteration of the reactor.
|
||||
# @public Since *Async v1*.
|
||||
class Notification < Condition
|
||||
# Signal to a given task that it should resume operations.
|
||||
def signal(value = nil, task: Task.current)
|
||||
return if @waiting.empty?
|
||||
|
||||
Fiber.scheduler.push Signal.new(self.exchange, value)
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
Signal = Struct.new(:waiting, :value) do
|
||||
def alive?
|
||||
true
|
||||
end
|
||||
|
||||
def transfer
|
||||
waiting.each do |fiber|
|
||||
fiber.transfer(value) if fiber.alive?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :Signal
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,171 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2018-2024, by Samuel Williams.
|
||||
# Copyright, 2019, by Ryan Musgrave.
|
||||
# Copyright, 2020-2022, by Bruno Sutic.
|
||||
|
||||
require_relative "notification"
|
||||
|
||||
module Async
|
||||
# A queue which allows items to be processed in order.
|
||||
#
|
||||
# It has a compatible interface with {Notification} and {Condition}, except that it's multi-value.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
class Queue
|
||||
# Create a new queue.
|
||||
#
|
||||
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
||||
# @parameter available [Notification] The notification to use for signaling when items are available.
|
||||
def initialize(parent: nil, available: Notification.new)
|
||||
@items = []
|
||||
@parent = parent
|
||||
@available = available
|
||||
end
|
||||
|
||||
# @attribute [Array] The items in the queue.
|
||||
attr :items
|
||||
|
||||
# @returns [Integer] The number of items in the queue.
|
||||
def size
|
||||
@items.size
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether the queue is empty.
|
||||
def empty?
|
||||
@items.empty?
|
||||
end
|
||||
|
||||
# Add an item to the queue.
|
||||
def push(item)
|
||||
@items << item
|
||||
|
||||
@available.signal unless self.empty?
|
||||
end
|
||||
|
||||
# Compatibility with {::Queue#push}.
|
||||
def <<(item)
|
||||
self.push(item)
|
||||
end
|
||||
|
||||
# Add multiple items to the queue.
|
||||
def enqueue(*items)
|
||||
@items.concat(items)
|
||||
|
||||
@available.signal unless self.empty?
|
||||
end
|
||||
|
||||
# Remove and return the next item from the queue.
|
||||
def dequeue
|
||||
while @items.empty?
|
||||
@available.wait
|
||||
end
|
||||
|
||||
@items.shift
|
||||
end
|
||||
|
||||
# Compatibility with {::Queue#pop}.
|
||||
def pop
|
||||
self.dequeue
|
||||
end
|
||||
|
||||
# Process each item in the queue.
|
||||
#
|
||||
# @asynchronous Executes the given block concurrently for each item.
|
||||
#
|
||||
# @parameter arguments [Array] The arguments to pass to the block.
|
||||
# @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
|
||||
# @parameter options [Hash] The options to pass to the task.
|
||||
# @yields {|task| ...} When the system is idle, the block will be executed in a new task.
|
||||
def async(parent: (@parent or Task.current), **options, &block)
|
||||
while item = self.dequeue
|
||||
parent.async(item, **options, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# Enumerate each item in the queue.
|
||||
def each
|
||||
while item = self.dequeue
|
||||
yield item
|
||||
end
|
||||
end
|
||||
|
||||
# Signal the queue with a value, the same as {#enqueue}.
|
||||
def signal(value = nil)
|
||||
self.enqueue(value)
|
||||
end
|
||||
|
||||
# Wait for an item to be available, the same as {#dequeue}.
|
||||
def wait
|
||||
self.dequeue
|
||||
end
|
||||
end
|
||||
|
||||
# A queue which limits the number of items that can be enqueued.
|
||||
# @public Since *Async v1*.
|
||||
class LimitedQueue < Queue
|
||||
# Create a new limited queue.
|
||||
#
|
||||
# @parameter limit [Integer] The maximum number of items that can be enqueued.
|
||||
# @parameter full [Notification] The notification to use for signaling when the queue is full.
|
||||
def initialize(limit = 1, full: Notification.new, **options)
|
||||
super(**options)
|
||||
|
||||
@limit = limit
|
||||
@full = full
|
||||
end
|
||||
|
||||
# @attribute [Integer] The maximum number of items that can be enqueued.
|
||||
attr :limit
|
||||
|
||||
# @returns [Boolean] Whether trying to enqueue an item would block.
|
||||
def limited?
|
||||
@items.size >= @limit
|
||||
end
|
||||
|
||||
# Add an item to the queue.
|
||||
#
|
||||
# If the queue is full, this method will block until there is space available.
|
||||
#
|
||||
# @parameter item [Object] The item to add to the queue.
|
||||
def push(item)
|
||||
while limited?
|
||||
@full.wait
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
# Add multiple items to the queue.
|
||||
#
|
||||
# If the queue is full, this method will block until there is space available.
|
||||
#
|
||||
# @parameter items [Array] The items to add to the queue.
|
||||
def enqueue(*items)
|
||||
while !items.empty?
|
||||
while limited?
|
||||
@full.wait
|
||||
end
|
||||
|
||||
available = @limit - @items.size
|
||||
@items.concat(items.shift(available))
|
||||
|
||||
@available.signal unless self.empty?
|
||||
end
|
||||
end
|
||||
|
||||
# Remove and return the next item from the queue.
|
||||
#
|
||||
# If the queue is empty, this method will block until an item is available.
|
||||
#
|
||||
# @returns [Object] The next item in the queue.
|
||||
def dequeue
|
||||
item = super
|
||||
|
||||
@full.signal
|
||||
|
||||
return item
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2017-2024, by Samuel Williams.
|
||||
# Copyright, 2017, by Kent Gruber.
|
||||
# Copyright, 2018, by Sokolov Yura.
|
||||
|
||||
require_relative "scheduler"
|
||||
|
||||
module Async
|
||||
# A wrapper around the the scheduler which binds it to the current thread automatically.
|
||||
class Reactor < Scheduler
|
||||
# @deprecated Replaced by {Kernel::Async}.
|
||||
def self.run(...)
|
||||
Async(...)
|
||||
end
|
||||
|
||||
# Initialize the reactor and assign it to the current Fiber scheduler.
|
||||
def initialize(...)
|
||||
super
|
||||
|
||||
Fiber.set_scheduler(self)
|
||||
end
|
||||
|
||||
# Close the reactor and remove it from the current Fiber scheduler.
|
||||
def scheduler_close
|
||||
self.close
|
||||
end
|
||||
|
||||
public :sleep
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,582 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2020-2024, by Samuel Williams.
|
||||
# Copyright, 2020, by Jun Jiang.
|
||||
# Copyright, 2021, by Julien Portalier.
|
||||
|
||||
require_relative "clock"
|
||||
require_relative "task"
|
||||
require_relative "worker_pool"
|
||||
|
||||
require "io/event"
|
||||
|
||||
require "console"
|
||||
require "resolv"
|
||||
|
||||
module Async
|
||||
begin
|
||||
require "fiber/profiler"
|
||||
Profiler = Fiber::Profiler
|
||||
rescue LoadError
|
||||
# Fiber::Profiler is not available.
|
||||
Profiler = nil
|
||||
end
|
||||
|
||||
# Handles scheduling of fibers. Implements the fiber scheduler interface.
|
||||
class Scheduler < Node
|
||||
WORKER_POOL = ENV.fetch("ASYNC_SCHEDULER_WORKER_POOL", nil).then do |value|
|
||||
value == "true" ? true : nil
|
||||
end
|
||||
|
||||
# Raised when an operation is attempted on a closed scheduler.
|
||||
class ClosedError < RuntimeError
|
||||
# Create a new error.
|
||||
#
|
||||
# @parameter message [String] The error message.
|
||||
def initialize(message = "Scheduler is closed!")
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Whether the fiber scheduler is supported.
|
||||
# @public Since *Async v1*.
|
||||
def self.supported?
|
||||
true
|
||||
end
|
||||
|
||||
# Create a new scheduler.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
# @parameter parent [Node | Nil] The parent node to use for task hierarchy.
|
||||
# @parameter selector [IO::Event::Selector] The selector to use for event handling.
|
||||
def initialize(parent = nil, selector: nil, profiler: Profiler&.default, worker_pool: WORKER_POOL)
|
||||
super(parent)
|
||||
|
||||
@selector = selector || ::IO::Event::Selector.new(Fiber.current)
|
||||
@profiler = profiler
|
||||
|
||||
@interrupted = false
|
||||
|
||||
@blocked = 0
|
||||
|
||||
@busy_time = 0.0
|
||||
@idle_time = 0.0
|
||||
|
||||
@timers = ::IO::Event::Timers.new
|
||||
if worker_pool == true
|
||||
@worker_pool = WorkerPool.new
|
||||
else
|
||||
@worker_pool = worker_pool
|
||||
end
|
||||
|
||||
if @worker_pool
|
||||
self.singleton_class.prepend(WorkerPool::BlockingOperationWait)
|
||||
end
|
||||
end
|
||||
|
||||
# Compute the scheduler load according to the busy and idle times that are updated by the run loop.
|
||||
#
|
||||
# @returns [Float] The load of the scheduler. 0.0 means no load, 1.0 means fully loaded or over-loaded.
|
||||
def load
|
||||
total_time = @busy_time + @idle_time
|
||||
|
||||
# If the total time is zero, then the load is zero:
|
||||
return 0.0 if total_time.zero?
|
||||
|
||||
# We normalize to a 1 second window:
|
||||
if total_time > 1.0
|
||||
ratio = 1.0 / total_time
|
||||
@busy_time *= ratio
|
||||
@idle_time *= ratio
|
||||
|
||||
# We don't need to divide here as we've already normalised it to a 1s window:
|
||||
return @busy_time
|
||||
else
|
||||
return @busy_time / total_time
|
||||
end
|
||||
end
|
||||
|
||||
# Invoked when the fiber scheduler is being closed.
|
||||
#
|
||||
# Executes the run loop until all tasks are finished, then closes the scheduler.
|
||||
def scheduler_close(error = $!)
|
||||
# If the execution context (thread) was handling an exception, we want to exit as quickly as possible:
|
||||
unless error
|
||||
self.run
|
||||
end
|
||||
ensure
|
||||
self.close
|
||||
end
|
||||
|
||||
# Terminate all child tasks.
|
||||
def terminate
|
||||
# If that doesn't work, take more serious action:
|
||||
@children&.each do |child|
|
||||
child.terminate
|
||||
end
|
||||
|
||||
return @children.nil?
|
||||
end
|
||||
|
||||
# Terminate all child tasks and close the scheduler.
|
||||
# @public Since *Async v1*.
|
||||
def close
|
||||
self.run_loop do
|
||||
until self.terminate
|
||||
self.run_once!
|
||||
end
|
||||
end
|
||||
|
||||
Kernel.raise "Closing scheduler with blocked operations!" if @blocked > 0
|
||||
ensure
|
||||
# We want `@selector = nil` to be a visible side effect from this point forward, specifically in `#interrupt` and `#unblock`. If the selector is closed, then we don't want to push any fibers to it.
|
||||
selector = @selector
|
||||
@selector = nil
|
||||
|
||||
selector&.close
|
||||
|
||||
worker_pool = @worker_pool
|
||||
@worker_pool = nil
|
||||
|
||||
worker_pool&.close
|
||||
|
||||
consume
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether the scheduler has been closed.
|
||||
# @public Since *Async v1*.
|
||||
def closed?
|
||||
@selector.nil?
|
||||
end
|
||||
|
||||
# @returns [String] A description of the scheduler.
|
||||
def to_s
|
||||
"\#<#{self.description} #{@children&.size || 0} children (#{stopped? ? 'stopped' : 'running'})>"
|
||||
end
|
||||
|
||||
# Interrupt the event loop and cause it to exit.
|
||||
# @asynchronous May be called from any thread.
|
||||
def interrupt
|
||||
@interrupted = true
|
||||
@selector&.wakeup
|
||||
end
|
||||
|
||||
# Transfer from the calling fiber to the event loop.
|
||||
def transfer
|
||||
@selector.transfer
|
||||
end
|
||||
|
||||
# Yield the current fiber and resume it on the next iteration of the event loop.
|
||||
def yield
|
||||
@selector.yield
|
||||
end
|
||||
|
||||
# Schedule a fiber (or equivalent object) to be resumed on the next loop through the reactor.
|
||||
# @parameter fiber [Fiber | Object] The object to be resumed on the next iteration of the run-loop.
|
||||
def push(fiber)
|
||||
@selector.push(fiber)
|
||||
end
|
||||
|
||||
# Raise an exception on a specified fiber with the given arguments.
|
||||
#
|
||||
# This internally schedules the current fiber to be ready, before raising the exception, so that it will later resume execution.
|
||||
#
|
||||
# @parameter fiber [Fiber] The fiber to raise the exception on.
|
||||
# @parameter *arguments [Array] The arguments to pass to the fiber.
|
||||
def raise(...)
|
||||
@selector.raise(...)
|
||||
end
|
||||
|
||||
# Resume execution of the specified fiber.
|
||||
#
|
||||
# @parameter fiber [Fiber] The fiber to resume.
|
||||
# @parameter arguments [Array] The arguments to pass to the fiber.
|
||||
def resume(fiber, *arguments)
|
||||
@selector.resume(fiber, *arguments)
|
||||
end
|
||||
|
||||
# Invoked when a fiber tries to perform a blocking operation which cannot continue. A corresponding call {unblock} must be performed to allow this fiber to continue.
|
||||
#
|
||||
# @public Since *Async v2*.
|
||||
# @asynchronous May only be called on same thread as fiber scheduler.
|
||||
#
|
||||
# @parameter blocker [Object] The object that is blocking the fiber.
|
||||
# @parameter timeout [Float | Nil] The maximum time to block, or if nil, indefinitely.
|
||||
def block(blocker, timeout)
|
||||
# $stderr.puts "block(#{blocker}, #{Fiber.current}, #{timeout})"
|
||||
fiber = Fiber.current
|
||||
|
||||
if timeout
|
||||
timer = @timers.after(timeout) do
|
||||
if fiber.alive?
|
||||
fiber.transfer(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
@blocked += 1
|
||||
@selector.transfer
|
||||
ensure
|
||||
@blocked -= 1
|
||||
end
|
||||
ensure
|
||||
timer&.cancel!
|
||||
end
|
||||
|
||||
# Unblock a fiber that was previously blocked.
|
||||
#
|
||||
# @public Since *Async v2* and *Ruby v3.1*.
|
||||
# @asynchronous May be called from any thread.
|
||||
#
|
||||
# @parameter blocker [Object] The object that was blocking the fiber.
|
||||
# @parameter fiber [Fiber] The fiber to unblock.
|
||||
def unblock(blocker, fiber)
|
||||
# $stderr.puts "unblock(#{blocker}, #{fiber})"
|
||||
|
||||
# This operation is protected by the GVL:
|
||||
if selector = @selector
|
||||
selector.push(fiber)
|
||||
selector.wakeup
|
||||
end
|
||||
end
|
||||
|
||||
# Sleep for the specified duration.
|
||||
#
|
||||
# @public Since *Async v2* and *Ruby v3.1*.
|
||||
# @asynchronous May be non-blocking.
|
||||
#
|
||||
# @parameter duration [Numeric | Nil] The time in seconds to sleep, or if nil, indefinitely.
|
||||
def kernel_sleep(duration = nil)
|
||||
if duration
|
||||
self.block(nil, duration)
|
||||
else
|
||||
self.transfer
|
||||
end
|
||||
end
|
||||
|
||||
# Resolve the address of the given hostname.
|
||||
#
|
||||
# @public Since *Async v2*.
|
||||
# @asynchronous May be non-blocking.
|
||||
#
|
||||
# @parameter hostname [String] The hostname to resolve.
|
||||
def address_resolve(hostname)
|
||||
# On some platforms, hostnames may contain a device-specific suffix (e.g. %en0). We need to strip this before resolving.
|
||||
# See <https://github.com/socketry/async/issues/180> for more details.
|
||||
hostname = hostname.split("%", 2).first
|
||||
::Resolv.getaddresses(hostname)
|
||||
end
|
||||
|
||||
if IO.method_defined?(:timeout)
|
||||
private def get_timeout(io)
|
||||
io.timeout
|
||||
end
|
||||
else
|
||||
private def get_timeout(io)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for the specified IO to become ready for the specified events.
|
||||
#
|
||||
# @public Since *Async v2*.
|
||||
# @asynchronous May be non-blocking.
|
||||
#
|
||||
# @parameter io [IO] The IO object to wait on.
|
||||
# @parameter events [Integer] The events to wait for, e.g. `IO::READABLE`, `IO::WRITABLE`, etc.
|
||||
# @parameter timeout [Float | Nil] The maximum time to wait, or if nil, indefinitely.
|
||||
def io_wait(io, events, timeout = nil)
|
||||
fiber = Fiber.current
|
||||
|
||||
if timeout
|
||||
# If an explicit timeout is specified, we expect that the user will handle it themselves:
|
||||
timer = @timers.after(timeout) do
|
||||
fiber.transfer
|
||||
end
|
||||
elsif timeout = get_timeout(io)
|
||||
# Otherwise, if we default to the io's timeout, we raise an exception:
|
||||
timer = @timers.after(timeout) do
|
||||
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become ready!")
|
||||
end
|
||||
end
|
||||
|
||||
return @selector.io_wait(fiber, io, events)
|
||||
ensure
|
||||
timer&.cancel!
|
||||
end
|
||||
|
||||
if ::IO::Event::Support.buffer?
|
||||
# Read from the specified IO into the buffer.
|
||||
#
|
||||
# @public Since *Async v2* and Ruby with `IO::Buffer` support.
|
||||
# @asynchronous May be non-blocking.
|
||||
#
|
||||
# @parameter io [IO] The IO object to read from.
|
||||
# @parameter buffer [IO::Buffer] The buffer to read into.
|
||||
# @parameter length [Integer] The minimum number of bytes to read.
|
||||
# @parameter offset [Integer] The offset within the buffer to read into.
|
||||
def io_read(io, buffer, length, offset = 0)
|
||||
fiber = Fiber.current
|
||||
|
||||
if timeout = get_timeout(io)
|
||||
timer = @timers.after(timeout) do
|
||||
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become readable!")
|
||||
end
|
||||
end
|
||||
|
||||
@selector.io_read(fiber, io, buffer, length, offset)
|
||||
ensure
|
||||
timer&.cancel!
|
||||
end
|
||||
|
||||
if RUBY_ENGINE != "ruby" || RUBY_VERSION >= "3.3.1"
|
||||
# Write the specified buffer to the IO.
|
||||
#
|
||||
# @public Since *Async v2* and *Ruby v3.3.1* with `IO::Buffer` support.
|
||||
# @asynchronous May be non-blocking.
|
||||
#
|
||||
# @parameter io [IO] The IO object to write to.
|
||||
# @parameter buffer [IO::Buffer] The buffer to write from.
|
||||
# @parameter length [Integer] The minimum number of bytes to write.
|
||||
# @parameter offset [Integer] The offset within the buffer to write from.
|
||||
def io_write(io, buffer, length, offset = 0)
|
||||
fiber = Fiber.current
|
||||
|
||||
if timeout = get_timeout(io)
|
||||
timer = @timers.after(timeout) do
|
||||
fiber.raise(::IO::TimeoutError, "Timeout (#{timeout}s) while waiting for IO to become writable!")
|
||||
end
|
||||
end
|
||||
|
||||
@selector.io_write(fiber, io, buffer, length, offset)
|
||||
ensure
|
||||
timer&.cancel!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for the specified process ID to exit.
|
||||
#
|
||||
# @public Since *Async v2*.
|
||||
# @asynchronous May be non-blocking.
|
||||
#
|
||||
# @parameter pid [Integer] The process ID to wait for.
|
||||
# @parameter flags [Integer] A bit-mask of flags suitable for `Process::Status.wait`.
|
||||
# @returns [Process::Status] A process status instance.
|
||||
# @asynchronous May be non-blocking..
|
||||
def process_wait(pid, flags)
|
||||
return @selector.process_wait(Fiber.current, pid, flags)
|
||||
end
|
||||
|
||||
# Run one iteration of the event loop.
|
||||
#
|
||||
# When terminating the event loop, we already know we are finished. So we don't need to check the task tree. This is a logical requirement because `run_once` ignores transient tasks. For example, a single top level transient task is not enough to keep the reactor running, but during termination we must still process it in order to terminate child tasks.
|
||||
#
|
||||
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
||||
# @returns [Boolean] Whether there is more work to do.
|
||||
private def run_once!(timeout = nil)
|
||||
start_time = Async::Clock.now
|
||||
|
||||
interval = @timers.wait_interval
|
||||
|
||||
# If there is no interval to wait (thus no timers), and no tasks, we could be done:
|
||||
if interval.nil?
|
||||
# Allow the user to specify a maximum interval if we would otherwise be sleeping indefinitely:
|
||||
interval = timeout
|
||||
elsif interval < 0
|
||||
# We have timers ready to fire, don't sleep in the selctor:
|
||||
interval = 0
|
||||
elsif timeout and interval > timeout
|
||||
interval = timeout
|
||||
end
|
||||
|
||||
begin
|
||||
@selector.select(interval)
|
||||
rescue Errno::EINTR
|
||||
# Ignore.
|
||||
end
|
||||
|
||||
@timers.fire
|
||||
|
||||
# Compute load:
|
||||
end_time = Async::Clock.now
|
||||
total_duration = end_time - start_time
|
||||
idle_duration = @selector.idle_duration
|
||||
busy_duration = total_duration - idle_duration
|
||||
|
||||
@busy_time += busy_duration
|
||||
@idle_time += idle_duration
|
||||
|
||||
# The reactor still has work to do:
|
||||
return true
|
||||
end
|
||||
|
||||
# Run one iteration of the event loop.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
# @asynchronous Must be invoked from blocking (root) fiber.
|
||||
#
|
||||
# @parameter timeout [Float | Nil] The maximum timeout, or if nil, indefinite.
|
||||
# @returns [Boolean] Whether there is more work to do.
|
||||
def run_once(timeout = nil)
|
||||
Kernel.raise "Running scheduler on non-blocking fiber!" unless Fiber.blocking?
|
||||
|
||||
if self.finished?
|
||||
self.stop
|
||||
end
|
||||
|
||||
# If we are finished, we stop the task tree and exit:
|
||||
if @children.nil?
|
||||
return false
|
||||
end
|
||||
|
||||
return run_once!(timeout)
|
||||
end
|
||||
|
||||
# Checks and clears the interrupted state of the scheduler.
|
||||
#
|
||||
# @returns [Boolean] Whether the reactor has been interrupted.
|
||||
private def interrupted?
|
||||
if @interrupted
|
||||
@interrupted = false
|
||||
return true
|
||||
end
|
||||
|
||||
if Thread.pending_interrupt?
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
# Stop all children, including transient children.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
def stop
|
||||
@children&.each do |child|
|
||||
child.stop
|
||||
end
|
||||
end
|
||||
|
||||
private def run_loop(&block)
|
||||
interrupt = nil
|
||||
|
||||
begin
|
||||
# In theory, we could use Exception here to be a little bit safer, but we've only shown the case for SignalException to be a problem, so let's not over-engineer this.
|
||||
Thread.handle_interrupt(::SignalException => :never) do
|
||||
until self.interrupted?
|
||||
# If we are finished, we need to exit:
|
||||
break unless yield
|
||||
end
|
||||
end
|
||||
rescue Interrupt => interrupt
|
||||
# If an interrupt did occur during an iteration of the event loop, we need to handle it. More specifically, `self.stop` is not safe to interrupt without potentially corrupting the task tree.
|
||||
Thread.handle_interrupt(::SignalException => :never) do
|
||||
Console.debug(self) do |buffer|
|
||||
buffer.puts "Scheduler interrupted: #{interrupt.inspect}"
|
||||
self.print_hierarchy(buffer)
|
||||
end
|
||||
|
||||
self.stop
|
||||
end
|
||||
|
||||
retry
|
||||
end
|
||||
|
||||
# If the event loop was interrupted, and we finished exiting normally (due to the interrupt), we need to re-raise the interrupt so that the caller can handle it too.
|
||||
if interrupt
|
||||
Kernel.raise(interrupt)
|
||||
end
|
||||
end
|
||||
|
||||
# Run the reactor until all tasks are finished. Proxies arguments to {#async} immediately before entering the loop, if a block is provided.
|
||||
#
|
||||
# Forwards all parameters to {#async} if a block is given.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
#
|
||||
# @yields {|task| ...} The top level task, if a block is given.
|
||||
# @returns [Task] The initial task that was scheduled into the reactor.
|
||||
def run(...)
|
||||
Kernel.raise ClosedError if @selector.nil?
|
||||
|
||||
begin
|
||||
@profiler&.start
|
||||
|
||||
initial_task = self.async(...) if block_given?
|
||||
|
||||
self.run_loop do
|
||||
run_once
|
||||
end
|
||||
|
||||
return initial_task
|
||||
ensure
|
||||
@profiler&.stop
|
||||
end
|
||||
end
|
||||
|
||||
# Start an asynchronous task within the specified reactor. The task will be executed until the first blocking call, at which point it will yield and and this method will return.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
# @asynchronous May context switch immediately to new task.
|
||||
# @deprecated Use {#run} or {Task#async} instead.
|
||||
#
|
||||
# @yields {|task| ...} Executed within the task.
|
||||
# @returns [Task] The task that was scheduled into the reactor.
|
||||
def async(*arguments, **options, &block)
|
||||
# warn "Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated
|
||||
|
||||
Kernel.raise ClosedError if @selector.nil?
|
||||
|
||||
task = Task.new(Task.current? || self, **options, &block)
|
||||
|
||||
task.run(*arguments)
|
||||
|
||||
return task
|
||||
end
|
||||
|
||||
def fiber(...)
|
||||
return async(...).fiber
|
||||
end
|
||||
|
||||
# Invoke the block, but after the specified timeout, raise {TimeoutError} in any currenly blocking operation. If the block runs to completion before the timeout occurs or there are no non-blocking operations after the timeout expires, the code will complete without any exception.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
# @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
|
||||
#
|
||||
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
|
||||
# @parameter exception [Class] The exception class to raise.
|
||||
# @parameter message [String] The message to pass to the exception.
|
||||
# @yields {|duration| ...} The block to execute with a timeout.
|
||||
def with_timeout(duration, exception = TimeoutError, message = "execution expired", &block)
|
||||
fiber = Fiber.current
|
||||
|
||||
timer = @timers.after(duration) do
|
||||
if fiber.alive?
|
||||
fiber.raise(exception, message)
|
||||
end
|
||||
end
|
||||
|
||||
yield timer
|
||||
ensure
|
||||
timer&.cancel!
|
||||
end
|
||||
|
||||
# Invoke the block, but after the specified timeout, raise the specified exception with the given message. If the block runs to completion before the timeout occurs or there are no non-blocking operations after the timeout expires, the code will complete without any exception.
|
||||
#
|
||||
# @public Since *Async v1* and *Ruby v3.1*. May be invoked from `Timeout.timeout`.
|
||||
# @asynchronous May raise an exception at any interruption point (e.g. blocking operations).
|
||||
#
|
||||
# @parameter duration [Numeric] The time in seconds, in which the task should complete.
|
||||
# @parameter exception [Class] The exception class to raise.
|
||||
# @parameter message [String] The message to pass to the exception.
|
||||
# @yields {|duration| ...} The block to execute with a timeout.
|
||||
def timeout_after(duration, exception, message, &block)
|
||||
with_timeout(duration, exception, message) do |timer|
|
||||
yield duration
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,41 @@
|
||||
A synchronization primitive, which limits access to a given resource, such as a limited number of database connections, open files, or network connections.
|
||||
|
||||
## Example
|
||||
|
||||
~~~ ruby
|
||||
require 'async'
|
||||
require 'async/semaphore'
|
||||
require 'net/http'
|
||||
|
||||
Sync do
|
||||
# Only allow two concurrent tasks at a time:
|
||||
semaphore = Async::Semaphore.new(2)
|
||||
|
||||
# Generate an array of 10 numbers:
|
||||
terms = ['ruby', 'python', 'go', 'java', 'c++']
|
||||
|
||||
# Search for the terms:
|
||||
terms.each do |term|
|
||||
semaphore.async do |task|
|
||||
Console.info("Searching for #{term}...")
|
||||
response = Net::HTTP.get(URI "https://www.google.com/search?q=#{term}")
|
||||
Console.info("Got response #{response.size} bytes.")
|
||||
end
|
||||
end
|
||||
end
|
||||
~~~
|
||||
|
||||
### Output
|
||||
|
||||
~~~
|
||||
0.0s info: Searching for ruby... [ec=0x3c] [pid=50523]
|
||||
0.04s info: Searching for python... [ec=0x21c] [pid=50523]
|
||||
1.7s info: Got response 182435 bytes. [ec=0x3c] [pid=50523]
|
||||
1.71s info: Searching for go... [ec=0x834] [pid=50523]
|
||||
3.0s info: Got response 204854 bytes. [ec=0x21c] [pid=50523]
|
||||
3.0s info: Searching for java... [ec=0xf64] [pid=50523]
|
||||
4.32s info: Got response 103235 bytes. [ec=0x834] [pid=50523]
|
||||
4.32s info: Searching for c++... [ec=0x12d4] [pid=50523]
|
||||
4.65s info: Got response 109697 bytes. [ec=0xf64] [pid=50523]
|
||||
6.64s info: Got response 87249 bytes. [ec=0x12d4] [pid=50523]
|
||||
~~~
|
||||
@@ -0,0 +1,127 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2018-2024, by Samuel Williams.
|
||||
|
||||
require_relative "list"
|
||||
|
||||
module Async
|
||||
# A synchronization primitive, which limits access to a given resource.
|
||||
# @public Since *Async v1*.
|
||||
class Semaphore
|
||||
# @parameter limit [Integer] The maximum number of times the semaphore can be acquired before it blocks.
|
||||
# @parameter parent [Task | Semaphore | Nil] The parent for holding any children tasks.
|
||||
def initialize(limit = 1, parent: nil)
|
||||
@count = 0
|
||||
@limit = limit
|
||||
@waiting = List.new
|
||||
|
||||
@parent = parent
|
||||
end
|
||||
|
||||
# The current number of tasks that have acquired the semaphore.
|
||||
attr :count
|
||||
|
||||
# The maximum number of tasks that can acquire the semaphore.
|
||||
attr :limit
|
||||
|
||||
# The tasks waiting on this semaphore.
|
||||
attr :waiting
|
||||
|
||||
# Allow setting the limit. This is useful for cases where the semaphore is used to limit the number of concurrent tasks, but the number of tasks is not known in advance or needs to be modified.
|
||||
#
|
||||
# On increasing the limit, some tasks may be immediately resumed. On decreasing the limit, some tasks may execute until the count is < than the limit.
|
||||
#
|
||||
# @parameter limit [Integer] The new limit.
|
||||
def limit= limit
|
||||
difference = limit - @limit
|
||||
@limit = limit
|
||||
|
||||
# We can't suspend
|
||||
if difference > 0
|
||||
difference.times do
|
||||
break unless node = @waiting.first
|
||||
|
||||
node.resume
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Is the semaphore currently acquired?
|
||||
def empty?
|
||||
@count.zero?
|
||||
end
|
||||
|
||||
# Whether trying to acquire this semaphore would block.
|
||||
def blocking?
|
||||
@count >= @limit
|
||||
end
|
||||
|
||||
# Run an async task. Will wait until the semaphore is ready until spawning and running the task.
|
||||
def async(*arguments, parent: (@parent or Task.current), **options)
|
||||
wait
|
||||
|
||||
parent.async(**options) do |task|
|
||||
@count += 1
|
||||
|
||||
begin
|
||||
yield task, *arguments
|
||||
ensure
|
||||
self.release
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Acquire the semaphore, block if we are at the limit.
|
||||
# If no block is provided, you must call release manually.
|
||||
# @yields {...} When the semaphore can be acquired.
|
||||
# @returns The result of the block if invoked.
|
||||
def acquire
|
||||
wait
|
||||
|
||||
@count += 1
|
||||
|
||||
return unless block_given?
|
||||
|
||||
begin
|
||||
return yield
|
||||
ensure
|
||||
self.release
|
||||
end
|
||||
end
|
||||
|
||||
# Release the semaphore. Must match up with a corresponding call to `acquire`. Will release waiting fibers in FIFO order.
|
||||
def release
|
||||
@count -= 1
|
||||
|
||||
while (@limit - @count) > 0 and node = @waiting.first
|
||||
node.resume
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
class FiberNode < List::Node
|
||||
def initialize(fiber)
|
||||
@fiber = fiber
|
||||
end
|
||||
|
||||
def resume
|
||||
if @fiber.alive?
|
||||
Fiber.scheduler.resume(@fiber)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :FiberNode
|
||||
|
||||
# Wait until the semaphore becomes available.
|
||||
def wait
|
||||
return unless blocking?
|
||||
|
||||
@waiting.stack(FiberNode.new(Fiber.current)) do
|
||||
Fiber.scheduler.transfer while blocking?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
A sequence of instructions, defined by a block, which is executed sequentially and managed by the scheduler. A task can be in one of the following states: `initialized`, `running`, `completed`, `failed`, `cancelled` or `stopped`.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Initialized
|
||||
Initialized --> Running : Run
|
||||
|
||||
Running --> Completed : Return Value
|
||||
Running --> Failed : Exception
|
||||
|
||||
Completed --> [*]
|
||||
Failed --> [*]
|
||||
|
||||
Running --> Stopped : Stop
|
||||
Stopped --> [*]
|
||||
Completed --> Stopped : Stop
|
||||
Failed --> Stopped : Stop
|
||||
Initialized --> Stopped : Stop
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```ruby
|
||||
require 'async'
|
||||
|
||||
# Create an asynchronous task that sleeps for 1 second:
|
||||
Async do |task|
|
||||
sleep(1)
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,459 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2017-2024, by Samuel Williams.
|
||||
# Copyright, 2017, by Kent Gruber.
|
||||
# Copyright, 2017, by Devin Christensen.
|
||||
# Copyright, 2020, by Patrik Wenger.
|
||||
# Copyright, 2023, by Math Ieu.
|
||||
|
||||
require "fiber"
|
||||
require "console"
|
||||
|
||||
require_relative "node"
|
||||
require_relative "condition"
|
||||
|
||||
Fiber.attr_accessor :async_task
|
||||
|
||||
module Async
|
||||
# Raised when a task is explicitly stopped.
|
||||
class Stop < Exception
|
||||
# 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.
|
||||
def initialize(task)
|
||||
@task = task
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Raised if a timeout occurs on a specific Fiber. Handled gracefully by `Task`.
|
||||
# @public Since *Async v1*.
|
||||
class TimeoutError < StandardError
|
||||
# Create a new timeout error.
|
||||
#
|
||||
# @parameter message [String] The error message.
|
||||
def initialize(message = "execution expired")
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# @public Since *Async v1*.
|
||||
class Task < Node
|
||||
# Raised when a child task is created within a task that has finished execution.
|
||||
class FinishedError < RuntimeError
|
||||
# Create a new finished error.
|
||||
#
|
||||
# @parameter message [String] The error message.
|
||||
def initialize(message = "Cannot create child task within a task that has finished execution!")
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# @deprecated With no replacement.
|
||||
def self.yield
|
||||
Fiber.scheduler.transfer
|
||||
end
|
||||
|
||||
# Run the given block of code in a task, asynchronously, in the given scheduler.
|
||||
def self.run(scheduler, *arguments, **options, &block)
|
||||
self.new(scheduler, **options, &block).tap do |task|
|
||||
task.run(*arguments)
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new task.
|
||||
# @parameter reactor [Reactor] the reactor this task will run within.
|
||||
# @parameter parent [Task] the parent task.
|
||||
def initialize(parent = Task.current?, finished: nil, **options, &block)
|
||||
super(parent, **options)
|
||||
|
||||
# These instance variables are critical to the state of the task.
|
||||
# In the initialized state, the @block should be set, but the @fiber should be nil.
|
||||
# In the running state, the @fiber should be set.
|
||||
# In a finished state, the @block should be nil, and the @fiber should be nil.
|
||||
@block = block
|
||||
@fiber = nil
|
||||
|
||||
@status = :initialized
|
||||
@result = nil
|
||||
@finished = finished
|
||||
|
||||
@defer_stop = nil
|
||||
end
|
||||
|
||||
# @returns [Scheduler] The scheduler for this task.
|
||||
def reactor
|
||||
self.root
|
||||
end
|
||||
|
||||
# @returns [Array(Thread::Backtrace::Location) | Nil] The backtrace of the task, if available.
|
||||
def backtrace(*arguments)
|
||||
@fiber&.backtrace(*arguments)
|
||||
end
|
||||
|
||||
# Annotate the task with a description.
|
||||
#
|
||||
# This will internally try to annotate the fiber if it is running, otherwise it will annotate the task itself.
|
||||
#
|
||||
# @parameter annotation [String] The description to annotate the task with.
|
||||
def annotate(annotation, &block)
|
||||
if @fiber
|
||||
@fiber.annotate(annotation, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# @returns [Object] The annotation of the task.
|
||||
def annotation
|
||||
if @fiber
|
||||
@fiber.annotation
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# @returns [String] A description of the task and it's current status.
|
||||
def to_s
|
||||
"\#<#{self.description} (#{@status})>"
|
||||
end
|
||||
|
||||
# @deprecated Prefer {Kernel#sleep} except when compatibility with `stable-v1` is required.
|
||||
def sleep(duration = nil)
|
||||
super
|
||||
end
|
||||
|
||||
# Execute the given block of code, raising the specified exception if it exceeds the given duration during a non-blocking operation.
|
||||
def with_timeout(duration, exception = TimeoutError, message = "execution expired", &block)
|
||||
Fiber.scheduler.with_timeout(duration, exception, message, &block)
|
||||
end
|
||||
|
||||
# Yield back to the reactor and allow other fibers to execute.
|
||||
def yield
|
||||
Fiber.scheduler.yield
|
||||
end
|
||||
|
||||
# @attribute [Fiber] The fiber which is being used for the execution of this task.
|
||||
attr :fiber
|
||||
|
||||
# @returns [Boolean] Whether the internal fiber is alive, i.e. it is actively executing.
|
||||
def alive?
|
||||
@fiber&.alive?
|
||||
end
|
||||
|
||||
# Whether we can remove this node from the reactor graph.
|
||||
# @returns [Boolean]
|
||||
def finished?
|
||||
# If the block is nil and the fiber is nil, it means the task has finished execution. This becomes true after `finish!` is called.
|
||||
super && @block.nil? && @fiber.nil?
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether the task is running.
|
||||
def running?
|
||||
@status == :running
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether the task failed with an exception.
|
||||
def failed?
|
||||
@status == :failed
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether the task has been stopped.
|
||||
def stopped?
|
||||
@status == :stopped
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether the task has completed execution and generated a result.
|
||||
def completed?
|
||||
@status == :completed
|
||||
end
|
||||
|
||||
# Alias for {#completed?}.
|
||||
def complete?
|
||||
self.completed?
|
||||
end
|
||||
|
||||
# @attribute [Symbol] The status of the execution of the task, one of `:initialized`, `:running`, `:complete`, `:stopped` or `:failed`.
|
||||
attr :status
|
||||
|
||||
# Begin the execution of the task.
|
||||
#
|
||||
# @raises [RuntimeError] If the task is already running.
|
||||
def run(*arguments)
|
||||
if @status == :initialized
|
||||
@status = :running
|
||||
|
||||
schedule do
|
||||
@block.call(self, *arguments)
|
||||
rescue => error
|
||||
# I'm not completely happy with this overhead, but the alternative is to not log anything which makes debugging extremely difficult. Maybe we can introduce a debug wrapper which adds extra logging.
|
||||
if @finished.nil?
|
||||
warn(self, "Task may have ended with unhandled exception.", exception: error)
|
||||
end
|
||||
|
||||
raise
|
||||
end
|
||||
else
|
||||
raise RuntimeError, "Task already running!"
|
||||
end
|
||||
end
|
||||
|
||||
# Run an asynchronous task as a child of the current task.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
# @asynchronous May context switch immediately to the new task.
|
||||
#
|
||||
# @yields {|task| ...} in the context of the new task.
|
||||
# @raises [FinishedError] If the task has already finished.
|
||||
# @returns [Task] The child task.
|
||||
def async(*arguments, **options, &block)
|
||||
raise FinishedError if self.finished?
|
||||
|
||||
task = Task.new(self, **options, &block)
|
||||
|
||||
# When calling an async block, we deterministically execute it until the first blocking operation. We don't *have* to do this - we could schedule it for later execution, but it's useful to:
|
||||
#
|
||||
# - Fail at the point of the method call where possible.
|
||||
# - Execute determinstically where possible.
|
||||
# - Avoid scheduler overhead if no blocking operation is performed.
|
||||
#
|
||||
# There are different strategies (greedy vs non-greedy). We are currently using a greedy strategy.
|
||||
task.run(*arguments)
|
||||
|
||||
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`.
|
||||
#
|
||||
# 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).
|
||||
#
|
||||
# @raises [RuntimeError] If the task's fiber is the current fiber.
|
||||
# @returns [Object] The final expression/result of the task's block.
|
||||
def wait
|
||||
raise "Cannot wait on own fiber!" if Fiber.current.equal?(@fiber)
|
||||
|
||||
# `finish!` will set both of these to nil before signaling the condition:
|
||||
if @block || @fiber
|
||||
@finished ||= Condition.new
|
||||
@finished.wait
|
||||
end
|
||||
|
||||
if @status == :failed
|
||||
raise @result
|
||||
else
|
||||
return @result
|
||||
end
|
||||
end
|
||||
|
||||
# Access the result of the task without waiting. May be nil if the task is not completed. Does not raise exceptions.
|
||||
attr :result
|
||||
|
||||
# Stop 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.
|
||||
#
|
||||
# @parameter later [Boolean] Whether to stop the task later, or immediately.
|
||||
def stop(later = false)
|
||||
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!
|
||||
end
|
||||
|
||||
# If the fiber is alive, we need to stop 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 = true
|
||||
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 later
|
||||
# If the fiber is the current fiber and we want to stop it later, schedule it:
|
||||
Fiber.scheduler.push(Stop::Later.new(self))
|
||||
else
|
||||
# Otherwise, raise the exception directly:
|
||||
raise Stop, "Stopping current task!"
|
||||
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)
|
||||
rescue FiberError => error
|
||||
# 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))
|
||||
end
|
||||
end
|
||||
else
|
||||
# We are not running, but children might be, so transition directly into stopped state:
|
||||
stop!
|
||||
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.
|
||||
#
|
||||
# You can nest calls to defer_stop, but the stop will only be deferred until the outermost block exits.
|
||||
#
|
||||
# If stop 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?
|
||||
begin
|
||||
# If we are not deferring stop already, we can defer it now:
|
||||
@defer_stop = false
|
||||
|
||||
yield
|
||||
rescue Stop
|
||||
# If we are exiting due to a stop, we shouldn't try to invoke stop again:
|
||||
@defer_stop = nil
|
||||
raise
|
||||
ensure
|
||||
defer_stop = @defer_stop
|
||||
|
||||
# We need to ensure the state is reset before we exit the block:
|
||||
@defer_stop = nil
|
||||
|
||||
# If we were asked to stop, we should do so now:
|
||||
if defer_stop
|
||||
raise Stop, "Stopping current task (was deferred)!"
|
||||
end
|
||||
end
|
||||
else
|
||||
# If we are deferring stop already, entering it again is a no-op.
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether stop has been deferred.
|
||||
def stop_deferred?
|
||||
@defer_stop
|
||||
end
|
||||
|
||||
# Lookup the {Task} for the current fiber. Raise `RuntimeError` if none is available.
|
||||
# @returns [Task]
|
||||
# @raises[RuntimeError] If task was not {set!} for the current fiber.
|
||||
def self.current
|
||||
Fiber.current.async_task or raise RuntimeError, "No async task available!"
|
||||
end
|
||||
|
||||
# Check if there is a task defined for the current fiber.
|
||||
# @returns [Interface(:async) | Nil]
|
||||
def self.current?
|
||||
Fiber.current.async_task
|
||||
end
|
||||
|
||||
# @returns [Boolean] Whether this task is the currently executing task.
|
||||
def current?
|
||||
Fiber.current.equal?(@fiber)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def warn(...)
|
||||
Console.warn(...)
|
||||
end
|
||||
|
||||
# Finish the current task, moving any children to the parent.
|
||||
def finish!
|
||||
# Don't hold references to the fiber or block after the task has finished:
|
||||
@fiber = nil
|
||||
@block = nil # If some how we went directly from initialized to finished.
|
||||
|
||||
# Attempt to remove this node from the task tree.
|
||||
consume
|
||||
|
||||
# If this task was being used as a future, signal completion here:
|
||||
if @finished
|
||||
@finished.signal(self)
|
||||
@finished = nil
|
||||
end
|
||||
end
|
||||
|
||||
# State transition into the completed state.
|
||||
def completed!(result)
|
||||
@result = result
|
||||
@status = :completed
|
||||
end
|
||||
|
||||
# State transition into the failed state.
|
||||
def failed!(exception = false)
|
||||
@result = exception
|
||||
@status = :failed
|
||||
end
|
||||
|
||||
def stopped!
|
||||
# Console.info(self, status:) {"Task #{self} was stopped with #{@children&.size.inspect} children!"}
|
||||
@status = :stopped
|
||||
|
||||
stopped = 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.
|
||||
retry
|
||||
end
|
||||
|
||||
if stopped
|
||||
raise Stop, "Stopping current task!"
|
||||
end
|
||||
end
|
||||
|
||||
def stop!
|
||||
stopped!
|
||||
|
||||
finish!
|
||||
end
|
||||
|
||||
def schedule(&block)
|
||||
@fiber = Fiber.new(annotation: self.annotation) do
|
||||
begin
|
||||
completed!(yield)
|
||||
rescue Stop
|
||||
stopped!
|
||||
rescue StandardError => error
|
||||
failed!(error)
|
||||
rescue Exception => exception
|
||||
failed!(exception)
|
||||
|
||||
# This is a critical failure, we should stop the reactor:
|
||||
raise
|
||||
ensure
|
||||
# Console.info(self) {"Task ensure $! = #{$!} with #{@children&.size.inspect} children!"}
|
||||
finish!
|
||||
end
|
||||
end
|
||||
|
||||
@fiber.async_task = self
|
||||
|
||||
self.root.resume(@fiber)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2021-2024, by Samuel Williams.
|
||||
|
||||
require_relative "condition"
|
||||
|
||||
module Async
|
||||
# A synchronization primitive that allows one task to wait for another task to resolve a value.
|
||||
class Variable
|
||||
# Create a new variable.
|
||||
#
|
||||
# @parameter condition [Condition] The condition to use for synchronization.
|
||||
def initialize(condition = Condition.new)
|
||||
@condition = condition
|
||||
@value = nil
|
||||
end
|
||||
|
||||
# Resolve the value.
|
||||
#
|
||||
# Signals all waiting tasks.
|
||||
#
|
||||
# @parameter value [Object] The value to resolve.
|
||||
def resolve(value = true)
|
||||
@value = value
|
||||
condition = @condition
|
||||
@condition = nil
|
||||
|
||||
self.freeze
|
||||
|
||||
condition.signal(value)
|
||||
end
|
||||
|
||||
# Alias for {#resolve}.
|
||||
def value=(value)
|
||||
self.resolve(value)
|
||||
end
|
||||
|
||||
# Whether the value has been resolved.
|
||||
#
|
||||
# @returns [Boolean] Whether the value has been resolved.
|
||||
def resolved?
|
||||
@condition.nil?
|
||||
end
|
||||
|
||||
# Wait for the value to be resolved.
|
||||
#
|
||||
# @returns [Object] The resolved value.
|
||||
def wait
|
||||
@condition&.wait
|
||||
return @value
|
||||
end
|
||||
|
||||
# Alias for {#wait}.
|
||||
def value
|
||||
self.wait
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2017-2024, by Samuel Williams.
|
||||
|
||||
module Async
|
||||
VERSION = "2.23.0"
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
A synchronization primitive, which allows you to wait for tasks to complete in order of completion. This is useful for implementing a task pool, where you want to wait for the first task to complete, and then cancel the rest.
|
||||
|
||||
If you try to wait for more things than you have added, you will deadlock.
|
||||
|
||||
## Example
|
||||
|
||||
~~~ ruby
|
||||
require 'async'
|
||||
require 'async/semaphore'
|
||||
require 'async/barrier'
|
||||
require 'async/waiter'
|
||||
|
||||
Sync do
|
||||
barrier = Async::Barrier.new
|
||||
waiter = Async::Waiter.new(parent: barrier)
|
||||
semaphore = Async::Semaphore.new(2, parent: waiter)
|
||||
|
||||
# Sleep sort the numbers:
|
||||
generator = Async do
|
||||
while true
|
||||
semaphore.async do |task|
|
||||
number = rand(1..10)
|
||||
sleep(number)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
numbers = []
|
||||
|
||||
4.times do
|
||||
# Wait for all the numbers to be sorted:
|
||||
numbers << waiter.wait
|
||||
end
|
||||
|
||||
# Don't generate any more numbers:
|
||||
generator.stop
|
||||
|
||||
# Stop all tasks which we don't care about:
|
||||
barrier.stop
|
||||
|
||||
Console.info("Smallest", numbers)
|
||||
end
|
||||
~~~
|
||||
|
||||
### Output
|
||||
|
||||
~~~
|
||||
0.0s info: Smallest
|
||||
| [3, 3, 1, 2]
|
||||
~~~
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2022-2024, by Samuel Williams.
|
||||
# Copyright, 2024, by Patrik Wenger.
|
||||
|
||||
module Async
|
||||
# A composable synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore} and/or {Barrier}.
|
||||
class Waiter
|
||||
# Create a waiter instance.
|
||||
#
|
||||
# @parameter parent [Interface(:async) | Nil] The parent task to use for asynchronous operations.
|
||||
# @parameter finished [Async::Condition] The condition to signal when a task completes.
|
||||
def initialize(parent: nil, finished: Async::Condition.new)
|
||||
@finished = finished
|
||||
@done = []
|
||||
|
||||
@parent = parent
|
||||
end
|
||||
|
||||
# Execute a child task and add it to the waiter.
|
||||
# @asynchronous Executes the given block concurrently.
|
||||
def async(parent: (@parent or Task.current), **options, &block)
|
||||
parent.async(**options) do |task|
|
||||
yield(task)
|
||||
ensure
|
||||
@done << task
|
||||
@finished.signal
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for the first `count` tasks to complete.
|
||||
# @parameter count [Integer | Nil] The number of tasks to wait for.
|
||||
# @returns [Array(Async::Task)] If an integer is given, the tasks which have completed.
|
||||
# @returns [Async::Task] Otherwise, the first task to complete.
|
||||
def first(count = nil)
|
||||
minimum = count || 1
|
||||
|
||||
while @done.size < minimum
|
||||
@finished.wait
|
||||
end
|
||||
|
||||
return @done.shift(*count)
|
||||
end
|
||||
|
||||
# Wait for the first `count` tasks to complete.
|
||||
# @parameter count [Integer | Nil] The number of tasks to wait for.
|
||||
def wait(count = nil)
|
||||
if count
|
||||
first(count).map(&:wait)
|
||||
else
|
||||
first.wait
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,182 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
require "etc"
|
||||
|
||||
module Async
|
||||
# A simple work pool that offloads work to a background thread.
|
||||
#
|
||||
# @private
|
||||
class WorkerPool
|
||||
# Used to augment the scheduler to add support for blocking operations.
|
||||
module BlockingOperationWait
|
||||
# Wait for the given work to be executed.
|
||||
#
|
||||
# @public Since *Async v2.19* and *Ruby v3.4*.
|
||||
# @asynchronous May be non-blocking.
|
||||
#
|
||||
# @parameter work [Proc] The work to execute on a background thread.
|
||||
# @returns [Object] The result of the work.
|
||||
def blocking_operation_wait(work)
|
||||
@worker_pool.call(work)
|
||||
end
|
||||
end
|
||||
|
||||
# Execute the given work in a background thread.
|
||||
class Promise
|
||||
# Create a new promise.
|
||||
#
|
||||
# @parameter work [Proc] The work to be done.
|
||||
def initialize(work)
|
||||
@work = work
|
||||
@state = :pending
|
||||
@value = nil
|
||||
@guard = ::Mutex.new
|
||||
@condition = ::ConditionVariable.new
|
||||
@thread = nil
|
||||
end
|
||||
|
||||
# Execute the work and resolve the promise.
|
||||
def call
|
||||
work = nil
|
||||
|
||||
@guard.synchronize do
|
||||
@thread = ::Thread.current
|
||||
|
||||
return unless work = @work
|
||||
end
|
||||
|
||||
resolve(work.call)
|
||||
rescue Exception => error
|
||||
reject(error)
|
||||
end
|
||||
|
||||
private def resolve(value)
|
||||
@guard.synchronize do
|
||||
@work = nil
|
||||
@thread = nil
|
||||
@value = value
|
||||
@state = :resolved
|
||||
@condition.broadcast
|
||||
end
|
||||
end
|
||||
|
||||
private def reject(error)
|
||||
@guard.synchronize do
|
||||
@work = nil
|
||||
@thread = nil
|
||||
@value = error
|
||||
@state = :failed
|
||||
@condition.broadcast
|
||||
end
|
||||
end
|
||||
|
||||
# Cancel the work and raise an exception in the background thread.
|
||||
def cancel
|
||||
return unless @work
|
||||
|
||||
@guard.synchronize do
|
||||
@work = nil
|
||||
@state = :cancelled
|
||||
@thread&.raise(Interrupt)
|
||||
end
|
||||
end
|
||||
|
||||
# Wait for the work to be done.
|
||||
#
|
||||
# @returns [Object] The result of the work.
|
||||
def wait
|
||||
@guard.synchronize do
|
||||
while @state == :pending
|
||||
@condition.wait(@guard)
|
||||
end
|
||||
|
||||
if @state == :failed
|
||||
raise @value
|
||||
else
|
||||
return @value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# A background worker thread.
|
||||
class Worker
|
||||
# Create a new worker.
|
||||
def initialize
|
||||
@work = ::Thread::Queue.new
|
||||
@thread = ::Thread.new(&method(:run))
|
||||
end
|
||||
|
||||
# Execute work until the queue is closed.
|
||||
def run
|
||||
while work = @work.pop
|
||||
work.call
|
||||
end
|
||||
end
|
||||
|
||||
# Close the worker thread.
|
||||
def close
|
||||
if thread = @thread
|
||||
@thread = nil
|
||||
thread.kill
|
||||
end
|
||||
end
|
||||
|
||||
# Call the work and notify the scheduler when it is done.
|
||||
def call(work)
|
||||
promise = Promise.new(work)
|
||||
|
||||
@work.push(promise)
|
||||
|
||||
begin
|
||||
return promise.wait
|
||||
ensure
|
||||
promise.cancel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new work pool.
|
||||
#
|
||||
# @parameter size [Integer] The number of threads to use.
|
||||
def initialize(size: Etc.nprocessors)
|
||||
@ready = ::Thread::Queue.new
|
||||
|
||||
size.times do
|
||||
@ready.push(Worker.new)
|
||||
end
|
||||
end
|
||||
|
||||
# Close the work pool. Kills all outstanding work.
|
||||
def close
|
||||
if ready = @ready
|
||||
@ready = nil
|
||||
ready.close
|
||||
|
||||
while worker = ready.pop
|
||||
worker.close
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Offload work to a thread.
|
||||
#
|
||||
# @parameter work [Proc] The work to be done.
|
||||
def call(work)
|
||||
if ready = @ready
|
||||
worker = ready.pop
|
||||
|
||||
begin
|
||||
worker.call(work)
|
||||
ensure
|
||||
ready.push(worker)
|
||||
end
|
||||
else
|
||||
raise RuntimeError, "No worker available!"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2017-2024, by Samuel Williams.
|
||||
# Copyright, 2017, by Kent Gruber.
|
||||
|
||||
warn "Async::Wrapper is deprecated and will be removed on 2025-03-31. Please use native interfaces instead.", uplevel: 1, category: :deprecated
|
||||
|
||||
module Async
|
||||
# Represents an asynchronous IO within a reactor.
|
||||
# @deprecated With no replacement. Prefer native interfaces.
|
||||
class Wrapper
|
||||
# An exception that occurs when the asynchronous operation was cancelled.
|
||||
class Cancelled < StandardError
|
||||
end
|
||||
|
||||
# @parameter io the native object to wrap.
|
||||
# @parameter reactor [Reactor] the reactor that is managing this wrapper, or not specified, it's looked up by way of {Task.current}.
|
||||
def initialize(io, reactor = nil)
|
||||
@io = io
|
||||
@reactor = reactor
|
||||
|
||||
@timeout = nil
|
||||
end
|
||||
|
||||
attr_accessor :reactor
|
||||
|
||||
# Dup the underlying IO.
|
||||
def dup
|
||||
self.class.new(@io.dup)
|
||||
end
|
||||
|
||||
# The underlying native `io`.
|
||||
attr :io
|
||||
|
||||
# Wait for the io to become readable.
|
||||
def wait_readable(timeout = @timeout)
|
||||
@io.to_io.wait_readable(timeout) or raise TimeoutError
|
||||
end
|
||||
|
||||
# Wait for the io to become writable.
|
||||
def wait_priority(timeout = @timeout)
|
||||
@io.to_io.wait_priority(timeout) or raise TimeoutError
|
||||
end
|
||||
|
||||
# Wait for the io to become writable.
|
||||
def wait_writable(timeout = @timeout)
|
||||
@io.to_io.wait_writable(timeout) or raise TimeoutError
|
||||
end
|
||||
|
||||
# Wait fo the io to become either readable or writable.
|
||||
# @parameter duration [Float] timeout after the given duration if not `nil`.
|
||||
def wait_any(timeout = @timeout)
|
||||
@io.to_io.wait(::IO::READABLE|::IO::WRITABLE|::IO::PRIORITY, timeout) or raise TimeoutError
|
||||
end
|
||||
|
||||
# Close the underlying IO.
|
||||
def close
|
||||
@io.close
|
||||
end
|
||||
|
||||
# Whether the underlying IO is closed.
|
||||
def closed?
|
||||
@io.closed?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2019-2024, by Samuel Williams.
|
||||
|
||||
require_relative "../async/reactor"
|
||||
|
||||
module Kernel
|
||||
# Run the given block of code in a task, asynchronously, creating a reactor if necessary.
|
||||
#
|
||||
# The preferred method to invoke asynchronous behavior at the top level.
|
||||
#
|
||||
# - When invoked within an existing reactor task, it will run the given block
|
||||
# asynchronously. Will return the task once it has been scheduled.
|
||||
# - When invoked at the top level, will create and run a reactor, and invoke
|
||||
# the block as an asynchronous task. Will block until the reactor finishes
|
||||
# running.
|
||||
#
|
||||
# @yields {|task| ...} The block that will execute asynchronously.
|
||||
# @parameter task [Async::Task] The task that is executing the given block.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
# @asynchronous May block until given block completes executing.
|
||||
def Async(...)
|
||||
if current = ::Async::Task.current?
|
||||
return current.async(...)
|
||||
elsif scheduler = Fiber.scheduler
|
||||
::Async::Task.run(scheduler, ...)
|
||||
else
|
||||
# This calls Fiber.set_scheduler(self):
|
||||
reactor = ::Async::Reactor.new
|
||||
|
||||
begin
|
||||
return reactor.run(...)
|
||||
ensure
|
||||
Fiber.set_scheduler(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2019-2024, by Samuel Williams.
|
||||
# Copyright, 2020, by Brian Morearty.
|
||||
# Copyright, 2024, by Patrik Wenger.
|
||||
|
||||
require_relative "../async/reactor"
|
||||
|
||||
# Extensions to all Ruby objects.
|
||||
module Kernel
|
||||
# Run the given block of code synchronously, but within a reactor if not already in one.
|
||||
#
|
||||
# @yields {|task| ...} The block that will execute asynchronously.
|
||||
# @parameter task [Async::Task] The task that is executing the given block.
|
||||
#
|
||||
# @public Since *Async v1*.
|
||||
# @asynchronous Will block until given block completes executing.
|
||||
def Sync(annotation: nil, &block)
|
||||
if task = ::Async::Task.current?
|
||||
if annotation
|
||||
task.annotate(annotation) {yield task}
|
||||
else
|
||||
yield task
|
||||
end
|
||||
elsif scheduler = Fiber.scheduler
|
||||
::Async::Task.run(scheduler, &block).wait
|
||||
else
|
||||
# This calls Fiber.set_scheduler(self):
|
||||
reactor = Async::Reactor.new
|
||||
|
||||
begin
|
||||
return reactor.run(annotation: annotation, finished: ::Async::Condition.new, &block).wait
|
||||
ensure
|
||||
Fiber.set_scheduler(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
require_relative "async/task"
|
||||
@@ -0,0 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
require_relative "../../../async/task"
|
||||
require "metrics/provider"
|
||||
|
||||
Metrics::Provider(Async::Task) do
|
||||
ASYNC_TASK_SCHEDULED = Metrics.metric("async.task.scheduled", :counter, description: "The number of tasks scheduled.")
|
||||
ASYNC_TASK_FINISHED = Metrics.metric("async.task.finished", :counter, description: "The number of tasks finished.")
|
||||
|
||||
def schedule(&block)
|
||||
ASYNC_TASK_SCHEDULED.emit(1)
|
||||
|
||||
super(&block)
|
||||
ensure
|
||||
ASYNC_TASK_FINISHED.emit(1)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2024, by Samuel Williams.
|
||||
|
||||
require_relative "async/task"
|
||||
require_relative "async/barrier"
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2022, by Samuel Williams.
|
||||
|
||||
require_relative "../../../async/barrier"
|
||||
require "traces/provider"
|
||||
|
||||
Traces::Provider(Async::Barrier) do
|
||||
def wait
|
||||
attributes = {
|
||||
"size" => self.size
|
||||
}
|
||||
|
||||
Traces.trace("async.barrier.wait", attributes: attributes) {super}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Released under the MIT License.
|
||||
# Copyright, 2022, by Samuel Williams.
|
||||
|
||||
require_relative "../../../async/task"
|
||||
require "traces/provider"
|
||||
|
||||
Traces::Provider(Async::Task) do
|
||||
def schedule(&block)
|
||||
# If we are not actively tracing anything, then we can skip this:
|
||||
unless Traces.active?
|
||||
return super(&block)
|
||||
end
|
||||
|
||||
unless self.transient?
|
||||
trace_context = Traces.trace_context
|
||||
end
|
||||
|
||||
attributes = {
|
||||
# We use the instance variable as it corresponds to the user-provided block.
|
||||
"block" => @block,
|
||||
"transient" => self.transient?,
|
||||
}
|
||||
|
||||
# Run the trace in the context of the child task:
|
||||
super do
|
||||
Traces.trace_context = trace_context
|
||||
|
||||
if annotation = self.annotation
|
||||
attributes["annotation"] = annotation
|
||||
end
|
||||
|
||||
Traces.trace("async.task", attributes: attributes) do
|
||||
# Yes, this is correct, we already called super above:
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
# MIT License
|
||||
|
||||
Copyright, 2017-2024, by Samuel Williams.
|
||||
Copyright, 2017, by Kent Gruber.
|
||||
Copyright, 2017, by Devin Christensen.
|
||||
Copyright, 2018, by Sokolov Yura.
|
||||
Copyright, 2018, by Jiang Jinyang.
|
||||
Copyright, 2019, by Jeremy Jung.
|
||||
Copyright, 2019, by Ryan Musgrave.
|
||||
Copyright, 2020-2023, by Olle Jonsson.
|
||||
Copyright, 2020, by Salim Semaoune.
|
||||
Copyright, 2020, by Brian Morearty.
|
||||
Copyright, 2020, by Stefan Wrobel.
|
||||
Copyright, 2020-2024, by Patrik Wenger.
|
||||
Copyright, 2020, by Ken Muryoi.
|
||||
Copyright, 2020, by Jun Jiang.
|
||||
Copyright, 2020-2022, by Bruno Sutic.
|
||||
Copyright, 2021, by Julien Portalier.
|
||||
Copyright, 2022, by Shannon Skipper.
|
||||
Copyright, 2022, by Masafumi Okura.
|
||||
Copyright, 2022, by Trevor Turk.
|
||||
Copyright, 2022, by Masayuki Yamamoto.
|
||||
Copyright, 2023, by Leon Löchner.
|
||||
Copyright, 2023, by Colin Kelley.
|
||||
Copyright, 2023, by Math Ieu.
|
||||
Copyright, 2023, by Emil Tin.
|
||||
Copyright, 2023, by Gert Goet.
|
||||
Copyright, 2024, by Dimitar Peychinov.
|
||||
Copyright, 2024, by Jamie McCarthy.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,94 @@
|
||||
# 
|
||||
|
||||
Async is a composable asynchronous I/O framework for Ruby based on [io-event](https://github.com/socketry/io-event).
|
||||
|
||||
> "Lately I've been looking into `async`, as one of my projects –
|
||||
> [tus-ruby-server](https://github.com/janko/tus-ruby-server) – would really benefit from non-blocking I/O. It's really
|
||||
> beautifully designed." *– [janko](https://github.com/janko)*
|
||||
|
||||
[](https://github.com/socketry/async/actions?workflow=Test)
|
||||
[<img src="https://api.gitsponsors.com/api/badge/img?id=87380483" height="20"/>](https://api.gitsponsors.com/api/badge/link?p=U4gCxvzG7eUksiJSe0MSlPHWWhBYryqj6i48tx5L7/r/2NgkAToKb6dEm31bAftU3H+7BVwk3VhUBtE4GHqHJTPfWPR6xo2BQVoT15rFAGAsLFgdT2kKopIfCGV/QDOm7BrkodS2//R7NUMksAdaCQ==)
|
||||
|
||||
## Features
|
||||
|
||||
- Scalable event-driven I/O for Ruby. Thousands of clients per process\!
|
||||
- Light weight fiber-based concurrency. No need for callbacks\!
|
||||
- Multi-thread/process containers for parallelism.
|
||||
- Growing eco-system of event-driven components.
|
||||
|
||||
## Usage
|
||||
|
||||
Please see the [project documentation](https://socketry.github.io/async/) for more details.
|
||||
|
||||
- [Getting Started](https://socketry.github.io/async/guides/getting-started/index) - This guide shows how to add async to your project and run code asynchronously.
|
||||
|
||||
- [Asynchronous Tasks](https://socketry.github.io/async/guides/asynchronous-tasks/index) - This guide explains how asynchronous tasks work and how to use them.
|
||||
|
||||
- [Scheduler](https://socketry.github.io/async/guides/scheduler/index) - This guide gives an overview of how the scheduler is implemented.
|
||||
|
||||
- [Compatibility](https://socketry.github.io/async/guides/compatibility/index) - This guide gives an overview of the compatibility of Async with Ruby and other frameworks.
|
||||
|
||||
- [Best Practices](https://socketry.github.io/async/guides/best-practices/index) - This guide gives an overview of best practices for using Async.
|
||||
|
||||
- [Debugging](https://socketry.github.io/async/guides/debugging/index) - This guide explains how to debug issues with programs that use Async.
|
||||
|
||||
## Releases
|
||||
|
||||
Please see the [project releases](https://socketry.github.io/async/releases/index) for all releases.
|
||||
|
||||
### v2.23.0
|
||||
|
||||
- Rename `ASYNC_SCHEDULER_DEFAULT_WORKER_POOL` to `ASYNC_SCHEDULER_WORKER_POOL`.
|
||||
- [Fiber Stall Profiler](https://socketry.github.io/async/releases/index#fiber-stall-profiler)
|
||||
|
||||
### v2.21.1
|
||||
|
||||
- [Worker Pool](https://socketry.github.io/async/releases/index#worker-pool)
|
||||
|
||||
### v2.20.0
|
||||
|
||||
- [Traces and Metrics Providers](https://socketry.github.io/async/releases/index#traces-and-metrics-providers)
|
||||
|
||||
### v2.19.0
|
||||
|
||||
- [Async::Scheduler Debugging](https://socketry.github.io/async/releases/index#async::scheduler-debugging)
|
||||
- [Console Shims](https://socketry.github.io/async/releases/index#console-shims)
|
||||
|
||||
### v2.18.0
|
||||
|
||||
- Add support for `Sync(annotation:)`, so that you can annotate the block with a description of what it does, even if it doesn't create a new task.
|
||||
|
||||
### v2.17.0
|
||||
|
||||
- Introduce `Async::Queue#push` and `Async::Queue#pop` for compatibility with `::Queue`.
|
||||
|
||||
### v2.16.0
|
||||
|
||||
- [Better Handling of Async and Sync in Nested Fibers](https://socketry.github.io/async/releases/index#better-handling-of-async-and-sync-in-nested-fibers)
|
||||
|
||||
## See Also
|
||||
|
||||
- [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client/server.
|
||||
- [async-websocket](https://github.com/socketry/async-websocket) — Asynchronous client and server websockets.
|
||||
- [async-dns](https://github.com/socketry/async-dns) — Asynchronous DNS resolver and server.
|
||||
- [falcon](https://github.com/socketry/falcon) — A rack compatible server built on top of `async-http`.
|
||||
- [rubydns](https://github.com/ioquatix/rubydns) — An easy to use Ruby DNS server.
|
||||
- [slack-ruby-bot](https://github.com/slack-ruby/slack-ruby-bot) — A client for making slack bots.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions to this project.
|
||||
|
||||
1. Fork it.
|
||||
2. Create your feature branch (`git checkout -b my-new-feature`).
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`).
|
||||
4. Push to the branch (`git push origin my-new-feature`).
|
||||
5. Create new Pull Request.
|
||||
|
||||
### Developer Certificate of Origin
|
||||
|
||||
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
||||
|
||||
### Community Guidelines
|
||||
|
||||
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
||||
@@ -0,0 +1,121 @@
|
||||
# Releases
|
||||
|
||||
## v2.23.0
|
||||
|
||||
- Rename `ASYNC_SCHEDULER_DEFAULT_WORKER_POOL` to `ASYNC_SCHEDULER_WORKER_POOL`.
|
||||
|
||||
### Fiber Stall Profiler
|
||||
|
||||
After several iterations of experimentation, we are officially introducing the fiber stall profiler, implemented using the optional `fiber-profiler` gem. This gem is not included by default, but can be added to your project:
|
||||
|
||||
``` bash
|
||||
$ bundle add fiber-profiler
|
||||
```
|
||||
|
||||
After adding the gem, you can enable the fiber stall profiler by setting the `FIBER_PROFILER_CAPTURE=true` environment variable:
|
||||
|
||||
``` bash
|
||||
$ FIBER_PROFILER_CAPTURE=true bundle exec ruby -rasync -e 'Async{Fiber.blocking{sleep 0.1}}'
|
||||
Fiber stalled for 0.105 seconds
|
||||
-e:1 in c-call '#<Class:Fiber>#blocking' (0.105s)
|
||||
-e:1 in c-call 'Kernel#sleep' (0.105s)
|
||||
Skipped 1 calls that were too short to be meaningful.
|
||||
```
|
||||
|
||||
The fiber profiler will help you find problems with your code that cause the event loop to stall, which can be a common source of performance issues in asynchronous code.
|
||||
|
||||
## v2.21.1
|
||||
|
||||
### Worker Pool
|
||||
|
||||
Ruby 3.4 will feature a new fiber scheduler hook, `blocking_operation_wait` which allows the scheduler to redirect the work given to `rb_nogvl` to a worker pool.
|
||||
|
||||
The Async scheduler optionally supports this feature using a worker pool, by using the following environment variable:
|
||||
|
||||
ASYNC_SCHEDULER_WORKER_POOL=true
|
||||
|
||||
This will cause the scheduler to use a worker pool for general blocking operations, rather than blocking the event loop.
|
||||
|
||||
It should be noted that this isn't a net win, as the overhead of using a worker pool can be significant compared to the `rb_nogvl` work. As such, it is recommended to benchmark your application with and without the worker pool to determine if it is beneficial.
|
||||
|
||||
## v2.20.0
|
||||
|
||||
### Traces and Metrics Providers
|
||||
|
||||
Async now has [traces](https://github.com/socketry/traces) and [metrics](https://github.com/socketry/metrics) providers for various core classes. This allows you to emit traces and metrics to a suitable backend (including DataDog, New Relic, OpenTelemetry, etc.) for monitoring and debugging purposes.
|
||||
|
||||
To take advantage of this feature, you will need to introduce your own `config/traces.rb` and `config/metrics.rb`. Async's own repository includes these files for testing purposes, you could copy them into your own project and modify them as needed.
|
||||
|
||||
## v2.19.0
|
||||
|
||||
### Async::Scheduler Debugging
|
||||
|
||||
Occasionally on issues, I encounter people asking for help and I need more information. Pressing Ctrl-C to exit a hung program is common, but it usually doesn't provide enough information to diagnose the problem. Setting the `CONSOLE_LEVEL=debug` environment variable will now print additional information about the scheduler when you interrupt it, including a backtrace of the current tasks.
|
||||
|
||||
> CONSOLE_LEVEL=debug bundle exec ruby ./test.rb
|
||||
^C 0.0s debug: Async::Reactor [oid=0x974] [ec=0x988] [pid=9116] [2024-11-08 14:12:03 +1300]
|
||||
| Scheduler interrupted: Interrupt
|
||||
| #<Async::Reactor:0x0000000000000974 1 children (running)>
|
||||
| #<Async::Task:0x000000000000099c /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:185:in `transfer' (running)>
|
||||
| → /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:185:in `transfer'
|
||||
| /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:185:in `block'
|
||||
| /Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:207:in `kernel_sleep'
|
||||
| /Users/samuel/Developer/socketry/async/test.rb:7:in `sleep'
|
||||
| /Users/samuel/Developer/socketry/async/test.rb:7:in `sleepy'
|
||||
| /Users/samuel/Developer/socketry/async/test.rb:12:in `block in <top (required)>'
|
||||
| /Users/samuel/Developer/socketry/async/lib/async/task.rb:197:in `block in run'
|
||||
| /Users/samuel/Developer/socketry/async/lib/async/task.rb:420:in `block in schedule'
|
||||
/Users/samuel/Developer/socketry/async/lib/async/scheduler.rb:317:in `select': Interrupt
|
||||
... (backtrace continues) ...
|
||||
|
||||
This gives better visibility into what the scheduler is doing, and should help diagnose issues.
|
||||
|
||||
### Console Shims
|
||||
|
||||
The `async` gem depends on `console` gem, because my goal was to have good logging by default without thinking about it too much. However, some users prefer to avoid using the `console` gem for logging, so I've added an experimental set of shims which should allow you to bypass the `console` gem entirely.
|
||||
|
||||
``` ruby
|
||||
require 'async/console'
|
||||
require 'async'
|
||||
|
||||
Async{raise "Boom"}
|
||||
```
|
||||
|
||||
Will now use `Kernel#warn` to print the task failure warning:
|
||||
|
||||
#<Async::Task:0x00000000000012d4 /home/samuel/Developer/socketry/async/lib/async/task.rb:104:in `backtrace' (running)>
|
||||
Task may have ended with unhandled exception.
|
||||
(irb):4:in `block in <top (required)>': Boom (RuntimeError)
|
||||
from /home/samuel/Developer/socketry/async/lib/async/task.rb:197:in `block in run'
|
||||
from /home/samuel/Developer/socketry/async/lib/async/task.rb:420:in `block in schedule'
|
||||
|
||||
## v2.18.0
|
||||
|
||||
- Add support for `Sync(annotation:)`, so that you can annotate the block with a description of what it does, even if it doesn't create a new task.
|
||||
|
||||
## v2.17.0
|
||||
|
||||
- Introduce `Async::Queue#push` and `Async::Queue#pop` for compatibility with `::Queue`.
|
||||
|
||||
## v2.16.0
|
||||
|
||||
### Better Handling of Async and Sync in Nested Fibers
|
||||
|
||||
Interleaving bare fibers within `Async` and `Sync` blocks should not cause problems, but it presents a number of issues in the current implementation. Tracking the parent-child relationship between tasks, when they are interleaved with bare fibers, is difficult. The current implementation assumes that if there is no parent task, then it should create a new reactor. This is not always the case, as the parent task might not be visible due to nested Fibers. As a result, `Async` will create a new reactor, trying to stop the existing one, causing major internal consistency issues.
|
||||
|
||||
I encountered this issue when trying to use `Async` within a streaming response in Rails. The `protocol-rack` [uses a normal fiber to wrap streaming responses](https://github.com/socketry/protocol-rack/blob/cb1ca44e9deadb9369bdb2ea03416556aa927c5c/lib/protocol/rack/body/streaming.rb#L24-L28), and if you try to use `Async` within it, it will create a new reactor, causing the server to lock up.
|
||||
|
||||
Ideally, `Async` and `Sync` helpers should work when any `Fiber.scheduler` is defined. Right now, it's unrealistic to expect `Async::Task` to work in any scheduler, but at the very least, the following should work:
|
||||
|
||||
``` ruby
|
||||
reactor = Async::Reactor.new # internally calls Fiber.set_scheduler
|
||||
|
||||
# This should run in the above reactor, rather than creating a new one.
|
||||
Async do
|
||||
puts "Hello World"
|
||||
end
|
||||
```
|
||||
|
||||
In order to do this, bare `Async` and `Sync` blocks should use `Fiber.scheduler` as a parent if possible.
|
||||
|
||||
See <https://github.com/socketry/async/pull/340> for more details.
|
||||
Reference in New Issue
Block a user