Skip to content

Durable jobs and scheduling annotations

The job host owns claim, restore, renewal and completion. A job component owns its application state only while the actual invocation remains admitted. Persistent scheduling uses durable occurrences, not a timer that exists only in one JVM.

@DurableJob

Declares one finite job entry. Target: method. Attributes: required value; policy = "default", version = 1.

@DurableJob(value = "rebuild-index", policy = "index-jobs")
public String rebuild(String partition) {
    return partition;
}

The callback has one concrete data argument and an allowed data/void/asynchronous result. The host installs the same ManagedJobFactory as factory and required policy, plus its finite ManagedJobProfile. Each mutable activation component has one job entry. The annotation does not install a scheduler or make external I/O exactly once.

@Schedule

Selects a trusted persistent trigger for a job entry. Target: method. Required attribute: value.

@DurableJob(value = "rebuild-index", policy = "index-jobs")
@Schedule("nightly-index")
public String rebuild(String partition) {
    return partition;
}

The host profile supplies time zone, DST gap/fold rules, missed-fire threshold, catch-up and overlap behavior. FORBID uses a native serial group; ALLOW permits independent runs. A changed semantic definition cannot silently resume an incompatible occurrence history.

@Checkpoint

Marks a job state snapshot hook. Target: method. Attributes: none.

@Checkpoint
public Integer savedPosition() {
    return nextBatch;
}

Pair it with @Restore when the component has mutable per-run state. The injected ManagedJobContext records the checkpoint at an explicit safe point. The host does not scan the heap or serialize the Java stack.

@Restore

Marks the matching state restoration hook. Target: method. Attributes: none.

@Restore
public void restorePosition(Integer saved) {
    nextBatch = saved == null ? 0 : saved;
}

A fresh run receives null, so reset state explicitly. A takeover receives saved state through the actual application's allowed schema codec. Restoration does not authorize a stale worker's effects.

@Progress

Marks bounded progress state. Targets: method and parameter in the public declaration; the managed job profile uses its supported method hook. Attributes: none.

@Progress
public Integer progress() {
    return nextBatch;
}

Progress is associated with the current run/fence and persisted at explicit context safe points. It is status data, not proof that an external operation committed. Keep it bounded and free of secrets.

Example: resume a batch job from a safe point

A rebuild saves the next batch only after that batch reaches its application-defined safe point. This component's host registers the index-jobs job profile, nightly-index trigger and current job context binding.

@J7mComponent
public final class RebuildJob {
    private final ManagedProvider<ManagedJobContext> context;
    private int nextBatch;

    @Inject
    public RebuildJob(@J7mResource("job-context")
                      ManagedProvider<ManagedJobContext> context) {
        this.context = context;
    }

    @DurableJob(value = "rebuild-index", policy = "index-jobs")
    @Schedule("nightly-index")
    public String rebuild(String partition) {
        while (nextBatch < 10) {
            // Rebuild this bounded batch using the application's safe protocol.
            nextBatch++;
            context.get().checkpoint();
        }
        return partition + ":complete";
    }

    @Checkpoint
    public Integer savedPosition() { return nextBatch; }

    @Restore
    public void restorePosition(Integer saved) {
        nextBatch = saved == null ? 0 : saved;
    }

    @Progress
    public Integer progress() { return nextBatch; }
}

These are application declarations for an installed host, not a standalone scheduler. A fresh run restores zero. After saving position 4, a takeover restores 4 and continues with the next batch instead of starting from zero. checkpoint() invokes the state/progress hooks at that safe point; it does not serialize a Java stack.

The batch operation is deliberately application-specific: implement it with idempotent or fenced effects before using this pattern against an external index. Saving a counter after an arbitrary external write does not make that write exactly once. The schedule's timezone, overlap and missed-fire behavior come from the trusted trigger profile. See durable jobs and persistent schedules.