feat(net): invio/ping/download verso Will-Executor in parallelo (anti-freeze)
Problema: i server Will-Executor venivano contattati in sequenza e, su timeout, send_request riprovava 10x con sleep 3s (~130s per server morto). Un solo server irraggiungibile bloccava l'intera operazione (impallamento UI). Soluzione: - ThreadPoolExecutor: ping e push ora in parallelo (tempo ~= server piu' lento, non la somma). Un server morto non blocca piu' gli altri. - Fast-fail per operazioni interattive (ping/info/download): max_retries=0, niente retry-storm. - Feedback live: callback on_each() aggiorna il dialog server-per-server (thread-safe via pyqtSignal di BalWaitingDialog.update). - Push transazioni: parallelo ma con retry per-server mantenuti (no perdita tx). File: - bal/core/willexecutors.py: send_request(+max_retries,+retry_sleep), get_info_task fast-fail, NEW ping_servers_parallel(), push_transactions_parallel(), DEFAULT_TIMEOUT=5. - bal/gui/qt/window.py: ping_willexecutors_task + push_transactions_to_willexecutors riscritti su helper paralleli con feedback live; fetch_will_executors_list fast-fail. - bal/core/util.py: BUGFIX get_value_amount usava in_output (bool) invece di din_output (tupla) -> TypeError. Scoperto dai test ufficiali Gitea. Test (contro il codice refactor): - pytest tests/ ufficiali: 117 core + 65 gui = 182 passed. - smoke/external_zip/windows_overflow/gui_fixes: OK. - parallel_ping_test (nuovo): 0.50s per 8 server vs ~4.00s sequenziale. - ruff: nessun nuovo problema introdotto (codice nuovo PEP8-compliant). Aggiunti i test ufficiali del repo Gitea + REPORT_NETWORKING_PARALLELO.md.
This commit is contained in:
committed by
steal
parent
b1c8bba9e9
commit
03985a2566
130
tests/parallel_ping_test.py
Normal file
130
tests/parallel_ping_test.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
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
|
||||
|
||||
print(f"\n[OK] parallel networking test passed for package {PKG!r}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user