diff --git a/doc/design/v2/runner/README.md b/doc/design/v2/runner/README.md new file mode 100644 index 0000000..b643356 --- /dev/null +++ b/doc/design/v2/runner/README.md @@ -0,0 +1,43 @@ +# UDF Runner v2 Design + +This directory describes the runner-side architecture for protocol v2. It is a design document set; it does not +define an implementation or commit to a concrete C++ class layout. + +The runner is organized into two source-level namespaces: + +1. The API namespace contains the caller-facing C++ contracts. +2. The Internal namespace owns sockets, accepting, worker scheduling, protocol contexts, and implementation + dependencies. + +`Runner` is the composition root for those components. A runner user transfers a `WorkerFactory` to a production +factory, which constructs `Runner` and its remaining owned components. Unit tests use the same construction seam with +owned test doubles. + +The API namespace must not expose third-party symbols from the Internal namespace. A dependency may cross this +boundary only when it is vendored into an owned project namespace or uses a well-known interoperable ABI, such as the +Arrow C Data Interface. + +## Documents + +- [architecture.md](architecture.md) defines the namespace boundary and component responsibilities. +- [lifecycle.md](lifecycle.md) defines connection, worker, context, and shutdown lifecycles. + +Mermaid `.mmd` files are the diagram sources of truth. Matching `.svg` files are rendered views linked from the +documents. + +## Relationship to the protocol + +The runner implements the protocol described in the [v2 protocol design](../README.md). In particular: + +- [low_level.md](../protocol/low_level.md) defines framing, streams, transport bindings, and close behavior. +- [call_lifecycle.md](../protocol/call_lifecycle.md) defines the generic call abstraction. +- [high_level_calls.md](../protocol/high_level_calls.md) defines `Run`, Function operations, and callbacks. +- [high_level_payloads.md](../protocol/high_level_payloads.md) defines JSON and named payload contracts. + +The runner `Context` is the implementation boundary for those protocol rules. This design does not redefine their +wire format. + +## Initial transport + +The first concrete transport is a Unix-domain stream socket. The socket and acceptor contracts are intentionally +transport-neutral enough to support a future TCP/TLS binding without changing the protocol context contract. diff --git a/doc/design/v2/runner/architecture.md b/doc/design/v2/runner/architecture.md new file mode 100644 index 0000000..d77d0a6 --- /dev/null +++ b/doc/design/v2/runner/architecture.md @@ -0,0 +1,106 @@ +# Runner v2 Architecture + +## Layering + +```text +API namespace: caller-facing C++ contracts + | + v +Internal namespace: sockets, acceptor, worker pool, factory, context manager, context + | + v +Protocol v2 +``` + +Related diagrams: + +- [layered_architecture.svg](layered_architecture.svg) +- [component_relationships.svg](component_relationships.svg) + +Dependencies point downward only. The API namespace contains only project-owned caller-facing contracts. The Internal +namespace may use C++ standard-library types, exceptions, and third-party libraries such as Arrow C++. + +Third-party symbols must not leak from the Internal namespace through the API namespace. A dependency is allowed at +the API boundary only when it is vendored into an owned project namespace or communicated through a well-known ABI. +The current well-known ABI exception is the Arrow C Data Interface: `ArrowArray` and `ArrowSchema` may cross the +boundary under its release and ownership contract; Arrow C++ types remain internal. + +## Components + +### Runner + +`Runner` is the composition root. A runner user transfers ownership of a `WorkerFactory` to a production factory. +The production factory constructs a `Runner` with that factory, transport configuration, limits, a `SocketAcceptor`, +a worker pool, and a `ContextManager`. `Runner` validates and connects those components, starts and stops them, and +destroys them in dependency-safe order after all work has completed. + +Unit tests use the same construction seam to create a `Runner` with owned fakes, stubs, or instrumented component +implementations. The production factory is responsible only for choosing concrete production implementations; the +runner remains responsible for their configuration and composition. + +### Socket + +`Socket` represents one connected byte stream. It owns the accepted descriptor and provides the operations required +by the protocol transport: receive bytes, send bytes, observe closure, and close. The initial implementation uses a +Unix-domain stream descriptor. The abstraction must not expose Unix-specific address types to the protocol context. + +The socket owns its descriptor after construction. Closing or destroying the socket makes the descriptor unusable; +ownership must not be duplicated implicitly. + +### SocketAcceptor + +`SocketAcceptor` owns the listening endpoint, performs bind/listen setup, and accepts connected sockets. It submits +each accepted descriptor to the worker scheduler. Accept failures are classified as retryable, shutdown-related, or +fatal and must not silently become worker failures. + +The initial acceptor listens on a Unix-domain socket. The interface leaves endpoint configuration and accepted-socket +creation abstract so a TCP/TLS acceptor can be added later. + +### WorkerFactory and worker pool + +The runner user initially owns `WorkerFactory` and transfers it to the production factory, which transfers ownership +to `Runner`. The factory is used by reusable worker-pool resources to obtain a worker callable or worker object for +an accepted descriptor. `Runner` destroys the factory only after stopping and joining all worker activity. + +The pool is reusable, but a worker invocation handles one accepted connection. A worker must not retain a protocol +context or descriptor after its invocation returns. Pool sizing, queue limits, and shutdown policy are explicit runner +configuration rather than hidden global state. + +### Worker + +A worker receives one accepted descriptor, asks the `ContextManager` to create a protocol context, runs that context +until normal close, peer disconnect, cancellation, or error, and then releases the context and descriptor. The worker +does not parse protocol messages outside the context boundary. + +### ContextManager + +`ContextManager` converts an owned connected descriptor into a connection-scoped `Context`. It centralizes context +construction, protocol dependencies, limits, cancellation, and cleanup. Context creation failure must close or reclaim +the descriptor according to the documented ownership handoff. + +### Context + +`Context` is the runner-side interface to protocol v2. It owns one connection and exposes the operations needed by the +protocol implementation: framing, control-stream initialization, stream dispatch, call/data handling, callbacks, +keepalive, and close/error processing. + +The context is not an application callback object and is not shared between worker invocations. It is the sole owner +of protocol state for its connection and must enforce the validation, ordering, flow-control, and close rules from the +protocol documents. + +## Data and dependency flow + +```text +runner user --moves WorkerFactory--> production factory + | + v + Runner owns factory, acceptor, pool, and context manager + | +SocketAcceptor -> worker pool -> Worker(fd) -> ContextManager.create(fd) -> protocol Context + | + Socket bytes + Arrow C data values +``` + +Arrow record batches and schemas may cross the API boundary as `ArrowArray` and `ArrowSchema` under the Arrow C Data +Interface. Arrow C++ objects remain Internal-namespace implementation details and are released according to that +interface's ownership rules. diff --git a/doc/design/v2/runner/component_relationships.mmd b/doc/design/v2/runner/component_relationships.mmd new file mode 100644 index 0000000..8df1229 --- /dev/null +++ b/doc/design/v2/runner/component_relationships.mmd @@ -0,0 +1,15 @@ +flowchart LR + User[Runner user] -->|transfers WorkerFactory| Production[Production factory] + Production -->|constructs| Runner[Runner\ncomposition root] + Runner -->|owns| Factory[WorkerFactory] + Runner -->|owns| Acceptor[SocketAcceptor\nlistening endpoint] + Runner -->|owns| Queue[Reusable worker pool] + Runner -->|owns| Manager[ContextManager] + Acceptor -->|accepted fd| Queue + Factory -->|creates worker| Queue + Queue -->|one fd per invocation| Worker[Worker callable] + Worker -->|create context| Manager[ContextManager] + Manager -->|owns connection context| Context[Protocol Context] + Context --> Socket[Socket abstraction] + Socket --> Transport[Unix stream socket\nfuture TCP/TLS binding] + Context --> V2[Protocol v2 interface] diff --git a/doc/design/v2/runner/component_relationships.svg b/doc/design/v2/runner/component_relationships.svg new file mode 100644 index 0000000..40bb7af --- /dev/null +++ b/doc/design/v2/runner/component_relationships.svg @@ -0,0 +1 @@ +

transfers WorkerFactory

constructs

owns

owns

owns

owns

accepted fd

creates worker

one fd per invocation

create context

owns connection context

Runner user

Production factory

Runner
composition root

WorkerFactory

SocketAcceptor
listening endpoint

Reusable worker pool

ContextManager

Worker callable

Protocol Context

Socket abstraction

Unix stream socket
future TCP/TLS binding

Protocol v2 interface

\ No newline at end of file diff --git a/doc/design/v2/runner/connection_lifecycle.mmd b/doc/design/v2/runner/connection_lifecycle.mmd new file mode 100644 index 0000000..1204597 --- /dev/null +++ b/doc/design/v2/runner/connection_lifecycle.mmd @@ -0,0 +1,29 @@ +sequenceDiagram + participant User as Runner user + participant Production as Production factory + participant Runner + participant Acceptor as SocketAcceptor + participant Pool as Worker pool + participant Factory as WorkerFactory + participant Worker + participant Manager as ContextManager + participant Context + + User->>Production: transfer WorkerFactory + Production->>Runner: construct with owned components + Runner->>Acceptor: configure and listen + Acceptor->>Acceptor: accept connection + Acceptor->>Pool: enqueue owned fd + Pool->>Factory: create worker for fd + Factory-->>Pool: worker callable + Pool->>Worker: invoke(fd) + Worker->>Manager: create_context(fd) + Manager-->>Worker: Context(connection) + Worker->>Context: run protocol v2 + Context-->>Context: close / disconnect / cancel / error + Context-->>Worker: return and release connection + Worker-->>Pool: invocation complete + + alt queue, worker, or context creation failure + Acceptor-->>Acceptor: close fd exactly once + end diff --git a/doc/design/v2/runner/connection_lifecycle.svg b/doc/design/v2/runner/connection_lifecycle.svg new file mode 100644 index 0000000..74360f2 --- /dev/null +++ b/doc/design/v2/runner/connection_lifecycle.svg @@ -0,0 +1 @@ +ContextContextManagerWorkerWorkerFactoryWorker poolSocketAcceptorRunnerProduction factoryRunner userContextContextManagerWorkerWorkerFactoryWorker poolSocketAcceptorRunnerProduction factoryRunner useralt[queue,worker, orcontextcreationfailure]transfer WorkerFactoryconstruct with owned componentsconfigure and listenaccept connectionenqueue owned fdcreate worker for fdworker callableinvoke(fd)create_context(fd)Context(connection)run protocol v2close / disconnect / cancel / errorreturn and release connectioninvocation completeclose fd exactly once \ No newline at end of file diff --git a/doc/design/v2/runner/descriptor_ownership.mmd b/doc/design/v2/runner/descriptor_ownership.mmd new file mode 100644 index 0000000..f91a975 --- /dev/null +++ b/doc/design/v2/runner/descriptor_ownership.mmd @@ -0,0 +1,15 @@ +stateDiagram-v2 + [*] --> AcceptorOwned: accept() + AcceptorOwned --> QueueOwned: enqueue succeeds + AcceptorOwned --> Released: queue rejects + QueueOwned --> WorkerOwned: worker starts + QueueOwned --> Released: worker unavailable + WorkerOwned --> ContextOwned: context creation succeeds + WorkerOwned --> Released: context creation fails + ContextOwned --> Released: close / disconnect / cancellation / error + Released --> [*] + + note right of AcceptorOwned + The descriptor has one owner + at every handoff. + end note diff --git a/doc/design/v2/runner/descriptor_ownership.svg b/doc/design/v2/runner/descriptor_ownership.svg new file mode 100644 index 0000000..2d20b0b --- /dev/null +++ b/doc/design/v2/runner/descriptor_ownership.svg @@ -0,0 +1 @@ +

accept()

enqueue succeeds

queue rejects

worker starts

worker unavailable

context creation succeeds

context creation fails

close / disconnect / cancellation / error

AcceptorOwned

QueueOwned

Released

WorkerOwned

ContextOwned

The descriptor has one owner
at every handoff.

\ No newline at end of file diff --git a/doc/design/v2/runner/layered_architecture.mmd b/doc/design/v2/runner/layered_architecture.mmd new file mode 100644 index 0000000..d754135 --- /dev/null +++ b/doc/design/v2/runner/layered_architecture.mmd @@ -0,0 +1,9 @@ +flowchart TB + Api[API namespace\ncaller-facing C++ contracts] + Internal[Internal namespace\nsockets, acceptor, worker pool, contexts] + Arrow[Arrow C Data Interface\nArrowArray and ArrowSchema] + Protocol[Protocol v2\nframing, streams, calls, payloads] + + Api --> Internal + Api --- Arrow + Internal --> Protocol diff --git a/doc/design/v2/runner/layered_architecture.svg b/doc/design/v2/runner/layered_architecture.svg new file mode 100644 index 0000000..fe510e3 --- /dev/null +++ b/doc/design/v2/runner/layered_architecture.svg @@ -0,0 +1 @@ +

API namespace
caller-facing C++ contracts

Internal namespace
sockets, acceptor, worker pool, contexts

Arrow C Data Interface
ArrowArray and ArrowSchema

Protocol v2
framing, streams, calls, payloads

\ No newline at end of file diff --git a/doc/design/v2/runner/lifecycle.md b/doc/design/v2/runner/lifecycle.md new file mode 100644 index 0000000..b4fcfc7 --- /dev/null +++ b/doc/design/v2/runner/lifecycle.md @@ -0,0 +1,92 @@ +# Runner v2 Lifecycle + +## Startup + +1. The runner user transfers ownership of a `WorkerFactory` to the production factory. +2. The production factory creates a `Runner` with that factory, transport configuration, limits, a `SocketAcceptor`, + a reusable worker pool, and a `ContextManager`. +3. The runner validates and connects its owned components, then the acceptor binds and listens on the configured + Unix-domain endpoint. +4. The runner starts accepting connections and dispatching descriptors. + +The runner must reject invalid endpoint configuration before starting worker threads. Startup failure leaves no live +listener or worker-owned descriptor. + +## Accepted connection + +Related diagrams: + +- [connection_lifecycle.svg](connection_lifecycle.svg) +- [descriptor_ownership.svg](descriptor_ownership.svg) + +```text +SocketAcceptor + | accept() + v +owned connected fd + | enqueue + v +reusable worker-pool resource + | invoke Runner-owned WorkerFactory product + v +Worker(fd) + | ContextManager.create(fd) + v +Context(connection) + | initialize control stream and run protocol + v +normal close / peer disconnect / cancellation / error +``` + +The descriptor has exactly one owner at each handoff: + +- the acceptor owns it until successful submission; +- the worker owns it while creating the context; +- the context owns it after successful creation; +- the context closes or transfers it during final teardown. + +Failed queue submission, worker creation, or context creation must close the descriptor exactly once. + +## Worker and context behavior + +The worker creates a fresh context for every accepted descriptor. The context then: + +1. performs protocol initialization and advertises or validates capabilities as required by the endpoint role; +2. receives and dispatches framed stream messages; +3. manages calls, data streams, callbacks, keepalive, flow control, and validation; +4. handles normal close, error close, cancellation, and peer disconnect; +5. releases all protocol and transport resources before returning. + +Worker-pool resources may be reused after the worker returns, but connection-scoped protocol state may not be reused. + +## Shutdown + +Shutdown has three phases: + +1. Stop accepting new descriptors and wake a blocked accept operation. +2. Stop queueing work and request cancellation for queued and active workers. +3. Wait for active contexts to close or reach the configured shutdown deadline, then reclaim remaining resources. + +The runner must close the listening socket before reporting that accepting has stopped. After all workers have +returned, it destroys its contexts, worker pool, acceptor, context manager, and `WorkerFactory` in dependency-safe +order. + +## Failure cases + +| Failure | Required behavior | +| --- | --- | +| Accept failure | Retry transient failures; terminate cleanly for shutdown; surface fatal failures. | +| Queue full | Apply the configured admission policy; do not leak the accepted descriptor. | +| Worker creation failure | Close the descriptor and report the failure through runner diagnostics. | +| Context creation failure | Reclaim the descriptor and return the creation error. | +| Protocol validation error | Let the context perform protocol error/close handling, then terminate the connection if required. | +| Peer disconnect | Let the context release all connection state and return normally unless cleanup fails. | +| Worker exception | Contain it within the Internal namespace and guarantee descriptor/context cleanup before reporting it through the API contract. | + +## Concurrency requirements + +- A context is single-owner unless the protocol implementation explicitly documents internal concurrency. +- Worker-pool resources may run concurrently, but no connection-scoped stream state is shared between contexts. +- The acceptor, queue, pool, and context manager must define their thread-safety guarantees in the API contract; + Internal-namespace implementation details remain encapsulated. +- Cancellation and shutdown must be safe when they race with accept, queue submission, context creation, or peer close.