add configurable RRDP snapshot and delta timeouts

This commit is contained in:
2026-09-29 16:39:17 +08:00
parent e3a1d0a6ff
commit 9c056aac64
12 changed files with 225 additions and 36 deletions

View File

@@ -8,6 +8,11 @@ ccr-out = "/output/result.ccr"
run-timeout-secs = 1800 run-timeout-secs = 1800
require-complete = true require-complete = true
ta-refresh-policy = "allow-valid-cache" ta-refresh-policy = "allow-valid-cache"
# Notification/TA requests use http-timeout; RRDP data bodies have independent
# per-resource limits and are selected automatically within the same run.
http-timeout-secs = 10
rrdp-snapshot-timeout-secs = 600
rrdp-delta-timeout-secs = 180
# Set false to ignore all constraints files, including adjacent auto-discovery. # Set false to ignore all constraints files, including adjacent auto-discovery.
ta-constraints-enabled = true ta-constraints-enabled = true
log-level = "info" log-level = "info"

View File

@@ -64,7 +64,9 @@ The remaining common keys and defaults are:
| Key | Default | | Key | Default |
| --- | ---: | | --- | ---: |
| `max-ca-depth` | 64 | | `max-ca-depth` | 64 |
| `http-timeout-secs` | 300 (0 is normalized to 1) | | `http-timeout-secs` | 10 (0 is normalized to 1) |
| `rrdp-snapshot-timeout-secs` | 600 (0 is normalized to 1) |
| `rrdp-delta-timeout-secs` | 180 (0 is normalized to 1) |
| `http-root-cert` | `[]` | | `http-root-cert` | `[]` |
| `parallel-max-repo-sync-workers-global` | 8 | | `parallel-max-repo-sync-workers-global` | 8 |
| `parallel-phase2-object-workers` | 8 | | `parallel-phase2-object-workers` | 8 |

View File

@@ -55,7 +55,9 @@ those choices do not change the built-in defaults.
| `ta-constraints-enabled` | Boolean | true | Apply explicit or adjacent TA constraints files. False skips all constraints loading and enforcement, including adjacent discovery; unused missing or malformed files are ignored. | | `ta-constraints-enabled` | Boolean | true | Apply explicit or adjacent TA constraints files. False skips all constraints loading and enforcement, including adjacent discovery; unused missing or malformed files are ignored. |
| `ta-refresh-policy` | String | `allow-valid-cache` | On automatic TA download failure, permit a revalidated, usable cached certificate. `require-fresh` prohibits fallback. Does not replace fixed TA files. | | `ta-refresh-policy` | String | `allow-valid-cache` | On automatic TA download failure, permit a revalidated, usable cached certificate. `require-fresh` prohibits fallback. Does not replace fixed TA files. |
| `max-ca-depth` | Nonnegative integer | 64 | Maximum CA validation-tree depth. Zero is accepted by configuration parsing, but is a depth limit, not unlimited traversal. Usually keep 64. | | `max-ca-depth` | Nonnegative integer | 64 | Maximum CA validation-tree depth. Zero is accepted by configuration parsing, but is a depth limit, not unlimited traversal. Usually keep 64. |
| `http-timeout-secs` | Nonnegative integer, seconds | 300 | HTTP fetch timeout. Zero is normalized to one second, not unlimited. Does not set the whole-run or rsync timeout. | | `http-timeout-secs` | Nonnegative integer, seconds | 10 | Timeout for RRDP notification, TA/TAL and other small HTTP requests. Snapshot/delta bodies use their resource-specific settings below. Zero is normalized to one second, not unlimited. Does not set the whole-run or rsync timeout. |
| `rrdp-snapshot-timeout-secs` | Nonnegative integer, seconds | 600 | Timeout for downloading an RRDP snapshot response body. Zero is normalized to one second. Selected by the RRDP resource type, including after same-origin redirects. |
| `rrdp-delta-timeout-secs` | Nonnegative integer, seconds | 180 | Timeout for downloading an RRDP delta response body. Zero is normalized to one second. Each delta resource uses this value; it is independent of the validation run's overall timeout. |
| `http-root-cert` | Array of path strings | `[]` | Additional HTTP TLS root certificates in PEM format. Every file must be readable and parseable. Empty means no custom additions; system TLS trust remains. These are not RPKI TA certificates. | | `http-root-cert` | Array of path strings | `[]` | Additional HTTP TLS root certificates in PEM format. Every file must be readable and parseable. Empty means no custom additions; system TLS trust remains. These are not RPKI TA certificates. |
| `parallel-max-repo-sync-workers-global` | Positive integer | 8 | Global repository-sync worker count. Zero is rejected. | | `parallel-max-repo-sync-workers-global` | Positive integer | 8 | Global repository-sync worker count. Zero is rejected. |
| `parallel-phase2-object-workers` | Positive integer | 8 | Phase-2 object-processing worker count. Zero is rejected. | | `parallel-phase2-object-workers` | Positive integer | 8 | Phase-2 object-processing worker count. Zero is rejected. |
@@ -279,7 +281,9 @@ TA refresh policy does not make an anchor with an explicit ta automatic.
log-format = "json" log-format = "json"
log-level = "debug" log-level = "debug"
log-progress-interval-secs = 0 log-progress-interval-secs = 0
http-timeout-secs = 600 http-timeout-secs = 10 # notification, TA/TAL and other small HTTP requests
rrdp-snapshot-timeout-secs = 600
rrdp-delta-timeout-secs = 180
http-root-cert = ["tls/internal-root.pem"] http-root-cert = ["tls/internal-root.pem"]
parallel-phase2-object-workers = 4 parallel-phase2-object-workers = 4
parallel-max-repo-sync-workers-global = 4 parallel-max-repo-sync-workers-global = 4
@@ -290,6 +294,10 @@ parallel-repo-worker-queue-capacity = 1024
Replace existing top-level settings; do not add duplicate keys. Prepare the Replace existing top-level settings; do not add duplicate keys. Prepare the
custom TLS PEM file first, or keep [] when no extra roots are needed. This does custom TLS PEM file first, or keep [] when no extra roots are needed. This does
not change the TAL public key, relax TA validation or disable system TLS checks. not change the TAL public key, relax TA validation or disable system TLS checks.
The snapshot and delta timeouts are selected per RRDP resource, not per
validation invocation: an `auto` run may use the snapshot value for one
repository and the delta value for another. RRDP notification and TA/TAL HTTP
requests continue to use `http-timeout-secs`.
### 8.4 Validation policy and rsync transport: top-level fragment ### 8.4 Validation policy and rsync transport: top-level fragment

View File

@@ -7,7 +7,7 @@ shutdown-grace-secs = 30
# Common settings: these choices are explicit, not all built-in defaults. # Common settings: these choices are explicit, not all built-in defaults.
ta-refresh-policy = "allow-valid-cache" ta-refresh-policy = "allow-valid-cache"
max-ca-depth = 64 max-ca-depth = 64
http-timeout-secs = 300 http-timeout-secs = 10
http-root-cert = [] # optional: ["tls/internal-root.pem"] http-root-cert = [] # optional: ["tls/internal-root.pem"]
parallel-max-repo-sync-workers-global = 8 parallel-max-repo-sync-workers-global = 8
parallel-phase2-object-workers = 8 parallel-phase2-object-workers = 8

View File

@@ -8,7 +8,7 @@ shutdown-grace-secs = 30
# Common settings: these choices are explicit, not all built-in defaults. # Common settings: these choices are explicit, not all built-in defaults.
ta-refresh-policy = "allow-valid-cache" ta-refresh-policy = "allow-valid-cache"
max-ca-depth = 64 max-ca-depth = 64
http-timeout-secs = 300 http-timeout-secs = 10
http-root-cert = [] # optional: ["tls/internal-root.pem"] http-root-cert = [] # optional: ["tls/internal-root.pem"]
parallel-max-repo-sync-workers-global = 8 parallel-max-repo-sync-workers-global = 8
parallel-phase2-object-workers = 8 parallel-phase2-object-workers = 8

View File

@@ -8,7 +8,9 @@ publish-dir = "publish/full"
# Common settings: these choices are explicit, not all built-in defaults. # Common settings: these choices are explicit, not all built-in defaults.
ta-refresh-policy = "allow-valid-cache" ta-refresh-policy = "allow-valid-cache"
max-ca-depth = 64 max-ca-depth = 64
http-timeout-secs = 300 http-timeout-secs = 10
rrdp-snapshot-timeout-secs = 600
rrdp-delta-timeout-secs = 180
http-root-cert = [] # optional: ["tls/internal-root.pem"] http-root-cert = [] # optional: ["tls/internal-root.pem"]
parallel-max-repo-sync-workers-global = 8 parallel-max-repo-sync-workers-global = 8
parallel-phase2-object-workers = 8 parallel-phase2-object-workers = 8

View File

@@ -21,7 +21,7 @@ rsync-scope = "module-root" # "host" or "publication-point"
ta-refresh-policy = "allow-valid-cache" ta-refresh-policy = "allow-valid-cache"
max-ca-depth = 64 max-ca-depth = 64
http-timeout-secs = 300 http-timeout-secs = 10
http-root-cert = [] http-root-cert = []
parallel-max-repo-sync-workers-global = 8 parallel-max-repo-sync-workers-global = 8
parallel-phase2-object-workers = 8 parallel-phase2-object-workers = 8

View File

@@ -28,6 +28,8 @@ pub struct Config {
pub ta_constraints_enabled: bool, pub ta_constraints_enabled: bool,
pub max_ca_depth: usize, pub max_ca_depth: usize,
pub http_timeout_secs: u64, pub http_timeout_secs: u64,
pub rrdp_snapshot_timeout_secs: u64,
pub rrdp_delta_timeout_secs: u64,
pub http_root_cert: Vec<PathBuf>, pub http_root_cert: Vec<PathBuf>,
pub parallel_max_repo_sync_workers_global: usize, pub parallel_max_repo_sync_workers_global: usize,
pub parallel_phase2_object_workers: usize, pub parallel_phase2_object_workers: usize,
@@ -117,7 +119,11 @@ impl Default for Config {
ta_refresh_policy: "allow-valid-cache".into(), ta_refresh_policy: "allow-valid-cache".into(),
ta_constraints_enabled: true, ta_constraints_enabled: true,
max_ca_depth: 64, max_ca_depth: 64,
http_timeout_secs: 300, http_timeout_secs: 10,
rrdp_snapshot_timeout_secs:
crate::repository::fetch::http::DEFAULT_RRDP_SNAPSHOT_TIMEOUT_SECS,
rrdp_delta_timeout_secs:
crate::repository::fetch::http::DEFAULT_RRDP_DELTA_TIMEOUT_SECS,
http_root_cert: Vec::new(), http_root_cert: Vec::new(),
parallel_max_repo_sync_workers_global: 8, parallel_max_repo_sync_workers_global: 8,
parallel_phase2_object_workers: 8, parallel_phase2_object_workers: 8,
@@ -356,6 +362,15 @@ impl Config {
normalizations.push(json!({"field":"http-timeout-secs","input":0,"effective":1})); normalizations.push(json!({"field":"http-timeout-secs","input":0,"effective":1}));
config.http_timeout_secs = 1; config.http_timeout_secs = 1;
} }
if config.rrdp_snapshot_timeout_secs < 1 {
normalizations
.push(json!({"field":"rrdp-snapshot-timeout-secs","input":0,"effective":1}));
config.rrdp_snapshot_timeout_secs = 1;
}
if config.rrdp_delta_timeout_secs < 1 {
normalizations.push(json!({"field":"rrdp-delta-timeout-secs","input":0,"effective":1}));
config.rrdp_delta_timeout_secs = 1;
}
if config.parallel_repo_worker_queue_capacity < 1024 { if config.parallel_repo_worker_queue_capacity < 1024 {
normalizations.push(json!({"field":"parallel-repo-worker-queue-capacity","input":config.parallel_repo_worker_queue_capacity,"effective":1024})); normalizations.push(json!({"field":"parallel-repo-worker-queue-capacity","input":config.parallel_repo_worker_queue_capacity,"effective":1024}));
config.parallel_repo_worker_queue_capacity = 1024; config.parallel_repo_worker_queue_capacity = 1024;
@@ -618,6 +633,8 @@ resource-validation-mode = "rfc6487"
validation-time = "2026-09-22T08:00:00+08:00" validation-time = "2026-09-22T08:00:00+08:00"
sync-preference = "rsync-only" sync-preference = "rsync-only"
rsync-timeout-secs = 47 rsync-timeout-secs = 47
rrdp-snapshot-timeout-secs = 720
rrdp-delta-timeout-secs = 90
rsync-command = {:?} rsync-command = {:?}
rsync-mirror-root = "mirror" rsync-mirror-root = "mirror"
rsync-scope = "publication-point" rsync-scope = "publication-point"
@@ -652,6 +669,8 @@ tal = {:?}
); );
assert_eq!(config.sync_preference().unwrap(), SyncPreference::RsyncOnly); assert_eq!(config.sync_preference().unwrap(), SyncPreference::RsyncOnly);
assert_eq!(config.rsync_timeout_secs, 47); assert_eq!(config.rsync_timeout_secs, 47);
assert_eq!(config.rrdp_snapshot_timeout_secs, 720);
assert_eq!(config.rrdp_delta_timeout_secs, 90);
assert_eq!(config.rsync_scope, RsyncScopePolicy::PublicationPoint); assert_eq!(config.rsync_scope, RsyncScopePolicy::PublicationPoint);
assert_eq!( assert_eq!(
config.effective_validation_time().unwrap().unix_timestamp(), config.effective_validation_time().unwrap().unix_timestamp(),
@@ -662,6 +681,8 @@ tal = {:?}
assert_eq!(printed["effective"]["resource-validation-mode"], "rfc6487"); assert_eq!(printed["effective"]["resource-validation-mode"], "rfc6487");
assert_eq!(printed["effective"]["sync-preference"], "rsync-only"); assert_eq!(printed["effective"]["sync-preference"], "rsync-only");
assert_eq!(printed["effective"]["rsync-timeout-secs"], 47); assert_eq!(printed["effective"]["rsync-timeout-secs"], 47);
assert_eq!(printed["effective"]["rrdp-snapshot-timeout-secs"], 720);
assert_eq!(printed["effective"]["rrdp-delta-timeout-secs"], 90);
assert_eq!( assert_eq!(
printed["effective"]["validation-time"], printed["effective"]["validation-time"],
"2026-09-22T00:00:00Z" "2026-09-22T00:00:00Z"
@@ -689,6 +710,51 @@ tal = {:?}
); );
} }
#[test]
fn rrdp_timeout_defaults_are_distinct_and_zero_values_are_normalized() {
let fixture = crate::test_support::synthetic_repository();
let directory = tempfile::tempdir().unwrap();
let config_path = directory.path().join("validate.toml");
std::fs::write(
&config_path,
format!(
r#"
config-version = 1
mode = "validate"
out = "out"
rrdp-snapshot-timeout-secs = 0
rrdp-delta-timeout-secs = 0
[[anchors]]
id = "custom"
tal = {:?}
"#,
fixture.tal().to_string_lossy(),
),
)
.unwrap();
let (config, printed) = Config::load(&config_path, "validate").unwrap();
assert_eq!(Config::default().http_timeout_secs, 10);
assert_eq!(Config::default().rrdp_snapshot_timeout_secs, 600);
assert_eq!(Config::default().rrdp_delta_timeout_secs, 180);
assert_eq!(config.rrdp_snapshot_timeout_secs, 1);
assert_eq!(config.rrdp_delta_timeout_secs, 1);
assert_eq!(printed["effective"]["rrdp-snapshot-timeout-secs"], 1);
assert_eq!(printed["effective"]["rrdp-delta-timeout-secs"], 1);
let normalizations = printed["normalizations"].as_array().unwrap();
assert!(
normalizations
.iter()
.any(|item| item["field"] == "rrdp-snapshot-timeout-secs")
);
assert!(
normalizations
.iter()
.any(|item| item["field"] == "rrdp-delta-timeout-secs")
);
}
#[test] #[test]
fn constraints_selection_disable_override_and_reload() { fn constraints_selection_disable_override_and_reload() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();

View File

@@ -128,6 +128,8 @@ fn execute(config: &Config, run_id: &str) -> Result<i32, String> {
strict_policy: Some(config.strict), strict_policy: Some(config.strict),
resource_validation_mode: Some(config.resource_validation_mode), resource_validation_mode: Some(config.resource_validation_mode),
disable_rrdp: sync_preference == SyncPreference::RsyncOnly, disable_rrdp: sync_preference == SyncPreference::RsyncOnly,
rrdp_snapshot_timeout_secs: Some(config.rrdp_snapshot_timeout_secs),
rrdp_delta_timeout_secs: Some(config.rrdp_delta_timeout_secs),
rsync_command: config.rsync_command.clone(), rsync_command: config.rsync_command.clone(),
rsync_timeout_secs: config.rsync_timeout_secs, rsync_timeout_secs: config.rsync_timeout_secs,
rsync_mirror_root: config.rsync_mirror_root.clone(), rsync_mirror_root: config.rsync_mirror_root.clone(),

View File

@@ -28,6 +28,9 @@ pub fn with_scoped_http_timeout_override<R>(timeout: Duration, f: impl FnOnce()
/// Default User-Agent sent with every outgoing HTTP request (RRDP /// Default User-Agent sent with every outgoing HTTP request (RRDP
/// notification/snapshot/delta and TAL/TA downloads). /// notification/snapshot/delta and TAL/TA downloads).
pub const DEFAULT_HTTP_USER_AGENT: &str = "panda-rpki/0.2"; pub const DEFAULT_HTTP_USER_AGENT: &str = "panda-rpki/0.2";
pub const DEFAULT_LARGE_BODY_TIMEOUT_SECS: u64 = 180;
pub const DEFAULT_RRDP_SNAPSHOT_TIMEOUT_SECS: u64 = 600;
pub const DEFAULT_RRDP_DELTA_TIMEOUT_SECS: u64 = 180;
/// Environment variable that overrides the HTTP User-Agent. Unset, empty, or /// Environment variable that overrides the HTTP User-Agent. Unset, empty, or
/// values containing characters that are illegal in a header value fall back /// values containing characters that are illegal in a header value fall back
@@ -53,8 +56,12 @@ pub struct HttpFetcherConfig {
pub connect_timeout: Duration, pub connect_timeout: Duration,
/// Short timeout used for connection establishment and small metadata objects. /// Short timeout used for connection establishment and small metadata objects.
pub timeout: Duration, pub timeout: Duration,
/// Larger timeout used for RRDP snapshot / delta bodies. /// Generic large-body timeout, retained for untyped HTTP callers.
pub large_body_timeout: Duration, pub large_body_timeout: Duration,
/// Timeout for RRDP snapshot response bodies.
pub rrdp_snapshot_timeout: Duration,
/// Timeout for RRDP delta response bodies.
pub rrdp_delta_timeout: Duration,
pub user_agent: String, pub user_agent: String,
/// Extra PEM trust anchors for HTTPS transport tests or controlled endpoints. /// Extra PEM trust anchors for HTTPS transport tests or controlled endpoints.
pub extra_root_certificates_pem: Vec<Vec<u8>>, pub extra_root_certificates_pem: Vec<Vec<u8>>,
@@ -65,7 +72,9 @@ impl Default for HttpFetcherConfig {
Self { Self {
connect_timeout: Duration::from_secs(15), connect_timeout: Duration::from_secs(15),
timeout: Duration::from_secs(30), timeout: Duration::from_secs(30),
large_body_timeout: Duration::from_secs(180), large_body_timeout: Duration::from_secs(DEFAULT_LARGE_BODY_TIMEOUT_SECS),
rrdp_snapshot_timeout: Duration::from_secs(DEFAULT_RRDP_SNAPSHOT_TIMEOUT_SECS),
rrdp_delta_timeout: Duration::from_secs(DEFAULT_RRDP_DELTA_TIMEOUT_SECS),
user_agent: resolve_http_user_agent(std::env::var(HTTP_USER_AGENT_ENV).ok()), user_agent: resolve_http_user_agent(std::env::var(HTTP_USER_AGENT_ENV).ok()),
extra_root_certificates_pem: Vec::new(), extra_root_certificates_pem: Vec::new(),
} }
@@ -83,15 +92,22 @@ pub struct BlockingHttpFetcher {
large_body_client: Client, large_body_client: Client,
retry_short_client: Client, retry_short_client: Client,
rrdp_short_client: Client, rrdp_short_client: Client,
rrdp_large_body_client: Client, rrdp_snapshot_client: Client,
rrdp_delta_client: Client,
rrdp_retry_short_client: Client, rrdp_retry_short_client: Client,
short_timeout: Duration, short_timeout: Duration,
large_body_timeout: Duration, large_body_timeout: Duration,
rrdp_snapshot_timeout: Duration,
rrdp_delta_timeout: Duration,
} }
impl BlockingHttpFetcher { impl BlockingHttpFetcher {
pub fn new(config: HttpFetcherConfig) -> Result<Self, String> { pub fn new(config: HttpFetcherConfig) -> Result<Self, String> {
let short_timeout = config.timeout; let short_timeout = config.timeout;
let rrdp_snapshot_timeout = config.rrdp_snapshot_timeout;
let rrdp_delta_timeout = config.rrdp_delta_timeout;
// Preserve the existing timeout behavior for untyped HTTP callers.
// RRDP's typed fetch path below uses its exact snapshot/delta profile.
let large_body_timeout = std::cmp::max(config.large_body_timeout, config.timeout); let large_body_timeout = std::cmp::max(config.large_body_timeout, config.timeout);
let connect_timeout = std::cmp::min(config.connect_timeout, config.timeout); let connect_timeout = std::cmp::min(config.connect_timeout, config.timeout);
let short_client = Self::client_builder( let short_client = Self::client_builder(
@@ -126,10 +142,18 @@ impl BlockingHttpFetcher {
)? )?
.build() .build()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let rrdp_large_body_client = Self::rrdp_client_builder( let rrdp_snapshot_client = Self::rrdp_client_builder(
&config, &config,
connect_timeout, connect_timeout,
large_body_timeout, rrdp_snapshot_timeout,
config.user_agent.clone(),
)?
.build()
.map_err(|e| e.to_string())?;
let rrdp_delta_client = Self::rrdp_client_builder(
&config,
connect_timeout,
rrdp_delta_timeout,
config.user_agent.clone(), config.user_agent.clone(),
)? )?
.build() .build()
@@ -147,10 +171,13 @@ impl BlockingHttpFetcher {
large_body_client, large_body_client,
retry_short_client, retry_short_client,
rrdp_short_client, rrdp_short_client,
rrdp_large_body_client, rrdp_snapshot_client,
rrdp_delta_client,
rrdp_retry_short_client, rrdp_retry_short_client,
short_timeout, short_timeout,
large_body_timeout, large_body_timeout,
rrdp_snapshot_timeout,
rrdp_delta_timeout,
}) })
} }
@@ -300,19 +327,28 @@ impl BlockingHttpFetcher {
} }
} }
fn rrdp_client_for_uri(&self, uri: &str) -> (&Client, &'static str, Duration) { fn rrdp_client_for_resource(
&self,
resource: RrdpResourceKind,
) -> (&Client, &'static str, Duration) {
if let Some(timeout) = HTTP_TIMEOUT_OVERRIDE.with(|cell| *cell.borrow()) { if let Some(timeout) = HTTP_TIMEOUT_OVERRIDE.with(|cell| *cell.borrow()) {
return (&self.rrdp_retry_short_client, "retry_short", timeout); return (&self.rrdp_retry_short_client, "retry_short", timeout);
} }
if uses_large_body_timeout(uri) { match resource {
( RrdpResourceKind::Notification => {
&self.rrdp_large_body_client,
"large_body",
self.large_body_timeout,
)
} else {
(&self.rrdp_short_client, "short", self.short_timeout) (&self.rrdp_short_client, "short", self.short_timeout)
} }
RrdpResourceKind::Snapshot => (
&self.rrdp_snapshot_client,
"rrdp_snapshot",
self.rrdp_snapshot_timeout,
),
RrdpResourceKind::Delta => (
&self.rrdp_delta_client,
"rrdp_delta",
self.rrdp_delta_timeout,
),
}
} }
fn rrdp_response( fn rrdp_response(
@@ -336,7 +372,7 @@ impl BlockingHttpFetcher {
} }
for redirects in 0..=MAX_RRDP_REDIRECTS { for redirects in 0..=MAX_RRDP_REDIRECTS {
let (client, profile, timeout) = self.rrdp_client_for_uri(current.as_str()); let (client, profile, timeout) = self.rrdp_client_for_resource(resource);
let response = client let response = client
.get(current.clone()) .get(current.clone())
.send() .send()
@@ -729,6 +765,8 @@ mod tests {
let http = BlockingHttpFetcher::new(HttpFetcherConfig { let http = BlockingHttpFetcher::new(HttpFetcherConfig {
timeout: Duration::from_secs(3), timeout: Duration::from_secs(3),
large_body_timeout: Duration::from_secs(9), large_body_timeout: Duration::from_secs(9),
rrdp_snapshot_timeout: Duration::from_secs(9),
rrdp_delta_timeout: Duration::from_secs(6),
..HttpFetcherConfig::default() ..HttpFetcherConfig::default()
}) })
.expect("http"); .expect("http");
@@ -744,6 +782,68 @@ mod tests {
assert_eq!(timeout_large, Duration::from_secs(9)); assert_eq!(timeout_large, Duration::from_secs(9));
} }
#[test]
fn typed_rrdp_resource_selects_its_own_timeout_independent_of_uri() {
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
timeout: Duration::from_secs(10),
rrdp_snapshot_timeout: Duration::from_secs(600),
rrdp_delta_timeout: Duration::from_secs(180),
..HttpFetcherConfig::default()
})
.expect("http");
let (_, notification_profile, notification_timeout) =
http.rrdp_client_for_resource(RrdpResourceKind::Notification);
let (_, snapshot_profile, snapshot_timeout) =
http.rrdp_client_for_resource(RrdpResourceKind::Snapshot);
let (_, delta_profile, delta_timeout) =
http.rrdp_client_for_resource(RrdpResourceKind::Delta);
assert_eq!(notification_profile, "short");
assert_eq!(notification_timeout, Duration::from_secs(10));
assert_eq!(snapshot_profile, "rrdp_snapshot");
assert_eq!(snapshot_timeout, Duration::from_secs(600));
assert_eq!(delta_profile, "rrdp_delta");
assert_eq!(delta_timeout, Duration::from_secs(180));
}
#[test]
fn rrdp_snapshot_and_delta_requests_enforce_independent_timeouts() {
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind");
let addr = listener.local_addr().expect("addr");
let server = thread::spawn(move || {
for _ in 0..2 {
let (mut stream, _) = listener.accept().expect("accept");
let mut request = [0u8; 1024];
let _ = stream.read(&mut request);
thread::sleep(StdDuration::from_millis(250));
let response =
b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nok";
let _ = stream.write_all(response);
}
});
let uri = format!("http://{addr}/resource.xml");
let origin = RrdpOrigin::parse(&uri, RrdpResourceKind::Notification).expect("origin");
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
timeout: Duration::from_secs(3),
rrdp_snapshot_timeout: Duration::from_secs(3),
rrdp_delta_timeout: Duration::from_millis(100),
..HttpFetcherConfig::default()
})
.expect("http");
assert_eq!(
http.fetch_rrdp(RrdpResourceKind::Snapshot, &uri, &origin)
.expect("snapshot response should fit its timeout"),
b"ok"
);
assert!(
http.fetch_rrdp(RrdpResourceKind::Delta, &uri, &origin)
.is_err()
);
server.join().expect("server thread");
}
#[test] #[test]
fn resolve_http_user_agent_falls_back_to_default() { fn resolve_http_user_agent_falls_back_to_default() {
assert_eq!(resolve_http_user_agent(None), DEFAULT_HTTP_USER_AGENT); assert_eq!(resolve_http_user_agent(None), DEFAULT_HTTP_USER_AGENT);

View File

@@ -112,13 +112,19 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
let validation_started = std::time::Instant::now(); let validation_started = std::time::Instant::now();
crate::logging::observer::phase("runtime_setup"); crate::logging::observer::phase("runtime_setup");
let collect_current_repo_objects = false; let collect_current_repo_objects = false;
let out = if let Some(dir) = args.rsync_local_dir.as_ref() { let mut http_config = HttpFetcherConfig {
let http = BlockingHttpFetcher::new(HttpFetcherConfig {
timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)), timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
extra_root_certificates_pem: http_root_certificates_pem.clone(), extra_root_certificates_pem: http_root_certificates_pem.clone(),
..HttpFetcherConfig::default() ..HttpFetcherConfig::default()
}) };
.map_err(|e| e.to_string())?; if let Some(timeout) = args.rrdp_snapshot_timeout_secs {
http_config.rrdp_snapshot_timeout = std::time::Duration::from_secs(timeout.max(1));
}
if let Some(timeout) = args.rrdp_delta_timeout_secs {
http_config.rrdp_delta_timeout = std::time::Duration::from_secs(timeout.max(1));
}
let out = if let Some(dir) = args.rsync_local_dir.as_ref() {
let http = BlockingHttpFetcher::new(http_config.clone()).map_err(|e| e.to_string())?;
let rsync = LocalDirRsyncFetcher::new(dir); let rsync = LocalDirRsyncFetcher::new(dir);
run_online_validation_with_fetchers( run_online_validation_with_fetchers(
Arc::clone(&store), Arc::clone(&store),
@@ -132,12 +138,7 @@ pub(crate) fn run_config(args: RunConfig) -> Result<(), String> {
timing.as_ref().map(|(_, t)| t), timing.as_ref().map(|(_, t)| t),
)? )?
} else { } else {
let http = BlockingHttpFetcher::new(HttpFetcherConfig { let http = BlockingHttpFetcher::new(http_config).map_err(|e| e.to_string())?;
timeout: std::time::Duration::from_secs(args.http_timeout_secs.max(1)),
extra_root_certificates_pem: http_root_certificates_pem.clone(),
..HttpFetcherConfig::default()
})
.map_err(|e| e.to_string())?;
let rsync = SystemRsyncFetcher::new(SystemRsyncConfig { let rsync = SystemRsyncFetcher::new(SystemRsyncConfig {
rsync_bin: args rsync_bin: args
.rsync_command .rsync_command

View File

@@ -79,6 +79,9 @@ pub struct RunConfig {
pub rsync_command: Option<PathBuf>, pub rsync_command: Option<PathBuf>,
pub http_timeout_secs: u64, pub http_timeout_secs: u64,
/// Per-resource RRDP response timeouts; None selects the fetcher default.
pub rrdp_snapshot_timeout_secs: Option<u64>,
pub rrdp_delta_timeout_secs: Option<u64>,
pub http_root_cert_paths: Vec<PathBuf>, pub http_root_cert_paths: Vec<PathBuf>,
pub rsync_timeout_secs: u64, pub rsync_timeout_secs: u64,
pub rsync_mirror_root: Option<PathBuf>, pub rsync_mirror_root: Option<PathBuf>,