Web › Module 6 › Lesson 4
Lab — Race Condition Lab
Simulate a one-time coupon race locally, then fix it with an atomic update
Opening
Redeem twice — get paid twice?
Build a tiny in-memory coupon service, fire parallel redeems, observe double credit, then add an atomic guard. Use localhost only.
1. Step 1 — Vulnerable coupon API
race_lab.pyfrom flask import Flask, jsonify import threading, time app = Flask(__name__) uses = {"remaining": 1} lock = threading.Lock() @app.post("/redeem-unsafe") def redeem_unsafe(): remaining = uses["remaining"] time.sleep(0.05) # widen race window if remaining > 0: uses["remaining"] = remaining - 1 return jsonify(ok=True, credit=10) return jsonify(ok=False), 400 @app.post("/redeem-safe") def redeem_safe(): with lock: if uses["remaining"] <= 0: return jsonify(ok=False), 400 uses["remaining"] -= 1 return jsonify(ok=True, credit=10) if __name__ == "__main__": app.run(port=5060, threaded=True)
from flask import Flask, jsonify
import threading, time
app = Flask(__name__)
uses = {"remaining": 1}
lock = threading.Lock()
@app.post("/redeem-unsafe")
def redeem_unsafe():
remaining = uses["remaining"]
time.sleep(0.05) # widen race window
if remaining > 0:
uses["remaining"] = remaining - 1
return jsonify(ok=True, credit=10)
return jsonify(ok=False), 400
@app.post("/redeem-safe")
def redeem_safe():
with lock:
if uses["remaining"] <= 0:
return jsonify(ok=False), 400
uses["remaining"] -= 1
return jsonify(ok=True, credit=10)
if __name__ == "__main__":
app.run(port=5060, threaded=True)2. Step 2 — Parallel redeem
Reset remaining=1, then send two simultaneous POSTs to /redeem-unsafe (two terminals or a small script). Both may return ok=True. Repeat against /redeem-safe — only one should succeed.
Lab checklist
Production note
In-memory locks fail across multiple servers. Prefer database constraints and idempotency keys.