|
1 | 1 | public without sharing class RoundRobinRepository extends AbstractCacheRepo { |
2 | 2 | private static Map<String, RoundRobin__c> CACHED_ASSIGNMENTS; |
3 | 3 |
|
4 | | - @SuppressWarnings('PMD.ApexCRUDViolation') |
5 | 4 | public void accept(IThreadSafeCacheVisitor visitor, List<SObject> records) { |
6 | 5 | RoundRobin__c currentAssignment = this.getCurrentAssignment(visitor.getVisitKey()); |
7 | 6 | visitor.visitRecords(records, currentAssignment); |
@@ -47,39 +46,56 @@ public without sharing class RoundRobinRepository extends AbstractCacheRepo { |
47 | 46 |
|
48 | 47 | @SuppressWarnings('PMD.ApexCRUDViolation') |
49 | 48 | private Boolean commitUpdatedAssignment(RoundRobin__c assignment) { |
50 | | - Boolean wasCommitSuccessful = true; |
51 | 49 | Map<String, RoundRobin__c> currentCache = this.getCachedAssignments(); |
52 | 50 | if ( |
53 | 51 | currentCache.containsKey(assignment.Name) && |
54 | 52 | currentCache.get(assignment.Name).LastUpdated__c > CACHED_ASSIGNMENTS.get(assignment.Name).LastUpdated__c |
55 | 53 | ) { |
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; |
73 | 69 | } |
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; |
78 | 92 | } |
| 93 | + } else { |
| 94 | + insert assignment; |
79 | 95 | } |
80 | 96 |
|
81 | 97 | CACHED_ASSIGNMENTS.put(assignment.Name, assignment); |
82 | | - return wasCommitSuccessful; |
| 98 | + return true; |
83 | 99 | } |
84 | 100 |
|
85 | 101 | private Map<String, RoundRobin__c> getCachedAssignments() { |
|
0 commit comments