Skip to main content

platform_runtime/
functions.rs

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}