The otap-df-engine crate is the in-core execution engine for OTAP Dataflow.
It is responsible for running pipeline nodes, wiring bounded channels, routing
runtime messages, and enforcing the engine's drain and shutdown behavior inside
one pipeline runtime.
The engine is not the whole process. In this project, the process-level controller and the in-core engine have distinct responsibilities:
Controller: Resolves configuration, allocates cores, creates topic bindings, spawns per-core pipeline runtimes, and drives pipeline lifecycle from admin or process-level events.Engine: Runs receivers, processors, exporters, channels, timers, completion unwinding, and graceful drain/shutdown inside one pipeline runtime on one core.
This README documents the current OtapPdata-based engine used by OTAP
Dataflow. It focuses on the runtime model that contributors and advanced users
need to understand when working on the engine or reasoning about pipeline
behavior.
DAG: A directed acyclic graph of nodes and connections describing one pipeline.Pipeline: The configured DAG itself.Pipeline runtime: One instantiated copy of a pipeline running on one assigned core. A pipeline configured onncores producesnpipeline runtimes.Receiver / ingress: A node that admits external work into the DAG and producespdatafor downstream nodes.Processor: A node that consumespdata, transforms it, and may emit zero, one, or many downstreampdatamessages.Exporter / egress: A node that consumespdataand terminates the in-process DAG path.pdata: The data unit flowing on the forward path. In this project,pdatameansOtapPdata.Ack/Nack / completion: The completion state of an in-flightpdatarequest. Completion traffic travels on the return path and is surfaced asAckorNackcontrol messages.Hyper-edge: A runtime wiring unit that groups compatible logical connections onto one bounded underlyingpdatachannel.Topic: A named in-process transport used to connect pipelines without a direct DAG edge. Topic receiver/exporter nodes bridge between a pipeline runtime and the topic runtime.
A list of the design principles followed by this project can be found in
../../docs/design-principles.md. More
specifically, the engine implemented in this crate follows a share-nothing,
thread-per-core approach:
- one single-threaded async runtime per assigned core
- one pipeline runtime per deployed pipeline per assigned core
- no implicit work stealing or cross-core scheduling in the hot data path
- bounded channels and explicit backpressure instead of unbounded buffering
?Send-friendly traits so local,!Sendimplementations can avoid synchronization when possible- listener setup that can rely on
SO_REUSEPORTwhere the platform supports it
This split matters operationally. The controller decides where and when pipelines run; the engine decides how a single pipeline runtime behaves once it is running on a core.
One configured pipeline can therefore produce several independent pipeline runtimes. Each runtime owns its own nodes, bounded channels, timers, runtime control state, and completion unwinding state. Immutable resources can be shared, but hot mutable data-path state stays local to the runtime.
The controller can also assign multiple pipeline runtimes to the same core in some circumstances. This can happen intentionally when different pipeline configurations are consolidated onto the same core set, and it can also happen transiently during rollout-style lifecycle events such as live reconfiguration when overlapping old and new runtimes are expected to coexist for a period.
Topics are the engine-supported way to connect pipelines without direct DAG
edges. Topic declaration, validation, mode inference, capability checks, and
cycle checks happen during controller startup. Each pipeline runtime then
receives a TopicSet with pipeline-scoped bindings, and topic exporter/receiver
nodes bridge between the DAG and the topic runtime. See
../../docs/topic-architecture.md for the
full topic-specific architecture.
Operationally, topics play three main roles in the system:
- they decouple pipelines that together form one larger logical flow, which makes it possible to evolve or reconfigure one part of that flow without necessarily interrupting another part. A common pattern is to separate an ingress-oriented pipeline from a downstream processing-and-export pipeline so the latter can change independently while ingress listeners and network connections stay stable
- they provide the cross-pipeline delivery pattern required by the topology, whether that means balanced delivery, broadcast delivery, or a mixed topic serving both kinds of consumers. The example configurations use this for scenarios such as multitenant isolation, best-effort tap pipelines, and mixed-criticality processing paths
- they can carry tracked publish outcomes across the topic hop, so Ack/Nack propagation can be bridged across pipelines when the topic binding and node configuration enable it
OtapPdata is the pdata type used by the engine in this project.
At a high level, OtapPdata can carry:
- OTLP bytes batches, typically represented as
OtlpProtoBytes - OTAP Arrow batches, typically represented as
OtapArrowRecords
This distinction is important for performance. The engine does not require every ingress path to deserialize OTLP bytes into the OTAP in-memory representation immediately. Some receivers, processors, and exporters can route, forward, inspect, or translate requests while still operating on OTLP bytes. Conversion to OTAP records happens on demand when a component actually needs the decoded representation.
That lets the runtime preserve a zero- or low-deserialization path where possible while still supporting components that need OTAP Arrow batches for batching, transformation, encoding, or export.
The engine uses bounded channels only.
At runtime, compatible DAG connections are grouped into hyper-edges, and each
hyper-edge is wired onto one bounded underlying pdata channel. With ordinary
multi-destination one_of wiring, multiple consumers on the same hyper-edge
compete on that shared channel. Generic hyper-edge broadcast fan-out is not
implemented at the channel-wiring level, but explicit broadcast-style fan-out
inside a pipeline is supported through the dedicated fanout processor.
Each pipeline runtime uses three channel families:
pdatachannels These carry onlypdata. Receivers and processors produce onto them; processors and exporters consume them.- Node-control channels
There is one bounded node-control channel per node. Receivers consume this
channel in competition with their external ingress sources and are expected
to prioritize control. Processors and exporters consume node control together
with
pdatathrough role-specific inboxes. - Pipeline runtime channels
Each pipeline runtime owns two bounded shared MPSC channels:
- a runtime-control channel for timers, delayed-data requests,
receiver-drain notifications, and shutdown requests, consumed by
RuntimeCtrlMsgManager - a pipeline-completion channel for
DeliverAck/DeliverNack, consumed byPipelineCompletionMsgDispatcher
- a runtime-control channel for timers, delayed-data requests,
receiver-drain notifications, and shutdown requests, consumed by
The split between the runtime-control channel and the pipeline-completion channel is there to protect liveness.
These two message families have different jobs:
- runtime-control traffic keeps the runtime itself progressing: timers, delayed-data wakeups, receiver drain notifications, and shutdown orchestration
- completion traffic unwinds the outcome of already admitted work:
AckandNackdelivery back to the closest interested upstream node
If both flows share one bounded queue, a burst in one category can crowd out
the other. That is especially dangerous because nodes publish onto these paths
from inside their own async loops. Once that shared queue is full, producers
can block in send().await, which can amplify backpressure and, in some
topologies, contribute to circular wait.
A realistic example looks like this:
# An OTLP receiver admits requests with wait_for_result enabled.
# A retry/fanout processor forwards pdata downstream and temporarily closes
# admission while it waits for outstanding completions to reduce its inflight
# count.
# An exporter hits a temporary outage and emits a burst of DeliverNack.
# The processor also needs to enqueue StartTimer / DelayData to retry work.
#
# If DeliverNack, StartTimer, DelayData, and Shutdown all share one bounded
# channel, the DeliverNack burst can fill it first.
# Then:
# - the exporter can block trying to publish more completion traffic
# - the processor can block trying to publish retry/timer work
# - receiver drain / shutdown work can be delayed behind completion churn
# - the processor may stay closed to pdata because the control work needed to
# reopen or retry is stuck behind the same queue
# - backpressure propagates upstream and admitted requests stop making forward
# progress
#
# With separate channels:
# - runtime-control traffic stays live even under heavy Ack/Nack churn
# - Ack/Nack unwinding stays live even while timers or delayed-data requests
# are busy
# - one path being saturated no longer prevents the other from draining
ProcessorInbox and ExporterInbox both prefer control over
pdata, but neither gives control absolute priority. After a bounded burst of
control messages, the channel forces one pdata receive attempt when node-level
admission allows it, so control storms do not starve the forward data path.
recv_when(...) is the receive-side primitive that enforces that admission
control. When the guard is false, pdata stays queued while control messages
continue to be delivered, which lets a node reduce in-flight state and surface
backpressure upstream.
Processors and exporters use that mechanism differently. Processors do not
usually call recv_when(...) themselves because the engine owns their receive
loop. Instead, a processor exposes accept_pdata(), and the engine feeds that
policy into ProcessorInbox::recv_when(...) on the processor's
behalf. Exporters own their run loops directly, so they call
ExporterInbox::recv() or ExporterInbox::recv_when(...)
themselves. The two mechanisms therefore serve the same admission-control goal
at different layers: accept_pdata() is the processor-side readiness hook,
while recv_when(...) is the channel primitive used by self-driven exporter
loops.
The shutdown contract is also role-specific. ProcessorInbox
continues to honor closed admission during shutdown, so a processor that keeps
accept_pdata() == false until the deadline may still strand buffered pdata.
ExporterInbox is different: once shutdown is latched, it still
force-drains already buffered channel data even if the exporter has temporarily
closed normal admission.
The current message families are:
- Node control messages:
Ack,Nack,Config,TimerTick,CollectTelemetry,Wakeup,DelayedData,DrainIngress,Shutdown - Runtime control messages:
StartTimer,CancelTimer,StartTelemetryTimer,CancelTelemetryTimer,DelayData,ReceiverDrained,Shutdown - Pipeline completion messages:
DeliverAck,DeliverNack
The runtime has five concurrent message paths that together explain most engine behavior:
-
Forward
pdataflow Receivers admit external work and emitOtapPdataonpdatachannels. Processors and exporters then consume thatpdatathroughProcessorInboxandExporterInbox. -
Node-control delivery Receivers consume node control in competition with external ingress. By contrast, processors and exporters consume node control and
pdatathrough their role-specific inboxes, which give control preferred but bounded-fair treatment. -
Runtime-control flow Nodes send timer requests, delayed-data requests,
ReceiverDrained, and runtimeShutdownrequests to the runtime-control channel. That channel is consumed byRuntimeCtrlMsgManager, which handles orchestration and turns due work back into node-control messages. -
Ack/Nack completion flow Nodes that complete or reject work send
DeliverAckandDeliverNackon the pipeline-completion channel.PipelineCompletionMsgDispatcherconsumes that channel, unwinds the caller subscription stack stored inpdata, and deliversAck/Nacknode-control messages to the closest interested upstream node. -
Drain and shutdown flow Graceful shutdown enters through
RuntimeControlMsg::Shutdown. The runtime first sendsDrainIngressto receivers, waits forReceiverDrained, then sends downstreamShutdown. Drain propagates as receivers stop admitting new work, drop their forward senders, and downstreampdatachannels gradually empty and close. The later sections on Ack/Nack delivery and graceful shutdown cover this in more detail.
Processors can schedule local wakeups through the processor effect handler:
set_wakeup(slot, when)schedules or replaces the wakeup forslotand returns whether that slot was inserted or replaced, along with the accepted wakeup revisioncancel_wakeup(slot)removes the wakeup forslotif one is live
This API is intentionally processor-local:
WakeupSlotis scoped to one processor instance, not globally across the pipeline- a processor can define its own slot constants such as
WakeupSlot(0) - a processor can also encode compact structured local identifiers directly in
the widened
WakeupSlot(pub u128)payload when that is more natural - the engine does not interpret slot meaning; it only routes the slot back to the originating processor
Wakeups are delivered through ProcessorInbox as
NodeControlMsg::Wakeup { slot, when, revision }. They therefore participate
in the same receive loop and the same bounded fairness policy as other control
traffic.
The current runtime properties and guarantees are:
- Keyed replacement: there is at most one live wakeup per slot; scheduling the same slot again replaces the previous due time
- Revisioned delivery: every accepted schedule gets a scheduler-assigned revision; re-scheduling a live slot gives it a new revision so processors can ignore stale wakeups for reused slots
- Cancellation: canceling a live slot prevents that wakeup from being delivered later
- Bounded live state: scheduler state is bounded by the number of live wakeup slots accepted for the processor
- Deterministic ordering: if two wakeups have the same due time, they are delivered in schedule order
- No payload retention: wakeups carry only
(slot, when, revision)and do not retain deferredpdata - Shutdown rejection and drop: once processor shutdown is latched, new wakeups are rejected and pending wakeups are dropped immediately
- No flush-on-shutdown guarantee: pending wakeups are not drained or forced through during shutdown
Wakeups are best-effort runtime scheduling signals. They are not durable work items and are not part of the runtime-control delayed-data mechanism that is still used by retry-oriented flows outside this API.
The runtime is organized around a small set of guarantees:
- Isolated control paths: runtime orchestration and Ack/Nack unwinding use separate bounded runtime channels and dedicated runtime components.
- Bounded progress under load: control remains responsive while
pdata, timer expiry, telemetry collection, and delayed-data resumption still make progress under sustained control traffic. - Explicit node-level admission control: processors can temporarily pause
pdatadelivery throughaccept_pdata(), and exporters can apply the same pattern in their run loops withExporterInbox::recv_when(false). The engine uses two entry points because processor receive loops are engine-owned while exporter receive loops are node-owned. During shutdown, exporters still drain already buffered channel data, while processors keep their current stricter admission semantics. - Receiver-first graceful drain: graceful shutdown drains ingress first,
then shuts down downstream consumers after receivers report
ReceiverDrained. - Producer-visible completion semantics for
wait_for_result: receivers that expose downstream completion to telemetry producers can only provide a useful contract if those producers act on the result. Upstream senders are expected to treat temporary failures as retryable and resend with an appropriate backoff policy, while permanent refusals should not be retried. The engine reports completion; it does not persist or replay those upstream requests on the producer's behalf. - Bounded memory and surfaced backpressure: all communication paths remain bounded, so pressure appears in the relevant channel family rather than being hidden behind unbounded queues.
Effect handlers are the node-facing abstraction for performing engine-managed side effects. They let receivers, processors, and exporters interact with the runtime without owning the runtime internals directly.
In practice, effect handlers are how nodes:
- send
pdatato downstream ports - subscribe to Ack/Nack interests on the forward path
- emit Ack/Nack outcomes onto the pipeline-completion channel
- schedule or cancel timers on the runtime-control channel
- schedule or cancel processor-local wakeups
- return delayed data
- report
ReceiverDrained - create listeners and sockets with engine-defined socket options
The codebase exposes two families of effect handlers:
-
Local effect handlers (
!Send) These are the default and preferred path. They align with the engine's single-threaded runtime model and avoid synchronization overhead where the component and its futures do not need to beSend. -
Shared effect handlers (
Send) These exist for integrations that requireSend-bound components or futures, such as Tonic-based receivers and similar libraries.
The important design point is that the effect handler is not just a message sender. It is the node's capability object for interacting with the engine's forward path, completion path, runtime-control path, and listener setup.
The engine includes a built-in mechanism for returning the success or failure of data requests through the pipeline. Components can opt in to Ack (positive acknowledgement) or Nack (negative acknowledgement) control messages.
Ack/Nack unwinding is intentionally separated from timer and shutdown
orchestration. Nodes publish return-path events on the pipeline-completion channel,
and PipelineCompletionMsgDispatcher unwinds the caller subscription stack and
forwards Ack / Nack node-control messages to the closest interested
upstream node.
The data layer above the engine is responsible for the physical representation
of request context. The engine effect handler and OtapPdata cooperate through
the following calling convention:
- A producer subscribes to a set of
Interestsbefore sendingpdata. - Those subscriptions are recorded as frames in the
pdatacontext stack. - A downstream consumer later calls
notify_ackornotify_nack. - The dispatcher unwinds the context stack until it finds the closest frame interested in that outcome.
Interests is an 8-bit flag set. The most important flags are:
ACKSNACKSRETURN_DATA
Each subscription frame carries:
- the interested node id
RouteDatafor the forward path- user-defined
CallData
When the return path is formed, the engine creates UnwindData, which combines
the route metadata with the return-path timestamp.
By default, the payload is dropped on the return path to avoid holding request
memory longer than necessary. If RETURN_DATA is set, the caller asks for the
payload to be preserved on the return path. Even then, callers should still
reason in terms of bounded graceful behavior rather than guaranteed payload
recovery under every failure mode.
When a producer wants to be informed of the outcome via Ack/Nack, it uses a call sequence like:
use otap_df_engine::{Interests, ProducerEffectHandlerExtension};
async fn process(
msg: Message<OtapPdata>,
effect: &mut local::processor::EffectHandler<OtapPdata>,
) -> Result<(), EngineError> {
match msg {
Message::PData(mut pdata) => {
let call_state = SomeCallData::new(...);
effect.subscribe_to(
Interests::ACKS | Interests::NACKS,
call_state.into(),
&mut pdata,
);
effect.send_message(pdata).await
}
Message::Control(_) => Ok(()),
}
}When a consumer finishes processing pdata and wants to return an outcome, it
uses the consumer-side effect-handler extension:
use otap_df_engine::ConsumerEffectHandlerExtension;
async fn export(
msg: Message<OtapPdata>,
effect: &local::exporter::EffectHandler<OtapPdata>,
) -> Result<(), EngineError> {
match msg {
Message::PData(pdata) => {
effect.notify_ack(AckMsg::new(pdata)).await
}
Message::Control(_) => Ok(()),
}
}notify_nack works the same way, but returns a NackMsg. The engine does not
invent the next unwind frame on its own; the data-layer-aware extension methods
build the next Ack/Nack message using the context stored in pdata.
A processor that subscribes to Ack/Nack will typically both consume and produce Ack/Nack:
async fn process(
msg: Message<OtapPdata>,
effect: &mut local::processor::EffectHandler<OtapPdata>,
) -> Result<(), EngineError> {
match msg {
Message::PData(_) => Ok(()),
Message::Control(ctrl) => match ctrl {
NodeControlMsg::Ack(ack) => {
let my_state: SomeCallData = ack.unwind.route.calldata.clone().try_into()?;
// update local state
effect.notify_ack(ack).await
}
NodeControlMsg::Nack(mut nack) => {
let my_state: SomeCallData = nack.unwind.route.calldata.clone().try_into()?;
// update local state
nack.reason = format!("more info: {}", nack.reason);
effect.notify_nack(nack).await
}
_ => Ok(()),
},
}
}Without a matching interest, a node is skipped on the return path. For
topic-specific Ack/Nack bridging across topic hops, see
../../docs/topic-architecture.md.
The engine supports graceful shutdown of pipeline runtimes and their nodes.
Shutdown is initiated by sending RuntimeControlMsg::Shutdown to the
runtime-control channel; it is not implemented by broadcasting Shutdown
directly to every node at once.
In practice, that shutdown request can enter the runtime through:
- direct admin/API shutdown requests handled by the controller
- broader process shutdown flows triggered by OS signals and translated into pipeline shutdown requests
When graceful shutdown starts, the runtime control manager:
- Enters ingress-draining mode.
- Cancels recurring timers.
- Flushes queued delayed data back to the originating nodes as
NodeControlMsg::DelayedData. - Sends
NodeControlMsg::DrainIngressto every receiver.
Each receiver is then responsible for stopping admission of new external work while keeping receiver-local drain state alive long enough to finish local cleanup. For example, an RPC receiver can keep its wait-for-result registry, transport shutdown, and telemetry finalization alive until it has no more admitted requests to resolve.
Once ingress is closed and receiver-local drain work is complete, the receiver
reports RuntimeControlMsg::ReceiverDrained.
After all receivers have reported ReceiverDrained, the control manager sends
NodeControlMsg::Shutdown to processors and exporters. ProcessorInbox
continues delivering control messages while only draining pdata when the
processor reopens admission. ExporterInbox also continues delivering
control messages, but it force-drains already buffered input-channel pdata
even if the exporter has temporarily closed normal admission. In both cases,
the channel returns final shutdown once inputs are drained or the shutdown
deadline is reached.
As receivers exit and drop their pdata senders, downstream channels drain and
close progressively toward exporters. Once a downstream input is fully drained
or closed, the corresponding consumer receives Shutdown and exits its run
loop.
If the shutdown deadline expires, receivers may force-resolve remaining receiver-local waiters and the runtime control manager forces the remaining nodes to exit.
When no fatal error occurs and nodes honor the shutdown deadline, this sequence allows the runtime to stop gracefully while preserving bounded memory and backpressure semantics.
All node types, as well as the pipeline engine itself, are designed for isolated testing. A receiver, processor, exporter, or runtime component can be tested without constructing a full deployed pipeline.
The engine provides a substantial testing surface, including:
- component wrappers and dedicated test runtimes for receivers, processors, and exporters
- helper channels and control-message harnesses
- validation contexts for asserting forward-path and control-path behavior
- single-threaded async test utilities aligned with the engine's local runtime model
This keeps runtime behavior testable at the level where bugs usually occur: channel interaction, Ack/Nack unwinding, runtime-control handling, and component-local state transitions.
For a contributor-facing overview of when to use unit tests, node harnesses, small pipeline liveness tests, validation scenarios, or DST, see the Testing Guide.
In addition to ordinary unit and integration tests, the engine includes a deterministic simulation testing (DST) layer for concurrency-sensitive runtime behavior.
The DST approach combines:
- a deterministic clock (
SimClock) so deadlines, timer expiry, and delayed data wakeups can be advanced explicitly - seeded interleavings so the same scenario can be replayed via
DST_SEEDor expanded into a larger sweep viaDST_SEEDS - real engine actors and channels rather than parallel mock semantics
This is important for the control plane because many of the interesting failure
classes are about ordering and bounded progress, not only about local business
logic. The DST harness therefore runs real ProcessorInbox,
ExporterInbox, RuntimeCtrlMsgManager, and
PipelineCompletionMsgDispatcher logic inside the engine's single-threaded
runtime model.
The current DST scenarios cover five main families:
- role-specific channel fairness and drain behavior: bounded-fair control vs
pdata, processor shutdown draining after admission reopens, exporter shutdown draining of bufferedpdata, and deadline-forced shutdown when a processor keeps admission closed - runtime-control vs pipeline-completion progress under load: timer and delayed
data delivery, Ack/Nack unwinding,
RETURN_DATA, and receiver-first shutdown ordering under mixed runtime noise - heavy ingress / backpressure / interblock scenarios: sustained receiver
traffic, bounded
pdatachannels, processor admission gating and reopen, mixed Ack/Nack completions, and clean drain propagation - explicit known-limitation coverage for processor closed admission through the
shutdown deadline: the DST suite documents the current case where buffered
pdataremains stranded if a processor never reopens admission before the deadline - receiver
wait_for_resultbehavior during drain and shutdown: the OTLP receiver DST suite exercises Ack, temporary Nack, permanent Nack, and shutdown/unavailable completion at the deadline
The seed controls are:
DST_SEED=<u64>to replay one specific deterministic runDST_SEEDS=<n>to appendngenerated seeds after the fixed regression seeds
The current DST coverage is intentionally scoped. It does not yet model topic-based routing, cross-pipeline Ack/Nack across topic hops, or fatal process failure. Those cases still rely on ordinary tests, design review, and future targeted coverage.
Receivers, processors, and exporters are registered through a compile-time
plugin system built on linkme. Each node type
publishes a factory under its URN, and the engine's factory tables use those
distributed slices to instantiate nodes at runtime.
This gives the engine a stable runtime lookup model without requiring dynamic loading. The current system is compile-time only: built-in plugins are registered into the binary, and the controller/engine use those factory tables when building pipeline runtimes.
The engine emits telemetry in terms of process, core, pipeline runtime, and node identity. These attributes let operators and contributors correlate engine metrics and events with the runtime structure described above.
The policies.telemetry.runtime_metrics level now gates three related
observability surfaces:
- channel endpoint transport metrics
- per-node produced/consumed outcome metrics
- shared pipeline-scoped control-plane metrics
For the shared control plane, the engine exports two pipeline metric families:
pipeline.runtime_controlThis family is owned byRuntimeCtrlMsgManagerand helps explain graceful drain state, pending control-send backpressure, active timers, delayed-data backlog, and whether shutdown finished naturally or was forced by deadline.pipeline.completionThis family is owned byPipelineCompletionMsgDispatcherand helps explain whether completion traffic is arriving, being delivered upstream, being dropped because no frame was interested, or building up as a buffered backlog.
The runtime_metrics levels apply as follows:
none: disables channel endpoint, per-node outcome, and shared control-plane metric exportbasic: exports channel transport metrics plus the shared control-plane state gauges such aspipeline.runtime_control.drain.active,pipeline.runtime_control.drain.pending_receivers, andpipeline.completion.pending_sends.bufferednormal: adds message and phase counters such aspipeline.runtime_control.shutdown.received,pipeline.runtime_control.downstream_shutdown.sent,pipeline.completion.deliver_ack.received, andpipeline.completion.ack.sentdetailed: adds the expensive summaries, including drain durations onpipeline.runtime_controland unwind-depth distribution onpipeline.completion
Operationally, these two pipeline-scoped metric families help answer different questions:
- if receivers appear stuck during shutdown,
pipeline.runtime_controlshows whether drain is active, how many receivers are still pending, whether timer or delayed-data work is still queued, and whether the shutdown deadline was ultimately forced - if upstream callers are not seeing final Ack/Nack outcomes,
pipeline.completionshows whether completions are reaching the dispatcher, whether they are being delivered to an interested frame, or whether the unwind ran out of interested subscribers
| Scope | Attribute | Type | Description |
|---|---|---|---|
| Resource | process_instance_id | string | Unique process instance identifier (base32-encoded UUID v7). |
| Resource | host_id | string | Host identifier (e.g. hostname). |
| Resource | container_id | string | Container identifier (e.g. Docker/containerd container ID). |
| Engine | core_id | integer | Core identifier. |
| Engine | numa_node_id | integer | NUMA node identifier. |
| Pipeline | pipeline_id | string | Pipeline identifier. |
| Node | node_id | string | Node unique identifier (in scope of the pipeline). |
| Node | node_type | string | Node type (e.g. "receiver", "processor", "exporter"). |
The engine also emits a small set of pipeline-level lifecycle events around graceful drain:
ShutdownRequested: shutdown was accepted for the pipeline runtimeIngressDrainStarted:DrainIngresswas sent to receiversReceiversDrained: all pending receivers reportedReceiverDrainedDownstreamShutdownStarted: downstreamShutdownwas sent to non-receiversDrainDeadlineReached: the shutdown deadline expired before natural drain completion
These events are intentionally low-volume. They are useful when a pipeline appears to stall during shutdown because they let operators distinguish between "ingress was never told to stop", "receivers have not drained yet", "downstream shutdown has already started", and "the runtime had to force completion at the deadline".
The engine is designed to address a small number of important runtime failure classes without pretending that every failure becomes harmless:
-
Bounded memory and explicit backpressure All forward-path and control-path communication remains bounded. Pressure is surfaced on the relevant channel family instead of being hidden behind unbounded buffering.
-
Separation of orchestration and result traffic Runtime-control work and Ack/Nack unwinding travel on separate runtime paths, so completion traffic does not share the same queue as timers, delayed data, and receiver-drain coordination.
-
Bounded-fair progress The runtime avoids absolute control priority. Control remains responsive, but
pdata, due timers, telemetry collection, and delayed-data resumption still make progress under sustained control load. -
Receiver-first graceful drain Shutdown begins by stopping ingress rather than abruptly terminating the whole DAG. This gives receivers a place to finish admitted work, unwind late Ack/Nack outcomes, and then let downstream drain naturally.
-
Explicit drain-time behavior During graceful shutdown, recurring timers are canceled, delayed data is returned explicitly, and downstream shutdown is gated by
ReceiverDrained. -
Invalid cross-pipeline topic topologies are rejected early Topic declaration and cycle validation happen before runtimes start, so the engine does not need to discover topic feedback loops at runtime.
These protections still live within explicit limits:
- graceful shutdown is bounded by a deadline
- fatal process failure bypasses graceful behavior
- correctness depends on nodes honoring the runtime contracts and continuing to poll while draining
Those limits are intentional. The engine favors explicit contracts, bounded resource use, and predictable runtime behavior over hidden retries or unbounded buffering.