Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions force-app/main/default/classes/Async.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ public inherited sharing class Async {
return new QueueableBuilder(job);
}

public static QueueableBuilder queueable(List<QueueableJob> jobs) {
return new QueueableBuilder(jobs);
}

public static QueueableChainBuilder chainBuilder() {
return new QueueableChainBuilder();
}

public static BatchableBuilder batchable(Object job) {
return new BatchableBuilder(job);
}
Expand Down
199 changes: 198 additions & 1 deletion force-app/main/default/classes/AsyncTest.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
* PMD False Positives:
* - ApexAssertionsShouldIncludeMessage: IMO not all assertions need a message
* - EmptyStatementBlock: It is test class, some methods are just to create jobs placeholders
* - CognitiveComplexity: Test class with many test methods
**/
@SuppressWarnings('PMD.ApexAssertionsShouldIncludeMessage,PMD.EmptyStatementBlock')
@SuppressWarnings(
'PMD.ApexAssertionsShouldIncludeMessage,PMD.EmptyStatementBlock,PMD.CognitiveComplexity'
)
@IsTest
@TestVisible
private class AsyncTest implements Database.Batchable<SObject> {
Expand Down Expand Up @@ -1773,4 +1776,198 @@ private class AsyncTest implements Database.Batchable<SObject> {
}
}

// -- Bulk chaining API tests --

@IsTest
private static void shouldEnqueueListOfQueueablesSuccessfully() {
List<QueueableJob> jobs = new List<QueueableJob>();
for (Integer i = 0; i < 5; i++) {
jobs.add(new SuccessfulQueueableTest());
}

Assert.areEqual(0, [SELECT COUNT() FROM Account]);

Test.startTest();
Async.Result ar = Async.queueable(jobs).enqueue();
Test.stopTest();

Assert.areEqual(
QueueableManager.EnqueueType.NEW_CHAIN,
ar.queueableChainState.enqueueType,
'Should be enqueued as a new chain.'
);
Assert.areEqual(5, [SELECT COUNT() FROM Account], 'All 5 jobs should have executed.');
}

@IsTest
private static void shouldEnqueueListOfQueueablesWithDelaySuccessfully() {
List<QueueableJob> jobs = new List<QueueableJob>();
for (Integer i = 0; i < 3; i++) {
jobs.add(new SuccessfulQueueableTest());
}

Assert.areEqual(0, [SELECT COUNT() FROM Account]);

Test.startTest();
Async.Result ar = Async.queueable(jobs).delay(1).enqueue();
Test.stopTest();

Assert.areEqual(
QueueableManager.EnqueueType.NEW_CHAIN,
ar.queueableChainState.enqueueType,
'Should be enqueued as a new chain.'
);
Assert.areEqual(3, [SELECT COUNT() FROM Account], 'All 3 jobs should have executed.');
}

@IsTest
private static void shouldFailOnEmptyListOfQueueables() {
List<QueueableJob> jobs = new List<QueueableJob>();

Test.startTest();
try {
Async.queueable(jobs).enqueue();
Assert.fail('Should have thrown an exception for empty list.');
} catch (Async.IllegalArgumentException e) {
Assert.areEqual(
QueueableManager.ERROR_MESSAGE_NO_JOB_SET,
e.getMessage(),
'Should throw no job set error.'
);
}
Test.stopTest();
}

@IsTest
private static void shouldFailOnNullListOfQueueables() {
List<QueueableJob> jobs = null;

Test.startTest();
try {
Async.queueable(jobs).enqueue();
Assert.fail('Should have thrown an exception for null list.');
} catch (Async.IllegalArgumentException e) {
Assert.areEqual(
QueueableManager.ERROR_MESSAGE_NO_JOB_SET,
e.getMessage(),
'Should throw no job set error.'
);
}
Test.stopTest();
}

@IsTest
private static void shouldEnqueueChainBuilderSuccessfully() {
Assert.areEqual(0, [SELECT COUNT() FROM Account]);

Test.startTest();
Async.Result ar = Async.chainBuilder()
.add(new SuccessfulQueueableTest())
.add(new SuccessfulQueueableTest())
.add(new SuccessfulQueueableTest())
.enqueue();
Test.stopTest();

Assert.areEqual(
QueueableManager.EnqueueType.NEW_CHAIN,
ar.queueableChainState.enqueueType,
'Should be enqueued as a new chain.'
);
Assert.areEqual(
3,
[SELECT COUNT() FROM Account],
'All 3 jobs from chainBuilder should have executed.'
);
}

@IsTest
private static void shouldChainWithChainBuilderWithoutEnqueue() {
Assert.areEqual(0, [SELECT COUNT() FROM Account]);

Test.startTest();
Async.Result ar = Async.chainBuilder()
.add(new SuccessfulQueueableTest())
.add(new SuccessfulQueueableTest())
.chain();

// chain() only adds to the chain; enqueue triggers execution
Async.queueable(new SuccessfulQueueableTest()).enqueue();
Test.stopTest();

Assert.areEqual(
3,
[SELECT COUNT() FROM Account],
'All 3 jobs should have executed (2 chained + 1 enqueued).'
);
}

@IsTest
private static void shouldEnqueueChainBuilderWithTwoJobs() {
Assert.areEqual(0, [SELECT COUNT() FROM Account]);

Test.startTest();
Async.Result ar = Async.chainBuilder()
.add(new SuccessfulQueueableTest())
.add(new SuccessfulQueueableTest())
.enqueue();
Test.stopTest();

Assert.areEqual(QueueableManager.EnqueueType.NEW_CHAIN, ar.queueableChainState.enqueueType);
Assert.areEqual(2, ar.queueableChainState.jobs.size(), 'Chain state should show 2 jobs.');
Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Both jobs should have executed.');
}

@IsTest
private static void shouldEnqueueChainBuilderWithListOfJobs() {
List<QueueableJob> jobs = new List<QueueableJob>();
for (Integer i = 0; i < 4; i++) {
jobs.add(new SuccessfulQueueableTest());
}

Assert.areEqual(0, [SELECT COUNT() FROM Account]);

Test.startTest();
Async.Result ar = Async.chainBuilder().add(jobs).enqueue();
Test.stopTest();

Assert.areEqual(
QueueableManager.EnqueueType.NEW_CHAIN,
ar.queueableChainState.enqueueType,
'Should be enqueued as a new chain.'
);
Assert.areEqual(
4,
[SELECT COUNT() FROM Account],
'All 4 jobs from list should have executed.'
);
}

@IsTest
private static void shouldEnqueueChainBuilderWithListAndSingleJobs() {
List<QueueableJob> jobs = new List<QueueableJob>();
for (Integer i = 0; i < 3; i++) {
jobs.add(new SuccessfulQueueableTest());
}

Assert.areEqual(0, [SELECT COUNT() FROM Account]);

Test.startTest();
Async.Result ar = Async.chainBuilder()
.add(new SuccessfulQueueableTest())
.add(jobs)
.add(new SuccessfulQueueableTest())
.enqueue();
Test.stopTest();

Assert.areEqual(
QueueableManager.EnqueueType.NEW_CHAIN,
ar.queueableChainState.enqueueType,
'Should be enqueued as a new chain.'
);
Assert.areEqual(
5,
[SELECT COUNT() FROM Account],
'All 5 jobs (1 single + 3 list + 1 single) should have executed.'
);
}
}
10 changes: 10 additions & 0 deletions force-app/main/default/classes/queue/QueueableBuilder.cls
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ public inherited sharing class QueueableBuilder {
this.job = job.clone();
}

public QueueableBuilder(List<QueueableJob> jobs) {
if (jobs == null || jobs.isEmpty()) {
throw new Async.IllegalArgumentException(QueueableManager.ERROR_MESSAGE_NO_JOB_SET);
}
for (Integer i = 0; i < jobs.size() - 1; i++) {
QueueableManager.get().chain(jobs.get(i));
}
this.job = jobs.get(jobs.size() - 1).clone();
}

public QueueableBuilder asyncOptions(AsyncOptions asyncOptions) {
if (job.delay != null) {
throw new IllegalArgumentException(
Expand Down
20 changes: 20 additions & 0 deletions force-app/main/default/classes/queue/QueueableChainBuilder.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
public inherited sharing class QueueableChainBuilder {
private List<QueueableJob> jobs = new List<QueueableJob>();

public QueueableChainBuilder add(QueueableJob job) {
return add(new List<QueueableJob>{ job });
}

public QueueableChainBuilder add(List<QueueableJob> jobs) {
this.jobs.addAll(jobs);
return this;
}

public Async.Result enqueue() {
return new QueueableBuilder(jobs).enqueue();
}

public Async.Result chain() {
return new QueueableBuilder(jobs).chain();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>65.0</apiVersion>
<status>Active</status>
</ApexClass>
2 changes: 1 addition & 1 deletion force-app/main/default/classes/queue/QueueableManager.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ public inherited sharing class QueueableManager {
public static final String ERROR_MESSAGE_ASYNC_OPTIONS_AFTER_DELAY = 'Cannot set asyncOptions after delay has been set';
public static final String ERROR_MESSAGE_DELAY_AFTER_ASYNC_OPTIONS = 'Cannot set delay after asyncOptions has been set';
private static final String ERROR_MESSAGE_CHAIN_NULL = 'QueueableChain cannot be null';
private static final String ERROR_MESSAGE_NO_JOB_SET = 'No Queueable job has been set to enqueue';
public static final String ERROR_MESSAGE_NO_JOB_SET = 'No Queueable job has been set to enqueue';
private static final String ERROR_MESSAGE_FINALIZER_JOB_NOT_SET = 'No Finalizer job has been set to attach finalizer';
@TestVisible
private static final String ERROR_MESSAGE_CANNOT_ATTACH_FINALIZER = 'Cannot attach finalizer when not in a QueueableChain context';
Expand Down
Loading