I am working on an ASP.NET Web API 2 project with .NET Framework 4.6.2. When I send two concurrent requests to a specific endpoint from Postman, only one record is updated in the database.
Here is the code I'm having the issue:
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
// Code omitted for brevity.
List<GameBank> gameBankResult = null;
//Query GameBank database
gameBankResult = _unitOfWork.GameBankRepository.GetGames(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
if (gameBankResult != null && gameBankResult.Count() >= requestDto.quantity)
{
var k = requestDto.quantity - 1;
for (var i = k; i >= 0; --i)
{
gameBankResult[i].clientTrxRef = gameRequest.clientTrxRef;
gameBankResult[i].referenceId = gameRequest.referenceId;
gameBankResult[i].requestDateTime = DateTime.Now;
gameBankResult[i].responseDateTime = DateTime.Now;
}
//***** UPDATE GameBank *****
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
if (requestDto.quantity == 1)
{
//Code omitted for brevity.
}
}
_unitOfWork.Save();
return response;
}
I tried DbUpdateConcurrencyException handling in my code above. This seems working for only 2 concurrent requests, but if there are more than 2 concurrent requests I am having the same issue.
//Update GameBank
try
{
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
_unitOfWork.Save();
}
catch (DbUpdateConcurrencyException)
{
// Refresh and retry
gameBankResult[k] = _unitOfWork.GameBankRepository.GetByID(gameBankResult[k].GameBankID);
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
_unitOfWork.Save();
}
I tried using a transaction. But when I tried a performance test in Postman, 150 requests were sent (3 virtual users for 1 minute with a fixed load profile). Only 134 records were updated in the table. I got deadlocks.
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
// Code omitted for brevity.
List<GameBank> gameBankResult = null;
using (var scope = new TransactionScope(TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
//Query GameBank database
gameBankResult = _unitOfWork.GameBankRepository.GetGames(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
if (gameBankResult != null && gameBankResult.Count() >= requestDto.quantity)
{
var k = requestDto.quantity - 1;
for (var i = k; i >= 0; --i)
{
gameBankResult[i].clientTrxRef = gameRequest.clientTrxRef;
gameBankResult[i].referenceId = gameRequest.referenceId;
gameBankResult[i].requestDateTime = DateTime.Now;
gameBankResult[i].responseDateTime = DateTime.Now;
}
//***** UPDATE GameBank *****
_unitOfWork.GameBankRepository.Update(gameBankResult[k]);
if (requestDto.quantity == 1)
{
//Code omitted for brevity.
}
}
_unitOfWork.Save();
scope.Complete();
}
return response;
}
So I removed the transaction and added WebApiThrottle to force requests every second.
Here is the service:
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
HttpResponseMessage response = null;
//ProductCode Conversion
var productCode =
_unitOfWork.ProductCodeRepository.GetByCode(p => p.clientCode == requestDto.productCode);
if (productCode != null)
{
requestDto.productCode = productCode.gameCode;
}
var gameRequest = _mapper.Map<RequestDto, GameRequest>(requestDto);
//Unique reference ID
gameRequest.referenceId = Guid.NewGuid();
var gameRequestDto = _mapper.Map<GameRequest, GameRequestDto>(gameRequest);
//Create signature
gameRequest = UtilitiesWatson.CreateSignature(gameRequestDto, RequestType.Initiate);
//Set service
gameRequest.service = "OUR";
gameRequest.customerID = 5; //WATSON
gameRequest.clientTrxRef = requestDto.clientTrxRef; //WATSON
//Add initiation request into database
_unitOfWork.GameRepository.Insert(gameRequest);
_unitOfWork.Save();
GameBank gameBankResult = null;
gameBankResult = _unitOfWork.GameBankRepository.GetGame(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
_unitOfWork.Save();
if (gameBankResult != null)
{
gameBankResult.clientTrxRef = gameRequest.clientTrxRef;
gameBankResult.referenceId = gameRequest.referenceId;
gameBankResult.requestDateTime = DateTime.Now;
gameBankResult.responseDateTime = DateTime.Now;
_unitOfWork.GameBankRepository.Update(gameBankResult);
_unitOfWork.Save();
var gameBankConfirmResponse =
_mapper.Map<GameBank, GameConfirmResponse>(gameBankResult);
gameBankConfirmResponse.purchaseStatusDate = DateTime.Now;
gameBankConfirmResponse.clientTrxRef = gameRequest.clientTrxRef;
//ProductCode Conversion
var productCodeReverse = _unitOfWork.ProductCodeRepository.GetByCode(p =>
p.gameCode == requestDto.productCode);
if (productCodeReverse != null)
{
gameBankConfirmResponse.productCode = productCodeReverse.clientCode;
}
var resultResponse = JsonConvert.SerializeObject(gameBankConfirmResponse,
Formatting.Indented,
new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
response = new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.OK,
Content = new StringContent(resultResponse, System.Text.Encoding.UTF8,
"application/json"),
};
//Set service
gameBankConfirmResponse.service = "OUR";
gameBankConfirmResponse.clientTrxRef = requestDto.clientTrxRef;
_unitOfWork.GameConfirmResponseRepository.Insert(gameBankConfirmResponse);
_unitOfWork.Save();
}
return response;
}
Here is the WebApiThrottle in WebApiConfig:
config.MessageHandlers.Add(new ThrottlingHandler()
{
Policy = new ThrottlePolicy(perSecond: 2, perMinute: 28)
{
IpThrottling = true,
EndpointThrottling = true,
EndpointRules = new Dictionary<string, RateLimits>
{
{ "api/v2/game/watson/purchase", new RateLimits { PerSecond = 1, PerMinute = 22, PerHour = 1100 } }
}
},
Repository = new CacheRepository(),
QuotaExceededMessage = "You may only perform this action every {0} seconds."
});
What are your suggestions? (My goal is to handle multiple concurrent requests that's why I am trying to find a proper way.)
Here is the final working code:
private HttpResponseMessage CallGameNew(RequestDto requestDto)
{
HttpResponseMessage response = null;
//ProductCode Conversion
var productCode =
_unitOfWork.ProductCodeRepository.GetByCode(p => p.clientCode == requestDto.productCode);
if (productCode != null)
{
requestDto.productCode = productCode.gameCode;
}
var gameRequest = _mapper.Map<RequestDto, GameRequest>(requestDto);
//Unique reference ID
gameRequest.referenceId = Guid.NewGuid();
var gameRequestDto = _mapper.Map<GameRequest, GameRequestDto>(gameRequest);
//Create signature
gameRequest = UtilitiesWatson.CreateSignature(gameRequestDto, RequestType.Initiate);
//Set service
gameRequest.service = "OUR";
gameRequest.customerID = 5; //WATSON
gameRequest.clientTrxRef = requestDto.clientTrxRef; //WATSON
//Add initiation request into database
_unitOfWork.GameRepository.Insert(gameRequest);
_unitOfWork.Save();
GameBank gameBankResult = null;
while (true)
{
try
{
gameBankResult = _unitOfWork.GameBankRepository.GetGame(g =>
g.productCode == requestDto.productCode && g.referenceId == Guid.Empty);
_unitOfWork.Save();
if (gameBankResult != null)
{
gameBankResult.clientTrxRef = gameRequest.clientTrxRef;
gameBankResult.referenceId = gameRequest.referenceId;
gameBankResult.requestDateTime = DateTime.Now;
gameBankResult.responseDateTime = DateTime.Now;
_unitOfWork.GameBankRepository.Update(gameBankResult);
_unitOfWork.Save();
break; //exit from while loop
}
}
catch (DbUpdateConcurrencyException)
{
_unitOfWork.ClearChangeTracker(); //IS REQUIRED, so the next select will read new RowVersion also
Thread.Sleep((new Random()).Next(0, 1000)); //if you want to add a random pause 0-1 second
}
}
var gameBankConfirmResponse =
_mapper.Map<GameBank, GameConfirmResponse>(gameBankResult);
gameBankConfirmResponse.purchaseStatusDate = DateTime.Now;
gameBankConfirmResponse.clientTrxRef = gameRequest.clientTrxRef;
//ProductCode Conversion
var productCodeReverse = _unitOfWork.ProductCodeRepository.GetByCode(p =>
p.gameCode == requestDto.productCode);
if (productCodeReverse != null)
{
gameBankConfirmResponse.productCode = productCodeReverse.clientCode;
}
var resultResponse = JsonConvert.SerializeObject(gameBankConfirmResponse,
Formatting.Indented,
new JsonSerializerSettings()
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
response = new HttpResponseMessage
{
StatusCode = System.Net.HttpStatusCode.OK,
Content = new StringContent(resultResponse, System.Text.Encoding.UTF8,
"application/json"),
};
//Set service
gameBankConfirmResponse.service = "OUR";
gameBankConfirmResponse.clientTrxRef = requestDto.clientTrxRef;
_unitOfWork.GameConfirmResponseRepository.Insert(gameBankConfirmResponse);
_unitOfWork.Save();
return response;
}