Race Condition Overview0:00
We have five customers that wanted to buy the same product at the exact same time. The chances of that are, well, if we have a global audience, then the chances of that are actually pretty good. But the problem is that we only had two of those items in stock and unfortunately all five of those customers received order confirmations. That's an issue. It's a race condition and we need to fix that.
Inspect Stock and Orders0:24
That's an issue. It's a race condition and we need to fix that. So the product in question is Elite Widget 47. It has an ID of 47 and we can see that it is outta stock because of course we had five people that wanted to buy the two that we had in stock. Now the first thing that I want to do is check the database because it could be possible that we have negative stock for this product. So let's find that product
negative stock for this product. So let's find that product and hopefully our stock quantity is zero. Good, because it could be negative and that would be a whole nother issue. So thankfully we have a quantity of zero and we are okay as far as that is concerned. But you know, I also want to see the orders that were for making or, or the orders that were placed for this product.
for making or, or the orders that were placed for this product. So we're gonna find the order. Where has items that has the product that we are currently looking for. So we want where the product ID is 47, but you know, we only want those that have been done today. So we want where the date for the created act is today. And then let's just get the count. We don't necessarily need to see all of those orders,
Locate Checkout Bug1:48
And then let's just get the count. We don't necessarily need to see all of those orders, but we can see that yes, there were five orders that were placed for this order today. So let's take a look at why this happens. We are inside of the checkout controller. We have the store method here, and the first thing that we do is get the cart. We make sure that we have a cart or that the cart isn't empty.
We make sure that we have a cart or that the cart isn't empty. Then we log and then here we go. We are checking to see if we have those products in stock. And that's really all that we are doing here. And right here. If the stock quantity is less than what we actually have, then we back out saying that, sorry, we don't have that quantity. So we are checking here. And then later on we are uh, making the order.
So we are checking here. And then later on we are uh, making the order. And then more importantly, right here we are decrementing, you know, our quantity. So there's a time lapse between when we check and when we actually update the database. That's an issue. It's a common issue, but thankfully it's one that we can fix. Now I have a test script that we can use to essentially replicate this whole thing.
Reproduce with Test Script2:53
Now I have a test script that we can use to essentially replicate this whole thing. Basically what it's doing is getting just five random users and they are placing an order for this same product. And we can see that each one of those users has an actual order that is submitted and checked out. And that's a problem. Now notice here though, that we are left with a stock of one. That's kind of strange.
with a stock of one. That's kind of strange. But if we run this again, we're gonna see that the final stock is zero. If we run it another time, we're gonna see that the final stock is zero. The the interesting thing here is that the final stock that we end with can be variable because it is a race condition. We end up with something that, uh,
because it is a race condition. We end up with something that, uh, what's typically called the last right wins. Basically the last thing that writes to the database is the thing that wins. That's what has written to the database of course. So we're going to see a variable in the final state. Most of the time it's probably going to be zero, but we've already seen where the final state is won. That's all part of the race condition.
Fix with Transaction Locking3:55
but we've already seen where the final state is won. That's all part of the race condition. So now the question is how do we fix this? And really we need three things. We need a transaction, we need a lock on the database record, and then we need to update the database. So three things and let's see. That's all fine, that's all fine. Really this is where we need to start our changes.
That's all fine, that's all fine. Really this is where we need to start our changes. So the first thing that we are going to do is add a try. And we are going to catch a custom exception. Uh, we already have an insufficient stock exception that we are going to catch and then we would do something there for right now, we're gonna leave that alone. But what we essentially want to do is create a database transaction.
But what we essentially want to do is create a database transaction. If you're not familiar with a transaction, they are actually really cool because they allow us to make multiple changes to a database. And especially if those changes are all dependent upon one another, so that we could say, okay, within this single transaction, update this database, create this record, get the ID from that record, use it over here so we can have all of these um,
create this record, get the ID from that record, use it over here so we can have all of these um, queries and SQL statements executing. And then if for some reason one of those things goes wrong, we can just roll it back 'cause everything is part of that transaction. So it's a great way of making very important changes to the database such as, you know, placing an order. So the first thing that we want to do here is create our transaction.
So the first thing that we want to do here is create our transaction. We're gonna use the DB facade to do that. And we have a method called transaction. This receives a callback and we want to use the cart because we want to interact with the cart so that we can, you know, get the items from the cart and work with them that way. But one thing that we are going to do is, is create an array
and work with them that way. But one thing that we are going to do is, is create an array of all of the items within the cart that we have, or well, yeah, that, that's what we have the items from the cart. Because remember what the main issue is here. The first thing we we do is check the stock, then we do some other things and then we actually update the database. And part of that second part of
and then we actually update the database. And part of that second part of where we update the database, uh, this right here where we are creating the items that are part of the order, we are getting the product from the database. We don't need to do that because we've already checked the database for the stock there. So we're going to essentially build an array that contains all of the order items that we need. There it is. I got lost there.
all of the order items that we need. There it is. I got lost there. And so that's what this order items is for. That way we don't have to keep hitting the database for information that we should already have. So we will have our order items, then we will have our subtotal, which we'll just initialize as zero. And we're gonna do this in three phases. The first phase is going
And we're gonna do this in three phases. The first phase is going to essentially build this order items array. So we want to iterate over the items in the cart because for each item, you know, that is going to be an actual record in the database that we essentially want to lock before we are done with this transaction. So to do that, we are going to use our product facade, not our facade.
So to do that, we are going to use our product facade, not our facade. That's our product model where the ID is the item product id. So we're gonna get that product, but we want to lock for update. It's gonna lock that record and then we want the first or the first. I should not talk while I type because I'm gonna end up typing whatever it is that I talk.
I should not talk while I type because I'm gonna end up typing whatever it is that I talk. Okay? So we get our product, we lock that record, that's the important thing. Well that's one of the important things because then what we'll do is check the quantity. Uh, so, uh, we don't need that line anymore. Um, let's just lift this out. This is where we're checking the stock of what we have. So if we don't have stock for this particular product,
This is where we're checking the stock of what we have. So if we don't have stock for this particular product, we need to tell the user, but we don't want to go back in history. Now instead we want to throw a new insufficient stock exception. There are three things that we need to supply here. The first is the product name. The second is the number of products or, or the number of the, the quantity
The second is the number of products or, or the number of the, the quantity of this particular product that we are checking. So quantity. And then we need to know how many we actually have, which is, I think it's stock quantity. We'll find out. So if we don't have the stock for it, we throw this insufficient stock exception and that's gonna be great so that then we can just go on from there,
and that's gonna be great so that then we can just go on from there, which is really just kind of replicating all of this. But we're gonna do it in, in a little bit of a different order so that we throw here. Then we'll get the item subtotal based upon the price at time multiplied by the quantity. Quantity. Alright, so we have the item subtotal. Let's go ahead and create the subtotal, which is going to keep a running total of all of the item subtotals.
Let's go ahead and create the subtotal, which is going to keep a running total of all of the item subtotals. So then what we want to do is build our order items. So we will do that and we essentially want four things. We want the product, which of course is going to be the product that we are currently working with. We want the quantity and we get that from item quantity. Then we want the price, which also comes from item price at time.
Then we want the price, which also comes from item price at time. And then finally we want the subtotal, which will just be our items subtotal variable there. It helps if we use the right bracket instead of the curly brackets. Okay, so we have the square bracket there, we have the closing there. Alright, so we've added to our order items and that's it. So we've built this order items array that contains all
Alright, so we've added to our order items and that's it. So we've built this order items array that contains all of the information about the items from our cart so that the next thing that we need to do is phase two, which is to actually create the order. And this is where we need the tax rate. So let's go ahead and let's pull that in. We also need the tax here. We don't need the subtotal because now we have that, we don't need the total
We don't need the subtotal because now we have that, we don't need the total because we can actually calculate that when it comes time to. So we'll have the tax rate followed by the amount of tax based upon the subtotal and the tax rate. And then well, we create our order. So let's grab that code. Let's get rid of some of these comments. We don't need this for each anymore. And let's paste in this order creation all.
We don't need this for each anymore. And let's paste in this order creation all. So user id, auth id, order number is autogenerated. Subtotal is subtotal tax. The total here is going to be the subtotal plus the tax status pending placed at now. So we've created our order, which means that the next thing we need to do is phase three. And this is, you know, updating the database.
that the next thing we need to do is phase three. And this is, you know, updating the database. So to do this, we need to iterate over our order items as item. And this is where we essentially do this right here to where before we were iterating over the items within the cart, then we were fetching the individual products that we were going to buy. We already have that stuff. So really what we need is this code right here
We already have that stuff. So really what we need is this code right here where we create the order item. So for each item in our orders item, we will create an order item. Uh, things are gonna be a little bit different though. We, we have our order ID there, but the product ID comes from the item and the key is product. And it's gonna be the same thing for everything else.
and the key is product. And it's gonna be the same thing for everything else. So we have the quantity key from the item, we have the item price, and then we have the item subtotal. But here's the thing, for every item that we create here, or for every order item that we create, we are going to decrement the stock. And we'll do that because we have the product right here that we need to update.
And we'll do that because we have the product right here that we need to update. We'll increment the stock quantity based upon the item quantity from the order. You know, if we have an order of 10 items, then part of this transaction is going to be decrementing the stock for those 10 items based upon the amount of those 10 items that we have within our order. And if anything goes wrong, then the transaction is rolled back
And if anything goes wrong, then the transaction is rolled back and we already have a lock on these records. So nothing else can make changes to it. In fact, depending upon the database, nothing else can access that record. But that depends upon the database engine. I think SQL Lights, I don't necessarily know what SQL Light is going to do. I know it's locking behavior is a little bit different than
what SQL Light is going to do. I know it's locking behavior is a little bit different than what you would find with my SQL or Microsoft SQL or, or any other database. But yeah, so we iterate over the order items so that we can add those things to the order that we just created. We decrement the stock and then we need to clear the cart because we have just created the orders and all of that stuff.
because we have just created the orders and all of that stuff. So we will clear the cart and then finally we will return the order. So that's basically it, that is our transaction. And then once that transaction is done, then the lock on those records are released and then other things can access and make changes to those. So we're good to go there. And if there's any problem as far as a stock issue,
So we're good to go there. And if there's any problem as far as a stock issue, it's going to throw that insufficient stock exception, which is where we could go back and I shouldn't have deleted that, so I don't exactly remember what it did, but we'll go back with I think it was error and then we could get message there. However, if the transaction is successful, then uh, let's clean this up.
However, if the transaction is successful, then uh, let's clean this up. We don't need to do any of that anymore. We don't need to clear the cart. Let's put, uh, this logging code right here because this was for, uh, saying that we decremented, you know, the stock within the log. Let's see, what else do we need to do, uh, when the order is submitted? We need to do that and then we need to go back.
when the order is submitted? We need to do that and then we need to go back. Okay, now the hard part is making sure that I don't delete any of the curly braces that we would otherwise need after the transaction. Then we will log there, we will redirect there and okay, we are good there. Uh, but we do have an issue. We are logging inside of a loop. We need to change this because item quantity as well as
We are logging inside of a loop. We need to change this because item quantity as well as the product name here. So we'll just do that. We'll do the same thing for the stock quantity. Three phases we get our transaction. We then check to be sure that we have the stock because if not, we need to throw that exception. Then we build our order items so that then after we create the order, we can use those order items
Then we build our order items so that then after we create the order, we can use those order items to create the actual order items for the order. We don't have to hit the database again because we already have those records, in which case we can decrement everything and then everything else works like it's supposed to. Now we're not gonna actually run this code because what I want to do is run that test again, that simulated the race condition to begin with.
Verify Fix via Endpoint16:05
because what I want to do is run that test again, that simulated the race condition to begin with. I have an API endpoint, which basically does the exact same thing, just a little minor differences because it's an API endpoint. But what I want to do then is hit a different endpoint. I want to hit the fixed endpoint because this is going to show us the difference between having a race condition
because this is going to show us the difference between having a race condition and then having, you know, a proper implementation to where we lock the records, we use transactions and things like that. So when we run that script, again, we're gonna see that we have two items in stock. We have five buyers. But now let's look at the results. Uh, the first two users can't access it because user three really well, user two,
Uh, the first two users can't access it because user three really well, user two, let's not confuse me, user two actually won the first race. So they got the lock. They are the lucky ones that get to order the product. User three doesn't. But user four is also lucky. If we run this again, we're gonna get different results and we can see that user zero and user two we're the winners In this particular case.
and we can see that user zero and user two we're the winners In this particular case. If we run this again, so these race conditions are fixed, basically, at least as far as this case is concerned. If there's other race conditions, well we're not gonna address those, but this is how we fix it. We use a transaction, we lock database records and we do everything that we need to within
We use a transaction, we lock database records and we do everything that we need to within that transaction.
