eventloop

package
v1.4.0 Latest Latest
Warning

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

Go to latest
Published: Apr 19, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package eventloop is internal infrastructure used by the celeris PostgreSQL and Redis drivers. It is not a supported public API: no backwards-compatibility guarantees are made for the types and functions exported here, and import from outside the celeris module is not supported.

What it provides

A stripped-down event loop — one worker per CPU (or per request) owning an epoll fd on Linux, a net.Conn per FD on other platforms — that drivers use when no celeris HTTP server is available. When a Server is registered via Resolve, the Server's own engine.EventLoopProvider is returned and the standalone loop is never created; drivers and HTTP workers then share the same kernel resources.

ServerProvider indirection

Celeris's root package imports engine, and engine is imported here, so we cannot import celeris directly (import cycle). Instead, *celeris.Server is expected to satisfy ServerProvider:

type ServerProvider interface {
    EventLoopProvider() engine.EventLoopProvider
}

Resolve accepts a nil provider to request the standalone loop, or a non-nil *celeris.Server (typed as ServerProvider) to hook into the HTTP event loop. Every Resolve must be paired with a Release so the standalone loop's refcount can drop to zero and shut down cleanly.

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyRegistered = errors.New("celeris/eventloop: fd already registered")

ErrAlreadyRegistered is returned when RegisterConn is called for a fd that is already registered on this worker.

View Source
var ErrLoopClosed = errors.New("celeris/eventloop: loop is closed")

ErrLoopClosed is returned when an operation is issued against a Loop that has already been closed.

Functions

func IsAsyncServer

func IsAsyncServer(sp ServerProvider) bool

IsAsyncServer reports whether sp opts into async handler dispatch. Safe to call with a nil sp — returns false.

func Release

func Release(provider engine.EventLoopProvider)

Release decrements the refcount for the standalone Loop. If the provider was not the standalone Loop (e.g. it came from a Server) the call is a no-op. When the last driver releases the standalone Loop, it is closed.

func Resolve

Resolve returns an engine.EventLoopProvider suitable for driver use.

If sp is non-nil and its engine implements EventLoopProvider, that provider is returned. Otherwise Resolve spawns (or reuses) a package-level standalone Loop and increments its refcount. Server-backed providers are not refcounted — the Server owns its engine's lifetime.

Every successful Resolve must be paired with a Release so the standalone Loop can be torn down when the last driver releases it.

Types

type AsyncHandlerProvider

type AsyncHandlerProvider interface {
	AsyncHandlers() bool
}

AsyncHandlerProvider is optionally implemented by ServerProvider to report whether the server dispatches HTTP handlers to spawned goroutines rather than running them inline on LockOSThread'd worker Gs. Drivers opened WithEngine(srv) consult this via type assertion — when the caller will run on an unlocked handler G, the direct net.Conn path becomes the fastest choice (Go netpoll parks the G on EPOLLIN without blocking an M). When the caller runs on a locked worker, the mini-loop busy-poll path is preferred (direct net.Conn.Read on a locked M triggers stoplockedm+startlockedm futex storms).

type Loop

type Loop struct {
	// contains filtered or unexported fields
}

Loop is the standalone event loop used by drivers when no HTTP server is available. The struct is shared across all platforms; platform-specific details live behind the loopWorker implementations created by newLoop.

func New

func New(workers int) (*Loop, error)

New creates a standalone Loop. If workers <= 0, runtime.NumCPU() is used. The caller must invoke Loop.Close when finished. Drivers must unregister any FDs they own before closing the loop.

func (*Loop) Close

func (l *Loop) Close() error

Close stops all workers and releases their resources. Drivers must unregister their FDs before calling Close; FDs still registered at Close time receive onClose(ErrLoopClosed).

func (*Loop) NumWorkers

func (l *Loop) NumWorkers() int

NumWorkers satisfies engine.EventLoopProvider.

func (*Loop) WorkerLoop

func (l *Loop) WorkerLoop(n int) engine.WorkerLoop

WorkerLoop satisfies engine.EventLoopProvider. Panics if n is out of range.

type ServerProvider

type ServerProvider interface {
	// EventLoopProvider returns the server engine's event-loop provider, or
	// nil when the engine does not expose one (e.g. the net/http fallback).
	EventLoopProvider() engine.EventLoopProvider
}

ServerProvider is implemented by *celeris.Server. The indirection avoids a dependency cycle (celeris root imports engine; driver/internal/eventloop cannot import celeris). Callers cast *celeris.Server to this interface.

type SyncBusyRoundTripper

type SyncBusyRoundTripper interface {
	WriteAndPollBusy(fd int, data []byte, rbuf []byte, onRecv func([]byte)) (ok bool, err error)
}

SyncBusyRoundTripper is the variant of SyncRoundTripper that skips the runtime.Gosched() yield in its poll spin. Intended for callers known to run on a runtime.LockOSThread'd goroutine (the celeris HTTP engine's io_uring / epoll worker), where every Gosched incurs a stoplockedm + startlockedm futex pair and collapses integrated throughput.

Unlocked callers (standalone mini-loop + foreign HTTP) should use SyncRoundTripper. Locked callers (celeris HTTP engine host) must use WriteAndPollBusy. Drivers select the path at open time based on whether an engine was supplied — see the driver's WithEngine option.

type SyncMultiRoundTripper

type SyncMultiRoundTripper interface {
	WriteAndPollMulti(fd int, data []byte, rbuf []byte, onRecv func([]byte), isDone func() bool, beforeRearm func()) (ok bool, err error)
}

SyncMultiRoundTripper extends SyncRoundTripper with a multi-response variant for pipelined protocols. WriteAndPollMulti writes data, then repeatedly polls and reads until isDone reports true or a hard timeout expires. isDone is called after each onRecv batch (under recvMu) so the driver can check how many protocol frames have been parsed.

beforeRearm runs after all reads complete but before EPOLLIN is re-armed and recvMu is released. The driver uses it to transition from direct-index dispatch to bridge-queue dispatch so the event loop sees a consistent state when it resumes reads on this fd. Pass nil to skip.

Returns ok=true when isDone fired. ok=false means the caller should fall back to the async event-loop path (no data arrived or isDone never fired).

type SyncRoundTripper

type SyncRoundTripper interface {
	WriteAndPoll(fd int, data []byte, rbuf []byte, onRecv func([]byte)) (ok bool, err error)
}

SyncRoundTripper is an optional interface for workers that support a combined write+read fast path. The driver calls WriteAndPoll to send query bytes and then poll for the response on the calling goroutine (no event-loop round trip, no channel signal, no futex). The caller must supply a readBuf and the same onRecv callback as RegisterConn. If the socket returns EAGAIN before any data is read, ok=false is returned and the caller should fall back to the normal async path.

Contract:

  • EPOLLIN is temporarily masked while the caller reads, preventing the event loop from racing the read. It is restored on return.
  • The caller must NOT hold c.mu during the read (flushLocked already released it). Only the writing side holds c.mu; the read side is serialized by the EPOLLIN mask.
  • Edge-triggered epoll: we drain to EAGAIN inside WriteAndPoll, so no stale edge is left. After EPOLLIN is re-armed, the next kernel-buffer arrival fires a fresh edge.

Jump to

Keyboard shortcuts

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