Skip to main content

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")?, &parameters);
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");