Problem/Motivation
Let me set up a little scenario that hopefully illustrates this bug:
1. Say you have a pull mapping set up for Accounts in Salesforce.
2. Say your cron task is set to run hourly, and there are 100,000 items to process in the pull queue.
3. 7:00AM: Cron runs and processes 25,000 items, but there are still 75,000 items left to process in the queue.
4. 7:30AM: "Acme Corporation" Account's name has been updated to "Acme Corp." on Salesforce.
5. 8:00AM: Cron runs and adds the "Acme Corporation" -> "Acme Corp." update to the queue and processes say another 25,000 items - leaving 50,001 items left to process in the queue.
6. 8:30AM: "Acme Corp." Account's name has been updated to from "Acme Corp." to "Acme Inc." on Salesforce.
7. 9:00AM: Cron runs and adds the "Acme Corp." to "Acme Inc." update to the queue and processes say another 25,000 items - leaving 25,002 items left to process in the queue.
8. 10:00AM: Cron runs and processes say another 25,000 items - leaving just two items left to process in the queue (our two updates to Acme).
9. 11:00AM: Cron runs and processes the "Acme Corporation" -> "Acme Corp." update first. As part of this it sets $mapping_object->entity_updated = time(); (11:00AM). The account's name is now "Acme Corp." in Drupal. The second update is now ready for processing ("Acme Corp." to "Acme Inc."). BUT this update is skipped, because salesforce_pull_process_records() only updates a record if $sf_object_updated (8:30AM) > $mapping_object->entity_updated (11:00AM).
Result: All data was queued and processed, yet we have "Acme Inc." for the Account's name on Salesforce and "Acme Corp." for the Account's name in Drupal (which is incorrect because the second update was skipped).
To summarize this bug: If multiple updates for a single record land in the pull queue together, only the first update is processed.
Proposed resolution
(Maybe) When updating or creating a drupal record via salesforce pull, set $mapping_object->entity_updated = $sf_object_updated? Or create a new property on Salesforce Mapping Objects - something like $mapping_object->sf_object_updated and check against that? Needs discussion...
Remaining tasks
Refine proposed resolution, resolve via patch, reviews.
| Comment | File | Size | Author |
|---|---|---|---|
| #11 | salesforce-multiple-updates-in-queue-bug-3002919-11-stable-3-2.patch | 2.62 KB | chrisolof |
| #10 | salesforce-multiple-updates-in-queue-bug-3002919-10-4.patch | 2.67 KB | chrisolof |
Comments
Comment #2
aaronbaumanIs this not an issue for 8.x as well?
Comment #3
chrisolofYes it looks like this probably affects 8.x as well. I have not verified it with actual testing, but the logic looks the same (see MappedObject.php's pull() method and and PullBase.php's updateEntity() method).
From what I can tell this check stems from trying to prevent overwriting newer data on Drupal with older data from Salesforce. Say a record is updated directly on Drupal before an older update that occurred on Salesforce is processed. It would seem that this check tries to prevent the older data from overwriting the newer data.
Seems like a good idea and the more I look at it I'm starting to think the check itself may need adjustment/improvement, rather than what's stored.
Idea:
Imagine we have $entity_updated, which actually reflects the last time the Drupal entity in question was updated/modified (by any route) - not just what's in $smo->entity_updated. Let $smo->entity_updated reflect the last time we, the Salesforce module updated the Drupal entity.
I think this approach could solve the issue and require no additions/changes to existing data structures.
Comment #4
aaronbaumanif i'm reading this correctly, the core issue is that the pull queue contains multiple entries for the same record-pair
In that case, it might be more straightforward (although probably greater effort) to extend core queue so that we can maintain a single pull queue item per record-pair. this would also make pull queue processing more efficient, since we'd have to process a record-pair only once.
at a higher level, the decision to base an entire update on a single timestamp field has always irked me.
more broadly, i'd like to be able to specify a system of record to resolve these conflicts per-mapping (maybe even per-field?), rather than relying on timestamps.
Comment #5
chrisolofInteresting... So rather than blindly add to the queue, if an unprocessed queue item already exists for a record, simply update that existing item in the queue with current data...
I think given the structure of the queue table we'd be somewhat limited here unless we identify the salesforce ID or mapping object ID (which may not yet exist for new records - tricky) in the queue name - which is what I think you're suggesting. But setting up a queue per SF ID (or record pair - though you wouldn't necessarily know the drupal ID at the point of queuing) seems like it would introduce more complexity in processing which could easily outweigh anything saved by processing only the most recent update for a record.
And I think the problem with the timestamp check would remain. Too often the check fails to find the Drupal entity's actual last modified/updated date and instead falls back to $smo->entity_updated, which I feel is really the root of the problem. Drupal-side record updates that are more recent than the data in the queue could still get wiped out.
So for those reasons I'm still thinking the approach in #3 may be the way to go. It eliminates the bug and improves the timestamp check (to prevent overwriting newer data on Drupal with older data from Salesforce).
But I'd like to add an additional idea here: the most complex (and undocumented) part of the timestamp check outlined in #3 is determining where to source the last modified timestamp for the given Drupal entity. We could try to guess better with more code - which was what I was originally thinking. Or, perhaps better, we could allow users to provide that (select the correct property on the Drupal entity) in the mapping settings. Basically the flip side of this:
With that I think we'd really be in a good spot. A checkbox for turning the timestamp check off entirely would also be nice.
Anyway if you think this is a viable approach I'd be willing to craft a patch for review.
Comment #6
aaronbaumanI like the idea of explicitly setting a date field to compare.
I don't think this solves the problem of multiple updates to one record during the same cron run, but seems like it's necessary either way.
Also in D8 we have EntityChangedInterface, which we should use for default, if implemented.
For D7, i think you're right, it's probably not worth the overhead of maintaining and managing a separate queue if we can solve this another way. (Been down that road in the 7.x-2.x branch.)
For D8, we're already doing this for the push queue, thanks to a much more robust queue api.
Drupal\salesforce_push\PushQueueextends core database queue by normalizing queue item data, so that we can optimize adding items to the queue. APullQueueclass would allow for similar optimizations, e.g. using SFID as a unique key.Clearly these 2 issues are tightly related, but I don't think they're strictly interdepent.
Maybe these should be two separate threads?
Comment #7
chrisolofLet me expand on how I think the proposed logic change solves the bug. Here's the logic:
Illustration of how this solves the issue:
1. Say you have a pull mapping set up for Accounts in Salesforce.
2. Say your cron task is set to run hourly, and there are 100,000 items to process in the pull queue.
3. 7:00AM: Cron runs and processes 25,000 items, but there are still 75,000 items left to process in the queue.
4. 7:30AM: "Acme Corporation" Account's name has been updated to "Acme Corp." on Salesforce.
5. 8:00AM: Cron runs and adds the "Acme Corporation" -> "Acme Corp." update to the queue and processes say another 25,000 items - leaving 50,001 items left to process in the queue.
6. 8:30AM: "Acme Corp." Account's name has been updated to from "Acme Corp." to "Acme Inc." on Salesforce.
7. 9:00AM: Cron runs and adds the "Acme Corp." to "Acme Inc." update to the queue and processes say another 25,000 items - leaving 25,002 items left to process in the queue.
8. 10:00AM: Cron runs and processes say another 25,000 items - leaving just two items left to process in the queue (our two updates to Acme).
9. 11:00AM: Cron runs and processes the "Acme Corporation" -> "Acme Corp." update first. As part of this it sets $mapping_object->entity_updated = time(); (11:00AM). The account's name is now "Acme Corp." in Drupal. The second update is now ready for processing ("Acme Corp." to "Acme Inc."). This second update is processed as well because $updated_on_drupal_after_last_pull is FALSE ($entity_updated (11:00AM) is not greater than $smo->entity_updated (11:00AM)).
Result: All data was queued and processed, and we have "Acme Inc." for the Account's name on both Salesforce and Drupal.
Sourcing $entity_updated in the above is what led me to the Drupal entity last modified date property setting idea in the mapping. Takes the guesswork out of it. I think you're right that we could split that part off into a separate issue - and it involves its own discussion surrounding sane defaults, upgrade path, and interface additions. For this issue we could keep the entity's last modified date source unaltered. In other words we'd keep using $entity->updated for the source. The only slight difference to the sourcing there, which also is what fixes the bug, is that we would never source the entity's last modified date from $smo->entity_updated in cases where $entity->updated is not available (in the logic above, $entity_updated would simply be NULL in those cases). In fact the only consequence I can think of surrounding that slight sourcing change (combined with the new record-skip logic) is the bug at hand being squashed.
Anyway that's the patch I'm thinking of writing, pending your buy-in on approach (of course).
Comment #8
aaronbaumanyeah, this makes sense.
i guess it's the best approach for D7, among all the not great options
this is probably gonna be pretty edge case for my own projects.
we should get input from someone at ThinkShout or anyone else who's relying big pull queue loads in 7.x before we push this though.
Comment #9
chrisolofD7 patch in the works.
Comment #10
chrisolofI'll be testing this today to ensure it squashes the bug.
Comment #11
chrisolofThis is the same patch, but against the stable 7.x-3.2 release.
Comment #12
chrisolofJust confirming that this patch is working perfectly for me in tests.
To test I needed a pile of updates to the same record in the queue. I achieved this by setting 'skip on cron' TRUE in salesforce_pull.module:
I then updated the name of a record in salesforce to "Update 1" and ran cron. After this I verified the update was sitting in the queue and that the record was unchanged in Drupal. I repeated this until I had three name change updates sitting in the queue ("Update 1", "Update 2", and "Update 3"). So three updates aimed at the same Drupal record, all sitting together in the queue.
I then set 'skip on cron' to FALSE and ran cron. The result was exactly what I expected. All three updates were processed, in order, leaving me with "Update 3" on both Salesforce and Drupal. Bug fixed. If you did this same experiment with the 8.x-3.x code you'd instead see "Update 1" in Drupal, which is incorrect.
Comment #13
aaronbauman7.x is no longer supported