Skip to content

Abstract Batchable/Schedulable jobs (framework-owned base) for retry & lifecycle control #37

Description

@Mateusz7410

Problem

For Queueables the framework owns the execution lifecycle via QueueableJob + the chain finalizer, which is what makes features like retry (#34) possible. For Batchable and Schedulable it does not: Async.batchable(Object job) and Async.schedulable(Schedulable job) accept the standard Database.Batchable / Schedulable interfaces and hand them straight to Database.executeBatch / System.schedule unwrapped.

Because the consumer's class implements the raw platform interface, the framework has zero runtime hook into how the job runs — no place to observe failures, capture results, or retry. This is the gap called out in ASYNC-0002 ("we just pass the standard Batchable/Schedulable interfaces, which means we have no power over how the job runs").

Proposal

Introduce a framework-owned abstract base for batch (mirroring QueueableJob), so the framework controls execute/finish and can layer on retry, result capture, and config:

public abstract class BatchableJob implements Database.Batchable<SObject> {
    public abstract Iterable<SObject> start(Database.BatchableContext bc);
    public abstract void work(Database.BatchableContext bc, List<SObject> scope);
    // framework owns execute()/finish(): scope-failure capture, retry, AsyncResult__c
}

Async.batchable(new MyBatch())
    .scopeSize(200)
    .retryFailedScopes(2)
    .enqueue();

Design notes / feasibility

  • Batch retry semantics differ from Queueable. You retry failed scopes, not the whole job. Natural hooks:
    • Database.RaisesPlatformEvents + BatchApexErrorEvent — capture which scopes failed and why, then re-submit a batch over just those records.
    • Or re-executeBatch from finish() for whole-job retry on transient failure.
  • Schedulable "retry" mostly doesn't map — a schedule firing failure is really a reschedule, already largely covered by CronBuilder / SchedulableManager. Likely out of scope; revisit if there's demand.
  • This is a new subsystem, not a small change — it adds an abstract base + wrapper execution path + (likely) a new platform-event-driven failure capture. Should be planned as its own milestone.
  • Backward compatibility: keep Async.batchable(Object) accepting raw Database.Batchable as-is; the new BatchableJob base is additive opt-in.

Open Questions

  • Whole-job retry vs scope-level retry vs both?
  • Use BatchApexErrorEvent (more moving parts, finer-grained) or simpler finish()-driven re-submit first?
  • Do we want a unified result/observability story across Queueable + Batchable (AsyncResult__c)?
  • Is Schedulable abstraction worth it at all, or close as "won't do"?

Acceptance Criteria (high level — to refine)

  • Framework-owned BatchableJob abstract base with start/work overrides
  • Opt-in scope (and/or whole-job) retry for transient failures
  • Existing Async.batchable(Object) raw-interface path unchanged (backward compatible)
  • Result/failure capture consistent with the Queueable path
  • Decision recorded on Schedulable (support vs out-of-scope)
  • Unit tests + docs

Context: split out from #34 (Queueable retry). Tracked separately because it's a larger design effort than the finalizer-based Queueable retry.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions