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