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

v.1.6.10 - Rollup Conductor tweaks #554

Merged
merged 8 commits into from
Jan 18, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ sfge-*.log.gz
.pmdCache
#OSX
.DS_Store
.config
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ 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=04t6g000008OaJkAAK">
<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaLCAA0">
<img alt="Deploy to Salesforce"
src="./media/deploy-package-to-prod.png">
</a>

<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaJkAAK">
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008OaLCAA0">
<img alt="Deploy to Salesforce Sandbox"
src="./media/deploy-package-to-sandbox.png">
</a>
Expand Down Expand Up @@ -226,7 +226,7 @@ These are the fields on the `Rollup Control` custom metadata type:
- Apex Rollup (optional) - lookup field to the `Rollup__mdt` metadata record.
- `Should Abort Run` - if done at the `Org_Defaults` level, completely shuts down all rollup operations in the org. Otherwise, can be used on an individual rollup basis to turn on/off.
- `Should Duplicate Rules Be Ignored` (defaults to false) - By default, duplicate rules are enforced on rollup updates. Set this to true to bypass duplicate rules for rollups.
- `Should Run As` - a picklist dictating the preferred method for running rollup operations. Possible values are `Queueable`, `Batchable`, or `Synchronous Rollup`.
- `Should Run As` - a picklist dictating the preferred method for running rollup operations. Possible values are `Queueable`, `Batchable`, or `Synchronous Rollup`. By default, Apex Rollup runs asynchronously as a queueable. Only one queueable can be fired from a process that's already asynchronous, and while Apex Rollup automatically detects such things, if _another_ bit of code that runs _after_ Apex Rollup needs to use that Queueable, `Batchable` may be a better option. When set to `Synchronous Rollup`, all calculations occur prior to an insert / update / delete being finished on the children records.
- `Should Run Single Records Synchronously` - Apex Rollup typically uses the `Should Run As` picklist to determine the default execution context for rollups (which tends to be async). This checkbox deviates from that methodology by forcing single record updates to run sync (whenever possible), which helps with handling updates from datatables or other features using Lightning Data Service (LDS).
- `Should Skip Resetting Parent Fields` (defaults to false) - for full recalculations and REFRESH-based child item updates, Apex Rollup by default assumes that for a parent record with no matching children, the parent-level field should be reset. If this checkbox is set to true, those parent records without results will simply be ignored, and will not be updated.
- `Trigger Or Invocable Name` - If you are using custom Apex, a schedulable, or rolling up by way of the Invocable action and can't use the Apex Rollup lookup field. Use the pattern `trigger_fieldOnCalcItem_to_rollupFieldOnTarget_rollup` - for example: 'trigger_opportunity_stagename_to_account_name_rollup' (use lowercase on the field names). If there is a matching Rollup Limit record, those rules will be used. The first part of the string comes from how a rollup has been invoked - either by `trigger`, `invocable`, or `schedule`. A scheduled flow still uses `invocable`!
Expand Down
98 changes: 91 additions & 7 deletions extra-tests/classes/RollupFullRecalcTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1565,7 +1565,6 @@ private class RollupFullRecalcTests {
@IsTest
static void shouldResetParentValuesWithoutMatchingChildrenFromBulkRouteOverLimitWithBatching() {
RollupParentResetProcessor.maxQueryRows = 0;
RollupAsyncProcessor.shouldRunAsBatch = true;
Rollup.defaultControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable, IsRollupLoggingEnabled__c = true);
Account acc = [SELECT Id FROM Account];
acc.AccountNumber = 'someString';
Expand Down Expand Up @@ -1778,7 +1777,6 @@ private class RollupFullRecalcTests {
@IsTest
static void shouldFullRecalcWithInWhereClauses() {
RollupParentResetProcessor.maxQueryRows = 0;
RollupAsyncProcessor.shouldRunAsBatch = true;
Rollup.defaultControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable);
Account acc = [SELECT Id FROM Account];

Expand Down Expand Up @@ -1838,7 +1836,6 @@ private class RollupFullRecalcTests {

@IsTest
static void shouldNotRequeryForAverageItemsInFullRecalc() {
RollupAsyncProcessor.shouldRunAsBatch = true;
Rollup.defaultControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Synchronous, MaxLookupRowsBeforeBatching__c = 2);
Account acc = [SELECT Id FROM Account];
Account second = new Account(Name = 'Second');
Expand Down Expand Up @@ -1885,7 +1882,6 @@ private class RollupFullRecalcTests {

@IsTest
static void shouldNotRequeryInFullRecalcForFirstLast() {
RollupAsyncProcessor.shouldRunAsBatch = true;
Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 2, ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Synchronous);
Account acc = [SELECT Id FROM Account];
Account second = new Account(Name = 'Second');
Expand Down Expand Up @@ -2147,7 +2143,7 @@ private class RollupFullRecalcTests {
static void addsCabooseForMultiplFullRecalcsToDifferentParents() {
// when there are multiple children types passed to Rollup.performBulkFullRecalc (or any other method ending in a full recalc),
// and there are multiple instances of the full batch recalcs that are present in the list of rollups
Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 1, MaxLookupRowsBeforeBatching__c = 2);
Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 100, MaxLookupRowsBeforeBatching__c = 2, IsRollupLoggingEnabled__c = true);
Rollup.onlyUseMockMetadata = true;

Account parentOne = [SELECT Id FROM Account];
Expand Down Expand Up @@ -2194,7 +2190,7 @@ private class RollupFullRecalcTests {
new Rollup__mdt(
CalcItem__c = 'Contact',
LookupFieldOnCalcItem__c = 'AccountId',
RollupFieldOnCalcItem__c = 'LastName',
RollupFieldOnCalcItem__c = 'Name',
RollupOperation__c = 'CONCAT',
LookupObject__c = 'Account',
LookupFieldOnLookupObject__c = 'Id',
Expand All @@ -2205,6 +2201,16 @@ private class RollupFullRecalcTests {
);
Test.stopTest();

Assert.areEqual(
1,
[SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'Queueable' AND ApexClass.Name = :getNamespaceSafeClassName(RollupAsyncProcessor.class)],
'Conductor should begin async as queueable'
);
Assert.areEqual(
2,
[SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'BatchApexWorker' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)],
'Only two batch classes should have run'
);
parentOne = [SELECT AnnualRevenue, Description, NumberOfEmployees FROM Account WHERE Id = :parentOne.Id];
Assert.areEqual(childOne.PreferenceRank, parentOne.AnnualRevenue);
Assert.areEqual('Concat, One', parentOne.Description);
Expand All @@ -2215,14 +2221,92 @@ private class RollupFullRecalcTests {
Assert.areEqual(10, parentTwo.NumberOfEmployees);
}

@IsTest
static void addsCabooseForMultiplFullRecalcsToDifferentParentsWithoutOtherRollups() {
// when there are multiple children types passed to Rollup.performBulkFullRecalc (or any other method ending in a full recalc),
// and there are multiple instances of the full batch recalcs that are present in the list of rollups
Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 100, MaxLookupRowsBeforeBatching__c = 2, IsRollupLoggingEnabled__c = true);
Rollup.onlyUseMockMetadata = true;

Account parentOne = [SELECT Id FROM Account];
Account parentTwo = new Account(Name = 'Parent Two');
insert parentTwo;

// with two different parents this one should be a batch
insert new List<Opportunity>{
new Opportunity(AccountId = parentOne.Id, StageName = 'One', CloseDate = System.today(), Name = 'Another child rollup', Amount = 5),
new Opportunity(AccountId = parentTwo.Id, StageName = 'Two', CloseDate = System.today(), Name = 'Child rollup two', Amount = 10)
};

// same thing here; we need TWO different batches to prove that this is working (because getAsyncRollup() shortcuts to the first rollup in the list when
// there is only one AND it's a full recalc, but we need this to work for multiple batch full recalcs at once)
ContactPointAddress childOne = new ContactPointAddress(ParentId = parentOne.Id, Name = 'child one', PreferenceRank = 25);
insert new List<ContactPointAddress>{
childOne,
new ContactPointAddress(ParentId = parentOne.Id, Name = 'Concat'),
new ContactPointAddress(ParentId = parentOne.Id, Name = 'One'),
new ContactPointAddress(ParentId = parentTwo.Id, Name = 'Concat'),
new ContactPointAddress(ParentId = parentTwo.Id, Name = 'Two')
};

Test.startTest();
Rollup.performBulkFullRecalc(
new List<Rollup__mdt>{
new Rollup__mdt(
CalcItem__c = 'ContactPointAddress',
LookupFieldOnCalcItem__c = 'ParentId',
RollupFieldOnCalcItem__c = 'PreferenceRank',
RollupOperation__c = 'SUM',
LookupObject__c = 'Account',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'AnnualRevenue'
),
new Rollup__mdt(
CalcItem__c = 'Opportunity',
LookupFieldOnCalcItem__c = 'AccountId',
RollupFieldOnCalcItem__c = 'Amount',
RollupOperation__c = 'MAX',
LookupObject__c = 'Account',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'NumberOfEmployees'
),
new Rollup__mdt(
CalcItem__c = 'ContactPointAddress',
LookupFieldOnCalcItem__c = 'ParentId',
RollupFieldOnCalcItem__c = 'Name',
RollupOperation__c = 'CONCAT',
LookupObject__c = 'Account',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'Description'
)
},
Rollup.InvocationPoint.FROM_FULL_RECALC_LWC.name()
);
Test.stopTest();

Assert.areEqual(
2,
[SELECT COUNT() FROM AsyncApexJob WHERE JobType = 'BatchApexWorker' AND ApexClass.Name = :getNamespaceSafeClassName(RollupFullBatchRecalculator.class)],
'Only two batch classes should have run'
);
parentOne = [SELECT AnnualRevenue, Description, NumberOfEmployees FROM Account WHERE Id = :parentOne.Id];
Assert.areEqual(childOne.PreferenceRank, parentOne.AnnualRevenue);
Assert.areEqual('Concat, One, child one', parentOne.Description);
Assert.areEqual(5, parentOne.NumberOfEmployees);
parentTwo = [SELECT AnnualRevenue, Description, NumberOfEmployees FROM Account WHERE Id = :parentTwo.Id];
Assert.areEqual(null, parentTwo.AnnualRevenue);
Assert.areEqual('Concat, Two', parentTwo.Description);
Assert.areEqual(10, parentTwo.NumberOfEmployees);
}

@IsTest
static void allowsMultipleRollupsToSameFieldWithDifferentBatches() {
// an interesting one because it's again an implementation detail - this time,
// of the batch "caboose" process. if the full recalc is BELOW the batch limit,
// the queueable will already correctly handle everything since the shared parent field will only be reset once.
// batch processes, on the other hand, would otherwise simply see the existing value on the parent as something
// that needs to be cleared out
Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 1, MaxLookupRowsBeforeBatching__c = 1);
Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 100, MaxLookupRowsBeforeBatching__c = 1, IsRollupLoggingEnabled__c = true);
Rollup.onlyUseMockMetadata = true;
Account parent = [SELECT Id FROM Account];

Expand Down
10 changes: 5 additions & 5 deletions extra-tests/classes/RollupIntegrationTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1525,7 +1525,6 @@ private class RollupIntegrationTests {
MaxRollupRetries__c = 1,
ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Synchronous
);
RollupAsyncProcessor.shouldRunAsBatch = true;
Rollup.shouldRun = true;
Rollup.records = cpas;
Rollup.rollupMetadata = new List<Rollup__mdt>{
Expand Down Expand Up @@ -1609,8 +1608,7 @@ private class RollupIntegrationTests {
Rollup.records = new List<ContactPointAddress>{ cpa };
Rollup.shouldRun = true;
Rollup.apexContext = TriggerOperation.AFTER_INSERT;
RollupAsyncProcessor.shouldRunAsBatch = true;
Rollup.defaultControl = new RollupControl__mdt(MaxParentRowsUpdatedAtOnce__c = 0);
Rollup.defaultControl = new RollupControl__mdt(MaxParentRowsUpdatedAtOnce__c = 0, ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable);

Test.startTest();
Rollup.sumFromApex(ContactPointAddress.PreferenceRank, ContactPointAddress.ParentId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc();
Expand Down Expand Up @@ -1779,14 +1777,15 @@ private class RollupIntegrationTests {
static void shouldFilterNonMatchingRollupsOutOfBatch() {
Contact con = new Contact(FirstName = 'Something', LastName = 'Required');
insert con;
RollupControl__mdt control = RollupControl__mdt.getInstance(Rollup.CONTROL_ORG_DEFAULTS);
RollupControl__mdt control = RollupControl__mdt.getInstance(Rollup.CONTROL_ORG_DEFAULTS).clone();
control.ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable;
Rollup.FilterResults results = new Rollup.FilterResults();
results.matchingItemIds.add(con.Id);
RollupAsyncProcessor batchProcessor = new RollupAsyncProcessor(
results,
Contact.FirstName,
Contact.Id,
Contact.Id,
Contact.AccountId,
Contact.FirstName,
Campaign.SObjectType,
Contact.SObjectType,
Expand All @@ -1804,6 +1803,7 @@ private class RollupIntegrationTests {
);
RollupAsyncProcessor conductor = new RollupAsyncProcessor(Rollup.InvocationPoint.FROM_APEX, new List<Contact>{ con }, new Map<Id, SObject>());
conductor.rollups.add(batchProcessor);

Test.startTest();
Database.executeBatch(conductor);
Test.stopTest();
Expand Down
17 changes: 11 additions & 6 deletions extra-tests/classes/RollupTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2441,7 +2441,7 @@ private class RollupTests {
static void shouldRunSuccessfullyAsBatch() {
RollupTestUtils.DMLMock mock = RollupTestUtils.loadAccountIdMock(new List<ContactPointAddress>{ new ContactPointAddress(PreferenceRank = 1) });
Rollup.apexContext = TriggerOperation.AFTER_INSERT;
RollupAsyncProcessor.shouldRunAsBatch = true;
Rollup.defaultControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable, MaxLookupRowsBeforeBatching__c = 1);

Test.startTest();
Rollup.countFromApex(ContactPointAddress.PreferenceRank, ContactPointAddress.ParentId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc();
Expand Down Expand Up @@ -2499,7 +2499,7 @@ private class RollupTests {
}

@IsTest
static void shouldNotRunAsBatchableWhenDefaultIsBatchableAndRecordsAreLessThanBatchableLimit() {
static void shouldRunAsBatchableWhenDefaultIsBatchableAndRecordsAreLessThanBatchableLimit() {
RollupTestUtils.DMLMock mock = RollupTestUtils.loadAccountIdMock(new List<ContactPointAddress>{ new ContactPointAddress(PreferenceRank = 1) });
Rollup.apexContext = TriggerOperation.AFTER_INSERT;
Rollup.defaultControl = new RollupControl__mdt(MaxLookupRowsBeforeBatching__c = 1000, ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable);
Expand All @@ -2511,7 +2511,7 @@ private class RollupTests {
System.assertEquals(1, mock.Records.size(), 'Rollup run should have run');
Account acc = (Account) mock.Records[0];
System.assertEquals(1, acc.AnnualRevenue);
System.assertEquals('Completed', [SELECT Status FROM AsyncApexJob WHERE JobType = 'Queueable' LIMIT 1]?.Status);
System.assertEquals('Completed', [SELECT Status FROM AsyncApexJob WHERE JobType = 'BatchApexWorker' LIMIT 1]?.Status);
}

@IsTest
Expand Down Expand Up @@ -2898,7 +2898,7 @@ private class RollupTests {
insert cpas;

RollupTestUtils.DMLMock mock = RollupTestUtils.loadMock(cpas);
Rollup.defaultControl = new RollupControl__mdt(MaxNumberOfQueries__c = 2, MaxRollupRetries__c = 1);
Rollup.defaultControl = new RollupControl__mdt(MaxNumberOfQueries__c = 2, MaxRollupRetries__c = 100);
// start as synchronous rollup to allow for one deferral
Rollup.specificControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Synchronous, MaxNumberOfQueries__c = 2);
Rollup.rollupMetadata = new List<Rollup__mdt>{
Expand All @@ -2922,11 +2922,11 @@ private class RollupTests {
System.assertEquals(1, mock.Records.size(), 'Grandparent record should have been found!');
User updatedUser = (User) mock.Records[0];
System.assertEquals(cpas[0].Name + ', ' + cpas[1].Name, updatedUser.AboutMe, 'Grandparent rollup should have worked!');
System.assertEquals('Completed', [SELECT Status FROM AsyncApexJob WHERE JobType = 'Queueable'].Status);
}

@IsTest
static void shouldDeferGrandparentRollupSafelyTillAllParentRecordsAreRetrievedWithBatch() {
RollupAsyncProcessor.shouldRunAsBatch = true;
Account acc = [SELECT Id, OwnerId FROM Account];
List<ContactPointAddress> cpas = new List<ContactPointAddress>{
new ContactPointAddress(ParentId = acc.Id, Name = 'One'),
Expand All @@ -2935,7 +2935,12 @@ private class RollupTests {
insert cpas;

RollupTestUtils.DMLMock mock = RollupTestUtils.loadMock(cpas);
Rollup.defaultControl = new RollupControl__mdt(MaxNumberOfQueries__c = 2, MaxRollupRetries__c = 100);
Rollup.defaultControl = new RollupControl__mdt(
MaxLookupRowsBeforeBatching__c = 0,
MaxNumberOfQueries__c = 2,
MaxRollupRetries__c = 100,
ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Batchable
);
// start as synchronous rollup to allow for one deferral
Rollup.specificControl = new RollupControl__mdt(ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.Synchronous, MaxNumberOfQueries__c = 4);
Rollup.rollupMetadata = new List<Rollup__mdt>{
Expand Down
Loading