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>
95 lines
3.1 KiB
Python
95 lines
3.1 KiB
Python
import asyncio
|
|
|
|
from fastapi import APIRouter
|
|
from loguru import logger
|
|
|
|
from .crud import db
|
|
from .tasks import wait_for_paid_invoices
|
|
from .views import libra_generic_router
|
|
from .views_api import libra_api_router
|
|
|
|
libra_ext: APIRouter = APIRouter(prefix="/libra", tags=["Libra"])
|
|
libra_ext.include_router(libra_generic_router)
|
|
libra_ext.include_router(libra_api_router)
|
|
|
|
libra_static_files = [
|
|
{
|
|
"path": "/libra/static",
|
|
"name": "libra_static",
|
|
}
|
|
]
|
|
|
|
scheduled_tasks: list[asyncio.Task] = []
|
|
|
|
|
|
def libra_stop():
|
|
"""Clean up background tasks on extension shutdown"""
|
|
for task in scheduled_tasks:
|
|
try:
|
|
task.cancel()
|
|
except Exception as ex:
|
|
logger.warning(ex)
|
|
|
|
# Close the Fava client's shared HTTP connection pool. libra_stop is
|
|
# synchronous, so schedule the close; if the loop is already gone the
|
|
# sockets die with the process anyway.
|
|
from .fava_client import _fava_client
|
|
|
|
if _fava_client is not None:
|
|
try:
|
|
asyncio.get_event_loop().create_task(_fava_client.aclose())
|
|
except Exception as ex:
|
|
logger.warning(f"Could not close Fava HTTP client: {ex}")
|
|
|
|
|
|
def libra_start():
|
|
"""Initialize Libra extension background tasks"""
|
|
from lnbits.tasks import create_permanent_unique_task
|
|
from .fava_client import init_fava_client
|
|
from .models import LibraSettings
|
|
from .tasks import wait_for_account_sync
|
|
|
|
async def _init_fava():
|
|
"""Load saved settings from DB, fall back to defaults."""
|
|
from .crud import db as libra_db
|
|
|
|
settings = None
|
|
try:
|
|
row = await libra_db.fetchone(
|
|
"SELECT * FROM extension_settings LIMIT 1",
|
|
model=LibraSettings,
|
|
)
|
|
if row:
|
|
settings = row
|
|
logger.info(f"Loaded Libra settings from DB: {settings.fava_url}/{settings.fava_ledger_slug}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not load settings from DB: {e}")
|
|
|
|
if not settings:
|
|
settings = LibraSettings()
|
|
logger.info(f"Using default Libra settings: {settings.fava_url}/{settings.fava_ledger_slug}")
|
|
|
|
init_fava_client(
|
|
fava_url=settings.fava_url,
|
|
ledger_slug=settings.fava_ledger_slug,
|
|
timeout=settings.fava_timeout
|
|
)
|
|
logger.info(f"Fava client initialized: {settings.fava_url}/{settings.fava_ledger_slug}")
|
|
|
|
try:
|
|
asyncio.get_event_loop().create_task(_init_fava())
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize Fava client: {e}")
|
|
logger.warning("Libra will not function without Fava. Please configure Fava settings.")
|
|
|
|
# Start background tasks
|
|
task = create_permanent_unique_task("ext_libra", wait_for_paid_invoices)
|
|
scheduled_tasks.append(task)
|
|
|
|
# Start account sync task (runs hourly)
|
|
sync_task = create_permanent_unique_task("ext_libra_account_sync", wait_for_account_sync)
|
|
scheduled_tasks.append(sync_task)
|
|
logger.info("Libra account sync task started (runs hourly)")
|
|
|
|
|
|
__all__ = ["libra_ext", "libra_static_files", "db", "libra_start", "libra_stop"]
|