Skip to content

Commit

Permalink
v0.1.2 - Bulkification updates for flow (#10)
Browse files Browse the repository at this point in the history
* Bulkification updates for flow
  • Loading branch information
jamessimone authored Mar 17, 2023
1 parent e98ca44 commit 3c5e5d6
Show file tree
Hide file tree
Showing 8 changed files with 82 additions and 51 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

## Deployment

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

<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008fjhFAAQ">
<a href="https://test.salesforce.com/packaging/installPackage.apexp?p0=04t6g000008fjhyAAA">
<img alt="Deploy to Salesforce Sandbox"
src="./media/deploy-package-to-sandbox.png">
</a>
Expand Down
31 changes: 16 additions & 15 deletions core/classes/FlowRoundRobinAssigner.cls
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
global without sharing class FlowRoundRobinAssigner {
@TestVisible
private static RoundRobinAssigner.IAssignmentRepo stubAssignmentRepo;
@TestVisible
private static Boolean hasBeenUpdated = false;

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

@InvocableMethod(category='Round Robin' label='Round robin records')
global static void assign(List<FlowInput> flowInputs) {
if (hasBeenUpdated == false) {
for (FlowInput input : flowInputs) {
if (input.recordsToRoundRobin?.isEmpty() != false && input.recordToRoundRobin != null) {
input.recordsToRoundRobin = new List<SObject>{ input.recordToRoundRobin };
}
if (input.recordsToRoundRobin.isEmpty() == false) {
SELF.trackAssignedIds(input);
SELF.roundRobin(input);
}
FlowInput bulkifiedInput;
for (FlowInput input : flowInputs) {
if (bulkifiedInput == null) {
bulkifiedInput = input;
}
if (input.recordsToRoundRobin?.isEmpty() != false && input.recordToRoundRobin != null) {
input.recordsToRoundRobin = new List<SObject>{ input.recordToRoundRobin };
}
bulkifiedInput.recordsToRoundRobin.addAll(input.recordsToRoundRobin);
}

if (bulkifiedInput?.recordsToRoundRobin.isEmpty() == false) {
SELF.trackAssignedIds(bulkifiedInput);
SELF.roundRobin(bulkifiedInput);
}
if (bulkifiedInput?.updateRecords == true) {
update bulkifiedInput.recordsToRoundRobin;
}
}

Expand All @@ -44,10 +49,6 @@ global without sharing class FlowRoundRobinAssigner {
RoundRobinAssigner.IAssignmentRepo assignmentRepo = this.getAssignmentRepo(input);
RoundRobinAssigner.Details assignmentDetails = this.getAssignmentDetails(input);
new RoundRobinAssigner(assignmentRepo, assignmentDetails).assignOwners(input.recordsToRoundRobin);
if (input.updateRecords) {
update input.recordsToRoundRobin;
hasBeenUpdated = true;
}
}

private void validateInput(FlowInput input) {
Expand Down
1 change: 0 additions & 1 deletion core/classes/FlowRoundRobinAssignerTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ private class FlowRoundRobinAssignerTests {

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

System.assertEquals(true, FlowRoundRobinAssigner.hasBeenUpdated);
System.assertEquals(UserInfo.getUserId(), cpa.OwnerId);
}

Expand Down
10 changes: 9 additions & 1 deletion core/classes/QueryAssigner.cls
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
public without sharing class QueryAssigner implements RoundRobinAssigner.IAssignmentRepo {
private static final Map<String, List<SObject>> QUERY_TO_RECORDS = new Map<String, List<SObject>>();
private final List<Id> validAssignmentIds;

public QueryAssigner(String query, String assignmentFieldName) {
Set<Id> assignmentIds = new Set<Id>();
List<SObject> matchingRecords = Database.query(query);
List<SObject> matchingRecords;
if (QUERY_TO_RECORDS.containsKey(query)) {
matchingRecords = QUERY_TO_RECORDS.get(query);
} else {
matchingRecords = Database.query(query);
QUERY_TO_RECORDS.put(query, matchingRecords);
}

for (SObject matchingRecord : matchingRecords) {
assignmentIds.add((Id) matchingRecord.get(assignmentFieldName));
}
Expand Down
64 changes: 40 additions & 24 deletions core/classes/RoundRobinRepository.cls
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
public without sharing class RoundRobinRepository extends AbstractCacheRepo {
private static Map<String, RoundRobin__c> CACHED_ASSIGNMENTS;

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

@SuppressWarnings('PMD.ApexCRUDViolation')
private Boolean commitUpdatedAssignment(RoundRobin__c assignment) {
Boolean wasCommitSuccessful = true;
Map<String, RoundRobin__c> currentCache = this.getCachedAssignments();
if (
currentCache.containsKey(assignment.Name) &&
currentCache.get(assignment.Name).LastUpdated__c > CACHED_ASSIGNMENTS.get(assignment.Name).LastUpdated__c
) {
assignment = currentCache.get(assignment.Name);
wasCommitSuccessful = false;
} else {
assignment.LastUpdated__c = System.now();
/**
* integration tests with after save Flows have shown something unfortunate:
* though the second (recursive) call to the assigner is spawned in a second transaction
* the RoundRobin__c.getAll() still doesn't contain the Id of the inserted record (for the times where the assignment
* is being run for the first time).
* That means that we can't just call "upsert", and instead have to do this goofy
* song and dance to ensure the Id is appended correctly
*/
if (assignment.Id == null) {
List<RoundRobin__c> existingAssignments = [SELECT Id FROM RoundRobin__c WHERE Name = :assignment.Name];
if (existingAssignments.isEmpty() == false) {
assignment.Id = existingAssignments[0].Id;
}
return false;
}
assignment.LastUpdated__c = System.now();
/**
* integration tests with after save Flows have shown something unfortunate:
* though the second (recursive) call to the assigner is spawned in a second transaction
* the RoundRobin__c.getAll() call still doesn't contain the Id of the inserted record (for the times where the assignment
* is being run for the first time).
* That means that we can't just call "upsert", and instead have to do this goofy
* song and dance to ensure the Id is appended correctly
*/
if (assignment.Id == null) {
List<RoundRobin__c> existingAssignments = [SELECT Id FROM RoundRobin__c WHERE Name = :assignment.Name];
if (existingAssignments.isEmpty() == false) {
assignment.Id = existingAssignments[0].Id;
}
if (assignment.Id != null) {
update assignment;
} else {
insert assignment;
}
if (assignment.Id != null) {
try {
/**
* if two separate threads are trying to round robin at the same time, the LastUpdated__c check above
* isn't enough to ensure write-safety, and unfortunately FOR UPDATE is the only mutex Apex offers
* as a write-safe guarantee. One downside (among many) is that FOR UPDATE frequently throws; another is
* that another locking thread can release early - let's protect against both those eventualities
*/
RoundRobin__c lockedAssignment = [SELECT Id, Name, LastUpdated__c FROM RoundRobin__c WHERE Id = :assignment.Id FOR UPDATE];
if (lockedAssignment.LastUpdated__c >= assignment.LastUpdated__c) {
// lock was released early, but the existing Index__c now almost certainly has stale values in it
// re-round robin to get the now-correct values
return false;
}
lockedAssignment.Index__c = assignment.Index__c;
lockedAssignment.LastUpdated__c = assignment.LastUpdated__c;
update lockedAssignment;
// purely for the map assignment, below
assignment = lockedAssignment;
} catch (DmlException ex) {
return false;
}
} else {
insert assignment;
}

CACHED_ASSIGNMENTS.put(assignment.Name, assignment);
return wasCommitSuccessful;
return true;
}

private Map<String, RoundRobin__c> getCachedAssignments() {
Expand Down
14 changes: 10 additions & 4 deletions integration-tests/classes/RoundRobinFlowIntegrationTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ private class RoundRobinFlowIntegrationTests {
roundRobinUser.LastName = 'roundRobinUser';
insert roundRobinUser;

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

lead = [SELECT Id, OwnerId FROM Lead];
System.assertEquals(roundRobinUser.Id, lead.OwnerId);
leads = [SELECT Id, OwnerId FROM Lead];
for (Lead lead : leads) {
System.assertEquals(roundRobinUser.Id, lead.OwnerId);
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "salesforce-round-robin",
"version": "0.1.1",
"version": "0.1.2",
"description": "Round robin records in Salesforce (SFDC) using Flow or Apex. Performant, fair, fast assignment with configurable user pools",
"repository": {
"type": "git",
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 @@
"definitionFile": "config/project-scratch-def.json",
"package": "salesforce-round-robin",
"path": "core",
"versionName": "Reverts mutex due to bulkification issues with Flow, adds more safety for recursive Flow transactions",
"versionNumber": "0.1.1.0",
"versionName": "Better flow bulkification for updates, reinstated mutex",
"versionNumber": "0.1.2.0",
"versionDescription": "Invocable & Apex-ready Round Robin Assigner for Salesforce Flow, Process Builder, Apex and more",
"releaseNotesUrl": "https://github.com/jamessimone/salesforce-round-robin/releases/latest"
},
Expand All @@ -21,6 +21,7 @@
"salesforce-round-robin": "0Ho6g000000GnClCAK",
"salesforce-round-robin@0.0.4-0": "04t6g000008SjZEAA0",
"salesforce-round-robin@0.1.0-0": "04t6g000008SjpyAAC",
"salesforce-round-robin@0.1.1-0": "04t6g000008fjhFAAQ"
"salesforce-round-robin@0.1.1-0": "04t6g000008fjhFAAQ",
"salesforce-round-robin@0.1.2-0": "04t6g000008fjhyAAA"
}
}

0 comments on commit 3c5e5d6

Please sign in to comment.