Polish account-creation flow: insertion point, user_id consistency, startup race
Three small fixes shaken out by live testing on aio-demo: 1. fava_client.add_account: when the target file has no Open directives yet (e.g. the empty accounts/users.beancount seed), append at end of file instead of inserting at index 0. Keeps the seed header comments at the top where they belong. 2. account_sync.sync_single_account_from_beancount: read the full user_id from Beancount metadata when present, fall back to the name-derived 8-char prefix otherwise. crud.get_or_create_user_account writes the full 32-char user_id into Beancount metadata when creating per-user accounts; the sync function was only looking at the account name and returning the prefix, so the post-sync `WHERE user_id=:user_id` query in crud.py missed the row and fell through the UNIQUE-constraint recovery path. Three lines of warning noise per user-account creation. 3. tasks.wait_for_account_sync: await `wait_for_fava_client()` (new helper backed by an asyncio.Event in fava_client.py) before the first sync iteration. Previously the sync task started in libra_start() raced the fire-and-forget `_init_fava()` coroutine and reliably crashed the first run with "Fava client not initialized". Refs: aiolabs/libra#28
This commit is contained in:
parent
d82443d040
commit
09a5d6ed55
3 changed files with 40 additions and 3 deletions
|
|
@ -320,11 +320,20 @@ async def sync_single_account_from_beancount(account_name: str) -> bool:
|
||||||
|
|
||||||
# Create in Libra DB
|
# Create in Libra DB
|
||||||
account_type = infer_account_type_from_name(account_name)
|
account_type = infer_account_type_from_name(account_name)
|
||||||
user_id = extract_user_id_from_account_name(account_name)
|
|
||||||
|
|
||||||
|
# Prefer the full user_id stored in Beancount metadata (libra writes it
|
||||||
|
# when crud.get_or_create_user_account calls fava.add_account). Fall
|
||||||
|
# back to the name-derived 8-char prefix for accounts imported without
|
||||||
|
# metadata. This keeps user_id consistent with what the caller will
|
||||||
|
# query for, avoiding a churn cycle through the UNIQUE-constraint
|
||||||
|
# recovery path in crud.py.
|
||||||
description = None
|
description = None
|
||||||
|
meta_user_id = None
|
||||||
if "meta" in bc_account and isinstance(bc_account["meta"], dict):
|
if "meta" in bc_account and isinstance(bc_account["meta"], dict):
|
||||||
description = bc_account["meta"].get("description")
|
description = bc_account["meta"].get("description")
|
||||||
|
meta_user_id = bc_account["meta"].get("user_id")
|
||||||
|
|
||||||
|
user_id = meta_user_id or extract_user_id_from_account_name(account_name)
|
||||||
|
|
||||||
await create_account(
|
await create_account(
|
||||||
CreateAccount(
|
CreateAccount(
|
||||||
|
|
|
||||||
|
|
@ -1626,9 +1626,12 @@ class FavaClient:
|
||||||
logger.info(f"Account {account_name} already exists in {target_file}")
|
logger.info(f"Account {account_name} already exists in {target_file}")
|
||||||
return {"data": sha256sum, "mtime": source_data.get("mtime", "")}
|
return {"data": sha256sum, "mtime": source_data.get("mtime", "")}
|
||||||
|
|
||||||
# Step 3: Find insertion point (after last Open directive AND its metadata)
|
# Step 3: Find insertion point (after last Open directive AND its metadata).
|
||||||
|
# If the file has no Open directives yet (e.g. the empty
|
||||||
|
# accounts/users.beancount seed), append at end of file
|
||||||
|
# so the seed header comments stay at the top.
|
||||||
lines = source.split('\n')
|
lines = source.split('\n')
|
||||||
insert_index = 0
|
insert_index = None
|
||||||
for i, line in enumerate(lines):
|
for i, line in enumerate(lines):
|
||||||
if line.strip().startswith(('open ', f'{opening_date.year}-')) and 'open' in line:
|
if line.strip().startswith(('open ', f'{opening_date.year}-')) and 'open' in line:
|
||||||
# Found an Open directive, now skip over any metadata lines
|
# Found an Open directive, now skip over any metadata lines
|
||||||
|
|
@ -1636,6 +1639,8 @@ class FavaClient:
|
||||||
# Skip metadata lines (lines starting with whitespace)
|
# Skip metadata lines (lines starting with whitespace)
|
||||||
while insert_index < len(lines) and lines[insert_index].startswith((' ', '\t')) and lines[insert_index].strip():
|
while insert_index < len(lines) and lines[insert_index].startswith((' ', '\t')) and lines[insert_index].strip():
|
||||||
insert_index += 1
|
insert_index += 1
|
||||||
|
if insert_index is None:
|
||||||
|
insert_index = len(lines)
|
||||||
|
|
||||||
# Step 4: Format Open directive as Beancount text
|
# Step 4: Format Open directive as Beancount text
|
||||||
currencies_str = ", ".join(currencies)
|
currencies_str = ", ".join(currencies)
|
||||||
|
|
@ -1989,6 +1994,10 @@ class FavaClient:
|
||||||
|
|
||||||
# Singleton instance (configured from settings)
|
# Singleton instance (configured from settings)
|
||||||
_fava_client: Optional[FavaClient] = None
|
_fava_client: Optional[FavaClient] = None
|
||||||
|
# Set by init_fava_client; await for background tasks that must not run
|
||||||
|
# before the client exists (otherwise they raise "Fava client not initialized"
|
||||||
|
# during the first ~500ms of startup).
|
||||||
|
_fava_client_ready: asyncio.Event = asyncio.Event()
|
||||||
|
|
||||||
|
|
||||||
def init_fava_client(fava_url: str, ledger_slug: str, timeout: float = 10.0):
|
def init_fava_client(fava_url: str, ledger_slug: str, timeout: float = 10.0):
|
||||||
|
|
@ -2002,9 +2011,21 @@ def init_fava_client(fava_url: str, ledger_slug: str, timeout: float = 10.0):
|
||||||
"""
|
"""
|
||||||
global _fava_client
|
global _fava_client
|
||||||
_fava_client = FavaClient(fava_url, ledger_slug, timeout)
|
_fava_client = FavaClient(fava_url, ledger_slug, timeout)
|
||||||
|
_fava_client_ready.set()
|
||||||
logger.info(f"Fava client initialized: {fava_url}/{ledger_slug}")
|
logger.info(f"Fava client initialized: {fava_url}/{ledger_slug}")
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_fava_client() -> FavaClient:
|
||||||
|
"""Block until init_fava_client() has been called, then return the client.
|
||||||
|
|
||||||
|
Use this from background tasks started in libra_start() — they otherwise
|
||||||
|
race the fire-and-forget _init_fava() coroutine and crash with
|
||||||
|
"Fava client not initialized" on first iteration.
|
||||||
|
"""
|
||||||
|
await _fava_client_ready.wait()
|
||||||
|
return get_fava_client()
|
||||||
|
|
||||||
|
|
||||||
def get_fava_client() -> FavaClient:
|
def get_fava_client() -> FavaClient:
|
||||||
"""
|
"""
|
||||||
Get the configured Fava client.
|
Get the configured Fava client.
|
||||||
|
|
|
||||||
7
tasks.py
7
tasks.py
|
|
@ -134,8 +134,15 @@ async def wait_for_account_sync():
|
||||||
Background task that periodically syncs accounts from Beancount to Libra DB.
|
Background task that periodically syncs accounts from Beancount to Libra DB.
|
||||||
|
|
||||||
Runs hourly to ensure Libra DB stays in sync with Beancount.
|
Runs hourly to ensure Libra DB stays in sync with Beancount.
|
||||||
|
|
||||||
|
Blocks on `wait_for_fava_client()` before the first iteration so we don't
|
||||||
|
race the fire-and-forget `_init_fava()` started in `libra_start()` and
|
||||||
|
fail the first sync with "Fava client not initialized".
|
||||||
"""
|
"""
|
||||||
|
from .fava_client import wait_for_fava_client
|
||||||
|
|
||||||
logger.info("[LIBRA] Account sync background task started")
|
logger.info("[LIBRA] Account sync background task started")
|
||||||
|
await wait_for_fava_client()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue