fix(provision): publish default stall in background to avoid blocking signup (#7)
Some checks failed
ci.yml / fix(provision): publish default stall in background to avoid blocking signup (#7) (pull_request) Failing after 0s

`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) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-03 18:37:32 +02:00
commit 774c3586a1

View file

@ -288,19 +288,58 @@ async def provision_merchant(
# Publish the kind 30017 stall event so customers' clients can resolve # Publish the kind 30017 stall event so customers' clients can resolve
# the stall name when they fetch products. Non-fatal on failure: a # the stall name when they fetch products. Non-fatal on failure: a
# later product publish (or webapp self-heal) will retry. # later product publish (or webapp self-heal) will retry.
try: #
stall_event = await sign_and_send_to_nostr(merchant, default_stall) # Fire-and-forget: `nostr_client.publish_nostr_event` has no per-relay
default_stall.event_id = stall_event.id # deadline and will block indefinitely if every configured relay is
await update_stall(merchant.id, default_stall) # unreachable (cf. aiolabs/nostrmarket#7). When `provision_merchant`
except Exception as ex: # is called from the eager signup hook (lnbits/core/services/users.py
logger.warning( # ::_create_default_merchant, aiolabs/lnbits#46), inline-awaiting that
f"[NOSTRMARKET] Failed to publish default stall for " # publish hangs the uvicorn worker on `POST /auth/register` forever.
f"merchant {merchant.id}: {ex}" # 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 return merchant
# Generous bound: signing through the bunker can take 12 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 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)
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}"
)
async def handle_order_paid(order_id: str, merchant_pubkey: str): async def handle_order_paid(order_id: str, merchant_pubkey: str):
try: try:
order = await update_order_paid_status(order_id, True) order = await update_order_paid_status(order_id, True)