Skip to content

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

@Mateusz7410

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 ASYNC-0002 / #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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions