oxide_batch/lib.rs
1//! The public facade for `OxideBatch`.
2//!
3//! `OxideBatch` models a batch job as a named definition launched with typed
4//! parameters. Identifying parameters select a logical [`JobInstance`], while
5//! each launch or restart receives a distinct [`JobExecutionId`].
6//!
7//! ```
8//! use oxide_batch::{
9//! JobInstanceKey, JobName, JobParameter, JobParameters, ParameterName,
10//! ParameterRole, ParameterValue,
11//! };
12//!
13//! let mut parameters = JobParameters::new();
14//! parameters.insert(
15//! ParameterName::new("business_date")?,
16//! JobParameter::new(
17//! ParameterValue::string("2026-07-29")?,
18//! ParameterRole::Identifying,
19//! ),
20//! )?;
21//!
22//! let key = JobInstanceKey::new(JobName::new("daily_import")?, ¶meters);
23//! assert_eq!(key.identifying_parameter_count(), 1);
24//! # Ok::<(), oxide_batch::DomainError>(())
25//! ```
26//!
27//! Parameter values are sensitive by default. Their [`Debug`](std::fmt::Debug)
28//! representations expose the value kind, but not the underlying value.
29//!
30//! Repository operations run inside an explicit [`RepositoryUnitOfWork`].
31//! Time and identifiers are injected into the reference
32//! [`InMemoryJobRepository`], so tests do not depend on wall-clock time or
33//! process-global identifier state. A unit of work publishes changes only
34//! after [`RepositoryUnitOfWork::commit`] succeeds; dropping it rolls back its
35//! staged metadata.
36//!
37//! [`JobLauncher`] runs one-step [`TaskletJob`] definitions on an
38//! application-owned executor. Tasklets borrow their call-scoped
39//! [`TaskletContext`], receive cooperative [`StopToken`] state, and persist
40//! completed, failed, panicked, or stopped outcomes through the repository.
41//! Synchronous bodies use [`BlockingTaskletAdapter`] with an explicit nonzero
42//! concurrency bound.
43//!
44//! [`JobExecutionListener`] and [`StepExecutionListener`] callbacks nest around
45//! tasklet work with deterministic ordering. [`LifecycleEvent`] values are
46//! emitted after corresponding metadata commits through a non-authoritative
47//! [`LifecycleEventSink`]. Their structured fields exclude parameters,
48//! contexts, records, credentials, and arbitrary user error payloads.
49//!
50//! M2 chunk definitions use [`ChunkStep`], [`ChunkJob`], [`ItemReader`],
51//! [`ItemProcessor`], [`ItemWriter`], and [`ChunkCompletion`]. The
52//! [`JobLauncher::launch_chunk`] path reuses job/step lifecycle metadata while
53//! [`ChunkTransaction`] isolates adapter-owned commit and rollback. End of
54//! input, filtering, cooperative stopping, failures, unknown commit, and
55//! post-commit acknowledgement remain distinct typed outcomes. An enlisted
56//! writer borrows [`BusinessTransaction`] for only its call; no database-driver
57//! type crosses the facade.
58//!
59//! M3 adds runtime-neutral fault-tolerance values. [`FaultPolicy`] combines a
60//! [`FaultClassifier`] over stable [`FaultPhase`] and [`FailureCategory`]
61//! inputs with a bounded [`RetryLimit`], [`RetryStateLimit`], [`SkipLimit`],
62//! and a deterministic [`BackoffPolicy`]. [`FaultPolicy::decide`] is a pure
63//! function of the policy, a framework-owned [`FaultDescriptor`], and
64//! [`FaultEvidence`], so a restart reproduces the same decision. Waiting uses
65//! an injected [`BackoffSleeper`] rather than wall-clock time, and
66//! [`RollbackDisposition::CommitSafeSkip`] still records a skip instead of
67//! silently dropping an item.
68//!
69//! [`ItemListenerSet`] owns the M3 [`ReadListener`], [`ProcessListener`],
70//! [`WriteListener`], [`RetryListener`], and [`SkipListener`] families. Before
71//! callbacks run in registration order and stop at the first failure; the
72//! matching completion callbacks run only the entered listeners in reverse
73//! order and aggregate every failure. A panic is classified exactly like a
74//! returned [`ListenerError`], and no callback receives an error payload.
75//!
76//! [`ChunkStep::with_fault_runtime`] installs a [`FaultRuntime`] and makes that
77//! policy executable. A retryable fault rolls the chunk attempt back, reserves
78//! its ordinal through a bounded [`FaultStateStore`], runs the retry scope,
79//! waits the injected backoff, and replays the chunk from inputs it already
80//! read, so a stateful reader never rewinds. An accepted skip is provisional
81//! until the commit that records it, and a commit-safe skip additionally
82//! requires [`ChunkDeliveryMode::AtomicSameResource`] and an enlisted
83//! transaction. [`ChunkExecutionReport`] returns per-phase
84//! [`RetryCounts`] and [`SkipCounts`], rollback and no-rollback counts, and
85//! redacted [`ItemListenerFailure`] values.
86//!
87//! [`Checkpoint`] and [`ExecutionContext`] retain bounded versioned JSON through
88//! application-owned [`VersionedStateCodec`] implementations. Codec signatures
89//! exchange JSON object bytes, keeping serializer types out of the public
90//! contract. Their `Debug` output never includes payloads.
91//!
92//! A codec declares its current schema version and the [`StateSchemaUpgrade`]
93//! edges it can apply. Decoding an older recorded version walks one bounded,
94//! deterministic chain of those edges and only then calls
95//! [`VersionedStateCodec::decode`], so a codec parses exactly one shape. A
96//! recorded version newer than the codec, or one with no declared path to the
97//! current version, fails closed rather than being truncated, defaulted, or
98//! reinterpreted.
99//!
100//! [`DefinitionManifest`] reads canonical definition bytes back without
101//! guessing. A newer format, a non-canonical encoding, a floating-point value,
102//! an out-of-bound graph, or a digest that does not match the supplied bytes
103//! fails closed.
104//!
105//! ```
106//! use oxide_batch::{
107//! ComponentRevision, DefinitionIdentity, DefinitionManifest, DefinitionRevision, JobName,
108//! StepName,
109//! };
110//!
111//! let identity = DefinitionIdentity::tasklet(
112//! &JobName::new("daily_import")?,
113//! &StepName::new("import")?,
114//! DefinitionRevision::new("2026-07-31")?,
115//! &ComponentRevision::new("tasklet-v1")?,
116//! )?;
117//! let manifest = DefinitionManifest::read_verified(
118//! identity.canonical_manifest(),
119//! identity.manifest_digest(),
120//! )?;
121//! assert_eq!(manifest.format(), 1);
122//! assert_eq!(manifest.node_count(), None);
123//! # Ok::<(), Box<dyn std::error::Error>>(())
124//! ```
125//!
126//! A multi-step definition is declared as a [`FlowGraph`] of [`FlowNode`]
127//! values joined by exit-pattern [`FlowTransition`] edges and compiled into an
128//! immutable [`CompiledExecutionPlan`] that owns the canonical manifest and
129//! fingerprint:
130//!
131//! ```
132//! use oxide_batch::{
133//! ComponentRevision, DefinitionRevision, ExitPattern, FlowGraph, FlowNode, FlowTarget,
134//! FlowTransition, JobName, NodeId, StepComponents, StepNode, StepName, TerminalKind,
135//! };
136//!
137//! let load = NodeId::new("load")?;
138//! let report = NodeId::new("report")?;
139//! let plan = FlowGraph::new(load.clone())
140//! .with_node(FlowNode::step(StepNode::new(
141//! load.clone(),
142//! StepName::new("load")?,
143//! StepComponents::Tasklet(ComponentRevision::new("load-v1")?),
144//! )))
145//! .with_node(FlowNode::step(StepNode::new(
146//! report.clone(),
147//! StepName::new("report")?,
148//! StepComponents::Tasklet(ComponentRevision::new("report-v1")?),
149//! )))
150//! .with_sequence(load, FlowTarget::Node(report.clone()))?
151//! .with_sequence(report, FlowTarget::Terminal(TerminalKind::Complete))?
152//! .compile(&JobName::new("daily_import")?, DefinitionRevision::new("v1")?)?;
153//!
154//! assert_eq!(plan.manifest_format(), 2);
155//! assert_eq!(plan.node_count(), 2);
156//! # Ok::<(), Box<dyn std::error::Error>>(())
157//! ```
158//!
159//! [`ExitPattern`] selects one of those transitions from the bounded
160//! [`ExitCode`] a step reports, never from a [`BatchStatus`]:
161//!
162//! ```
163//! use oxide_batch::{ExitCode, ExitPattern};
164//!
165//! let failed = ExitPattern::new("FAILED")?;
166//! let any = ExitPattern::new("*")?;
167//! assert!(failed.matches(&ExitCode::new("FAILED")?));
168//! assert!(!failed.matches(&ExitCode::new("COMPLETED")?));
169//! assert!(any.matches(&ExitCode::new("COMPLETED")?));
170//! assert!(failed.specificity() > any.specificity());
171//! # Ok::<(), Box<dyn std::error::Error>>(())
172//! ```
173//!
174//! [`FaultPolicy::decide`] is a pure function of the policy, a
175//! framework-owned [`FaultDescriptor`], and [`FaultEvidence`]:
176//!
177//! ```
178//! use std::time::Duration;
179//!
180//! use oxide_batch::{
181//! BackoffPolicy, ChunkDeliveryMode, ClassifierRevision, FailureCategory, FailureId,
182//! FailureSummary, FaultAction, FaultClassifier, FaultDecision, FaultDescriptor,
183//! FaultEvidence, FaultPhase, FaultPolicy, FaultRule, RetryLimit, RetryOrdinal,
184//! RetryStateLimit, SkipCounts, SkipLimit,
185//! };
186//!
187//! let classifier = FaultClassifier::new(
188//! ClassifierRevision::new("import_v1")?,
189//! [FaultRule::new(
190//! FaultPhase::Write,
191//! FailureCategory::Timeout,
192//! FaultAction::retry(),
193//! )?],
194//! )?;
195//! let policy = FaultPolicy::new(
196//! classifier,
197//! RetryLimit::new(3)?,
198//! RetryStateLimit::new(64)?,
199//! SkipLimit::NONE,
200//! BackoffPolicy::exponential(Duration::from_millis(50), 2, Duration::from_secs(5))?,
201//! )?;
202//!
203//! let fault = FaultDescriptor::new(
204//! FaultPhase::Write,
205//! FailureSummary::new(FailureCategory::Timeout, FailureId::new(1)?),
206//! RetryOrdinal::INITIAL,
207//! SkipCounts::ZERO,
208//! true,
209//! ChunkDeliveryMode::AtomicSameResource,
210//! );
211//! assert_eq!(
212//! policy.decide(&fault, FaultEvidence::NONE),
213//! FaultDecision::Retry {
214//! ordinal: RetryOrdinal::new(1)?,
215//! delay: Duration::from_millis(50),
216//! }
217//! );
218//! # Ok::<(), Box<dyn std::error::Error>>(())
219//! ```
220//!
221//! Run the complete in-memory example from the workspace root:
222//!
223//! ```text
224//! cargo run -p oxide-batch --example first_job
225//! ```
226//!
227//! The application supplies the async executor; public tasklet and repository
228//! contracts use [`BoxFuture`] rather than executor- or database-driver types.
229
230#![forbid(unsafe_code)]
231
232mod chunk;
233mod chunk_runtime;
234mod diagnostics;
235mod fault;
236mod fault_state;
237mod flow;
238mod item_listener;
239mod listener;
240mod repository;
241mod runtime;
242mod service;
243mod shutdown;
244mod telemetry;
245
246pub use chunk::{
247 BusinessStatement, BusinessTransaction, BusinessTransactionError, BusinessValue,
248 BusinessValueKind, BusinessWriteResult, ChunkCommitReceipt, ChunkCompletion,
249 ChunkCompletionContext, ChunkCompletionError, ChunkCompletionOutcome, ChunkFaultProgress,
250 ChunkTransaction, ChunkTransactionContext, ChunkTransactionError, ChunkTransactionManager,
251 InheritedStepProgress, ItemProcessor, ItemReader, ItemWriter, ProcessContext, ProcessOutcome,
252 ProcessorError, ReadContext, ReadOutcome, ReaderError, WriteContext, WriteOutcome, WriterError,
253};
254pub use chunk_runtime::{
255 ChunkAttemptOutcome, ChunkExecutionOutcome, ChunkExecutionReport, ChunkFailure, ChunkJob,
256 ChunkLaunchReport, ChunkListener, ChunkListenerContext, ChunkListenerError,
257 ChunkListenerFailure, ChunkListenerFailureKind, ChunkListenerPhase, ChunkStep,
258};
259pub use diagnostics::{
260 DiagnosticField, EventComponent, EventSeverity, ExecutionAttempt, ExecutionCorrelation,
261 LifecycleEvent, LifecycleEventKind, LifecycleEventSink, MetricLabel,
262};
263pub use fault::{BackoffOutcome, BackoffSleeper};
264pub use fault_state::{
265 FaultProgress, FaultRuntime, FaultStateEntry, FaultStateEnvelope, FaultStateError,
266 FaultStateFormatError, FaultStateStore, InMemoryFaultState, RetryCounts, RetryKey,
267 RetryReservation,
268};
269pub use flow::{
270 DeciderError, DecisionInput, DecisionStepInput, FlowEvent, FlowEventKind, FlowEventSink,
271 FlowExecutionOutcome, FlowFailure, FlowJob, FlowJobError, FlowLaunchReport, FlowLauncher,
272 FlowRuntimeError, JobExecutionDecider, PartitionFactoryError, PartitionPlanFactory,
273 PartitionPlanRequest, PartitionTaskletFactory, PartitionWorkerInput, TaskletStepFactory,
274};
275pub use item_listener::{
276 BeforeCallbackOutcome, ItemListenerContext, ItemListenerError, ItemListenerFailure,
277 ItemListenerPhase, ItemListenerSet, ProcessListener, ReadListener, RetryListener, RetryOutcome,
278 SkipListener, WriteListener,
279};
280pub use listener::{
281 JobExecutionListener, ListenerContext, ListenerError, ListenerFailure, ListenerFailureKind,
282 ListenerPhase, StepExecutionListener,
283};
284pub use oxide_batch_core::{
285 BackoffKind, BackoffPolicy, BatchStatus, Checkpoint, ChunkComponentRevisions, ChunkCount,
286 ChunkCounts, ChunkDeliveryMode, ChunkError, ChunkProgress, ChunkRestartContract, ChunkSize,
287 ClassifierRevision, ComponentRevision, DefinitionError, DefinitionIdentity, DefinitionManifest,
288 DefinitionRevision, DefinitionTokenKind, DefinitionUpgrade, DefinitionUpgradeKey, DomainError,
289 DurableStateKind, ExecutionContext, ExecutionCounts, ExecutionMetadata, ExecutionTimestamps,
290 ExecutionVersion, ExitCode, ExitStatus, FailureCategory, FailureId, FailureSummary,
291 FaultAction, FaultClassifier, FaultDecision, FaultDescriptor, FaultEvidence, FaultPhase,
292 FaultPolicy, FaultPolicyError, FaultRule, FlowTarget, IdentifierKind, InFlightPolicy,
293 JobExecution, JobExecutionId, JobInstance, JobInstanceId, JobInstanceKey, JobName,
294 JobParameter, JobParameters, LifecycleError, LifecycleTransition, MAX_NODES, MAX_PARTITIONS,
295 MAX_TRANSITIONS, ManifestError, NameKind, NodeId, OperatorRequestId, ParameterName,
296 ParameterRole, ParameterValue, ParameterValueKind, RecoveryDecisionId, RetentionActionId,
297 RetryLimit, RetryOrdinal, RetryStateLimit, RollbackDisposition, SkipCounts, SkipLimit,
298 StartControls, StartLimit, StateCodecError, StateError, StateLimits, StateSchemaId,
299 StateSchemaUpgrade, StateSchemaVersion, StepDefinitionUpgrade, StepExecution, StepExecutionId,
300 StepName, StepPartitionId, TerminalKind, VersionedStateCodec,
301};
302pub use oxide_batch_plan::{
303 CompiledExecutionPlan, DeciderRevision, DecisionInputVersion, DecisionNode, ExitPattern,
304 FlowGraph, FlowNode, FlowSelectionError, FlowTransition, JoinNode, LocalFailurePolicy,
305 MAX_BRANCH_STEPS, MAX_OUTGOING_TRANSITIONS, MAX_PARTITION_WORKERS, MAX_PATTERN_BYTES,
306 MAX_SPLIT_BRANCHES, PartitionBudget, PartitionCount, PartitionedStepNode, PatternSpecificity,
307 PlanError, SplitBranch, SplitBudget, SplitNode, StepComponents, StepNode,
308};
309pub use oxide_batch_repository::{
310 ActorRef, AuthorizationClass, BoxFuture, Clock, Cursor, CursorError, CursorKey,
311 DEFAULT_MAX_CLOCK_SKEW, DEFAULT_PAGE_SIZE, DEFAULT_PURGE_AGE, DEFAULT_STALE_THRESHOLD,
312 DefinitionDescriptor, ExecutionControl, ExplorerError, ExplorerQuery, ExplorerRepository,
313 FlowDecision, FlowDecisionId, FlowDecisionRequest, FlowDecisionSequence, FlowStepState,
314 FlowTransitionKind, IdGenerationError, IdGenerator, JobExecutionProjection,
315 JobInstanceProjection, JobInstanceSelection, JobRepository, MAX_ACTOR_REF_BYTES,
316 MAX_CLOCK_SKEW, MAX_CURSOR_BYTES, MAX_OPERATION_ID_BYTES, MAX_PAGE_SIZE,
317 MAX_PARTITION_CONTEXT_BYTES, MAX_PARTITION_KEY_BYTES, MAX_PURGE_BATCH, MAX_REASON_CODE_BYTES,
318 MAX_RESPONSE_BYTES, MAX_STALE_THRESHOLD, MIN_CLOCK_SKEW, MIN_PURGE_AGE, MIN_STALE_THRESHOLD,
319 MIN_UNRESOLVED_AGE, MaxClockSkew, MonotonicClock, MonotonicInstant, OperationId,
320 OperatorAction, OperatorOutcomeClass, OperatorRecord, OperatorRecordDraft, OperatorRejection,
321 OperatorRequest, OwnerObservation, OwnerToken, Page, PageRequest, PageSize,
322 ParameterDescriptor, PartitionAggregate, PartitionAggregationError, PartitionKey,
323 PartitionPlanEntry, PartitionResult, PartitionValueError, PurgeBatchBound, PurgeCandidate,
324 PurgeCounts, PurgePlan, PurgePlanRequest, PurgeSurvey, QueryWindow, ReasonCode,
325 RecoveryDecision, RecoveryDirective, RecoveryDisposition, RecoveryError, RecoveryEvidence,
326 RecoveryField, RecoveryMarkers, RecoveryProposal, RecoveryRepository, RecoveryRequest,
327 RecoveryRequestError, RecoveryResult, RecoverySnapshot, RecoveryStepEvidence,
328 RepositoryCapability, RepositoryDescriptor, RepositoryError, RepositoryUnitOfWork,
329 RequestDigest, RequestField, RequestFieldError, RetentionAction, RetentionError, RetentionHold,
330 RetentionOutcome, RetentionRecord, RetentionRecordDraft, SequentialIdGenerator, StaleThreshold,
331 StateEnvelopeDescriptor, StepExecutionProjection, StepPartition, StepPartitionProjection,
332 SystemClock, SystemMonotonicClock, TerminalStatusSet, aggregate_step_partitions,
333};
334#[cfg(feature = "postgres")]
335pub use repository::{
336 CaCertificate, PostgresChunkStateError, PostgresChunkStateProvider,
337 PostgresChunkTransactionManager, PostgresConfig, PostgresConfigError, PostgresDurableStepState,
338 PostgresExplorer, PostgresFaultState, PostgresJobRepository, PostgresMigrator, TlsMode,
339};
340pub use repository::{InMemoryExplorer, InMemoryJobRepository};
341pub use runtime::{
342 BlockingTasklet, BlockingTaskletAdapter, BlockingTaskletContext, JobLauncher, LaunchError,
343 LaunchReport, StopPollInterval, StopSource, StopTiming, StopToken, Tasklet, TaskletContext,
344 TaskletError, TaskletExecutionOutcome, TaskletFailure, TaskletJob, TaskletOutcome, TaskletStep,
345};
346pub use service::{
347 JobExplorer, JobOperator, OperatorError, OperatorOutcome, RecoveryProposer, RetentionReport,
348 RetentionService,
349};
350pub use shutdown::{
351 DEFAULT_SHUTDOWN_DEADLINE, DEFAULT_TELEMETRY_FLUSH_DEADLINE, DrainResult,
352 MAX_SHUTDOWN_DEADLINE, MAX_TELEMETRY_FLUSH_DEADLINE, MIN_SHUTDOWN_DEADLINE,
353 MIN_TELEMETRY_FLUSH_DEADLINE, ShutdownCoordinator, ShutdownDeadline, ShutdownError,
354 ShutdownHookError, ShutdownHookStatus, ShutdownReport, ShutdownRequest, ShutdownSignal,
355 ShutdownTaskPhase, TaskJoinDeadline, TelemetryFlushDeadline, TelemetryFlushStatus,
356 UnjoinedPhase,
357};
358pub use telemetry::{
359 DEFAULT_DROP_REPORT_WINDOW, DEFAULT_EXPORT_QUEUE_RECORDS, DEFAULT_RETAINED_EVENT_CAPACITY,
360 DEFAULT_RETAINED_EVENTS_PER_EXECUTION, DropReportWindow, EnqueueResult, EventTiming,
361 ExportError, ExportFlushReport, ExportQueueBound, ExporterConfigurationError,
362 IncidentBufferConfigurationError, IncidentEventBuffer, MAX_DROP_REPORT_WINDOW,
363 MAX_EXPORT_QUEUE_RECORDS, MAX_METRIC_NAME_ALLOWLIST, MAX_RETAINED_EVENTS_PER_EXECUTION,
364 METRIC_CARDINALITY_BUDGET, MIN_DROP_REPORT_WINDOW, MIN_EXPORT_QUEUE_RECORDS,
365 MetricCardinalityGuard, MetricConfigurationError, MetricDimensions, MetricFamily,
366 MetricObservation, MetricUnit, OTHER_LABEL_VALUE, TELEMETRY_EVENT_CATALOG,
367 TELEMETRY_SCHEMA_VERSION, TELEMETRY_SPAN_CATALOG, TelemetryEventKind, TelemetryEventSink,
368 TelemetryExportSink, TelemetryExporter, TelemetryQueue, TelemetryRecord, TelemetrySpanKind,
369 TelemetrySpanStatus,
370};
371
372/// The version of the `OxideBatch` facade crate.
373pub const VERSION: &str = env!("CARGO_PKG_VERSION");