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
Refactor Selector to Query Once #1360
Refactor Selector to Query Once #1360
Conversation
Cache built per-constructor combo
@@ -1235,9 +1235,7 @@ global with sharing class RollupService { | |||
} | |||
Map<String, RollupSummary> shadowRollupsByMdtId = new Map<String, RollupSummary>(); | |||
for ( | |||
RollupSummary shadowRollup : new RollupSummariesSelector.CustomObjectSelector( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change allows accessing the data cache instead of re-querying the records. Added a new public method to the class to allow requesting records that aren't active.
@@ -113,6 +113,9 @@ private with sharing class RollupServiceTest3 { | |||
rollupSummary.CalculationMode__c = 'Scheduled'; | |||
insert rollupSummary; | |||
|
|||
// burn the rollup cache so it can see the record |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rollup config's were changed after the cache is build the first time. Clearing the cache allows the new rollups to be seen.
@@ -34,6 +34,11 @@ public class RollupSummariesSelector { | |||
@TestVisible | |||
private CustomMetadataSelector m_customMetadataSelector; | |||
|
|||
@TestVisible | |||
private static Map<String, List<RollupSummary>> summariesCacheByKey = new Map<String, List<RollupSummary>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static cache for holding the queried data between instances.
@TestVisible | ||
private static Map<String, List<RollupSummary>> summariesCacheByKey = new Map<String, List<RollupSummary>>(); | ||
|
||
private List<RollupSummary> allSummaries; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per-instance data that we can access to fill requests.
@@ -49,16 +54,42 @@ public class RollupSummariesSelector { | |||
enforceSecurity, | |||
forEdit | |||
); | |||
|
|||
String cacheKey = String.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The data cache key is the security/usage parameters provided in the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which results in something like false|true
- for readability's sake, what do you think about String.valueOf(cacheKeyEnum)
, cacheKeyEnum
being a readable representation of the four states
SelectedForReadWithSecurity
SelectedForEditWithSecurity
SelectedForReadWithoutSecurity
SelectedForEditWithoutSecurity
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that. I didn't make reading the cache key a priority because it was never exposed anywhere. I didn't think anyone would need it for troubleshooting or comprehending the code.
I wouldn't want to change the constructor interface so the caller passed in an Enum, I think that would be a very large change. Converting the current passed in variables to an enum would just be a multi-step if
chain that didn't feel like was adding value.
I think if we wanted to build a string based on constructor variables that could make sense, rather that deriving one from the string values directly. That could be done easily with two ternary statements.
I'll put through a change so we can see if it feels better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One additional thought on this. Maybe the concern is more about tests that need to pre-fill caches, in which case they need to know the cache key so a human will have to read/set the data.
What if we provided a @TestVisible
method using the recommended Enum to set the cache value. It would mean we'd have to go ahead and add an internal method with the if
chain that translates the constructor arguments to an enum, I'd probably also expose an additional constructor override that allows you to pass the enum directly and we'd slowly convert the codebase to use that. This would be a much longer play but the test classes that need to set the cache directly might really appreciate it down the road.
I'd probably provide a public method to clear the cache as well, no parameters though, just burn all the caches.
I'm also thinking about the ideal future where test classes can set RollupSummary config state without inserting records, this will be necessary when we eventually want to deprecate the Custom Object which is still used heavily in our tests.
records = m_customObjectSelector.selectById(idSet); | ||
records.addAll(m_customMetadataSelector.selectById(idSet)); | ||
records.addAll(m_customMetadataSelector.selectByDeveloperName(idSet)); | ||
for (RollupSummary rs : this.allSummaries) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Outer methods now pull from the local instance's memory.
/** | ||
* Returns active lookup rollup summary definitions for the given rollup unique names | ||
**/ | ||
public List<RollupSummary> selectByUniqueName(Set<String> uniqueNames) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to enable mapping the stub mdt records for scheduled items in RollupService.
@@ -167,99 +219,6 @@ public class RollupSummariesSelector { | |||
return LookupRollupSummary__c.sObjectType; | |||
} | |||
|
|||
public List<RollupSummary> selectById(Set<String> idSet) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that we're not running queries for specific use cases we are able to remove nearly all of these per-type queries.
} | ||
|
||
rollupSummaryComparables.sort(); | ||
private static List<RollupSummary> sortSummaries( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sortSummaries moved to the outer class scope so it can be access but the new logic to pull from in memory data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, Anthony. Much appreciated.
@@ -807,8 +807,7 @@ private with sharing class RollupServiceTest { | |||
// Assert rollup | |||
System.assertEquals( | |||
0, | |||
[SELECT AnnualRevenue FROM Account WHERE Id = :account.Id] | |||
.AnnualRevenue | |||
[SELECT AnnualRevenue FROM Account WHERE Id = :account.Id].AnnualRevenue |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these seem auto formatting changes? How did these happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My computer runs prettier on each save. I don't know why these lines differed on this execution of prettier.
@@ -133,6 +133,8 @@ private class RollupServiceTest5 { | |||
rollupSummaryOpp.CalculationMode__c = 'Scheduled'; | |||
insert rollupSummaryOpp; | |||
|
|||
RollupSummariesSelector.summariesCacheByKey = new Map<String, List<RollupSummary>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am surprised to find the necessity to reset the @testvisible cache here but not in other test methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first time you cause a rollup to be executed during a test the cache is built. For a few tests we run once then insert a new Rollup record and cause the rollups to be executed again. In those cases the cached data doesn't hold the new config records and thus we have to invalidate the cache before proceeding.
This scenarios should never be an issue in production execution because we shouldn't be modifying rollup configs as a product of a transaction that executes a rollup. (At least I've never heard of it happening)
If we thought it might be a concern. We already have a trigger on the custom objects, we could add to that trigger logic to invalidate the cache after insert/update.
I didn't do anything about it because the custom object is a relic that should be deprecated and removed rather than reinforced and nobody could update the CMDT real-time as part of a triggered action.
@@ -49,16 +54,42 @@ public class RollupSummariesSelector { | |||
enforceSecurity, | |||
forEdit | |||
); | |||
|
|||
String cacheKey = String.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which results in something like false|true
- for readability's sake, what do you think about String.valueOf(cacheKeyEnum)
, cacheKeyEnum
being a readable representation of the four states
SelectedForReadWithSecurity
SelectedForEditWithSecurity
SelectedForReadWithoutSecurity
SelectedForEditWithoutSecurity
return false; | ||
} | ||
|
||
private Boolean containsIgnoreCase(Set<String> collection, String val) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with api 58 ( see your other PR) set implements iterable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that would be a great way to do it. Just take a param of iterable string and not need the overrides. I expect the 58 to be merged first then I can modify this PR before it is merged. Thanks!
@@ -243,6 +246,9 @@ private with sharing class RollupServiceTest3 { | |||
rollupSummary.CalculationMode__c = 'Scheduled'; | |||
insert rollupSummary; | |||
|
|||
// burn the cache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Examples of clearing the cache mid-transaction.
RollupSummariesSelector rollupSelector = new RollupSummariesSelector(false, false); | ||
rollupSelector.m_customMetadataSelector = mock; | ||
// fill cache | ||
RollupSummariesSelector.setRollupCache( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New test-only means of settings the cache without having to know how the cache key is constructed.
|
||
private List<RollupSummary> allSummaries; | ||
|
||
public static void clearRollupCache() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New method to clear the existing cache of data, if needed.
} | ||
|
||
@TestVisible | ||
private static void setRollupCache( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Useful data mocking if needed. Will be very useful when we want to refactor out all the tests that add Custom Objects to use Custom Metadata while being able to ignore what might already exist in the org.
@aheber It looks like this test failed when you merged main branch into this branch. |
Thanks, I'll get it cleaned up today. |
Leveraging new ability for Set to implement Iterable in v58.0
} | ||
} | ||
|
||
private static String getCacheKey(boolean enforceSecurity, boolean forEdit) { | ||
return String.format( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!
private Boolean containsIgnoreCase(List<String> collection, String val) { | ||
if (val == null) { | ||
return collection.contains(val); | ||
private Boolean containsIgnoreCase(Iterable<String> collection, String val) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Elegant. but s
, i
, val
are a bit crude :)
looks like this is stuck on automated tests. |
@sfenton3 this has passing tests again. |
@aheber All set, thanks! |
Significant refactor of the selector layer to query the config records from both sources once and cache them. Cache's must be separate by constructor's different security scopes to avoid leaking data.
Critical Changes
Huge changes to how Rollup Configs are retrieved. Primarily to keep SOQL counts low in the event of recursion or chained rollups where one rollup update may cascade into another rollup. Not a big issue for most people but we do get reports of DLRS running over it's query limits from time to time.
This is a high risk change because this impacts nearly every aspect of the application. Will need extra QA on this including UI-based testing because this powers the config editing interfaces as well as the rollup engine.
Issues Closed
#1256