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;