fix(lib): let an unanswered upload offer expire

An upload offer counted against the slot cap until the peer accepted
it, and a peer that never answered, having gone away or hung, held that
slot for the life of the process; with two slots by default, two such
peers shut uploads down until a restart. An offer nobody answers within
thirty seconds is now dropped, recorded as a failed upload so the
operator sees why, and the slot goes to the next in line.
This commit is contained in:
Michel de Graaf
2026-09-07 23:30:33 +02:00
parent 2d774c779f
commit 431080699d
4 changed files with 150 additions and 4 deletions
+13 -2
View File
@@ -23,6 +23,7 @@ impl Client {
if last_sweep.elapsed() >= CONNECT_SWEEP_INTERVAL {
last_sweep = Instant::now();
Self::sweep_expired_connects(&client_context);
Self::sweep_stale_offers(&client_context);
}
let operation = match next {
Ok(operation) => operation,
@@ -718,9 +719,10 @@ impl Client {
// address (from the code-9 GetPeerAddress) and
// stream the file, or queue until it resolves.
let (job_addr, downloader) = match client_context
.read_safe()
.write_safe()
{
Ok(ctx) => {
Ok(mut ctx) => {
ctx.mark_offer_answered(token);
let Some(job) = ctx.uploads.get(&token) else {
continue;
};
@@ -875,6 +877,15 @@ impl Client {
}
}
fn sweep_stale_offers(client_context: &Arc<RwLock<ClientContext>>) {
let freed = client_context
.write_safe()
.is_ok_and(|mut ctx| ctx.expire_stale_offers(Instant::now()));
if freed {
Self::pump_upload_queue(client_context);
}
}
fn sweep_expired_connects(client_context: &Arc<RwLock<ClientContext>>) {
let expired = match client_context.write_safe() {
Ok(mut ctx) => ctx.take_expired_connects(Instant::now()),