fix(fava): serialize source mutations, share HTTP client, validate BQL input
Fava-client hardening cluster (CODE-REVIEW-2026-06 #7, #14, #15, #19 + libra-#23, libra-#53): - New FavaClient.transform_source_line does the whole read-checksum-modify-write under the global write lock and maps Fava 409/412 to ChecksumConflictError. The approve and reject endpoints used to do this dance with raw httpx and no lock — two concurrent mutations raced each other and every other ledger writer (libra-#23). They now route through the new method and translate conflicts to HTTP 409. - update_entry_source / delete_entry raise ChecksumConflictError on 409/412 instead of leaking raw HTTPStatusError. - One shared httpx.AsyncClient per FavaClient (12 per-call instantiations removed — no more TCP handshake per request); closed via libra_stop. Health probes keep their 2s timeout per-request. - Account names/patterns are validated against ^[A-Za-z0-9:_-]+$ before interpolation into BQL string literals. - The posting amount regexes are consolidated into module-level compiled patterns, all decimal-tolerant — the old integer-only SATS pattern silently dropped decimal-SATS postings (Fava's @@->@ normalisation emits them) from balances. - add-account no longer verifies its own write with a second serialized get_all_accounts round-trip (libra-#53): sync_single_account_from_beancount grows an assume_exists path. New test: concurrent approve+reject must both land (was lost-update/412 before the lock). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c0c8acbe30
commit
4d63e08a69
5 changed files with 305 additions and 123 deletions
|
|
@ -285,7 +285,11 @@ async def sync_accounts_from_beancount(force_full_sync: bool = False) -> dict:
|
|||
return stats
|
||||
|
||||
|
||||
async def sync_single_account_from_beancount(account_name: str) -> bool:
|
||||
async def sync_single_account_from_beancount(
|
||||
account_name: str,
|
||||
description: Optional[str] = None,
|
||||
assume_exists: bool = False,
|
||||
) -> bool:
|
||||
"""
|
||||
Sync a single account from Beancount to Libra DB.
|
||||
|
||||
|
|
@ -294,6 +298,13 @@ async def sync_single_account_from_beancount(account_name: str) -> bool:
|
|||
|
||||
Args:
|
||||
account_name: Hierarchical account name (e.g., "Expenses:Food")
|
||||
description: Description for the Libra DB row (only used with
|
||||
assume_exists — otherwise read from Beancount metadata)
|
||||
assume_exists: Skip the Fava existence lookup. Pass when the
|
||||
caller just wrote the Open directive itself — verifying via
|
||||
a second serialized get_all_accounts round-trip doubles the
|
||||
latency of every account create for no information gain
|
||||
(libra-#53).
|
||||
|
||||
Returns:
|
||||
True if account was created/updated, False if it already existed or failed
|
||||
|
|
@ -306,6 +317,22 @@ async def sync_single_account_from_beancount(account_name: str) -> bool:
|
|||
logger.debug(f"Account already exists: {account_name}")
|
||||
return False
|
||||
|
||||
if assume_exists:
|
||||
try:
|
||||
await create_account(
|
||||
CreateAccount(
|
||||
name=account_name,
|
||||
account_type=infer_account_type_from_name(account_name),
|
||||
description=description,
|
||||
user_id=extract_user_id_from_account_name(account_name),
|
||||
)
|
||||
)
|
||||
logger.info(f"Created account (writer-asserted): {account_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to sync account {account_name}: {e}")
|
||||
return False
|
||||
|
||||
# Get from Beancount
|
||||
fava = get_fava_client()
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue