Skip to content

Commit ca53979

Browse files
author
Threepwood-7
committed
RUST-PAR-017: persist upload waiting-queue entries across disconnects
The waiting queue was bound to the TCP connection: every connection exit released the peer's queue entry, so a disconnected waiter lost its accumulated wait and re-entered at the tail on reconnect. The oracle removes only an ACTIVE slot on disconnect (CUpDownClient::Disconnected removes US_UPLOADING/US_CONNECTING, BaseClient.cpp:1172-1175) and KEEPS a US_ONUPLOADQUEUE waiter (BaseClient.cpp:1229 bDelete = (m_eUploadState != US_ONUPLOADQUEUE)), which then lives up to MAX_PURGEQUEUETIME (1 h, UploadQueue.cpp:223) between re-asks with its wait-start time intact. release_session now detaches a waiting entry (connected = false) instead of deleting it; the existing waiting timeout ages detached waiters out. A returning peer re-attaches through the oracle same-client resolution (CUpDownClient::Compare, DownloadClient.cpp:275: user hash when both sides know one, else IP + advertised TCP port) without resetting queued_at (re-ask lands on the same queue entry, UploadQueue.cpp: 1865-1869), and a located UDP reask now refreshes the waiter's last-request time (SetLastUpRequest, ClientUDPSocket.cpp:307) so a UDP-reasking detached waiter is not purged early. A slot granted to a waiter with no live connection is recorded as a pending promotion for the outbound promote-connect driver (AddUpNextClient US_CONNECTING connect-out, UploadQueue.cpp:327-361), wired to the TCP layer in the next commit; the queue snapshot exposes the new connected flag.
1 parent f2768ed commit ca53979

7 files changed

Lines changed: 369 additions & 14 deletions

File tree

crates/emulebb-ed2k/src/ed2k_transfer.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,11 @@ pub(crate) use piece_store::Ed2kVerifiedRangeReader;
8585
use source_exchange::SourceExchangeState;
8686
use upload_queue::DEFAULT_SOFT_QUEUE_SIZE;
8787
use upload_queue::Ed2kUploadQueueState;
88+
pub(crate) use upload_queue::is_low_id_client_id;
8889
pub(crate) use upload_queue::{
89-
Ed2kUploadFirewallContext, Ed2kUploadPeerIdentity, Ed2kUploadQueueConfig,
90-
Ed2kUploadRangeAdmission, Ed2kUploadSessionHandle, Ed2kUploadSessionStatus,
90+
Ed2kUploadFirewallContext, Ed2kUploadPeerIdentity, Ed2kUploadPendingPromotion,
91+
Ed2kUploadQueueConfig, Ed2kUploadRangeAdmission, Ed2kUploadSessionHandle,
92+
Ed2kUploadSessionStatus,
9193
};
9294
pub use upload_queue::{Ed2kUploadQueueCapacitySnapshot, Ed2kUploadThrottleReservation};
9395
pub use upload_queue::{Ed2kUploadQueueSnapshotEntry, Ed2kUploadSessionPhaseSnapshot};

crates/emulebb-ed2k/src/ed2k_transfer/reask_reciprocity.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ impl Ed2kTransferRuntime {
9595
queue_size,
9696
};
9797

98+
// A located re-ask for the matching file refreshes the waiter's
99+
// last-request time (oracle SetLastUpRequest, ClientUDPSocket.cpp:307):
100+
// a disconnected waiter that keeps re-asking over UDP must not be
101+
// purged by the waiting timeout (MAX_PURGEQUEUETIME).
102+
if req.sender_located && req.file_matches {
103+
self.upload_queue
104+
.lock()
105+
.await
106+
.refresh_waiting_activity_by_udp(sender_ip, sender_udp_port, Instant::now());
107+
}
108+
98109
// Framing comes from the located client; for an unlocated FileNotFound /
99110
// QueueFull we have no hash/crypt context, so reply in the clear.
100111
let framing = ReciprocityReplyFraming {

crates/emulebb-ed2k/src/ed2k_transfer/tests/upload_queue.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,3 +864,199 @@ async fn upload_queue_friend_slot_bypasses_soft_limit_gate() {
864864
Ed2kUploadSessionStatus::Waiting { .. }
865865
));
866866
}
867+
868+
#[tokio::test]
869+
async fn upload_queue_waiter_survives_disconnect_with_wait_time_intact() {
870+
let root = unique_test_dir("ed2k-upload-queue-waiter-survives-disconnect");
871+
let runtime = Ed2kTransferRuntime::load_or_create(&root).unwrap();
872+
runtime.configure_upload_queue(one_slot_config()).await;
873+
let file_hash = Ed2kHash::from_bytes([0x7C; 16]);
874+
// Timeline in the past so the synthetic instants stay comparable to the
875+
// real `Instant::now()` used by snapshot-style entry points.
876+
let t0 = std::time::Instant::now() - std::time::Duration::from_secs(20);
877+
878+
let (_active_handle, active_status) = runtime
879+
.begin_upload_session_at(upload_peer(1, 0x11, 0x0A00_0001), &file_hash, t0)
880+
.await;
881+
assert_eq!(active_status, Ed2kUploadSessionStatus::Granted);
882+
// Older waiter: queued first, so its accumulated wait outranks the later one.
883+
let (older_handle, older_status) = runtime
884+
.begin_upload_session_at(upload_peer(2, 0x22, 0x0A00_0002), &file_hash, t0)
885+
.await;
886+
assert_eq!(older_status, Ed2kUploadSessionStatus::Waiting { rank: 1 });
887+
let (younger_handle, younger_status) = runtime
888+
.begin_upload_session_at(
889+
upload_peer(3, 0x33, 0x0A00_0003),
890+
&file_hash,
891+
t0 + std::time::Duration::from_secs(5),
892+
)
893+
.await;
894+
assert_eq!(younger_status, Ed2kUploadSessionStatus::Waiting { rank: 2 });
895+
896+
// The older waiter's connection drops: its queue entry must survive with
897+
// its wait-start time (master keeps US_ONUPLOADQUEUE clients on disconnect,
898+
// BaseClient.cpp:1229) instead of being erased with the connection.
899+
runtime.release_upload_session(&older_handle).await;
900+
assert_eq!(
901+
runtime.upload_queue_snapshot().await.len(),
902+
3,
903+
"the disconnected waiter must keep its queue entry"
904+
);
905+
// Wait-start intact: the disconnected older waiter still outranks the
906+
// connected younger one (rank derives from the waiting-time score).
907+
assert_eq!(
908+
runtime
909+
.poll_upload_session_at(
910+
&younger_handle,
911+
false,
912+
t0 + std::time::Duration::from_secs(10)
913+
)
914+
.await,
915+
Ed2kUploadSessionStatus::Waiting { rank: 2 },
916+
"a disconnected waiter with more accumulated wait must keep rank 1"
917+
);
918+
}
919+
920+
#[tokio::test]
921+
async fn upload_queue_reask_reattaches_disconnected_waiter_without_wait_reset() {
922+
let root = unique_test_dir("ed2k-upload-queue-reask-reattach");
923+
let runtime = Ed2kTransferRuntime::load_or_create(&root).unwrap();
924+
runtime.configure_upload_queue(one_slot_config()).await;
925+
let file_hash = Ed2kHash::from_bytes([0x7D; 16]);
926+
let t0 = std::time::Instant::now() - std::time::Duration::from_secs(60);
927+
928+
let (_active_handle, active_status) = runtime
929+
.begin_upload_session_at(upload_peer(1, 0x11, 0x0A00_0001), &file_hash, t0)
930+
.await;
931+
assert_eq!(active_status, Ed2kUploadSessionStatus::Granted);
932+
let (older_handle, older_status) = runtime
933+
.begin_upload_session_at(upload_peer(2, 0x22, 0x0A00_0002), &file_hash, t0)
934+
.await;
935+
assert_eq!(older_status, Ed2kUploadSessionStatus::Waiting { rank: 1 });
936+
let (_younger_handle, younger_status) = runtime
937+
.begin_upload_session_at(
938+
upload_peer(3, 0x33, 0x0A00_0003),
939+
&file_hash,
940+
t0 + std::time::Duration::from_secs(10),
941+
)
942+
.await;
943+
assert_eq!(younger_status, Ed2kUploadSessionStatus::Waiting { rank: 2 });
944+
945+
// The older waiter disconnects, then re-asks on a NEW connection with a new
946+
// server-assigned client id (same user hash) — the oracle resolves the same
947+
// client by user hash (CUpDownClient::Compare) and the re-ask lands on the
948+
// persisted entry WITHOUT resetting its wait time (UploadQueue.cpp:1865-1869).
949+
runtime.release_upload_session(&older_handle).await;
950+
let mut returning = upload_peer(2, 0x22, 0x0A00_0002);
951+
returning.client_id = Some(0x0B00_0099);
952+
let (reattached_handle, reattached_status) = runtime
953+
.begin_upload_session_at(
954+
returning,
955+
&file_hash,
956+
t0 + std::time::Duration::from_secs(30),
957+
)
958+
.await;
959+
assert_eq!(
960+
reattached_status,
961+
Ed2kUploadSessionStatus::Waiting { rank: 1 },
962+
"the re-ask must re-attach with the original wait time, not restart at the tail"
963+
);
964+
// The stale pre-disconnect handle no longer owns the session.
965+
assert_eq!(
966+
runtime
967+
.poll_upload_session_at(
968+
&older_handle,
969+
false,
970+
t0 + std::time::Duration::from_secs(31)
971+
)
972+
.await,
973+
Ed2kUploadSessionStatus::Stale
974+
);
975+
assert_eq!(
976+
runtime
977+
.poll_upload_session_at(
978+
&reattached_handle,
979+
false,
980+
t0 + std::time::Duration::from_secs(31)
981+
)
982+
.await,
983+
Ed2kUploadSessionStatus::Waiting { rank: 1 }
984+
);
985+
}
986+
987+
#[tokio::test]
988+
async fn upload_queue_slot_grant_to_disconnected_waiter_queues_outbound_promotion() {
989+
let root = unique_test_dir("ed2k-upload-queue-disconnected-promotion");
990+
let runtime = Ed2kTransferRuntime::load_or_create(&root).unwrap();
991+
runtime.configure_upload_queue(one_slot_config()).await;
992+
let file_hash = Ed2kHash::from_bytes([0x7E; 16]);
993+
let t0 = std::time::Instant::now() - std::time::Duration::from_secs(20);
994+
995+
let (active_handle, active_status) = runtime
996+
.begin_upload_session_at(upload_peer(1, 0x11, 0x0A00_0001), &file_hash, t0)
997+
.await;
998+
assert_eq!(active_status, Ed2kUploadSessionStatus::Granted);
999+
let (waiting_handle, waiting_status) = runtime
1000+
.begin_upload_session_at(upload_peer(2, 0x22, 0x0A00_0002), &file_hash, t0)
1001+
.await;
1002+
assert_eq!(waiting_status, Ed2kUploadSessionStatus::Waiting { rank: 1 });
1003+
1004+
// The waiter disconnects, then the active slot frees: the disconnected
1005+
// waiter is promoted and handed to the outbound promote-connect path
1006+
// (master AddUpNextClient US_CONNECTING connect-out, UploadQueue.cpp:327-361).
1007+
runtime.release_upload_session(&waiting_handle).await;
1008+
runtime.release_upload_session(&active_handle).await;
1009+
1010+
let grants = runtime.take_pending_upload_promotions().await;
1011+
assert_eq!(
1012+
grants.len(),
1013+
1,
1014+
"one outbound promote-connect grant expected"
1015+
);
1016+
let grant = &grants[0];
1017+
assert_eq!(grant.peer.user_hash, Some([0x22; 16]));
1018+
assert_eq!(grant.file_hash, file_hash.to_string());
1019+
assert_eq!(
1020+
runtime.poll_upload_session(&grant.handle, false).await,
1021+
Ed2kUploadSessionStatus::Granted,
1022+
"the grant handle must own the promoted session"
1023+
);
1024+
// Draining is one-shot until another disconnected promotion happens.
1025+
assert!(runtime.take_pending_upload_promotions().await.is_empty());
1026+
1027+
// A failed outbound connect drops the grant entirely (master deletes the
1028+
// client on a failed TryToConnect), freeing the slot for the next waiter.
1029+
runtime.release_upload_session(&grant.handle).await;
1030+
assert!(
1031+
runtime.upload_queue_snapshot().await.is_empty(),
1032+
"a dropped grant must not linger in the queue"
1033+
);
1034+
}
1035+
1036+
#[tokio::test]
1037+
async fn upload_queue_connected_waiter_promotion_needs_no_outbound_connect() {
1038+
let root = unique_test_dir("ed2k-upload-queue-connected-promotion");
1039+
let runtime = Ed2kTransferRuntime::load_or_create(&root).unwrap();
1040+
runtime.configure_upload_queue(one_slot_config()).await;
1041+
let file_hash = Ed2kHash::from_bytes([0x7F; 16]);
1042+
1043+
let (active_handle, active_status) = runtime
1044+
.begin_upload_session(upload_peer(1, 0x11, 0x0A00_0001), &file_hash)
1045+
.await;
1046+
assert_eq!(active_status, Ed2kUploadSessionStatus::Granted);
1047+
let (waiting_handle, waiting_status) = runtime
1048+
.begin_upload_session(upload_peer(2, 0x22, 0x0A00_0002), &file_hash)
1049+
.await;
1050+
assert_eq!(waiting_status, Ed2kUploadSessionStatus::Waiting { rank: 1 });
1051+
1052+
// The waiter still has its live connection when the slot frees: its own
1053+
// session loop observes the grant and sends OP_ACCEPTUPLOADREQ inline
1054+
// (master AddUpNextClient connected branch, UploadQueue.cpp:355-361), so
1055+
// no outbound promote-connect is queued.
1056+
runtime.release_upload_session(&active_handle).await;
1057+
assert_eq!(
1058+
runtime.poll_upload_session(&waiting_handle, true).await,
1059+
Ed2kUploadSessionStatus::Granted
1060+
);
1061+
assert!(runtime.take_pending_upload_promotions().await.is_empty());
1062+
}

crates/emulebb-ed2k/src/ed2k_transfer/tests/upload_queue_firewalled_callback.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ async fn fill_queue_past_threshold(
3333
.begin_upload_session_at(upload_peer(1, 0x01, 0x0A00_0001), file_hash, now)
3434
.await;
3535
assert_eq!(active_status, Ed2kUploadSessionStatus::Granted);
36-
// 51 waiters (> 50) from distinct IPs/hashes (so the per-IP cap never fires).
36+
// 51 waiters (> 50) from distinct IPs/hashes (so the per-IP cap never
37+
// fires, and no waiter shares the granted peer's 0x01 user hash — a shared
38+
// hash now resolves to the SAME client, oracle CUpDownClient::Compare).
3739
for index in 0..51u32 {
3840
let octet = (index % 200) as u8 + 50;
39-
let user_marker = (index % 200) as u8 + 1;
41+
let user_marker = (index % 200) as u8 + 2;
4042
let client_id = 0x0A01_0000 + index;
4143
let (_handle, status) = runtime
4244
.begin_upload_session_at(upload_peer(octet, user_marker, client_id), file_hash, now)

crates/emulebb-ed2k/src/ed2k_transfer/upload.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,14 +268,30 @@ impl Ed2kTransferRuntime {
268268
.reserve_upload_payload(byte_count, now)
269269
}
270270

271-
/// Release one upload slot or waiting entry after disconnect or explicit cancel.
271+
/// Release one upload session after disconnect or explicit cancel: an
272+
/// active slot is freed, while a WAITING entry survives with its wait-start
273+
/// time (master keeps US_ONUPLOADQUEUE clients on disconnect,
274+
/// BaseClient.cpp:1229) and ages out on the waiting timeout.
272275
pub(crate) async fn release_upload_session(&self, handle: &Ed2kUploadSessionHandle) {
273276
self.upload_queue
274277
.lock()
275278
.await
276279
.release_session(handle, Instant::now());
277280
}
278281

282+
/// Drain the granted-but-disconnected waiter promotions that need an
283+
/// outbound connect + OP_ACCEPTUPLOADREQ (master `AddUpNextClient`,
284+
/// UploadQueue.cpp:327-361). Each grant is rebound to a fresh connection id
285+
/// owned by the promote-connect driver.
286+
pub(crate) async fn take_pending_upload_promotions(
287+
&self,
288+
) -> Vec<super::Ed2kUploadPendingPromotion> {
289+
self.upload_queue.lock().await.take_pending_promotions(|| {
290+
self.next_upload_connection_id
291+
.fetch_add(1, Ordering::Relaxed)
292+
})
293+
}
294+
279295
/// Release one queue-visible upload client selected from REST management state.
280296
pub async fn release_upload_client(&self, client_id: &str, waiting_queue: bool) -> bool {
281297
self.upload_queue

0 commit comments

Comments
 (0)