From feb39ee2dee3d8638040f6c1787d5945a1364a0b Mon Sep 17 00:00:00 2001 From: Alberto Balbo Date: Thu, 6 Aug 2026 18:28:02 +0200 Subject: [PATCH] Togli il tetto alle richieste e l'attesa fra i poll: era il collo di bottiglia Misurato sui registri di tre giorni di prove reali (886 dossier, 4,86 milioni di interrogazioni). Il tetto globale di 40 richieste al secondo veniva spartito fra tutte le aste seguite, fino a 31 insieme: la distribuzione risultava tagliata netta sul limite (p90 39, p99 41, saturo nell'8% dei secondi) e ogni singola asta finiva interrogata ogni ~596 ms invece dei 220 configurati per la finestra critica. Con il timer a un secondo restava un solo sguardo prima della scadenza, e la puntata partiva su una stima della scadenza vecchia di mezzo secondo. Le quattro soglie di cadenza (lontano/medio/vicino/critico) non descrivevano quindi niente di reale: qualunque numero ci si scrivesse, il tetto decideva al posto loro. Adesso non c'e' nessuna attesa fra un poll e il successivo: si riparte appena la risposta e' arrivata, cioe' ~17 interrogazioni al secondo per asta con il ping misurato di 57 ms. Resta la sola cadenza allargata per le aste non ancora cominciate, che non e' una scelta di ritmo: fino all'apertura il server non ha nulla di diverso da dire. Senza tetto fisso serviva pero' qualcosa che reagisse, altrimenti l'unico segnale di "troppe richieste" sarebbe stato un blocco dell'account. Il freno ora lo detta il server: un 429 o un 503 mette in pausa il polling per un secondo, raddoppiando fino a otto se insistono, e la pausa si azzera al primo giro riuscito. Le puntate hanno priorita' Critical e non passano dal freno: se il momento giusto e' adesso, una pausa preventiva costerebbe l'asta. Il tetto resta configurabile per chi lo vuole, con 0 = nessun limite come nuovo predefinito. Scelta dell'utente, dopo avergli mostrato il rischio: niente tetto, il massimo che regge la rete. Co-Authored-By: Claude Opus 5 --- Mimante/Engine/AuctionRunner.cs | 32 +++++++----- Mimante/Modifiche.txt | 8 --- Mimante/Net/BidooHttpClient.cs | 73 ++++++++++++++++++++++++++-- Mimante/Utilities/SettingsManager.cs | 13 ++++- 4 files changed, 101 insertions(+), 25 deletions(-) diff --git a/Mimante/Engine/AuctionRunner.cs b/Mimante/Engine/AuctionRunner.cs index aff5070..9395021 100644 --- a/Mimante/Engine/AuctionRunner.cs +++ b/Mimante/Engine/AuctionRunner.cs @@ -164,22 +164,28 @@ namespace AutoBidder.Engine private int NextPollDelay(AppSettings settings) { // Un'asta non ancora cominciata ha una cadenza tutta sua: vedi NotStartedDelay. + // È l'unico caso rimasto in cui si aspetta, e non è una scelta di ritmo: fino + // all'apertura il server non ha niente di diverso da dire, e interrogarlo + // seicento volte al minuto per tre ore non anticipa l'inizio di un istante. var notStarted = NotStartedDelay(settings); if (notStarted > 0) return notStarted; - var remaining = _auction.EstimatedRemainingMs(); - - if (remaining == double.MaxValue) return settings.PollIntervalFarMs; - - // In sola osservazione non c'è puntata da azzeccare: si risparmiano chiamate. - var critical = _auction.State == RunState.Active - ? settings.PollIntervalCriticalMs - : settings.PollIntervalNearMs; - - if (remaining <= settings.CriticalWindowMs) return critical; - if (remaining <= 10_000) return settings.PollIntervalNearMs; - if (remaining <= 60_000) return settings.PollIntervalMidMs; - return settings.PollIntervalFarMs; + // ── Nessuna attesa: la cadenza la detta la rete ────────────────────── + // + // Prima qui c'erano quattro soglie configurabili (lontano / medio / vicino / + // critico). Sui registri di tre giorni non hanno mai funzionato come scritto: + // con 40 richieste al secondo di tetto globale spartite fra tutte le aste + // seguite, i 220 ms della finestra critica diventavano 596 ms misurati, e con + // il timer a un secondo restava un solo sguardo prima della scadenza. + // + // Il ritmo adesso è quello del giro di andata e ritorno: si riparte appena la + // risposta è arrivata. Con ping ~57 ms misurato sono circa diciassette + // interrogazioni al secondo per asta. + // + // Il freno non è più un numero scelto a tavolino ma la risposta del server: + // vedi la reazione a 429 e 503 in PollLoopAsync. Se Bidoo dice di rallentare, + // si rallenta; finché non lo dice, si va al massimo che regge la rete. + return 0; } /// diff --git a/Mimante/Modifiche.txt b/Mimante/Modifiche.txt index 90c531e..e69de29 100644 --- a/Mimante/Modifiche.txt +++ b/Mimante/Modifiche.txt @@ -1,8 +0,0 @@ -Cose da fare: - - Non caricare i sorgenti del programma nella release. Deve solo essere presente la versione portable e la versione installabile. - - Il nome del setup deve essere AutoBidder_Versione.exe (ad esempio AutoBidder_4.14.0.exe) - - Colora il pulsante stop di rosso (così come quello verde è verde) per quanto riguarda i globali. Colora anche quelli relativi alla singola asta presenti sulla griglia di monitoraggio AutoBidder_Versione - - Verifica tutta l'applicazione e tutto il codice per cercare di ottimizzare l'applicazione per renderla più veloce e meno bisognosa di risorse - - Aggiungi la possibilità di pulire le aste non valide o con non abbastanza dati, come per esempio quelle non seguite completamente dall'inizio alla fine. In generale devo poterle pulire - - Agiungi una impostazione per indicare il costo di una puntata (fisso a 20 centesimi, che corrisponde al piano meno vantaggioso per l'acquisto di puntate dal sito di Bidoo) - \ No newline at end of file diff --git a/Mimante/Net/BidooHttpClient.cs b/Mimante/Net/BidooHttpClient.cs index af9041c..d491477 100644 --- a/Mimante/Net/BidooHttpClient.cs +++ b/Mimante/Net/BidooHttpClient.cs @@ -111,6 +111,53 @@ namespace AutoBidder.Net /// Aggiorna i limiti di ritmo a caldo, senza ricreare il trasporto. public void Configure(double requestsPerSecond) => _limiter.SetRate(requestsPerSecond); + // ── Freno dettato dal server ───────────────────────────────────────── + // + // Senza tetto fisso serve qualcosa che reagisca, altrimenti l'unico segnale di + // "troppe richieste" sarebbe un blocco dell'account. Un 429 o un 503 mettono in + // pausa il polling per un secondo, raddoppiando fino a otto se insistono, e la + // pausa si azzera al primo giro andato bene. + // + // Le puntate hanno priorità Critical e non passano da qui: se il momento giusto + // è adesso, un freno preventivo costerebbe l'asta. + + private const int FrenoMinimoMs = 1000; + private const int FrenoMassimoMs = 8000; + + private long _frenoFinoA; + private int _frenoCorrenteMs = FrenoMinimoMs; + + /// Millisecondi di pausa ancora da scontare. Zero quando si va liberi. + public int FrenoResiduoMs + { + get + { + var mancano = (Interlocked.Read(ref _frenoFinoA) - Stopwatch.GetTimestamp()) + * 1000 / Stopwatch.Frequency; + return mancano > 0 ? (int)mancano : 0; + } + } + + private void Frena() + { + var durata = Volatile.Read(ref _frenoCorrenteMs); + + Interlocked.Exchange(ref _frenoFinoA, + Stopwatch.GetTimestamp() + durata * Stopwatch.Frequency / 1000); + + Volatile.Write(ref _frenoCorrenteMs, Math.Min(FrenoMassimoMs, durata * 2)); + } + + private void Rilascia() => Volatile.Write(ref _frenoCorrenteMs, FrenoMinimoMs); + + private ValueTask RispettaFrenoAsync(CancellationToken ct) + { + var mancano = FrenoResiduoMs; + return mancano <= 0 + ? ValueTask.CompletedTask + : new ValueTask(Task.Delay(mancano, ct)); + } + /// Costruisce una GET già completa di header di autenticazione e di contesto. public HttpRequestMessage BuildGet(string url, string? referer = null, bool ajax = true) { @@ -206,6 +253,7 @@ namespace AutoBidder.Net if (!critical) { + await RispettaFrenoAsync(ct).ConfigureAwait(false); await _limiter.WaitAsync(ct).ConfigureAwait(false); await _concurrency.WaitAsync(ct).ConfigureAwait(false); } @@ -224,6 +272,12 @@ namespace AutoBidder.Net if (!response.IsSuccessStatusCode) Interlocked.Increment(ref _requestsFailed); + // Tolto il tetto fisso, il freno lo dà il server. 429 e 503 sono il modo in + // cui dice "stai chiedendo troppo": si smette per un attimo invece di + // insistere finché non arriva un blocco vero. Le puntate non passano di qui. + if ((int)response.StatusCode is 429 or 503) Frena(); + else if (response.IsSuccessStatusCode) Rilascia(); + return new HttpOutcome { Success = response.IsSuccessStatusCode, @@ -304,9 +358,20 @@ namespace AutoBidder.Net private double _tokens; private long _lastTicks; + /// + /// Zero o negativo = nessun limite: il secchiello si toglie di mezzo del tutto. + /// + /// Serve perché il tetto globale era il vero collo di bottiglia. Misurato sui + /// registri: con 24 aste seguite insieme e 40 richieste al secondo di tetto, la + /// distribuzione delle richieste era tagliata netta sul limite (p90 39, p99 41) e + /// ogni asta finiva interrogata ogni ~596 ms invece dei 220 configurati — proprio + /// negli ultimi secondi, dove quel ritardo costa l'asta. + /// + private bool Illimitato => _ratePerSecond <= 0; + public TokenBucket(double ratePerSecond, int capacity) { - _ratePerSecond = Math.Max(1, ratePerSecond); + _ratePerSecond = ratePerSecond; _capacity = Math.Max(1, capacity); _tokens = _capacity; _lastTicks = Stopwatch.GetTimestamp(); @@ -316,8 +381,8 @@ namespace AutoBidder.Net { lock (_sync) { - _ratePerSecond = Math.Max(1, ratePerSecond); - _capacity = Math.Max(8, _ratePerSecond); + _ratePerSecond = ratePerSecond; + _capacity = Math.Max(8, Math.Abs(_ratePerSecond)); if (_tokens > _capacity) _tokens = _capacity; } } @@ -328,6 +393,8 @@ namespace AutoBidder.Net lock (_sync) { + if (Illimitato) return ValueTask.CompletedTask; + var now = Stopwatch.GetTimestamp(); var elapsedSeconds = (now - _lastTicks) / (double)Stopwatch.Frequency; _lastTicks = now; diff --git a/Mimante/Utilities/SettingsManager.cs b/Mimante/Utilities/SettingsManager.cs index 206ca83..c148212 100644 --- a/Mimante/Utilities/SettingsManager.cs +++ b/Mimante/Utilities/SettingsManager.cs @@ -82,7 +82,18 @@ namespace AutoBidder.Utilities /// Tetto complessivo di richieste al secondo verso Bidoo (le puntate non sono soggette /// al limite: hanno corsia preferenziale). Default: 40. /// - public double MaxRequestsPerSecond { get; set; } = 40; + /// + /// Tetto di richieste al secondo verso Bidoo. 0 = nessun tetto, ed è il + /// predefinito: il ritmo lo detta la rete, e a frenare è il server con 429/503. + /// + /// Il vecchio predefinito era 40, ed era il vero collo di bottiglia. Misurato + /// sui registri di tre giorni: con fino a 31 aste seguite insieme la distribuzione + /// delle richieste era tagliata netta sul tetto (p90 39, p99 41), e ogni asta + /// veniva interrogata ogni ~596 ms invece dei 220 configurati per la finestra + /// critica. Con il timer a un secondo restava un solo sguardo prima della + /// scadenza: da lì le puntate perse e il tempo che non scorreva. + /// + public double MaxRequestsPerSecond { get; set; } = 0; /// /// Porta la granularità del timer di sistema a 1 ms mentre il monitor è in funzione.