Gate network parsers on a working proxy and remember what worked
The pool now warms up from what the previous run learned instead of starting cold every launch. Startup probes the remembered proxies first, stops as soon as ProxyMinimumLive of them answer, and writes the survivors to proxies.state.json after the warm-up and again on shutdown. Only proxies that ever answered are stored: the feed republishes a few thousand dead addresses every five minutes, and "was dead an hour ago" says almost nothing. Remembered state is a hint, not a verdict. A restored proxy sorts first in the warm-up queue but is not counted live until it answers in this session - otherwise a launch a week later would report live proxies it had never spoken to, the warm-up would skip the very entries it exists to re-check, and the parser gate would open on week-old evidence. That gate is the other half: a parser declaring RequiresNetwork will not run while the pool has nothing live. The Parse page disables the run button and shows a banner that leads to the Proxies page. Parsers that work on pasted text are never gated - they have nothing to route, and blocking them would make the app useless whenever the public lists are down. Two new settings cover the escape hatch and the target: "allow network parsers without a proxy" and how many live proxies to find at startup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
85656e70b0
commit
44fb0d3a5f
@@ -64,6 +64,20 @@ public sealed class ProxyPool : IProxyPool
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int LiveCount
|
||||
{
|
||||
get
|
||||
{
|
||||
var now = _time.GetUtcNow();
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
return _entries.Count(entry => entry.Health == ProxyHealthState.Alive && entry.IsAvailable(now));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? Changed;
|
||||
|
||||
@@ -268,6 +282,115 @@ public sealed class ProxyPool : IProxyPool
|
||||
return aliveCount;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int> WarmUpAsync(
|
||||
int targetLive,
|
||||
IProgress<ProxySweepProgress>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(targetLive, 1);
|
||||
|
||||
var options = Options;
|
||||
var candidates = WarmUpOrder();
|
||||
var total = candidates.Count;
|
||||
|
||||
if (total == 0 || LiveCount >= targetLive)
|
||||
{
|
||||
progress?.Report(new ProxySweepProgress(0, 0, LiveCount));
|
||||
return LiveCount;
|
||||
}
|
||||
|
||||
// Stops the remaining probes the moment the target is met. Linked so the caller's own
|
||||
// cancellation still wins.
|
||||
using var stop = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
using var limiter = new SemaphoreSlim(options.ProbeConcurrency, options.ProbeConcurrency);
|
||||
|
||||
var checkedCount = 0;
|
||||
|
||||
var work = candidates.Select(async entry =>
|
||||
{
|
||||
if (stop.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await limiter.WaitAsync(stop.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _probe.ProbeAsync(entry.Endpoint, options, stop.Token).ConfigureAwait(false);
|
||||
entry.RecordProbe(
|
||||
_time.GetUtcNow(),
|
||||
result.Alive,
|
||||
result.Latency,
|
||||
result.Error,
|
||||
quarantineOnFailure: options.BaseQuarantine
|
||||
);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Either the target was reached or the caller gave up; neither is the proxy's fault.
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
limiter.Release();
|
||||
}
|
||||
|
||||
var live = LiveCount;
|
||||
progress?.Report(new ProxySweepProgress(Interlocked.Increment(ref checkedCount), total, live));
|
||||
|
||||
if (live >= targetLive)
|
||||
{
|
||||
await stop.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
});
|
||||
|
||||
await Task.WhenAll(work).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_strategy.Reset();
|
||||
}
|
||||
|
||||
RaiseChanged();
|
||||
return LiveCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Orders candidates for a warm-up: what worked last time, then what looks most promising.
|
||||
/// </summary>
|
||||
/// <remarks>Public so the ordering can be asserted without running probes.</remarks>
|
||||
public IReadOnlyList<ProxyEntry> WarmUpOrder()
|
||||
{
|
||||
var now = _time.GetUtcNow();
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
return
|
||||
[
|
||||
.. _entries
|
||||
.Where(entry => entry.IsAvailable(now))
|
||||
.OrderByDescending(entry => entry.Health == ProxyHealthState.Alive)
|
||||
.ThenByDescending(entry => entry.WasAliveOnLastRun)
|
||||
.ThenByDescending(entry => entry.SuccessCount > 0)
|
||||
.ThenBy(entry => entry.Latency ?? TimeSpan.MaxValue)
|
||||
.ThenByDescending(entry => entry.SuccessRate)
|
||||
.ThenByDescending(entry => entry.Endpoint.Score),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Records the outcome of a lease. Called by <see cref="ProxyLease"/>.</summary>
|
||||
internal void ReportOutcome(ProxyEntry entry, bool success, TimeSpan? latency, string? error)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user