Files
cbd420/RACE_CONDITION_FIX.md
2025-12-21 08:43:43 +01:00

5.2 KiB

Race Condition Fix - Implementation Summary

Problem

When multiple buyers attempt to purchase the last available units simultaneously, a race condition occurs where inventory can be oversold.

Solution

Implemented a 10-minute reservation system using the pending_orders table to temporarily hold inventory during checkout.

Changes Made

1. Database Migration

  • File: migrations/add_expires_at_to_pending_orders.sql
  • Added expires_at column to pending_orders table
  • Added index on expires_at for efficient cleanup queries

To apply the migration:

-- Run the migration file
SOURCE migrations/add_expires_at_to_pending_orders.sql;

2. Create Invoice Endpoint (app/api/payments/create-invoice/route.ts)

  • Atomic Inventory Reservation: Uses database transactions to atomically check and reserve inventory
  • Pending Orders Check: Includes non-expired pending orders when calculating available inventory
  • Expiration Time: Sets expires_at to 10 minutes from creation
  • NOWPayments Integration: Sets invoice_timeout to 600 seconds (10 minutes) when creating invoice

3. Active Drop Endpoint (app/api/drops/active/route.ts)

  • Fill Calculation: Includes non-expired pending orders in fill calculation
  • UI Display: Progress bar now shows total reserved inventory (sales + pending orders)

4. Cleanup Endpoint (app/api/payments/cleanup-expired/route.ts)

  • Automatic Cleanup: Removes expired pending orders
  • NOWPayments Status Check: Verifies payment status before cleanup
  • Monitoring: GET endpoint to check count of expired orders

5. UI Updates (app/components/Drop.tsx)

  • Visual Indicator: Added note that items are held for 10 minutes during checkout
  • Progress Bar: Automatically reflects reserved inventory (including pending orders)

Setup Instructions

1. Run Database Migration

mysql -u your_user -p your_database < migrations/add_expires_at_to_pending_orders.sql

2. Set Up Cleanup Job

The cleanup endpoint should be called periodically (recommended: every 1-2 minutes) to remove expired pending orders.

Option A: Cron Job (Linux/Mac)

# Add to crontab (crontab -e)
*/2 * * * * curl -X POST https://your-domain.com/api/payments/cleanup-expired -H "Authorization: Bearer YOUR_CLEANUP_TOKEN"

Option B: Vercel Cron (if using Vercel)

Add to vercel.json:

{
  "crons": [{
    "path": "/api/payments/cleanup-expired",
    "schedule": "*/2 * * * *"
  }]
}

Option C: Node.js Cron Library

const cron = require('node-cron');
const fetch = require('node-fetch');

cron.schedule('*/2 * * * *', async () => {
  await fetch('http://localhost:3000/api/payments/cleanup-expired', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.CLEANUP_API_TOKEN}`
    }
  });
});

3. Environment Variables

Add to your .env file:

# Required: URL for external IPN handler service
IPN_CALLBACK_URL=http://your-ipn-service.com/api/payments/ipn-callback

# Optional: Token for cleanup endpoint security
CLEANUP_API_TOKEN=your-secure-random-token

Note: IPN callbacks are handled by an external service. The IPN_CALLBACK_URL must point to your external IPN handler that processes payment confirmations and creates sales. See IPN_INTEGRATION_README.md for integration details.

How It Works

  1. Purchase Initiation: When a buyer clicks "Join Drop":

    • System checks available inventory (sales + non-expired pending orders)
    • If available, creates pending order with 10-minute expiration
    • Creates NOWPayments invoice with 10-minute timeout
    • Inventory is now "on hold" for 10 minutes
  2. During Checkout:

    • Progress bar shows reserved inventory (including pending orders)
    • Other buyers see reduced availability
    • If buyer completes payment within 10 minutes → sale is created
    • If buyer doesn't complete payment within 10 minutes → order expires
  3. Expiration:

    • Cleanup job runs every 1-2 minutes
    • Expired pending orders are removed
    • Inventory becomes available again
    • NOWPayments invoice expires on their end
  4. Payment Confirmation (handled by external IPN service):

    • External IPN handler receives payment notification from NOWPayments
    • Checks if pending order is expired
    • Validates final inventory availability
    • Creates sale record if valid
    • Deletes pending order

Testing

  1. Test Race Condition:

    • Open two browser windows
    • Try to purchase the last available unit simultaneously
    • Only one should succeed
    • The other should see "Not enough inventory remaining"
  2. Test Expiration:

    • Create a pending order
    • Wait 10+ minutes
    • Run cleanup endpoint manually
    • Verify order is removed
  3. Test Payment Flow:

    • Create pending order
    • Complete payment within 10 minutes
    • Verify sale is created
    • Verify pending order is deleted

Notes

  • Pending orders are automatically included in inventory calculations
  • The 10-minute hold prevents overselling while giving buyers time to complete payment
  • NOWPayments invoices also expire after 10 minutes
  • Cleanup job should run frequently (every 1-2 minutes) to free up inventory quickly