1use std::path::Path;
14use std::sync::Arc;
15use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
16use std::time::Duration;
17
18use lash_core::{
19 AwaitEventResolver, DurabilityTier, EffectHost, ExecutionScope, PluginError, ProcessCommand,
20 ProcessEffectOutcome, RuntimeEffectCommand, RuntimeEffectController,
21 RuntimeEffectControllerError, RuntimeEffectEnvelope, RuntimeEffectLocalExecutor,
22 RuntimeEffectOutcome, RuntimeError, ScopedEffectController,
23};
24
25use super::*;
26
27const STATUS_IN_PROGRESS: &str = "in_progress";
28const STATUS_COMPLETED: &str = "completed";
29const STATUS_FAILED: &str = "failed";
30const DEFAULT_LEASE_TTL: Duration = Duration::from_secs(30);
31const BUSY_POLL: Duration = Duration::from_millis(25);
32
33static EFFECT_OWNER_COUNTER: AtomicU64 = AtomicU64::new(1);
34
35#[derive(Clone, Debug)]
37pub struct SqliteEffectReplayOptions {
38 pub lease_ttl: Duration,
39}
40
41impl Default for SqliteEffectReplayOptions {
42 fn default() -> Self {
43 Self {
44 lease_ttl: DEFAULT_LEASE_TTL,
45 }
46 }
47}
48
49struct SqliteEffectReplayInner {
50 conn: SqliteConnection,
51 owner_id: String,
52 lease_counter: AtomicU64,
53 replay_mode: AtomicBool,
54 lease_ttl_ms: u64,
55}
56
57#[derive(Clone)]
63pub struct SqliteEffectHost {
64 inner: Arc<SqliteEffectReplayInner>,
65}
66
67#[derive(Clone)]
69pub struct SqliteRuntimeEffectController {
70 inner: Arc<SqliteEffectReplayInner>,
71 scope: ExecutionScope,
72}
73
74struct ClaimedEffect {
75 scope_id: String,
76 replay_key: String,
77 envelope_hash: String,
78 lease_token: String,
79 due_at_ms: Option<u64>,
80}
81
82enum PreparedEffect {
83 ReplayOutcome {
84 outcome: Box<RuntimeEffectOutcome>,
85 due_at_ms: Option<u64>,
86 },
87 ReplayError(RuntimeEffectControllerError),
88 Claimed(ClaimedEffect),
89 Busy {
90 retry_at_ms: u64,
91 },
92}
93
94impl SqliteEffectHost {
95 pub async fn open(path: &Path) -> tokio_rusqlite::Result<Self> {
96 Self::open_with_options(path, SqliteEffectReplayOptions::default()).await
97 }
98
99 pub async fn open_with_options(
100 path: &Path,
101 options: SqliteEffectReplayOptions,
102 ) -> tokio_rusqlite::Result<Self> {
103 Ok(Self {
104 inner: open_effect_replay_inner(path, StoreBacking::File, options).await?,
105 })
106 }
107
108 pub async fn memory() -> tokio_rusqlite::Result<Self> {
109 Self::memory_with_options(SqliteEffectReplayOptions::default()).await
110 }
111
112 pub async fn memory_with_options(
113 options: SqliteEffectReplayOptions,
114 ) -> tokio_rusqlite::Result<Self> {
115 Ok(Self {
116 inner: open_effect_replay_memory_inner(options).await?,
117 })
118 }
119
120 pub fn start_replay(&self) {
123 self.inner.replay_mode.store(true, Ordering::SeqCst);
124 }
125}
126
127impl AwaitEventResolver for SqliteEffectHost {
128 fn durability_tier(&self) -> DurabilityTier {
129 DurabilityTier::Durable
130 }
131
132 fn requires_durable_attachment_store(&self) -> bool {
133 true
134 }
135}
136
137impl EffectHost for SqliteEffectHost {
138 fn scoped<'run>(
139 &'run self,
140 scope: ExecutionScope,
141 ) -> Result<ScopedEffectController<'run>, RuntimeError> {
142 let controller = SqliteRuntimeEffectController {
143 inner: Arc::clone(&self.inner),
144 scope: scope.clone(),
145 };
146 ScopedEffectController::shared(Arc::new(controller), scope)
147 }
148
149 fn scoped_static(
150 &self,
151 scope: ExecutionScope,
152 ) -> Result<Option<ScopedEffectController<'static>>, RuntimeError> {
153 let controller = SqliteRuntimeEffectController {
154 inner: Arc::clone(&self.inner),
155 scope: scope.clone(),
156 };
157 Ok(Some(ScopedEffectController::shared(
158 Arc::new(controller),
159 scope,
160 )?))
161 }
162}
163
164impl SqliteRuntimeEffectController {
165 pub async fn open(path: &Path, scope: ExecutionScope) -> tokio_rusqlite::Result<Self> {
166 Self::open_with_options(path, scope, SqliteEffectReplayOptions::default()).await
167 }
168
169 pub async fn open_with_options(
170 path: &Path,
171 scope: ExecutionScope,
172 options: SqliteEffectReplayOptions,
173 ) -> tokio_rusqlite::Result<Self> {
174 Ok(Self {
175 inner: open_effect_replay_inner(path, StoreBacking::File, options).await?,
176 scope,
177 })
178 }
179
180 pub async fn memory(scope: ExecutionScope) -> tokio_rusqlite::Result<Self> {
181 Self::memory_with_options(scope, SqliteEffectReplayOptions::default()).await
182 }
183
184 pub async fn memory_with_options(
185 scope: ExecutionScope,
186 options: SqliteEffectReplayOptions,
187 ) -> tokio_rusqlite::Result<Self> {
188 Ok(Self {
189 inner: open_effect_replay_memory_inner(options).await?,
190 scope,
191 })
192 }
193
194 pub fn start_replay(&self) {
197 self.inner.replay_mode.store(true, Ordering::SeqCst);
198 }
199
200 async fn prepare_effect(
201 &self,
202 envelope: &RuntimeEffectEnvelope,
203 ) -> Result<PreparedEffect, RuntimeEffectControllerError> {
204 let replay_key = envelope
205 .invocation
206 .replay_key()
207 .ok_or_else(|| {
208 RuntimeEffectControllerError::new(
209 "sqlite_effect_replay_key_missing",
210 "runtime effect envelope requires replay.key",
211 )
212 })?
213 .to_string();
214 let envelope_hash = envelope.stable_hash()?;
215 let scope_id = self.scope.id().to_string();
216 let now = current_epoch_ms();
217 let lease_token = self.inner.next_lease_token();
218 let due_at_ms = sleep_due_at_ms(envelope, now);
219 let lease_expires_at_ms = now.saturating_add(self.inner.lease_ttl_ms);
220 let replay_mode = self.inner.replay_mode.load(Ordering::SeqCst);
221 let owner_id = self.inner.owner_id.clone();
222 let lease_ttl_ms = self.inner.lease_ttl_ms;
223
224 let outcome: Result<PreparedEffect, RuntimeEffectControllerError> = self
232 .inner
233 .conn
234 .write(move |tx| {
235 let row = tx
236 .query_row(
237 "SELECT envelope_hash, status, outcome_json, error_json,
238 lease_owner_id, lease_token, lease_expires_at_ms, due_at_ms
239 FROM runtime_effect_replay
240 WHERE scope_id = ?1 AND replay_key = ?2",
241 params![scope_id.as_str(), replay_key.as_str()],
242 |row| {
243 Ok((
244 row.get::<_, String>(0)?,
245 row.get::<_, String>(1)?,
246 row.get::<_, Option<String>>(2)?,
247 row.get::<_, Option<String>>(3)?,
248 row.get::<_, i64>(6)?,
249 row.get::<_, Option<i64>>(7)?,
250 ))
251 },
252 )
253 .optional()?;
254
255 let Some((
256 existing_hash,
257 status,
258 outcome_json,
259 error_json,
260 lease_expires_row,
261 existing_due_row,
262 )) = row
263 else {
264 if replay_mode {
265 return Ok(Err(RuntimeEffectControllerError::new(
266 "sqlite_effect_replay_missing",
267 format!(
268 "no recorded runtime effect for scope `{scope_id}` and replay key `{replay_key}`"
269 ),
270 )));
271 }
272 let due_at_param = due_at_ms.map(|value| value as i64);
273 tx.execute(
274 "INSERT INTO runtime_effect_replay (
275 scope_id, replay_key, envelope_hash, status, outcome_json,
276 error_json, lease_owner_id, lease_token, lease_expires_at_ms,
277 due_at_ms, created_at_ms, updated_at_ms
278 )
279 VALUES (?1, ?2, ?3, ?4, NULL, NULL, ?5, ?6, ?7, ?8, ?9, ?10)",
280 params![
281 scope_id.as_str(),
282 replay_key.as_str(),
283 envelope_hash.as_str(),
284 STATUS_IN_PROGRESS,
285 owner_id.as_str(),
286 lease_token.as_str(),
287 lease_expires_at_ms as i64,
288 due_at_param,
289 now as i64,
290 now as i64,
291 ],
292 )?;
293 return Ok(Ok(PreparedEffect::Claimed(ClaimedEffect {
294 scope_id,
295 replay_key,
296 envelope_hash,
297 lease_token,
298 due_at_ms,
299 })));
300 };
301
302 if existing_hash != envelope_hash {
303 return Ok(Err(RuntimeEffectControllerError::new(
304 "sqlite_effect_replay_hash_conflict",
305 format!(
306 "runtime effect replay key `{replay_key}` in scope `{scope_id}` was reused with a different envelope hash"
307 ),
308 )));
309 }
310
311 let lease_expires_at_ms = lease_expires_row as u64;
312 let existing_due_at_ms = existing_due_row.map(|value| value as u64);
313
314 match status.as_str() {
315 STATUS_COMPLETED => {
316 let Some(json) = outcome_json else {
317 return Ok(Err(RuntimeEffectControllerError::new(
318 "sqlite_effect_replay_corrupt_row",
319 "completed runtime effect row is missing outcome_json",
320 )));
321 };
322 let outcome = match serde_json::from_str(&json) {
323 Ok(outcome) => outcome,
324 Err(err) => return Ok(Err(effect_decode_error(err))),
325 };
326 Ok(Ok(PreparedEffect::ReplayOutcome {
327 outcome: Box::new(outcome),
328 due_at_ms: existing_due_at_ms,
329 }))
330 }
331 STATUS_FAILED => {
332 let Some(json) = error_json else {
333 return Ok(Err(RuntimeEffectControllerError::new(
334 "sqlite_effect_replay_corrupt_row",
335 "failed runtime effect row is missing error_json",
336 )));
337 };
338 let err = match serde_json::from_str(&json) {
339 Ok(err) => err,
340 Err(err) => return Ok(Err(effect_decode_error(err))),
341 };
342 Ok(Ok(PreparedEffect::ReplayError(err)))
343 }
344 STATUS_IN_PROGRESS if lease_expires_at_ms > now => Ok(Ok(PreparedEffect::Busy {
345 retry_at_ms: lease_expires_at_ms,
346 })),
347 STATUS_IN_PROGRESS => {
348 let due_at_ms = existing_due_at_ms.or(due_at_ms);
349 let due_at_param = due_at_ms.map(|value| value as i64);
350 tx.execute(
351 "UPDATE runtime_effect_replay
352 SET lease_owner_id = ?3,
353 lease_token = ?4,
354 lease_expires_at_ms = ?5,
355 due_at_ms = ?6,
356 updated_at_ms = ?7
357 WHERE scope_id = ?1 AND replay_key = ?2",
358 params![
359 scope_id.as_str(),
360 replay_key.as_str(),
361 owner_id.as_str(),
362 lease_token.as_str(),
363 current_epoch_ms().saturating_add(lease_ttl_ms) as i64,
364 due_at_param,
365 current_epoch_ms() as i64,
366 ],
367 )?;
368 Ok(Ok(PreparedEffect::Claimed(ClaimedEffect {
369 scope_id,
370 replay_key,
371 envelope_hash,
372 lease_token,
373 due_at_ms,
374 })))
375 }
376 other => Ok(Err(RuntimeEffectControllerError::new(
377 "sqlite_effect_replay_corrupt_row",
378 format!("unknown runtime effect replay status `{other}`"),
379 ))),
380 }
381 })
382 .await
383 .map_err(effect_sqlite_error)?;
384 outcome
385 }
386
387 async fn finalize_effect(
388 &self,
389 claim: &ClaimedEffect,
390 outcome: &Result<RuntimeEffectOutcome, RuntimeEffectControllerError>,
391 ) -> Result<(), RuntimeEffectControllerError> {
392 let (status, outcome_json, error_json) = match outcome {
393 Ok(outcome) => (
394 STATUS_COMPLETED,
395 Some(serde_json::to_string(outcome).map_err(effect_encode_error)?),
396 None,
397 ),
398 Err(err) => (
399 STATUS_FAILED,
400 None,
401 Some(serde_json::to_string(err).map_err(effect_encode_error)?),
402 ),
403 };
404 let now = current_epoch_ms();
405 let scope_id = claim.scope_id.clone();
406 let replay_key = claim.replay_key.clone();
407 let envelope_hash = claim.envelope_hash.clone();
408 let owner_id = self.inner.owner_id.clone();
409 let lease_token = claim.lease_token.clone();
410 let status = status.to_string();
411
412 let result: Result<(), RuntimeEffectControllerError> = self
413 .inner
414 .conn
415 .write(move |tx| {
416 let changed = tx.execute(
417 "UPDATE runtime_effect_replay
418 SET status = ?6,
419 outcome_json = ?7,
420 error_json = ?8,
421 lease_owner_id = NULL,
422 lease_token = NULL,
423 lease_expires_at_ms = 0,
424 updated_at_ms = ?9
425 WHERE scope_id = ?1
426 AND replay_key = ?2
427 AND envelope_hash = ?3
428 AND lease_owner_id = ?4
429 AND lease_token = ?5
430 AND status = 'in_progress'
431 AND lease_expires_at_ms > ?10",
432 params![
433 scope_id.as_str(),
434 replay_key.as_str(),
435 envelope_hash.as_str(),
436 owner_id.as_str(),
437 lease_token.as_str(),
438 status.as_str(),
439 outcome_json,
440 error_json,
441 now as i64,
442 now as i64,
443 ],
444 )?;
445 if changed != 1 {
446 return Ok(Err(RuntimeEffectControllerError::new(
447 "sqlite_effect_replay_lease_lost",
448 format!(
449 "runtime effect replay lease was lost before finalizing scope `{scope_id}` replay key `{replay_key}`"
450 ),
451 )));
452 }
453 Ok(Ok(()))
454 })
455 .await
456 .map_err(effect_sqlite_error)?;
457 result
458 }
459
460 async fn renew_effect_lease(
461 &self,
462 claim: &ClaimedEffect,
463 ) -> Result<(), RuntimeEffectControllerError> {
464 let now = current_epoch_ms();
465 let renewed_expires_at = now.saturating_add(self.inner.lease_ttl_ms);
466 let scope_id = claim.scope_id.clone();
467 let replay_key = claim.replay_key.clone();
468 let envelope_hash = claim.envelope_hash.clone();
469 let owner_id = self.inner.owner_id.clone();
470 let lease_token = claim.lease_token.clone();
471
472 let result: Result<(), RuntimeEffectControllerError> = self
473 .inner
474 .conn
475 .write(move |tx| {
476 let changed = tx.execute(
477 "UPDATE runtime_effect_replay
478 SET lease_expires_at_ms = ?6,
479 updated_at_ms = ?7
480 WHERE scope_id = ?1
481 AND replay_key = ?2
482 AND envelope_hash = ?3
483 AND lease_owner_id = ?4
484 AND lease_token = ?5
485 AND status = 'in_progress'
486 AND lease_expires_at_ms > ?8",
487 params![
488 scope_id.as_str(),
489 replay_key.as_str(),
490 envelope_hash.as_str(),
491 owner_id.as_str(),
492 lease_token.as_str(),
493 renewed_expires_at as i64,
494 now as i64,
495 now as i64,
496 ],
497 )?;
498 if changed != 1 {
499 return Ok(Err(RuntimeEffectControllerError::new(
500 "sqlite_effect_replay_lease_lost",
501 format!(
502 "runtime effect replay lease was lost while executing scope `{scope_id}` replay key `{replay_key}`"
503 ),
504 )));
505 }
506 Ok(Ok(()))
507 })
508 .await
509 .map_err(effect_sqlite_error)?;
510 result
511 }
512
513 async fn execute_claimed_effect_with_renewal(
514 &self,
515 claim: &ClaimedEffect,
516 envelope: RuntimeEffectEnvelope,
517 local_executor: RuntimeEffectLocalExecutor<'_>,
518 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
519 let renew_every = Duration::from_millis((self.inner.lease_ttl_ms / 3).max(1));
520 let effect = self.execute_claimed_effect(claim, envelope, local_executor);
521 tokio::pin!(effect);
522 let renew_sleep = tokio::time::sleep(renew_every);
523 tokio::pin!(renew_sleep);
524
525 loop {
526 tokio::select! {
527 result = &mut effect => return result,
528 _ = &mut renew_sleep => {
529 self.renew_effect_lease(claim).await?;
530 renew_sleep.as_mut().reset(tokio::time::Instant::now() + renew_every);
531 }
532 }
533 }
534 }
535
536 async fn execute_claimed_effect(
537 &self,
538 claim: &ClaimedEffect,
539 envelope: RuntimeEffectEnvelope,
540 local_executor: RuntimeEffectLocalExecutor<'_>,
541 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
542 if matches!(envelope.command, RuntimeEffectCommand::Sleep { .. }) {
543 sleep_until_due(claim.due_at_ms).await;
544 return Ok(RuntimeEffectOutcome::Sleep);
545 }
546 match envelope.command {
547 RuntimeEffectCommand::Process { command } => {
548 let result = self
549 .execute_process_command(*command, local_executor)
550 .await?;
551 Ok(RuntimeEffectOutcome::Process { result })
552 }
553 _ => local_executor.execute(envelope).await,
554 }
555 }
556
557 async fn execute_process_command(
558 &self,
559 command: ProcessCommand,
560 local_executor: RuntimeEffectLocalExecutor<'_>,
561 ) -> Result<ProcessEffectOutcome, RuntimeEffectControllerError> {
562 let execution = local_executor.into_process()?;
563 let registry = execution.registry;
564 match command {
565 ProcessCommand::Start {
566 registration,
567 grant,
568 execution_context: _,
569 } => {
570 let registration_id = registration.id.clone();
571 let record = registry.register_process(registration).await?;
572 if let Some(grant) = grant {
573 registry
574 .grant_handle(&grant.session_scope, ®istration_id, grant.descriptor)
575 .await?;
576 }
577 Ok(ProcessEffectOutcome::Start { record })
578 }
579 ProcessCommand::List {
580 session_scope,
581 mode,
582 } => {
583 let entries = match mode {
584 lash_core::ProcessListMode::Live => {
585 registry.list_live_handle_grants(&session_scope).await?
586 }
587 lash_core::ProcessListMode::All => {
588 registry.list_handle_grants(&session_scope).await?
589 }
590 };
591 Ok(ProcessEffectOutcome::List { entries })
592 }
593 ProcessCommand::Transfer {
594 from_scope,
595 to_scope,
596 process_ids,
597 } => {
598 registry
599 .transfer_handle_grants(&from_scope, &to_scope, &process_ids)
600 .await?;
601 Ok(ProcessEffectOutcome::Transfer)
602 }
603 ProcessCommand::DeleteSession { session_id } => {
604 let report = registry.delete_session_process_state(&session_id).await?;
605 Ok(ProcessEffectOutcome::DeleteSession { report })
606 }
607 ProcessCommand::Await { process_id } => {
608 let output = registry.await_process(&process_id).await?;
609 Ok(ProcessEffectOutcome::Await { output })
610 }
611 ProcessCommand::Cancel { process_id, reason } => {
612 registry
613 .append_event(
614 &process_id,
615 lash_core::ProcessEventAppendRequest::cancel_requested(&process_id, reason),
616 )
617 .await?;
618 let record = registry.get_process(&process_id).await.ok_or_else(|| {
619 PluginError::Session(format!("unknown process `{process_id}`"))
620 })?;
621 Ok(ProcessEffectOutcome::Cancel { record })
622 }
623 ProcessCommand::Signal {
624 process_id,
625 request,
626 ..
627 } => {
628 let result = registry.append_event(&process_id, request).await?;
629 Ok(ProcessEffectOutcome::Signal {
630 event: result.event,
631 })
632 }
633 }
634 }
635}
636
637impl AwaitEventResolver for SqliteRuntimeEffectController {
638 fn durability_tier(&self) -> DurabilityTier {
639 DurabilityTier::Durable
640 }
641
642 fn requires_durable_attachment_store(&self) -> bool {
643 true
644 }
645
646 fn supports_durable_effects(&self) -> bool {
647 true
648 }
649}
650
651#[async_trait::async_trait]
652impl RuntimeEffectController for SqliteRuntimeEffectController {
653 async fn execute_effect(
654 &self,
655 envelope: RuntimeEffectEnvelope,
656 local_executor: RuntimeEffectLocalExecutor<'_>,
657 ) -> Result<RuntimeEffectOutcome, RuntimeEffectControllerError> {
658 loop {
659 match self.prepare_effect(&envelope).await? {
660 PreparedEffect::ReplayOutcome { outcome, due_at_ms } => {
661 sleep_until_due(due_at_ms).await;
662 return Ok(*outcome);
663 }
664 PreparedEffect::ReplayError(err) => return Err(err),
665 PreparedEffect::Claimed(claim) => {
666 let result = self
667 .execute_claimed_effect_with_renewal(&claim, envelope, local_executor)
668 .await;
669 let finalize = self.finalize_effect(&claim, &result).await;
670 return match (result, finalize) {
671 (Ok(outcome), Ok(())) => Ok(outcome),
672 (Err(err), Ok(())) => Err(err),
673 (_, Err(err)) => Err(err),
674 };
675 }
676 PreparedEffect::Busy { retry_at_ms } => {
677 sleep_until_retry(retry_at_ms).await;
678 }
679 }
680 }
681 }
682}
683
684async fn open_effect_replay_inner(
685 path: &Path,
686 backing: StoreBacking,
687 options: SqliteEffectReplayOptions,
688) -> tokio_rusqlite::Result<Arc<SqliteEffectReplayInner>> {
689 let conn = SqliteConnection::open(path).await?;
690 ensure_effect_schema(&conn).await?;
691 apply_pragmas(&conn, backing).await?;
692 Ok(Arc::new(SqliteEffectReplayInner::new(conn, options)))
693}
694
695async fn open_effect_replay_memory_inner(
696 options: SqliteEffectReplayOptions,
697) -> tokio_rusqlite::Result<Arc<SqliteEffectReplayInner>> {
698 let conn = SqliteConnection::open_in_memory().await?;
699 ensure_effect_schema(&conn).await?;
700 apply_pragmas(&conn, StoreBacking::Memory).await?;
701 Ok(Arc::new(SqliteEffectReplayInner::new(conn, options)))
702}
703
704impl SqliteEffectReplayInner {
705 fn new(conn: SqliteConnection, options: SqliteEffectReplayOptions) -> Self {
706 let sequence = EFFECT_OWNER_COUNTER.fetch_add(1, Ordering::SeqCst);
707 Self {
708 conn,
709 owner_id: format!(
710 "pid{}-{sequence}-{}",
711 std::process::id(),
712 current_epoch_ms()
713 ),
714 lease_counter: AtomicU64::new(1),
715 replay_mode: AtomicBool::new(false),
716 lease_ttl_ms: duration_ms(options.lease_ttl),
717 }
718 }
719
720 fn next_lease_token(&self) -> String {
721 let sequence = self.lease_counter.fetch_add(1, Ordering::SeqCst);
722 format!("{}:{sequence}", self.owner_id)
723 }
724}
725
726fn duration_ms(duration: Duration) -> u64 {
727 let millis = duration.as_millis();
728 if millis == 0 {
729 1
730 } else {
731 millis.min(u128::from(u64::MAX)) as u64
732 }
733}
734
735fn sleep_due_at_ms(envelope: &RuntimeEffectEnvelope, now: u64) -> Option<u64> {
736 match envelope.command {
737 RuntimeEffectCommand::Sleep { duration_ms } => Some(now.saturating_add(duration_ms)),
738 _ => None,
739 }
740}
741
742async fn sleep_until_due(due_at_ms: Option<u64>) {
743 let Some(due_at_ms) = due_at_ms else {
744 return;
745 };
746 let now = current_epoch_ms();
747 if due_at_ms > now {
748 tokio::time::sleep(Duration::from_millis(due_at_ms - now)).await;
749 }
750}
751
752async fn sleep_until_retry(retry_at_ms: u64) {
753 let now = current_epoch_ms();
754 let delay = if retry_at_ms > now {
755 Duration::from_millis(retry_at_ms - now).min(BUSY_POLL)
756 } else {
757 BUSY_POLL
758 };
759 tokio::time::sleep(delay).await;
760}
761
762fn effect_sqlite_error(err: rusqlite::Error) -> RuntimeEffectControllerError {
763 RuntimeEffectControllerError::new("sqlite_effect_replay_store", err.to_string())
764}
765
766fn effect_encode_error(err: serde_json::Error) -> RuntimeEffectControllerError {
767 RuntimeEffectControllerError::new(
768 "sqlite_effect_replay_encode",
769 format!("failed to encode runtime effect replay row: {err}"),
770 )
771}
772
773fn effect_decode_error(err: serde_json::Error) -> RuntimeEffectControllerError {
774 RuntimeEffectControllerError::new(
775 "sqlite_effect_replay_decode",
776 format!("failed to decode runtime effect replay row: {err}"),
777 )
778}