Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+15
View File
@@ -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
+74
View File
@@ -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
+59
View File
@@ -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"
+311
View File
@@ -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
+324
View File
@@ -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
+171
View File
@@ -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
+30
View File
@@ -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
```
+459
View File
@@ -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
+50
View File
@@ -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]
~~~
+56
View File
@@ -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
+40
View File
@@ -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
+39
View File
@@ -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
+47
View File
@@ -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.
+94
View File
@@ -0,0 +1,94 @@
# ![Async](assets/logo.webp)
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)*
[![Development Status](https://github.com/socketry/async/workflows/Test/badge.svg)](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.
+121
View File
@@ -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.