From 774c3586a172ce5130617ba7518a77611e78e846 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 3 Jun 2026 18:37:32 +0200 Subject: [PATCH] fix(provision): publish default stall in background to avoid blocking signup (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `provision_merchant` is awaited inline by lnbits's eager default-merchant hook (lnbits/core/services/users.py::_create_default_merchant, aiolabs/lnbits#46). The pre-fix code inline-awaited `sign_and_send_to_nostr(merchant, default_stall)`, whose terminal `nostr_client.publish_nostr_event` has no per-relay deadline — every configured external relay being unreachable from the lnbits process pinned the uvicorn worker on `POST /auth/register` forever, with no exception ever raised. Subsequent signup / login attempts then queued behind that worker, locking out the instance until restart. This was filed as aiolabs/nostrmarket#7 and reproduces deterministically on the regtest dev stack whenever external relays aren't reachable from the docker network. The same hang reproduces whether or not the NIP-46 bunker is in the loop — the publish is the culprit, not the signer. Fix: - Schedule the publish via `asyncio.create_task(...)`. The signup response returns immediately after the DB rows we control are committed; the publish completes (or fails, or times out) in the background. Matches the existing comment "Non-fatal on failure: a later product publish (or webapp self-heal) will retry." - Wrap the background publish in `asyncio.wait_for` with a 30 s cap so a permanently-unreachable relay set doesn't leave an asyncio task pinned for the lifetime of the uvicorn process. Timeout logs at warning; `event_id` simply stays NULL on the stall row until a later republish lands it. Verified locally (regtest, bunker disabled, LocalSigner path): - signup `POST /auth/register` returns in <3 s with a valid JWT - background publish lands the kind-30017 stall event on the relay ~12 s later - merchant / stall rows persist with the expected names Co-Authored-By: Claude Opus 4.7 (1M context) --- services.py | 49 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/services.py b/services.py index 5da63ef..444a9d5 100644 --- a/services.py +++ b/services.py @@ -288,18 +288,57 @@ async def provision_merchant( # Publish the kind 30017 stall event so customers' clients can resolve # the stall name when they fetch products. Non-fatal on failure: a # later product publish (or webapp self-heal) will retry. + # + # Fire-and-forget: `nostr_client.publish_nostr_event` has no per-relay + # deadline and will block indefinitely if every configured relay is + # unreachable (cf. aiolabs/nostrmarket#7). When `provision_merchant` + # is called from the eager signup hook (lnbits/core/services/users.py + # ::_create_default_merchant, aiolabs/lnbits#46), inline-awaiting that + # publish hangs the uvicorn worker on `POST /auth/register` forever. + # The DB rows we just wrote are sufficient to serve the wallet UI; + # the stall event_id gets backfilled when the publish completes (or + # stays NULL until a later resubscribe-driven republish lands it). + asyncio.create_task( + _publish_default_stall_background(merchant.id, merchant, default_stall) + ) + + return merchant + + +# Generous bound: signing through the bunker can take 1–2 s on a cold +# session, plus the relay publish itself. 30 s is well over both, and +# the cap matters only when the relay set is unreachable. +STALL_PUBLISH_TIMEOUT_S = 30.0 + + +async def _publish_default_stall_background( + merchant_id: str, merchant: Merchant, default_stall: Stall +) -> None: + """Background helper for `provision_merchant`'s default-stall publish. + + Bounded by `STALL_PUBLISH_TIMEOUT_S` so even a permanently-unreachable + relay set doesn't pin an asyncio task forever. Errors and timeouts are + logged at warning — never raised, since the caller scheduled-and-forgot. + """ try: - stall_event = await sign_and_send_to_nostr(merchant, default_stall) + stall_event = await asyncio.wait_for( + sign_and_send_to_nostr(merchant, default_stall), + timeout=STALL_PUBLISH_TIMEOUT_S, + ) default_stall.event_id = stall_event.id - await update_stall(merchant.id, default_stall) + await update_stall(merchant_id, default_stall) + except asyncio.TimeoutError: + logger.warning( + f"[NOSTRMARKET] Default stall publish for merchant " + f"{merchant_id} timed out after {STALL_PUBLISH_TIMEOUT_S}s; " + f"event_id stays NULL until a later republish lands it" + ) except Exception as ex: logger.warning( f"[NOSTRMARKET] Failed to publish default stall for " - f"merchant {merchant.id}: {ex}" + f"merchant {merchant_id}: {ex}" ) - return merchant - async def handle_order_paid(order_id: str, merchant_pubkey: str): try: