runtara_core/persistence/mod.rs
1//! Persistence interfaces and backends for runtara-core.
2//!
3//! This module defines the persistence abstraction and backend implementations.
4
5pub mod postgres;
6pub mod sqlite;
7
8pub use self::postgres::PostgresPersistence;
9pub use self::sqlite::SqlitePersistence;
10
11use crate::error::CoreError;
12
13/// Instance record from the persistence layer.
14#[derive(Debug, Clone, sqlx::FromRow)]
15pub struct InstanceRecord {
16 /// Unique identifier for the instance.
17 pub instance_id: String,
18 /// Tenant identifier for multi-tenancy isolation.
19 pub tenant_id: String,
20 /// Version of the workflow definition.
21 pub definition_version: i32,
22 /// Current status (pending, running, suspended, completed, failed, cancelled).
23 pub status: String,
24 /// Last checkpoint ID if instance was checkpointed.
25 pub checkpoint_id: Option<String>,
26 /// Current attempt number (for retries).
27 pub attempt: i32,
28 /// Maximum allowed attempts before permanent failure.
29 pub max_attempts: i32,
30 /// When the instance was created.
31 pub created_at: DateTime<Utc>,
32 /// When the instance started running.
33 pub started_at: Option<DateTime<Utc>>,
34 /// When the instance finished (completed, failed, or cancelled).
35 pub finished_at: Option<DateTime<Utc>>,
36 /// Input data provided at launch time.
37 #[sqlx(default)]
38 pub input: Option<Vec<u8>>,
39 /// Output data from successful completion.
40 pub output: Option<Vec<u8>>,
41 /// Error message from failure.
42 pub error: Option<String>,
43 /// When a sleeping instance should be woken.
44 pub sleep_until: Option<DateTime<Utc>>,
45 /// How/why the instance reached its terminal state.
46 #[sqlx(default)]
47 pub termination_reason: Option<String>,
48 /// Process exit code if available.
49 #[sqlx(default)]
50 pub exit_code: Option<i32>,
51}
52
53/// Checkpoint record from the persistence layer.
54#[derive(Debug, Clone, sqlx::FromRow)]
55pub struct CheckpointRecord {
56 /// Database primary key.
57 pub id: i64,
58 /// Instance this checkpoint belongs to.
59 pub instance_id: String,
60 /// Unique checkpoint identifier within the instance.
61 pub checkpoint_id: String,
62 /// Serialized state data.
63 pub state: Vec<u8>,
64 /// When the checkpoint was created.
65 pub created_at: DateTime<Utc>,
66 /// Whether this checkpoint is marked for compensation (saga pattern).
67 #[sqlx(default)]
68 pub is_compensatable: bool,
69 /// Step ID to execute for compensation/rollback.
70 #[sqlx(default)]
71 pub compensation_step_id: Option<String>,
72 /// Serialized data for the compensation step.
73 #[sqlx(default)]
74 pub compensation_data: Option<Vec<u8>>,
75 /// Current state of compensation (none, pending, triggered, completed, failed).
76 #[sqlx(default)]
77 pub compensation_state: Option<String>,
78 /// Order in which to execute compensation (higher = compensate first).
79 #[sqlx(default)]
80 pub compensation_order: i32,
81}
82
83/// Event record from the persistence layer.
84#[derive(Debug, Clone, sqlx::FromRow)]
85pub struct EventRecord {
86 /// Database primary key (None when inserting new events).
87 #[sqlx(default)]
88 pub id: Option<i64>,
89 /// Instance this event belongs to.
90 pub instance_id: String,
91 /// Type of event (heartbeat, completed, failed, suspended, custom).
92 pub event_type: String,
93 /// Associated checkpoint ID if applicable.
94 pub checkpoint_id: Option<String>,
95 /// Optional event payload data.
96 pub payload: Option<Vec<u8>>,
97 /// When the event occurred.
98 pub created_at: DateTime<Utc>,
99 /// Arbitrary subtype for custom events.
100 pub subtype: Option<String>,
101}
102
103/// Signal record from the persistence layer.
104#[derive(Debug, Clone, sqlx::FromRow)]
105pub struct SignalRecord {
106 /// Instance this signal is for.
107 pub instance_id: String,
108 /// Type of signal (cancel, pause, resume).
109 pub signal_type: String,
110 /// Optional signal payload data.
111 pub payload: Option<Vec<u8>>,
112 /// When the signal was created.
113 pub created_at: DateTime<Utc>,
114 /// When the signal was acknowledged by the instance.
115 pub acknowledged_at: Option<DateTime<Utc>>,
116}
117
118/// Pending custom signal scoped to a specific checkpoint.
119#[derive(Debug, Clone, sqlx::FromRow)]
120pub struct CustomSignalRecord {
121 /// Instance this signal is for.
122 pub instance_id: String,
123 /// Target checkpoint/wait key.
124 pub checkpoint_id: String,
125 /// Optional payload.
126 pub payload: Option<Vec<u8>>,
127 /// When the signal was created.
128 pub created_at: DateTime<Utc>,
129}
130
131/// Sort order for event queries.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum EventSortOrder {
134 /// Newest events first (default).
135 #[default]
136 Desc,
137 /// Oldest events first.
138 Asc,
139}
140
141/// Filter options for listing events.
142#[derive(Debug, Clone, Default)]
143pub struct ListEventsFilter {
144 /// Filter by event type (e.g., "custom", "started", "completed").
145 pub event_type: Option<String>,
146 /// Filter by subtype (e.g., "step_debug_start", "step_debug_end", "workflow_log").
147 pub subtype: Option<String>,
148 /// Filter events created at or after this time.
149 pub created_after: Option<DateTime<Utc>>,
150 /// Filter events created before this time.
151 pub created_before: Option<DateTime<Utc>>,
152 /// Full-text search in JSON payload content.
153 pub payload_contains: Option<String>,
154 /// Filter by scope_id in the event payload (for hierarchy filtering).
155 /// When set, only events with matching scope_id in their payload are returned.
156 pub scope_id: Option<String>,
157 /// Filter by parent_scope_id in the event payload (for hierarchy filtering).
158 /// When set, only events with matching parent_scope_id in their payload are returned.
159 /// Use this to get direct children of a scope.
160 pub parent_scope_id: Option<String>,
161 /// When true, only return events that have no parent_scope_id (root-level scopes).
162 /// This is useful for getting top-level execution scopes.
163 pub root_scopes_only: bool,
164 /// Sort order for events by created_at.
165 pub sort_order: EventSortOrder,
166}
167
168// ============================================================================
169// Step Summary Types (for paired step_debug_start/end events)
170// ============================================================================
171
172/// Status of a step execution.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum StepStatus {
175 /// Step is currently running (has start event, no end event yet).
176 Running,
177 /// Step completed successfully.
178 Completed,
179 /// Step failed with an error.
180 Failed,
181}
182
183/// Summary of a step execution, pairing step_debug_start and step_debug_end events.
184#[derive(Debug, Clone)]
185pub struct StepSummaryRecord {
186 /// Unique step identifier within the instance.
187 pub step_id: String,
188 /// Human-readable step name.
189 pub step_name: Option<String>,
190 /// Step type (e.g., "Agent", "Conditional", "Split").
191 pub step_type: String,
192 /// Current status of the step.
193 pub status: StepStatus,
194 /// When the step started executing.
195 pub started_at: DateTime<Utc>,
196 /// When the step completed (None if still running).
197 pub completed_at: Option<DateTime<Utc>>,
198 /// Duration in milliseconds (None if still running).
199 pub duration_ms: Option<i64>,
200 /// Step inputs from step_debug_start payload.
201 pub inputs: Option<serde_json::Value>,
202 /// Step outputs from step_debug_end payload.
203 pub outputs: Option<serde_json::Value>,
204 /// Error details from step_debug_end payload (if failed).
205 pub error: Option<serde_json::Value>,
206 /// Scope ID for nested execution contexts (Split/While/StartScenario).
207 pub scope_id: Option<String>,
208 /// Parent scope ID for hierarchy.
209 pub parent_scope_id: Option<String>,
210}
211
212/// Filter options for listing step summaries.
213#[derive(Debug, Clone, Default)]
214pub struct ListStepSummariesFilter {
215 /// Sort order for steps by started_at.
216 pub sort_order: EventSortOrder,
217 /// Filter by step status.
218 pub status: Option<StepStatus>,
219 /// Filter by step type (e.g., "Agent", "Conditional").
220 pub step_type: Option<String>,
221 /// Filter by scope_id (for steps within a specific scope).
222 pub scope_id: Option<String>,
223 /// Filter by parent_scope_id (for direct children of a scope).
224 pub parent_scope_id: Option<String>,
225 /// When true, only return steps with no parent_scope_id (root-level steps).
226 pub root_scopes_only: bool,
227}
228
229/// Error history record for structured error tracking.
230#[derive(Debug, Clone, sqlx::FromRow)]
231pub struct ErrorHistoryRecord {
232 /// Database primary key.
233 pub id: i64,
234 /// Instance this error belongs to.
235 pub instance_id: String,
236 /// Checkpoint ID where error occurred (if applicable).
237 pub checkpoint_id: Option<String>,
238 /// Step ID where error occurred (if applicable).
239 pub step_id: Option<String>,
240 /// Machine-readable error code (e.g., "RATE_LIMITED").
241 pub error_code: String,
242 /// Human-readable error message.
243 pub error_message: String,
244 /// Error category (unknown, transient, permanent, business).
245 pub category: String,
246 /// Error severity (info, warning, error, critical).
247 pub severity: String,
248 /// Retry hint (unknown, retry_immediately, retry_with_backoff, retry_after, do_not_retry).
249 pub retry_hint: Option<String>,
250 /// Milliseconds for retry_after hint.
251 pub retry_after_ms: Option<i64>,
252 /// Additional context as JSON.
253 pub attributes: Option<serde_json::Value>,
254 /// Cause error ID for error chains.
255 pub cause_error_id: Option<i64>,
256 /// When the error was recorded.
257 pub created_at: DateTime<Utc>,
258}
259
260/// Compensation log record for audit trail.
261#[derive(Debug, Clone, sqlx::FromRow)]
262pub struct CompensationLogRecord {
263 /// Database primary key.
264 pub id: i64,
265 /// Instance this compensation is for.
266 pub instance_id: String,
267 /// Checkpoint being compensated.
268 pub checkpoint_id: String,
269 /// Step executed for compensation.
270 pub compensation_step_id: String,
271 /// Attempt number (for retries).
272 pub attempt_number: i32,
273 /// When compensation started.
274 pub started_at: DateTime<Utc>,
275 /// When compensation finished (None if still running).
276 pub finished_at: Option<DateTime<Utc>>,
277 /// Whether compensation succeeded.
278 pub success: Option<bool>,
279 /// Error message if compensation failed.
280 pub error_message: Option<String>,
281 /// Reference to error_history entry.
282 pub error_id: Option<i64>,
283}
284
285/// Wake queue entry from the persistence layer.
286#[derive(Debug, Clone, sqlx::FromRow)]
287pub struct WakeEntry {
288 /// Database primary key.
289 pub id: i64,
290 /// Instance to wake.
291 pub instance_id: String,
292 /// Checkpoint to resume from.
293 pub checkpoint_id: String,
294 /// When to wake the instance.
295 pub wake_at: DateTime<Utc>,
296 /// When this wake entry was created.
297 pub created_at: DateTime<Utc>,
298}
299use async_trait::async_trait;
300use chrono::{DateTime, Utc};
301
302/// Persistence interface used by core handlers.
303#[allow(missing_docs)]
304#[async_trait]
305pub trait Persistence: Send + Sync {
306 async fn register_instance(&self, instance_id: &str, tenant_id: &str) -> Result<(), CoreError>;
307
308 async fn get_instance(&self, instance_id: &str) -> Result<Option<InstanceRecord>, CoreError>;
309
310 async fn update_instance_status(
311 &self,
312 instance_id: &str,
313 status: &str,
314 started_at: Option<DateTime<Utc>>,
315 ) -> Result<(), CoreError>;
316
317 async fn update_instance_checkpoint(
318 &self,
319 instance_id: &str,
320 checkpoint_id: &str,
321 ) -> Result<(), CoreError>;
322
323 async fn complete_instance(
324 &self,
325 instance_id: &str,
326 output: Option<&[u8]>,
327 error: Option<&str>,
328 ) -> Result<(), CoreError>;
329
330 /// Complete an instance with extended fields (stderr, checkpoint).
331 ///
332 /// This is an extended version of `complete_instance` that supports additional
333 /// fields needed by the Environment layer. The default implementation calls
334 /// the basic `complete_instance` and ignores the extra fields.
335 ///
336 /// Environment implementations should override this to store stderr and checkpoint.
337 async fn complete_instance_extended(
338 &self,
339 instance_id: &str,
340 status: &str,
341 output: Option<&[u8]>,
342 error: Option<&str>,
343 _stderr: Option<&str>,
344 _checkpoint_id: Option<&str>,
345 ) -> Result<(), CoreError> {
346 // Default: delegate to basic complete_instance, ignoring stderr/checkpoint
347 let error_for_basic = if status == "failed" { error } else { None };
348 let output_for_basic = if status == "completed" { output } else { None };
349 self.complete_instance(instance_id, output_for_basic, error_for_basic)
350 .await
351 }
352
353 /// Complete an instance only if its current status is 'running'.
354 ///
355 /// This prevents race conditions where both Core (via SDK) and Environment
356 /// (via container monitor) try to complete the same instance. Returns true
357 /// if the update was applied, false if skipped.
358 ///
359 /// Default implementation always applies the update (no guard).
360 async fn complete_instance_if_running(
361 &self,
362 instance_id: &str,
363 status: &str,
364 output: Option<&[u8]>,
365 error: Option<&str>,
366 stderr: Option<&str>,
367 checkpoint_id: Option<&str>,
368 ) -> Result<bool, CoreError> {
369 // Default: always apply (returns true)
370 self.complete_instance_extended(instance_id, status, output, error, stderr, checkpoint_id)
371 .await?;
372 Ok(true)
373 }
374
375 /// Complete an instance with termination tracking metadata.
376 ///
377 /// This version includes `termination_reason` and `exit_code` for unambiguous
378 /// identification of how/why the instance terminated.
379 ///
380 /// Default implementation delegates to `complete_instance_extended`, ignoring
381 /// the termination fields.
382 #[allow(clippy::too_many_arguments)]
383 async fn complete_instance_with_termination(
384 &self,
385 instance_id: &str,
386 status: &str,
387 termination_reason: Option<&str>,
388 exit_code: Option<i32>,
389 output: Option<&[u8]>,
390 error: Option<&str>,
391 stderr: Option<&str>,
392 checkpoint_id: Option<&str>,
393 ) -> Result<(), CoreError> {
394 // Default: delegate to extended, ignoring termination fields
395 let _ = termination_reason;
396 let _ = exit_code;
397 self.complete_instance_extended(instance_id, status, output, error, stderr, checkpoint_id)
398 .await
399 }
400
401 /// Complete an instance with termination tracking, only if status is 'running'.
402 ///
403 /// Returns true if the update was applied, false if skipped (instance already
404 /// has a terminal status).
405 #[allow(clippy::too_many_arguments)]
406 async fn complete_instance_with_termination_if_running(
407 &self,
408 instance_id: &str,
409 status: &str,
410 termination_reason: Option<&str>,
411 exit_code: Option<i32>,
412 output: Option<&[u8]>,
413 error: Option<&str>,
414 stderr: Option<&str>,
415 checkpoint_id: Option<&str>,
416 ) -> Result<bool, CoreError> {
417 // Default: always apply (returns true)
418 self.complete_instance_with_termination(
419 instance_id,
420 status,
421 termination_reason,
422 exit_code,
423 output,
424 error,
425 stderr,
426 checkpoint_id,
427 )
428 .await?;
429 Ok(true)
430 }
431
432 /// Update execution metrics for an instance (memory, CPU usage).
433 ///
434 /// This is an environment-specific operation for storing cgroup metrics.
435 /// Core implementations can ignore this (default is no-op).
436 async fn update_instance_metrics(
437 &self,
438 _instance_id: &str,
439 _memory_peak_bytes: Option<u64>,
440 _cpu_usage_usec: Option<u64>,
441 ) -> Result<(), CoreError> {
442 // Default: no-op (Core doesn't track metrics)
443 Ok(())
444 }
445
446 /// Update instance stderr output.
447 ///
448 /// This is an environment-specific operation for storing container stderr.
449 /// Core implementations can ignore this (default is no-op).
450 async fn update_instance_stderr(
451 &self,
452 _instance_id: &str,
453 _stderr: &str,
454 ) -> Result<(), CoreError> {
455 // Default: no-op (Core doesn't track stderr)
456 Ok(())
457 }
458
459 /// Store input data for an instance.
460 ///
461 /// This is an environment-specific operation for storing instance input.
462 /// Core implementations can ignore this (default is no-op).
463 async fn store_instance_input(
464 &self,
465 _instance_id: &str,
466 _input: &[u8],
467 ) -> Result<(), CoreError> {
468 // Default: no-op (Core doesn't store input)
469 Ok(())
470 }
471
472 async fn save_checkpoint(
473 &self,
474 instance_id: &str,
475 checkpoint_id: &str,
476 state: &[u8],
477 ) -> Result<(), CoreError>;
478
479 async fn load_checkpoint(
480 &self,
481 instance_id: &str,
482 checkpoint_id: &str,
483 ) -> Result<Option<CheckpointRecord>, CoreError>;
484
485 async fn list_checkpoints(
486 &self,
487 instance_id: &str,
488 checkpoint_id: Option<&str>,
489 limit: i64,
490 offset: i64,
491 created_after: Option<DateTime<Utc>>,
492 created_before: Option<DateTime<Utc>>,
493 ) -> Result<Vec<CheckpointRecord>, CoreError>;
494
495 async fn count_checkpoints(
496 &self,
497 instance_id: &str,
498 checkpoint_id: Option<&str>,
499 created_after: Option<DateTime<Utc>>,
500 created_before: Option<DateTime<Utc>>,
501 ) -> Result<i64, CoreError>;
502
503 async fn insert_event(&self, event: &EventRecord) -> Result<(), CoreError>;
504
505 async fn insert_signal(
506 &self,
507 instance_id: &str,
508 signal_type: &str,
509 payload: &[u8],
510 ) -> Result<(), CoreError>;
511
512 async fn get_pending_signal(
513 &self,
514 instance_id: &str,
515 ) -> Result<Option<SignalRecord>, CoreError>;
516
517 async fn acknowledge_signal(&self, instance_id: &str) -> Result<(), CoreError>;
518
519 async fn insert_custom_signal(
520 &self,
521 instance_id: &str,
522 checkpoint_id: &str,
523 payload: &[u8],
524 ) -> Result<(), CoreError>;
525
526 async fn take_pending_custom_signal(
527 &self,
528 instance_id: &str,
529 checkpoint_id: &str,
530 ) -> Result<Option<CustomSignalRecord>, CoreError>;
531
532 async fn save_retry_attempt(
533 &self,
534 instance_id: &str,
535 checkpoint_id: &str,
536 attempt: i32,
537 error_message: Option<&str>,
538 ) -> Result<(), CoreError>;
539
540 async fn list_instances(
541 &self,
542 tenant_id: Option<&str>,
543 status: Option<&str>,
544 limit: i64,
545 offset: i64,
546 ) -> Result<Vec<InstanceRecord>, CoreError>;
547
548 async fn health_check_db(&self) -> Result<bool, CoreError>;
549
550 async fn count_active_instances(&self) -> Result<i64, CoreError>;
551
552 /// Set the sleep_until timestamp for an instance.
553 async fn set_instance_sleep(
554 &self,
555 instance_id: &str,
556 sleep_until: DateTime<Utc>,
557 ) -> Result<(), CoreError>;
558
559 /// Clear the sleep_until timestamp for an instance.
560 async fn clear_instance_sleep(&self, instance_id: &str) -> Result<(), CoreError>;
561
562 /// Get instances that are due to wake (sleep_until <= now).
563 async fn get_sleeping_instances_due(
564 &self,
565 limit: i64,
566 ) -> Result<Vec<InstanceRecord>, CoreError>;
567
568 /// List events for an instance with filtering and pagination.
569 ///
570 /// Events are returned in reverse chronological order (newest first).
571 async fn list_events(
572 &self,
573 instance_id: &str,
574 filter: &ListEventsFilter,
575 limit: i64,
576 offset: i64,
577 ) -> Result<Vec<EventRecord>, CoreError>;
578
579 /// Count events for an instance with filtering.
580 async fn count_events(
581 &self,
582 instance_id: &str,
583 filter: &ListEventsFilter,
584 ) -> Result<i64, CoreError>;
585
586 // ========================================================================
587 // Step Summaries (paired step_debug_start/end events)
588 // ========================================================================
589
590 /// List step summaries for an instance, pairing step_debug_start and step_debug_end events.
591 ///
592 /// Returns unified step execution records with status, timing, inputs/outputs.
593 /// Steps are matched by step_id within the same scope context.
594 async fn list_step_summaries(
595 &self,
596 instance_id: &str,
597 filter: &ListStepSummariesFilter,
598 limit: i64,
599 offset: i64,
600 ) -> Result<Vec<StepSummaryRecord>, CoreError>;
601
602 /// Count step summaries for an instance with filtering.
603 async fn count_step_summaries(
604 &self,
605 instance_id: &str,
606 filter: &ListStepSummariesFilter,
607 ) -> Result<i64, CoreError>;
608
609 // ========================================================================
610 // Structured Error Tracking (optional - default implementations no-op)
611 // ========================================================================
612
613 /// Record a structured error in the error history table.
614 ///
615 /// Returns the error ID for chaining or reference.
616 #[allow(clippy::too_many_arguments)]
617 async fn record_error(
618 &self,
619 _instance_id: &str,
620 _checkpoint_id: Option<&str>,
621 _step_id: Option<&str>,
622 _error_code: &str,
623 _error_message: &str,
624 _category: &str,
625 _severity: &str,
626 _retry_hint: Option<&str>,
627 _retry_after_ms: Option<i64>,
628 _attributes: Option<&serde_json::Value>,
629 _cause_error_id: Option<i64>,
630 ) -> Result<i64, CoreError> {
631 // Default: no-op, return 0
632 Ok(0)
633 }
634
635 /// Get the most recent error for an instance.
636 async fn get_last_error(
637 &self,
638 _instance_id: &str,
639 ) -> Result<Option<ErrorHistoryRecord>, CoreError> {
640 // Default: no-op
641 Ok(None)
642 }
643
644 /// List errors for an instance.
645 async fn list_errors(
646 &self,
647 _instance_id: &str,
648 _limit: i64,
649 _offset: i64,
650 ) -> Result<Vec<ErrorHistoryRecord>, CoreError> {
651 // Default: empty list
652 Ok(vec![])
653 }
654
655 // ========================================================================
656 // Compensation Framework (optional - default implementations no-op)
657 // ========================================================================
658
659 /// Mark a checkpoint as compensatable (for saga pattern).
660 async fn register_compensatable_checkpoint(
661 &self,
662 _instance_id: &str,
663 _checkpoint_id: &str,
664 _compensation_step_id: &str,
665 _compensation_data: Option<&[u8]>,
666 _compensation_order: i32,
667 ) -> Result<(), CoreError> {
668 // Default: no-op
669 Ok(())
670 }
671
672 /// Get all compensatable checkpoints for an instance (in reverse order).
673 async fn get_compensatable_checkpoints(
674 &self,
675 _instance_id: &str,
676 ) -> Result<Vec<CheckpointRecord>, CoreError> {
677 // Default: empty list
678 Ok(vec![])
679 }
680
681 /// Update the compensation state of a checkpoint.
682 async fn set_checkpoint_compensation_state(
683 &self,
684 _instance_id: &str,
685 _checkpoint_id: &str,
686 _state: &str,
687 ) -> Result<(), CoreError> {
688 // Default: no-op
689 Ok(())
690 }
691
692 /// Update the instance-level compensation state.
693 async fn set_instance_compensation_state(
694 &self,
695 _instance_id: &str,
696 _state: &str,
697 _reason: Option<&str>,
698 ) -> Result<(), CoreError> {
699 // Default: no-op
700 Ok(())
701 }
702
703 /// Log a compensation attempt.
704 async fn log_compensation_attempt(
705 &self,
706 _instance_id: &str,
707 _checkpoint_id: &str,
708 _compensation_step_id: &str,
709 _success: bool,
710 _error_message: Option<&str>,
711 _error_id: Option<i64>,
712 ) -> Result<(), CoreError> {
713 // Default: no-op
714 Ok(())
715 }
716
717 /// Count pending compensations for an instance.
718 async fn count_pending_compensations(&self, _instance_id: &str) -> Result<i64, CoreError> {
719 // Default: 0
720 Ok(0)
721 }
722
723 /// Check if all compensations for an instance succeeded.
724 async fn all_compensations_succeeded(&self, _instance_id: &str) -> Result<bool, CoreError> {
725 // Default: true (no compensations = all succeeded)
726 Ok(true)
727 }
728
729 // ========================================================================
730 // Data Retention / Cleanup (optional - default implementations no-op)
731 // ========================================================================
732
733 /// Get terminal instance IDs older than the specified timestamp.
734 ///
735 /// Only returns instances with terminal status: completed, failed, cancelled.
736 /// Returns instance IDs ordered by finished_at (oldest first) for batch processing.
737 async fn get_terminal_instances_older_than(
738 &self,
739 _older_than: DateTime<Utc>,
740 _limit: i64,
741 ) -> Result<Vec<String>, CoreError> {
742 // Default: empty list (no cleanup supported)
743 Ok(vec![])
744 }
745
746 /// Delete instances by their IDs.
747 ///
748 /// This deletes from the instances table; child tables with ON DELETE CASCADE
749 /// (checkpoints, events, signals, etc.) are automatically cleaned up.
750 ///
751 /// Environment implementations should override this to clean up environment-specific
752 /// tables (container_registry, container_status, etc.) before calling the parent.
753 ///
754 /// Returns the count of deleted instances.
755 async fn delete_instances_batch(&self, _instance_ids: &[String]) -> Result<u64, CoreError> {
756 // Default: no-op (no deletion supported)
757 Ok(0)
758 }
759}