Skip to content

Commit

Permalink
v1.1.3 - added Batch Chunk Size to RollupControl__mdt, bugfixes/conso…
Browse files Browse the repository at this point in the history
…lidation of behavior surrounding CMDT being read-only when retrieved from cache
  • Loading branch information
jamessimone committed Feb 23, 2021
1 parent 44e313e commit 74d26d6
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 24 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ These are the fields on the `Rollup Control` custom metadata type:
- `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 `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`!
- `Max Query Rows` - (defaults to 100) - Configure this number to decide how many queries Rollup is allowed to issue before restarting in another context. Consider the downstream query needs when your parent objects are updated when configuring this field. By safely requeueing Rollup in conjunction with this number, we ensure no query limit is ever hit.
- `Max Rollup Retries` - (defaults to 100) - Only configurable on the Org Default record. Use in conjunction with `Max Query Rows`. This determines the maximum possible rollup jobs (either batched or queued) that can be spawned from a single overall rollup operation due to the prior one(s) exceeding the configured query limit.
- `Batch Chunk Size` - (defaults to 2000) - The amount of records passed into each batchable job in the event that Rollup batches. Default is 2000, which is the vanilla Salesforce default for batch jobs.

### Flow / Process Builder Invocable

Expand Down
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.1.2",
"version": "1.1.3",
"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
56 changes: 35 additions & 21 deletions rollup/main/default/classes/Rollup.cls
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,9 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
}

public String runCalc() {
// clone because the RollupControl__mdt is read-only when retrieved from the cache
RollupControl__mdt orgDefaults = getSingleControlOrDefault(RollupControl__mdt.DeveloperName, CONTROL_ORG_DEFAULTS, defaultControl).clone(true, true);
RollupControl__mdt orgDefaults = getSingleControlOrDefault(RollupControl__mdt.DeveloperName, CONTROL_ORG_DEFAULTS, defaultControl);
this.rollupControl = orgDefaults;
// side effect in the below method - rollups can be removed from this.rollups if a limit record ShouldAbortRun__c == true
// side effect in the below method - rollups can be removed from this.rollups if a control record ShouldAbortRun__c == true
this.ingestRollupControlData(orgDefaults);

this.isNoOp = this.rollups.isEmpty() && this.syncRollups.isEmpty();
Expand Down Expand Up @@ -271,7 +270,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
return 'Running rollups flagged to go synchronously';
} else if (shouldRunAsBatch && hasMoreThanOneTarget == false) {
// safe to batch because the QueryLocator will only return one type of SObject
return Database.executeBatch(new Rollup(this));
return Database.executeBatch(new Rollup(this), this.rollupControl.BatchChunkSize__c.intValue());
} else if (
shouldRunAsBatch == false && (orgDefaults.MaxLookupRowsForQueueable__c == null || totalCountOfRecords <= orgDefaults.MaxLookupRowsForQueueable__c)
) {
Expand Down Expand Up @@ -1349,6 +1348,20 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
return Database.countQuery(countQuery);
}

private static List<SObject> getMetadataFromCache(SObject metadataRecord) {
// CMDT is read-only when returned from the cache
Map<String, SObject> metadataMap;
switch on metadataRecord {
when Rollup__mdt rollupMeta {
metadataMap = Rollup__mdt.getAll().deepClone();
}
when RollupControl__mdt rollupControl {
metadataMap = RollupControl__mdt.getAll().deepClone();
}
}
return metadataMap?.values();
}

private static void flattenBatches(Rollup outerRollup, List<Rollup> rollups) {
for (Rollup rollup : rollups) {
if (rollup.rollups.isEmpty() == false) {
Expand Down Expand Up @@ -1418,7 +1431,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
if (rollupMetadata != null) {
return rollupMetadata;
}
List<Rollup__mdt> rollupMetadatas = Rollup__mdt.getAll().values();
List<Rollup__mdt> rollupMetadatas = (List<Rollup__mdt>) getMetadataFromCache(new Rollup__mdt());
for (Integer index = rollupMetadatas.size() - 1; index >= 0; index--) {
Rollup__mdt rollupMetadata = rollupMetadatas[index];
if (rollupMetadata.CalcItem__c != sObjectType.getDescribe().getName()) {
Expand Down Expand Up @@ -1463,18 +1476,16 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
SObjectField lookupFieldOnOpObject = getSObjectFieldByName(lookupObjectDescribe, rollupMetadata.LookupFieldOnLookupObject__c);
SObjectField rollupFieldOnOpObject = getSObjectFieldByName(lookupObjectDescribe, rollupMetadata.RollupFieldOnLookupObject__c);

// reset the CMDT field-level definitions using the SObjectField tokens. Since the CMDT is read-only
// when retrieved from the cache, clone it first
Rollup__mdt clonedMetadata = rollupMetadata.clone(true, true);
clonedMetadata.RollupFieldOnCalcItem__c = rollupFieldOnCalcItem.getDescribe().getName();
clonedMetadata.LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.getDescribe().getName();
clonedMetadata.LookupFieldOnLookupObject__c = lookupFieldOnOpObject.getDescribe().getName();
clonedMetadata.RollupFieldOnLookupObject__c = rollupFieldOnOpObject.getDescribe().getName();
// reset the CMDT field-level definitions using the SObjectField tokens
rollupMetadata.RollupFieldOnCalcItem__c = rollupFieldOnCalcItem.getDescribe().getName();
rollupMetadata.LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.getDescribe().getName();
rollupMetadata.LookupFieldOnLookupObject__c = lookupFieldOnOpObject.getDescribe().getName();
rollupMetadata.RollupFieldOnLookupObject__c = rollupFieldOnOpObject.getDescribe().getName();

RollupControl__mdt rollupControl;
if (clonedMetadata.RollupControl__c != null) {
if (rollupMetadata.RollupControl__c != null) {
// for CMDT-driven rollups, the rollup record is always tied to a RollupControl__mdt record
rollupControl = new RollupControl__mdt(Id = clonedMetadata.RollupControl__c);
rollupControl = new RollupControl__mdt(Id = rollupMetadata.RollupControl__c);
} else {
String controlKey = getRollupControlKey(rollupInvokePoint, rollupFieldOnCalcItem, lookupSObjectType, rollupFieldOnOpObject);
rollupControl = getSingleControlOrDefault(RollupControl__mdt.TriggerOrInvocableName__c, controlKey, null);
Expand All @@ -1490,10 +1501,10 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
calcItems,
oldCalcItems,
batchRollup,
RollupEvaluator.getEvaluator(eval, clonedMetadata, oldCalcItems, sObjectType),
RollupEvaluator.getEvaluator(eval, rollupMetadata, oldCalcItems, sObjectType),
rollupControl?.Id,
rollupInvokePoint,
clonedMetadata
rollupMetadata
);
}
return batchRollup;
Expand Down Expand Up @@ -1649,13 +1660,13 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
}

private static RollupControl__mdt getSingleControlOrDefault(SObjectField whereField, Object whereValue, RollupControl__mdt testOverrideData) {
// there are multiple spots where testOverrideData can be supplied, which is why it's necessary to pass this value
// there are multiple spots where testOverrideData can be supplied, which is why it's necessary to pass the argument to this method
if (testOverrideData != null) {
return testOverrideData;
} else if (whereField == RollupControl__mdt.Id) {
return RollupControl__mdt.getInstance((Id) whereValue);
return RollupControl__mdt.getInstance((Id) whereValue)?.clone(true, true);
}
List<RollupControl__mdt> rollupControls = RollupControl__mdt.getAll().values();
List<RollupControl__mdt> rollupControls = (List<RollupControl__mdt>) getMetadataFromCache(new RollupControl__mdt());
for (RollupControl__mdt rollupControl : rollupControls) {
if (rollupControl.get(whereField) == whereValue) {
return rollupControl;
Expand All @@ -1671,12 +1682,15 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
// this *should* be impossible, since the record is included on install, but ...
sensibleDefault = new RollupControl__mdt(
DeveloperName = CONTROL_ORG_DEFAULTS,
BatchChunkSize__c = 2000,
MaxLookupRowsBeforeBatching__c = Limits.getLimitDmlRows() / 3,
MaxLookupRowsForQueueable__c = Limits.getLimitDmlRows() / 2,
MaxQueryRows__c = Limits.getLimitQueryRows() / 2,
ShouldAbortRun__c = false,
ShouldRunAs__c = 'Queueable'
);
} else {
sensibleDefault = sensibleDefault.clone(true, true);
}
return sensibleDefault;
}
Expand Down Expand Up @@ -1747,7 +1761,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
this.rollups.addAll(this.deferredRollups);
this.deferredRollups.clear();
if (System.isBatch()) {
Database.executeBatch(this);
Database.executeBatch(this, this.rollupControl.BatchChunkSize__c.intValue());
} else {
System.enqueueJob(this);
}
Expand Down Expand Up @@ -1967,7 +1981,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
}

public void execute(SchedulableContext sc) {
List<Rollup__mdt> allMetadata = Rollup__mdt.getall().values();
List<Rollup__mdt> allMetadata = getMetadataFromCache(new Rollup__mdt());
Map<String, List<Rollup__mdt>> lookupObjectToMetadata = new Map<String, List<Rollup__mdt>>();
for (Rollup__mdt meta : allMetadata) {
if (rollupMetadataIds.contains(meta.Id) == false) {
Expand Down
2 changes: 1 addition & 1 deletion rollup/main/default/classes/RollupTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2418,7 +2418,7 @@ private class RollupTests {
static void shouldRunAsBatchableWhenSpecificRollupIsBatchable() {
DMLMock mock = loadAccountIdMock(new List<Opportunity>{ new Opportunity(Amount = 1) });
Rollup.apexContext = TriggerOperation.AFTER_INSERT;
Rollup.specificControl = new RollupControl__mdt(ShouldRunAs__c = 'Batchable');
Rollup.specificControl = new RollupControl__mdt(ShouldRunAs__c = 'Batchable', BatchChunkSize__c = 1);

Test.startTest();
Rollup.countFromApex(Opportunity.Amount, Opportunity.AccountId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<label>Org Defaults</label>
<protected>false</protected>
<values>
<field>BatchChunkSize__c</field>
<value xsi:type="xsd:double">2000.0</value>
</values>
<values>
<field>MaxLookupRowsBeforeBatching__c</field>
<value xsi:type="xsd:double">3000.0</value>
Expand All @@ -12,10 +16,14 @@
</values>
<values>
<field>MaxQueryRows__c</field>
<value xsi:nil="true"/>
<value xsi:type="xsd:double">100.0</value>
</values>
<values>
<field>MaxRollupRetries__c</field>
<value xsi:type="xsd:double">100.0</value>
</values>
<values>
<field>MaximumRollupRetries__c</field>
<value xsi:nil="true"/>
</values>
<values>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
<behavior>Edit</behavior>
<field>MaxLookupRowsForQueueable__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>BatchChunkSize__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>BatchChunkSize__c</fullName>
<defaultValue>2000</defaultValue>
<description>The amount of records passed into each batchable job in the event that Rollup batches. Default is 2000, which is the vanilla Salesforce default.</description>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<inlineHelpText>The amount of records passed into each batchable job in the event that Rollup batches. Default is 2000, which is the vanilla Salesforce default.</inlineHelpText>
<label>Batch Chunk Size</label>
<precision>18</precision>
<required>false</required>
<scale>0</scale>
<type>Number</type>
<unique>false</unique>
</CustomField>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>MaxRollupRetries__c</fullName>
<defaultValue>100</defaultValue>
<description>Use in conjunction with Max Query Rows. This determines the maximum possible rollup jobs (either batched or queued) that can be spawned from a single overall rollup operation. Default is 100.</description>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<inlineHelpText>Use in conjunction with Max Query Rows. This determines the maximum possible rollup jobs (either batched or queued) that can be spawned from a single overall rollup operation. Default is 100.</inlineHelpText>
<label>Max Rollup Retries</label>
<precision>18</precision>
<required>false</required>
<scale>0</scale>
<type>Number</type>
<unique>false</unique>
</CustomField>
5 changes: 5 additions & 0 deletions rollup/main/default/profiles/Admin.profile-meta.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
<enabled>true</enabled>
<name>Rollup__mdt</name>
</customMetadataTypeAccesses>
<fieldPermissions>
<editable>true</editable>
<field>RollupControl__mdt.BatchChunkSize__c</field>
<readable>true</readable>
</fieldPermissions>
<fieldPermissions>
<editable>true</editable>
<field>RollupControl__mdt.MaxLookupRowsBeforeBatching__c</field>
Expand Down

0 comments on commit 74d26d6

Please sign in to comment.