View Issue Details

IDProjectCategoryView StatusLast Update
0007726OXID eShop (all versions)1.03. Basket, checkout processpublic2024-10-30 15:25
ReporterGalanx Assigned To 
PrioritynormalSeveritymajorReproducibilitysometimes
Status confirmedResolutionopen 
Product Version6.5.4 
Summary0007726: Race Condition Leads to Basket Deletion When Adding Items Quickly in Multi-Pod Kubernetes Setup with Large Baskets
DescriptionIn an OXID eShop 6.5 environment running in a Kubernetes cluster with multiple pods, customers have reported that their shopping basket sometimes gets deleted or emptied when they attempt to add items to the basket quickly, especially when the basket already contains a large number of items. Upon investigating the logs, we noticed that a duplicate entry MySQL error is logged just before the basket is lost.

The issue appears to be caused by race conditions when multiple pods process simultaneous requests to modify and save the same basket. When a customer rapidly adds items to a basket, different pods may handle these requests concurrently, causing conflicts during the basket recalculation and save operations. Specifically:

The calculateBasket() method calls _save(), which first deletes the entire basket from the database, then attempts to reinsert each item individually.
If two pods attempt to delete and reinsert the basket contents at the same time, this can lead to conflicting operations, duplicate entry errors, or the basket being lost entirely.
This issue becomes more pronounced when the basket contains a large number of items, as the process of deleting and reinserting all items takes longer, increasing the chance of concurrent transactions and race conditions.

Analysis:
Concurrent Save Operations (_save):

When the customer adds an item to the basket, the calculateBasket() method is triggered, which eventually calls the _save() method.
The _save() method first deletes all basket contents from the database, and then re-inserts each item individually.
If two or more pods execute _save() concurrently, each pod will delete the basket and attempt to reinsert the items, leading to conflicts and duplicate entry errors in MySQL.
Race Condition on Basket Deletion and Reinsertion:

If Pod A deletes the basket and begins reinserting items, and Pod B deletes the basket before Pod A has finished, this can cause inconsistencies in the basket state, potentially leading to partial or complete loss of the basket.
Impact of Large Baskets:

When the basket contains a large number of items (e.g., 300+), the deletion and reinsertion process takes significantly longer. This increases the risk of concurrent save operations across different pods, exacerbating the issue and leading to a higher likelihood of conflicting transactions and basket deletion.
Steps To Reproduce- Set up OXID eShop 6.5 in a Kubernetes cluster with at least two pods.
- Enable session handling via Redis to centralize session storage across the pods.
- As a customer, add around 300 items to the basket.
- After the basket is populated with a large number of items, add new products to the basket quickly in succession (e.g., add multiple products or increase quantities rapidly).
- Observe that the basket may be deleted or reset unexpectedly after some items are added.
Additional InformationThe issue is more pronounced when a customer’s basket contains a large number of items (e.g., 300+), as the time required to delete and reinsert the items increases the likelihood of conflicting transactions between different pods.
TagsBasket, Checkout, Product domain and basket rewrite
ThemeNot defined
BrowserNot defined
PHP VersionNot defined
Database VersionNot defined

Activities

QA

2024-09-26 11:44

administrator   ~0017607

Last edited: 2024-09-26 11:45

I was able to (brute force) reproduce the case in a single server setup with the OXID eShop version 7.1.

To do that, it's necessary to simulate pods and one database server which does process requests at different paces.
The pods will be simulated in a simple way by having ten open tabs in one browser. To have the database work at different paces, the class Basket has to be manipulated.

Open vendor/oxid-esales/oxideshop-ce/source/Application/Model/Basket.php and edit the method Basket::save. Change the lines:
$oSavedBasket->delete();

                //then save
to
$oSavedBasket->delete();
sleep(rand(1, 20));
                //then save


This change simulates a request (to delete the basket before repopulating it again) processed in time x, while an other requests is done in time y.

1. Install Enterprise Edition 7.1.0
2. Log in with an user in the frontend.
3. Open one product in 10 different tabs.
4. Click the button "to basket" as fast as you can in every tab.
5. Wait until every tab is processed.
6. Open the basket. It probably has an item count different to 10. If not:
7. Repeat steps 4., 5. and 6.

During my test I was able to reproduce the case by the second try. I had only 2 items in the basket instead of 10.
To test the test case, I removed the product from the basket, removed the additional code in Basket::save and tested it again: Always 10 items of the product in the basket.

-MK