From c6290edde0a703d3802028f7bdbb1727561a62db Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Wed, 19 Aug 2026 20:02:31 +0200 Subject: [PATCH 1/3] Document v2 runner architecture diagrams --- doc/design/v2/runner/README.md | 37 ++++++ doc/design/v2/runner/abi.md | 118 ++++++++++++++++++ doc/design/v2/runner/abi_boundary.mmd | 12 ++ doc/design/v2/runner/abi_boundary.svg | 1 + doc/design/v2/runner/architecture.md | 92 ++++++++++++++ .../v2/runner/component_relationships.mmd | 10 ++ .../v2/runner/component_relationships.svg | 1 + doc/design/v2/runner/connection_lifecycle.mmd | 25 ++++ doc/design/v2/runner/connection_lifecycle.svg | 1 + doc/design/v2/runner/descriptor_ownership.mmd | 15 +++ doc/design/v2/runner/descriptor_ownership.svg | 1 + doc/design/v2/runner/layered_architecture.mmd | 11 ++ doc/design/v2/runner/layered_architecture.svg | 1 + doc/design/v2/runner/lifecycle.md | 89 +++++++++++++ 14 files changed, 414 insertions(+) create mode 100644 doc/design/v2/runner/README.md create mode 100644 doc/design/v2/runner/abi.md create mode 100644 doc/design/v2/runner/abi_boundary.mmd create mode 100644 doc/design/v2/runner/abi_boundary.svg create mode 100644 doc/design/v2/runner/architecture.md create mode 100644 doc/design/v2/runner/component_relationships.mmd create mode 100644 doc/design/v2/runner/component_relationships.svg create mode 100644 doc/design/v2/runner/connection_lifecycle.mmd create mode 100644 doc/design/v2/runner/connection_lifecycle.svg create mode 100644 doc/design/v2/runner/descriptor_ownership.mmd create mode 100644 doc/design/v2/runner/descriptor_ownership.svg create mode 100644 doc/design/v2/runner/layered_architecture.mmd create mode 100644 doc/design/v2/runner/layered_architecture.svg create mode 100644 doc/design/v2/runner/lifecycle.md diff --git a/doc/design/v2/runner/README.md b/doc/design/v2/runner/README.md new file mode 100644 index 0000000..0710dba --- /dev/null +++ b/doc/design/v2/runner/README.md @@ -0,0 +1,37 @@ +# 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 three layers: + +1. The private C++ implementation owns sockets, accepting, worker scheduling, and protocol contexts. +2. The C ABI exposes stable opaque handles, callback/vtable contracts, status values, and Arrow C Data Interface + values. +3. A header-only C++ facade provides RAII and typed C++ adapters on top of the C ABI. + +## Documents + +- [architecture.md](architecture.md) defines the layers and component responsibilities. +- [lifecycle.md](lifecycle.md) defines connection, worker, context, and shutdown lifecycles. +- [abi.md](abi.md) defines the C ABI and the header-only C++ facade contract. + +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/abi.md b/doc/design/v2/runner/abi.md new file mode 100644 index 0000000..54301f9 --- /dev/null +++ b/doc/design/v2/runner/abi.md @@ -0,0 +1,118 @@ +# Runner v2 ABI Design + +## C ABI principles + +Related diagram: + +- [abi_boundary.svg](abi_boundary.svg) + +The C ABI is the stable boundary for callers written in C or other languages. It must not expose C++ classes, +templates, exceptions, standard-library containers, RTTI-dependent types, or Arrow C++ types. + +The ABI uses: + +- opaque forward-declared handles for runner, acceptor, socket, worker factory, context manager, worker, and context; +- callback/vtable structs for injected behavior and implementation dependencies; +- explicit status values for success, invalid arguments, closed peer, cancelled operation, resource exhaustion, and + implementation failure; +- an error inspection function returning a thread-local, null-terminated diagnostic string; +- explicit destroy/release operations for every owning handle; +- fixed-width integer types and `size_t` only where buffer length is paired with a pointer; +- `ArrowArray` and `ArrowSchema` for record-batch exchange, following the Arrow C Data Interface release callbacks. + +Illustrative shape: + +```c +typedef struct udf_v2_context udf_v2_context; +typedef struct udf_v2_context_manager udf_v2_context_manager; + +typedef struct { + int (*create_context)(void* user_data, int socket_fd, + udf_v2_context** out_context); + void (*destroy)(void* user_data); + void* user_data; +} udf_v2_context_manager_vtable; + +typedef struct { + int (*create_worker)(void* user_data, int socket_fd, + const udf_v2_context_manager_vtable* manager, + void** out_worker); + int (*run_worker)(void* user_data, void* worker); + void (*destroy_worker)(void* user_data, void* worker); + void (*destroy)(void* user_data); + void* user_data; +} udf_v2_worker_factory_vtable; +``` + +The exact exported names remain subject to the implementation review, but every vtable must document callback +ordering, reentrancy, thread affinity, nullability, and ownership. + +## Ownership and descriptor handoff + +The C ABI must make descriptor ownership explicit. A successful context-creation callback transfers the descriptor to +the returned context. A failed callback retains responsibility for closing or otherwise reclaiming the descriptor; +the runner must not close it a second time. + +Opaque handles are owned by the creator until an explicitly documented transfer. Borrowed handles are valid only for +the duration of the callback that supplies them. Destroy functions must tolerate the documented null/empty state and +must not invoke user callbacks after their parent owner has been destroyed. + +## Status and error handling + +C callbacks return a stable integer status. They must not throw across the ABI. The runner catches implementation +exceptions and converts them to a failure status with diagnostic text. + +The error API is associated with the calling thread and remains valid until the next ABI call on that thread or until +the owning object is destroyed. Callers must copy the text if they need it longer. Successful calls clear the previous +error for that thread. + +The ABI must distinguish at least: + +- success; +- invalid argument or incompatible vtable; +- operation cancelled or runner shutting down; +- peer closed the connection; +- queue/resource limit reached; +- protocol validation failure; +- internal implementation failure. + +## Callback/vtable rules + +- Vtables are versioned and include a size/version field so compatible extensions can be detected. +- Required callbacks are validated before the runner starts. +- Optional callbacks have documented defaults and are never called when absent. +- `user_data` belongs to the vtable provider and remains alive until all callbacks and destruction operations finish. +- Callbacks may execute on acceptor or worker-pool threads; the ABI documents this rather than promising a single + caller thread. +- Reentrant calls are forbidden unless explicitly marked reentrant. +- A callback must not retain borrowed pointers or handles after returning. + +## Arrow C Data Interface + +Arrow record batches and schemas cross the ABI only through `struct ArrowArray` and `struct ArrowSchema`. + +- Producers initialize output structures and provide valid release callbacks. +- Consumers either import the structures or release them according to the Arrow C Data Interface contract. +- A successful transfer moves release ownership to the consumer; a failed transfer leaves the producer responsible. +- No Arrow C++ symbol, `std::shared_ptr`, or Arrow C++ exception crosses the ABI. +- The C++ implementation may use Arrow C++ bridge helpers internally, but the exported symbol surface remains C-only. + +## Header-only C++ facade + +The C++ facade is implemented entirely in headers and forwards to the C ABI. It provides: + +- RAII wrappers for owning opaque handles; +- non-owning views for borrowed handles and Arrow C data; +- typed status/error conversion to C++ exceptions or an equivalent typed error result; +- adapters from C++ worker factories and callables to C callback/vtable structures; +- compile-time checks for supported callback signatures; +- explicit move-only ownership for descriptors, contexts, and workers. + +The facade must not require callers to link against private C++ implementation symbols. Its ABI compatibility is the C +ABI's compatibility; changing the facade's inline implementation must not change the C handle layout or vtable rules. + +## Compatibility and versioning + +The ABI version is negotiated independently from the protocol version. New vtable fields are appended, guarded by the +declared size, and given safe defaults. Existing fields cannot change meaning or ownership. Protocol capability and +message-version negotiation remains the responsibility of `Context` and the v2 protocol layer. diff --git a/doc/design/v2/runner/abi_boundary.mmd b/doc/design/v2/runner/abi_boundary.mmd new file mode 100644 index 0000000..9ecff68 --- /dev/null +++ b/doc/design/v2/runner/abi_boundary.mmd @@ -0,0 +1,12 @@ +flowchart LR + Cpp[Private C++ types\nclasses, exceptions, Arrow C++] + Handles[C ABI\nopaque handles and callback/vtables] + Data[Arrow C Data Interface\nArrowArray / ArrowSchema] + Facade[Header-only C++ facade\nRAII, typed adapters, exceptions] + Consumer[C or other-language consumer] + + Cpp --> Handles + Handles --- Data + Handles --> Consumer + Facade --> Handles + Facade --> Data diff --git a/doc/design/v2/runner/abi_boundary.svg b/doc/design/v2/runner/abi_boundary.svg new file mode 100644 index 0000000..6d8a889 --- /dev/null +++ b/doc/design/v2/runner/abi_boundary.svg @@ -0,0 +1 @@ +

Private C++ types
classes, exceptions, Arrow C++

C ABI
opaque handles and callback/vtables

Arrow C Data Interface
ArrowArray / ArrowSchema

Header-only C++ facade
RAII, typed adapters, exceptions

C or other-language consumer

\ No newline at end of file diff --git a/doc/design/v2/runner/architecture.md b/doc/design/v2/runner/architecture.md new file mode 100644 index 0000000..08dbd59 --- /dev/null +++ b/doc/design/v2/runner/architecture.md @@ -0,0 +1,92 @@ +# Runner v2 Architecture + +## Layering + +```text +Header-only C++ facade + | + v +Stable C ABI: opaque handles, vtables, status/error values, Arrow C Data Interface + | + v +Private C++ implementation: sockets, acceptor, worker pool, factory, context manager, context +``` + +Related diagrams: + +- [layered_architecture.svg](layered_architecture.svg) +- [component_relationships.svg](component_relationships.svg) + +Dependencies point downward only. The private implementation may use C++ standard-library types, exceptions, and +the Arrow C++ library. None of those types cross the C ABI. The header-only facade depends only on the C ABI headers, +the C++ standard library, and the Arrow C Data Interface declarations. + +## Components + +### 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 does not decide how workers are constructed. An external owner injects a `WorkerFactory`. The factory is +used by reusable worker-pool resources to obtain a worker callable or worker object for an accepted descriptor. + +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 +external WorkerFactory + | +SocketAcceptor -> worker pool -> Worker(fd) + | + v + ContextManager.create(fd) + | + v + protocol Context + | + Socket bytes + Arrow C data values +``` + +Arrow record batches and schemas cross the public boundary as `ArrowArray` and `ArrowSchema`. Arrow C++ objects are +implementation details and are released according to the Arrow C Data Interface 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..38496f7 --- /dev/null +++ b/doc/design/v2/runner/component_relationships.mmd @@ -0,0 +1,10 @@ +flowchart LR + External[External owner] -->|injects| Factory[WorkerFactory] + Acceptor[SocketAcceptor\nlistening endpoint] -->|accepted fd| Queue[Reusable worker pool] + 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..6763aba --- /dev/null +++ b/doc/design/v2/runner/component_relationships.svg @@ -0,0 +1 @@ +

injects

accepted fd

creates worker

one fd per invocation

create context

owns connection context

External owner

WorkerFactory

SocketAcceptor
listening endpoint

Reusable worker pool

Worker callable

ContextManager

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..62c8dba --- /dev/null +++ b/doc/design/v2/runner/connection_lifecycle.mmd @@ -0,0 +1,25 @@ +sequenceDiagram + participant Owner as External owner + participant Acceptor as SocketAcceptor + participant Pool as Worker pool + participant Factory as WorkerFactory + participant Worker + participant Manager as ContextManager + participant Context + + Owner->>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..61cb060 --- /dev/null +++ b/doc/design/v2/runner/connection_lifecycle.svg @@ -0,0 +1 @@ +ContextContextManagerWorkerWorkerFactoryWorker poolSocketAcceptorExternal ownerContextContextManagerWorkerWorkerFactoryWorker poolSocketAcceptorExternal owneralt[queue,worker, orcontextcreationfailure]configure 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..00a40fa --- /dev/null +++ b/doc/design/v2/runner/layered_architecture.mmd @@ -0,0 +1,11 @@ +flowchart TB + Facade[Header-only C++ facade\nRAII, typed adapters, C++ callables] + CAbi[Stable C ABI\nopaque handles, vtables, status/error values] + Arrow[Arrow C Data Interface\nArrowArray and ArrowSchema] + Impl[Private C++ implementation\nsockets, acceptor, worker pool, contexts] + Protocol[Protocol v2\nframing, streams, calls, payloads] + + Facade --> CAbi + CAbi --> Impl + CAbi --- Arrow + Impl --> 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..e377a69 --- /dev/null +++ b/doc/design/v2/runner/layered_architecture.svg @@ -0,0 +1 @@ +

Header-only C++ facade
RAII, typed adapters, C++ callables

Stable C ABI
opaque handles, vtables, status/error values

Arrow C Data Interface
ArrowArray and ArrowSchema

Private C++ implementation
sockets, acceptor, worker pool, contexts

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..5441aa3 --- /dev/null +++ b/doc/design/v2/runner/lifecycle.md @@ -0,0 +1,89 @@ +# Runner v2 Lifecycle + +## Startup + +1. The external owner creates transport configuration, a `WorkerFactory`, a `ContextManager`, and runner limits. +2. The runner creates a `SocketAcceptor` and a reusable worker pool. +3. 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 injected 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. It must not destroy injected +factory or context-manager dependencies until all workers have returned. + +## 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 | Convert it at the ABI boundary to a status/error and guarantee descriptor/context cleanup. | + +## 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 C++ and C ABI + contracts. +- Cancellation and shutdown must be safe when they race with accept, queue submission, context creation, or peer close. From c666982744ae2b5387bfdb4aab543b4d410b036d Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Tue, 1 Sep 2026 18:33:47 +0200 Subject: [PATCH 2/3] docs: replace runner ABI design with namespaces --- doc/design/v2/runner/README.md | 16 +-- doc/design/v2/runner/abi.md | 118 ------------------ doc/design/v2/runner/abi_boundary.mmd | 12 -- doc/design/v2/runner/abi_boundary.svg | 1 - doc/design/v2/runner/architecture.md | 21 ++-- doc/design/v2/runner/layered_architecture.mmd | 12 +- doc/design/v2/runner/layered_architecture.svg | 2 +- doc/design/v2/runner/lifecycle.md | 6 +- 8 files changed, 31 insertions(+), 157 deletions(-) delete mode 100644 doc/design/v2/runner/abi.md delete mode 100644 doc/design/v2/runner/abi_boundary.mmd delete mode 100644 doc/design/v2/runner/abi_boundary.svg diff --git a/doc/design/v2/runner/README.md b/doc/design/v2/runner/README.md index 0710dba..6155d0e 100644 --- a/doc/design/v2/runner/README.md +++ b/doc/design/v2/runner/README.md @@ -3,18 +3,20 @@ 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 three layers: +The runner is organized into two source-level namespaces: -1. The private C++ implementation owns sockets, accepting, worker scheduling, and protocol contexts. -2. The C ABI exposes stable opaque handles, callback/vtable contracts, status values, and Arrow C Data Interface - values. -3. A header-only C++ facade provides RAII and typed C++ adapters on top of the C ABI. +1. The API namespace contains the caller-facing C++ contracts. +2. The Internal namespace owns sockets, accepting, worker scheduling, protocol contexts, and implementation + dependencies. + +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 layers and component responsibilities. +- [architecture.md](architecture.md) defines the namespace boundary and component responsibilities. - [lifecycle.md](lifecycle.md) defines connection, worker, context, and shutdown lifecycles. -- [abi.md](abi.md) defines the C ABI and the header-only C++ facade contract. Mermaid `.mmd` files are the diagram sources of truth. Matching `.svg` files are rendered views linked from the documents. diff --git a/doc/design/v2/runner/abi.md b/doc/design/v2/runner/abi.md deleted file mode 100644 index 54301f9..0000000 --- a/doc/design/v2/runner/abi.md +++ /dev/null @@ -1,118 +0,0 @@ -# Runner v2 ABI Design - -## C ABI principles - -Related diagram: - -- [abi_boundary.svg](abi_boundary.svg) - -The C ABI is the stable boundary for callers written in C or other languages. It must not expose C++ classes, -templates, exceptions, standard-library containers, RTTI-dependent types, or Arrow C++ types. - -The ABI uses: - -- opaque forward-declared handles for runner, acceptor, socket, worker factory, context manager, worker, and context; -- callback/vtable structs for injected behavior and implementation dependencies; -- explicit status values for success, invalid arguments, closed peer, cancelled operation, resource exhaustion, and - implementation failure; -- an error inspection function returning a thread-local, null-terminated diagnostic string; -- explicit destroy/release operations for every owning handle; -- fixed-width integer types and `size_t` only where buffer length is paired with a pointer; -- `ArrowArray` and `ArrowSchema` for record-batch exchange, following the Arrow C Data Interface release callbacks. - -Illustrative shape: - -```c -typedef struct udf_v2_context udf_v2_context; -typedef struct udf_v2_context_manager udf_v2_context_manager; - -typedef struct { - int (*create_context)(void* user_data, int socket_fd, - udf_v2_context** out_context); - void (*destroy)(void* user_data); - void* user_data; -} udf_v2_context_manager_vtable; - -typedef struct { - int (*create_worker)(void* user_data, int socket_fd, - const udf_v2_context_manager_vtable* manager, - void** out_worker); - int (*run_worker)(void* user_data, void* worker); - void (*destroy_worker)(void* user_data, void* worker); - void (*destroy)(void* user_data); - void* user_data; -} udf_v2_worker_factory_vtable; -``` - -The exact exported names remain subject to the implementation review, but every vtable must document callback -ordering, reentrancy, thread affinity, nullability, and ownership. - -## Ownership and descriptor handoff - -The C ABI must make descriptor ownership explicit. A successful context-creation callback transfers the descriptor to -the returned context. A failed callback retains responsibility for closing or otherwise reclaiming the descriptor; -the runner must not close it a second time. - -Opaque handles are owned by the creator until an explicitly documented transfer. Borrowed handles are valid only for -the duration of the callback that supplies them. Destroy functions must tolerate the documented null/empty state and -must not invoke user callbacks after their parent owner has been destroyed. - -## Status and error handling - -C callbacks return a stable integer status. They must not throw across the ABI. The runner catches implementation -exceptions and converts them to a failure status with diagnostic text. - -The error API is associated with the calling thread and remains valid until the next ABI call on that thread or until -the owning object is destroyed. Callers must copy the text if they need it longer. Successful calls clear the previous -error for that thread. - -The ABI must distinguish at least: - -- success; -- invalid argument or incompatible vtable; -- operation cancelled or runner shutting down; -- peer closed the connection; -- queue/resource limit reached; -- protocol validation failure; -- internal implementation failure. - -## Callback/vtable rules - -- Vtables are versioned and include a size/version field so compatible extensions can be detected. -- Required callbacks are validated before the runner starts. -- Optional callbacks have documented defaults and are never called when absent. -- `user_data` belongs to the vtable provider and remains alive until all callbacks and destruction operations finish. -- Callbacks may execute on acceptor or worker-pool threads; the ABI documents this rather than promising a single - caller thread. -- Reentrant calls are forbidden unless explicitly marked reentrant. -- A callback must not retain borrowed pointers or handles after returning. - -## Arrow C Data Interface - -Arrow record batches and schemas cross the ABI only through `struct ArrowArray` and `struct ArrowSchema`. - -- Producers initialize output structures and provide valid release callbacks. -- Consumers either import the structures or release them according to the Arrow C Data Interface contract. -- A successful transfer moves release ownership to the consumer; a failed transfer leaves the producer responsible. -- No Arrow C++ symbol, `std::shared_ptr`, or Arrow C++ exception crosses the ABI. -- The C++ implementation may use Arrow C++ bridge helpers internally, but the exported symbol surface remains C-only. - -## Header-only C++ facade - -The C++ facade is implemented entirely in headers and forwards to the C ABI. It provides: - -- RAII wrappers for owning opaque handles; -- non-owning views for borrowed handles and Arrow C data; -- typed status/error conversion to C++ exceptions or an equivalent typed error result; -- adapters from C++ worker factories and callables to C callback/vtable structures; -- compile-time checks for supported callback signatures; -- explicit move-only ownership for descriptors, contexts, and workers. - -The facade must not require callers to link against private C++ implementation symbols. Its ABI compatibility is the C -ABI's compatibility; changing the facade's inline implementation must not change the C handle layout or vtable rules. - -## Compatibility and versioning - -The ABI version is negotiated independently from the protocol version. New vtable fields are appended, guarded by the -declared size, and given safe defaults. Existing fields cannot change meaning or ownership. Protocol capability and -message-version negotiation remains the responsibility of `Context` and the v2 protocol layer. diff --git a/doc/design/v2/runner/abi_boundary.mmd b/doc/design/v2/runner/abi_boundary.mmd deleted file mode 100644 index 9ecff68..0000000 --- a/doc/design/v2/runner/abi_boundary.mmd +++ /dev/null @@ -1,12 +0,0 @@ -flowchart LR - Cpp[Private C++ types\nclasses, exceptions, Arrow C++] - Handles[C ABI\nopaque handles and callback/vtables] - Data[Arrow C Data Interface\nArrowArray / ArrowSchema] - Facade[Header-only C++ facade\nRAII, typed adapters, exceptions] - Consumer[C or other-language consumer] - - Cpp --> Handles - Handles --- Data - Handles --> Consumer - Facade --> Handles - Facade --> Data diff --git a/doc/design/v2/runner/abi_boundary.svg b/doc/design/v2/runner/abi_boundary.svg deleted file mode 100644 index 6d8a889..0000000 --- a/doc/design/v2/runner/abi_boundary.svg +++ /dev/null @@ -1 +0,0 @@ -

Private C++ types
classes, exceptions, Arrow C++

C ABI
opaque handles and callback/vtables

Arrow C Data Interface
ArrowArray / ArrowSchema

Header-only C++ facade
RAII, typed adapters, exceptions

C or other-language consumer

\ No newline at end of file diff --git a/doc/design/v2/runner/architecture.md b/doc/design/v2/runner/architecture.md index 08dbd59..d0b6c8e 100644 --- a/doc/design/v2/runner/architecture.md +++ b/doc/design/v2/runner/architecture.md @@ -3,13 +3,13 @@ ## Layering ```text -Header-only C++ facade +API namespace: caller-facing C++ contracts | v -Stable C ABI: opaque handles, vtables, status/error values, Arrow C Data Interface +Internal namespace: sockets, acceptor, worker pool, factory, context manager, context | v -Private C++ implementation: sockets, acceptor, worker pool, factory, context manager, context +Protocol v2 ``` Related diagrams: @@ -17,9 +17,13 @@ Related diagrams: - [layered_architecture.svg](layered_architecture.svg) - [component_relationships.svg](component_relationships.svg) -Dependencies point downward only. The private implementation may use C++ standard-library types, exceptions, and -the Arrow C++ library. None of those types cross the C ABI. The header-only facade depends only on the C ABI headers, -the C++ standard library, and the Arrow C Data Interface declarations. +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 @@ -88,5 +92,6 @@ SocketAcceptor -> worker pool -> Worker(fd) Socket bytes + Arrow C data values ``` -Arrow record batches and schemas cross the public boundary as `ArrowArray` and `ArrowSchema`. Arrow C++ objects are -implementation details and are released according to the Arrow C Data Interface ownership rules. +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/layered_architecture.mmd b/doc/design/v2/runner/layered_architecture.mmd index 00a40fa..d754135 100644 --- a/doc/design/v2/runner/layered_architecture.mmd +++ b/doc/design/v2/runner/layered_architecture.mmd @@ -1,11 +1,9 @@ flowchart TB - Facade[Header-only C++ facade\nRAII, typed adapters, C++ callables] - CAbi[Stable C ABI\nopaque handles, vtables, status/error values] + Api[API namespace\ncaller-facing C++ contracts] + Internal[Internal namespace\nsockets, acceptor, worker pool, contexts] Arrow[Arrow C Data Interface\nArrowArray and ArrowSchema] - Impl[Private C++ implementation\nsockets, acceptor, worker pool, contexts] Protocol[Protocol v2\nframing, streams, calls, payloads] - Facade --> CAbi - CAbi --> Impl - CAbi --- Arrow - Impl --> Protocol + Api --> Internal + Api --- Arrow + Internal --> Protocol diff --git a/doc/design/v2/runner/layered_architecture.svg b/doc/design/v2/runner/layered_architecture.svg index e377a69..fe510e3 100644 --- a/doc/design/v2/runner/layered_architecture.svg +++ b/doc/design/v2/runner/layered_architecture.svg @@ -1 +1 @@ -

Header-only C++ facade
RAII, typed adapters, C++ callables

Stable C ABI
opaque handles, vtables, status/error values

Arrow C Data Interface
ArrowArray and ArrowSchema

Private C++ implementation
sockets, acceptor, worker pool, contexts

Protocol v2
framing, streams, calls, payloads

\ No newline at end of file +

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 index 5441aa3..3d94d3b 100644 --- a/doc/design/v2/runner/lifecycle.md +++ b/doc/design/v2/runner/lifecycle.md @@ -78,12 +78,12 @@ factory or context-manager dependencies until all workers have returned. | 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 | Convert it at the ABI boundary to a status/error and guarantee descriptor/context cleanup. | +| 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 C++ and C ABI - contracts. +- 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. From 19178e85f0a7f9726f7b669155f6a8567cf11b67 Mon Sep 17 00:00:00 2001 From: Torsten Kilias Date: Tue, 1 Sep 2026 18:54:45 +0200 Subject: [PATCH 3/3] docs: make runner the composition root --- doc/design/v2/runner/README.md | 4 +++ doc/design/v2/runner/architecture.md | 35 ++++++++++++------- .../v2/runner/component_relationships.mmd | 9 +++-- .../v2/runner/component_relationships.svg | 2 +- doc/design/v2/runner/connection_lifecycle.mmd | 8 +++-- doc/design/v2/runner/connection_lifecycle.svg | 2 +- doc/design/v2/runner/lifecycle.md | 15 ++++---- 7 files changed, 50 insertions(+), 25 deletions(-) diff --git a/doc/design/v2/runner/README.md b/doc/design/v2/runner/README.md index 6155d0e..b643356 100644 --- a/doc/design/v2/runner/README.md +++ b/doc/design/v2/runner/README.md @@ -9,6 +9,10 @@ The runner is organized into two source-level namespaces: 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. diff --git a/doc/design/v2/runner/architecture.md b/doc/design/v2/runner/architecture.md index d0b6c8e..d77d0a6 100644 --- a/doc/design/v2/runner/architecture.md +++ b/doc/design/v2/runner/architecture.md @@ -27,6 +27,17 @@ boundary under its release and ownership contract; Arrow C++ types remain intern ## 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 @@ -47,8 +58,9 @@ creation abstract so a TCP/TLS acceptor can be added later. ### WorkerFactory and worker pool -The runner does not decide how workers are constructed. An external owner injects a `WorkerFactory`. The factory is -used by reusable worker-pool resources to obtain a worker callable or worker object for an accepted descriptor. +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 @@ -79,17 +91,14 @@ protocol documents. ## Data and dependency flow ```text -external WorkerFactory - | -SocketAcceptor -> worker pool -> Worker(fd) - | - v - ContextManager.create(fd) - | - v - protocol Context - | - Socket bytes + Arrow C data values +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 diff --git a/doc/design/v2/runner/component_relationships.mmd b/doc/design/v2/runner/component_relationships.mmd index 38496f7..8df1229 100644 --- a/doc/design/v2/runner/component_relationships.mmd +++ b/doc/design/v2/runner/component_relationships.mmd @@ -1,6 +1,11 @@ flowchart LR - External[External owner] -->|injects| Factory[WorkerFactory] - Acceptor[SocketAcceptor\nlistening endpoint] -->|accepted fd| Queue[Reusable worker pool] + 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] diff --git a/doc/design/v2/runner/component_relationships.svg b/doc/design/v2/runner/component_relationships.svg index 6763aba..40bb7af 100644 --- a/doc/design/v2/runner/component_relationships.svg +++ b/doc/design/v2/runner/component_relationships.svg @@ -1 +1 @@ -

injects

accepted fd

creates worker

one fd per invocation

create context

owns connection context

External owner

WorkerFactory

SocketAcceptor
listening endpoint

Reusable worker pool

Worker callable

ContextManager

Protocol Context

Socket abstraction

Unix stream socket
future TCP/TLS binding

Protocol v2 interface

\ No newline at end of file +

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 index 62c8dba..1204597 100644 --- a/doc/design/v2/runner/connection_lifecycle.mmd +++ b/doc/design/v2/runner/connection_lifecycle.mmd @@ -1,5 +1,7 @@ sequenceDiagram - participant Owner as External owner + 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 @@ -7,7 +9,9 @@ sequenceDiagram participant Manager as ContextManager participant Context - Owner->>Acceptor: configure and listen + 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 diff --git a/doc/design/v2/runner/connection_lifecycle.svg b/doc/design/v2/runner/connection_lifecycle.svg index 61cb060..74360f2 100644 --- a/doc/design/v2/runner/connection_lifecycle.svg +++ b/doc/design/v2/runner/connection_lifecycle.svg @@ -1 +1 @@ -ContextContextManagerWorkerWorkerFactoryWorker poolSocketAcceptorExternal ownerContextContextManagerWorkerWorkerFactoryWorker poolSocketAcceptorExternal owneralt[queue,worker, orcontextcreationfailure]configure 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 +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/lifecycle.md b/doc/design/v2/runner/lifecycle.md index 3d94d3b..b4fcfc7 100644 --- a/doc/design/v2/runner/lifecycle.md +++ b/doc/design/v2/runner/lifecycle.md @@ -2,9 +2,11 @@ ## Startup -1. The external owner creates transport configuration, a `WorkerFactory`, a `ContextManager`, and runner limits. -2. The runner creates a `SocketAcceptor` and a reusable worker pool. -3. The acceptor binds and listens on the configured Unix-domain endpoint. +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 @@ -25,7 +27,7 @@ owned connected fd | enqueue v reusable worker-pool resource - | invoke injected WorkerFactory product + | invoke Runner-owned WorkerFactory product v Worker(fd) | ContextManager.create(fd) @@ -65,8 +67,9 @@ Shutdown has three phases: 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. It must not destroy injected -factory or context-manager dependencies until all workers have returned. +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