mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 12:28:22 +00:00
perf: cut mint requests per payout cycle
This commit is contained in:
+22
-10
@@ -1165,7 +1165,15 @@ async def get_wallet(
|
||||
retry_on_rate_limit: bool = True,
|
||||
force_reload: bool = False,
|
||||
load_proofs: bool = True,
|
||||
force_reload_proofs: bool = False,
|
||||
) -> Wallet:
|
||||
"""Return the cached wallet for a mint/unit, refreshing state as needed.
|
||||
|
||||
``force_reload`` refreshes both mint keysets and proofs from scratch.
|
||||
``force_reload_proofs`` only bypasses the proof reload interval, keeping
|
||||
the cached keysets: proofs live in the local DB, so this costs no mint
|
||||
requests beyond the normal keyset refresh interval.
|
||||
"""
|
||||
global _wallets, _wallet_last_load, _wallet_last_mint_load, _wallet_load_locks
|
||||
id = f"{mint_url}_{unit}"
|
||||
lock = _wallet_load_locks.setdefault(id, asyncio.Lock())
|
||||
@@ -1197,6 +1205,7 @@ async def get_wallet(
|
||||
last_proof_load = _wallet_last_load.get(id)
|
||||
if (
|
||||
force_reload
|
||||
or force_reload_proofs
|
||||
or last_proof_load is None
|
||||
or now - last_proof_load
|
||||
>= _WALLET_PROOF_RELOAD_MIN_INTERVAL_SECONDS
|
||||
@@ -1545,12 +1554,20 @@ async def _payout_mint_and_unit(mint_url: str, unit: str) -> None:
|
||||
try:
|
||||
# Runs under wallet_operation_guard; a cached wallet may carry a proof
|
||||
# snapshot up to 30s stale from another process's reservation, so the
|
||||
# cross-process lock is only safe with a fresh reload.
|
||||
wallet = await get_wallet(mint_url, unit, force_reload=True)
|
||||
# cross-process lock is only safe with a fresh proof reload. Proofs
|
||||
# come from the local DB; keysets stay on their normal refresh interval
|
||||
# so each cycle does not re-fetch every keyset from the mint.
|
||||
wallet = await get_wallet(mint_url, unit, force_reload_proofs=True)
|
||||
proofs = get_proofs_per_mint_and_unit(wallet, mint_url, unit, not_reserved=True)
|
||||
if not proofs:
|
||||
# Nothing to pay out, so skip the settle delay rather than hold the
|
||||
# cross-process guard (and block credits) for a wallet with no funds.
|
||||
min_amount = (
|
||||
settings.min_payout_sat
|
||||
if unit == "sat"
|
||||
else _sats_to_msats(settings.min_payout_sat)
|
||||
)
|
||||
if sum(proof.amount for proof in proofs) <= min_amount:
|
||||
# Nothing payable even before subtracting liabilities, so skip the
|
||||
# proof-state check and settle delay rather than spend mint quota
|
||||
# and hold the cross-process guard (blocking credits) for nothing.
|
||||
return
|
||||
proofs = await slow_filter_spend_proofs(proofs, wallet)
|
||||
await asyncio.sleep(5)
|
||||
@@ -1582,11 +1599,6 @@ async def _payout_mint_and_unit(mint_url: str, unit: str) -> None:
|
||||
user_balance = _msats_to_sats_ceil(user_balance)
|
||||
proofs_balance = sum(proof.amount for proof in proofs)
|
||||
available_balance = proofs_balance - user_balance
|
||||
min_amount = (
|
||||
settings.min_payout_sat
|
||||
if unit == "sat"
|
||||
else _sats_to_msats(settings.min_payout_sat)
|
||||
)
|
||||
if available_balance > min_amount:
|
||||
amount_received = await raw_send_to_lnurl(
|
||||
wallet,
|
||||
|
||||
@@ -156,7 +156,7 @@ async def test_periodic_payout_isolates_failing_mint() -> None:
|
||||
from routstr.core.settings import settings
|
||||
|
||||
async def _get_wallet(
|
||||
mint_url: str, unit: str, force_reload: bool = False
|
||||
mint_url: str, unit: str, force_reload_proofs: bool = False
|
||||
) -> MagicMock:
|
||||
if mint_url == "http://bad:3338":
|
||||
raise RuntimeError("mint unreachable")
|
||||
@@ -226,7 +226,9 @@ async def test_periodic_payout_handles_session_creation_failure() -> None:
|
||||
patch("routstr.wallet.get_wallet", AsyncMock(return_value=MagicMock())),
|
||||
patch(
|
||||
"routstr.wallet.get_proofs_per_mint_and_unit",
|
||||
MagicMock(return_value=[MagicMock(amount=100_000)]),
|
||||
# Above min_payout_sat for both sat and msat wallets so neither
|
||||
# unit is skipped before the liability session is opened.
|
||||
MagicMock(return_value=[MagicMock(amount=1_000_000)]),
|
||||
),
|
||||
patch(
|
||||
"routstr.wallet.slow_filter_spend_proofs",
|
||||
@@ -255,3 +257,84 @@ async def test_payout_units_excludes_units_the_sender_cannot_pay() -> None:
|
||||
AsyncMock(return_value=["usd", "sat", "eur", "msat"]),
|
||||
):
|
||||
assert await _payout_units("http://mint:3338") == ["sat", "msat"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_periodic_payout_reloads_proofs_without_refetching_keysets() -> None:
|
||||
"""The payout cycle must not force a full keyset refresh from the mint."""
|
||||
from routstr.core.settings import settings
|
||||
|
||||
get_wallet = AsyncMock(return_value=MagicMock())
|
||||
|
||||
with (
|
||||
patch.object(settings, "cashu_mints", ["http://mint:3338"]),
|
||||
patch.object(settings, "primary_mint", "http://mint:3338"),
|
||||
patch.object(settings, "receive_ln_address", "owner@ln.tld"),
|
||||
patch.object(settings, "payout_interval_seconds", _INTERVAL),
|
||||
patch.object(settings, "min_payout_sat", 10),
|
||||
patch("routstr.wallet.asyncio.sleep", _one_cycle_sleep()),
|
||||
patch("routstr.wallet.db.create_session", _fake_session),
|
||||
patch(
|
||||
"routstr.wallet._get_supported_mint_units",
|
||||
AsyncMock(return_value=["sat"]),
|
||||
),
|
||||
patch("routstr.wallet.get_wallet", get_wallet),
|
||||
patch(
|
||||
"routstr.wallet.get_proofs_per_mint_and_unit",
|
||||
MagicMock(return_value=[MagicMock(amount=100_000)]),
|
||||
),
|
||||
patch(
|
||||
"routstr.wallet.slow_filter_spend_proofs",
|
||||
AsyncMock(side_effect=lambda proofs, wallet: proofs),
|
||||
),
|
||||
patch(
|
||||
"routstr.wallet.db.total_user_liability",
|
||||
AsyncMock(return_value=0),
|
||||
),
|
||||
patch("routstr.wallet.raw_send_to_lnurl", AsyncMock(return_value=1000)),
|
||||
):
|
||||
with pytest.raises(_LoopBreak):
|
||||
await periodic_payout()
|
||||
|
||||
get_wallet.assert_awaited_once_with(
|
||||
"http://mint:3338", "sat", force_reload_proofs=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_periodic_payout_skips_proof_state_check_below_min_payout() -> None:
|
||||
"""A wallet at or below min_payout_sat costs no mint requests."""
|
||||
from routstr.core.settings import settings
|
||||
|
||||
check_state = AsyncMock(side_effect=lambda proofs, wallet: proofs)
|
||||
raw_send = AsyncMock(return_value=1000)
|
||||
|
||||
with (
|
||||
patch.object(settings, "cashu_mints", ["http://mint:3338"]),
|
||||
patch.object(settings, "primary_mint", "http://mint:3338"),
|
||||
patch.object(settings, "receive_ln_address", "owner@ln.tld"),
|
||||
patch.object(settings, "payout_interval_seconds", _INTERVAL),
|
||||
patch.object(settings, "min_payout_sat", 210),
|
||||
patch("routstr.wallet.asyncio.sleep", _one_cycle_sleep()),
|
||||
patch("routstr.wallet.db.create_session", _fake_session),
|
||||
patch(
|
||||
"routstr.wallet._get_supported_mint_units",
|
||||
AsyncMock(return_value=["sat"]),
|
||||
),
|
||||
patch("routstr.wallet.get_wallet", AsyncMock(return_value=MagicMock())),
|
||||
patch(
|
||||
"routstr.wallet.get_proofs_per_mint_and_unit",
|
||||
MagicMock(return_value=[MagicMock(amount=200)]),
|
||||
),
|
||||
patch("routstr.wallet.slow_filter_spend_proofs", check_state),
|
||||
patch(
|
||||
"routstr.wallet.db.total_user_liability",
|
||||
AsyncMock(return_value=0),
|
||||
),
|
||||
patch("routstr.wallet.raw_send_to_lnurl", raw_send),
|
||||
):
|
||||
with pytest.raises(_LoopBreak):
|
||||
await periodic_payout()
|
||||
|
||||
check_state.assert_not_awaited()
|
||||
raw_send.assert_not_awaited()
|
||||
|
||||
@@ -143,6 +143,19 @@ async def test_get_wallet_force_reload_bypasses_reload_interval() -> None:
|
||||
assert mock_wallet.load_proofs.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_wallet_force_reload_proofs_keeps_cached_keysets() -> None:
|
||||
from routstr.wallet import get_wallet
|
||||
|
||||
mock_wallet = Mock(load_mint=AsyncMock(), load_proofs=AsyncMock())
|
||||
with patch("routstr.wallet.Wallet.with_db", AsyncMock(return_value=mock_wallet)):
|
||||
await get_wallet("http://mint:3338", "sat")
|
||||
await get_wallet("http://mint:3338", "sat", force_reload_proofs=True)
|
||||
|
||||
assert mock_wallet.load_mint.await_count == 1
|
||||
assert mock_wallet.load_proofs.await_count == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_recieve_token_holds_wallet_operation_guard() -> None:
|
||||
inside_guard = False
|
||||
@@ -2017,7 +2030,7 @@ async def test_payout_reloads_wallet_snapshot_under_guard() -> None:
|
||||
await _payout_mint_and_unit("https://mint.example.com", "sat")
|
||||
|
||||
mock_get_wallet.assert_awaited_once_with(
|
||||
"https://mint.example.com", "sat", force_reload=True
|
||||
"https://mint.example.com", "sat", force_reload_proofs=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user