Skip to content

Commit d517cd9

Browse files
author
Threepwood-7
committed
RUST-PAR-017: dial disconnected waiters for slot grants; rank only on re-ask
There was no way to deliver a slot grant to a waiter without a live connection, so rust compensated by holding every waiting connection open forever (500 ms poll) and pushing unsolicited OP_QUEUERANKING refreshes every 10 s -- packets the oracle never sends. In the oracle, when the queue grants a slot to a client with no connected socket, AddUpNextClient sets US_CONNECTING and opens an OUTBOUND client connection (TryToConnect, UploadQueue.cpp:327-361); once established, the client sends OP_HELLO and pushes OP_ACCEPTUPLOADREQ on that connection (ConnectionEstablished, BaseClient.cpp:1634-1641), and a failed connect deletes the client, dropping the grant so the slot moves to the next waiter. The new ed2k_tcp::upload_promote driver (spawned by the listener) drains the pending promotions recorded by the runtime queue, dials each HighID peer's advertised endpoint -- a LowID grant without a live callback path is dropped like the oracle failed-connect path -- and serves the session through the standard listener dispatch (Ed2kSessionSource:: PromotedUpload seeds the peer identity and pushes HELLO + accept before the packet loop). Queue rank now goes out only in response to a re-ask, matching the oracle SendRankingInfo call sites (UploadQueue.cpp:1866,1963,1986): the 10 s refresh timer and the rank-changed push are removed. An idle WAITING connection is closed after 30 s like the oracle client-socket timeout (CClientReqSocket::CheckTimeOut, thePrefs.GetConnectionTimeout() default 30 s on this branch) while the queue entry survives the close and is dialed back on a grant. Residual divergence: an ACTIVE (granted/uploading) connection is still not idle-closed by rust; the underfill slot recycle handles stalled active slots, so only waiting connections adopt the socket timeout here.
1 parent ca53979 commit d517cd9

8 files changed

Lines changed: 545 additions & 112 deletions

File tree

crates/emulebb-ed2k/src/ed2k_tcp.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ mod identity;
4747
mod listener;
4848
mod obfuscation;
4949
mod transport;
50+
mod upload_promote;
5051
pub(in crate::ed2k_tcp) use codec::{
5152
PeerSourceExchangeRequest, SourceExchangePeer, decode_aich_file_hash_answer,
5253
decode_aich_recovery_answer_payload, decode_aich_recovery_request_payload,
@@ -127,7 +128,7 @@ pub(in crate::ed2k_tcp) use identity::{
127128
};
128129
pub(crate) use listener::reply_with_firewall_udp;
129130
#[cfg(test)]
130-
use listener::{Ed2kConnectionContext, handle_connection};
131+
use listener::{Ed2kConnectionContext, Ed2kSessionSource, handle_connection};
131132
pub use listener::{Ed2kListenerOptions, run_ed2k_listener};
132133
use obfuscation::{
133134
Rc4KeyStream, accept_incoming_obfuscation_handshake, is_plain_ed2k_protocol_marker,
@@ -220,10 +221,12 @@ pub(crate) const MAX_ED2K_PACKET_LEN: usize = 2_000_000;
220221
const MAX_PEER_DECOMPRESSED_PACKET_LEN: usize = 50_000;
221222
const ED2K_CONNECTION_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
222223
const ED2K_UPLOAD_QUEUE_POLL_INTERVAL: Duration = Duration::from_millis(500);
223-
#[cfg(not(test))]
224-
const ED2K_UPLOAD_QUEUE_REFRESH_INTERVAL: Duration = Duration::from_secs(10);
225-
#[cfg(test)]
226-
const ED2K_UPLOAD_QUEUE_REFRESH_INTERVAL: Duration = Duration::from_millis(200);
224+
/// Idle cutoff for a connection whose peer only holds a WAITING queue entry:
225+
/// the oracle closes an idle client socket after the connection timeout
226+
/// (`CClientReqSocket::CheckTimeOut`, `thePrefs.GetConnectionTimeout()` default
227+
/// 30 s on this fork) while the US_ONUPLOADQUEUE entry itself survives up to
228+
/// MAX_PURGEQUEUETIME between re-asks.
229+
const ED2K_WAITING_CONNECTION_IDLE_TIMEOUT: Duration = ED2K_CONNECTION_IDLE_TIMEOUT;
227230
const FIREWALL_HELPER_POST_REQUEST_KEEPALIVE_SECS: u64 = 10;
228231
const ED2K_UPLOAD_PACKET_SPLIT_THRESHOLD: usize = 13_000;
229232
const ED2K_UPLOAD_PACKET_FRAGMENT_LEN: usize = 10_240;

crates/emulebb-ed2k/src/ed2k_tcp/listener/mod.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ use super::{Ed2kHelloIdentity, Ed2kSecureIdent};
2424
mod session;
2525

2626
pub(crate) use session::reply_with_firewall_udp;
27-
#[cfg(test)]
28-
pub(in crate::ed2k_tcp) use session::{Ed2kConnectionContext, handle_connection};
27+
pub(in crate::ed2k_tcp) use session::{
28+
Ed2kConnectionContext, Ed2kSessionSource, handle_connection,
29+
};
2930

3031
/// Inputs for the long-lived ED2K TCP listener task.
3132
pub struct Ed2kListenerOptions {
@@ -65,11 +66,11 @@ pub async fn run_ed2k_listener(options: Ed2kListenerOptions) {
6566
// Resolve the VPN bind interface index once (from the listener's local addr)
6667
// so each accepted socket can egress-pin to the tunnel (IP_UNICAST_IF) without
6768
// a per-connection interface lookup.
68-
let bind_if_index = match listener.local_addr() {
69+
let (bind_ip, bind_if_index) = match listener.local_addr() {
6970
Ok(addr) => match addr.ip() {
7071
std::net::IpAddr::V4(v4) => {
7172
match crate::networking::require_bind_if_index(v4, "eD2K listener") {
72-
Ok(index) => index,
73+
Ok(index) => (v4, index),
7374
Err(error) => {
7475
warn!("eD2K listener disabled: {error:#}");
7576
return;
@@ -86,6 +87,25 @@ pub async fn run_ed2k_listener(options: Ed2kListenerOptions) {
8687
return;
8788
}
8889
};
90+
// Outbound promote-connect driver: hands upload slots to waiters whose
91+
// connection is gone by dialing their advertised endpoint and pushing
92+
// OP_ACCEPTUPLOADREQ (master AddUpNextClient connect-out,
93+
// UploadQueue.cpp:327-361).
94+
tokio::spawn(
95+
super::upload_promote::UploadPromoteDriver {
96+
dht: dht.clone(),
97+
server_state: Arc::clone(&server_state),
98+
kad_firewall: Arc::clone(&kad_firewall),
99+
secure_ident: Arc::clone(&secure_ident),
100+
transfer_runtime: Arc::clone(&transfer_runtime),
101+
hello_identity,
102+
reachability: reachability.clone(),
103+
buddy_registry: buddy_registry.clone(),
104+
bind_ip,
105+
shutdown: Arc::clone(&shutdown),
106+
}
107+
.run(),
108+
);
89109
while !shutdown.load(Ordering::Relaxed) {
90110
match listener.accept().await {
91111
Ok((stream, peer_addr)) => {
@@ -144,7 +164,7 @@ pub async fn run_ed2k_listener(options: Ed2kListenerOptions) {
144164
// inbound slot is released on every exit path (Drop).
145165
let _inbound_guard = inbound_guard;
146166
if let Err(error) = session::handle_connection(
147-
stream,
167+
session::Ed2kSessionSource::Inbound(stream),
148168
peer_addr,
149169
session::Ed2kConnectionContext {
150170
dht: &dht,

crates/emulebb-ed2k/src/ed2k_tcp/listener/session.rs

Lines changed: 134 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use tokio::sync::mpsc;
1818
use crate::{
1919
buddy_socket::BuddySocketRegistry,
2020
ed2k_server::Ed2kServerState,
21-
ed2k_transfer::{Ed2kTransferRuntime, Ed2kUploadPeerIdentity},
21+
ed2k_transfer::{Ed2kTransferRuntime, Ed2kUploadPeerIdentity, Ed2kUploadPendingPromotion},
2222
kad_firewall::KadFirewallState,
2323
};
2424

@@ -36,7 +36,7 @@ use super::super::dump::{
3636
};
3737
use super::super::hello::{
3838
DecodedHelloProfile, build_hello_responses, decode_emule_info_profile, decode_hello_profile,
39-
encode_emule_info_answer,
39+
encode_emule_info_answer, encode_hello_request,
4040
};
4141
use super::super::identity::{Ed2kPeerSecureIdentState, begin_secure_ident_probe};
4242
use super::super::{
@@ -71,6 +71,20 @@ use shared_file::{
7171
use upload_payload::{UploadPayloadOutcome, UploadPayloadRequest, serve_upload_payload};
7272
use upload_queue::{ListenerQueuePoll, ListenerUploadQueue};
7373

74+
/// Transport source for one eD2k peer session: an accepted inbound socket, or
75+
/// an already-established OUTBOUND connection dialed to hand an upload slot to
76+
/// a disconnected waiter (oracle `AddUpNextClient` US_CONNECTING connect-out,
77+
/// UploadQueue.cpp:327-361).
78+
pub(in crate::ed2k_tcp) enum Ed2kSessionSource {
79+
Inbound(TcpStream),
80+
PromotedUpload {
81+
// Boxed: the established transport dwarfs the plain inbound socket
82+
// (clippy::large_enum_variant).
83+
transport: Box<Ed2kTransport>,
84+
grant: Box<Ed2kUploadPendingPromotion>,
85+
},
86+
}
87+
7488
pub(in crate::ed2k_tcp) struct Ed2kConnectionContext<'a> {
7589
pub(in crate::ed2k_tcp) dht: &'a DhtNode,
7690
pub(in crate::ed2k_tcp) server_state: &'a Arc<RwLock<Ed2kServerState>>,
@@ -87,7 +101,7 @@ pub(in crate::ed2k_tcp) struct Ed2kConnectionContext<'a> {
87101

88102
#[allow(clippy::cognitive_complexity)]
89103
pub(in crate::ed2k_tcp) async fn handle_connection(
90-
stream: TcpStream,
104+
source: Ed2kSessionSource,
91105
peer_addr: SocketAddr,
92106
context: Ed2kConnectionContext<'_>,
93107
) -> Result<()> {
@@ -101,15 +115,6 @@ pub(in crate::ed2k_tcp) async fn handle_connection(
101115
reachability,
102116
buddy_registry,
103117
} = context;
104-
let local_addr = stream.local_addr().with_context(|| {
105-
format!("failed to resolve local eD2k listener address for {peer_addr}")
106-
})?;
107-
dump_ed2k_tcp_listener_meta(
108-
peer_addr,
109-
None,
110-
"tcp_accept",
111-
format!("local_addr={local_addr}"),
112-
);
113118
let kad_udp_port = dht
114119
.bind_addr()
115120
.context("failed to resolve Kad bind address for eD2k hello response")?
@@ -125,51 +130,83 @@ pub(in crate::ed2k_tcp) async fn handle_connection(
125130
};
126131
let response_identity =
127132
enrich_hello_identity(response_identity, server_state, kad_firewall).await;
128-
let mut transport = match tokio::time::timeout(
129-
ED2K_CONNECTION_IDLE_TIMEOUT,
130-
Ed2kTransport::accept(stream, hello_identity.user_hash),
131-
)
132-
.await
133-
{
134-
Ok(Ok(transport)) => transport,
135-
Ok(Err(error)) => {
133+
let (mut transport, promoted_grant) = match source {
134+
Ed2kSessionSource::Inbound(stream) => {
135+
let local_addr = stream.local_addr().with_context(|| {
136+
format!("failed to resolve local eD2k listener address for {peer_addr}")
137+
})?;
136138
dump_ed2k_tcp_listener_meta(
137139
peer_addr,
138140
None,
139-
"accept_failed",
140-
format!("local_addr={local_addr} error={error:#}"),
141+
"tcp_accept",
142+
format!("local_addr={local_addr}"),
141143
);
142-
return Err(error).with_context(|| {
143-
format!("failed to accept inbound eD2k peer transport from {peer_addr}")
144-
});
144+
let transport = match tokio::time::timeout(
145+
ED2K_CONNECTION_IDLE_TIMEOUT,
146+
Ed2kTransport::accept(stream, hello_identity.user_hash),
147+
)
148+
.await
149+
{
150+
Ok(Ok(transport)) => transport,
151+
Ok(Err(error)) => {
152+
dump_ed2k_tcp_listener_meta(
153+
peer_addr,
154+
None,
155+
"accept_failed",
156+
format!("local_addr={local_addr} error={error:#}"),
157+
);
158+
return Err(error).with_context(|| {
159+
format!("failed to accept inbound eD2k peer transport from {peer_addr}")
160+
});
161+
}
162+
Err(_) => {
163+
dump_ed2k_tcp_listener_meta(
164+
peer_addr,
165+
None,
166+
"accept_timeout",
167+
format!(
168+
"local_addr={local_addr} idle_timeout_secs={}",
169+
ED2K_CONNECTION_IDLE_TIMEOUT.as_secs()
170+
),
171+
);
172+
anyhow::bail!("timed out waiting for initial eD2k peer bytes");
173+
}
174+
};
175+
(transport, None)
145176
}
146-
Err(_) => {
177+
// A slot grant for a disconnected waiter arrives on a connection WE
178+
// dialed (master AddUpNextClient US_CONNECTING connect-out); the
179+
// transport handshake already happened in the promote driver.
180+
Ed2kSessionSource::PromotedUpload { transport, grant } => {
147181
dump_ed2k_tcp_listener_meta(
148182
peer_addr,
149-
None,
150-
"accept_timeout",
151-
format!(
152-
"local_addr={local_addr} idle_timeout_secs={}",
153-
ED2K_CONNECTION_IDLE_TIMEOUT.as_secs()
154-
),
183+
Some(transport.mode),
184+
"promote_connect",
185+
format!("file_hash={}", grant.file_hash),
155186
);
156-
anyhow::bail!("timed out waiting for initial eD2k peer bytes");
187+
(*transport, Some(*grant))
157188
}
158189
};
190+
let local_addr = transport
191+
.stream
192+
.local_addr()
193+
.with_context(|| format!("failed to resolve local eD2k session address for {peer_addr}"))?;
159194
transport
160195
.stream
161196
.set_nodelay(true)
162-
.with_context(|| format!("failed to enable TCP_NODELAY for inbound peer {peer_addr}"))?;
197+
.with_context(|| format!("failed to enable TCP_NODELAY for peer {peer_addr}"))?;
163198
debug!(
164-
"accepted eD2k TCP peer from {peer_addr} transport={}",
199+
"eD2k TCP peer session with {peer_addr} transport={}",
165200
transport.mode.as_str()
166201
);
167-
dump_ed2k_tcp_listener_meta(
168-
peer_addr,
169-
Some(transport.mode),
170-
"accept",
171-
format!("udp_port={kad_udp_port}"),
172-
);
202+
if promoted_grant.is_none() {
203+
dump_ed2k_tcp_listener_meta(
204+
peer_addr,
205+
Some(transport.mode),
206+
"accept",
207+
format!("udp_port={kad_udp_port}"),
208+
);
209+
}
173210
let mut peer_secure_ident = Ed2kPeerSecureIdentState::default();
174211
let mut requested_file_hash: Option<Ed2kHash> = None;
175212
let mut peer_supports_aich = false;
@@ -192,13 +229,52 @@ pub(in crate::ed2k_tcp) async fn handle_connection(
192229
// a flood of these is a share-probe and the peer is banned (MFC file_request_flood).
193230
let mut failed_file_req_count: u32 = 0;
194231

232+
// A promoted-upload outbound session announces itself and pushes the slot
233+
// grant before entering the dispatch loop: the oracle sends OP_HELLO and
234+
// then OP_ACCEPTUPLOADREQ on the fresh connection it opened for the
235+
// promoted waiter (ConnectionEstablished, BaseClient.cpp:1634-1641).
236+
if let Some(grant) = promoted_grant {
237+
peer_upload_identity = grant.peer.clone();
238+
peer_upload_identity.should_crypt = transport.mode.is_obfuscated();
239+
bound_user_hash = grant.peer.user_hash;
240+
let file_hash = Ed2kHash::from_str(&grant.file_hash)
241+
.with_context(|| format!("invalid promoted upload file hash {}", grant.file_hash))?;
242+
requested_file_hash = Some(file_hash);
243+
let hello_packet = encode_hello_request(response_identity);
244+
dump_ed2k_tcp_listener_send(peer_addr, transport.mode, "hello_request", &hello_packet);
245+
transport.write_all(&hello_packet).await.with_context(|| {
246+
format!("failed to send OP_HELLO to promoted upload peer {peer_addr}")
247+
})?;
248+
let attached = upload_queue
249+
.attach_promoted_grant(
250+
transfer_runtime,
251+
&grant.peer,
252+
grant.handle,
253+
file_hash,
254+
&mut transport,
255+
peer_addr,
256+
)
257+
.await?;
258+
if !attached {
259+
// The grant went stale while connecting (aged out or re-owned by an
260+
// inbound reconnect): nothing to serve on this connection.
261+
return Ok(());
262+
}
263+
}
264+
195265
// Run the session loop inside a fallible async scope so that EVERY exit
196266
// path -- a clean `break`, a propagated `?` I/O error, or any other early
197267
// return from the loop body -- lands in `result` and falls through to the
198-
// unconditional `upload_queue.release(...)` below. The master always frees
199-
// the slot on teardown (`CUpDownClient::Disconnected`, BaseClient.cpp:1172);
200-
// without this wrapper an in-loop `?` would skip the release and leave the
201-
// slot pinned until the idle reaper reclaimed it.
268+
// unconditional `upload_queue.release(...)` below. The master frees an
269+
// ACTIVE slot on teardown (`CUpDownClient::Disconnected` removes
270+
// US_UPLOADING/US_CONNECTING, BaseClient.cpp:1172-1175) while a waiting
271+
// queue entry survives the disconnect (BaseClient.cpp:1229); the runtime
272+
// release applies exactly that split. Without this wrapper an in-loop `?`
273+
// would skip the release and leave the slot pinned until the idle reaper
274+
// reclaimed it.
275+
// Last time the peer actually sent us a packet: feeds the waiting-connection
276+
// idle close (oracle socket timeout) inside poll_on_timeout.
277+
let mut last_packet_at = tokio::time::Instant::now();
202278
let result: Result<()> = async {
203279
loop {
204280
let read_timeout = upload_queue.read_timeout();
@@ -233,7 +309,12 @@ pub(in crate::ed2k_tcp) async fn handle_connection(
233309
Ok(packet) => packet
234310
.with_context(|| format!("failed to read eD2k packet from {peer_addr}"))?,
235311
Err(_) => match upload_queue
236-
.poll_on_timeout(transfer_runtime, &mut transport, peer_addr)
312+
.poll_on_timeout(
313+
transfer_runtime,
314+
&mut transport,
315+
peer_addr,
316+
last_packet_at.elapsed(),
317+
)
237318
.await?
238319
{
239320
ListenerQueuePoll::Continue => continue,
@@ -248,7 +329,12 @@ pub(in crate::ed2k_tcp) async fn handle_connection(
248329
packet.with_context(|| format!("failed to read eD2k packet from {peer_addr}"))?
249330
}
250331
Err(_) => match upload_queue
251-
.poll_on_timeout(transfer_runtime, &mut transport, peer_addr)
332+
.poll_on_timeout(
333+
transfer_runtime,
334+
&mut transport,
335+
peer_addr,
336+
last_packet_at.elapsed(),
337+
)
252338
.await?
253339
{
254340
ListenerQueuePoll::Continue => continue,
@@ -259,6 +345,7 @@ pub(in crate::ed2k_tcp) async fn handle_connection(
259345
let Some(packet) = packet else {
260346
break Ok(());
261347
};
348+
last_packet_at = tokio::time::Instant::now();
262349
dump_ed2k_tcp_listener_recv(peer_addr, transport.mode, "session", &packet);
263350

264351
match (packet.protocol, packet.opcode) {

0 commit comments

Comments
 (0)