Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.7.4 - Allow Cursor Usage From Batch Apex #648

Merged
merged 4 commits into from
Jan 10, 2025
Merged
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ As well, don't miss [the Wiki](../../wiki), which includes even more info for co

## Deployment & Setup

<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OfdwAAC">
<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OfezAAC">
<img alt="Deploy to Salesforce" src="./media/deploy-package-to-prod.png">
</a>

<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OfdwAAC">
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OfezAAC">
<img alt="Deploy to Salesforce Sandbox" src="./media/deploy-package-to-sandbox.png">
</a>
<br/>
Expand Down
19 changes: 19 additions & 0 deletions extra-tests/classes/RollupCalcItemReplacerTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,25 @@ private class RollupCalcItemReplacerTests {
Assert.areEqual(0, records.size());
}

@IsTest
static void doesNotUseAndForMultipleParentWhereClauses() {
Account acc = [SELECT Id FROM Account];
RollupCalcItemReplacer replacer = new RollupCalcItemReplacer(
new RollupControl__mdt(IsRollupLoggingEnabled__c = true, ReplaceCalcItemsAsyncWhenOverCount__c = 3)
);

List<SObject> records = replacer.replace(
new List<Account>{ acc, acc },
new List<Rollup__mdt>{
new Rollup__mdt(CalcItemWhereClause__c = 'Id != \'' + acc.Id + '\' AND Owner.Name = null', CalcItem__c = 'Account'),
new Rollup__mdt(CalcItemWhereClause__c = 'Owner.Name != null AND Type != null', CalcItem__c = 'Account')
}
);

Assert.areEqual(1, records.size());
Assert.areEqual(acc.Id, records[0].Id);
}

@IsTest
static void doesNotReplaceForCustomTypeFields() {
List<Account> accounts = [SELECT Id FROM Account];
Expand Down
45 changes: 44 additions & 1 deletion extra-tests/classes/RollupIntegrationTests.cls
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@IsTest
private class RollupIntegrationTests {
private class RollupIntegrationTests implements Database.Batchable<SObject> {
@TestSetup
static void setup() {
Rollup.onlyUseMockMetadata = true;
Expand Down Expand Up @@ -2038,4 +2038,47 @@ private class RollupIntegrationTests {
acc = [SELECT AnnualRevenue FROM Account WHERE Id = :acc.Id LIMIT 1];
Assert.areEqual(2, acc.AnnualRevenue);
}

@IsTest
static void startingFromBatchDoesNotBlowUpCursorBasedFullRecalc() {
Test.startTest();
Database.executeBatch(new RollupIntegrationTests());
Test.stopTest();

Assert.areEqual(1, [SELECT AnnualRevenue FROM Account].AnnualRevenue);
}

public List<SObject> start(Database.BatchableContext bc) {
return [SELECT Id FROM Organization LIMIT 1];
}

public void execute(Database.BatchableContext bc, List<SObject> records) {
Rollup.defaultControl = new RollupControl__mdt(
BatchChunkSize__c = 1,
MaxLookupRowsBeforeBatching__c = 0,
IsRollupLoggingEnabled__c = true,
MaxRollupRetries__c = 1
);
Rollup.onlyUseMockMetadata = true;
Rollup.rollupMetadata = new List<Rollup__mdt>{
new Rollup__mdt(
RollupFieldOnCalcItem__c = 'Id',
LookupObject__c = 'Account',
LookupFieldOnCalcItem__c = 'ParentId',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'AnnualRevenue',
RollupOperation__c = 'REFRESH_COUNT',
CalcItem__c = 'ContactPointAddress'
)
};
ContactPointAddress con = new ContactPointAddress(ParentId = [SELECT Id FROM Account LIMIT 1].Id, Name = 'Child');
insert con;
Rollup.apexContext = TriggerOperation.AFTER_INSERT;
rollup.records = new List<ContactPointAddress>{ con };
rollup.shouldRun = true;
Rollup.runFromTrigger();
}

public void finish(Database.BatchableContext bc) {
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apex-rollup",
"version": "1.7.3",
"version": "1.7.4",
"description": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
"repository": {
"type": "git",
Expand Down
4 changes: 2 additions & 2 deletions rollup-namespaced/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ For more info, see the base `README`.

## Deployment & Setup

<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OfXYAA0">
<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008Off4AAC">
<img alt="Deploy to Salesforce"
src="./media/deploy-package-to-prod.png">
</a>

<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OfXYAA0">
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008Off4AAC">
<img alt="Deploy to Salesforce Sandbox"
src="./media/deploy-package-to-sandbox.png">
</a>
5 changes: 3 additions & 2 deletions rollup-namespaced/sfdx-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"default": true,
"package": "apex-rollup-namespaced",
"path": "rollup-namespaced/source/rollup",
"versionName": "Fixes namespace shadowing issue in RollupFieldInitializer",
"versionName": "Fixes RollupCalcItemReplacer usage with mutually exclusive where clauses, and enables Cursor usage from batch apex jobs",
"versionNumber": "1.2.3.0",
"versionDescription": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
"releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest",
Expand All @@ -27,6 +27,7 @@
"apex-rollup-namespaced@1.1.30": "04t6g000008OfSJAA0",
"apex-rollup-namespaced@1.2.0": "04t6g000008OfU0AAK",
"apex-rollup-namespaced@1.2.1": "04t6g000008OfWVAA0",
"apex-rollup-namespaced@1.2.2": "04t6g000008OfXYAA0"
"apex-rollup-namespaced@1.2.2": "04t6g000008OfXYAA0",
"apex-rollup-namespaced@1.2.3": "04t6g000008Off4AAC"
}
}
16 changes: 12 additions & 4 deletions rollup/core/classes/RollupAsyncProcessor.cls
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,9 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
if (this.isNoOp) {
logMessage = 'no-op, exiting early to avoid burning async job';
} else if (this.rollupControl.ShouldAbortRun__c) {
logMessage = String.valueOf(RollupControl__mdt.SObjectType) + '.' + String.valueOf(RollupControl__mdt.ShouldAbortRun__c) + ' set to true, exiting early';
logMessage = RollupControl__mdt.SObjectType.toString() + '.' + RollupControl__mdt.ShouldAbortRun__c + ' set to true, exiting early';
} else if (RollupSettings__c.getInstance().IsEnabled__c == false && shouldRunWithoutCustomSetting == false) {
logMessage = String.valueOf(RollupSettings__c.SObjectType) + '.' + String.valueOf(RollupSettings__c.IsEnabled__c) + ' is false, exiting early';
logMessage = RollupSettings__c.SObjectType.toString() + '.' + RollupSettings__c.IsEnabled__c + ' is false, exiting early';
} else {
if (syncRollups.isEmpty() == false) {
rollupProcessId = this.getNoProcessId();
Expand Down Expand Up @@ -506,10 +506,18 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme
}

protected String startBatchProcessor() {
String processId;
if (this.isFromBatchExecute) {
return new QueueableProcessor(this).startAsyncWork();
processId = new QueueableProcessor(this).startAsyncWork();
} else {
try {
processId = Database.executeBatch(this, this.rollupControl.BatchChunkSize__c.intValue());
} catch (Exception ex) {
this.logger.log('Could not start batch, trying again as Queueable', ex, System.LoggingLevel.WARN);
processId = new QueueableProcessor(this).startAsyncWork();
}
}
return Database.executeBatch(this, this.rollupControl.BatchChunkSize__c.intValue());
return processId;
}

protected List<SObject> getExistingLookupItems(Set<String> lookupKeys, RollupAsyncProcessor roll, Set<String> uniqueQueryFieldNames) {
Expand Down
2 changes: 1 addition & 1 deletion rollup/core/classes/RollupCalcItemReplacer.cls
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public without sharing class RollupCalcItemReplacer {
new List<String>(additionalQueryFields),
'Id',
'=',
String.join(optionalWhereClauses, ' AND ')
String.join(optionalWhereClauses, ' OR ')
);

calcItems = this.repo.setQuery(queryString).setArg(calcItems).get();
Expand Down
66 changes: 56 additions & 10 deletions rollup/core/classes/RollupFullBatchRecalculator.cls
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
public without sharing virtual class RollupFullBatchRecalculator extends RollupFullRecalcProcessor {
private final RollupState state = new RollupState();
private Database.Cursor cursor;
private Integer currentPosition = 0;
private CalcItemRetriever retriever;

private static final Integer DEFAULT_CHUNK_SIZE = 500;

Expand Down Expand Up @@ -56,29 +55,76 @@ public without sharing virtual class RollupFullBatchRecalculator extends RollupF
this.finalizer.addCaboose(this.cabooses.remove(0));
}
}
this.cursor = this.cursor ?? this.preStart().getCursor();

Integer countOfRecordsToReturn = this.rollupControl.BatchChunkSize__c.intValue();
if (countOfRecordsToReturn + this.currentPosition > this.cursor.getNumRecords()) {
countOfRecordsToReturn = this.cursor.getNumRecords() - this.currentPosition;
if (this.retriever == null) {
try {
this.retriever = new CursorBasedRetriever(this.preStart(), this.rollupControl);
} catch (Exception ex) {
this.logger.log('cursor use disallowed', ex, System.LoggingLevel.WARN);
return System.enqueueJob(new FullBatchQueueableFailsafe(this));
}
}
this.calcItems = this.cursor.fetch(this.currentPosition, countOfRecordsToReturn);
this.currentPosition += countOfRecordsToReturn;

this.calcItems = this.retriever.fetch();
return super.startAsyncWork();
}

private String runBatch() {
return this.startBatchProcessor();
}

protected override RollupFinalizer getFinalizer() {
return new FullRecalcFinalizer(this);
}

private interface CalcItemRetriever {
List<SObject> fetch();
Boolean shouldContinue();
}

private class CursorBasedRetriever implements CalcItemRetriever {
private final Database.Cursor cursor;
private Integer countOfRecordsToReturn;
private Integer currentPosition = 0;

public CursorBasedRetriever(RollupRepository repo, RollupControl__mdt control) {
this.cursor = repo.getCursor();
this.countOfRecordsToReturn = control.BatchChunkSize__c.intValue();
}

public List<SObject> fetch() {
if (this.countOfRecordsToReturn + this.currentPosition > this.cursor.getNumRecords()) {
this.countOfRecordsToReturn = this.cursor.getNumRecords() - this.currentPosition;
}
List<SObject> fetchedRecords = this.cursor.fetch(this.currentPosition, this.countOfRecordsToReturn);
this.currentPosition += this.countOfRecordsToReturn;
return fetchedRecords;
}

public Boolean shouldContinue() {
return this.currentPosition < this.cursor.getNumRecords();
}
}

private class FullBatchQueueableFailsafe implements System.Queueable {
private final RollupFullBatchRecalculator roll;

public FullBatchQueueableFailsafe(RollupFullBatchRecalculator roll) {
this.roll = roll;
}

public void execute(QueueableContext qc) {
this.roll.runBatch();
}
}

private class FullRecalcFinalizer extends RollupFinalizer {
private final RollupFullBatchRecalculator conductor;
public FullRecalcFinalizer(RollupFullBatchRecalculator conductor) {
this.conductor = conductor;
}

public override void handleSuccess() {
if (this.conductor.currentPosition < this.conductor.cursor.getNumRecords()) {
if (this.conductor.retriever?.shouldContinue() ?? false) {
this.conductor.startAsyncWork();
} else {
this.conductor.finish();
Expand Down
2 changes: 1 addition & 1 deletion rollup/core/classes/RollupLogger.cls
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
global without sharing virtual class RollupLogger implements ILogger {
@TestVisible
// this gets updated via the pipeline as the version number gets incremented
private static final String CURRENT_VERSION_NUMBER = 'v1.7.3';
private static final String CURRENT_VERSION_NUMBER = 'v1.7.4';
private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG;
private static final RollupPlugin PLUGIN = new RollupPlugin();

Expand Down
7 changes: 4 additions & 3 deletions sfdx-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"package": "apex-rollup",
"path": "rollup",
"scopeProfiles": true,
"versionName": "Fixes namespace shadowing issue in RollupFieldInitializer",
"versionNumber": "1.7.3.0",
"versionName": "Fixes RollupCalcItemReplacer usage with mutually exclusive where clauses, and enables Cursor usage from batch apex jobs",
"versionNumber": "1.7.4.0",
"versionDescription": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
"releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest",
"unpackagedMetadata": {
Expand Down Expand Up @@ -109,6 +109,7 @@
"apex-rollup@1.7.0": "04t6g000008OfTvAAK",
"apex-rollup@1.7.1": "04t6g000008OfWQAA0",
"apex-rollup@1.7.2": "04t6g000008OfXTAA0",
"apex-rollup@1.7.3": "04t6g000008OfdwAAC"
"apex-rollup@1.7.3": "04t6g000008OfdwAAC",
"apex-rollup@1.7.4": "04t6g000008OfezAAC"
}
}