As the famous saying goes, There are only two hard things in Computer Science: cache invalidation and naming things. The author of this saying has apparently never run into any problems with process synchronization.
Threads... Threads everywhere
It's easy to forget that although we usually don't write asynchronous code in PHP and the WordPress backend, each line of code executes sequentially; in practice, the server runs and executes multiple PHP processes simultaneously. For this reason,every plugin’s code is always executed concurrently. We must always bear in mind the problems that arise in multi-threaded and distributed environments.
A typical WordPress problem where multithreading rears its ugly head is when, based on the value of a variable stored in the database, we set a new value for this variable. Strangely enough, such situations happen very often.
Snake in the grass
Let's assume that, as in WooCommerce, there is a post_type named product, and its metadata store information about how many products are in stock. When a customer buys a product, we want to reduce the number of products in stock by one. How to do it? Nothing is easier: we read the number of products in stock, decrease the value by one, and save it back.
Trivial? Yes, as long as we do not receive a report from the first angry customer who has sold more products than they had in stock. The next customer reports that they still have products in stock, but the store says there are no more. What has happened? Has anyone hacked our WordPress to put random numbers into the database? Or maybe it's because we haven’t used WooCommerce methods? Let's try:
Now it's PRO. We can now go to sleep, sure that this time we did everything perfectly. Unfortunately, the morning will bring us another sad customer. What happened? How could we have made a fatal error in two lines of otherwise obviously correct code?
But why doesn't it work
As you might have guessed, the problem is that multiple PHP processes can modify the same information simultaneously. Imagine that two different customers purchase the same product at the same time. Both processes take place parallelly. The first process gets the number of products in stock, the second process also does it a millisecond later; both get the same value, say 9. Both processes decrease this value by 1 and get 8, and both write the number 8 to the database. We sold two products, but the shop thinks there are still 8 products in stock. If no one notices this, products that are not in stock will soon be purchased.
While one PHP process was calmly executing line-by-line commands, another process slipped into the same place in the code and mixed up everything.
Will this happen frequently, or is it a purely academic problem? Unfortunately, and counterintuitively, since threads tend to wait together for I/O operations, this type of code will often fail even in a small store. It's just a matter of time.
How to solve this problem?
Often, and this is also the case here, it is possible to replace several operations with one atomic operation. An atomic operation cannot be subdivided into smaller ones. Such an operation is either done or not done and will never be done a little. Because when we do the work with an atomic operation, other PHP processes will not enter between our commands. With one command, we will simultaneously get, change, and save the warehouse's stock. How to do it?
Certainly NOT like this:
Such refactoring does not change anything. PHP processes will continue to enter and execute the
Database to the rescue
We can take advantage of the database and the fact that a single UPDATE/DELETE operation is always atomic.
Now we have a code that will reduce the number of products in stock with one atomic operation.Note that we cannot use the wpdb::update method because this command does not enable decreasing the value, only overwriting it.
What to do when the task cannot be easily reduced to a single atomic operation? How does a database handle reading, changing, and writing values atomically? I will share this with you in the next posts!