async

package module
v0.0.0-...-b24a3ec Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-ruby-async/async

async — go-ruby-async

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's async gem — the fiber-based task tree with cancellation and failure propagation, the Async:: synchronization primitives, and the non-blocking IO reactor — modelling the gem's observable behaviour and vocabulary without any Ruby runtime.

It is the async backend for go-embedded-ruby (a later require "async" binding), but is a standalone, reusable Go module — a sibling of go-ruby-concurrent-ruby and go-ruby-set.

Scope: the full reactor — structured concurrency and non-blocking IO

async's deepest value is its non-blocking IO reactor — an epoll/kqueue/io_uring event loop that suspends a fiber when its socket would block and resumes it when the socket is ready. This package ships both layers: the structured-concurrency core (the task tree, cancellation, failure propagation, and the synchronization primitives) and the IO reactor on top of it. Where the gem calls into a polling event loop, this package runs the real (blocking) Go syscall on a goroutine and parks just that one fiber until it completes — Go's runtime poller gives us genuine async IO natively, so a goroutine + channel is the reactor's io_wait. The suspend/resume point is factored out into an injectable Scheduler seam (with an IOScheduler capability for the IO reactor), so the rbgo binding can supply the host VM's real fibers while the tests drive a deterministic in-memory scheduler.

Modelled (this package) Supplied by the Scheduler binding
Task tree: Async{} / task.async{}, states, wait/stop/result Real fiber suspend/resume (rbgo fibers)
Structured cancellation (stopping a parent stops its children) The host VM's own fiber runtime
Failure propagation (wait re-raises; a failed task stops its children) The wall-clock timer wheel (here: a virtual clock)
Barrier, Semaphore, Condition, Notification, Queue, LimitedQueue, Waiter
The non-blocking IO reactor: Socket read/write, Listener accept, Connect
Async::Stop, Async::TimeoutError (cancel in-flight IO too)

The Scheduler seam

Everywhere the gem's reactor would suspend and resume a fiber, this package calls through one small interface, so a binding can map it onto the host runtime:

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
}

// Optional capability: hosts the non-blocking IO reactor.
type IOScheduler interface {
	Scheduler
	AwaitIO(op func()) // run op on a goroutine; park this fiber until it returns
}

A Scheduler that also implements IOScheduler (one method, AwaitIO) can host the non-blocking IO reactor: an async IO call runs the real blocking syscall on a goroutine and parks its fiber until the syscall completes, keeping the loop alive meanwhile — so one blocked read suspends one fiber, not the loop, just as the gem's io_wait does.

The rbgo binding implements Scheduler on Fiber.yield / Fiber#resume plus a timer wheel. This package bundles CoScheduler, a deterministic cooperative scheduler with a virtual clock and the goroutine-backed IO reactor: it runs each task to its next suspension point in turn, advancing time only when nothing is runnable, so the pure-cooperative paths are exercised with no wall-clock sleeps and no leaked goroutines — which is what keeps the suite at 100% coverage. When real IO is outstanding the loop parks on the actual completion (and races it against the virtual clock, so with_timeout still fires over a blocking read). Tasks still blocked when the loop goes quiescent are torn down (their Yield returns false), so a well-formed program never strands a fiber.

Every blocking method takes the calling *Task (the binding passes Async::Task.current) so it suspends the right fiber and observes cancellation.

Install

go get github.com/go-ruby-async/async

Usage

package main

import (
	"fmt"
	"time"

	"github.com/go-ruby-async/async"
)

func main() {
	// Async{ |task| ... } — a root task on a fresh reactor.
	total, _ := async.Run(func(task *async.Task) (any, error) {
		sem := async.NewSemaphore(2) // at most 2 in flight
		barrier := async.NewBarrier()
		sum := 0

		for i := 1; i <= 5; i++ {
			i := i
			// barrier.async { ... } — a child task in the subtree.
			barrier.Async(task, func(child *async.Task) (any, error) {
				return sem.AcquireDo(child, func() (any, error) {
					child.Sleep(10 * time.Millisecond) // non-blocking
					sum += i
					return nil, nil
				})
			})
		}

		if err := barrier.Wait(task); err != nil { // join all children
			return nil, err
		}
		return sum, nil
	})

	fmt.Println(total) // 15
}

Async IO — an accept/connect round-trip where each read/write suspends its fiber on the reactor instead of blocking a thread:

ln, _ := async.Listen("tcp", "127.0.0.1:0")
defer ln.Close()

async.Run(func(task *async.Task) (any, error) {
	// server: accept one connection and echo a line
	task.Async(func(t *async.Task) (any, error) {
		conn, err := ln.Accept(t) // suspends until a client connects
		if err != nil {
			return nil, err
		}
		defer conn.Close()
		buf := make([]byte, 5)
		n, _ := conn.Read(t, buf)  // suspends until bytes arrive
		return conn.Write(t, buf[:n])
	})
	// client: connect, send, done
	client := task.Async(func(t *async.Task) (any, error) {
		conn, err := async.Connect(t, "tcp", ln.Addr().String())
		if err != nil {
			return nil, err
		}
		defer conn.Close()
		return conn.Write(t, []byte("hello"))
	})
	_, err := client.Wait(task)
	return nil, err
})

API

// Reactor entry points
func Run(body Body) (any, error)                 // Async{ |task| ... }
func RunOn(s Scheduler, body Body) (any, error)  // run on a host scheduler
type Body func(t *Task) (any, error)             // the Ruby block

// Task tree (Async::Task)
func (t *Task) Async(body Body) *Task            // task.async{ ... }
func (t *Task) Wait(caller *Task) (any, error)   // wait (re-raises failure)
func (t *Task) Stop()                            // stop (cascades to children)
func (t *Task) Sleep(d time.Duration)            // task.sleep
func (t *Task) Yield()                           // cooperative reschedule
func (t *Task) WithTimeout(d time.Duration, fn Body) (any, error) // with_timeout
func (t *Task) Result() any                      // result (no re-raise)
func (t *Task) Err() error
func (t *Task) State() State                     // Initialized/Running/Complete/Stopped/Failed
func (t *Task) RunningQ() bool                   // running?
func (t *Task) CompleteQ() bool                  // completed?
func (t *Task) FailedQ() bool                    // failed?
func (t *Task) StoppedQ() bool                   // stopped?
func (t *Task) Parent() *Task
func (t *Task) Children() []*Task
func (t *Task) Scheduler() Scheduler

// Primitives (the Async:: namespace)
func NewBarrier() *Barrier                        // Async::Barrier
func (b *Barrier) Async(parent *Task, body Body) *Task
func (b *Barrier) Wait(caller *Task) error        // wait for all
func (b *Barrier) Stop()

func NewSemaphore(limit int) *Semaphore           // Async::Semaphore
func (s *Semaphore) Acquire(t *Task)
func (s *Semaphore) Release()
func (s *Semaphore) AcquireDo(t *Task, fn func() (any, error)) (any, error)
func (s *Semaphore) Count() int
func (s *Semaphore) Limit() int
func (s *Semaphore) SetLimit(limit int)
func (s *Semaphore) Blocking() bool

func NewCondition() *Condition                    // Async::Condition
func (c *Condition) Wait(t *Task) any
func (c *Condition) Signal(value any)

func NewNotification() *Notification              // Async::Notification
func (n *Notification) Wait(t *Task)
func (n *Notification) Signal()

func NewQueue() *Queue                            // Async::Queue
func (q *Queue) Enqueue(v any)
func (q *Queue) Dequeue(t *Task) any

func NewLimitedQueue(limit int) *LimitedQueue     // Async::LimitedQueue (backpressure)
func (q *LimitedQueue) Enqueue(t *Task, v any)
func (q *LimitedQueue) Dequeue(t *Task) any

func NewWaiter(parent *Task) *Waiter              // Async::Waiter
func (w *Waiter) Async(body Body) *Task
func (w *Waiter) Wait(caller *Task, count int) ([]any, error)

// Non-blocking IO reactor (the Async::IO namespace)
func Wrap(conn net.Conn) *Socket                  // wrap a net.Conn (pipe/TCP)
func (s *Socket) Read(t *Task, p []byte) (int, error)   // suspends; cancellable
func (s *Socket) Write(t *Task, p []byte) (int, error)  // suspends; cancellable
func (s *Socket) Close() error
func (s *Socket) Conn() net.Conn

func Listen(network, addr string) (*Listener, error)    // Async::IO::Endpoint
func WrapListener(ln net.Listener) *Listener
func (l *Listener) Accept(t *Task) (*Socket, error)     // suspends; cancellable
func (l *Listener) Addr() net.Addr
func (l *Listener) Close() error

func Connect(t *Task, network, addr string) (*Socket, error) // suspends; cancellable

// Errors (raised into the host VM by the binding)
var ErrStop    error // Async::Stop
var ErrTimeout error // Async::TimeoutError
var ErrNoIO    error // the Scheduler cannot host IO (no IOScheduler)

Wait returns a stopped task's result as ErrStop and a failed task's as its error, mirroring Async::Task#wait re-raising. Stop raises the cancellation at the task's next suspension point (as async raises Async::Stop at the next fiber yield) and cascades to the task's children; a task that fails or is stopped stops its still-running children. The primitives suspend the calling fiber rather than a thread, exactly as the gem does.

Fidelity basis

Fidelity here is behavioural, pinned by the deterministic cooperative scheduler: unlike a stdlib library, async is a gem whose semantics only exist inside a running reactor, so there is no live-gem differential oracle. The task states, the parent→children cancellation tree, failure propagation, each primitive, and the IO reactor (a read/accept/connect suspends its fiber; a Stop or with_timeout cancels the in-flight operation) are checked against the documented behaviour of async 2.x on MRI 4.0.5, exercised over in-process net.Pipe and loopback TCP.

Residual (what Go cannot mirror exactly): the gem multiplexes fibers on a single OS thread through one poller syscall, whereas here each in-flight IO runs on its own goroutine — so fiber-vs-fiber IO scheduling order is Go-runtime dependent rather than poller-deterministic, and a task cancelled while parked on IO unwinds only once the underlying syscall returns (promptly, via a past deadline / listener close / dial-context cancel) rather than at a synchronous Async::Stop raise. Observable task/timeout/cancellation semantics match; the exact interleaving of concurrent ready sockets does not, by construction.

Tests & coverage

The deterministic, virtual-clock scheduler drives every test, so the structured-concurrency model — all task states, cancellation and failure propagation, teardown of stranded tasks, and each primitive — is exercised with no time.Sleep for correctness and no leaked goroutines. The IO reactor is tested end to end over in-process net.Pipe and loopback TCP (read/write, accept/connect, cancellation via Stop, and with_timeout over a blocking read). Coverage is held at 100% and the suite is -race clean.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

CGO-free, dependency-free, gofmt + go vet clean, -race clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-async/async authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

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

Constants

This section is empty.

Variables

View Source
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.

View Source
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

func Run

func Run(body Body) (any, error)

Run starts a reactor and runs body as its root task, returning the root's result or failure once the reactor is quiescent. It is the equivalent of Ruby's Async{ |task| ... }. The default scheduler is a deterministic cooperative one; use RunOn to supply a host fiber scheduler.

func RunOn

func RunOn(s Scheduler, body Body) (any, error)

RunOn runs body as the root task on the given Scheduler and drives it to quiescence. A binding passes its own fiber-backed Scheduler here.

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 NewBarrier

func NewBarrier() *Barrier

NewBarrier returns an empty barrier.

func (*Barrier) Async

func (b *Barrier) Async(parent *Task, body Body) *Task

Async spawns body as a child of parent and adds it to the barrier (Async::Barrier#async).

func (*Barrier) Empty

func (b *Barrier) Empty() bool

Empty reports whether the barrier tracks no tasks (Async::Barrier#empty?).

func (*Barrier) Size

func (b *Barrier) Size() int

Size returns the number of tracked tasks (Async::Barrier#size).

func (*Barrier) Stop

func (b *Barrier) Stop()

Stop cancels every tracked task and clears the barrier (Async::Barrier#stop).

func (*Barrier) Wait

func (b *Barrier) Wait(caller *Task) error

Wait joins every tracked task in turn, returning the first failure it encounters (ErrStop for a stopped task) and leaving the remaining tasks in the barrier; on success the barrier is emptied (Async::Barrier#wait).

type Body

type Body func(t *Task) (any, error)

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) Run

func (s *CoScheduler) Run()

Run drives the loop to quiescence.

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) Empty

func (c *Condition) Empty() bool

Empty reports whether no task is waiting (Async::Condition#empty?).

func (*Condition) Signal

func (c *Condition) Signal(value any)

Signal wakes every waiting task, delivering value to each (Async::Condition#signal).

func (*Condition) Wait

func (c *Condition) Wait(t *Task) any

Wait suspends the calling task until the condition is signalled, returning the signalled value (Async::Condition#wait).

func (*Condition) WaitCount

func (c *Condition) WaitCount() int

WaitCount returns the number of tasks currently waiting.

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

func Listen(network, addr string) (*Listener, error)

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

func WrapListener(ln net.Listener) *Listener

WrapListener adapts an existing net.Listener into a reactor-aware Listener.

func (*Listener) Accept

func (l *Listener) Accept(t *Task) (*Socket, error)

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.

func (*Listener) Addr

func (l *Listener) Addr() net.Addr

Addr returns the listener's network address (Async::IO::Endpoint#local_address).

func (*Listener) Close

func (l *Listener) Close() error

Close closes the listener (Async::IO::Endpoint#close).

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 NewQueue

func NewQueue() *Queue

NewQueue returns an empty queue.

func (*Queue) Dequeue

func (q *Queue) Dequeue(t *Task) any

Dequeue removes and returns the head item, suspending the calling task while the queue is empty (Async::Queue#dequeue / #pop).

func (*Queue) Empty

func (q *Queue) Empty() bool

Empty reports whether the queue holds no items (Async::Queue#empty?).

func (*Queue) Enqueue

func (q *Queue) Enqueue(v any)

Enqueue appends an item and wakes the longest-waiting dequeuer, if any (Async::Queue#enqueue / #push / #<<).

func (*Queue) Pop

func (q *Queue) Pop(t *Task) any

Pop is an alias for Dequeue (Async::Queue#pop).

func (*Queue) Push

func (q *Queue) Push(v any)

Push is an alias for Enqueue (Async::Queue#push).

func (*Queue) Size

func (q *Queue) Size() int

Size returns the number of buffered items (Async::Queue#size).

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

func NewSemaphore(limit int) *Semaphore

NewSemaphore returns a semaphore permitting limit concurrent holders. A limit below one is clamped to one.

func (*Semaphore) Acquire

func (s *Semaphore) Acquire(t *Task)

Acquire takes one permit, suspending the calling task while the semaphore is at its limit (Async::Semaphore#acquire).

func (*Semaphore) AcquireDo

func (s *Semaphore) AcquireDo(t *Task, fn func() (any, error)) (any, error)

AcquireDo acquires a permit, runs fn, and releases the permit even if fn panics, mirroring the block form Async::Semaphore#acquire{ ... }.

func (*Semaphore) Blocking

func (s *Semaphore) Blocking() bool

Blocking reports whether a further Acquire would block (Async::Semaphore#blocking?).

func (*Semaphore) Count

func (s *Semaphore) Count() int

Count returns the number of permits currently held (Async::Semaphore#count).

func (*Semaphore) Limit

func (s *Semaphore) Limit() int

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).

func (*Semaphore) SetLimit

func (s *Semaphore) SetLimit(limit int)

SetLimit adjusts the permit limit (Async::Semaphore#limit=). Raising it wakes as many waiting tasks as the new headroom allows. A limit below one is clamped to one.

func (*Semaphore) Waiting

func (s *Semaphore) Waiting() int

Waiting returns the number of tasks blocked in Acquire.

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

func Connect(t *Task, network, addr string) (*Socket, error)

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

func Wrap(conn net.Conn) *Socket

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) Close

func (s *Socket) Close() error

Close closes the underlying connection (Async::IO#close).

func (*Socket) Conn

func (s *Socket) Conn() net.Conn

Conn returns the underlying net.Conn.

func (*Socket) Read

func (s *Socket) Read(t *Task, p []byte) (int, error)

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.

func (*Socket) Write

func (s *Socket) Write(t *Task, p []byte) (int, error)

Write writes p, suspending t's fiber on the reactor until the write completes; it returns the number of bytes written and any error. A Stop or timeout on t unblocks an outstanding write via a past write deadline.

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

func (t *Task) Async(body Body) *Task

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) Children

func (t *Task) Children() []*Task

Children returns a snapshot of the task's live children.

func (*Task) CompleteQ

func (t *Task) CompleteQ() bool

CompleteQ reports whether the task completed successfully (#completed?).

func (*Task) Err

func (t *Task) Err() error

Err returns the task's failure (or ErrStop for a stopped task), or nil.

func (*Task) FailedQ

func (t *Task) FailedQ() bool

FailedQ reports whether the task failed (#failed?).

func (*Task) Parent

func (t *Task) Parent() *Task

Parent returns the task's parent, or nil for a root task.

func (*Task) Result

func (t *Task) Result() any

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) RunningQ

func (t *Task) RunningQ() bool

RunningQ reports whether the task is running (Async::Task#running?).

func (*Task) Scheduler

func (t *Task) Scheduler() Scheduler

Scheduler returns the scheduler the task runs on.

func (*Task) Sleep

func (t *Task) Sleep(d time.Duration)

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) State

func (t *Task) State() State

State returns the task's lifecycle state.

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) StoppedQ

func (t *Task) StoppedQ() bool

StoppedQ reports whether the task was stopped (#stopped?).

func (*Task) Wait

func (t *Task) Wait(caller *Task) (any, error)

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

func (t *Task) WithTimeout(d time.Duration, fn Body) (any, error)

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.

func (*Task) Yield

func (t *Task) Yield()

Yield reschedules the task cooperatively behind the other runnable tasks, honouring a concurrent cancellation. It mirrors task.yield / Fiber.yield to the reactor.

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 NewWaiter

func NewWaiter(parent *Task) *Waiter

NewWaiter returns a waiter that spawns its tasks as children of parent.

func (*Waiter) Async

func (w *Waiter) Async(body Body) *Task

Async spawns body under the waiter's parent and tracks it (Async::Waiter#async).

func (*Waiter) Size

func (w *Waiter) Size() int

Size returns the number of tracked tasks.

func (*Waiter) Wait

func (w *Waiter) Wait(caller *Task, count int) ([]any, error)

Wait joins the first count tracked tasks (all of them when count is negative or exceeds the number tracked) and returns their results in order, mirroring Async::Waiter#wait(count). It returns the first failure encountered.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL