Skip to content

Commit 90e87f7

Browse files
author
Threepwood-7
committed
RUST-BUG-085: apply SX2 answer cooldown
1 parent c72d82c commit 90e87f7

6 files changed

Lines changed: 214 additions & 32 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,10 @@ async fn remember_source_exchange_sources(
11041104
return Ok(());
11051105
}
11061106

1107+
transfer_runtime
1108+
.note_source_exchange_answer(file_hash_hex, std::time::Instant::now())
1109+
.await;
1110+
11071111
for source in sources {
11081112
if source.tcp_port == 0 || source.ip == [0, 0, 0, 0] {
11091113
continue;

crates/emulebb-ed2k/src/ed2k_transfer.rs

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ mod reask_reciprocity;
5050
mod requested_block;
5151
mod salvage;
5252
mod shared_catalog;
53+
mod source_exchange;
5354
mod store;
5455
mod transfer_sql;
5556
mod upload;
@@ -76,6 +77,7 @@ pub use model::{
7677
Ed2kCallbackIntent, Ed2kLocalIngestSummary, Ed2kPieceState, Ed2kResumeManifest, Ed2kSourceHint,
7778
Ed2kTransferJob, Ed2kTransferState,
7879
};
80+
use source_exchange::SourceExchangeState;
7981
use upload_queue::DEFAULT_SOFT_QUEUE_SIZE;
8082
use upload_queue::Ed2kUploadQueueState;
8183
pub(crate) use upload_queue::{
@@ -140,16 +142,6 @@ pub fn ed2k_part_count(file_size: u64) -> u16 {
140142
/// Canonical eMule upload block size used inside one ED2K part request.
141143
pub(crate) const ED2K_EMBLOCK_SIZE: u64 = 184_320;
142144
const PAYLOAD_FILE_NAME: &str = "pieces.bin";
143-
const SOURCE_EXCHANGE_REASK_INTERVAL: Duration = Duration::from_secs(40 * 60);
144-
const SOURCE_EXCHANGE_COMMON_REASK_INTERVAL: Duration = Duration::from_secs(160 * 60);
145-
const SOURCE_EXCHANGE_RARE_FILE: usize = 50;
146-
147-
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
148-
struct SourceExchangeRequestKey {
149-
file_hash: String,
150-
peer_addr: SocketAddr,
151-
user_hash: Option<[u8; 16]>,
152-
}
153145

154146
/// Runtime owner for ED2K transfer manifests, piece-store payloads, and the
155147
/// transfer-backed shared catalog.
@@ -162,7 +154,7 @@ pub struct Ed2kTransferRuntime {
162154
manifest_io: Arc<Mutex<()>>,
163155
manifest_cache: Arc<Mutex<HashMap<String, Ed2kResumeManifest>>>,
164156
manifest_checkpoint_state: Arc<Mutex<HashMap<String, Ed2kManifestCheckpointState>>>,
165-
source_exchange_requests: Arc<Mutex<HashMap<SourceExchangeRequestKey, Instant>>>,
157+
source_exchange: SourceExchangeState,
166158
/// Per-file accumulator of network-proposed AICH roots and their distinct
167159
/// proposing IPs. A network-learned root is only promoted to the
168160
/// salvage-authorizing `manifest.aich_root` once it clears the master's
@@ -295,7 +287,7 @@ impl Ed2kTransferRuntime {
295287
manifest_io: Arc::new(Mutex::new(())),
296288
manifest_cache: Arc::new(Mutex::new(HashMap::new())),
297289
manifest_checkpoint_state: Arc::new(Mutex::new(HashMap::new())),
298-
source_exchange_requests: Arc::new(Mutex::new(HashMap::new())),
290+
source_exchange: SourceExchangeState::default(),
299291
aich_root_corroboration: Arc::new(StdMutex::new(HashMap::new())),
300292
download_activity: Arc::new(StdMutex::new(HashMap::new())),
301293
download_sources: Arc::new(StdMutex::new(HashMap::new())),
@@ -512,28 +504,13 @@ impl Ed2kTransferRuntime {
512504
if !self.can_engage_file_source(current_source_count) {
513505
return false;
514506
}
515-
let reask_interval = source_exchange_reask_interval(current_source_count);
516-
let key = SourceExchangeRequestKey {
517-
file_hash: file_hash.to_string(),
518-
peer_addr,
519-
user_hash,
520-
};
521-
let mut requests = self.source_exchange_requests.lock().await;
522-
let allowed = requests
523-
.get(&key)
524-
.is_none_or(|last_requested| now.duration_since(*last_requested) > reask_interval);
525-
if allowed {
526-
requests.insert(key, now);
527-
}
528-
allowed
507+
self.source_exchange
508+
.should_request(file_hash, peer_addr, user_hash, current_source_count, now)
509+
.await
529510
}
530-
}
531511

532-
fn source_exchange_reask_interval(current_source_count: usize) -> Duration {
533-
if current_source_count > SOURCE_EXCHANGE_RARE_FILE {
534-
SOURCE_EXCHANGE_COMMON_REASK_INTERVAL
535-
} else {
536-
SOURCE_EXCHANGE_REASK_INTERVAL
512+
pub(crate) async fn note_source_exchange_answer(&self, file_hash: &str, now: Instant) {
513+
self.source_exchange.note_answer(file_hash, now).await;
537514
}
538515
}
539516

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use std::{
2+
collections::HashMap,
3+
net::SocketAddr,
4+
sync::Arc,
5+
time::{Duration, Instant},
6+
};
7+
8+
use tokio::sync::Mutex;
9+
10+
const SOURCE_EXCHANGE_REASK_INTERVAL: Duration = Duration::from_secs(40 * 60);
11+
const SOURCE_EXCHANGE_COMMON_REASK_INTERVAL: Duration = Duration::from_secs(160 * 60);
12+
const SOURCE_EXCHANGE_FILE_ANSWER_INTERVAL: Duration = Duration::from_secs(5 * 60);
13+
const SOURCE_EXCHANGE_COMMON_FILE_ANSWER_INTERVAL: Duration = Duration::from_secs(20 * 60);
14+
const SOURCE_EXCHANGE_RARE_FILE: usize = 50;
15+
const SOURCE_EXCHANGE_VERY_RARE_FILE: usize = SOURCE_EXCHANGE_RARE_FILE / 5;
16+
17+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18+
struct SourceExchangeRequestKey {
19+
file_hash: String,
20+
peer_addr: SocketAddr,
21+
user_hash: Option<[u8; 16]>,
22+
}
23+
24+
#[derive(Debug, Default)]
25+
pub(super) struct SourceExchangeState {
26+
requests: Arc<Mutex<HashMap<SourceExchangeRequestKey, Instant>>>,
27+
file_answers: Arc<Mutex<HashMap<String, Instant>>>,
28+
}
29+
30+
impl SourceExchangeState {
31+
pub(super) async fn should_request(
32+
&self,
33+
file_hash: &str,
34+
peer_addr: SocketAddr,
35+
user_hash: Option<[u8; 16]>,
36+
current_source_count: usize,
37+
now: Instant,
38+
) -> bool {
39+
let file_answer_interval = file_answer_interval(current_source_count);
40+
if !file_answer_interval.is_zero() {
41+
let answers = self.file_answers.lock().await;
42+
if answers.get(file_hash).is_some_and(|last_answered| {
43+
now.duration_since(*last_answered) <= file_answer_interval
44+
}) {
45+
return false;
46+
}
47+
}
48+
49+
let reask_interval = reask_interval(current_source_count);
50+
let key = SourceExchangeRequestKey {
51+
file_hash: file_hash.to_string(),
52+
peer_addr,
53+
user_hash,
54+
};
55+
let mut requests = self.requests.lock().await;
56+
let allowed = requests
57+
.get(&key)
58+
.is_none_or(|last_requested| now.duration_since(*last_requested) > reask_interval);
59+
if allowed {
60+
requests.insert(key, now);
61+
}
62+
allowed
63+
}
64+
65+
pub(super) async fn note_answer(&self, file_hash: &str, now: Instant) {
66+
self.file_answers
67+
.lock()
68+
.await
69+
.insert(file_hash.to_string(), now);
70+
}
71+
}
72+
73+
fn reask_interval(current_source_count: usize) -> Duration {
74+
if current_source_count > SOURCE_EXCHANGE_RARE_FILE {
75+
SOURCE_EXCHANGE_COMMON_REASK_INTERVAL
76+
} else {
77+
SOURCE_EXCHANGE_REASK_INTERVAL
78+
}
79+
}
80+
81+
fn file_answer_interval(current_source_count: usize) -> Duration {
82+
if current_source_count <= SOURCE_EXCHANGE_VERY_RARE_FILE {
83+
Duration::ZERO
84+
} else if current_source_count <= SOURCE_EXCHANGE_RARE_FILE {
85+
SOURCE_EXCHANGE_FILE_ANSWER_INTERVAL
86+
} else {
87+
SOURCE_EXCHANGE_COMMON_FILE_ANSWER_INTERVAL
88+
}
89+
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,67 @@ async fn source_exchange_respects_soft_source_cap() {
127127
"a cap-denied request must not consume the peer throttle"
128128
);
129129
}
130+
131+
#[tokio::test]
132+
async fn source_exchange_answer_cools_down_rare_and_common_files() {
133+
let root = unique_test_dir("ed2k-transfer-source-exchange-answer-cooldown");
134+
let runtime = Ed2kTransferRuntime::load_or_create(&root).unwrap();
135+
let now = Instant::now();
136+
let peer_addr = SocketAddr::from((Ipv4Addr::new(10, 1, 2, 6), 4662));
137+
138+
runtime.note_source_exchange_answer("very_rare", now).await;
139+
assert!(
140+
runtime
141+
.should_request_source_exchange("very_rare", peer_addr, None, 10, now)
142+
.await,
143+
"very rare files do not use the file-level answer cooldown"
144+
);
145+
146+
runtime.note_source_exchange_answer("rare", now).await;
147+
assert!(
148+
!runtime
149+
.should_request_source_exchange(
150+
"rare",
151+
peer_addr,
152+
None,
153+
11,
154+
now + Duration::from_secs(60)
155+
)
156+
.await
157+
);
158+
assert!(
159+
runtime
160+
.should_request_source_exchange(
161+
"rare",
162+
peer_addr,
163+
None,
164+
11,
165+
now + Duration::from_secs(5 * 60 + 1),
166+
)
167+
.await
168+
);
169+
170+
runtime.note_source_exchange_answer("common", now).await;
171+
assert!(
172+
!runtime
173+
.should_request_source_exchange(
174+
"common",
175+
peer_addr,
176+
None,
177+
51,
178+
now + Duration::from_secs(5 * 60 + 1),
179+
)
180+
.await
181+
);
182+
assert!(
183+
runtime
184+
.should_request_source_exchange(
185+
"common",
186+
peer_addr,
187+
None,
188+
51,
189+
now + Duration::from_secs(20 * 60 + 1),
190+
)
191+
.await
192+
);
193+
}

docs/active/INDEX.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ items.
9696
| [RUST-BUG-082](items/RUST-BUG-082.md) | Minor | DONE | Report SX2 answer source counts in live-wire evidence |
9797
| [RUST-BUG-083](items/RUST-BUG-083.md) | Major | DONE | Gate SX2 requests on the MFC soft source cap |
9898
| [RUST-BUG-084](items/RUST-BUG-084.md) | Major | DONE | Use MFC common-file SX2 reask timing |
99+
| [RUST-BUG-085](items/RUST-BUG-085.md) | Major | DONE | Apply MFC SX2 answer cooldown per file |
99100

100101
## Refactors (`REF`)
101102

docs/active/items/RUST-BUG-085.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
id: RUST-BUG-085
3+
title: Apply MFC SX2 answer cooldown per file
4+
status: done
5+
priority: Major
6+
category: bug
7+
workflow: local
8+
---
9+
10+
# RUST-BUG-085: Apply MFC SX2 answer cooldown per file
11+
12+
## Problem
13+
14+
Rust decoded `OP_ANSWERSOURCES2` and remembered any sources it carried, but it
15+
did not update a per-file answer timestamp. eMuleBB MFC calls
16+
`CPartFile::SetLastAnsweredTime()` as soon as a matching SX2 answer arrives,
17+
before adding sources, so empty answers still suppress subsequent SX2 requests
18+
for that file.
19+
20+
That leaves a selected-surface drift after `RUST-BUG-084`: Rust had the peer
21+
reask timing, but not the file-level answer cooldown that MFC applies to rare
22+
and common files.
23+
24+
## Acceptance
25+
26+
- [x] A matching `OP_ANSWERSOURCES2` records the file's SX2 answer time even when
27+
the answer contains zero sources.
28+
- [x] Very rare files stay exempt from the file-level answer cooldown.
29+
- [x] Rare files apply the MFC 5-minute file answer cooldown.
30+
- [x] Common files apply the MFC 20-minute file answer cooldown.
31+
- [x] Mismatched SX2 answer hashes are still ignored.
32+
33+
## Implementation Notes
34+
35+
- Added in-memory per-file SX2 answer timestamps to `Ed2kTransferRuntime`.
36+
- The SX2 request gate now checks the MFC rarity class before the peer/file
37+
throttle map is updated.
38+
- The download session records the answer timestamp after the answer hash
39+
matches the requested file and before source ingestion.
40+
41+
## Evidence
42+
43+
- MFC comparison:
44+
`srchybrid/DownloadClient.cpp` `CUpDownClient::IsSourceRequestAllowed`,
45+
`ListenSocket.cpp` `OP_ANSWERSOURCES2` handling, and `PartFile.h`
46+
`SetLastAnsweredTime`.
47+
- `cargo test -p emulebb-ed2k source_exchange -- --nocapture`

0 commit comments

Comments
 (0)