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
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2025, by Samuel Williams.
require_relative "event/version"
require_relative "event/selector"
require_relative "event/timers"
require_relative "event/native"
@@ -0,0 +1,171 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
require_relative "../support"
module IO::Event
# @namespace
module Debug
# Enforces the selector interface and delegates operations to a wrapped selector instance.
#
# You can enable this in the default selector by setting the `IO_EVENT_DEBUG_SELECTOR` environment variable. In addition, you can log all selector operations to a file by setting the `IO_EVENT_DEBUG_SELECTOR_LOG` environment variable. This is useful for debugging and understanding the behavior of the event loop.
class Selector
# Wrap the given selector with debugging.
#
# @parameter selector [Selector] The selector to wrap.
# @parameter env [Hash] The environment to read configuration from.
def self.wrap(selector, env = ENV)
log = nil
if log_path = env["IO_EVENT_DEBUG_SELECTOR_LOG"]
log = File.open(log_path, "w")
end
return self.new(selector, log: log)
end
# Initialize the debug selector with the given selector and optional log.
#
# @parameter selector [Selector] The selector to wrap.
# @parameter log [IO] The log to write debug messages to.
def initialize(selector, log: nil)
@selector = selector
@readable = {}
@writable = {}
@priority = {}
unless Fiber.current == selector.loop
Kernel::raise "Selector must be initialized on event loop fiber!"
end
@log = log
end
# The idle duration of the underlying selector.
#
# @returns [Numeric] The idle duration.
def idle_duration
@selector.idle_duration
end
# The current time.
#
# @returns [Numeric] The current time.
def now
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
# Log the given message.
#
# @asynchronous Will block the calling fiber and the entire event loop.
def log(message)
return unless @log
Fiber.blocking do
@log.puts("T+%10.1f; %s" % [now, message])
end
end
# Wakeup the the selector.
def wakeup
@selector.wakeup
end
# Close the selector.
def close
log("Closing selector")
if @selector.nil?
Kernel::raise "Selector already closed!"
end
@selector.close
@selector = nil
end
# Transfer from the calling fiber to the selector.
def transfer
log("Transfering to event loop")
@selector.transfer
end
# Resume the given fiber with the given arguments.
def resume(*arguments)
log("Resuming fiber with #{arguments.inspect}")
@selector.resume(*arguments)
end
# Yield to the selector.
def yield
log("Yielding to event loop")
@selector.yield
end
# Push the given fiber to the selector ready list, such that it will be resumed on the next call to {select}.
#
# @parameter fiber [Fiber] The fiber that is ready.
def push(fiber)
log("Pushing fiber #{fiber.inspect} to ready list")
@selector.push(fiber)
end
# Raise the given exception on the given fiber.
#
# @parameter fiber [Fiber] The fiber to raise the exception on.
# @parameter arguments [Array] The arguments to use when raising the exception.
def raise(fiber, *arguments)
log("Raising exception on fiber #{fiber.inspect} with #{arguments.inspect}")
@selector.raise(fiber, *arguments)
end
# Check if the selector is ready.
#
# @returns [Boolean] Whether the selector is ready.
def ready?
@selector.ready?
end
# Wait for the given process, forwarded to the underlying selector.
def process_wait(*arguments)
log("Waiting for process with #{arguments.inspect}")
@selector.process_wait(*arguments)
end
# Wait for the given IO, forwarded to the underlying selector.
def io_wait(fiber, io, events)
log("Waiting for IO #{io.inspect} for events #{events.inspect}")
@selector.io_wait(fiber, io, events)
end
# Read from the given IO, forwarded to the underlying selector.
def io_read(fiber, io, buffer, length, offset = 0)
log("Reading from IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}")
@selector.io_read(fiber, io, buffer, length, offset)
end
# Write to the given IO, forwarded to the underlying selector.
def io_write(fiber, io, buffer, length, offset = 0)
log("Writing to IO #{io.inspect} with buffer #{buffer}; length #{length} offset #{offset}")
@selector.io_write(fiber, io, buffer, length, offset)
end
# Forward the given method to the underlying selector.
def respond_to?(name, include_private = false)
@selector.respond_to?(name, include_private)
end
# Select for the given duration, forwarded to the underlying selector.
def select(duration = nil)
log("Selecting for #{duration.inspect}")
unless Fiber.current == @selector.loop
Kernel::raise "Selector must be run on event loop fiber!"
end
@selector.select(duration)
end
end
end
end
@@ -0,0 +1,44 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
module IO::Event
# A thread safe synchronisation primative.
class Interrupt
def self.attach(selector)
self.new(selector)
end
def initialize(selector)
@selector = selector
@input, @output = ::IO.pipe
@fiber = Fiber.new do
while true
if @selector.io_wait(@fiber, @input, IO::READABLE)
@input.read_nonblock(1)
end
end
end
@fiber.transfer
end
# Send a sigle byte interrupt.
def signal
@output.write(".")
@output.flush
rescue IOError
# Ignore.
end
def close
@input.close
@output.close
# @fiber.raise(::Interrupt)
end
end
private_constant :Interrupt
end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.
begin
require "IO_Event"
rescue LoadError => error
warn "Could not load native event selector: #{error}"
require_relative "selector/nonblock"
end
@@ -0,0 +1,147 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021, by Wander Hillen.
# Copyright, 2021-2024, by Samuel Williams.
class IO
module Event
# A priority queue implementation using a standard binary minheap. It uses straight comparison
# of its contents to determine priority.
# See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
class PriorityHeap
# Initializes the heap.
def initialize
# The heap is represented with an array containing a binary tree. See
# https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
# is built up.
@contents = []
end
# @returns [Object | Nil] the smallest element in the heap without removing it, or nil if the heap is empty.
def peek
@contents[0]
end
# @returns [Integer] the number of elements in the heap.
def size
@contents.size
end
# Removes and returns the smallest element in the heap, or nil if the heap is empty.
#
# @returns [Object | Nil] The smallest element in the heap, or nil if the heap is empty.
def pop
# If the heap is empty:
if @contents.empty?
return nil
end
# If we have only one item, no swapping is required:
if @contents.size == 1
return @contents.pop
end
# Take the root of the tree:
value = @contents[0]
# Remove the last item in the tree:
last = @contents.pop
# Overwrite the root of the tree with the item:
@contents[0] = last
# Bubble it down into place:
bubble_down(0)
# validate!
return value
end
# Add a new element to the heap, then rearrange elements until the heap invariant is true again.
#
# @parameter element [Object] The element to add to the heap.
def push(element)
# Insert the item at the end of the heap:
@contents.push(element)
# Bubble it up into position:
bubble_up(@contents.size - 1)
# validate!
return self
end
# Empties out the heap, discarding all elements
def clear!
@contents = []
end
# Validate the heap invariant. Every element except the root must not be smaller than its parent element. Note that it MAY be equal.
def valid?
# Notice we skip index 0 on purpose, because it has no parent
(1..(@contents.size - 1)).all? { |e| @contents[e] >= @contents[(e - 1) / 2] }
end
private
# Left here for reference, but unused.
# def swap(i, j)
# @contents[i], @contents[j] = @contents[j], @contents[i]
# end
def bubble_up(index)
parent_index = (index - 1) / 2 # watch out, integer division!
while index > 0 && @contents[index] < @contents[parent_index]
# If the node has a smaller value than its parent, swap these nodes to uphold the minheap invariant and update the index of the 'current' node. If the node is already at index 0, we can also stop because that is the root of the heap.
# swap(index, parent_index)
@contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
index = parent_index
parent_index = (index - 1) / 2 # watch out, integer division!
end
end
def bubble_down(index)
swap_value = 0
swap_index = nil
while true
left_index = (2 * index) + 1
left_value = @contents[left_index]
if left_value.nil?
# This node has no children so it can't bubble down any further. We're done here!
return
end
# Determine which of the child nodes has the smallest value:
right_index = left_index + 1
right_value = @contents[right_index]
if right_value.nil? or right_value > left_value
swap_value = left_value
swap_index = left_index
else
swap_value = right_value
swap_index = right_index
end
if @contents[index] < swap_value
# No need to swap, the minheap invariant is already satisfied:
return
else
# At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further:
# swap(index, swap_index)
@contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
index = swap_index
end
end
end
end
end
end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.
require_relative "native"
module IO::Event
unless self.const_defined?(:Profiler)
module Profiler
# The default profiler, if the platform supports it.
# Use `IO_EVENT_PROFILER=true` to enable it.
def self.default
nil
end
end
end
end
@@ -0,0 +1,48 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
require_relative "selector/select"
require_relative "debug/selector"
require_relative "support"
module IO::Event
# @namespace
module Selector
# The default selector implementation, which is chosen based on the environment and available implementations.
#
# @parameter env [Hash] The environment to read configuration from.
# @returns [Class] The default selector implementation.
def self.default(env = ENV)
if name = env["IO_EVENT_SELECTOR"]&.to_sym
return const_get(name)
end
if self.const_defined?(:URing)
URing
elsif self.const_defined?(:EPoll)
EPoll
elsif self.const_defined?(:KQueue)
KQueue
else
Select
end
end
# Create a new selector instance, according to the best available implementation.
#
# @parameter loop [Fiber] The event loop fiber.
# @parameter env [Hash] The environment to read configuration from.
# @returns [Selector] The new selector instance.
def self.new(loop, env = ENV)
selector = default(env).new(loop)
if debug = env["IO_EVENT_DEBUG_SELECTOR"]
selector = Debug::Selector.wrap(selector, env)
end
return selector
end
end
end
@@ -0,0 +1,21 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2022-2024, by Samuel Williams.
require "io/nonblock"
module IO::Event
module Selector
# Execute the given block in non-blocking mode.
#
# @parameter io [IO] The IO object to operate on.
# @yields {...} The block to execute.
def self.nonblock(io, &block)
io.nonblock(&block)
rescue Errno::EBADF
# Windows.
yield
end
end
end
@@ -0,0 +1,495 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2024, by Samuel Williams.
# Copyright, 2023, by Math Ieu.
require_relative "../interrupt"
require_relative "../support"
module IO::Event
module Selector
# A pure-Ruby implementation of the event selector.
class Select
# Initialize the selector with the given event loop fiber.
def initialize(loop)
@loop = loop
@waiting = Hash.new.compare_by_identity
@blocked = false
@ready = Queue.new
@interrupt = Interrupt.attach(self)
@idle_duration = 0.0
end
# @attribute [Fiber] The event loop fiber.
attr :loop
# @attribute [Float] This is the amount of time the event loop was idle during the last select call.
attr :idle_duration
# Wake up the event loop if it is currently sleeping.
def wakeup
if @blocked
@interrupt.signal
return true
end
return false
end
# Close the selector and release any resources.
def close
@interrupt.close
@loop = nil
@waiting = nil
end
Optional = Struct.new(:fiber) do
def transfer(*arguments)
fiber&.transfer(*arguments)
end
def alive?
fiber&.alive?
end
def nullify
self.fiber = nil
end
end
# Transfer from the current fiber to the event loop.
def transfer
@loop.transfer
end
# Transfer from the current fiber to the specified fiber. Put the current fiber into the ready list.
def resume(fiber, *arguments)
optional = Optional.new(Fiber.current)
@ready.push(optional)
fiber.transfer(*arguments)
ensure
optional.nullify
end
# Yield from the current fiber back to the event loop. Put the current fiber into the ready list.
def yield
optional = Optional.new(Fiber.current)
@ready.push(optional)
@loop.transfer
ensure
optional.nullify
end
# Append the given fiber into the ready list.
def push(fiber)
@ready.push(fiber)
end
# Transfer to the given fiber and raise an exception. Put the current fiber into the ready list.
def raise(fiber, *arguments)
optional = Optional.new(Fiber.current)
@ready.push(optional)
fiber.raise(*arguments)
ensure
optional.nullify
end
# @returns [Boolean] Whether the ready list is not empty, i.e. there are fibers ready to be resumed.
def ready?
!@ready.empty?
end
Waiter = Struct.new(:fiber, :events, :tail) do
def alive?
self.fiber&.alive?
end
# Dispatch the given events to the list of waiting fibers. If the fiber was not waiting for the given events, it is reactivated by calling the given block.
def dispatch(events, &reactivate)
# We capture the tail here, because calling reactivate might modify it:
tail = self.tail
if fiber = self.fiber
if fiber.alive?
revents = events & self.events
if revents.zero?
reactivate.call(self)
else
self.fiber = nil
fiber.transfer(revents)
end
else
self.fiber = nil
end
end
tail&.dispatch(events, &reactivate)
end
def invalidate
self.fiber = nil
end
def each(&block)
if fiber = self.fiber
yield fiber, self.events
end
self.tail&.each(&block)
end
end
# Wait for the given IO to become readable or writable.
#
# @parameter fiber [Fiber] The fiber that is waiting.
# @parameter io [IO] The IO object to wait on.
# @parameter events [Integer] The events to wait for.
def io_wait(fiber, io, events)
waiter = @waiting[io] = Waiter.new(fiber, events, @waiting[io])
@loop.transfer
ensure
waiter&.invalidate
end
# Wait for multiple IO objects to become readable or writable.
#
# @parameter readable [Array(IO)] The list of IO objects to wait for readability.
# @parameter writable [Array(IO)] The list of IO objects to wait for writability.
# @parameter priority [Array(IO)] The list of IO objects to wait for priority events.
def io_select(readable, writable, priority, timeout)
Thread.new do
IO.select(readable, writable, priority, timeout)
end.value
end
EAGAIN = -Errno::EAGAIN::Errno
EWOULDBLOCK = -Errno::EWOULDBLOCK::Errno
# Whether the given error code indicates that the operation should be retried.
protected def again?(errno)
errno == EAGAIN or errno == EWOULDBLOCK
end
if Support.fiber_scheduler_v3?
# Ruby 3.3+, full IO::Buffer support.
# Read from the given IO to the buffer.
#
# @parameter length [Integer] The minimum number of bytes to read.
# @parameter offset [Integer] The offset into the buffer to read to.
def io_read(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
while true
result = Fiber.blocking{buffer.read(io, 0, offset)}
if result < 0
if again?(result)
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result == 0
break
else
total += result
break if total >= length
offset += result
end
end
end
return total
end
# Write to the given IO from the buffer.
#
# @parameter length [Integer] The minimum number of bytes to write.
# @parameter offset [Integer] The offset into the buffer to write from.
def io_write(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
while true
result = Fiber.blocking{buffer.write(io, 0, offset)}
if result < 0
if again?(result)
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result == 0
break result
else
total += result
break if total >= length
offset += result
end
end
end
return total
end
elsif Support.fiber_scheduler_v2?
# Ruby 3.2, most IO::Buffer support, but slightly clunky read/write methods.
def io_read(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
maximum_size = buffer.size - offset
while maximum_size > 0
result = Fiber.blocking{buffer.read(io, maximum_size, offset)}
if again?(result)
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result < 0
return result
else
total += result
offset += result
break if total >= length
end
maximum_size = buffer.size - offset
end
end
return total
end
def io_write(fiber, io, buffer, length, offset = 0)
total = 0
Selector.nonblock(io) do
maximum_size = buffer.size - offset
while maximum_size > 0
result = Fiber.blocking{buffer.write(io, maximum_size, offset)}
if again?(result)
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return result
end
elsif result < 0
return result
else
total += result
offset += result
break if total >= length
end
maximum_size = buffer.size - offset
end
end
return total
end
elsif Support.fiber_scheduler_v1?
# Ruby <= 3.1, limited IO::Buffer support.
def io_read(fiber, _io, buffer, length, offset = 0)
# We need to avoid any internal buffering, so we use a duplicated IO object:
io = IO.for_fd(_io.fileno, autoclose: false)
total = 0
maximum_size = buffer.size - offset
while maximum_size > 0
case result = blocking{io.read_nonblock(maximum_size, exception: false)}
when :wait_readable
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return EWOULDBLOCK
end
when :wait_writable
if length > 0
self.io_wait(fiber, io, IO::WRITABLE)
else
return EWOULDBLOCK
end
when nil
break
else
buffer.set_string(result, offset)
size = result.bytesize
total += size
offset += size
break if size >= length
length -= size
end
maximum_size = buffer.size - offset
end
return total
rescue IOError => error
return -Errno::EBADF::Errno
rescue SystemCallError => error
return -error.errno
end
def io_write(fiber, _io, buffer, length, offset = 0)
# We need to avoid any internal buffering, so we use a duplicated IO object:
io = IO.for_fd(_io.fileno, autoclose: false)
total = 0
maximum_size = buffer.size - offset
while maximum_size > 0
chunk = buffer.get_string(offset, maximum_size)
case result = blocking{io.write_nonblock(chunk, exception: false)}
when :wait_readable
if length > 0
self.io_wait(fiber, io, IO::READABLE)
else
return EWOULDBLOCK
end
when :wait_writable
if length > 0
self.io_wait(fiber, io, IO::WRITABLE)
else
return EWOULDBLOCK
end
else
total += result
offset += result
break if result >= length
length -= result
end
maximum_size = buffer.size - offset
end
return total
rescue IOError => error
return -Errno::EBADF::Errno
rescue SystemCallError => error
return -error.errno
end
def blocking(&block)
fiber = Fiber.new(blocking: true, &block)
return fiber.resume(fiber)
end
end
def process_wait(fiber, pid, flags)
Thread.new do
Process::Status.wait(pid, flags)
end.value
end
private def pop_ready
unless @ready.empty?
count = @ready.size
count.times do
fiber = @ready.pop
fiber.transfer if fiber.alive?
end
return true
end
end
def select(duration = nil)
if pop_ready
# If we have popped items from the ready list, they may influence the duration calculation, so we don't delay the event loop:
duration = 0
end
readable = Array.new
writable = Array.new
priority = Array.new
@waiting.each do |io, waiter|
waiter.each do |fiber, events|
if (events & IO::READABLE) > 0
readable << io
end
if (events & IO::WRITABLE) > 0
writable << io
end
if (events & IO::PRIORITY) > 0
priority << io
end
end
end
duration = 0 unless @ready.empty?
error = nil
if duration&.>(0)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
else
@idle_duration = 0.0
end
# We need to handle interrupts on blocking IO. Every other implementation uses EINTR, but that doesn't work with `::IO.select` as it will retry the call on EINTR.
Thread.handle_interrupt(::Exception => :on_blocking) do
@blocked = true
readable, writable, priority = ::IO.select(readable, writable, priority, duration)
rescue ::Exception => error
# Requeue below...
ensure
@blocked = false
if start_time
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@idle_duration = end_time - start_time
end
end
if error
# Requeue the error into the pending exception queue:
Thread.current.raise(error)
return 0
end
ready = Hash.new(0).compare_by_identity
readable&.each do |io|
ready[io] |= IO::READABLE
end
writable&.each do |io|
ready[io] |= IO::WRITABLE
end
priority&.each do |io|
ready[io] |= IO::PRIORITY
end
ready.each do |io, events|
@waiting.delete(io).dispatch(events) do |waiter|
# Re-schedule the waiting IO:
waiter.tail = @waiting[io]
@waiting[io] = waiter
end
end
return ready.size
end
end
end
end
@@ -0,0 +1,57 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2022-2024, by Samuel Williams.
class IO
module Event
# Helper methods for detecting support for various features.
module Support
# Some features are only availble if the IO::Buffer class is available.
#
# @returns [Boolean] Whether the IO::Buffer class is available.
def self.buffer?
IO.const_defined?(:Buffer)
end
# The basic fiber scheduler was introduced along side the IO::Buffer class.
#
# @returns [Boolean] Whether the IO::Buffer class is available.
#
# To be removed on 31 Mar 2025.
def self.fiber_scheduler_v1?
IO.const_defined?(:Buffer)
end
# More advanced read/write methods and blocking controls were introduced in Ruby 3.2.
#
# To be removed on 31 Mar 2026.
def self.fiber_scheduler_v2?
# Some interface changes were back-ported incorrectly:
# https://github.com/ruby/ruby/pull/10778
# Specifically "Improvements to IO::Buffer read/write/pread/pwrite."
# Missing correct size calculation.
return false if RUBY_VERSION >= "3.2.5"
IO.const_defined?(:Buffer) and Fiber.respond_to?(:blocking) and IO::Buffer.instance_method(:read).arity == -1
end
# Updated inferfaces for read/write and IO::Buffer were introduced in Ruby 3.3, including pread/pwrite.
#
# To become the default 31 Mar 2026.
def self.fiber_scheduler_v3?
if fiber_scheduler_v2?
return true if RUBY_VERSION >= "3.3"
# Feature detection if required:
begin
IO::Buffer.new.slice(0, 0).write(STDOUT)
return true
rescue
return false
end
end
end
end
end
end
@@ -0,0 +1,149 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2024, by Samuel Williams.
require_relative "priority_heap"
class IO
module Event
# An efficient sorted set of timers.
class Timers
# A handle to a scheduled timer.
class Handle
# Initialize the handle with the given time and block.
#
# @parameter time [Float] The time at which the block should be called.
# @parameter block [Proc] The block to call.
def initialize(time, block)
@time = time
@block = block
end
# @attribute [Float] The time at which the block should be called.
attr :time
# @attribute [Proc | Nil] The block to call when the timer fires.
attr :block
# Compare the handle with another handle.
#
# @parameter other [Handle] The other handle to compare with.
# @returns [Boolean] Whether the handle is less than the other handle.
def < other
@time < other.time
end
# Compare the handle with another handle.
#
# @parameter other [Handle] The other handle to compare with.
# @returns [Boolean] Whether the handle is greater than the other handle.
def > other
@time > other.time
end
# Invoke the block.
def call(...)
@block.call(...)
end
# Cancel the timer.
def cancel!
@block = nil
end
# @returns [Boolean] Whether the timer has been cancelled.
def cancelled?
@block.nil?
end
end
# Initialize the timers.
def initialize
@heap = PriorityHeap.new
@scheduled = []
end
# @returns [Integer] The number of timers in the heap.
def size
flush!
return @heap.size
end
# Schedule a block to be called at a specific time in the future.
#
# @parameter time [Float] The time at which the block should be called, relative to {#now}.
# @parameter block [Proc] The block to call.
def schedule(time, block)
handle = Handle.new(time, block)
@scheduled << handle
return handle
end
# Schedule a block to be called after a specific time offset, relative to the current time as returned by {#now}.
#
# @parameter offset [#to_f] The time offset from the current time at which the block should be called.
# @yields {|now| ...} When the timer fires.
def after(offset, &block)
schedule(self.now + offset.to_f, block)
end
# Compute the time interval until the next timer fires.
#
# @parameter now [Float] The current time.
# @returns [Float | Nil] The time interval until the next timer fires, if any.
def wait_interval(now = self.now)
flush!
while handle = @heap.peek
if handle.cancelled?
@heap.pop
else
return handle.time - now
end
end
end
# @returns [Float] The current time.
def now
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
end
# Fire all timers that are ready to fire.
#
# @parameter now [Float] The current time.
def fire(now = self.now)
# Flush scheduled timers into the heap:
flush!
# Get the earliest timer:
while handle = @heap.peek
if handle.cancelled?
@heap.pop
elsif handle.time <= now
# Remove the earliest timer from the heap:
@heap.pop
# Call the block:
handle.call(now)
else
break
end
end
end
# Flush all scheduled timers into the heap.
#
# This is a small optimization which assumes that most timers (timeouts) will be cancelled.
protected def flush!
while handle = @scheduled.pop
@heap.push(handle) unless handle.cancelled?
end
end
end
end
end
@@ -0,0 +1,12 @@
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2021-2025, by Samuel Williams.
# @namespace
class IO
# @namespace
module Event
VERSION = "1.9.0"
end
end