feat: create TaskManager singleton class (#3775)

Co-authored-by: dadofsambonzuki <dadofsambonzuki@users.noreply.github.com>
This commit is contained in:
dni ⚡
2026-07-08 14:07:38 +02:00
committed by GitHub
co-authored by dadofsambonzuki
parent 6ab413775a
commit fd9009f760
19 changed files with 450 additions and 381 deletions
+2 -1
View File
@@ -82,7 +82,8 @@ async def test_admin_audit_monitor_and_test_email(
headers={"Authorization": f"Bearer {superuser_token}"},
)
assert monitor.status_code == 200
assert "invoice_listeners" in monitor.json()
task_names = [t["name"] for t in monitor.json()]
assert any("invoice_listener" in name for name in task_names)
test_email = await client.get(
"/admin/api/v1/testemail",
+9 -6
View File
@@ -13,10 +13,13 @@ from lnbits.core.services import (
fee_reserve_total,
get_balance_delta,
)
from lnbits.core.services.payments import pay_invoice, update_wallet_balance
from lnbits.core.services.payments import (
pay_invoice,
update_wallet_balance,
)
from lnbits.core.services.users import create_user_account
from lnbits.exceptions import PaymentError
from lnbits.tasks import create_task, wait_for_paid_invoices
from lnbits.task_manager import task_manager
from lnbits.wallets import get_funding_source
from ..helpers import is_fake, is_regtest
@@ -160,12 +163,11 @@ async def test_create_real_invoice(
assert not payment_status["paid"]
on_paid_mock = mocker.AsyncMock()
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
task_manager.register_invoice_listener(on_paid_mock, "test_create_invoice")
pay_real_invoice(invoice["bolt11"])
await asyncio.sleep(1)
assert on_paid_mock.call_count == 1
payment = on_paid_mock.call_args_list[0][0][0]
@@ -393,12 +395,11 @@ async def test_receive_real_invoice_set_pending_and_check_state(
assert not payment_status["paid"]
on_paid_mock = mocker.AsyncMock()
create_task(wait_for_paid_invoices("test_create_invoice", on_paid_mock)())
task_manager.register_invoice_listener(on_paid_mock, "test_create_invoice")
pay_real_invoice(invoice["bolt11"])
await asyncio.sleep(1)
assert on_paid_mock.call_count == 1
payment = on_paid_mock.call_args_list[0][0][0]
@@ -412,6 +413,8 @@ async def test_receive_real_invoice_set_pending_and_check_state(
payment_status = response.json()
assert payment_status["paid"]
assert payment
# set the incoming invoice to pending
payment.status = PaymentState.PENDING
await update_payment(payment)
+24 -14
View File
@@ -5,6 +5,7 @@ import pytest
from pytest_mock.plugin import MockerFixture
from lnbits.settings import Settings
from lnbits.task_manager import task_manager
from lnbits.utils.cache import Cache, Cached
key = "foo"
@@ -13,11 +14,10 @@ value = "bar"
@pytest.fixture
async def cache():
cache = Cache(interval=0.1)
task = asyncio.create_task(cache.invalidate_forever())
cache = Cache()
task = task_manager.create_permanent_task(cache.invalidate_cache, interval=1)
yield cache
task.cancel()
task_manager.cancel_task(task)
@pytest.mark.anyio
@@ -31,13 +31,13 @@ async def test_cache_get_set(cache):
@pytest.mark.anyio
async def test_cache_expiry(cache):
# gets expired by `get` call
cache.set(key, value, expiry=0.01)
await asyncio.sleep(0.02)
cache.set(key, value, expiry=1)
await asyncio.sleep(2)
assert not cache.get(key)
# gets expired by invalidation task
cache.set(key, value, expiry=0.1)
await asyncio.sleep(0.2)
cache.set(key, value, expiry=1)
await asyncio.sleep(2)
assert key not in cache._values
assert not cache.get(key)
@@ -94,23 +94,33 @@ async def test_cache_pop_expired_returns_default(cache):
async def test_invalidate_forever_logs_and_recovers_from_errors(
settings: Settings, mocker: MockerFixture
):
test_cache = Cache(interval=0)
logger_error = mocker.patch("lnbits.utils.cache.logger.error")
test_cache = Cache()
original_running = settings.lnbits_running
calls = 0
async def fake_sleep(_interval):
original_invalidate = test_cache.invalidate_cache
async def fake_invalidate():
nonlocal calls
calls += 1
if calls == 1:
raise RuntimeError("boom")
settings.lnbits_running = False
await original_invalidate()
mocker.patch.object(test_cache, "invalidate_cache", side_effect=fake_invalidate)
mocker.patch("lnbits.task_manager.asyncio.sleep")
logger_error = mocker.patch("lnbits.task_manager.logger.error")
bg_task = None
try:
settings.lnbits_running = True
mocker.patch("lnbits.utils.cache.asyncio.sleep", side_effect=fake_sleep)
await test_cache.invalidate_forever()
bg_task = task_manager.create_permanent_task(test_cache.invalidate_cache)
await bg_task.task
finally:
settings.lnbits_running = original_running
if bg_task:
task_manager.cancel_task(bg_task)
logger_error.assert_called_once_with("Error invalidating cache")
assert logger_error.called
assert calls == 2
+10 -5
View File
@@ -1717,7 +1717,9 @@ async def test_check_fiat_status_handles_internal_states(mocker: MockerFixture):
"lnbits.core.services.fiat_providers.get_fiat_provider",
AsyncMock(return_value=provider),
)
queue_put = mocker.patch("lnbits.tasks.internal_invoice_queue.put", AsyncMock())
queue_put = mocker.patch(
"lnbits.task_manager.task_manager.internal_invoice_queue.put_nowait"
)
success_status = await check_fiat_status(
Payment(
@@ -1734,7 +1736,8 @@ async def test_check_fiat_status_handles_internal_states(mocker: MockerFixture):
)
assert success_status.success is True
queue_put.assert_awaited_once_with("fiat_pending")
queue_put.assert_called_once()
assert queue_put.call_args[0][0].checking_id == "fiat_pending"
await check_fiat_status(
Payment(
@@ -1749,7 +1752,7 @@ async def test_check_fiat_status_handles_internal_states(mocker: MockerFixture):
extra={"fiat_checking_id": "stripe_checking_id"},
)
)
assert queue_put.await_count == 1
assert queue_put.call_count == 1
@pytest.mark.anyio
@@ -1786,7 +1789,9 @@ async def test_check_fiat_status_persists_successful_payment(
"lnbits.fiat.StripeWallet.get_invoice_status",
AsyncMock(return_value=FiatPaymentStatus(paid=True)),
)
queue_put = mocker.patch("lnbits.tasks.internal_invoice_queue.put", AsyncMock())
queue_put = mocker.patch(
"lnbits.task_manager.task_manager.internal_invoice_queue.put_nowait"
)
status = await check_fiat_status(payment)
@@ -1794,7 +1799,7 @@ async def test_check_fiat_status_persists_successful_payment(
assert payment.status == PaymentState.SUCCESS
updated_payment = await get_payment(payment.checking_id)
assert updated_payment.status == PaymentState.SUCCESS
queue_put.assert_awaited_once_with(payment.checking_id)
queue_put.assert_called_once_with(payment)
@pytest.mark.anyio
+17 -12
View File
@@ -12,15 +12,12 @@ from lnbits.core.crud import create_wallet, get_standalone_payment, get_wallet
from lnbits.core.crud.payments import get_payment, get_payments_paginated
from lnbits.core.models import PaymentState, Wallet
from lnbits.core.services import create_invoice, create_user_account, pay_invoice
from lnbits.core.services.payments import update_wallet_balance
from lnbits.core.services.payments import (
update_wallet_balance,
)
from lnbits.exceptions import InvoiceError, PaymentError
from lnbits.settings import Settings
from lnbits.tasks import (
create_task,
internal_invoice_listener,
internal_invoice_queue,
wait_for_paid_invoices,
)
from lnbits.task_manager import task_manager
from lnbits.wallets.base import PaymentResponse
from lnbits.wallets.fake import FakeWallet
@@ -237,24 +234,30 @@ async def test_notification_for_internal_payment(
test_name = "test_notification_for_internal_payment"
# Drain stale items left by session-scoped fixtures (e.g. update_wallet_balance)
while not internal_invoice_queue.empty():
while not task_manager.internal_invoice_queue.empty():
try:
internal_invoice_queue.get_nowait()
task_manager.internal_invoice_queue.get_nowait()
except asyncio.QueueEmpty:
break
on_paid_mock = mocker.AsyncMock()
create_task(internal_invoice_listener())
create_task(wait_for_paid_invoices(test_name, on_paid_mock)())
# create_task(internal_invoice_listener())
task_manager.register_invoice_listener(on_paid_mock, test_name)
payment = await create_invoice(
wallet_id=to_wallet.id,
amount=123,
memo=test_name,
webhook="http://test.404.lnbits.com",
)
await pay_invoice(
paid_payment = await pay_invoice(
wallet_id=to_wallet.id, payment_request=payment.bolt11, extra={"tag": "lnurlp"}
)
assert paid_payment.status == PaymentState.SUCCESS.value
assert paid_payment.bolt11 == payment.bolt11
assert paid_payment.amount == -123_000
await asyncio.sleep(1)
assert on_paid_mock.call_count == 1
@@ -264,6 +267,8 @@ async def test_notification_for_internal_payment(
assert _payment.status == PaymentState.SUCCESS.value
assert _payment.bolt11 == payment.bolt11
assert _payment.amount == 123_000
assert _payment.checking_id == payment.checking_id
updated_payment = await get_payment(_payment.checking_id)
assert (
updated_payment.webhook_status is not None
+3 -3
View File
@@ -197,8 +197,7 @@ async def test_update_wallet_balance_validates_credit_and_debit(
settings.lnbits_wallet_limit_max_balance = 0
queue_mock = mocker.patch(
"lnbits.tasks.internal_invoice_queue_put",
mocker.AsyncMock(),
"lnbits.task_manager.task_manager.internal_invoice_queue.put_nowait",
)
await update_wallet_balance(wallet, 5)
@@ -212,7 +211,8 @@ async def test_update_wallet_balance_validates_credit_and_debit(
]
assert credit_payments
assert credit_payments[0].status == PaymentState.SUCCESS
queue_mock.assert_awaited_once_with(credit_payments[0].checking_id)
queue_mock.assert_called_once()
assert queue_mock.call_args[0][0].checking_id == credit_payments[0].checking_id
@pytest.mark.anyio