Improved SSE listeners (#865)
* logging listeners * comments * generate privkey upon init * listener queue * remove duplicate check * make format * reuse channel * error handling in sse listener * uuid for listeners * register named invoices * uuid for listeners and listener list * fix poetry lock * setuptools * requirements asyncio timeout * setuptool;s * make format * remove async-timeout * async_timeout readd * try lower setuptools version * try lower lower setuptools version * back to current version + fix, maybe * fix worflows to use poetry 1.2.1 * remove uneeded setuptools from build-system * fix up formatting workflow * debug to trace * more traces * debug logs to trace Co-authored-by: dni ⚡ <office@dnilabs.com>
This commit is contained in:
+62
-22
@@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from http import HTTPStatus
|
||||
from typing import Callable, List
|
||||
from typing import Callable, Dict, List
|
||||
|
||||
from fastapi.exceptions import HTTPException
|
||||
from loguru import logger
|
||||
@@ -18,20 +19,6 @@ from lnbits.settings import WALLET
|
||||
|
||||
from .core import db
|
||||
|
||||
deferred_async: List[Callable] = []
|
||||
|
||||
|
||||
def record_async(func: Callable) -> Callable:
|
||||
def recorder(state):
|
||||
deferred_async.append(func)
|
||||
|
||||
return recorder
|
||||
|
||||
|
||||
async def run_deferred_async():
|
||||
for func in deferred_async:
|
||||
asyncio.create_task(catch_everything_and_restart(func))
|
||||
|
||||
|
||||
async def catch_everything_and_restart(func):
|
||||
try:
|
||||
@@ -50,18 +37,48 @@ async def send_push_promise(a, b) -> None:
|
||||
pass
|
||||
|
||||
|
||||
invoice_listeners: List[asyncio.Queue] = []
|
||||
class SseListenersDict(dict):
|
||||
"""
|
||||
A dict of sse listeners.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = None):
|
||||
self.name = name or f"sse_listener_{str(uuid.uuid4())[:8]}"
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
assert type(key) == str, f"{key} is not a string"
|
||||
assert type(value) == asyncio.Queue, f"{value} is not an asyncio.Queue"
|
||||
logger.trace(f"sse: adding listener {key} to {self.name}. len = {len(self)+1}")
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
|
||||
return super().__delitem__(key)
|
||||
|
||||
_RaiseKeyError = object() # singleton for no-default behavior
|
||||
|
||||
def pop(self, key, v=_RaiseKeyError) -> None:
|
||||
logger.trace(f"sse: removing listener from {self.name}. len = {len(self)-1}")
|
||||
return super().pop(key)
|
||||
|
||||
|
||||
def register_invoice_listener(send_chan: asyncio.Queue):
|
||||
invoice_listeners: Dict[str, asyncio.Queue] = SseListenersDict("invoice_listeners")
|
||||
|
||||
|
||||
def register_invoice_listener(send_chan: asyncio.Queue, name: str = None):
|
||||
"""
|
||||
A method intended for extensions to call when they want to be notified about
|
||||
new invoice payments incoming.
|
||||
A method intended for extensions (and core/tasks.py) to call when they want to be notified about
|
||||
new invoice payments incoming. Will emit all incoming payments.
|
||||
"""
|
||||
invoice_listeners.append(send_chan)
|
||||
name_unique = f"{name or 'no_name'}_{str(uuid.uuid4())[:8]}"
|
||||
logger.trace(f"sse: registering invoice listener {name_unique}")
|
||||
invoice_listeners[name_unique] = send_chan
|
||||
|
||||
|
||||
async def webhook_handler():
|
||||
"""
|
||||
Returns the webhook_handler for the selected wallet if present. Used by API.
|
||||
"""
|
||||
handler = getattr(WALLET, "webhook_listener", None)
|
||||
if handler:
|
||||
return await handler()
|
||||
@@ -72,18 +89,36 @@ internal_invoice_queue: asyncio.Queue = asyncio.Queue(0)
|
||||
|
||||
|
||||
async def internal_invoice_listener():
|
||||
"""
|
||||
internal_invoice_queue will be filled directly in core/services.py
|
||||
after the payment was deemed to be settled internally.
|
||||
|
||||
Called by the app startup sequence.
|
||||
"""
|
||||
while True:
|
||||
checking_id = await internal_invoice_queue.get()
|
||||
logger.info("> got internal payment notification", checking_id)
|
||||
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
||||
|
||||
|
||||
async def invoice_listener():
|
||||
"""
|
||||
invoice_listener will collect all invoices that come directly
|
||||
from the backend wallet.
|
||||
|
||||
Called by the app startup sequence.
|
||||
"""
|
||||
async for checking_id in WALLET.paid_invoices_stream():
|
||||
logger.info("> got a payment notification", checking_id)
|
||||
asyncio.create_task(invoice_callback_dispatcher(checking_id))
|
||||
|
||||
|
||||
async def check_pending_payments():
|
||||
"""
|
||||
check_pending_payments is called during startup to check for pending payments with
|
||||
the backend and also to delete expired invoices. Incoming payments will be
|
||||
checked only once, outgoing pending payments will be checked regularly.
|
||||
"""
|
||||
outgoing = True
|
||||
incoming = True
|
||||
|
||||
@@ -133,9 +168,14 @@ async def perform_balance_checks():
|
||||
|
||||
|
||||
async def invoice_callback_dispatcher(checking_id: str):
|
||||
"""
|
||||
Takes incoming payments, sets pending=False, and dispatches them to
|
||||
invoice_listeners from core and extensions.
|
||||
"""
|
||||
payment = await get_standalone_payment(checking_id, incoming=True)
|
||||
if payment and payment.is_in:
|
||||
logger.trace("sending invoice callback for payment", checking_id)
|
||||
logger.trace(f"sse sending invoice callback for payment {checking_id}")
|
||||
await payment.set_pending(False)
|
||||
for send_chan in invoice_listeners:
|
||||
for chan_name, send_chan in invoice_listeners.items():
|
||||
logger.trace(f"sse sending to chan: {chan_name}")
|
||||
await send_chan.put(payment)
|
||||
|
||||
Reference in New Issue
Block a user