|
8 | 8 | ) |
9 | 9 | import os |
10 | 10 | import pytest |
| 11 | +import random |
11 | 12 | import subprocess |
12 | 13 | import time |
13 | 14 | import tempfile |
14 | 15 | import unittest |
| 16 | +from concurrent import futures as concurrent_futures |
15 | 17 |
|
16 | 18 |
|
17 | 19 | def direction(src, dst): |
@@ -1908,6 +1910,47 @@ def test_reservations_leak(node_factory, executor): |
1908 | 1910 | assert l1.daemon.is_in_log("askrene-unreserve failed") is None |
1909 | 1911 |
|
1910 | 1912 |
|
| 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 | + |
1911 | 1954 | def test_unreserve_all(node_factory): |
1912 | 1955 | """Test removing all reservations.""" |
1913 | 1956 | l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True) |
|
0 commit comments