mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-09 17:25:42 +00:00
fix(network): restore native async resolver for route-aware transfers (#38)
- Use NativeAsyncResolver as primary for queue transfers and magnet probes when route contract is available - Fall back to standard system resolver options on stock Aria2 builds without custom flags - Guarantee minimum 5s cleanup reserve budget to prevent zero-duration probe cleanup timeouts - Prevent Aria2 single-threaded main loop freezing under TUN proxies (ref #38)
This commit is contained in:
+20
-16
@@ -9200,8 +9200,8 @@ async fn remove_magnet_metadata_probe_dir(path: &std::path::Path) -> Result<(),
|
||||
}
|
||||
|
||||
const MAGNET_METADATA_TOTAL_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const MAGNET_SYSTEM_RESOLVER_ATTEMPT: Duration = Duration::from_secs(20);
|
||||
const MAGNET_ALTERNATE_RESOLVER_ATTEMPT: Duration = Duration::from_secs(40);
|
||||
const MAGNET_PRIMARY_RESOLVER_ATTEMPT: Duration = Duration::from_secs(35);
|
||||
const MAGNET_FALLBACK_RESOLVER_ATTEMPT: Duration = Duration::from_secs(25);
|
||||
const MAGNET_PROBE_CLEANUP_RESERVE: Duration = Duration::from_secs(5);
|
||||
|
||||
fn magnet_probe_schedule(total_timeout: Duration) -> crate::torrent_probe::MetadataProbeSchedule {
|
||||
@@ -9310,18 +9310,18 @@ async fn resolve_magnet_metadata(
|
||||
let route_contract_available = app_handle
|
||||
.state::<Aria2DaemonGuard>()
|
||||
.route_contract_available();
|
||||
let mut system_options = base_options.clone();
|
||||
crate::network::apply_aria2_system_resolver_for_daemon(
|
||||
&mut system_options,
|
||||
let mut primary_options = base_options.clone();
|
||||
crate::network::apply_aria2_transfer_resolver(
|
||||
&mut primary_options,
|
||||
route_contract_available,
|
||||
);
|
||||
let first_result = crate::torrent_probe::run_bounded_metadata_probe(
|
||||
client,
|
||||
&sanitized_source,
|
||||
system_options,
|
||||
primary_options,
|
||||
&metadata_path,
|
||||
"system",
|
||||
magnet_probe_schedule(MAGNET_SYSTEM_RESOLVER_ATTEMPT),
|
||||
if route_contract_available { "route" } else { "stock" },
|
||||
magnet_probe_schedule(MAGNET_PRIMARY_RESOLVER_ATTEMPT),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -9333,7 +9333,9 @@ async fn resolve_magnet_metadata(
|
||||
.err()
|
||||
.is_some_and(crate::torrent_probe::allows_resolver_fallback) =>
|
||||
{
|
||||
let cleanup_budget = operation_deadline.saturating_duration_since(Instant::now());
|
||||
let cleanup_budget = operation_deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.max(MAGNET_PROBE_CLEANUP_RESERVE);
|
||||
match tokio::time::timeout(cleanup_budget, remove_magnet_metadata_probe_dir(&probe_dir))
|
||||
.await
|
||||
{
|
||||
@@ -9350,7 +9352,9 @@ async fn resolve_magnet_metadata(
|
||||
);
|
||||
}
|
||||
}
|
||||
let create_budget = operation_deadline.saturating_duration_since(Instant::now());
|
||||
let create_budget = operation_deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.max(MAGNET_PROBE_CLEANUP_RESERVE);
|
||||
match tokio::time::timeout(create_budget, tokio::fs::create_dir_all(&probe_dir)).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(error)) => {
|
||||
@@ -9365,10 +9369,10 @@ async fn resolve_magnet_metadata(
|
||||
);
|
||||
}
|
||||
}
|
||||
let alternate_budget = MAGNET_ALTERNATE_RESOLVER_ATTEMPT
|
||||
let fallback_budget = MAGNET_FALLBACK_RESOLVER_ATTEMPT
|
||||
.min(operation_deadline.saturating_duration_since(Instant::now()));
|
||||
let mut alternate_options = base_options;
|
||||
crate::network::apply_aria2_route_contract(&mut alternate_options);
|
||||
let mut fallback_options = base_options;
|
||||
crate::network::apply_aria2_system_resolver_for_daemon(&mut fallback_options, true);
|
||||
crate::torrent_probe::run_bounded_metadata_probe(
|
||||
std::sync::Arc::new(Aria2RpcClient {
|
||||
port: state
|
||||
@@ -9377,10 +9381,10 @@ async fn resolve_magnet_metadata(
|
||||
secret: state.aria2_secret.clone(),
|
||||
}),
|
||||
&sanitized_source,
|
||||
alternate_options,
|
||||
fallback_options,
|
||||
&metadata_path,
|
||||
"alternate",
|
||||
magnet_probe_schedule(alternate_budget),
|
||||
"system",
|
||||
magnet_probe_schedule(fallback_budget),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -66,6 +66,31 @@ pub(crate) fn apply_aria2_route_contract(
|
||||
);
|
||||
}
|
||||
|
||||
/// Select the appropriate resolver options for a transfer based on whether the
|
||||
/// running Aria2 daemon advertises the Firelink route contract.
|
||||
///
|
||||
/// When the Firelink route contract is available, transfers MUST use the
|
||||
/// native-async resolver (`dns-resolver=native-async`, `async-dns=true`,
|
||||
/// `network-target-policy=firelink-v1`). This executes the OS resolver
|
||||
/// (`getaddrinfo`) on asynchronous worker threads, fully honoring TUN, VPN,
|
||||
/// and proxy routes (e.g. Shadowrocket, Happ, Sing-box, V2RayN) without
|
||||
/// blocking Aria2's single-threaded event loop or its RPC server.
|
||||
///
|
||||
/// When the route contract is unavailable (stock Aria2 builds), Firelink falls
|
||||
/// back to standard system resolver options (`async-dns=false`) without
|
||||
/// passing custom options that would be rejected by stock binaries.
|
||||
pub(crate) fn apply_aria2_transfer_resolver(
|
||||
options: &mut serde_json::Map<String, serde_json::Value>,
|
||||
route_contract_available: bool,
|
||||
) {
|
||||
if route_contract_available {
|
||||
apply_aria2_route_contract(options);
|
||||
} else {
|
||||
apply_aria2_system_resolver_for_daemon(options, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn has_string_capability(version: &serde_json::Value, field: &str, expected: &str) -> bool {
|
||||
version
|
||||
.get(field)
|
||||
@@ -494,6 +519,33 @@ mod tests {
|
||||
assert_eq!(patched_options.get("async-dns"), Some(&serde_json::json!("false")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aria2_transfer_resolver_selects_route_contract_when_available_and_stock_otherwise() {
|
||||
let mut patched_options = serde_json::Map::new();
|
||||
apply_aria2_transfer_resolver(&mut patched_options, true);
|
||||
assert_eq!(
|
||||
patched_options.get("async-dns"),
|
||||
Some(&serde_json::json!("true"))
|
||||
);
|
||||
assert_eq!(
|
||||
patched_options.get("dns-resolver"),
|
||||
Some(&serde_json::json!(ARIA2_DNS_RESOLVER))
|
||||
);
|
||||
assert_eq!(
|
||||
patched_options.get("network-target-policy"),
|
||||
Some(&serde_json::json!(ARIA2_NETWORK_TARGET_POLICY))
|
||||
);
|
||||
|
||||
let mut stock_options = serde_json::Map::new();
|
||||
apply_aria2_transfer_resolver(&mut stock_options, false);
|
||||
assert_eq!(
|
||||
stock_options.get("async-dns"),
|
||||
Some(&serde_json::json!("false"))
|
||||
);
|
||||
assert!(!stock_options.contains_key("dns-resolver"));
|
||||
assert!(!stock_options.contains_key("network-target-policy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_aria2_baseline_response_is_accepted_without_firelink_fields() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -8342,15 +8342,17 @@ impl SidecarSpawner for ProductionSpawner {
|
||||
let attempt_epoch = state.queue_manager.current_aria2_control_epoch(id).await;
|
||||
let admission_started = Instant::now();
|
||||
let mut options = serde_json::Map::new();
|
||||
// Keep hostname resolution on the system/TUN route for every normal
|
||||
// and Torrent transfer. The optional Firelink resolver is reserved for
|
||||
// an explicit magnet metadata fallback and is never forced onto stock
|
||||
// Aria2 builds.
|
||||
// Keep hostname resolution route-aware and non-blocking for every
|
||||
// normal and Torrent transfer. When the Firelink route contract is
|
||||
// available, transfers use NativeAsyncResolver on background worker
|
||||
// threads to honor TUN, VPN, and proxy routes without blocking Aria2's
|
||||
// single-threaded event loop. Stock Aria2 builds fall back to clean
|
||||
// standard resolver options without unsupported flags.
|
||||
let route_contract_available = self
|
||||
.app_handle
|
||||
.state::<crate::Aria2DaemonGuard>()
|
||||
.route_contract_available();
|
||||
crate::network::apply_aria2_system_resolver_for_daemon(
|
||||
crate::network::apply_aria2_transfer_resolver(
|
||||
&mut options,
|
||||
route_contract_available,
|
||||
);
|
||||
|
||||
@@ -250,8 +250,11 @@ pub(crate) async fn run_metadata_probe_with_deadlines<C: RpcClient + 'static>(
|
||||
}
|
||||
.await;
|
||||
|
||||
let cleanup_budget = cleanup_deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.max(Duration::from_secs(5));
|
||||
let cleanup_result = match tokio::time::timeout(
|
||||
cleanup_deadline.saturating_duration_since(Instant::now()),
|
||||
cleanup_budget,
|
||||
cleanup_metadata_probe(client.as_ref(), &gid),
|
||||
)
|
||||
.await
|
||||
@@ -497,9 +500,12 @@ impl<C: RpcClient + 'static> ProbeCleanupGuard<C> {
|
||||
}
|
||||
|
||||
let mut first_error = None;
|
||||
let cleanup_budget = deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.max(Duration::from_secs(5));
|
||||
for gid in self.gids.clone() {
|
||||
match tokio::time::timeout(
|
||||
deadline.saturating_duration_since(Instant::now()),
|
||||
cleanup_budget,
|
||||
cleanup_metadata_probe(self.client.as_ref(), &gid),
|
||||
)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user