Can you show the code you are using for storing the amounts as well? If you store it in multiple places, please show them all.
Model::decrement() and Model::increment() not working efficiently
Hey guys, I am getting data from a request and I have created a model observer to do some computations on the data after it has been saved. The computations are like incrementing the user balance based on the amount that came in the request. The shocking thing however is that this is working for some instances and not others, even though the amount is received and saved in the database. What could be happening?
public function created(XpgTransaction $xpgTransaction)
{ $userId = $xpgTransaction->user_id;
$user = User::where('id', $userId)->first();
$userBalance = $user->casino_bakiye;
if($xpgTransaction->type =="debit"){
$debitBalance = $xpgTransaction->amount;
$user->decrement('casino_bakiye', $debitBalance);
}
if($xpgTransaction->type =="credit"){
$creditBalance = $xpgTransaction->amount;
$user->increment('casino_bakiye', $creditBalance);
}
}
You probably have what is known as a race condition. Consider hiring. :) I also can't tell if you are using database transactions which is crucial when it comes to movement of monetary amounts.
public function Debit(){
$data = json_decode(file_get_contents('php://input'), true);
$username = $data['Login'];
$user = User::where('username', $username)->first();
if(!$user){
$rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
return response()->json($rivalaoResponse);
}
$userId = $user->id;
$userBalance = $user->casino_bakiye;
$operator = $data['OperatorId'];
$session = $data['Session'];
$gameId = $data['GameId'];
$roundId = $data['RoundId'];
$sequence = $data['Sequence'];
$amount = $data['Amount'];
$debitDetails =$data['DebitDetails'];
if($sequence){
$game = XpgTransaction::where('gameId', $gameId)
->where('roundId', $roundId)
->where('sequence', $sequence)
->first();
if(!$game){
$trxn = new XpgTransaction;
$trxn->user_id = $userId;
$trxn->operatorId = $operator;
$trxn->session = $session;
$trxn->gameId =$gameId;
$trxn->roundId =$roundId;
$trxn->sequence = $sequence;
$trxn->type = "debit";
$trxn->debitDetails = $debitDetails;
$trxn ->request=json_encode($data);
$trxn->amount = $amount;
$trxn->save();
$updatedBalance = $user->fresh()->casino_bakiye;
if($updatedBalance !=null){
$rivalaoResponse = ["d"=>["Data"=>[(float)$updatedBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
return response()->json($rivalaoResponse);
}
$rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"Invalid balance returned"]];
return response()->json($rivalaoResponse);
}
return response()->json($game);
$rivalaoResponse = ["d"=>["ErrorCode"=>-20,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
return response()->json($rivalaoResponse);
}
@simonangatia if all amounts are saved in the database, what is the working inconsistently?
Look at this and you will see that it works for many requests but not a few. https://imgur.com/a/mFg4g3W
What I am doing is that after the amount being saved in the database, it should increment or decrement the user balance in the user's model. Something that is working for some but not others
What's the issue? duplicate entry?
Can add headers to your table please? My brain can only store 10 variables at a time. :D
Ok, just a minute
Why is it $user->casino_bakiye and not $user->casino_balance?
Can do add a $user->refresh()?
...
$user->refresh();
$userId = $user->id;
$userBalance = $user->casino_bakiye;
$operator = $data['OperatorId'];
$session = $data['Session'];
$gameId = $data['GameId'];
$roundId = $data['RoundId'];
$sequence = $data['Sequence'];
$amount = $data['Amount'];
Why is $userBalance unused?
casino_bakiye is on users table, casino_balance on xpgtransactions table
- Have you looked into transactions? https://laravel.com/docs/8.x/database#database-transactions
- Why is
$userBalanceunused?
Let me post the new code then
public function Debit(){
$data = json_decode(file_get_contents('php://input'), true);
$username = $data['Login'];
$user = User::where('username', $username)->first();
if(!$user){
$rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
return response()->json($rivalaoResponse);
}
$type="debit";
$userId = $user->id;
$amount = $data['Amount'];
$gameId = $data['GameId'];
$roundId = $data['RoundId'];
$sequence = $data['Sequence'];
if($sequence){
$game = XpgTransaction::where('gameId', $gameId)
->where('roundId', $roundId)
->where('sequence', $sequence)
->first();
if(!$game){
$trxn = new XpgTransaction;
$trxn->user_id = $userId;
$trxn ->request=json_encode($data);
$trxn->type =$type;
$trxn->casino_balance = (float)$user->casino_bakiye;
$currentBalance = decrement_user_balance($user->id, $amount);
$rivalaoResponse = ["d"=>["Data"=>[$currentBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
try {
return response()->json($rivalaoResponse);
} finally {
$trxn->save();
}
}
$rivalaoResponse = ["d"=>["ErrorCode"=>-20,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
return response()->json($rivalaoResponse);
}
}
<?php
use App\User;
if (! function_exists('decrement_user_balance')) {
function decrement_user_balance($userId, $amount)
{
$user = User::find($userId);
$user->decrement('casino_bakiye', $amount);
return $user->casino_bakiye;
}
}
The above is the helpers.php code
@simonangatia where does decrement_user_balance() come from?
I think it is easier that you understand the concept of the problem before we go any further
You are trying to move money, which is:
- Decrease from source
- Increase at destination
The BIG question is, is 1 and 2 guaranteed to be atomic, i.e. 1 single operation? Please google if you don't understand atomic.
Are you using database transactions (please google also) to guarantee nothing else will happen between 1 and 2?
Try and figure out what happens below:
- Source is 10
- X reads source and intend to minus 1 to it, i.e. 9
- Y reads source and intend to minus 1 to it, i.e. 9 //Problem, Y should not read until X has written
- X writes 9 to source
- Y writes 9 to source
At step 5, is source value 9 correct? No. After X and Y, it should be 8. But it is 9 because 3 happened before 4. And you are left wondering why Y did not decrement. That is the problem you are facing, also known as a race condition (Once again, please google).
Thank you for helping but allow me to explain more. The problem is not the balance being accidentally decreased or increased. The decreasing and increasing is working efficiently. I have two tables in question:
-
Userstable which contains user's balance column(casino_bakiye) 2.Xpgtransactionstable: Where I save transactions coming from the API.
I also have a route /debit where debit requests are sent. In the request, there is amount and username among the data sent.
So what I am required to do is
- Get the user from the users table with the username that comes with the request
- Get that user's balance 3.decrement that balance
4.return a response with that user's current balance. 5.save the transaction to the Xpgtransactions table
For example, if the current user's balance is 1000 , then the request comes with amount as 100,
I am to decrement that 1000 by 100 and return the resulting amount in the response. Meaning that
1000 - 100 = 900
So I have to return 900, thats after decrementing that user's balance by 100.
Then save the transaction amount to xpgtransactions table.
THE PROBLEM:
Now the problem is that with a few transactions, the decrement logic doesn't happen. It just but saves the transaction to the Xpgtransactions table. So I wonder how it skips decrementing the amount and saves the data to xpgtransactions table.
So in this case, if the current user amount is 1000 and the request comes with 100, I need to decrement 1000 by 100: 1000 - 100 = 900 now in this case it returns 1000 instead of 900, it skips decrementing logic.
So that is mu concern
@simonangatia look at my earlier example steps 1 to 5, can you match Y to your problem and see that it skips the decrementing logic?
Now the question is, what do I do to make it work in sequence? So that data shouldn't be saved in the database before the balance is decremented? I have try if-else statements but didn't work
Thank you for suggesting db transactions. Let me read about it and see if that will help
Brother, I have tried the transactions but the same error happens. I am even shocked. Though I am rolling back the requests but the API doesn't required that. So I must get the all the data saved. No provision for rolling back
public function Debit(){
$data = json_decode(file_get_contents('php://input'), true);
$username = $data['Login'];
$user = User::where('username', $username)->first();
if(!$user){
$rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
return response()->json($rivalaoResponse);
}
$type="debit";
$userId = $user->id;
$amount = $data['Amount'];
$gameId = $data['GameId'];
$roundId = $data['RoundId'];
$sequence = $data['Sequence'];
$game = XpgTransaction::where('gameId', $gameId)
->where('roundId', $roundId)
->where('sequence', $sequence)
->first();
DB::beginTransaction();
try {
if(!$game){
$trxn = new XpgTransaction;
$trxn->user_id = $userId;
$trxn ->request=json_encode($data);
$trxn->type =$type;
$trxn->save();
if($user->decrement('casino_bakiye', $amount)){
$trxn->casino_balance = $user->refresh()->casino_bakiye;
$trxn->save();
DB::commit();
$currentBalance = $user->refresh()->casino_bakiye;
$rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
return response()->json($rivalaoResponse);
}else{
$currentBalance = $user->refresh()->casino_bakiye;
$rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>-1,"HasErrors"=>true,"Message"=>"Unknown error"]];
return response()->json($rivalaoResponse);
DB::rollBack();
}
}else{
$rivalaoResponse = ["d"=>["ErrorCode"=>-20,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
return response()->json($rivalaoResponse);
}
} catch (\Exception $e) {
$currentBalance = $user->refresh()->casino_bakiye;
$rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>-1,"HasErrors"=>true,"Message"=>"Unknown error"]];
return response()->json($rivalaoResponse);
DB::rollBack();
}
@simonangatia as mentioned right at my first post, race condition is not an easy issue, there are quite a number of ways to handle them, it is better you understand the issue at hand and formulate the logic yourself.
I will share pseudo code with you.
function Debit()
{
// 1. Source is 10
DB::beginTransaction();
try {
// 2. X reads source and intend to minus 1 to it, i.e. 9
// 4. X writes 9 to source
// all good
DB::commit();
} catch (\Exception $e) {
// something went wrong
DB::rollback();
// you must write logic to perform 2 and 4 again, maybe use a for loop or something
// or if the API allows for it, return an error ask it to call our Debit() again later
}
}
This way, if Y comes along and tries to call Debit() before X has commit(), an exception will be thrown.
I figured out and tried a while loop, It is working well on my side but the integration team is telling me that it is returning duplicate data
public function Debit(){
$data = json_decode(file_get_contents('php://input'), true);
$username = $data['Login'];
$user = User::where('username', $username)->first();
if(!$user){
$rivalaoResponse = ["d"=>["ErrorCode"=>-10,"HasErrors"=>true,"Message"=>"InvalidPlayer"]];
return response()->json($rivalaoResponse);
}
$type="debit";
$userId = $user->id;
$amount = $data['Amount'];
$gameId = $data['GameId'];
$roundId = $data['RoundId'];
$sequence = $data['Sequence'];
$initialBalance = $user->casino_bakiye;
$game = XpgTransaction::where('gameId', $gameId)
->where('roundId', $roundId)
->where('sequence', $sequence)
->first();
DB::beginTransaction();
try {
if(!$game){
$trxn = new XpgTransaction;
$trxn->user_id = $userId;
$trxn ->request=json_encode($data);
$trxn->type =$type;
$trxn->save();
do {
$user->decrement('casino_bakiye', $amount);
$trxn->casino_balance = $user->refresh()->casino_bakiye;
$trxn->save();
DB::commit();
} while ($user->refresh()->casino_bakiye ==$initialBalance);
$currentBalance = $user->refresh()->casino_bakiye;
$rivalaoResponse = ["d"=>["Data"=>[(float)$currentBalance],"ErrorCode"=>0,"HasErrors"=>false,"Message"=>""]];
return response()->json($rivalaoResponse);
}else{
$rivalaoResponse = ["d"=>["ErrorCode"=>-21,"HasErrors"=>true,"Message"=>"Duplicate transaction"]];
return response()->json($rivalaoResponse);
}
} catch (\Exception $e) {
$rivalaoResponse = ["d"=>["ErrorCode"=>-1,"HasErrors"=>true,"Message"=>"Unknown error"]];
return response()->json($rivalaoResponse);
}
}
Everything is working well on my side and all the data is saved and balance decremented. But they are telling me it is returning duplicate data
As per your other question. Transactions are not relevant here. You need to lock the table from update by other requests whilst you do what you need to do.
Transactions are for rolling back to a known state if an error occurs whilst updating multiple tables.
How do I do that please?
I have googled but haven't gotten good explanation for laravel
OK, I'll correct my previous reply a little. You still need a transaction in which to perform the pessimistic record locking.
See https://medium.com/@aslrousta/pessimistic-vs-optimistic-locking-in-laravel-264ec0b1ba2
and https://laravel.com/docs/8.x/queries#pessimistic-locking
Lock the table for as little time as possible. Increment the column and read back the new value in the transaction (only)
Please or to participate in this conversation.