1use crate::retries::RetryPolicy;
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4use platform_core::{
5 ActorContext, AppError, AppResult, CorrelationId, DbPool, ErrorCode, ExecutionContext,
6 ExecutionId, RuntimeSpanAttributes, TenantId, TraceContext, db::DbTransaction,
7 record_runtime_span_attributes, trace_context_from_headers, trace_headers,
8};
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11use std::collections::BTreeMap;
12use std::fmt::Debug;
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15use tracing::Instrument;
16use uuid::Uuid;
17
18const STALE_PROCESSING_LOCK_SECONDS: i64 = 300;
19
20#[async_trait]
21pub trait FunctionHandler: Debug + Send + Sync {
22 async fn call(&self, ctx: ExecutionContext, input: Value) -> AppResult<Value>;
23
24 fn observability(&self) -> Option<FunctionHandlerObservability> {
25 None
26 }
27}
28
29pub use FunctionHandler as RuntimeFunction;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct FunctionHandlerObservability {
33 pub source: String,
34 pub attributes: Value,
35}
36
37impl FunctionHandlerObservability {
38 pub fn new(source: impl Into<String>, attributes: Value) -> Self {
39 Self {
40 source: source.into(),
41 attributes: normalize_log_attributes(attributes),
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47enum ExecutionLogSeverity {
48 Info,
49 Error,
50}
51
52impl ExecutionLogSeverity {
53 fn as_str(self) -> &'static str {
54 match self {
55 Self::Info => "info",
56 Self::Error => "error",
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
62pub struct FunctionDefinition {
63 pub name: String,
64 pub version: u16,
65 pub queue: String,
66 pub retry_policy: RetryPolicy,
67 pub handler: Arc<dyn FunctionHandler>,
68}
69
70#[derive(Debug, Default, Clone)]
71pub struct FunctionRegistry {
72 functions: BTreeMap<String, FunctionDefinition>,
73}
74
75impl FunctionRegistry {
76 pub fn register(&mut self, function: FunctionDefinition) {
77 self.functions.insert(function.name.clone(), function);
78 }
79
80 pub fn get(&self, name: &str) -> Option<&FunctionDefinition> {
81 self.functions.get(name)
82 }
83
84 pub fn all(&self) -> impl Iterator<Item = &FunctionDefinition> {
85 self.functions.values()
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
90#[serde(rename_all = "snake_case")]
91pub enum FunctionRunStatus {
92 Pending,
93 Processing,
94 Completed,
95 Failed,
96 Dead,
97}
98
99impl FunctionRunStatus {
100 fn as_str(self) -> &'static str {
101 match self {
102 Self::Pending => "pending",
103 Self::Processing => "processing",
104 Self::Completed => "completed",
105 Self::Failed => "failed",
106 Self::Dead => "dead",
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
112pub struct EnqueueFunctionRequest {
113 pub function_name: String,
114 pub input_json: Value,
115 pub correlation_id: CorrelationId,
116 pub actor: ActorContext,
117 pub trace: TraceContext,
118 pub causation_id: Option<String>,
119 pub max_attempts: Option<i32>,
120}
121
122#[derive(Debug, Clone, Deserialize, Serialize)]
123pub struct ClaimedFunctionRun {
124 pub id: String,
125 pub function_name: String,
126 pub input_json: Value,
127 pub attempts: i32,
128 pub max_attempts: i32,
129 pub correlation_id: String,
130 pub actor: ActorContext,
131 pub trace: TraceContext,
132 pub causation_id: Option<String>,
133}
134
135#[derive(Debug, Clone)]
136pub struct RuntimeClient {
137 pool: DbPool,
138}
139
140impl RuntimeClient {
141 pub fn new(pool: DbPool) -> Self {
142 Self { pool }
143 }
144
145 pub async fn enqueue_function(&self, request: EnqueueFunctionRequest) -> AppResult<String> {
146 let mut tx = self.pool.begin().await.map_err(map_runtime_error)?;
147 let run = self.enqueue_function_in_tx(&mut tx, request).await?;
148 tx.commit().await.map_err(map_runtime_error)?;
149 self.record_function_enqueued(&run).await;
150 Ok(run.id)
151 }
152
153 pub(crate) async fn enqueue_function_in_tx(
154 &self,
155 tx: &mut DbTransaction<'_>,
156 request: EnqueueFunctionRequest,
157 ) -> AppResult<EnqueuedFunctionRun> {
158 let id = format!("fnrun_{}", Uuid::now_v7());
159 let max_attempts = request.max_attempts.unwrap_or(3);
160 let mut input_json = request.input_json;
161 attach_runtime_context_to_input(
162 &mut input_json,
163 &request.correlation_id,
164 &request.trace,
165 request.causation_id.as_deref(),
166 );
167 let span = tracing::info_span!(
168 "function_enqueue",
169 lenso.correlation_id = tracing::field::Empty,
170 lenso.story_id = tracing::field::Empty,
171 lenso.function_run_id = tracing::field::Empty,
172 lenso.execution.kind = tracing::field::Empty,
173 lenso.execution.name = tracing::field::Empty,
174 );
175 record_runtime_span_attributes(
176 &span,
177 &RuntimeSpanAttributes::function(
178 request.correlation_id.0.clone(),
179 id.clone(),
180 request.function_name.clone(),
181 ),
182 );
183
184 async {
185 sqlx::query(
186 r#"
187 insert into runtime.function_runs (
188 id,
189 function_name,
190 input_json,
191 max_attempts,
192 correlation_id,
193 actor
194 )
195 values ($1, $2, $3, $4, $5, $6)
196 "#,
197 )
198 .bind(&id)
199 .bind(&request.function_name)
200 .bind(&input_json)
201 .bind(max_attempts)
202 .bind(&request.correlation_id.0)
203 .bind(serde_json::to_value(&request.actor).map_err(map_serde_error)?)
204 .execute(&mut **tx)
205 .await
206 .map_err(map_runtime_error)
207 }
208 .instrument(span)
209 .await?;
210
211 Ok(EnqueuedFunctionRun {
212 id,
213 function_name: request.function_name,
214 correlation_id: request.correlation_id.0,
215 trace: request.trace,
216 max_attempts,
217 })
218 }
219
220 pub(crate) async fn record_function_enqueued(&self, run: &EnqueuedFunctionRun) {
221 self.record_function_execution_log(
222 &FunctionLogContext {
223 id: run.id.clone(),
224 function_name: run.function_name.clone(),
225 correlation_id: run.correlation_id.clone(),
226 trace: run.trace.clone(),
227 },
228 ExecutionLogSeverity::Info,
229 "Function run enqueued",
230 serde_json::json!({
231 "attempt": 0,
232 "max_attempts": run.max_attempts,
233 }),
234 )
235 .await;
236 }
237
238 async fn record_function_execution_log(
239 &self,
240 run: &FunctionLogContext,
241 severity: ExecutionLogSeverity,
242 body: &'static str,
243 attributes: Value,
244 ) {
245 emit_function_lifecycle_event(run, severity, body, &attributes, None);
246 if let Err(error) = insert_execution_log_projection(
247 &self.pool,
248 function_log_record(run, severity, body, attributes),
249 )
250 .await
251 {
252 tracing::warn!(
253 error = ?error,
254 function_run_id = %run.id,
255 "failed to write function execution log"
256 );
257 }
258 }
259}
260
261#[derive(Debug, Clone)]
262pub(crate) struct EnqueuedFunctionRun {
263 pub id: String,
264 function_name: String,
265 correlation_id: String,
266 trace: TraceContext,
267 max_attempts: i32,
268}
269
270#[derive(Debug, Clone)]
271pub struct RuntimeWorker {
272 pool: DbPool,
273 registry: Arc<FunctionRegistry>,
274 worker_id: String,
275}
276
277impl RuntimeWorker {
278 pub fn new(
279 pool: DbPool,
280 registry: Arc<FunctionRegistry>,
281 worker_id: impl Into<String>,
282 ) -> Self {
283 Self {
284 pool,
285 registry,
286 worker_id: worker_id.into(),
287 }
288 }
289
290 pub async fn claim_batch(&self, batch_size: i64) -> AppResult<Vec<ClaimedFunctionRun>> {
291 let span = tracing::info_span!(
292 "function_claim_batch",
293 worker_id = %self.worker_id,
294 lenso.execution.kind = "function_claim",
295 lenso.execution.name = "function.claim_batch",
296 );
297
298 async {
299 let runs = sqlx::query_as::<_, FunctionRunRow>(
300 r#"
301 with claimed as (
302 select id
303 from runtime.function_runs
304 where (
305 status in ('pending', 'failed')
306 and available_at <= now()
307 )
308 or (
309 status = 'processing'
310 and locked_at <= now() - ($1::double precision * interval '1 second')
311 )
312 order by available_at asc, created_at asc
313 limit $2
314 for update skip locked
315 )
316 update runtime.function_runs function_run
317 set status = 'processing',
318 locked_at = now(),
319 locked_by = $3,
320 started_at = coalesce(started_at, now()),
321 last_error = null,
322 updated_at = now()
323 from claimed
324 where function_run.id = claimed.id
325 returning
326 function_run.id,
327 function_run.function_name,
328 function_run.input_json,
329 function_run.attempts,
330 function_run.max_attempts,
331 function_run.correlation_id,
332 function_run.actor
333 "#,
334 )
335 .bind(stale_processing_lock_seconds())
336 .bind(batch_size)
337 .bind(&self.worker_id)
338 .fetch_all(&self.pool)
339 .await
340 .map(|rows| {
341 rows.into_iter()
342 .map(TryInto::try_into)
343 .collect::<AppResult<Vec<_>>>()
344 })
345 .map_err(map_runtime_error)??;
346
347 for run in &runs {
348 self.record_function_execution_log(
349 run,
350 ExecutionLogSeverity::Info,
351 "Function run claimed",
352 serde_json::json!({
353 "attempt": run.attempts + 1,
354 "max_attempts": run.max_attempts,
355 "worker_id": self.worker_id,
356 }),
357 )
358 .await;
359 }
360
361 Ok(runs)
362 }
363 .instrument(span)
364 .await
365 }
366
367 pub async fn claim_and_run_batch(&self, batch_size: i64) -> AppResult<usize> {
368 let span = tracing::info_span!(
369 "function_worker_loop",
370 worker_id = %self.worker_id,
371 lenso.execution.kind = "worker_loop",
372 lenso.execution.name = "runtime_worker.claim_and_run_batch",
373 );
374
375 async {
376 let runs = self.claim_batch(batch_size).await?;
377 let count = runs.len();
378
379 for run in runs {
380 self.run_claimed(run).await?;
381 }
382
383 Ok(count)
384 }
385 .instrument(span)
386 .await
387 }
388
389 async fn run_claimed(&self, run: ClaimedFunctionRun) -> AppResult<()> {
390 let span = tracing::info_span!(
391 "function_run",
392 lenso.correlation_id = tracing::field::Empty,
393 lenso.story_id = tracing::field::Empty,
394 lenso.function_run_id = tracing::field::Empty,
395 lenso.execution.kind = tracing::field::Empty,
396 lenso.execution.name = tracing::field::Empty,
397 );
398 record_runtime_span_attributes(
399 &span,
400 &RuntimeSpanAttributes::function(
401 run.correlation_id.clone(),
402 run.id.clone(),
403 run.function_name.clone(),
404 ),
405 );
406
407 async {
408 self.record_function_execution_log(
409 &run,
410 ExecutionLogSeverity::Info,
411 "Function run started",
412 serde_json::json!({
413 "attempt": run.attempts + 1,
414 "max_attempts": run.max_attempts,
415 "worker_id": self.worker_id,
416 }),
417 )
418 .await;
419
420 let Some(definition) = self.registry.get(&run.function_name) else {
421 let error = AppError::new(
422 ErrorCode::Internal,
423 format!("Runtime function {} is not registered", run.function_name),
424 )
425 .retryable();
426 self.mark_failed(&run, &error, RetryPolicy::default().initial_delay)
427 .await?;
428 return Ok(());
429 };
430
431 let attempt = u32::try_from(run.attempts + 1).unwrap_or(u32::MAX);
432 let ctx = ExecutionContext {
433 execution_id: ExecutionId(run.id.clone()),
434 function_name: run.function_name.clone(),
435 attempt,
436 queue: definition.queue.clone(),
437 correlation_id: CorrelationId::new(run.correlation_id.clone()),
438 causation_id: run.causation_id.clone(),
439 actor: run.actor.clone(),
440 tenant_id: None::<TenantId>,
441 trace: run.trace.clone(),
442 deadline: None::<DateTime<Utc>>,
443 };
444
445 let observability = definition.handler.observability();
446 let started_at = Utc::now();
447 let started = Instant::now();
448 let result = definition.handler.call(ctx, run.input_json.clone()).await;
449 let duration_ms = started.elapsed().as_millis().try_into().unwrap_or(i64::MAX);
450 if let Some(observability) = observability {
451 self.record_function_handler_operation_log(
452 &run,
453 observability,
454 started_at,
455 duration_ms,
456 result.as_ref().err(),
457 )
458 .await;
459 }
460
461 match result {
462 Ok(_output) => self.mark_completed(&run).await,
463 Err(error) => {
464 self.mark_failed(&run, &error, definition.retry_policy.initial_delay)
465 .await
466 }
467 }
468 }
469 .instrument(span)
470 .await
471 }
472
473 pub async fn mark_completed(&self, run: &ClaimedFunctionRun) -> AppResult<()> {
474 sqlx::query(
475 r#"
476 update runtime.function_runs
477 set status = 'completed',
478 completed_at = now(),
479 locked_at = null,
480 locked_by = null,
481 last_error = null,
482 updated_at = now()
483 where id = $1
484 "#,
485 )
486 .bind(&run.id)
487 .execute(&self.pool)
488 .await
489 .map(|_| ())
490 .map_err(map_runtime_error)?;
491
492 self.record_function_execution_log(
493 run,
494 ExecutionLogSeverity::Info,
495 "Function run completed",
496 serde_json::json!({
497 "attempt": run.attempts + 1,
498 "max_attempts": run.max_attempts,
499 "worker_id": self.worker_id,
500 }),
501 )
502 .await;
503
504 Ok(())
505 }
506
507 pub async fn mark_failed(
508 &self,
509 run: &ClaimedFunctionRun,
510 error: &AppError,
511 retry_delay: Duration,
512 ) -> AppResult<()> {
513 let next_attempt = run.attempts + 1;
514 let status = if next_attempt >= run.max_attempts {
515 FunctionRunStatus::Dead
516 } else if error.retryable {
517 FunctionRunStatus::Failed
518 } else {
519 FunctionRunStatus::Dead
520 };
521
522 let span = tracing::info_span!(
523 "function_run_fail",
524 lenso.correlation_id = tracing::field::Empty,
525 lenso.story_id = tracing::field::Empty,
526 lenso.function_run_id = tracing::field::Empty,
527 lenso.execution.kind = tracing::field::Empty,
528 lenso.execution.name = tracing::field::Empty,
529 );
530 record_runtime_span_attributes(
531 &span,
532 &RuntimeSpanAttributes::function(
533 run.correlation_id.clone(),
534 run.id.clone(),
535 run.function_name.clone(),
536 ),
537 );
538
539 async {
540 sqlx::query(
541 r#"
542 update runtime.function_runs
543 set status = $2,
544 attempts = attempts + 1,
545 available_at = case
546 when $2 = 'failed' then now() + ($4::double precision * interval '1 second')
547 else available_at
548 end,
549 locked_at = null,
550 locked_by = null,
551 last_error = $3,
552 updated_at = now()
553 where id = $1
554 "#,
555 )
556 .bind(&run.id)
557 .bind(status.as_str())
558 .bind(error.public_message.as_str())
559 .bind(retry_delay_seconds(retry_delay))
560 .execute(&self.pool)
561 .await
562 .map(|_| ())
563 .map_err(map_runtime_error)?;
564
565 self.record_function_execution_log(
566 run,
567 ExecutionLogSeverity::Error,
568 if status == FunctionRunStatus::Dead {
569 "Function run marked dead"
570 } else {
571 "Function run failed"
572 },
573 serde_json::json!({
574 "attempt": next_attempt,
575 "max_attempts": run.max_attempts,
576 "status": status.as_str(),
577 "retryable": error.retryable,
578 "error": error.public_message,
579 "worker_id": self.worker_id,
580 }),
581 )
582 .await;
583
584 Ok(())
585 }
586 .instrument(span)
587 .await
588 }
589
590 async fn record_function_execution_log(
591 &self,
592 run: &ClaimedFunctionRun,
593 severity: ExecutionLogSeverity,
594 body: &'static str,
595 attributes: Value,
596 ) {
597 emit_function_lifecycle_event(run, severity, body, &attributes, Some(&self.worker_id));
598 if let Err(error) = insert_execution_log_projection(
599 &self.pool,
600 function_log_record(run, severity, body, attributes),
601 )
602 .await
603 {
604 tracing::warn!(
605 error = ?error,
606 function_run_id = %run.id,
607 "failed to write function execution log"
608 );
609 }
610 }
611
612 async fn record_function_handler_operation_log(
613 &self,
614 run: &ClaimedFunctionRun,
615 observability: FunctionHandlerObservability,
616 started_at: DateTime<Utc>,
617 duration_ms: i64,
618 error: Option<&AppError>,
619 ) {
620 let body = if error.is_some() {
621 "Function handler operation failed"
622 } else {
623 "Function handler operation completed"
624 };
625 let severity = if error.is_some() {
626 ExecutionLogSeverity::Error
627 } else {
628 ExecutionLogSeverity::Info
629 };
630 let attributes = function_handler_operation_attributes(
631 run,
632 &self.worker_id,
633 observability,
634 duration_ms,
635 error,
636 );
637 emit_function_lifecycle_event(run, severity, body, &attributes, Some(&self.worker_id));
638 if let Err(error) = insert_execution_log_projection(
639 &self.pool,
640 function_log_record(run, severity, body, attributes).with_occurred_at(started_at),
641 )
642 .await
643 {
644 tracing::warn!(
645 error = ?error,
646 function_run_id = %run.id,
647 "failed to write function handler operation log"
648 );
649 }
650 }
651}
652
653#[derive(Debug)]
654struct FunctionLogContext {
655 id: String,
656 function_name: String,
657 correlation_id: String,
658 trace: TraceContext,
659}
660
661trait FunctionLogSource {
662 fn id(&self) -> String;
663 fn function_name(&self) -> String;
664 fn correlation_id(&self) -> String;
665 fn trace(&self) -> TraceContext;
666}
667
668impl FunctionLogSource for FunctionLogContext {
669 fn id(&self) -> String {
670 self.id.clone()
671 }
672
673 fn function_name(&self) -> String {
674 self.function_name.clone()
675 }
676
677 fn correlation_id(&self) -> String {
678 self.correlation_id.clone()
679 }
680
681 fn trace(&self) -> TraceContext {
682 self.trace.clone()
683 }
684}
685
686impl FunctionLogSource for ClaimedFunctionRun {
687 fn id(&self) -> String {
688 self.id.clone()
689 }
690
691 fn function_name(&self) -> String {
692 self.function_name.clone()
693 }
694
695 fn correlation_id(&self) -> String {
696 self.correlation_id.clone()
697 }
698
699 fn trace(&self) -> TraceContext {
700 self.trace.clone()
701 }
702}
703
704fn emit_function_lifecycle_event(
705 run: &impl FunctionLogSource,
706 severity: ExecutionLogSeverity,
707 body: &'static str,
708 attributes: &Value,
709 worker_id: Option<&str>,
710) {
711 match severity {
712 ExecutionLogSeverity::Error => {
713 tracing::error!(
714 function_run_id = %run.id(),
715 function_name = %run.function_name(),
716 correlation_id = %run.correlation_id(),
717 worker_id = worker_id.unwrap_or(""),
718 attributes = %attributes,
719 "{body}"
720 );
721 }
722 _ => {
723 tracing::info!(
724 function_run_id = %run.id(),
725 function_name = %run.function_name(),
726 correlation_id = %run.correlation_id(),
727 worker_id = worker_id.unwrap_or(""),
728 attributes = %attributes,
729 "{body}"
730 );
731 }
732 }
733}
734
735fn function_log_record(
736 run: &impl FunctionLogSource,
737 severity: ExecutionLogSeverity,
738 body: impl Into<String>,
739 attributes: Value,
740) -> ExecutionLogProjectionRecord {
741 ExecutionLogProjectionRecord::from_runtime_attrs(
742 RuntimeSpanAttributes::function(run.correlation_id(), run.id(), run.function_name()),
743 severity,
744 body,
745 )
746 .with_attributes(attributes)
747 .with_trace(run.trace())
748}
749
750#[derive(Debug, Clone)]
751struct ExecutionLogProjectionRecord {
752 correlation_id: String,
753 execution_id: String,
754 execution_type: String,
755 execution_name: String,
756 severity: ExecutionLogSeverity,
757 body: String,
758 attributes: Value,
759 trace: TraceContext,
760 service_name: String,
761 occurred_at: Option<DateTime<Utc>>,
762}
763
764impl ExecutionLogProjectionRecord {
765 fn from_runtime_attrs(
766 attrs: RuntimeSpanAttributes,
767 severity: ExecutionLogSeverity,
768 body: impl Into<String>,
769 ) -> Self {
770 let execution_id = attrs
771 .function_run_id
772 .clone()
773 .or(attrs.outbox_event_id)
774 .unwrap_or_else(|| attrs.story_id.clone());
775
776 Self {
777 correlation_id: attrs.correlation_id,
778 execution_id,
779 execution_type: attrs.execution_kind,
780 execution_name: attrs.execution_name,
781 severity,
782 body: body.into(),
783 attributes: Value::Object(Default::default()),
784 trace: TraceContext::default(),
785 service_name: "lenso".to_owned(),
786 occurred_at: None,
787 }
788 }
789
790 fn with_attributes(mut self, attributes: Value) -> Self {
791 self.attributes = attributes;
792 self
793 }
794
795 fn with_trace(mut self, trace: TraceContext) -> Self {
796 self.trace = trace;
797 self
798 }
799
800 fn with_occurred_at(mut self, occurred_at: DateTime<Utc>) -> Self {
801 self.occurred_at = Some(occurred_at);
802 self
803 }
804}
805
806async fn insert_execution_log_projection(
807 pool: &DbPool,
808 record: ExecutionLogProjectionRecord,
809) -> AppResult<String> {
810 let id = format!("elog_{}", Uuid::now_v7());
811 let occurred_at = record.occurred_at.unwrap_or_else(Utc::now);
812
813 sqlx::query(
814 r#"
815 insert into platform.execution_logs (
816 id,
817 correlation_id,
818 story_id,
819 execution_id,
820 execution_type,
821 execution_name,
822 occurred_at,
823 severity,
824 body,
825 attributes,
826 trace_id,
827 span_id,
828 service_name,
829 redacted_fields
830 )
831 values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
832 "#,
833 )
834 .bind(&id)
835 .bind(&record.correlation_id)
836 .bind(&record.correlation_id)
837 .bind(&record.execution_id)
838 .bind(&record.execution_type)
839 .bind(&record.execution_name)
840 .bind(occurred_at)
841 .bind(record.severity.as_str())
842 .bind(&record.body)
843 .bind(normalize_log_attributes(record.attributes))
844 .bind(&record.trace.trace_id)
845 .bind(&record.trace.span_id)
846 .bind(&record.service_name)
847 .bind(Vec::<String>::new())
848 .execute(pool)
849 .await
850 .map_err(map_runtime_error)?;
851
852 Ok(id)
853}
854
855fn function_handler_operation_attributes(
856 run: &ClaimedFunctionRun,
857 worker_id: &str,
858 observability: FunctionHandlerObservability,
859 duration_ms: i64,
860 error: Option<&AppError>,
861) -> Value {
862 let mut attributes = match observability.attributes {
863 Value::Object(attributes) => attributes,
864 other => serde_json::Map::from_iter([("value".to_owned(), other)]),
865 };
866 attributes.insert("source".to_owned(), Value::String(observability.source));
867 attributes.insert("attempt".to_owned(), serde_json::json!(run.attempts + 1));
868 attributes.insert(
869 "max_attempts".to_owned(),
870 serde_json::json!(run.max_attempts),
871 );
872 attributes.insert("duration_ms".to_owned(), serde_json::json!(duration_ms));
873 attributes.insert("success".to_owned(), serde_json::json!(error.is_none()));
874 attributes.insert("worker_id".to_owned(), serde_json::json!(worker_id));
875 attributes.insert(
876 "function_name".to_owned(),
877 serde_json::json!(run.function_name),
878 );
879 attributes.insert("request_id".to_owned(), serde_json::json!(run.id));
880 attributes.insert("trace_id".to_owned(), serde_json::json!(run.trace.trace_id));
881 attributes.insert("span_id".to_owned(), serde_json::json!(run.trace.span_id));
882
883 if let Some(error) = error {
884 attributes.insert(
885 "error_code".to_owned(),
886 serde_json::json!(error.code.as_str()),
887 );
888 attributes.insert("error".to_owned(), serde_json::json!(error.public_message));
889 attributes.insert("retryable".to_owned(), serde_json::json!(error.retryable));
890 attributes.insert("error_details".to_owned(), serde_json::json!(error.details));
891 }
892
893 Value::Object(attributes)
894}
895
896fn normalize_log_attributes(attributes: Value) -> Value {
897 match attributes {
898 Value::Object(_) => attributes,
899 other => serde_json::json!({ "value": other }),
900 }
901}
902
903fn stale_processing_lock_seconds() -> f64 {
904 STALE_PROCESSING_LOCK_SECONDS as f64
905}
906
907fn retry_delay_seconds(delay: Duration) -> f64 {
908 delay.as_secs_f64()
909}
910
911type FunctionRunRow = (String, String, Value, i32, i32, String, Value);
912
913impl TryFrom<FunctionRunRow> for ClaimedFunctionRun {
914 type Error = AppError;
915
916 fn try_from(row: FunctionRunRow) -> Result<Self, Self::Error> {
917 let (id, function_name, input_json, attempts, max_attempts, correlation_id, actor) = row;
918 let runtime_context = input_json.get("_lenso_runtime");
919 let trace = runtime_context
920 .map(trace_context_from_headers)
921 .unwrap_or_default();
922 let causation_id = runtime_context
923 .and_then(|context| context.get("causation_id"))
924 .and_then(Value::as_str)
925 .map(ToOwned::to_owned);
926 Ok(Self {
927 id,
928 function_name,
929 input_json,
930 attempts,
931 max_attempts,
932 correlation_id,
933 actor: serde_json::from_value(actor).map_err(map_serde_error)?,
934 trace,
935 causation_id,
936 })
937 }
938}
939
940fn attach_runtime_context_to_input(
941 input_json: &mut Value,
942 correlation_id: &CorrelationId,
943 trace: &TraceContext,
944 causation_id: Option<&str>,
945) {
946 let mut runtime_context = trace_headers(trace, correlation_id);
947 if let Some(causation_id) = causation_id {
948 runtime_context["causation_id"] = Value::String(causation_id.to_owned());
949 }
950
951 match input_json {
952 Value::Object(object) => {
953 object.insert("_lenso_runtime".to_owned(), runtime_context);
954 }
955 other => {
956 *other = serde_json::json!({
957 "payload": other.clone(),
958 "_lenso_runtime": runtime_context,
959 });
960 }
961 }
962}
963
964pub(crate) fn map_runtime_error(source: sqlx::Error) -> AppError {
965 AppError::new(ErrorCode::Internal, "Runtime operation failed").with_source(source)
966}
967
968fn map_serde_error(source: serde_json::Error) -> AppError {
969 AppError::new(ErrorCode::Internal, "Runtime payload serialization failed").with_source(source)
970}