Bug: Kafka auto-commit + lost update race condition in payment simulation
## Found by [Quorum](https://github.com/KaustubhUp025/quorum) — Distributed Coordination Reviewer Quorum is an AI-powered code review tool that detects coordination anti-patterns in distributed systems. It found two real bugs in this project during a ground-truth validation run. --- ### Bug 1 — `services/src/bank_consumers/kafka_utils.py`: Kafka Auto-Commit Enabled (RULE_08) **Severity:** HIGH ```python # Current code return KafkaConsumer( *topics, bootstrap_servers=settings.bootstrap_servers, group_id=group_id, auto_offset_reset="earliest", enable_auto_commit=True, # ← bug ... ) ``` **What is wrong:** With `enable_auto_commit=True`, Kafka commits the offset on a timer, regardless of whether the message was successfully processed. If the consumer crashes between the auto-commit and the completion of processing, the message is silently lost — it will never be redelivered. **Suggested fix:** ```python return KafkaConsumer( *topics, bootstrap_servers=settings.bootstrap_servers, group_id=group_id, auto_offset_reset="earliest", enable_auto_commit=False, # ← disable auto-commit ... ) # Then manually commit after successful processing: # consumer.commit() ``` **Reference:** [New Relic — Kafka Consumer Auto-Commit: Data Loss and Duplication](https://newrelic.com/blog/best-practices/kafka-consumer-auto-commit-data-loss-and-duplication) --- ### Bug 2 — `services/src/bank_consumers/simulate.py`: Lost Update Race Condition (RULE_10) **Severity:** CRITICAL ```python def _deposit(cur, account: dict) -> None: amount = round(random.uniform(50, 8000), 2) new_balance = float(account["balance"]) + amount # ← stale read from earlier cur.execute( "UPDATE accounts SET balance = %s WHERE id = %s", (new_balance, account["id"]), ) ``` **What is wrong:** `account["balance"]` was read earlier in `_get_accounts()`, in a separate transaction (with `conn.autocommit = True`). By the time `_deposit` runs, another concurrent writer may have already changed the balance. The `UPDATE` overwrites the concurrent change silently — classic lost update. **Suggested fix:** Use `SELECT ... FOR UPDATE` in the same transaction as the `UPDATE`: ```python def _deposit(cur, account_id: str, amount: float) -> None: # Re-read balance under a lock in the same transaction cur.execute( "SELECT balance FROM accounts WHERE id = %s FOR UPDATE", (account_id,), ) current_balance = cur.fetchone()[0] new_balance = float(current_balance) + amount cur.execute( "UPDATE accounts SET balance = %s WHERE id = %s", (new_balance, account_id), ) ``` **Reference:** Kleppmann — *Designing Data-Intensive Applications* §7 "The Trouble with Transactions" --- *This issue was automatically generated by [Quorum](https://github.com/KaustubhUp025/quorum), an open-source distributed coordination reviewer built with Gemini 2.5 Pro + GitLab MCP. Quorum reviews merge requests for coordination anti-patterns that static analysis tools like SonarQube and Semgrep cannot detect.*
issue