Preventing Duplicate Discounts: A Race Condition in ESHOPMAN Cart Promotions
Preventing Duplicate Discounts: A Race Condition in ESHOPMAN Cart Promotions
Ensuring accurate pricing and discount application is paramount for any e-commerce platform. The ESHOPMAN community recently identified and addressed a critical race condition within the platform's cart promotion workflow that could lead to incorrect cart totals due to duplicated discounts. This insight details the issue, its technical root cause, and the proposed solutions, offering valuable knowledge for ESHOPMAN developers and merchants.
The Problem: Inaccurate Cart Totals from Duplicate Adjustments
Under specific high-concurrency scenarios, when multiple concurrent requests are made to the ESHOPMAN Store API to apply the same promotion code to a single cart, the system could inadvertently create duplicate line item adjustments. While the promotion itself would appear only once in the cart's overall promotions list, the individual line items could show multiple identical adjustments for that single promotion. This resulted in the cart's discount_total being incorrectly calculated, potentially leading to zero or even negative cart totals.
Consider this reproduction scenario using concurrent API calls:
curl --http2 --parallel --parallel-immediate \
-H "Content-Type: application/json" \
-H "x-publishable-api-key: $PK" \
-H "Authorization: Bearer $JWT" \
--data-raw '{"promo_codes":["$CODE"]}' "$ESHOPMAN_STORE_API_URL/store/carts/$CART/promotions" \
--next \
-H "Content-Type: application/json" \
-H "x-publishable-api-key: $PK" \
-H "Authorization: Bearer $JWT" \
--data-raw '{"promo_codes":["$CODE"]}' "$ESHOPMAN_STORE_API_URL/store/carts/$CART/promotions"
After executing such requests, inspecting the cart would reveal that while the promotion is listed once, the items[].adjustments array could contain multiple entries for the same promotion, causing the discount_total to reflect these duplicates.
Understanding the Race Condition
The core of the issue lay within ESHOPMAN's internal updateCartPromotionsWorkflow, a key component in managing cart updates and promotions. Specifically, the cart's state was being fetched early in the workflow (via a step like useQueryGraphStep) before a crucial lock was acquired for that specific cart (via acquireLockStep). This sequence created a window for a race condition:
- Concurrent Reads: When two or more requests for the same cart and promotion arrived almost simultaneously, both workflow instances would read the same, pre-promotion snapshot of the cart. This snapshot would not yet reflect any pending promotion applications.
- Stale Computation: Each workflow instance would then proceed to compute the necessary promotion actions based on this stale cart snapshot. Since both read the same initial state, both would determine that the same adjustments needed to be applied.
- Serialized Writes, Duplicated Results: Although the
acquireLockStepeventually serialized the write operations, ensuring that only one workflow could modify the cart at a given moment, both workflows had already independently computed the same set of adjustments. Consequently, both would attempt to insert these identical adjustments, leading to duplicate records in the database. The ESHOPMAN platform's way of linking promotions to carts would deduplicate the promotion itself, but the underlying adjustment rows remained additive.
The Solution: Ensuring Cart State Consistency
The ESHOPMAN team, with valuable input from the community, identified two primary approaches to resolve this race condition and ensure that cart state is consistent during promotion application:
- Acquire Lock Before Fetching Cart: The most straightforward solution involves reordering the workflow steps. By acquiring the cart lock before fetching the cart's data, any subsequent read operation would guarantee access to the most up-to-date state, as only one workflow instance could proceed at a time.
- Re-fetch Cart After Acquiring Lock: Alternatively, if fetching the cart before the lock is unavoidable for some architectural reason, the cart could be explicitly re-fetched after the lock has been successfully acquired. This ensures that the promotion computation steps operate on a fresh, non-stale snapshot of the cart.
A community member has already initiated a pull request implementing the first option, moving the lock acquisition to occur before the cart fetch step within the ESHOPMAN core. This fix is crucial for maintaining data integrity and ensuring accurate financial calculations within ESHOPMAN storefronts deployed via HubSpot CMS.
Key Takeaway for ESHOPMAN Developers
This incident highlights the importance of careful concurrency management, especially in critical commerce workflows like cart and promotion handling. When developing custom features or integrations for ESHOPMAN, always consider potential race conditions where multiple users or processes might interact with the same data simultaneously. Proper locking mechanisms and ensuring data freshness are essential for robust and reliable headless commerce applications powered by ESHOPMAN and HubSpot.