Skip to content

Commit 3c5e5d6

Browse files
authored
v0.1.2 - Bulkification updates for flow (#10)
* Bulkification updates for flow
1 parent e98ca44 commit 3c5e5d6

File tree

8 files changed

+82
-51
lines changed

8 files changed

+82
-51
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
## Deployment
77

8-
<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008fjhFAAQ">
8+
<a href="https://login.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008fjhyAAA">
99
<img alt="Deploy to Salesforce"
1010
src="./media/deploy-package-to-prod.png">
1111
</a>
1212

13-
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008fjhFAAQ">
13+
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008fjhyAAA">
1414
<img alt="Deploy to Salesforce Sandbox"
1515
src="./media/deploy-package-to-sandbox.png">
1616
</a>

core/classes/FlowRoundRobinAssigner.cls

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
global without sharing class FlowRoundRobinAssigner {
22
@TestVisible
33
private static RoundRobinAssigner.IAssignmentRepo stubAssignmentRepo;
4-
@TestVisible
5-
private static Boolean hasBeenUpdated = false;
64

75
private static final Set<Id> PROCESSED_RECORD_IDS = new Set<Id>();
86
private static final FlowRoundRobinAssigner SELF = new FlowRoundRobinAssigner();
@@ -26,16 +24,23 @@ global without sharing class FlowRoundRobinAssigner {
2624

2725
@InvocableMethod(category='Round Robin' label='Round robin records')
2826
global static void assign(List<FlowInput> flowInputs) {
29-
if (hasBeenUpdated == false) {
30-
for (FlowInput input : flowInputs) {
31-
if (input.recordsToRoundRobin?.isEmpty() != false && input.recordToRoundRobin != null) {
32-
input.recordsToRoundRobin = new List<SObject>{ input.recordToRoundRobin };
33-
}
34-
if (input.recordsToRoundRobin.isEmpty() == false) {
35-
SELF.trackAssignedIds(input);
36-
SELF.roundRobin(input);
37-
}
27+
FlowInput bulkifiedInput;
28+
for (FlowInput input : flowInputs) {
29+
if (bulkifiedInput == null) {
30+
bulkifiedInput = input;
31+
}
32+
if (input.recordsToRoundRobin?.isEmpty() != false && input.recordToRoundRobin != null) {
33+
input.recordsToRoundRobin = new List<SObject>{ input.recordToRoundRobin };
3834
}
35+
bulkifiedInput.recordsToRoundRobin.addAll(input.recordsToRoundRobin);
36+
}
37+
38+
if (bulkifiedInput?.recordsToRoundRobin.isEmpty() == false) {
39+
SELF.trackAssignedIds(bulkifiedInput);
40+
SELF.roundRobin(bulkifiedInput);
41+
}
42+
if (bulkifiedInput?.updateRecords == true) {
43+
update bulkifiedInput.recordsToRoundRobin;
3944
}
4045
}
4146

@@ -44,10 +49,6 @@ global without sharing class FlowRoundRobinAssigner {
4449
RoundRobinAssigner.IAssignmentRepo assignmentRepo = this.getAssignmentRepo(input);
4550
RoundRobinAssigner.Details assignmentDetails = this.getAssignmentDetails(input);
4651
new RoundRobinAssigner(assignmentRepo, assignmentDetails).assignOwners(input.recordsToRoundRobin);
47-
if (input.updateRecords) {
48-
update input.recordsToRoundRobin;
49-
hasBeenUpdated = true;
50-
}
5152
}
5253

5354
private void validateInput(FlowInput input) {

core/classes/FlowRoundRobinAssignerTests.cls

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ private class FlowRoundRobinAssignerTests {
6060

6161
FlowRoundRobinAssigner.assign(new List<FlowRoundRobinAssigner.FlowInput>{ input });
6262

63-
System.assertEquals(true, FlowRoundRobinAssigner.hasBeenUpdated);
6463
System.assertEquals(UserInfo.getUserId(), cpa.OwnerId);
6564
}
6665

core/classes/QueryAssigner.cls

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
public without sharing class QueryAssigner implements RoundRobinAssigner.IAssignmentRepo {
2+
private static final Map<String, List<SObject>> QUERY_TO_RECORDS = new Map<String, List<SObject>>();
23
private final List<Id> validAssignmentIds;
34

45
public QueryAssigner(String query, String assignmentFieldName) {
56
Set<Id> assignmentIds = new Set<Id>();
6-
List<SObject> matchingRecords = Database.query(query);
7+
List<SObject> matchingRecords;
8+
if (QUERY_TO_RECORDS.containsKey(query)) {
9+
matchingRecords = QUERY_TO_RECORDS.get(query);
10+
} else {
11+
matchingRecords = Database.query(query);
12+
QUERY_TO_RECORDS.put(query, matchingRecords);
13+
}
14+
715
for (SObject matchingRecord : matchingRecords) {
816
assignmentIds.add((Id) matchingRecord.get(assignmentFieldName));
917
}

core/classes/RoundRobinRepository.cls

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
public without sharing class RoundRobinRepository extends AbstractCacheRepo {
22
private static Map<String, RoundRobin__c> CACHED_ASSIGNMENTS;
33

4-
@SuppressWarnings('PMD.ApexCRUDViolation')
54
public void accept(IThreadSafeCacheVisitor visitor, List<SObject> records) {
65
RoundRobin__c currentAssignment = this.getCurrentAssignment(visitor.getVisitKey());
76
visitor.visitRecords(records, currentAssignment);
@@ -47,39 +46,56 @@ public without sharing class RoundRobinRepository extends AbstractCacheRepo {
4746

4847
@SuppressWarnings('PMD.ApexCRUDViolation')
4948
private Boolean commitUpdatedAssignment(RoundRobin__c assignment) {
50-
Boolean wasCommitSuccessful = true;
5149
Map<String, RoundRobin__c> currentCache = this.getCachedAssignments();
5250
if (
5351
currentCache.containsKey(assignment.Name) &&
5452
currentCache.get(assignment.Name).LastUpdated__c > CACHED_ASSIGNMENTS.get(assignment.Name).LastUpdated__c
5553
) {
56-
assignment = currentCache.get(assignment.Name);
57-
wasCommitSuccessful = false;
58-
} else {
59-
assignment.LastUpdated__c = System.now();
60-
/**
61-
* integration tests with after save Flows have shown something unfortunate:
62-
* though the second (recursive) call to the assigner is spawned in a second transaction
63-
* the RoundRobin__c.getAll() still doesn't contain the Id of the inserted record (for the times where the assignment
64-
* is being run for the first time).
65-
* That means that we can't just call "upsert", and instead have to do this goofy
66-
* song and dance to ensure the Id is appended correctly
67-
*/
68-
if (assignment.Id == null) {
69-
List<RoundRobin__c> existingAssignments = [SELECT Id FROM RoundRobin__c WHERE Name = :assignment.Name];
70-
if (existingAssignments.isEmpty() == false) {
71-
assignment.Id = existingAssignments[0].Id;
72-
}
54+
return false;
55+
}
56+
assignment.LastUpdated__c = System.now();
57+
/**
58+
* integration tests with after save Flows have shown something unfortunate:
59+
* though the second (recursive) call to the assigner is spawned in a second transaction
60+
* the RoundRobin__c.getAll() call still doesn't contain the Id of the inserted record (for the times where the assignment
61+
* is being run for the first time).
62+
* That means that we can't just call "upsert", and instead have to do this goofy
63+
* song and dance to ensure the Id is appended correctly
64+
*/
65+
if (assignment.Id == null) {
66+
List<RoundRobin__c> existingAssignments = [SELECT Id FROM RoundRobin__c WHERE Name = :assignment.Name];
67+
if (existingAssignments.isEmpty() == false) {
68+
assignment.Id = existingAssignments[0].Id;
7369
}
74-
if (assignment.Id != null) {
75-
update assignment;
76-
} else {
77-
insert assignment;
70+
}
71+
if (assignment.Id != null) {
72+
try {
73+
/**
74+
* if two separate threads are trying to round robin at the same time, the LastUpdated__c check above
75+
* isn't enough to ensure write-safety, and unfortunately FOR UPDATE is the only mutex Apex offers
76+
* as a write-safe guarantee. One downside (among many) is that FOR UPDATE frequently throws; another is
77+
* that another locking thread can release early - let's protect against both those eventualities
78+
*/
79+
RoundRobin__c lockedAssignment = [SELECT Id, Name, LastUpdated__c FROM RoundRobin__c WHERE Id = :assignment.Id FOR UPDATE];
80+
if (lockedAssignment.LastUpdated__c >= assignment.LastUpdated__c) {
81+
// lock was released early, but the existing Index__c now almost certainly has stale values in it
82+
// re-round robin to get the now-correct values
83+
return false;
84+
}
85+
lockedAssignment.Index__c = assignment.Index__c;
86+
lockedAssignment.LastUpdated__c = assignment.LastUpdated__c;
87+
update lockedAssignment;
88+
// purely for the map assignment, below
89+
assignment = lockedAssignment;
90+
} catch (DmlException ex) {
91+
return false;
7892
}
93+
} else {
94+
insert assignment;
7995
}
8096

8197
CACHED_ASSIGNMENTS.put(assignment.Name, assignment);
82-
return wasCommitSuccessful;
98+
return true;
8399
}
84100

85101
private Map<String, RoundRobin__c> getCachedAssignments() {

integration-tests/classes/RoundRobinFlowIntegrationTests.cls

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ private class RoundRobinFlowIntegrationTests {
1414
roundRobinUser.LastName = 'roundRobinUser';
1515
insert roundRobinUser;
1616

17-
Lead lead = new Lead(LastName = 'Test Assignment', Company = 'Test');
18-
insert lead;
17+
List<Lead> leads = new List<Lead>();
18+
// stress test!
19+
for (Integer index = 0; index < 200; index++) {
20+
leads.add(new Lead(LastName = 'Assignment ' + index, Company = 'Round Robin'));
21+
}
22+
insert leads;
1923

20-
lead = [SELECT Id, OwnerId FROM Lead];
21-
System.assertEquals(roundRobinUser.Id, lead.OwnerId);
24+
leads = [SELECT Id, OwnerId FROM Lead];
25+
for (Lead lead : leads) {
26+
System.assertEquals(roundRobinUser.Id, lead.OwnerId);
27+
}
2228
}
2329
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "salesforce-round-robin",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"description": "Round robin records in Salesforce (SFDC) using Flow or Apex. Performant, fair, fast assignment with configurable user pools",
55
"repository": {
66
"type": "git",

sfdx-project.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"definitionFile": "config/project-scratch-def.json",
66
"package": "salesforce-round-robin",
77
"path": "core",
8-
"versionName": "Reverts mutex due to bulkification issues with Flow, adds more safety for recursive Flow transactions",
9-
"versionNumber": "0.1.1.0",
8+
"versionName": "Better flow bulkification for updates, reinstated mutex",
9+
"versionNumber": "0.1.2.0",
1010
"versionDescription": "Invocable & Apex-ready Round Robin Assigner for Salesforce Flow, Process Builder, Apex and more",
1111
"releaseNotesUrl": "https://github.com/jamessimone/salesforce-round-robin/releases/latest"
1212
},
@@ -21,6 +21,7 @@
2121
"salesforce-round-robin": "0Ho6g000000GnClCAK",
2222
"[email protected]": "04t6g000008SjZEAA0",
2323
"[email protected]": "04t6g000008SjpyAAC",
24-
"[email protected]": "04t6g000008fjhFAAQ"
24+
"[email protected]": "04t6g000008fjhFAAQ",
25+
"[email protected]": "04t6g000008fjhyAAA"
2526
}
2627
}

0 commit comments

Comments
 (0)