Files
bal-electrum-plugin/REPORT_NETWORKING_PARALLELO.md
GenSpark AI Developer 03985a2566 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.
2026-06-28 22:51:28 -04:00

7.3 KiB

Report tecnico — Networking parallelo (anti-freeze Will-Executor)

Destinatario: programmatore esterno / manutentore del plugin Autore: refactoring AI (lavoro su GitHub Bitcoin-after-life/test) Data: 2026-06-15 Branch: feature/networking-parallelo Repository Gitea privato kaibot/bal-plugin-ai: NON modificato (per richiesta esplicita).


1. Problema

Quando il plugin contatta i server Will-Executor (invio transazioni, ping/aggiornamento eredità, download lista), lo fa in sequenza. Se un server non risponde, il thread resta bloccato sui timeout di connessione e, peggio, sui retry:

  • send_request riprovava fino a 10 volte con time.sleep(3) ad ogni timeout → circa 130 secondi per ogni server irraggiungibile, sommati uno dopo l'altro.

Conseguenze:

  • Con pochi server già si avverte; con 20 server diventa ingestibile.
  • L'utente vede "Rimani in attesa — Non risponde" senza capire cosa succede.
  • Un singolo server morto blocca l'intera operazione.

2. Soluzione (in sintesi)

  1. Parallelismo con ThreadPoolExecutor: i server vengono contattati contemporaneamente. Il tempo totale ≈ server più lento, non la somma.
  2. Fast-fail per le operazioni interattive (ping/info/download): niente retry-storm, un solo timeout breve e il server viene marcato "KO".
  3. Feedback live: callback on_each(...) che aggiorna la finestra di attesa server-per-server, in modo thread-safe.
  4. Push transazioni: parallelo, ma mantiene i retry per ogni singolo server (una transazione reale non deve andare persa per un hiccup transitorio).

Perché è thread-safe

Network.send_http_on_proxy() usa asyncio.run_coroutine_threadsafe(coro, loop) e poi coro.result(): ogni chiamata schedula la propria coroutine sullo stesso loop asyncio condiviso di Electrum e blocca solo il proprio worker thread. Più chiamate concorrenti sono quindi sicure → ThreadPoolExecutor dà vero parallelismo.

Gli aggiornamenti UI passano per BalWaitingDialog.update() che emette un pyqtSignal → marshalling automatico sul thread GUI. I callback dai worker thread possono quindi aggiornare il dialog in sicurezza.


3. File modificati (tutto su GitHub Bitcoin-after-life/test, branch feature/networking-parallelo)

3.1 bal/core/willexecutors.py

send_request(...) — aggiunti due parametri keyword-only:

def send_request(method, url, data=None, *, timeout=10, handle_response=None,
                 count_reply=0, max_retries=10, retry_sleep=3):
  • max_retries / retry_sleep controllano i retry sui timeout.
  • Default invariato (10 / 3s) → il push critico mantiene il comportamento storico.
  • Le operazioni interattive passano max_retries=0 → fast-fail.

get_info_task(...) — fast-fail di default:

def get_info_task(url, willexecutor, *, timeout=DEFAULT_TIMEOUT,
                  max_retries=0, retry_sleep=0):
  • Se la risposta non è un dict (timeout/vuoto) → status="KO".

NUOVO ping_servers_parallel(willexecutors, *, on_each=None, max_workers=8, timeout=DEFAULT_TIMEOUT)

  • ThreadPoolExecutor + as_completed; un worker per server (_ping_one).
  • Muta willexecutors in place (come il vecchio ping_servers).
  • Invoca on_each(url, we, ok) man mano che arrivano i risultati.
  • Un worker che esplode non blocca gli altri (try/except difensivo).

NUOVO push_transactions_parallel(willexecutors, *, on_each=None, max_workers=8)

  • Push in parallelo solo verso le voci con chiave "txs".
  • Ogni server mantiene i propri retry (push_transactions_to_willexecutor).
  • on_each(url, we, ok, exc); raccoglie AlreadyPresentException separatamente.
  • Ritorna {url: (ok, exc)}.

DEFAULT_TIMEOUT = 5 (costante a livello di modulo).

3.2 bal/gui/qt/window.py

  • ping_willexecutors_task(self, wes) riscritto su ping_servers_parallel(...) con feedback live (set pinged/failed, get_title() mostra "Ok"/"Ko"/"waiting...").
  • push_transactions_to_willexecutors(self, force=False) riscritto su push_transactions_parallel(...). on_each fa book-keeping + update UI thread-safe; i server "already present" sono raccolti in already_present[] e il loro check_transaction viene eseguito dopo, nel task thread (logica di check originale intatta).
  • fetch_will_executors_list(...) download fast-fail: send_request("get", url, timeout=10, max_retries=1, retry_sleep=1).

3.3 bal/core/util.py — BUGFIX (regressione pre-esistente)

In get_value_amount (riga 324) era stato erroneamente usato Util.in_output(...) (ritorna bool) al posto di Util.din_output(...) (ritorna la tupla (same_amount, same_address)), causando:

TypeError: cannot unpack non-iterable bool object

Corretto ripristinando din_output. Bug scoperto eseguendo i test ufficiali del repo Gitea (tests/test_core_util.py::test_get_value_amount).


4. Verifica (ruff + test ufficiali)

4.1 ruff (lint / PEP8)

  • ruff check sul codice nuovo: nessun nuovo problema introdotto (i F403/F405/F401 presenti derivano dal pattern from .common import * dell'originale; conteggio identico HEAD vs working tree: 121 = 121).
  • Le funzioni parallele nuove rispettano il limite di 88 caratteri (0 E501).
  • ruff check tests/parallel_ping_test.pyAll checks passed.

4.2 Test ufficiali del repo Gitea kaibot/bal-plugin-ai/tests

Eseguiti contro il codice refactorizzato (con le modifiche networking):

Suite Esito
test_core_* (pytest) 117 passed
test_gui_* (pytest) 65 passed
smoke_test.py OK
external_zip_test.py OK
windows_overflow_test.py OK
gui_fixes_test.py OK
parallel_ping_test.py (nuovo) OK — 0.50s per 8 server (sequenziale ~4.00s)

Comandi (come da README):

QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
    python3 -m pytest tests/ -q
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
    python3 tests/smoke_test.py electrum.plugins.bal
QT_QPA_PLATFORM=offscreen PYTHONPATH=<electrum-src> \
    python3 tests/external_zip_test.py bal-electrum-plugin.zip

5. Note di integrazione / rischi

  • Nessuna modifica al protocollo server: solo il come (parallelo) e il quando (retry) delle chiamate cambia, non i payload.
  • Push transazioni: i retry per-server sono mantenuti apposta, per non perdere una transazione reale per un hiccup. Solo il ping/info/download usa fast-fail.
  • max_workers=8 è prudente; con molti server (es. 20) si può alzare, ma 8 worker già abbattono il tempo totale al server più lento.
  • Thread/UI: tutti gli aggiornamenti UI dai worker passano per BalWaitingDialog.update() (pyqtSignal) → safe. Non toccare quel canale.
  • Compatibilità: firme retro-compatibili (i nuovi parametri sono keyword-only con default che preservano il vecchio comportamento).

6. Come provare

  1. Installare lo zip bal-electrum-plugin.zip (Tools → Plugins → install from file).
  2. Configurare più Will-Executor, includendone almeno uno irraggiungibile.
  3. Lanciare invio transazioni / ping: il dialog mostra lo stato server-per-server e non resta più bloccato sul server morto.

SHA-256 dello zip stampato da build_zip.py a fine build (verificare l'integrità).