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.