You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is no first-class way to hand the framework a large data set and process it in tuned chunks
across chained Queueables, with per-chunk tracking and retry. Users hit this when they need to
process thousands of records in the background but do not want a Database.Batchable (heavier,
separate lifecycle) or one QueueableJob per record (blows limits, no coordination).
The data set can come from different places: an in-memory List / Set<Id>, a Database.Cursor
(SOQL cursor) for sets too large to hold in memory, or a raw SOQL string. The framework should be
agnostic to the source.
Proposal
A chunk entry point on Async that reuses the existing chaining + finalizer + retry machinery,
with a pluggable record source. The source is mandatory, so it goes in the entry method (cannot be
forgotten):
// in-memory ids or recordsAsync.chunk(newAccountRecalcJob(), ChunkSource.of(records))
.chunkSize(50)
.priority(5)
.retry(2)
.enqueue();
// large set via SOQL cursor - never held in memory at onceAsync.chunk(newAccountRecalcJob(), ChunkSource.query('SELECT Id FROM Account WHERE ...'))
.chunkSize(200)
.enqueue();
The framework:
Pages the source chunkSize records at a time via fetch(offset, count), carrying only the
integer position across the chain.
Drives chunks via chained Queueables (reusing existing chain + finalizer code, not a parallel
class). Pages are enqueued lazily (self-chaining), so a huge set never materializes a giant job
list; only one integer of state moves forward.
Tracks each chunk in AsyncResult__c; a failed chunk retries per the configured policy + backoff.
ChunkSource seam
publicabstractclassChunkSource {
publicabstractIntegersize();
publicabstractList<SObject> fetch(Integeroffset, Integercount);
publicstaticChunkSourceof(List<SObject> records); // in-memory, also the test fakepublicstaticChunkSourceofIds(Set<Id> ids); // id-only sourcepublicstaticChunkSourcecursor(Database.Cursorc); // wraps Database.CursorpublicstaticChunkSourcequery(Stringsoql); // convenience over Database.getCursor
}
size() + fetch(offset, count) mirror Database.Cursor, so the cursor adapter is a thin
delegate. ChunkSource.of(records) lets tests exercise chunking with in-memory records instead of
inserting data and opening a live cursor. Stays SOQL-Lib-free: callers pass their own cursor
(for example SOQL.of(...).toCursor()) into ChunkSource.cursor(...).
Verified on a scratch org that a Database.Cursor held as Queueable member state survives
re-enqueue across the chain (each link deserializes the cursor and fetches the correct page), so
no per-page re-open is needed.
Job base - two override shapes
Both are compile-enforced (one abstract method each), so a wrong override cannot become a silent
no-op:
publicabstractclassChunkJobextendsQueueableJob { // recordspublicabstractvoidwork(List<SObject> chunk);
}
publicabstractclassIdChunkJobextendsChunkJob { // ids conveniencepublicabstractvoidwork(Set<Id> ids);
// framework plucks ids from the fetched records and calls work(ids)
}
Change from the original issue: the record base overrides work(List<SObject> chunk); the id base
overrides work(Set<Id> ids). Pick whichever fits the job.
Within the chunk run (new): .stopRemainingChunksOnFailure(). Default off = keep processing
the remaining chunks even if one fails (best-effort, Batchable-like). When set, a chunk that
exhausts retries stops the remaining chunks of this run, then hands back to the outer chain per
the chain-level setting (so later chain jobs still run if the chain is set to continue).
Notes
Position against native Batchable: the value is a Queueable-based chunked run that keeps our
tracking, retry / backoff, dependsOn, and finalizer semantics.
Reuses QueueableJob chaining and the work() convention; a chunked job overrides work(List<SObject> chunk) (or work(Set<Id> ids)) instead of the no-arg work().
Acceptance criteria
New builder path; no change to any existing global signature. New globals: ChunkSource, ChunkJob, IdChunkJob, plus the Async.chunk entry.
The new globals are added to the public -> global rewrite list in scripts/create-unlocked-package-version.sh, and the built package is installed on a
no-namespace scratch and its tests pass (source deploy alone does not prove the rewrite).
Apex tests: chunking (in-memory), cursor delegation, id source, partial failure + retry, .stopRemainingChunksOnFailure() behavior, empty source is a safe no-op, invalid chunkSize
rejected.
website/api doc, including the cursor governor caps (rows, lifespan, daily cursor opens) so
users can choose chunk vs Batchable.
Motivation
There is no first-class way to hand the framework a large data set and process it in tuned chunks
across chained Queueables, with per-chunk tracking and retry. Users hit this when they need to
process thousands of records in the background but do not want a
Database.Batchable(heavier,separate lifecycle) or one
QueueableJobper record (blows limits, no coordination).The data set can come from different places: an in-memory
List/Set<Id>, aDatabase.Cursor(SOQL cursor) for sets too large to hold in memory, or a raw SOQL string. The framework should be
agnostic to the source.
Proposal
A
chunkentry point onAsyncthat reuses the existing chaining + finalizer + retry machinery,with a pluggable record source. The source is mandatory, so it goes in the entry method (cannot be
forgotten):
The framework:
chunkSizerecords at a time viafetch(offset, count), carrying only theinteger position across the chain.
class). Pages are enqueued lazily (self-chaining), so a huge set never materializes a giant job
list; only one integer of state moves forward.
AsyncResult__c; a failed chunk retries per the configured policy + backoff.ChunkSourceseamsize()+fetch(offset, count)mirrorDatabase.Cursor, so the cursor adapter is a thindelegate.
ChunkSource.of(records)lets tests exercise chunking with in-memory records instead ofinserting data and opening a live cursor. Stays SOQL-Lib-free: callers pass their own cursor
(for example
SOQL.of(...).toCursor()) intoChunkSource.cursor(...).Verified on a scratch org that a
Database.Cursorheld as Queueable member state survivesre-enqueue across the chain (each link deserializes the cursor and fetches the correct page), so
no per-page re-open is needed.
Job base - two override shapes
Both are compile-enforced (one abstract method each), so a wrong override cannot become a silent
no-op:
Change from the original issue: the record base overrides
work(List<SObject> chunk); the id baseoverrides
work(Set<Id> ids). Pick whichever fits the job.Failure handling - two independent levels
QueueableBuilder):continueOnJobExecuteFail/rollbackOnJobExecuteFail/ fail-chain (see Opt-infailChainOnJobExecuteFailto actually stop the chain on job failure #36) govern whether jobs after the chunk run stillexecute. Unchanged.
.stopRemainingChunksOnFailure(). Default off = keep processingthe remaining chunks even if one fails (best-effort, Batchable-like). When set, a chunk that
exhausts retries stops the remaining chunks of this run, then hands back to the outer chain per
the chain-level setting (so later chain jobs still run if the chain is set to continue).
Notes
Batchable: the value is a Queueable-based chunked run that keeps ourtracking, retry / backoff,
dependsOn, and finalizer semantics.QueueableJobchaining and thework()convention; a chunked job overrideswork(List<SObject> chunk)(orwork(Set<Id> ids)) instead of the no-argwork().Acceptance criteria
globalsignature. New globals:ChunkSource,ChunkJob,IdChunkJob, plus theAsync.chunkentry.public->globalrewrite list inscripts/create-unlocked-package-version.sh, and the built package is installed on ano-namespace scratch and its tests pass (source deploy alone does not prove the rewrite).
.stopRemainingChunksOnFailure()behavior, empty source is a safe no-op, invalidchunkSizerejected.
website/apidoc, including the cursor governor caps (rows, lifespan, daily cursor opens) sousers can choose chunk vs Batchable.