Prevent race condition and lock tables in hotel booking system PHP/MySQL - php

I am building a hotel style reservation system and I am loosing the way. I've got stuck at the exactly reservation moment when to add the reservation and I am concerned about come situations.
My flow is really easy, it lets multiple user to go through the reservation process for the same room until one of them press the reservation button before the others. The reservation information is kept in the session and is never stored in the db until somebody reach last step and press "book". In every step, the system check if the room/s is/are available if not it gives the error page.
Now the problem is preventing race condition combined to a last one mandatory check if still there are enough rooms left:
When user is in last step, he/she select the payment method and press book button. After checking if the selected payment option is ok (form data hasn't been altered) it redirects to the reservation controller where:
reservation controller
<?php
ReservationController extends JControllerLegacy{
public function placeReservation(){
//Do some checks to prevent direct access, booking existence and user completed all steps
$reservatioModel = $this ->getModel('Reservation')
if(!$reservationModel->placeReservation()){
//set errors and redirect to error page
return false;
}
//booking had been placed, passes the data to payment plugin
}
?>
and
Reservation model:
<?php
ReservationModel extends JModelLegacy{
public function placeReservation(){
//Get all variables and objects
//Lock the tables to prevent any insert while the last check
$db ->lockTable(#__bookings);
$db ->lockTable(#__booking_room);
//Perform the last mandatory check with tables locked to prevent read the table right one moment before another transaction insert the booking and allowing overbooking. THIS CHECK MUST BE 100% SURE NO ONE ELSE IS MANIPULATING/READING THE TABLE
if(!$this ->checkRoomsAvailability($data))
return false;
try{
$db ->transactionStart();
//Add the reservation in the booking table
if(!$this ->_saveBooking()){
$db ->rollBack();
return false;
}
//Add the room/s to the middle table #__booking_room
if(!$this -> _saveRooms())
$db ->rollBack();
return false;
}
$db ->transactionCommit();
}catch(Exception $e){
$db ->rollBack();
$this ->setError('We were not able to complete your reservation.');
return false;
}
$db ->unlockTables();
}
?>
The above mentioned tables are InnoDB, #__bookings hold the bookings and #__booking_room hold the information for the selected rooms (like avg_price, number of rooms, etc) and it has an foreign key to #__rooms and one to #__bookings
What am I concerned about?
1 - I am using other tables during the last availability check than the two locked and is giving me error.
2 - When the second user's execution arrives at the table locking point, does it wait until they get free or does it raise an error/exception? I could catch the exception and redirect user to the error page but locket table doesn't means reservation placed, it can occurs any issue while saving the information.
I hope my workflow is ok, please let me know if I am good to go or I should improve my code.
Thanks
EDIT:
To be mentioned, the business idea is like i.e. Expedia, booking, etc where you are allowed to select more rooms types and units per reservation. Indeed, my room table is not for specific/real room but is meant to be for room type with a fixed number of units available everyday if not already booked.

My approach would be simpler. In the table that stores the actual reservation, I would have a foreign key for the specific room being reserved, and have a UNIQUE index for the fields roomID, date. This will ensure that the same roomID cannot be recorded twice for the same date.
Next, when the customer confirms booking, I would run everything in a transaction as you're currently doing. When the code gets to the last place and tries to insert a new reservation, if a moment before another customer reserved that room, the DB will not allow you to insert another reservation for that room for that date. That's when I would either:
rollback and throw the error, or
try again with another room (with its own roomID) if there is another room of the same type still available

I can not say if your workflow aka business domain is the right choice as it depends on your business. But technically you want to have a booking transaction which also provides a check against double booking. The easiest way could be a room-booking-dates table with unique constraint on room is and date. So during your booking transaction you also insert each day with the room id into the table and the unique constraint will ensure that the second attempt will fail.
PS: If a table is locked, PHP will just wait for the unlock. No exception will be thrown (at least if the connection does not close after x seconds)

Related

How to perform update slots from 2 sql tables?

I am doing a booking cancellation function. I have successfully removed the data in reservation table as shown in the code below. However, I would like to update the status for the timeslots. For the timeslots table, 1 = not available for booking anymore, 0 = available for booking. While the user logs in into the account, they can view the appointment and perform booking cancellation, which is where I am stuck with on updating the timeslots to make it available for booking again.
For the reservations table, its status means visited or not visited, which is not related to this question. However, the reservation table is the table which shows the information to the users to view their appointment history, and here is where the cancellation feature will be provided, using a button.
The problem is that I do not know how to connect between the reservation table and the timeslot table. I need the timeslot table to show the updated status is 0 after cancellation happens for a particular day instead of continue showing 1. Below I will attach the database in SQL and the codes implemented.
Timeslots database:
Reservations database:
public function destroy($id,Request $request){
//delete reservation in records
$reservation = Reservation::find($id);
$reservationDelete = $reservation->delete();
//how to update the timeslots status to become 0 from timeslots database?
return redirect()->route('my.appointment')->with('message', 'Appointment has been cancelled successfully');
}
Maybe you should use MySQL transaction to do this both modification in 1 transaction.
I recommend to use 2 different query in 1 transaction.
Your description about this problem is not fully clear, sorry for misunderstand.

How to save the modifications of a column in sql

I'm coding a website where users can add concerts/events and other users can modify the informations provided for every concert/event added on the website.
I want to save every modification that has been made and the user that made the modification. For instance there is this functionnality on Wikipedia where every article has saves of every modification made and the user who made it.
For now, I just save the name of the user who made the last modification(s), and the modifications aren't saved: when a modification is made it will just overwrite the previous value of the corresponding column.
You can see under the current state of the tables I'm talking about. One for concerts with different informations: artist, date of the event, hour, links related to the event etc...
and another one for users.
Current sql tables
I think a first step would to create separate tables for every element of a concert that can be modified, with 2 foreign keys (one linked to user, one linked to concert) plus the value of the element. But still, if a modification is made I still don't know how to save the previous value and previous user.
I've thought of puting values inside an array but I don't know if this is possible in SQL and if this is the best solution.
Thanks for reading/helping
You should add a column that mark the create/update time to the concert table.
Then create a new history table (same schema of concert) for storing concert changes.
When a user edit a concert:
Save the current state of concert content to the history table.
Update the new change to the concert table with the user_id of the user who perform this action
That's it! When you want to list the changes history, just query the history table!
If you want to log the delete operations as well, add another column and mark the end period of each version (a little bit more complex)

Can i create a trigger for every condition that satisfies in the database?

I'm having a trouble regarding my school project. I am making a payroll system with 300 employees. Now, I'm having a problem regarding computing the salary of each employee. I came up with an idea of making a trigger every time the employee will timeout/logout and compute the salary of each employee per day and store it in database to reduce the query load of the system. Is it possible to make a trigger every-time each employee will timeout/logout and i will create a query inside the trigger to compute/insert the salary of employee per day?. Or any idea/suggestions to solve my problem ?

How to prevent race condition in online hotel booking

I am writing a hotel booking software using PHP and MySQL. I am pretty much done with it but I ran into a race condition problem. For example there is only one standard room left and when 2 guests both select it it shows available to both of them. I tried fixing it by checking the room again when the guest clicks confirm before payment but that still has issues. I also tried making the room status to pending when whoever clicks the confirm first but I can't figure out how to change it back to available if the guest decides not to pay or just closes the browser. I searched SO for answers but I didn't really find a definitive answer. Thanks in advance
One solution is to add two columns to a table in the database. One column is the session ID or user ID or whatever of the user that is being offered the room. The second column is a timestamp indicating when that offer will expire.
Then, in your app, only show rooms that have an expired timestamp in the hold column. (Set the initial timestamp to 0 so that it starts out expired.) When a room is selected, check the column again. If there's an unexpired timestamp there, the user gets a "sorry, you were too slow" message. Otherwise, put a timestamp there for 15 minutes into the future or whatever, and proceed.
You see this on travel sites and ticket-purchasing sites a lot where it says something like "We're holding these seats for you for another 14 minutes. Please complete the transaction by then or it will be released blah blah blah."
I would also go with the pending state. You could save the state together with the session id of this user and have a cronjob that deletes all pending states that have expired session ids associated to them.

What is the best way to increment invoice ID?

I'm developing a simple invoice system using PHP and MySQL.
My initial thought was to use ID (auto incremented) for invoice ID. But that will not work.
A temporary invoice is created when user commits an order to my Payment Service Provider.
If the transaction fails, I need to delete the temporary invoice.
The problem is that the invoice has been created and given and ID - and by law, I'm not allowed to delete any invoices that has been given a invoice number. So I need to add another column 'invoice_id' and add an ID after successful transaction.
Multiple users may do a purchase at the same time.
My question is, how can I make sure to retrieve the last created invoice ID and increment this?
Should I use a $_SESSION[]for storing invoice_id? Or should I retrieve latest ID from DB? If retrieving from DB, should I lock the table for this transaction?
Any guidance much appreciated.
Create a temporary table for invoices which have not been processes.
Once the invoice has been processed, move over to permanent table and assign an invoice id using the AUTO_INCREMENT option in mysql
This will allow for two tables, one for unprocessed and the other for processed. You can also place an id field to keep track of the movement from temp -> perm
I wouldn't delete it, I would just cancel it. Additional usually after a failed transaction the trader reminds the customer to pay, which is not possible, when the invoice doesn't exist anymore.
One approach would be to keep the invoice in session until the payment has been approved. If the payment is not approved, the invoice will die with the user's session.
Another approach would be to create a holding table for pending invoices. When payment is approved, simply move the data into the invoice table.
And the last approach, go ahead and create the invoice in the invoice table, but soft delete it if the payment fails. Just set a flag, deleted = 1 and in all your procs add "where deleted = 0".
I would generate purely random invoice numbers. Simplest strategy:
md5(uniqid())
But still – storing unfinished invoices in the database sounds more sane.
Also the random factor will make guessing the invoice number more difficult.

Categories