Skip to content

Commit db53bc4

Browse files
committed
askrene: added reservations leak test under load
1 parent 38eeb79 commit db53bc4

File tree

1 file changed

+43
-0
lines changed

1 file changed

+43
-0
lines changed

tests/test_askrene.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
)
99
import os
1010
import pytest
11+
import random
1112
import subprocess
1213
import time
1314
import tempfile
1415
import unittest
16+
from concurrent import futures as concurrent_futures
1517

1618

1719
def direction(src, dst):
@@ -1908,6 +1910,47 @@ def test_reservations_leak(node_factory, executor):
19081910
assert l1.daemon.is_in_log("askrene-unreserve failed") is None
19091911

19101912

1913+
def test_reservations_leak_under_load(node_factory, executor):
1914+
"""Stress-test reservation cleanup: concurrent payments over shared channels
1915+
must leave zero stale reservations after all payments settle."""
1916+
# Topology: two paths share l4 as a bottleneck relay.
1917+
# Path A: l1 -> l2 -> l4 -> l5
1918+
# Path B: l1 -> l3 -> l4 -> l6
1919+
# join_nodes([l1, l2, l4, l5]) creates channels: l1-l2, l2-l4, l4-l5
1920+
# join_nodes([l1, l3, l4, l6]) creates channels: l1-l3, l3-l4, l4-l6
1921+
zero_fee = {"fee-base": 0, "fee-per-satoshi": 0}
1922+
l1, l2, l3, l4, l5, l6 = node_factory.get_nodes(
1923+
6,
1924+
opts=[zero_fee] * 6,
1925+
)
1926+
node_factory.join_nodes([l1, l2, l4, l5], wait_for_announce=True) # creates channels: l1-l2, l2-l4, l4-l5
1927+
node_factory.join_nodes([l1, l3, l4, l6], wait_for_announce=True) # creates channels: l1-l3, l3-l4, l4-l6
1928+
1929+
NUM = 300
1930+
invoices = [l5.rpc.invoice(1000, f"inv-a-{i}", "x")["bolt11"] for i in range(NUM // 2)]
1931+
invoices += [l6.rpc.invoice(1000, f"inv-b-{i}", "x")["bolt11"] for i in range(NUM // 2)]
1932+
random.shuffle(invoices)
1933+
1934+
futs = [executor.submit(l1.rpc.xpay, inv) for inv in invoices]
1935+
1936+
# While payments are in flight, reservations must be non-empty: this
1937+
# checks the test isn't trivially passing on an empty table.
1938+
wait_for(lambda: l1.rpc.askrene_listreservations()["reservations"] != [])
1939+
1940+
# Make sure that we have channel contention by looking for repeating scids
1941+
def has_channel_contention():
1942+
res = l1.rpc.askrene_listreservations()["reservations"]
1943+
scids = [r["short_channel_id_dir"] for r in res]
1944+
return len(scids) != len(set(scids))
1945+
wait_for(has_channel_contention)
1946+
1947+
for f in concurrent_futures.as_completed(futs, timeout=TIMEOUT):
1948+
f.result() # raise on any payment failure
1949+
1950+
assert l1.rpc.askrene_listreservations() == {"reservations": []}
1951+
assert l1.daemon.is_in_log("reserve_remove failed") is None
1952+
1953+
19111954
def test_unreserve_all(node_factory):
19121955
"""Test removing all reservations."""
19131956
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True)

0 commit comments

Comments
 (0)