forked from bitcoinafterlife/bal-electrum-plugin
The Building Will wizard (BalBuildWillDialog.loop_push) still broadcast the will to will-executors sequentially -- a for-loop calling push_transactions_to_willexecutor one server at a time. This is the slow "Broadcasting your will to executors: Trasmissione" step the user saw: a slow/dead server blocked the whole wizard, just like the non-wizard path did before it was parallelized. Rewrite loop_push to use Willexecutors.push_transactions_parallel (the same helper already used by window.push_transactions_to_willexecutors): - Pre-filter to the user-selected will-executors only. - Push to all selected servers concurrently (ThreadPoolExecutor); each server keeps its own retry behaviour, but a slow/dead server no longer blocks the others. Total time ~= slowest server, not the sum. - on_each callback does thread-safe book-keeping + UI update via msg_edit_row (which emits a pyqtSignal marshalled to the GUI thread). - 'already present' servers are collected and their stored tx verified sequentially afterwards (original check_transaction logic preserved). - Preserve the retry flag and the _stopping cancellation checks. tests/parallel_ping_test.py: add a static check asserting loop_push uses push_transactions_parallel and no longer contains the sequential push loop. Tests: 182 official + smoke/overflow/gui_fixes/parallel/external_zip all pass. ruff: no new issues; new code is PEP8-compliant.
150 lines
5.5 KiB
Python
150 lines
5.5 KiB
Python
"""
|
|
Regression / behaviour test for the parallel will-executor networking.
|
|
|
|
Before this change, pinging / pushing to will-executor servers was done in a
|
|
sequential loop where every unreachable server blocked the whole batch for the
|
|
full timeout (plus up to 10 retries with 3s sleeps). With N servers the total
|
|
wall-clock time was the *sum* of every server's time, so a couple of dead
|
|
servers froze the GUI ("Non risponde") for minutes.
|
|
|
|
This test patches Willexecutors.get_info_task / push_transactions_to_willexecutor
|
|
with slow stubs and asserts that:
|
|
* ping_servers_parallel contacts servers concurrently (total time ~= the
|
|
slowest server, NOT the sum), and
|
|
* the on_each callback is invoked once per server with the right ok flag,
|
|
* push_transactions_parallel behaves the same way.
|
|
|
|
Run with:
|
|
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
|
|
python3 tests/parallel_ping_test.py <PLUGIN_IMPORT_NAME>
|
|
"""
|
|
import importlib
|
|
import sys
|
|
import time
|
|
|
|
PKG = sys.argv[1] if len(sys.argv) > 1 else "electrum.plugins.bal"
|
|
|
|
SLOW = 0.5 # seconds each simulated server takes to answer
|
|
N = 8 # number of servers
|
|
|
|
|
|
def main():
|
|
we_mod = importlib.import_module(f"{PKG}.core.willexecutors")
|
|
W = we_mod.Willexecutors
|
|
|
|
# ---- 1) ping_servers_parallel: time ~= slowest, not sum ----
|
|
def slow_get_info(url, we, **kwargs):
|
|
time.sleep(SLOW)
|
|
# half the servers "fail"
|
|
if "dead" in url:
|
|
we["status"] = "KO"
|
|
else:
|
|
we["status"] = 200
|
|
return we
|
|
|
|
orig_get_info = W.get_info_task
|
|
W.get_info_task = staticmethod(slow_get_info)
|
|
try:
|
|
wes = {}
|
|
for i in range(N):
|
|
kind = "dead" if i % 2 else "ok"
|
|
wes[f"https://{kind}-{i}.example"] = {}
|
|
|
|
seen = []
|
|
|
|
def on_each(url, we, ok):
|
|
seen.append((url, ok))
|
|
|
|
start = time.time()
|
|
W.ping_servers_parallel(wes, on_each=on_each, max_workers=N)
|
|
elapsed = time.time() - start
|
|
|
|
# Sequential would take ~ N * SLOW. Parallel must be far less.
|
|
sequential = N * SLOW
|
|
assert elapsed < sequential * 0.6, (
|
|
f"not parallel: {elapsed:.2f}s vs sequential {sequential:.2f}s")
|
|
print(f"[OK] ping parallel: {elapsed:.2f}s for {N} servers "
|
|
f"(sequential would be ~{sequential:.2f}s)")
|
|
|
|
# callback fired once per server, with correct ok flags
|
|
assert len(seen) == N, seen
|
|
for url, ok in seen:
|
|
assert ok == ("ok" in url), (url, ok)
|
|
print("[OK] on_each fired once per server with correct ok flag")
|
|
|
|
# results written back into the mapping
|
|
for url, we in wes.items():
|
|
if "ok" in url:
|
|
assert we["status"] == 200, (url, we)
|
|
else:
|
|
assert we["status"] == "KO", (url, we)
|
|
print("[OK] ping results written back into the willexecutors mapping")
|
|
finally:
|
|
W.get_info_task = orig_get_info
|
|
|
|
# ---- 2) push_transactions_parallel: time ~= slowest, not sum ----
|
|
def slow_push(we, **kwargs):
|
|
time.sleep(SLOW)
|
|
return "fail" not in we["url"]
|
|
|
|
orig_push = W.push_transactions_to_willexecutor
|
|
W.push_transactions_to_willexecutor = staticmethod(slow_push)
|
|
try:
|
|
wes = {}
|
|
for i in range(N):
|
|
kind = "fail" if i % 2 else "good"
|
|
wes[f"https://{kind}-{i}.example"] = {
|
|
"url": f"https://{kind}-{i}.example",
|
|
"txs": "deadbeef",
|
|
"txsids": [f"id{i}"],
|
|
}
|
|
|
|
pushed = []
|
|
|
|
def on_each_push(url, we, ok, exc):
|
|
pushed.append((url, ok))
|
|
|
|
start = time.time()
|
|
results = W.push_transactions_parallel(wes, on_each=on_each_push,
|
|
max_workers=N)
|
|
elapsed = time.time() - start
|
|
|
|
sequential = N * SLOW
|
|
assert elapsed < sequential * 0.6, (
|
|
f"push not parallel: {elapsed:.2f}s vs {sequential:.2f}s")
|
|
print(f"[OK] push parallel: {elapsed:.2f}s for {N} servers "
|
|
f"(sequential would be ~{sequential:.2f}s)")
|
|
|
|
assert len(results) == N, results
|
|
for url, (ok, exc) in results.items():
|
|
assert ok == ("good" in url), (url, ok)
|
|
print("[OK] push results correct for every server")
|
|
finally:
|
|
W.push_transactions_to_willexecutor = orig_push
|
|
|
|
# ---- 3) the wizard's loop_push must use the parallel helper ----
|
|
# The "Building Will" wizard broadcasts via BalBuildWillDialog.loop_push.
|
|
# It previously looped over servers sequentially (one
|
|
# push_transactions_to_willexecutor call at a time), which is exactly the
|
|
# slow path the user saw at "Broadcasting your will to executors". Make
|
|
# sure it now delegates to push_transactions_parallel.
|
|
import inspect
|
|
dialogs_mod = importlib.import_module(f"{PKG}.gui.qt.dialogs")
|
|
loop_push_src = inspect.getsource(dialogs_mod.BalBuildWillDialog.loop_push)
|
|
code = "\n".join(
|
|
line for line in loop_push_src.splitlines()
|
|
if not line.lstrip().startswith("#")
|
|
)
|
|
assert "push_transactions_parallel" in code, (
|
|
"wizard loop_push must use push_transactions_parallel (parallel push)")
|
|
assert "for url, willexecutor in willexecutors.items()" not in code, (
|
|
"wizard loop_push must not push to servers in a sequential loop")
|
|
print("[OK] wizard loop_push uses push_transactions_parallel (not sequential)")
|
|
|
|
print(f"\n[OK] parallel networking test passed for package {PKG!r}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|