Skip to main content

runledger_postgres/jobs/
types.rs

1use chrono::{DateTime, Utc};
2use runledger_core::jobs::{
3    JobEventType, JobFailure, JobFailureKind, JobStage, JobStatus, JobType, JobTypeName,
4};
5use serde_json::Value;
6use sqlx::types::Uuid;
7
8/// Maximum accepted schedule jitter, in seconds.
9///
10/// The scheduler treats jitter as a deterministic spread applied to future fire
11/// cursors, and the persistence layer rejects larger values.
12pub const JOB_SCHEDULE_MAX_JITTER_SECONDS: i32 = 86_400;
13
14/// Maximum page size accepted by public job and workflow list APIs.
15///
16/// This bounds accidental unbounded reads from admin/TUI surfaces while still
17/// allowing operators to inspect a large page when needed.
18pub const JOB_LIST_PAGE_LIMIT_MAX: i64 = 1_000;
19
20#[derive(Debug, Clone)]
21pub struct JobDefinitionUpsert<'a> {
22    pub job_type: JobType<'a>,
23    pub version: i32,
24    pub max_attempts: i32,
25    pub default_timeout_seconds: i32,
26    pub default_priority: i32,
27    pub is_enabled: bool,
28}
29
30#[derive(Debug, Clone)]
31pub struct JobDefinitionRecord {
32    pub job_type: JobTypeName,
33    pub version: i32,
34    pub max_attempts: i32,
35    pub default_timeout_seconds: i32,
36    pub default_priority: i32,
37    pub is_enabled: bool,
38    pub created_at: DateTime<Utc>,
39    pub updated_at: DateTime<Utc>,
40}
41
42/// Schedule row that blocks a job-definition catalog sync.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct JobScheduleJobTypeReference {
45    /// Active schedule name.
46    pub schedule_name: String,
47    /// Job type referenced by the active schedule.
48    pub job_type: JobTypeName,
49}
50
51#[derive(Debug, Clone)]
52pub struct JobDefinitionListFilter<'a> {
53    /// Admin list query input used for escaped `ILIKE` substring matching, not a canonical
54    /// persisted identifier boundary.
55    pub job_type: Option<&'a str>,
56    pub limit: i64,
57    pub offset: i64,
58}
59
60#[derive(Debug, Clone)]
61pub struct JobDefinitionUpdate {
62    pub max_attempts: Option<i32>,
63    pub default_timeout_seconds: Option<i32>,
64    pub default_priority: Option<i32>,
65    pub is_enabled: Option<bool>,
66}
67
68#[derive(Debug, Clone)]
69pub struct JobRuntimeConfigUpsert<'a> {
70    pub job_type: JobType<'a>,
71    pub schema_version: i32,
72    pub config: &'a Value,
73    pub updated_by_user_id: Option<Uuid>,
74}
75
76#[derive(Debug, Clone)]
77pub struct JobRuntimeConfigRecord {
78    pub job_type: JobTypeName,
79    pub schema_version: i32,
80    pub config: Value,
81    pub updated_by_user_id: Option<Uuid>,
82    pub created_at: DateTime<Utc>,
83    pub updated_at: DateTime<Utc>,
84}
85
86#[derive(Debug, Clone)]
87pub struct JobEnqueue<'a> {
88    pub job_type: JobType<'a>,
89    pub organization_id: Option<Uuid>,
90    pub payload: &'a Value,
91    pub priority: Option<i32>,
92    pub max_attempts: Option<i32>,
93    pub timeout_seconds: Option<i32>,
94    /// For keyed enqueues, this value is part of the stored idempotency request
95    /// snapshot. Retries must pass the same scheduled time as the original
96    /// enqueue instead of recomputing a fresh timestamp.
97    pub next_run_at: Option<DateTime<Utc>>,
98    pub idempotency_key: Option<&'a str>,
99    pub stage: Option<JobStage>,
100}
101
102#[derive(Debug, Clone)]
103pub struct JobScheduleRecord {
104    /// Stable schedule row identifier.
105    pub id: Uuid,
106    /// Unique schedule name.
107    pub name: String,
108    /// Job type enqueued whenever the schedule fires.
109    pub job_type: JobTypeName,
110    /// Optional organization scope copied into jobs created by this schedule.
111    pub organization_id: Option<Uuid>,
112    /// JSON payload template copied into each scheduled job before runtime
113    /// schedule metadata is merged.
114    pub payload_template: Value,
115    /// UTC cron expression used by the runtime scheduler.
116    pub cron_expr: String,
117    /// Whether the runtime scheduler may claim this schedule.
118    ///
119    /// Schedule upserts preserve this value for existing rows; use
120    /// `set_job_schedule_active` to pause or resume a schedule intentionally.
121    pub is_active: bool,
122    /// Maximum deterministic jitter, in seconds, applied when computing the next
123    /// fire cursor. Must not exceed [`JOB_SCHEDULE_MAX_JITTER_SECONDS`].
124    pub max_jitter_seconds: i32,
125    /// Next UTC instant at which this schedule is due for materialization.
126    pub next_fire_at: DateTime<Utc>,
127}
128
129/// Input for creating or updating a cron-backed job schedule.
130///
131/// Schedules are keyed by `name`. Updating an existing schedule refreshes the
132/// stored job type, payload template, cron expression, and jitter, while leaving
133/// scheduler-managed state intact. `organization_id` and `is_active` apply only
134/// when a new schedule row is inserted. `next_fire_at` applies on insert and
135/// when the cron expression changes.
136///
137/// Cron expressions are interpreted in UTC and must be accepted by
138/// `cron::Schedule::from_str`, the same parser used by `runledger-runtime` when
139/// materializing due schedules. The upsert validator rejects blank or padded
140/// schedule names, blank or padded cron expressions, invalid cron expressions,
141/// negative jitter, and jitter above [`JOB_SCHEDULE_MAX_JITTER_SECONDS`].
142///
143/// This input does not encode a compile-time job catalog. The PostgreSQL schema
144/// requires a matching job-definition row for `job_type`, but this API does not
145/// prove that a worker process has registered a runtime handler for that job
146/// type.
147#[derive(Debug, Clone)]
148pub struct JobScheduleUpsert<'a> {
149    /// Stable unique schedule name without surrounding whitespace.
150    pub name: &'a str,
151    /// Job type to enqueue whenever the schedule fires.
152    pub job_type: JobType<'a>,
153    /// Optional organization scope for enqueued jobs on first insert.
154    pub organization_id: Option<Uuid>,
155    /// JSON payload copied into each job created by the scheduler.
156    pub payload_template: &'a Value,
157    /// UTC cron expression without surrounding whitespace, validated on upsert
158    /// and parsed again when the schedule fires.
159    pub cron_expr: &'a str,
160    /// Whether the runtime scheduler should claim this schedule on first insert.
161    pub is_active: bool,
162    /// Initial fire cursor for the scheduler, also used when changing cron syntax.
163    pub next_fire_at: DateTime<Utc>,
164    /// Maximum deterministic jitter applied when materializing a due schedule,
165    /// capped at [`JOB_SCHEDULE_MAX_JITTER_SECONDS`].
166    pub max_jitter_seconds: i32,
167}
168
169/// One catalog-owned schedule sync entry.
170#[derive(Debug, Clone)]
171pub struct JobScheduleCatalogSyncEntry<'a> {
172    /// Schedule definition fields to upsert. Unlike plain schedule upserts,
173    /// catalog sync treats `is_active` as the authoritative desired active state
174    /// for both inserts and conflicts.
175    pub upsert: JobScheduleUpsert<'a>,
176}
177
178/// Result returned by [`super::schedules::sync_catalog_job_schedules_tx`].
179#[non_exhaustive]
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct JobScheduleCatalogSyncReport {
182    /// Schedule names upserted and active state applied during this sync.
183    pub synced_schedule_names: Vec<String>,
184}
185
186#[derive(Debug, Clone)]
187pub struct JobQueueRecord {
188    pub id: Uuid,
189    pub job_type: JobTypeName,
190    pub organization_id: Option<Uuid>,
191    pub payload: Value,
192    pub status: JobStatus,
193    pub priority: i32,
194    pub run_number: i32,
195    pub attempt: i32,
196    pub max_attempts: i32,
197    pub timeout_seconds: i32,
198    pub next_run_at: DateTime<Utc>,
199    pub lease_expires_at: Option<DateTime<Utc>>,
200    pub last_heartbeat_at: Option<DateTime<Utc>>,
201    pub worker_id: Option<String>,
202    pub started_at: Option<DateTime<Utc>>,
203    pub finished_at: Option<DateTime<Utc>>,
204    pub stage: JobStage,
205    pub progress_done: Option<i64>,
206    pub progress_total: Option<i64>,
207    pub progress_pct: Option<f64>,
208    pub checkpoint: Option<Value>,
209    pub output: Option<Value>,
210    pub idempotency_key: Option<String>,
211    pub status_reason: Option<String>,
212    pub last_error_code: Option<String>,
213    pub last_error_message: Option<String>,
214    pub created_at: DateTime<Utc>,
215    pub updated_at: DateTime<Utc>,
216}
217
218#[derive(Debug, Clone)]
219pub struct JobEventRecord {
220    pub id: i64,
221    pub job_id: Uuid,
222    pub run_number: i32,
223    pub attempt: Option<i32>,
224    pub event_type: JobEventType,
225    pub stage: Option<JobStage>,
226    pub progress_done: Option<i64>,
227    pub progress_total: Option<i64>,
228    pub payload: Value,
229    pub occurred_at: DateTime<Utc>,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct ReapedTerminalLeaseRecord {
234    pub job_id: Uuid,
235    pub job_type: JobTypeName,
236    pub organization_id: Option<Uuid>,
237    pub run_number: i32,
238    pub attempt: i32,
239    pub payload: Value,
240}
241
242#[non_exhaustive]
243#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum ReapedLeaseDisposition {
245    ReleasedToPending,
246    RetryScheduled {
247        retry_delay_ms: i32,
248        next_run_at: DateTime<Utc>,
249    },
250    DeadLetteredTerminal {
251        payload: Value,
252    },
253}
254
255#[non_exhaustive]
256#[derive(Debug, Clone)]
257pub struct ReapedLeaseRecord {
258    pub job_id: Uuid,
259    pub job_type: JobTypeName,
260    pub organization_id: Option<Uuid>,
261    pub run_number: i32,
262    pub attempt: i32,
263    pub max_attempts: i32,
264    pub worker_id: Option<String>,
265    pub started_without_renewal_heartbeat: bool,
266    pub failure: JobFailure,
267    pub disposition: ReapedLeaseDisposition,
268}
269
270#[non_exhaustive]
271#[derive(Debug, Clone)]
272pub struct ReapExpiredLeaseDeferredError {
273    pub job_id: Uuid,
274    pub run_number: i32,
275    pub attempt: i32,
276    pub error_code: String,
277    pub error_message: String,
278    pub sqlstate: Option<String>,
279}
280
281#[derive(Debug, Clone)]
282pub struct ReapExpiredLeasesResult {
283    pub processed: i64,
284    pub terminal_dead_lettered: Vec<ReapedTerminalLeaseRecord>,
285}
286
287#[non_exhaustive]
288#[derive(Debug, Clone)]
289pub struct ReapExpiredLeasesDetailedResult {
290    pub summary: ReapExpiredLeasesResult,
291    pub reaped_leases: Vec<ReapedLeaseRecord>,
292    pub deferred_row_error_count: usize,
293    pub deferred_row_errors: Vec<ReapExpiredLeaseDeferredError>,
294}
295
296#[derive(Debug, Clone)]
297pub struct JobMetricsRecord {
298    pub job_type: JobTypeName,
299    pub pending_count: i64,
300    pub leased_count: i64,
301    pub stale_leases: i64,
302    pub succeeded_24h: i64,
303    pub retryable_24h: i64,
304    pub terminal_24h: i64,
305    pub panicked_24h: i64,
306    pub timeout_24h: i64,
307    pub dead_lettered_24h: i64,
308    pub p50_duration_ms_24h: Option<f64>,
309    pub p95_duration_ms_24h: Option<f64>,
310}
311
312#[derive(Debug, Clone)]
313pub struct JobLogRecord {
314    pub id: i64,
315    pub job_id: Uuid,
316    pub run_number: i32,
317    pub attempt: Option<i32>,
318    pub level: String,
319    pub message: String,
320    pub payload: Value,
321    pub occurred_at: DateTime<Utc>,
322}
323
324#[derive(Debug, Clone)]
325pub struct JobLogRecordInput {
326    pub job_id: Uuid,
327    pub run_number: i32,
328    pub attempt: Option<i32>,
329    pub level: String,
330    pub message: String,
331    pub payload: Value,
332}
333
334#[derive(Debug, Clone)]
335pub struct JobProgressUpdate<'a> {
336    pub stage: Option<JobStage>,
337    pub progress_done: Option<i64>,
338    pub progress_total: Option<i64>,
339    pub checkpoint: Option<&'a Value>,
340}
341
342#[derive(Debug, Clone)]
343pub struct JobCompletionUpdate<'a> {
344    pub progress_done: Option<i64>,
345    pub progress_total: Option<i64>,
346    pub checkpoint: Option<&'a Value>,
347    pub output: Option<&'a Value>,
348}
349
350#[non_exhaustive]
351#[derive(Debug, Clone)]
352pub struct JobSuccessCompletionOutcome {
353    pub job_id: Uuid,
354    pub job_type: JobTypeName,
355    pub organization_id: Option<Uuid>,
356    pub run_number: i32,
357    pub attempt: i32,
358    pub max_attempts: i32,
359    pub progress_done: Option<i64>,
360    pub progress_total: Option<i64>,
361}
362
363#[derive(Debug, Clone)]
364pub struct JobFailureUpdate<'a> {
365    pub kind: JobFailureKind,
366    pub code: &'a str,
367    pub message: &'a str,
368    pub retry_delay_ms: Option<i32>,
369}
370
371#[non_exhaustive]
372#[derive(Debug, Clone, PartialEq, Eq)]
373pub enum JobFailureCompletionDisposition {
374    RetryScheduled {
375        retry_delay_ms: i32,
376        next_run_at: DateTime<Utc>,
377    },
378    DeadLettered {
379        reason: runledger_core::jobs::JobDeadLetterReason,
380    },
381}
382
383#[non_exhaustive]
384#[derive(Debug, Clone)]
385pub struct JobFailureCompletionOutcome {
386    pub job_id: Uuid,
387    pub job_type: JobTypeName,
388    pub organization_id: Option<Uuid>,
389    pub run_number: i32,
390    pub attempt: i32,
391    pub max_attempts: i32,
392    pub failure_kind: JobFailureKind,
393    pub failure_code: String,
394    pub failure_message: String,
395    pub disposition: JobFailureCompletionDisposition,
396}
397
398#[derive(Debug, Clone)]
399pub struct JobListFilter<'a> {
400    pub organization_id: Option<Uuid>,
401    pub status: Option<JobStatus>,
402    /// Admin list query input used for `ILIKE` substring matching, not a canonical persisted
403    /// identifier boundary.
404    pub job_type: Option<&'a str>,
405    pub limit: i64,
406    pub offset: i64,
407}
408
409#[derive(Debug, Clone)]
410pub struct JobRuntimeConfigListFilter<'a> {
411    /// Admin query filter string used for listing/runtime-config lookup filters, not a canonical
412    /// persisted identifier boundary.
413    pub job_type: Option<&'a str>,
414    pub limit: i64,
415    pub offset: i64,
416}