I have a Web API that has a method that has been working for several months without any issues (PurchaseController
). The list passed in was always around 100 or less. A few days ago, the volume spiked to around 600 items in the list which resulted in the app that passes the list getting a timeout instead of a success/failure response. It also seemed to make all other Web API methods run very slow until this method had finished running. So I modified the app code to break up the submissions into a configurable number of items at a time (currently set to 20 for the purpose of testing). I also modified the way the list was organized so that I could wrap each purchase in its own SQL transaction, in the hopes that this would allow other API methods to run without interruption.
However, once these changes were made, I ran into frequent errors like this when PurchaseController
was called:
Transaction (Process ID 62) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
The original PurchaseController
code:
public class PurchaseController : ControllerBase
{
/// <summary>
///
/// </summary>
/// <param name="Purchases"></param>
/// <returns></returns>
public JsonResult<APIResponse> Put([FromBody]CPurchases Purchases)
{
if (!ValidateCredentials(Request, this.ServiceName))
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
APIResponse response = new APIResponse();
try
{
using (var dbTransaction = Members.Database.BeginTransaction())
{
response = base.RecordEventRegistrations(Purchases.EventRegistrations);
if (!response.IsSuccess)
throw new Exception(response.Error);
response = base.RecordPublicationPurchases(Purchases.PublicationPurchases);
if (!response.IsSuccess)
throw new Exception(response.Error);
response = base.RecordSubscriptionPurchases(Purchases.SubscriptionPurchases);
if (!response.IsSuccess)
throw new Exception(response.Error);
response = base.RecordTransactions(Purchases.Transactions);
if (!response.IsSuccess)
throw new Exception(response.Error);
dbTransaction.Commit();
}
}
catch (Exception ex)
{
Utilities.Logging.LogToEventLog(ex);
response.IsSuccess = false;
response.Error = Utilities.Logging.GetInnermostException(ex);
}
return Json(response);
}
}
The new code that ran into deadlocks on virtually every call:
public class PurchaseController : ControllerBase
{
/// <summary>
/// </summary>
/// <param name="Purchases"></param>
/// <returns></returns>
public JsonResult<APIResponse> Put([FromBody]List<PurchaseDetails> Purchases)
{
if (!ValidateCredentials(Request, this.ServiceName))
{
throw new HttpResponseException(HttpStatusCode.Unauthorized);
}
APIResponse response = new APIResponse();
try
{
using (var dbTransaction = Members.Database.BeginTransaction())
{
foreach (var Purchase in Purchases)
{
response = base.RecordEventRegistrations(Purchase.EventRegistrations);
if (!response.IsSuccess)
throw new Exception(response.Error);
response = base.RecordPublicationPurchases(Purchase.PublicationPurchases);
if (!response.IsSuccess)
throw new Exception(response.Error);
response = base.RecordSubscriptionPurchases(Purchase.SubscriptionPurchases);
if (!response.IsSuccess)
throw new Exception(response.Error);
response = base.RecordTransactions(Purchase.PaymentTransactions);
if (!response.IsSuccess)
throw new Exception(response.Error);
dbTransaction.Commit();
}
}
}
catch (Exception ex)
{
Utilities.Logging.LogToEventLog(ex);
response.IsSuccess = false;
response.Error = Utilities.Logging.GetInnermostException(ex);
}
return Json(response);
}
}
I later moved the SQL transaction outside the foreach like this:
foreach (var Purchase in Purchases)
{
using (var dbTransaction = Members.Database.BeginTransaction())
{
and this made a huge difference in the frequency of deadlocks, but did not eliminate them. I also added System.Data.IsolationLevel.RepeatableRead
to the SQL transaction but it seemed to make no difference.
The code calling this method changed from:
public static APIResponse SavePurchases(IEnumerable<EventRegistration> Registrations, IEnumerable<PublicationPurchase> PublicationPurchases, IEnumerable<SubscriptionPurchase> SubscriptionPurchases, IEnumerable<PaymentTransactionModel> Transactions)
{
if (Registrations.Count() == 0 && PublicationPurchases.Count() == 0 && SubscriptionPurchases.Count() == 0 && Transactions.Count() == 0)
return new APIResponse() { IsSuccess = true };
CPurchases Purchases = new CPurchases()
{
EventRegistrations = Registrations,
PublicationPurchases = PublicationPurchases,
SubscriptionPurchases = SubscriptionPurchases,
Transactions = Transactions
};
return PutAPI("/Purchase", Purchases);
}
to
public static APIResponse SavePurchases(IEnumerable<PurchaseDetails> Purchases)
{
int PageSize = AppSettings.MaxNumberOfPurchasesPerAPIBatch;
APIResponse Response = new APIResponse() { IsSuccess = true };
List<PurchaseDetails> Page;
for (int i = 0; i < Purchases.Count(); i = i + PageSize)
{
Page = Purchases.Skip(i).Take(PageSize).ToList();
if (Page.Any())
{
Response = PutAPI("/Purchase", Page);
if (!Response.IsSuccess)
break;
}
}
return Response;
}
In addition, it is worth noting that the table the RecordEventRegistrations
modifies has a single trigger to enforce that the varchar event number is in all caps. This was put in place when the original code went live. And all database changes are done using Entity Framework 6.
No other code changes to the database, the app, or the Web API have been made in the past 2 weeks, so I'm pretty sure the problem is due to these code changes but I can't seem to figure out what or why.
UPDATE: I've been trying to get the deadlock graph for several hours but my account didn't have the correct permissions. Now that I have it I believe the problem is in the trigger, if I'm reading the graph properly. I've temporarily disabled the trigger and it seems to have resolved the deadlocks. But since the trigger has been unchanged for the past 6+ months, I'm guessing it has something to do with the volume of insert/updates over the last couple of days?
xml_report
<deadlock>
<victim-list>
<victimProcess id="process23368b65468"/>
</victim-list>
<process-list>
<process id="process23368b65468" taskpriority="0" logused="248" waitresource="PAGE: 6:1:84432 " waittime="3847" ownerId="22806353" transactionname="user_transaction" lasttranstarted="2020-12-30T10:33:09.433" XDES="0x2360aa86408" lockMode="U" schedulerid="4" kpid="3916" status="suspended" spid="54" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2020-12-30T10:33:09.437" lastbatchcompleted="2020-12-30T10:33:09.420" lastattention="1900-01-01T00:00:00.420" clientapp="EntityFramework" hostname="***-API-01" hostpid="1400" loginname="sa" isolationlevel="read committed (2)" xactid="22806353" currentdb="6" lockTimeout="4294967295" clientoption1="671219744" clientoption2="128056">
<executionStack>
<frame procname="members.dbo.tr_event_registration" line="8" stmtstart="332" stmtend="584" sqlhandle="0x03000600e3d0825d1a48970094ac000000000000000000000000000000000000000000000000000000000000"> UPDATE [Event_Registration] SET event_number = UPPER(event_number) WHERE event_number in (select event_number from inserted </frame>
<frame procname="adhoc" line="1" stmtstart="156" stmtend="506" sqlhandle="0x0200000015ef0508db49eeb79e583b351130d3afdb0ebac40000000000000000000000000000000000000000"> unknown </frame>
<frame procname="unknown" line="1" sqlhandle="0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"> unknown </frame>
</executionStack>
<inputbuf> (@0 datetime2(7),@1 char(2),@2 datetime2(7),@3 datetime2(7),@4 int,@5 char(5))UPDATE [dbo].[Event_Registration] SET [registration_date] = @0, [source] = @1, [payment_date] = @2, [time_stamp] = @3 WHERE (([entity_number] = @4) AND ([event_number] = @5)) </inputbuf>
</process>
<process id="process233a1618ca8" taskpriority="0" logused="328" waitresource="PAGE: 6:1:1848653 " waittime="3824" ownerId="22806350" transactionname="UPDATE" lasttranstarted="2020-12-30T10:33:09.383" XDES="0x2360db58e58" lockMode="U" schedulerid="1" kpid="6096" status="suspended" spid="90" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2020-12-30T10:33:09.380" lastbatchcompleted="2020-12-30T10:33:09.393" lastattention="1900-01-01T00:00:00.393" clientapp=".Net SqlClient Data Provider" hostname="***-MBR-01" hostpid="3936" loginname="sa" isolationlevel="read committed (2)" xactid="22806350" currentdb="6" lockTimeout="4294967295" clientoption1="671219744" clientoption2="128056">
<executionStack>
<frame procname="members.dbo.tr_event_registration" line="8" stmtstart="332" stmtend="584" sqlhandle="0x03000600e3d0825d1a48970094ac000000000000000000000000000000000000000000000000000000000000"> UPDATE [Event_Registration] SET event_number = UPPER(event_number) WHERE event_number in (select event_number from inserted </frame>
<frame procname="adhoc" line="1" stmtstart=&qu