Skip to main content

laminar_db/process_function/
mod.rs

1//! Keyed process functions with engine-owned state and timers.
2//!
3//! The engine owns every value and timer. A handler receives immutable activation snapshots and
4//! returns proposed changes; its private memory is never authoritative. Native handlers execute
5//! on the compute thread and must be trusted, bounded, and nonblocking. The optional remote
6//! transport is restricted to loopback workers on each database host. At-least-once delivery admits
7//! trusted native Rust and remote Rust with one replayable source that reproduces one physical
8//! channel in fixed batches. Each batch defines a reproducible event-time cut; independent-channel
9//! merging remains unadmitted. Cluster execution additionally requires splittable placement and
10//! the same immutable binding in every owner's sealed startup catalog. Supervised replay-safe
11//! Python also requires an unprivileged Linux worker in a read-only root image. Exactly-once
12//! process delivery is unsupported.
13
14use std::sync::Arc;
15
16use arrow::array::RecordBatch;
17use arrow_schema::SchemaRef;
18
19use crate::error::DbError;
20
21mod catalog;
22mod descriptor;
23mod operator;
24mod registration;
25#[cfg(feature = "process-remote")]
26pub mod remote;
27mod schema;
28mod source_order;
29
30#[cfg(all(test, feature = "cluster"))]
31pub(crate) mod cluster_recovery_tests;
32
33#[cfg(feature = "benchmark-internals")]
34pub mod benchmark;
35
36pub(crate) use operator::ProcessFunctionOperator;
37pub(crate) use schema::canonical_fields;
38
39// RECOVERY: v2 binds the complete descriptor; v1 carried only a schema digest.
40pub(crate) const STATE_CODEC_VERSION: u32 = 2;
41
42/// Execution boundary bound by the function manifest.
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum ProcessRuntime {
45    /// Trusted Rust code running on the compute thread.
46    NativeRust,
47    /// Rust code in a separate worker process over the local reference transport.
48    RemoteRust,
49    /// Python worker over the local loopback transport.
50    RemotePython,
51}
52
53/// Reviewed handler contract, independent of the pipeline's delivery guarantee.
54#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ProcessDeterminism {
57    /// No replay equivalence declaration. Existing native and Rust worker profiles are unchanged.
58    #[default]
59    Undeclared,
60    /// Trusted code derives results only from activations and their managed state. External
61    /// reads/writes, wall clock, randomness and worker-local business state are prohibited.
62    /// This is a reviewed code contract, not a sandbox or code-attestation claim.
63    ReplaySafe,
64}
65
66/// Immutable, versioned binding for one process function. The initial state codec is an
67/// optional signed 64-bit value; a present null is distinct from absent state.
68#[derive(Clone, Debug)]
69pub struct ProcessFunctionDescriptor {
70    /// Descriptor format version. Currently only 1 is admitted.
71    pub version: u32,
72    /// Runtime that executes the immutable implementation.
73    pub runtime: ProcessRuntime,
74    /// Stable function identity, independent of the output stream name.
75    pub function_id: String,
76    /// Stable pipeline state identity. Two pipelines must use different identities.
77    pub pipeline_state_id: String,
78    /// Hex SHA-256 of the trusted native build or other immutable implementation identity.
79    pub implementation_digest: String,
80    /// Effects contract bound into the canonical manifest and checkpoint identity.
81    pub determinism: ProcessDeterminism,
82    /// Optional Python runtime and import-tree identity checked by the local supervisor.
83    /// File hashes detect deployment drift; they do not enforce lifetime immutability.
84    pub python_environment: Option<PythonEnvironmentBinding>,
85    /// Exact input schema admitted from one direct source. V1 retains accepted per-key arrival
86    /// order; timestamps do not sort input. Replayable source positions alone do not define the
87    /// merge order of independent input partitions.
88    pub input_schema: SchemaRef,
89    /// Exact append-only output schema.
90    pub output_schema: SchemaRef,
91    /// Ordered, nonnullable canonical partition-key columns.
92    pub key_columns: Vec<String>,
93    /// Nonnullable UTC microsecond input timestamp column.
94    pub event_time_column: String,
95    /// UTC microsecond output timestamp column; output may not precede its activation.
96    pub output_event_time_column: String,
97    /// Stable name of the one managed `ValueState<Int64?>` slot.
98    pub value_state_name: String,
99    /// Names of supported event-time timers.
100    pub timer_names: Vec<String>,
101    /// Hard execution and retained-state limits.
102    pub limits: ProcessFunctionLimits,
103}
104
105/// Versioned file-tree binding for a supervised Python deployment. Absolute installation
106/// paths are excluded so an identical package can move without changing checkpoint identity.
107#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
108#[serde(deny_unknown_fields)]
109pub struct PythonEnvironmentBinding {
110    /// Tree fingerprint format. Currently only 1 is admitted.
111    pub version: u32,
112    /// Selected interpreter's UTF-8 path relative to the runtime root, using `/` separators.
113    pub executable: String,
114    /// Exact top-level handler module and callable, written as `module:function`.
115    pub handler: String,
116    /// Lowercase hex SHA-256 of the complete runtime tree, including bytecode and native files.
117    pub runtime_sha256: String,
118    /// Ordered tree digests: handler directory first, then the configured import roots.
119    pub import_roots_sha256: Vec<String>,
120}
121
122/// Per-function bounds, enforced before accepting a handler response.
123#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
124#[serde(deny_unknown_fields)]
125pub struct ProcessFunctionLimits {
126    /// Maximum distinct keys with state or timers.
127    pub max_keys: usize,
128    /// Maximum retained timers.
129    pub max_timers: usize,
130    /// Maximum retained state bytes, including key and timer index estimates.
131    pub max_state_bytes: usize,
132    /// Maximum key-distinct activations in one handler call.
133    pub max_batch_rows: usize,
134    /// Maximum input rows accepted in one graph step.
135    pub max_input_rows: usize,
136    /// Maximum Arrow input bytes accepted in one graph step.
137    pub max_input_bytes: usize,
138    /// Maximum output rows in one graph step.
139    pub max_output_rows: usize,
140    /// Maximum retained Arrow output bytes in one graph step.
141    pub max_output_bytes: usize,
142    /// Maximum timer callbacks in one graph step.
143    pub max_timer_callbacks_per_step: usize,
144}
145
146impl Default for ProcessFunctionLimits {
147    fn default() -> Self {
148        Self {
149            max_keys: 100_000,
150            max_timers: 100_000,
151            max_state_bytes: 64 * 1024 * 1024,
152            max_batch_rows: 256,
153            max_input_rows: 4096,
154            max_input_bytes: 16 * 1024 * 1024,
155            max_output_rows: 4096,
156            max_output_bytes: 16 * 1024 * 1024,
157            max_timer_callbacks_per_step: 256,
158        }
159    }
160}
161
162/// Immutable view of a declared value before one activation's transition.
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
164pub enum ValueState {
165    /// The key has never set this state, or has cleared it.
166    #[default]
167    Absent,
168    /// The state exists and contains SQL NULL.
169    Null,
170    /// The state exists and contains a signed 64-bit value.
171    Value(i64),
172}
173
174/// Explicit state transition; an output without a mutation leaves state unchanged.
175#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
176pub enum ValueMutation {
177    /// Retain the pre-transition value.
178    #[default]
179    Unchanged,
180    /// Set the state to a non-null value.
181    Set(i64),
182    /// Set a present SQL NULL.
183    SetNull,
184    /// Make the state absent.
185    Clear,
186}
187
188/// An input row or a named event-time timer callback.
189#[derive(Clone, Debug)]
190pub enum ProcessCallback {
191    /// One input row, with the descriptor's input schema.
192    Input(RecordBatch),
193    /// A timer callback with no fabricated input row.
194    Timer {
195        /// Declared timer name.
196        name: String,
197    },
198}
199
200/// One engine-assigned activation. Local `id` values follow the checkpointed operator sequence.
201/// Private cluster execution encodes a checkpointed vnode sequence and its fixed vnode index;
202/// replay identity requires the same callback order within that vnode. The key is the canonical
203/// partition ABI encoding and must never be rehashed by the handler.
204/// Preserving these IDs also requires the same watermark cuts relative to input and timer
205/// callbacks. V1 does not certify deterministic replay of an independent-channel merge.
206#[derive(Clone, Debug)]
207pub struct ProcessActivation {
208    /// Logical callback identity within the pipeline and operator, subject to its replay order.
209    pub id: u64,
210    /// Host-assigned canonical key bytes.
211    pub key: Arc<[u8]>,
212    /// The v1 UTF-8 key value, retained for callbacks after input has passed.
213    pub key_text: String,
214    /// Per-activation UTC event time in microseconds.
215    pub event_time_us: i64,
216    /// Callback kind and optional data row.
217    pub callback: ProcessCallback,
218    /// Consistent state before this activation.
219    pub state: ValueState,
220}
221
222/// Named event-time timer operation, applied with its activation's state transition.
223#[derive(Clone, Debug, PartialEq, Eq)]
224pub enum TimerOperation {
225    /// Register or replace the named timer. The timestamp must exceed the current watermark.
226    Set {
227        /// Declared timer name.
228        name: String,
229        /// UTC firing timestamp in microseconds.
230        at_us: i64,
231    },
232    /// Cancel a named timer; cancelling an absent timer is idempotent.
233    Cancel {
234        /// Declared timer name.
235        name: String,
236    },
237}
238
239/// Proposed result for one activation. The engine validates the complete handler response before
240/// applying any result in that invocation batch.
241#[derive(Clone, Debug)]
242pub struct ProcessActivationResult {
243    /// Must name exactly one activation in the request.
244    pub activation_id: u64,
245    /// Zero or more batches with the descriptor's output schema.
246    pub output: Vec<RecordBatch>,
247    /// Explicit managed-state mutation.
248    pub value: ValueMutation,
249    /// Ordered timer operations for this key.
250    pub timers: Vec<TimerOperation>,
251}
252
253/// Trusted, synchronous native handler. A call contains at most one activation for each key.
254/// The handler may vectorize over distinct keys but must not make one key's result depend on
255/// unrelated batch members or their order. Return one result per activation, including those
256/// producing zero rows. Errors discard the entire unaccepted call.
257pub trait NativeProcessFunction: Send + Sync + 'static {
258    /// Compute proposed outputs and mutations without external side effects.
259    ///
260    /// # Errors
261    /// Return an error for an invalid input or a failed computation. The engine keeps the
262    /// invocation unaccepted and replays from its last committed checkpoint when necessary.
263    fn invoke(
264        &self,
265        activations: &[ProcessActivation],
266    ) -> Result<Vec<ProcessActivationResult>, DbError>;
267}
268
269#[derive(Clone)]
270pub(crate) struct ProcessFunctionRegistration {
271    pub(crate) output_name: String,
272    pub(crate) source_name: String,
273    pub(crate) descriptor: ProcessFunctionDescriptor,
274    pub(crate) handler: ProcessHandler,
275}
276
277#[derive(Clone)]
278pub(crate) enum ProcessHandler {
279    Native(Arc<dyn NativeProcessFunction>),
280    #[cfg(feature = "process-remote")]
281    Remote(Arc<remote::RemoteProcessClient>),
282}
283
284impl ProcessHandler {
285    pub(crate) fn supports_replay(&self, descriptor: &ProcessFunctionDescriptor) -> bool {
286        if descriptor.runtime != ProcessRuntime::RemotePython {
287            return true;
288        }
289        if descriptor.determinism != ProcessDeterminism::ReplaySafe
290            || descriptor.python_environment.is_none()
291        {
292            return false;
293        }
294        match self {
295            #[cfg(feature = "process-remote")]
296            Self::Remote(client) => client.has_python_replay_binding(),
297            Self::Native(_) => false,
298        }
299    }
300}
301
302/// Registered function and its input/output stream binding.
303#[derive(Clone, Debug)]
304pub struct ProcessFunctionInfo {
305    /// Output stream name.
306    pub output_name: String,
307    /// Input source name.
308    pub source_name: String,
309    /// Validated immutable descriptor.
310    pub descriptor: ProcessFunctionDescriptor,
311}
312
313#[cfg(test)]
314pub(crate) mod tests;