Critical: No concurrency protection for Fava/Beancount ledger writes #4
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The Castle extension has no synchronization mechanisms protecting concurrent writes to the Fava/Beancount ledger. This can lead to data corruption, lost entries, duplicate payments, and unbalanced accounting records under concurrent load.
Problem Analysis
No Locks Found
After thorough code analysis, no asyncio.Lock, threading.Lock, or any mutual exclusion mechanism exists in the codebase for Fava write operations.
Critical Race Conditions
1.
fava_client.py:add_entry()(Lines 51-110)The core method that submits entries to Fava is completely unprotected:
Impact: Two concurrent expense submissions can:
2.
fava_client.py:add_account()(Lines 1217-1340)Account creation has a check-then-act race condition:
When two requests try to create the same user account:
3. Payment Settlement Race (
tasks.py:on_invoice_paid)Background task processing paid invoices:
If user pays invoice AND submits expense simultaneously, settlement entry uses stale balance data.
4. Duplicate Invoice Processing
Idempotency check followed by unprotected insert:
Failure Scenarios
Recommended Solutions
Option 1: Application-Level Locking (Recommended)
Add an asyncio.Lock in FavaClient:
Pros: Simple, effective, no external dependencies
Cons: Serializes ALL writes (potential bottleneck), doesn't scale horizontally
Option 2: Per-User Locking
Use a lock manager with per-user locks:
Pros: Better parallelism, users don't block each other
Cons: More complex, still doesn't help global operations
Option 3: Distributed Locking (Production Scale)
For multi-instance deployments, use Redis-based locking:
Pros: Works across multiple LNbits instances
Cons: Requires Redis dependency
Option 4: Optimistic Concurrency with Retry
For
add_account, implement retry with exponential backoff:Option 5: Fava Source of Truth + Idempotency Keys
Leverage Beancount links for idempotency:
Implementation Priority
FavaClient(Option 1)add_account(Option 4)Affected Files
fava_client.py- Core write operationsviews_api.py- All entry creation endpointstasks.py- Background payment processingcrud.py-get_or_create_user_account()Severity
CRITICAL - Under moderate concurrent load (>5 users), accounting data corruption is likely. The ledger can become unbalanced, entries can be lost, and duplicate payments can occur.