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:
Padreug 2026-07-12 15:42:44 +02:00
commit 4d63e08a69
5 changed files with 305 additions and 123 deletions

View file

@ -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: