Q1: I am bit confused about what will happen ? Will it fail(throw exception) or will it wait for 4-5 seconds and then if it doesn't get access it will throw an exception?
You will get somewhere around ten seconds before it will exclusively fail. The user will be advised to try their save again. The status code will be "UNABLE_TO_LOCK_ROW".
Q2 : In the above case will the second thread get access to the record ? or the query will not return any record ?
Either the prior transaction has already completed, so it would not appear in the results, or you'll get an error because the row can't be locked. There's never a scenario where it would be returned with non-matching criteria.
Q3 : When does the lock gets released ? the Salesforce doc doesn't seems to provide much insight into it. does the lock get released after a DML ? or it waits for full transaction to complete ?
The documentation clearly states:
While the records are locked by a client, the locking client can modify their field values in the database in the same transaction. Other clients have to wait until the transaction completes and the records are no longer locked before being able to update the same records. Other clients can still query the same records while they’re locked.
(Emphasis mine)
Clarification The lock isn't released until sometime after the final commit occurs, usually within a few milliseconds, although post-transaction logic can re-acquire the lock immediately, such as future methods.
So, I decided to test this with some code to get a more definitive answer.
First, I set code that ran around (but less than) 10 seconds, like this:
Controller
public with sharing class rowlock {
Id accountId = [SELECT Id, name FROM Account where name = 'test' LIMIT 1].Id;
public void updateAccount() {
Account[] a = [SELECT Id FROM Account WHERE Id = :accountId and name = 'test' FOR UPDATE];
for(integer i = 0; i < 3000000; i++) {
// Do nothing, haha!
}
if(!a.isempty()) {
a[0].name = 'test2';
update a[0];
}
system.debug(logginglevel.error, a.isempty());
}
}
Page
<apex:page controller="rowlock">
<apex:form id="form">
<script>
addEventListener("load", function() {
updateAccount();
updateAccount();
}, true);
</script>
<apex:actionFunction name="updateAccount" action="{!updateAccount}" reRender="form"/>
</apex:form>
</apex:page>
During this trial run, the page bombed out on the second transaction with "Record Currently Unavailable..."
Changing 3,000,000 to 100,000, both transactions passed. However, the first transaction (the one with the first lock) returned 1 row, while the second transaction returned 0 rows, thus proving that the lock actually resulted in the query results being modified to exclude the locked record since it no longer met the criteria.
Edit: Fixed the status code to current terminology, cleaned up the multiple strke-throughs, which were distracting.
One approach to try is to query for the Account
records using the FOR UPDATE
clause. Check out how Locking Statements work.
If you do a lot of heavy lifting in the Account
trigger, this approach could cause timeouts. For example, if each save takes approximately 4 seconds, the race condition you describe (3 concurrent operations) might not resolve within the allotted 10 seconds. This situation will result in a QueryException
. If you run into it you might want to spin up a @future
or Queueable
as a fallback.
Best Answer
This applies to lookup fields as well as master detail fields - check out the Lookup Skew section of this blog post:
http://blogs.developerforce.com/engineering/2013/04/managing-lookup-skew-to-avoid-record-lock-exceptions.html
The key statement is: