Documentation
¶
Overview ¶
Package async is a pure-Go (no cgo), MRI-4.0.5-faithful model of the structured-concurrency core of Ruby's async gem (socketry/async).
async is a fiber-based concurrency framework: an Async{} block runs a tree of cooperative tasks on a reactor, and blocking operations (waiting on a child, sleeping, acquiring a semaphore, reading a socket) suspend the running fiber back to the reactor instead of blocking a thread. This package reproduces both the *structured-concurrency* layer — the task tree with cancellation and failure propagation, plus the Async:: synchronization primitives — and the *IO reactor* on top of it, targeting a later rbgo binding where the host VM supplies real fibers.
The reactor and the Scheduler seam ¶
The point where a fiber suspends and is later resumed is expressed as an injectable Scheduler seam:
type Scheduler interface {
Defer(body func()) Fiber // spawn a fiber (a task) on the reactor
Yield() bool // suspend the running fiber; false on teardown
Resume(Fiber) // mark a suspended fiber runnable
Sleep(time.Duration) // suspend the running fiber on the timer wheel
Run() // drive the loop to quiescence
}
A Scheduler that also implements IOScheduler.AwaitIO can host the non-blocking IO reactor: instead of the gem's epoll/kqueue/io_uring event loop, an async IO operation runs the real (blocking) Go syscall on a goroutine — Go's runtime poller gives us genuine async IO natively — and parks just that one fiber until the syscall completes, keeping the loop alive meanwhile. The rbgo binding implements Scheduler on the host's real fibers and timer wheel. This package ships a deterministic, in-memory CoScheduler — a cooperative scheduler with a virtual clock plus this goroutine-backed IO reactor — so the whole model is exercised with no wall-clock sleeps for the pure-cooperative paths and no leaked goroutines, which is what keeps the test suite at 100% coverage.
Async IO ¶
Socket wraps a net.Conn and Listener wraps a net.Listener; their Read, Write, Accept and Connect suspend the calling fiber on the reactor rather than blocking a thread, and a Stop or with_timeout on the task cancels an in-flight operation (via a past IO deadline, a listener close, or a dial-context cancel). In-process net.Pipe and loopback TCP both work, so the reactor is exercised end to end without any external network.
Task tree ¶
Run (Async{}) starts a root Task; Task.Async (task.async{}) spawns a child. Every Task carries its result/failure and a lifecycle state (Initialized/Running/Complete/Stopped/Failed). Task.Wait joins a task, returning its result or its failure (ErrStop for a stopped task); Task.Stop cancels a task and, structurally, its children — cancellation is raised at the task's next suspension point, exactly as async raises Async::Stop at the next fiber yield. A task that fails or is stopped stops its still-running children.
Primitives (the Async:: namespace) ¶
Barrier, Semaphore, Condition, Notification, Queue, LimitedQueue and Waiter mirror the gem's synchronization objects. Each blocking method takes the calling *Task (the binding passes Async::Task.current) so it can suspend the right fiber and observe cancellation.
The package is CGO-free, depends only on the standard library, and is safe under the race detector.
Index ¶
- Variables
- func Run(body Body) (any, error)
- func RunOn(s Scheduler, body Body) (any, error)
- type Barrier
- type Body
- type CoScheduler
- type Condition
- type Fiber
- type IOScheduler
- type LimitedQueue
- type Listener
- type Notification
- type Queue
- type Scheduler
- type Semaphore
- func (s *Semaphore) Acquire(t *Task)
- func (s *Semaphore) AcquireDo(t *Task, fn func() (any, error)) (any, error)
- func (s *Semaphore) Blocking() bool
- func (s *Semaphore) Count() int
- func (s *Semaphore) Limit() int
- func (s *Semaphore) Release()
- func (s *Semaphore) SetLimit(limit int)
- func (s *Semaphore) Waiting() int
- type Socket
- type State
- type Task
- func (t *Task) Async(body Body) *Task
- func (t *Task) Children() []*Task
- func (t *Task) CompleteQ() bool
- func (t *Task) Err() error
- func (t *Task) FailedQ() bool
- func (t *Task) Parent() *Task
- func (t *Task) Result() any
- func (t *Task) RunningQ() bool
- func (t *Task) Scheduler() Scheduler
- func (t *Task) Sleep(d time.Duration)
- func (t *Task) State() State
- func (t *Task) Stop()
- func (t *Task) StoppedQ() bool
- func (t *Task) Wait(caller *Task) (any, error)
- func (t *Task) WithTimeout(d time.Duration, fn Body) (any, error)
- func (t *Task) Yield()
- type Waiter
Constants ¶
This section is empty.
Variables ¶
var ( // ErrStop mirrors Async::Stop: the exception raised inside a task's fiber to // cancel it. Waiting on a stopped task returns ErrStop. ErrStop = errors.New("async: stop") // ErrTimeout mirrors Async::TimeoutError, raised inside a task when a // Task.WithTimeout budget elapses before its block completes. ErrTimeout = errors.New("async: timeout") )
Package-level sentinel errors mirroring async's exception classes. A binding maps each to the corresponding Ruby exception when raising into the host VM.
var ErrNoIO = errors.New("async: scheduler does not support IO")
ErrNoIO is returned by the Async IO wrappers when the task's Scheduler does not implement IOScheduler, so it cannot host the non-blocking IO reactor. The bundled CoScheduler always can; a custom Scheduler must implement AwaitIO.
Functions ¶
Types ¶
type Barrier ¶
type Barrier struct {
// contains filtered or unexported fields
}
Barrier collects tasks and waits for them all, mirroring Async::Barrier. Tasks spawned through the barrier's Async are tracked so a single Wait joins them in order, and Stop cancels the whole group.
func (*Barrier) Async ¶
Async spawns body as a child of parent and adds it to the barrier (Async::Barrier#async).
type Body ¶
Body is the work a Task runs: it receives its own Task (so it can spawn children, sleep, or wait) and returns a result value or a failure. It is the Go shape of the Ruby block passed to Async{} / task.async{}; the rbgo binding wraps the host VM's block as a Body.
type CoScheduler ¶
type CoScheduler struct {
// contains filtered or unexported fields
}
CoScheduler is a deterministic, single-threaded cooperative scheduler with a virtual clock. It runs each fiber to its next suspension point in turn, advancing the clock only when no fiber is runnable, so timed behaviour is exercised without any real sleeping. It is the default Scheduler used by Run and the reference against which the rbgo binding's fiber scheduler is checked.
func NewScheduler ¶
func NewScheduler() *CoScheduler
NewScheduler returns a fresh deterministic cooperative scheduler.
func (*CoScheduler) AwaitIO ¶
func (s *CoScheduler) AwaitIO(op func())
AwaitIO runs op on a fresh goroutine and suspends the running fiber until op returns. The fiber is parked in the IO set, which keeps the loop from going quiescent (or tearing the fiber down) while the syscall is outstanding; the op goroutine reports back over ioDone, and the loop makes the fiber runnable again. This is the reactor's non-blocking-IO wait: a real, blocking Go IO call suspends one fiber (not the whole loop), exactly as the gem's io_wait suspends one fiber on the event loop.
func (*CoScheduler) Defer ¶
func (s *CoScheduler) Defer(body func()) Fiber
Defer spawns body as a new fiber and queues it to start.
func (*CoScheduler) Now ¶
func (s *CoScheduler) Now() time.Duration
Now returns the scheduler's current virtual time — the sum of the durations its Sleep calls have skipped past. It is zero until the first timer matures.
func (*CoScheduler) Resume ¶
func (s *CoScheduler) Resume(fib Fiber)
Resume marks a suspended fiber runnable. Suspended fibers are either blocked (Yield) or timed (Sleep); either way the fiber is queued and any stale timer entry for it is skipped when the clock advances.
func (*CoScheduler) Sleep ¶
func (s *CoScheduler) Sleep(d time.Duration)
Sleep suspends the running fiber on the timer wheel. A non-positive duration requeues it cooperatively behind the currently-runnable fibers.
func (*CoScheduler) Yield ¶
func (s *CoScheduler) Yield() bool
Yield suspends the running fiber until it is Resumed (returning true) or torn down (returning false).
type Condition ¶
type Condition struct {
// contains filtered or unexported fields
}
Condition is a synchronization point where tasks wait to be signalled, mirroring Async::Condition. A signal wakes every waiting task, optionally delivering a value that each waiter's Wait returns.
func NewCondition ¶
func NewCondition() *Condition
NewCondition returns a condition with no waiters.
func (*Condition) Signal ¶
Signal wakes every waiting task, delivering value to each (Async::Condition#signal).
type Fiber ¶
type Fiber interface {
// contains filtered or unexported methods
}
Fiber is an opaque handle to one cooperatively-scheduled unit of execution — one task's fiber. A Scheduler hands these out from Defer and takes them back in Resume. The concrete type is private to each Scheduler implementation.
type IOScheduler ¶
type IOScheduler interface {
Scheduler
// AwaitIO runs op on a fresh goroutine and suspends the currently-running
// fiber until op returns, then resumes it. It must be called only from a
// running fiber. op performs the real (blocking) IO and stores its result in
// variables the caller captured; op must not touch scheduler state. The
// channel hand-off from the op goroutine back through the reactor to the
// resumed fiber establishes the happens-before edge that makes reading those
// captured variables race-free.
AwaitIO(op func())
}
IOScheduler is the optional capability a Scheduler advertises when it can host asynchronous IO: the non-blocking IO reactor. A Task backs an Async::IO operation (socket read/write/accept/connect) by running the real, blocking syscall on a separate goroutine and parking its fiber via AwaitIO; the reactor keeps the loop alive until that goroutine reports completion and then resumes the fiber. This is how Go — which gives us real async IO natively through its runtime poller — stands in for the gem's epoll/kqueue/io_uring event loop.
The bundled CoScheduler implements it. The rbgo binding implements it on the host VM's fibers plus the same goroutine-backed completion mechanism (or the host's own io_wait). A Scheduler that does not implement IOScheduler cannot host Async IO, and the IO wrappers return ErrNoIO.
type LimitedQueue ¶
type LimitedQueue struct {
// contains filtered or unexported fields
}
LimitedQueue is a bounded FIFO with backpressure, mirroring Async::LimitedQueue: a producer enqueuing a full queue suspends until a consumer makes room.
func NewLimitedQueue ¶
func NewLimitedQueue(limit int) *LimitedQueue
NewLimitedQueue returns an empty queue holding at most limit items. A limit below one is clamped to one.
func (*LimitedQueue) Dequeue ¶
func (q *LimitedQueue) Dequeue(t *Task) any
Dequeue removes and returns the head item, suspending the calling task while the queue is empty, then wakes the longest-waiting producer (Async::LimitedQueue#dequeue).
func (*LimitedQueue) Empty ¶
func (q *LimitedQueue) Empty() bool
Empty reports whether the queue holds no items.
func (*LimitedQueue) Enqueue ¶
func (q *LimitedQueue) Enqueue(t *Task, v any)
Enqueue appends an item, suspending the calling task while the queue is full, then wakes the longest-waiting consumer (Async::LimitedQueue#enqueue).
func (*LimitedQueue) Limit ¶
func (q *LimitedQueue) Limit() int
Limit returns the maximum number of buffered items (Async::LimitedQueue#limit).
func (*LimitedQueue) LimitedQ ¶
func (q *LimitedQueue) LimitedQ() bool
LimitedQ reports whether the queue is at capacity (Async::LimitedQueue#limited?).
func (*LimitedQueue) Size ¶
func (q *LimitedQueue) Size() int
Size returns the number of buffered items.
type Listener ¶
type Listener struct {
// contains filtered or unexported fields
}
Listener is the reactor-aware wrapper around a net.Listener, mirroring Async::IO::Endpoint#accept: its Accept suspends the calling fiber until an inbound connection arrives, returning it as a Socket.
func Listen ¶
Listen opens a listening socket and wraps it for the reactor. It is not itself a blocking operation, so it does not park a fiber; use 127.0.0.1:0 for an in-process loopback listener with an OS-assigned port.
func WrapListener ¶
WrapListener adapts an existing net.Listener into a reactor-aware Listener.
func (*Listener) Accept ¶
Accept waits for the next inbound connection, suspending t's fiber on the reactor until one arrives, and returns it as a Socket. A Stop or timeout on t while Accept is outstanding cancels it: the reactor sets a past deadline on the listener when it supports one, else closes it, so Accept returns and the fiber unwinds.
type Notification ¶
type Notification struct {
// contains filtered or unexported fields
}
Notification is a one-way wakeup: a producer signals a consumer waiting on it, mirroring Async::Notification (a Condition specialised to valueless wakeups).
func NewNotification ¶
func NewNotification() *Notification
NewNotification returns a notification with no waiters.
func (*Notification) Signal ¶
func (n *Notification) Signal()
Signal wakes every task waiting on the notification (Async::Notification#signal).
func (*Notification) Wait ¶
func (n *Notification) Wait(t *Task)
Wait suspends the calling task until the notification is signalled (Async::Notification#wait).
func (*Notification) WaitCount ¶
func (n *Notification) WaitCount() int
WaitCount returns the number of tasks currently waiting.
type Queue ¶
type Queue struct {
// contains filtered or unexported fields
}
Queue is an unbounded FIFO channel between tasks, mirroring Async::Queue. A task dequeuing an empty queue suspends its fiber until an item is enqueued.
func (*Queue) Dequeue ¶
Dequeue removes and returns the head item, suspending the calling task while the queue is empty (Async::Queue#dequeue / #pop).
func (*Queue) Enqueue ¶
Enqueue appends an item and wakes the longest-waiting dequeuer, if any (Async::Queue#enqueue / #push / #<<).
type Scheduler ¶
type Scheduler interface {
// Defer spawns body as a new fiber scheduled to start on a later turn of the
// loop, returning its handle.
Defer(body func()) Fiber
// Yield suspends the currently-running fiber, returning control to the loop.
// It returns true when the fiber is later Resumed normally, and false when
// the loop is tearing the fiber down (no remaining work can ever resume it),
// in which case the caller must unwind.
Yield() bool
// Resume marks a suspended fiber runnable. It is a no-op if the fiber is not
// currently suspended (already runnable, running, or finished).
Resume(f Fiber)
// Sleep suspends the currently-running fiber until at least d has elapsed on
// the scheduler's clock. A non-positive d reschedules the fiber cooperatively
// behind the other runnable fibers.
Sleep(d time.Duration)
// Run drives the loop until it is quiescent: no fiber is runnable, no timer
// remains, and no IO is in flight. Fibers still blocked at quiescence are torn
// down (their Yield returns false) so no goroutine is leaked. A Scheduler that
// also implements IOScheduler keeps the loop alive while IO is outstanding.
Run()
}
Scheduler is the seam onto which the reactor is mapped. It is exactly the set of operations the structured-concurrency core needs from a fiber runtime: spawn a fiber (Defer), suspend the running fiber (Yield) and later resume it (Resume), suspend it on a timer (Sleep), and drive the loop (Run).
The rbgo binding implements this on the host VM's real fibers plus a timer wheel (and, later, the non-blocking IO reactor). The bundled CoScheduler is a deterministic in-memory implementation used by the tests.
type Semaphore ¶
type Semaphore struct {
// contains filtered or unexported fields
}
Semaphore is a counting semaphore that blocks acquirers once its limit is reached, mirroring Async::Semaphore. Waiting acquirers suspend their fiber (rather than a thread) and are resumed in FIFO order as permits are released.
func NewSemaphore ¶
NewSemaphore returns a semaphore permitting limit concurrent holders. A limit below one is clamped to one.
func (*Semaphore) Acquire ¶
Acquire takes one permit, suspending the calling task while the semaphore is at its limit (Async::Semaphore#acquire).
func (*Semaphore) AcquireDo ¶
AcquireDo acquires a permit, runs fn, and releases the permit even if fn panics, mirroring the block form Async::Semaphore#acquire{ ... }.
func (*Semaphore) Blocking ¶
Blocking reports whether a further Acquire would block (Async::Semaphore#blocking?).
func (*Semaphore) Count ¶
Count returns the number of permits currently held (Async::Semaphore#count).
func (*Semaphore) Limit ¶
Limit returns the maximum number of concurrent holders (Async::Semaphore#limit).
func (*Semaphore) Release ¶
func (s *Semaphore) Release()
Release returns one permit and wakes the next waiting task, if any (Async::Semaphore#release).
type Socket ¶
type Socket struct {
// contains filtered or unexported fields
}
Socket is the reactor-aware wrapper around a net.Conn, mirroring the gem's Async::IO::Socket / Async::IO::Stream: its Read and Write suspend the calling fiber back to the reactor while the real syscall runs on a goroutine, instead of blocking a thread. Go's runtime poller gives us genuine non-blocking IO, so each operation is a single goroutine plus a fiber park.
A Socket is bound to the task that performs each operation (passed explicitly, as the gem passes Async::Task.current), so a Stop or a with_timeout on that task cancels an in-flight Read/Write: the reactor sets a past deadline on the underlying connection, the syscall returns, and the fiber unwinds.
func Connect ¶
Connect dials network/addr, suspending t's fiber on the reactor until the connection is established, and returns it as a Socket. A Stop or timeout on t while the dial is outstanding cancels it through the dial context. It mirrors Async::IO::Endpoint#connect.
func Wrap ¶
Wrap adapts an existing net.Conn (a pipe end, an accepted connection, a dialed socket) into a reactor-aware Socket. In-process net.Pipe and loopback TCP both work, which is what the tests use — no external network.
func (*Socket) Read ¶
Read reads into p, suspending t's fiber on the reactor until the read completes; it returns the number of bytes read and any error, mirroring a non-blocking Async::IO read. A Stop or timeout on t while the read is outstanding unblocks it via a past read deadline and unwinds the fiber.
type State ¶
type State string
State is the lifecycle state of a Task, mirroring async's task status symbols :initialized, :running, :completed, :stopped, and :failed.
const ( // Initialized is the state of a task that has been created but whose body // has not started running yet. Initialized State = "initialized" // Running is the state of a task whose body is executing or suspended at a // blocking point. Running State = "running" // Complete is the state of a task whose body returned normally // (async :completed). Complete State = "complete" // Stopped is the state of a task that was cancelled — its Async::Stop // equivalent was raised at a suspension point and unwound the body. Stopped State = "stopped" // Failed is the state of a task whose body returned an error or panicked // (other than a cancellation). Failed State = "failed" )
type Task ¶
type Task struct {
// contains filtered or unexported fields
}
Task is one node of the structured-concurrency tree: a unit of work running on its own fiber, with a lifecycle state, a result or failure, a parent, and a set of children. It mirrors Async::Task.
func (*Task) Async ¶
Async spawns a child task running body and schedules it to run. It is the equivalent of task.async{ |child| ... }: the child joins this task's subtree, so stopping or failing this task stops the child.
func (*Task) Result ¶
Result returns the task's result value, or nil if it has not completed successfully. It mirrors Async::Task#result without re-raising.
func (*Task) Sleep ¶
Sleep suspends the task for at least d on the scheduler's clock, and honours a concurrent Stop/timeout by unwinding when it wakes. It mirrors task.sleep.
func (*Task) Stop ¶
func (t *Task) Stop()
Stop cancels the task and, structurally, its children. A parked task unwinds (Async::Stop) at its suspension point; a task that has not started never runs its body; a task stopping itself unwinds immediately. It mirrors Async::Task#stop.
func (*Task) Wait ¶
Wait joins the task, blocking the calling task until it finishes, then returns its result or its failure — ErrStop if it was stopped. It mirrors Async::Task#wait. The caller is the currently-running task (the binding passes Async::Task.current).
func (*Task) WithTimeout ¶
WithTimeout runs fn under a timeout budget d measured on the scheduler's clock. If fn does not finish within d, ErrTimeout is raised inside it (at its next suspension point) and returned; otherwise fn's own result is returned. It mirrors task.with_timeout(d){ ... } raising Async::TimeoutError.
type Waiter ¶
type Waiter struct {
// contains filtered or unexported fields
}
Waiter spawns a group of tasks under a parent and waits for a chosen number of them, mirroring Async::Waiter. Unlike Barrier it returns the tasks' results.
func (*Waiter) Async ¶
Async spawns body under the waiter's parent and tracks it (Async::Waiter#async).
