laminar_db/process_function/remote/
worker.rs1use std::collections::BTreeSet;
2use std::num::NonZeroU32;
3use std::sync::Arc;
4use std::time::Duration;
5
6use futures::Stream;
7use laminar_core::state::PartitionKeyCodecV1;
8use prost::Message;
9use sha2::{Digest, Sha256};
10use tokio::net::TcpListener;
11use tokio::sync::Semaphore;
12use tokio_stream::wrappers::TcpListenerStream;
13use tokio_util::sync::CancellationToken;
14use tonic::{Request, Response, Status};
15
16use super::codec::{decode_activation, encode_batch, encode_mutation, encode_timer};
17use super::wire::{self, host_frame, worker_frame};
18use super::{MAX_FRAME_BYTES, MAX_INVOCATION_WIRE_BYTES, MAX_IN_FLIGHT, PROTOCOL_VERSION};
19use crate::error::DbError;
20use crate::process_function::{
21 NativeProcessFunction, ProcessActivation, ProcessActivationResult, ProcessFunctionDescriptor,
22 ProcessRuntime,
23};
24
25#[derive(Clone)]
28pub struct RustReferenceWorker {
29 descriptor: Arc<ProcessFunctionDescriptor>,
30 digest: [u8; 32],
31 handler: Arc<dyn NativeProcessFunction>,
32 credits: Arc<Semaphore>,
33 #[cfg(all(test, feature = "cluster"))]
34 scope_observer: Option<tokio::sync::mpsc::Sender<wire::Open>>,
35}
36
37impl RustReferenceWorker {
38 pub fn new(
43 descriptor: ProcessFunctionDescriptor,
44 handler: Arc<dyn NativeProcessFunction>,
45 max_in_flight: usize,
46 ) -> Result<Self, DbError> {
47 if descriptor.runtime != ProcessRuntime::RemoteRust
48 || max_in_flight == 0
49 || max_in_flight > MAX_IN_FLIGHT
50 {
51 return Err(DbError::InvalidOperation(
52 "reference worker requires remote Rust and 1..=32 concurrent calls".into(),
53 ));
54 }
55 let manifest = descriptor.to_manifest_json()?;
56 let digest: [u8; 32] = Sha256::digest(manifest).into();
57 Ok(Self {
58 descriptor: Arc::new(descriptor),
59 digest,
60 handler,
61 credits: Arc::new(Semaphore::new(max_in_flight)),
62 #[cfg(all(test, feature = "cluster"))]
63 scope_observer: None,
64 })
65 }
66
67 #[cfg(all(test, feature = "cluster"))]
68 pub(crate) fn with_scope_observer(
69 mut self,
70 observer: tokio::sync::mpsc::Sender<wire::Open>,
71 ) -> Self {
72 self.scope_observer = Some(observer);
73 self
74 }
75
76 pub async fn serve_loopback(
82 self,
83 listener: TcpListener,
84 shutdown: CancellationToken,
85 ) -> Result<(), DbError> {
86 let address = listener
87 .local_addr()
88 .map_err(|error| DbError::Pipeline(format!("worker listener address: {error}")))?;
89 if !address.ip().is_loopback() {
90 return Err(DbError::Unsupported(
91 "process reference worker only accepts a loopback listener".into(),
92 ));
93 }
94 tonic::transport::Server::builder()
95 .add_service(
96 wire::process_worker_server::ProcessWorkerServer::new(self)
97 .max_decoding_message_size(MAX_FRAME_BYTES + 64 * 1024)
98 .max_encoding_message_size(MAX_FRAME_BYTES + 64 * 1024),
99 )
100 .serve_with_incoming_shutdown(TcpListenerStream::new(listener), shutdown.cancelled())
101 .await
102 .map_err(|error| DbError::Pipeline(format!("process worker server: {error}")))
103 }
104
105 async fn read_request(
106 &self,
107 mut inbound: tonic::Streaming<wire::HostFrame>,
108 ) -> Result<(Vec<u8>, Vec<ProcessActivation>), Status> {
109 let first = tokio::time::timeout(Duration::from_secs(5), inbound.message())
110 .await
111 .map_err(|_| Status::deadline_exceeded("process open frame timed out"))??
112 .ok_or_else(|| Status::invalid_argument("missing process open frame"))?;
113 let first_len = first.encoded_len();
114 let Some(host_frame::Kind::Open(open)) = first.kind else {
115 return Err(Status::invalid_argument("process open must be first"));
116 };
117 self.validate_open(&open)?;
118 #[cfg(all(test, feature = "cluster"))]
119 if let Some(observer) = &self.scope_observer {
120 observer
121 .try_send(open.clone())
122 .map_err(|_| Status::internal("test scope observer is full or closed"))?;
123 }
124 let now_ms = std::time::SystemTime::now()
125 .duration_since(std::time::UNIX_EPOCH)
126 .map_err(|_| Status::internal("system clock precedes Unix epoch"))?
127 .as_millis();
128 let remaining_ms = u128::from(open.deadline_unix_ms).saturating_sub(now_ms);
129 let remaining_ms = u64::try_from(remaining_ms)
130 .map_err(|_| Status::invalid_argument("invalid process deadline"))?;
131 let deadline = tokio::time::Instant::now() + Duration::from_millis(remaining_ms);
132 let vnode_count = NonZeroU32::new(open.vnode_count)
133 .ok_or_else(|| Status::invalid_argument("empty process vnode domain"))?;
134 let mut total_wire_bytes = first_len;
135 let mut total_input_bytes = 0usize;
136 let mut activations = Vec::new();
137 let mut ids = BTreeSet::new();
138 let mut keys = BTreeSet::new();
139 loop {
140 let frame = tokio::time::timeout_at(deadline, inbound.message())
141 .await
142 .map_err(|_| Status::deadline_exceeded("process request deadline expired"))??
143 .ok_or_else(|| {
144 Status::invalid_argument("process invocation ended without completion")
145 })?;
146 total_wire_bytes = total_wire_bytes
147 .checked_add(frame.encoded_len())
148 .filter(|total| *total <= MAX_INVOCATION_WIRE_BYTES)
149 .ok_or_else(|| {
150 Status::resource_exhausted("process request wire budget exceeded")
151 })?;
152 match frame.kind {
153 Some(host_frame::Kind::Activation(raw)) => {
154 if activations.len() >= self.descriptor.limits.max_batch_rows {
155 return Err(Status::resource_exhausted(
156 "process batch row limit exceeded",
157 ));
158 }
159 let activation = decode_activation(raw, &self.descriptor)
160 .map_err(|error| Status::invalid_argument(error.to_string()))?;
161 if PartitionKeyCodecV1::vnode_for_encoded(&activation.key, vnode_count)
162 != open.vnode
163 || !ids.insert(activation.id)
164 || !keys.insert(activation.key.to_vec())
165 {
166 return Err(Status::invalid_argument(
167 "process batch contains another vnode, repeated key, or repeated ID",
168 ));
169 }
170 if let crate::process_function::ProcessCallback::Input(batch) =
171 &activation.callback
172 {
173 total_input_bytes = total_input_bytes
174 .checked_add(batch.get_array_memory_size())
175 .filter(|total| *total <= self.descriptor.limits.max_input_bytes)
176 .ok_or_else(|| {
177 Status::resource_exhausted("process input byte limit exceeded")
178 })?;
179 }
180 activations.push(activation);
181 }
182 Some(host_frame::Kind::End(end)) => {
183 if end.count as usize != activations.len() || activations.is_empty() {
184 return Err(Status::invalid_argument("process end count mismatch"));
185 }
186 if tokio::time::timeout_at(deadline, inbound.message())
187 .await
188 .map_err(|_| {
189 Status::deadline_exceeded("process request deadline expired")
190 })??
191 .is_some()
192 {
193 return Err(Status::invalid_argument("process frame after end"));
194 }
195 return Ok((open.attempt_id, activations));
196 }
197 _ => return Err(Status::invalid_argument("invalid process request frame")),
198 }
199 }
200 }
201
202 fn validate_open(&self, open: &wire::Open) -> Result<(), Status> {
203 if open.protocol_version != PROTOCOL_VERSION
204 || open.descriptor_sha256 != self.digest
205 || open.operator_id.is_empty()
206 || open.operator_id.len() > 128
207 || open.vnode_count == 0
208 || open.vnode >= open.vnode_count
209 || open.batch_id.len() != 16
210 || open.attempt_id.len() != 16
211 || open.batch_id == open.attempt_id
212 || open.deadline_unix_ms == 0
213 {
214 return Err(Status::failed_precondition(
215 "process protocol, descriptor, or invocation scope mismatch",
216 ));
217 }
218 let now_ms = std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)
220 .map_err(|_| Status::internal("system clock precedes Unix epoch"))?
221 .as_millis();
222 if now_ms >= u128::from(open.deadline_unix_ms)
223 || u128::from(open.deadline_unix_ms) - now_ms > 30_000
224 {
225 return Err(Status::deadline_exceeded(
226 "process invocation deadline expired or exceeds 30 seconds",
227 ));
228 }
229 Ok(())
230 }
231
232 fn encode_response(
233 &self,
234 attempt_id: Vec<u8>,
235 activation_ids: &[u64],
236 results: Vec<ProcessActivationResult>,
237 ) -> Result<Vec<wire::WorkerFrame>, Status> {
238 if results.len() != activation_ids.len() {
239 return Err(Status::internal(
240 "reference handler returned wrong result count",
241 ));
242 }
243 let mut frames = Vec::new();
244 frames.push(wire::WorkerFrame {
245 kind: Some(worker_frame::Kind::Ack(wire::OpenAck {
246 protocol_version: PROTOCOL_VERSION,
247 descriptor_sha256: self.digest.to_vec(),
248 attempt_id,
249 })),
250 });
251 let expected = activation_ids.iter().copied().collect::<BTreeSet<_>>();
252 let mut seen = BTreeSet::new();
253 let mut output_rows = 0usize;
254 let mut output_bytes = 0usize;
255 let mut output_count = 0usize;
256 let mut timer_count = 0usize;
257 for result in results {
258 if !expected.contains(&result.activation_id) || !seen.insert(result.activation_id) {
259 return Err(Status::internal(
260 "reference handler returned invalid activation ID",
261 ));
262 }
263 let output_len = result.output.len();
264 let output_count_u32 = u32::try_from(output_len)
265 .map_err(|_| Status::resource_exhausted("too many process output batches"))?;
266 output_count = output_count
267 .checked_add(output_len)
268 .filter(|count| *count <= self.descriptor.limits.max_output_rows.max(1))
269 .ok_or_else(|| Status::resource_exhausted("process output batch limit exceeded"))?;
270 timer_count = timer_count
271 .checked_add(result.timers.len())
272 .filter(|count| *count <= self.descriptor.limits.max_timers)
273 .ok_or_else(|| Status::resource_exhausted("process result timer limit exceeded"))?;
274 frames.push(wire::WorkerFrame {
275 kind: Some(worker_frame::Kind::Result(wire::Result {
276 activation_id: result.activation_id,
277 output_count: output_count_u32,
278 mutation: Some(encode_mutation(result.value)),
279 timers: result.timers.into_iter().map(encode_timer).collect(),
280 })),
281 });
282 for batch in result.output {
283 output_rows = output_rows
284 .checked_add(batch.num_rows())
285 .filter(|rows| *rows <= self.descriptor.limits.max_output_rows)
286 .ok_or_else(|| {
287 Status::resource_exhausted("process output row limit exceeded")
288 })?;
289 output_bytes = output_bytes
290 .checked_add(batch.get_array_memory_size())
291 .filter(|bytes| *bytes <= self.descriptor.limits.max_output_bytes)
292 .ok_or_else(|| {
293 Status::resource_exhausted("process output byte limit exceeded")
294 })?;
295 let ipc = encode_batch(&batch, &self.descriptor.output_schema)
296 .map_err(|error| Status::internal(error.to_string()))?;
297 frames.push(wire::WorkerFrame {
298 kind: Some(worker_frame::Kind::Output(wire::Output {
299 activation_id: result.activation_id,
300 arrow_ipc: ipc,
301 })),
302 });
303 }
304 }
305 frames.push(wire::WorkerFrame {
306 kind: Some(worker_frame::Kind::Complete(wire::Complete {
307 result_count: u32::try_from(seen.len())
308 .map_err(|_| Status::resource_exhausted("process result count overflow"))?,
309 output_count: u32::try_from(output_count)
310 .map_err(|_| Status::resource_exhausted("process output count overflow"))?,
311 })),
312 });
313 frames.iter().try_fold(0usize, |sum, frame| {
314 sum.checked_add(frame.encoded_len())
315 .filter(|total| *total <= MAX_INVOCATION_WIRE_BYTES)
316 .ok_or_else(|| Status::resource_exhausted("process response wire budget exceeded"))
317 })?;
318 Ok(frames)
319 }
320}
321
322#[tonic::async_trait]
323impl wire::process_worker_server::ProcessWorker for RustReferenceWorker {
324 type ExchangeStream =
325 std::pin::Pin<Box<dyn Stream<Item = Result<wire::WorkerFrame, Status>> + Send>>;
326
327 async fn exchange(
328 &self,
329 request: Request<tonic::Streaming<wire::HostFrame>>,
330 ) -> Result<Response<Self::ExchangeStream>, Status> {
331 let permit = self
333 .credits
334 .clone()
335 .try_acquire_owned()
336 .map_err(|_| Status::resource_exhausted("process worker invocation slots full"))?;
337 let inbound = request.into_inner();
338 let (attempt_id, activations) = self.read_request(inbound).await?;
339 let handler = Arc::clone(&self.handler);
340 let activation_ids = activations.iter().map(|item| item.id).collect::<Vec<_>>();
341 let (permit, results) = tokio::task::spawn_blocking(move || {
344 let results = handler.invoke(&activations);
345 (permit, results)
346 })
347 .await
348 .map_err(|error| Status::internal(format!("process handler task failed: {error}")))?;
349 let frames = match results {
350 Ok(results) => self.encode_response(attempt_id, &activation_ids, results)?,
351 Err(error) => vec![wire::WorkerFrame {
352 kind: Some(worker_frame::Kind::Failure(wire::Failure {
353 message: error.to_string(),
354 })),
355 }],
356 };
357 let stream = tokio_stream::iter(frames.into_iter().map(move |frame| {
358 let _keep_credit = &permit;
359 Ok(frame)
360 }));
361 Ok(Response::new(Box::pin(stream)))
362 }
363}