A small architecture problem I encountered and would love a solution.
I have a website that sells products with limited inventory, as soon as a customer clicks a purchase button my server updates the database as if he bought with a flag of what and how much he bought in case he cancels.
The product is saved for the costumer for 75 seconds.
If the 75 seconds timer pass or he cancels/leaves the site, I restore the purchase and database to the original state before the purchase.
If payment is received I cancel the timer.
The reason for the complicated mechanism is that the operation of the purchase cannot be carried out immediately (I use third-party crypto services to receive payment and it can take a few seconds from the moment you click buy on the website until the actual payment is made)
This solution works when there is one server, but the 75 second timer is saved as a normal variable on the server. That is, in a situation where there are several servers, a timer can be created on one server and the actual purchase action will be sent to another server that will not be able to cancel the timer that restore the purchase, or alternatively if the server crashes and with it all the variables.
are there another ways to implement this? is there a tool where i can save all the timers in one persistant place?
this variable is an object that contain all the setTimeout timers :
let timeouts = {}
when the user intends to purchase i update the database and do:
timeouts = { ...timeouts, [buyerAddress]: setTimeout(() => restoreBuy(buyerAddress), 75000) }
if i get a payment i cancel the timeout:
clearTimeout(timeouts[buyerAddress])
the restoreBuy function activate when the user cancel or after 75 seconds. it restore the user object to the state before the purchase.
2
Answers
creating/maintaining timers for every purchase in every server sounds very bad practise. I think you should consider changing your solution, moving to multi-server architecture.
Usually, we deal with these issues on db level, for example you can have transactions that block any other update on the row you use, reduce the stock by 1 for a bit, maybe keep some kind of flag on the row you update etc. This depends on the technologies you use and your use case. Also, this will raise questions regarding race conditions which, again, you ll have to deal with depending on your tech-stack
The general solution is to not use a timer. Instead use a timestamp.
When an order is created create a timestamp of either the time of order or the time the order expires. This is recorded in the db along with the order.
Periodically check expired orders that have not yet been cancelled and cancel them and restore inventory (you can do this every 10 seconds, every minute etc. – just pick a frequency that your servers can handle).
This means you may from time to time run out of inventory because your cancel service have not yet processed expired orders. This is generally preferable to overselling as refunding the user may cost you more.
If you need the cancel processing to always be up to date then you can simply run the expired order processing every time someone submits a new order or request product listing. Obviously this increases processing time of orders and/or getting product listing so choose an implementation that balances accuracy with performance.