rpki/src/parallel/repo_runtime.rs

1487 lines
58 KiB
Rust

use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::parallel::repo_scheduler::TransportRequestAction;
use crate::parallel::repo_worker::{RepoTransportExecutor, RepoTransportWorkerPool};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::transport_prefetch::{
TransportPrefetchDispatchStats, TransportPrefetchRecorder, TransportPrefetchSnapshot,
};
use crate::parallel::types::{
RepoIdentity, RepoRequester, RepoRuntimeState, RepoTransportMode, RepoTransportResultEnvelope,
RepoTransportResultKind,
};
use crate::policy::SyncPreference;
use crate::report::Warning;
use crate::validation::tree::{CaInstanceHandle, DiscoveredChildCaInstance};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSyncRuntimeOutcome {
pub repo_sync_ok: bool,
pub repo_sync_err: Option<String>,
pub repo_sync_source: Option<String>,
pub repo_sync_phase: Option<String>,
pub repo_sync_duration_ms: u64,
pub warnings: Vec<Warning>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RepoSyncRequestStatus {
Ready {
identity: RepoIdentity,
outcome: RepoSyncRuntimeOutcome,
},
Pending {
identity: RepoIdentity,
state: RepoRuntimeState,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSyncRuntimeCompletion {
pub identity: RepoIdentity,
pub state: RepoRuntimeState,
pub outcome: RepoSyncRuntimeOutcome,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RepoSyncRuntimeEvent {
pub transport_identity: RepoIdentity,
pub completions: Vec<RepoSyncRuntimeCompletion>,
}
pub trait RepoSyncRuntime: Send + Sync {
fn sync_publication_point_repo(
&self,
ca: &CaInstanceHandle,
) -> Result<RepoSyncRuntimeOutcome, String>;
fn request_publication_point_repo(
&self,
ca: &CaInstanceHandle,
priority: u8,
) -> Result<RepoSyncRequestStatus, String>;
fn recv_repo_result_timeout(
&self,
timeout: Duration,
) -> Result<Option<RepoSyncRuntimeEvent>, String>;
fn drain_repo_results_timeout(
&self,
timeout: Duration,
max_events: usize,
) -> Result<Vec<RepoSyncRuntimeEvent>, String> {
let max_events = max_events.max(1);
let mut events = Vec::new();
for index in 0..max_events {
let poll_timeout = if index == 0 {
timeout
} else {
Duration::from_millis(0)
};
let Some(event) = self.recv_repo_result_timeout(poll_timeout)? else {
break;
};
events.push(event);
}
Ok(events)
}
fn reset_run_state(&self) -> Result<(), String>;
fn prefetch_discovered_children(
&self,
children: &[DiscoveredChildCaInstance],
) -> Result<(), String>;
fn prefetch_transport_requests(
&self,
snapshot: &TransportPrefetchSnapshot,
validation_time: time::OffsetDateTime,
) -> Result<TransportPrefetchDispatchStats, String>;
fn transport_prefetch_snapshot(&self) -> TransportPrefetchSnapshot;
}
pub struct Phase1RepoSyncRuntime<E: RepoTransportExecutor> {
coordinator: Mutex<GlobalRunCoordinator>,
worker_pool: Mutex<RepoTransportWorkerPool<E>>,
transport_prefetch_recorder: Option<Mutex<TransportPrefetchRecorder>>,
retry_short_rsync_scopes: Mutex<HashSet<String>>,
rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
sync_preference: SyncPreference,
}
impl<E: RepoTransportExecutor> Phase1RepoSyncRuntime<E> {
pub fn new(
coordinator: GlobalRunCoordinator,
worker_pool: RepoTransportWorkerPool<E>,
rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
sync_preference: SyncPreference,
) -> Self {
Self::new_with_failure_scope(
coordinator,
worker_pool,
rsync_scope_resolver,
Arc::new(|_base: &str| None),
sync_preference,
)
}
pub fn new_with_failure_scope(
coordinator: GlobalRunCoordinator,
worker_pool: RepoTransportWorkerPool<E>,
rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
sync_preference: SyncPreference,
) -> Self {
Self {
coordinator: Mutex::new(coordinator),
worker_pool: Mutex::new(worker_pool),
transport_prefetch_recorder: None,
retry_short_rsync_scopes: Mutex::new(HashSet::new()),
rsync_scope_resolver,
rsync_failure_scope_resolver,
sync_preference,
}
}
pub fn new_with_failure_scope_and_prefetch_recording(
coordinator: GlobalRunCoordinator,
worker_pool: RepoTransportWorkerPool<E>,
rsync_scope_resolver: Arc<dyn Fn(&str) -> String + Send + Sync>,
rsync_failure_scope_resolver: Arc<dyn Fn(&str) -> Option<String> + Send + Sync>,
sync_preference: SyncPreference,
record_transport_prefetch_requests: bool,
) -> Self {
Self {
coordinator: Mutex::new(coordinator),
worker_pool: Mutex::new(worker_pool),
transport_prefetch_recorder: record_transport_prefetch_requests
.then(|| Mutex::new(TransportPrefetchRecorder::default())),
retry_short_rsync_scopes: Mutex::new(HashSet::new()),
rsync_scope_resolver,
rsync_failure_scope_resolver,
sync_preference,
}
}
fn build_requester(ca: &CaInstanceHandle) -> RepoRequester {
RepoRequester {
tal_id: ca.tal_id.clone(),
rir_id: ca.tal_id.clone(),
parent_node_id: None,
ca_instance_handle_id: format!("{}:{}", ca.tal_id, ca.manifest_rsync_uri),
publication_point_rsync_uri: ca.publication_point_rsync_uri.clone(),
manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
}
}
fn build_identity(ca: &CaInstanceHandle) -> RepoIdentity {
RepoIdentity::new(ca.rrdp_notification_uri.clone(), ca.rsync_base_uri.clone())
}
fn request_transport_for_ca(
&self,
ca: &CaInstanceHandle,
priority: u8,
) -> Result<RepoSyncRequestStatus, String> {
let identity = Self::build_identity(ca);
let requester = Self::build_requester(ca);
let rsync_scope_uri = (self.rsync_scope_resolver)(&identity.rsync_base_uri);
let rsync_failure_scope_uri = (self.rsync_failure_scope_resolver)(&identity.rsync_base_uri);
if let Some(recorder) = self.transport_prefetch_recorder.as_ref() {
let mut recorder = recorder
.lock()
.expect("transport prefetch recorder lock poisoned");
recorder.record_registered_request(
&identity,
&requester,
priority,
rsync_scope_uri.clone(),
rsync_failure_scope_uri.clone(),
self.sync_preference,
);
}
let action = {
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator.register_transport_request(
identity.clone(),
requester,
time::OffsetDateTime::now_utc(),
priority,
rsync_scope_uri,
rsync_failure_scope_uri,
self.sync_preference,
false,
)
};
match action {
TransportRequestAction::Enqueue(task) => {
crate::progress_log::emit(
"phase1_repo_task_enqueued",
serde_json::json!({
"manifest_rsync_uri": ca.manifest_rsync_uri,
"publication_point_rsync_uri": ca.publication_point_rsync_uri,
"repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
"rsync_failure_scope_uri": task.rsync_failure_scope_uri,
"repo_key_notification_uri": task.repo_identity.notification_uri,
"priority": priority,
"transport_mode": match task.mode {
RepoTransportMode::Rrdp => "rrdp",
RepoTransportMode::Rsync => "rsync",
},
}),
);
self.drain_pending_transport_tasks()?;
Ok(RepoSyncRequestStatus::Pending {
identity,
state: self
.runtime_state_for_identity(&task.repo_identity)
.unwrap_or(RepoRuntimeState::WaitingRrdp),
})
}
TransportRequestAction::Waiting { state } => {
crate::progress_log::emit(
"phase1_repo_task_waiting",
serde_json::json!({
"manifest_rsync_uri": ca.manifest_rsync_uri,
"publication_point_rsync_uri": ca.publication_point_rsync_uri,
"repo_key_rsync_base_uri": identity.rsync_base_uri,
"rsync_failure_scope_uri": (self.rsync_failure_scope_resolver)(&identity.rsync_base_uri),
"repo_key_notification_uri": identity.notification_uri,
"priority": priority,
"runtime_state": format!("{state:?}"),
}),
);
Ok(RepoSyncRequestStatus::Pending { identity, state })
}
TransportRequestAction::ReusedSuccess(result)
| TransportRequestAction::ReusedTerminalFailure(result) => {
crate::progress_log::emit(
"phase1_repo_task_reused",
serde_json::json!({
"manifest_rsync_uri": ca.manifest_rsync_uri,
"publication_point_rsync_uri": ca.publication_point_rsync_uri,
"repo_key_rsync_base_uri": identity.rsync_base_uri,
"rsync_failure_scope_uri": result.rsync_failure_scope_uri,
"repo_key_notification_uri": identity.notification_uri,
"priority": priority,
"transport_mode": match result.mode {
RepoTransportMode::Rrdp => "rrdp",
RepoTransportMode::Rsync => "rsync",
},
}),
);
Ok(RepoSyncRequestStatus::Ready {
outcome: outcome_from_transport_result(
&result,
self.runtime_state_for_identity(&identity)
.unwrap_or(RepoRuntimeState::Init),
),
identity,
})
}
}
}
fn drain_pending_transport_tasks(&self) -> Result<(), String> {
loop {
let maybe_task = {
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator.pop_next_transport_task()
};
let Some(task) = maybe_task else {
break;
};
{
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator
.mark_transport_running(&task.dedup_key, time::OffsetDateTime::now_utc())?;
}
crate::progress_log::emit(
"phase1_repo_task_dispatched",
serde_json::json!({
"repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
"rsync_failure_scope_uri": task.rsync_failure_scope_uri,
"repo_key_notification_uri": task.repo_identity.notification_uri,
"requester_count": task.requesters.len(),
"priority": task.priority,
"transport_mode": match task.mode {
RepoTransportMode::Rrdp => "rrdp",
RepoTransportMode::Rsync => "rsync",
},
}),
);
let pool = self.worker_pool.lock().expect("worker pool lock poisoned");
pool.submit(task)?;
}
Ok(())
}
fn pump_one_transport_result(
&self,
timeout: Duration,
) -> Result<Option<RepoSyncRuntimeEvent>, String> {
let envelope = {
let pool = self.worker_pool.lock().expect("worker pool lock poisoned");
pool.recv_result_timeout(timeout)?
};
let Some(envelope) = envelope else {
return Ok(None);
};
let transport_identity = envelope.repo_identity.clone();
let completed_envelope = envelope.clone();
if let Some(recorder) = self.transport_prefetch_recorder.as_ref() {
recorder
.lock()
.expect("transport prefetch recorder lock poisoned")
.record_result(&envelope);
}
crate::progress_log::emit(
"phase1_repo_task_result",
serde_json::json!({
"repo_key_rsync_base_uri": envelope.repo_identity.rsync_base_uri,
"rsync_failure_scope_uri": envelope.rsync_failure_scope_uri,
"repo_key_notification_uri": envelope.repo_identity.notification_uri,
"timing_ms": envelope.timing_ms,
"transport_mode": match envelope.mode {
RepoTransportMode::Rrdp => "rrdp",
RepoTransportMode::Rsync => "rsync",
},
"result": match &envelope.result {
RepoTransportResultKind::Success { .. } => "success",
RepoTransportResultKind::Failed { .. } => "failed",
},
}),
);
let finished_at = time::OffsetDateTime::now_utc();
let completion = {
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator.complete_transport_result(envelope, finished_at)?
};
if !completion.follow_up_tasks.is_empty() {
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
for mut task in completion.follow_up_tasks {
if let crate::parallel::types::RepoDedupKey::RsyncScope { rsync_scope_uri } =
&task.dedup_key
{
if self
.retry_short_rsync_scopes
.lock()
.expect("retry short rsync scopes lock poisoned")
.contains(rsync_scope_uri)
{
task.retry_short_timeout = true;
}
}
crate::progress_log::emit(
"phase1_repo_task_enqueued",
serde_json::json!({
"manifest_rsync_uri": serde_json::Value::Null,
"publication_point_rsync_uri": task.requesters.first().map(|r| r.publication_point_rsync_uri.clone()),
"repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
"repo_key_notification_uri": task.repo_identity.notification_uri,
"priority": task.priority,
"transport_mode": "rsync",
}),
);
coordinator.push_transport_task(task);
}
}
self.drain_pending_transport_tasks()?;
let completions = {
let coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator
.finalized_runtime_records_for_transport_result(&completed_envelope)
.into_iter()
.filter_map(|record| {
let outcome = match record.state {
RepoRuntimeState::RrdpOk | RepoRuntimeState::RsyncOk => record
.last_success
.as_ref()
.map(|result| outcome_from_transport_result(result, record.state)),
RepoRuntimeState::FailedTerminal => record
.terminal_failure
.as_ref()
.map(|result| outcome_from_transport_result(result, record.state)),
_ => None,
}?;
Some(RepoSyncRuntimeCompletion {
identity: record.identity,
state: record.state,
outcome,
})
})
.collect::<Vec<_>>()
};
if completions.is_empty() {
return Ok(None);
}
Ok(Some(RepoSyncRuntimeEvent {
transport_identity,
completions,
}))
}
fn pump_transport_results(
&self,
timeout: Duration,
max_events: usize,
) -> Result<Vec<RepoSyncRuntimeEvent>, String> {
let max_events = max_events.max(1);
let mut events = Vec::new();
for index in 0..max_events {
let poll_timeout = if index == 0 {
timeout
} else {
Duration::from_millis(0)
};
let Some(event) = self.pump_one_transport_result(poll_timeout)? else {
break;
};
events.push(event);
}
Ok(events)
}
fn runtime_state_for_identity(&self, identity: &RepoIdentity) -> Option<RepoRuntimeState> {
let coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator
.runtime_record(identity)
.map(|record| record.state)
}
fn resolved_outcome_for_identity(
&self,
identity: &RepoIdentity,
) -> Option<RepoSyncRuntimeOutcome> {
let coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
let record = coordinator.runtime_record(identity)?;
match record.state {
RepoRuntimeState::RrdpOk | RepoRuntimeState::RsyncOk => record
.last_success
.as_ref()
.map(|result| outcome_from_transport_result(result, record.state)),
RepoRuntimeState::FailedTerminal => record
.terminal_failure
.as_ref()
.map(|result| outcome_from_transport_result(result, record.state)),
_ => None,
}
}
}
impl<E: RepoTransportExecutor> RepoSyncRuntime for Phase1RepoSyncRuntime<E> {
fn sync_publication_point_repo(
&self,
ca: &CaInstanceHandle,
) -> Result<RepoSyncRuntimeOutcome, String> {
if let RepoSyncRequestStatus::Ready { outcome, .. } =
self.request_publication_point_repo(ca, 0)?
{
return Ok(outcome);
}
let identity = Self::build_identity(ca);
loop {
if let Some(done) = self.resolved_outcome_for_identity(&identity) {
return Ok(done);
}
let _ = self.recv_repo_result_timeout(Duration::from_millis(50))?;
}
}
fn request_publication_point_repo(
&self,
ca: &CaInstanceHandle,
priority: u8,
) -> Result<RepoSyncRequestStatus, String> {
self.request_transport_for_ca(ca, priority)
}
fn recv_repo_result_timeout(
&self,
timeout: Duration,
) -> Result<Option<RepoSyncRuntimeEvent>, String> {
self.pump_one_transport_result(timeout)
}
fn drain_repo_results_timeout(
&self,
timeout: Duration,
max_events: usize,
) -> Result<Vec<RepoSyncRuntimeEvent>, String> {
self.pump_transport_results(timeout, max_events)
}
fn reset_run_state(&self) -> Result<(), String> {
{
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
if coordinator.stats.repo_tasks_running != 0 {
return Err(format!(
"cannot reset repo runtime with {} repo task(s) still running",
coordinator.stats.repo_tasks_running
));
}
coordinator.reset_run_state();
}
loop {
let maybe_result = {
let pool = self.worker_pool.lock().expect("worker pool lock poisoned");
pool.recv_result_timeout(Duration::from_millis(0))?
};
if maybe_result.is_none() {
break;
}
}
Ok(())
}
fn prefetch_discovered_children(
&self,
children: &[DiscoveredChildCaInstance],
) -> Result<(), String> {
for child in children {
let _ = self.request_publication_point_repo(&child.handle, 1)?;
}
Ok(())
}
fn prefetch_transport_requests(
&self,
snapshot: &TransportPrefetchSnapshot,
validation_time: time::OffsetDateTime,
) -> Result<TransportPrefetchDispatchStats, String> {
let mut stats = TransportPrefetchDispatchStats {
loaded_requests: snapshot.requests.len() as u64,
..TransportPrefetchDispatchStats::default()
};
for request in &snapshot.requests {
let identity = request.to_identity();
let current_rsync_scope_uri = (self.rsync_scope_resolver)(&identity.rsync_base_uri);
let current_rsync_failure_scope_uri =
(self.rsync_failure_scope_resolver)(&identity.rsync_base_uri);
if current_rsync_scope_uri != request.rsync_scope_uri
|| current_rsync_failure_scope_uri != request.rsync_failure_scope_uri
{
stats.skipped_incompatible += 1;
continue;
}
if request.retry_short_rsync_timeout() {
self.retry_short_rsync_scopes
.lock()
.expect("retry short rsync scopes lock poisoned")
.insert(current_rsync_scope_uri.clone());
}
let action = {
let mut coordinator = self.coordinator.lock().expect("coordinator lock poisoned");
coordinator.register_transport_request(
identity,
request.to_requester(),
validation_time,
request.priority,
current_rsync_scope_uri,
current_rsync_failure_scope_uri,
self.sync_preference,
request.retry_short_timeout(),
)
};
match action {
TransportRequestAction::Enqueue(task) => {
stats.enqueued_tasks += 1;
crate::progress_log::emit(
"phase1_repo_prefetch_enqueued",
serde_json::json!({
"repo_key_rsync_base_uri": task.repo_identity.rsync_base_uri,
"rsync_failure_scope_uri": task.rsync_failure_scope_uri,
"repo_key_notification_uri": task.repo_identity.notification_uri,
"priority": task.priority,
"transport_mode": match task.mode {
RepoTransportMode::Rrdp => "rrdp",
RepoTransportMode::Rsync => "rsync",
},
}),
);
}
TransportRequestAction::Waiting { .. } => {
stats.waiting_requests += 1;
}
TransportRequestAction::ReusedSuccess(_)
| TransportRequestAction::ReusedTerminalFailure(_) => {
stats.reused_results += 1;
}
}
}
self.drain_pending_transport_tasks()?;
Ok(stats)
}
fn transport_prefetch_snapshot(&self) -> TransportPrefetchSnapshot {
self.transport_prefetch_recorder
.as_ref()
.map(|recorder| {
recorder
.lock()
.expect("transport prefetch recorder lock poisoned")
.snapshot(self.sync_preference)
})
.unwrap_or_else(|| TransportPrefetchSnapshot::new(self.sync_preference, Vec::new()))
}
}
fn outcome_from_transport_result(
envelope: &RepoTransportResultEnvelope,
state: RepoRuntimeState,
) -> RepoSyncRuntimeOutcome {
match (&envelope.result, state) {
(RepoTransportResultKind::Success { source, warnings }, RepoRuntimeState::RrdpOk) => {
RepoSyncRuntimeOutcome {
repo_sync_ok: true,
repo_sync_err: None,
repo_sync_source: Some(source.clone()),
repo_sync_phase: Some("rrdp_ok".to_string()),
repo_sync_duration_ms: envelope.timing_ms,
warnings: warnings.clone(),
}
}
(RepoTransportResultKind::Success { source, warnings }, RepoRuntimeState::RsyncOk) => {
RepoSyncRuntimeOutcome {
repo_sync_ok: true,
repo_sync_err: None,
repo_sync_source: Some(source.clone()),
repo_sync_phase: Some(if envelope.repo_identity.notification_uri.is_some() {
"rrdp_failed_rsync_ok".to_string()
} else {
"rsync_only_ok".to_string()
}),
repo_sync_duration_ms: envelope.timing_ms,
warnings: warnings.clone(),
}
}
(
RepoTransportResultKind::Failed { detail, warnings },
RepoRuntimeState::FailedTerminal,
) => RepoSyncRuntimeOutcome {
repo_sync_ok: false,
repo_sync_err: Some(detail.clone()),
repo_sync_source: None,
repo_sync_phase: Some(if envelope.repo_identity.notification_uri.is_some() {
"rrdp_failed_rsync_failed".to_string()
} else {
"rsync_failed".to_string()
}),
repo_sync_duration_ms: envelope.timing_ms,
warnings: warnings.clone(),
},
_ => RepoSyncRuntimeOutcome {
repo_sync_ok: false,
repo_sync_err: Some("repo runtime state unresolved".to_string()),
repo_sync_source: None,
repo_sync_phase: Some("repo_runtime_unresolved".to_string()),
repo_sync_duration_ms: envelope.timing_ms,
warnings: Vec::new(),
},
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use crate::parallel::config::ParallelPhase1Config;
use crate::parallel::repo_runtime::{Phase1RepoSyncRuntime, RepoSyncRuntime};
use crate::parallel::repo_worker::{
RepoTransportExecutor, RepoTransportWorkerPool, RepoWorkerPoolConfig,
};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::transport_prefetch::TransportPrefetchSnapshot;
use crate::parallel::types::{
RepoRuntimeState, RepoTransportMode, RepoTransportResultEnvelope, RepoTransportResultKind,
RepoTransportTask, TalInputSpec,
};
use crate::policy::SyncPreference;
use crate::report::Warning;
use crate::validation::tree::{CaCertificateRef, CaInstanceHandle, DiscoveredChildCaInstance};
fn sample_ca(manifest: &str) -> CaInstanceHandle {
CaInstanceHandle {
depth: 0,
tal_id: "arin".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(vec![1, 2, 3]),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/".to_string(),
manifest_rsync_uri: manifest.to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
}
}
struct SuccessTransportExecutor;
impl RepoTransportExecutor for SuccessTransportExecutor {
fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
RepoTransportResultEnvelope {
dedup_key: task.dedup_key,
rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
repo_identity: task.repo_identity,
mode: task.mode,
tal_id: task.tal_id,
rir_id: task.rir_id,
timing_ms: 7,
result: RepoTransportResultKind::Success {
source: match task.mode {
RepoTransportMode::Rrdp => "rrdp".to_string(),
RepoTransportMode::Rsync => "rsync".to_string(),
},
warnings: vec![Warning::new("transport ok")],
},
}
}
}
struct CountingSuccessTransportExecutor {
count: Arc<AtomicUsize>,
}
impl RepoTransportExecutor for CountingSuccessTransportExecutor {
fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
self.count.fetch_add(1, Ordering::SeqCst);
RepoTransportResultEnvelope {
dedup_key: task.dedup_key,
rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
repo_identity: task.repo_identity,
mode: task.mode,
tal_id: task.tal_id,
rir_id: task.rir_id,
timing_ms: 7,
result: RepoTransportResultKind::Success {
source: match task.mode {
RepoTransportMode::Rrdp => "rrdp".to_string(),
RepoTransportMode::Rsync => "rsync".to_string(),
},
warnings: vec![Warning::new("transport ok")],
},
}
}
}
struct FailRrdpThenSucceedRsyncExecutor {
rrdp_count: Arc<AtomicUsize>,
rsync_count: Arc<AtomicUsize>,
}
impl RepoTransportExecutor for FailRrdpThenSucceedRsyncExecutor {
fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
match task.mode {
RepoTransportMode::Rrdp => {
self.rrdp_count.fetch_add(1, Ordering::SeqCst);
RepoTransportResultEnvelope {
dedup_key: task.dedup_key,
rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
repo_identity: task.repo_identity,
mode: RepoTransportMode::Rrdp,
tal_id: task.tal_id,
rir_id: task.rir_id,
timing_ms: 10,
result: RepoTransportResultKind::Failed {
detail: "rrdp failed".to_string(),
warnings: vec![Warning::new("rrdp failed")],
},
}
}
RepoTransportMode::Rsync => {
self.rsync_count.fetch_add(1, Ordering::SeqCst);
RepoTransportResultEnvelope {
dedup_key: task.dedup_key,
rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
repo_identity: task.repo_identity,
mode: RepoTransportMode::Rsync,
tal_id: task.tal_id,
rir_id: task.rir_id,
timing_ms: 12,
result: RepoTransportResultKind::Success {
source: "rsync".to_string(),
warnings: vec![Warning::new("rsync ok")],
},
}
}
}
}
}
struct FailRrdpThenFailRsyncExecutor {
rrdp_count: Arc<AtomicUsize>,
rsync_count: Arc<AtomicUsize>,
}
impl RepoTransportExecutor for FailRrdpThenFailRsyncExecutor {
fn execute_transport(&self, task: RepoTransportTask) -> RepoTransportResultEnvelope {
match task.mode {
RepoTransportMode::Rrdp => {
self.rrdp_count.fetch_add(1, Ordering::SeqCst);
RepoTransportResultEnvelope {
dedup_key: task.dedup_key,
rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
repo_identity: task.repo_identity,
mode: RepoTransportMode::Rrdp,
tal_id: task.tal_id,
rir_id: task.rir_id,
timing_ms: 10,
result: RepoTransportResultKind::Failed {
detail: "rrdp failed".to_string(),
warnings: vec![Warning::new("rrdp failed")],
},
}
}
RepoTransportMode::Rsync => {
self.rsync_count.fetch_add(1, Ordering::SeqCst);
RepoTransportResultEnvelope {
dedup_key: task.dedup_key,
rsync_failure_scope_uri: task.rsync_failure_scope_uri.clone(),
repo_identity: task.repo_identity,
mode: RepoTransportMode::Rsync,
tal_id: task.tal_id,
rir_id: task.rir_id,
timing_ms: 12,
result: RepoTransportResultKind::Failed {
detail: "rsync failed".to_string(),
warnings: vec![Warning::new("rsync failed")],
},
}
}
}
}
}
#[test]
fn phase1_runtime_waits_for_rrdp_transport_and_returns_rrdp_outcome() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let outcome = runtime
.sync_publication_point_repo(&sample_ca("rsync://example.test/repo/root.mft"))
.expect("sync repo");
assert!(outcome.repo_sync_ok);
assert_eq!(outcome.repo_sync_source.as_deref(), Some("rrdp"));
assert_eq!(outcome.repo_sync_phase.as_deref(), Some("rrdp_ok"));
}
#[test]
fn phase1_runtime_request_repo_returns_pending_then_repo_ready_event() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let ca = sample_ca("rsync://example.test/repo/root.mft");
let status = runtime
.request_publication_point_repo(&ca, 0)
.expect("request repo");
let identity = match status {
super::RepoSyncRequestStatus::Pending { identity, state } => {
assert_eq!(state, RepoRuntimeState::WaitingRrdp);
identity
}
other => panic!("expected pending, got {other:?}"),
};
let event = runtime
.recv_repo_result_timeout(Duration::from_secs(1))
.expect("repo event")
.expect("event");
assert_eq!(event.transport_identity, identity);
assert_eq!(event.completions.len(), 1);
assert_eq!(event.completions[0].identity, identity);
assert_eq!(event.completions[0].state, RepoRuntimeState::RrdpOk);
assert!(event.completions[0].outcome.repo_sync_ok);
assert_eq!(
event.completions[0].outcome.repo_sync_source.as_deref(),
Some("rrdp")
);
}
#[test]
fn phase1_runtime_request_repo_reuses_ready_event_result() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let ca = sample_ca("rsync://example.test/repo/root.mft");
let first = runtime
.request_publication_point_repo(&ca, 0)
.expect("request repo");
assert!(matches!(
first,
super::RepoSyncRequestStatus::Pending { .. }
));
let _ = runtime
.recv_repo_result_timeout(Duration::from_secs(1))
.expect("repo event")
.expect("event");
let second = runtime
.request_publication_point_repo(&ca, 0)
.expect("request repo reused");
match second {
super::RepoSyncRequestStatus::Ready { outcome, .. } => {
assert!(outcome.repo_sync_ok);
assert_eq!(outcome.repo_sync_phase.as_deref(), Some("rrdp_ok"));
}
other => panic!("expected ready reuse, got {other:?}"),
}
}
#[test]
fn phase1_runtime_repo_event_reports_all_finalized_identities_for_shared_rrdp() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let ca1 = sample_ca("rsync://example.test/repo/root.mft");
let mut ca2 = sample_ca("rsync://example.test/other/root.mft");
ca2.rsync_base_uri = "rsync://example.test/other/".to_string();
ca2.publication_point_rsync_uri = "rsync://example.test/other/".to_string();
let id1 = match runtime
.request_publication_point_repo(&ca1, 0)
.expect("request first")
{
super::RepoSyncRequestStatus::Pending { identity, .. } => identity,
other => panic!("expected first pending, got {other:?}"),
};
let id2 = match runtime
.request_publication_point_repo(&ca2, 0)
.expect("request second")
{
super::RepoSyncRequestStatus::Pending { identity, .. } => identity,
other => panic!("expected second pending, got {other:?}"),
};
assert_ne!(id1, id2);
let event = runtime
.recv_repo_result_timeout(Duration::from_secs(1))
.expect("repo event")
.expect("event");
let mut identities = event
.completions
.iter()
.map(|completion| completion.identity.clone())
.collect::<Vec<_>>();
identities.sort_by(|a, b| a.rsync_base_uri.cmp(&b.rsync_base_uri));
let mut expected = vec![id1, id2];
expected.sort_by(|a, b| a.rsync_base_uri.cmp(&b.rsync_base_uri));
assert_eq!(identities, expected);
assert!(
event
.completions
.iter()
.all(|completion| completion.state == RepoRuntimeState::RrdpOk)
);
}
#[test]
fn phase1_runtime_drains_multiple_ready_transport_events() {
let count = Arc::new(AtomicUsize::new(0));
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 2 },
CountingSuccessTransportExecutor {
count: Arc::clone(&count),
},
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let ca1 = sample_ca("rsync://example.test/repo/root.mft");
let mut ca2 = sample_ca("rsync://example.net/repo/root.mft");
ca2.rsync_base_uri = "rsync://example.net/repo/".to_string();
ca2.publication_point_rsync_uri = "rsync://example.net/repo/".to_string();
ca2.rrdp_notification_uri = Some("https://example.net/notify.xml".to_string());
assert!(matches!(
runtime
.request_publication_point_repo(&ca1, 0)
.expect("request ca1"),
super::RepoSyncRequestStatus::Pending { .. }
));
assert!(matches!(
runtime
.request_publication_point_repo(&ca2, 0)
.expect("request ca2"),
super::RepoSyncRequestStatus::Pending { .. }
));
let started = Instant::now();
while count.load(Ordering::SeqCst) < 2 && started.elapsed() < Duration::from_secs(1) {
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(count.load(Ordering::SeqCst), 2);
let events = runtime
.drain_repo_results_timeout(Duration::from_millis(0), 8)
.expect("drain events");
assert_eq!(events.len(), 2);
assert_eq!(
events
.iter()
.map(|event| event.completions.len())
.sum::<usize>(),
2
);
}
#[test]
fn phase1_runtime_reset_run_state_clears_completed_transport_reuse() {
let count = Arc::new(AtomicUsize::new(0));
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
CountingSuccessTransportExecutor {
count: Arc::clone(&count),
},
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let ca = sample_ca("rsync://example.test/repo/root.mft");
assert!(matches!(
runtime
.request_publication_point_repo(&ca, 0)
.expect("first request"),
super::RepoSyncRequestStatus::Pending { .. }
));
let _ = runtime
.recv_repo_result_timeout(Duration::from_secs(1))
.expect("first event")
.expect("event");
assert_eq!(count.load(Ordering::SeqCst), 1);
assert!(matches!(
runtime
.request_publication_point_repo(&ca, 0)
.expect("ready reuse before reset"),
super::RepoSyncRequestStatus::Ready { .. }
));
runtime.reset_run_state().expect("reset");
assert!(matches!(
runtime
.request_publication_point_repo(&ca, 0)
.expect("second request after reset"),
super::RepoSyncRequestStatus::Pending { .. }
));
let _ = runtime
.recv_repo_result_timeout(Duration::from_secs(1))
.expect("second event")
.expect("event");
assert_eq!(count.load(Ordering::SeqCst), 2);
}
#[test]
fn phase1_runtime_records_prefetch_snapshot_only_when_enabled() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let ca = sample_ca("rsync://example.test/repo/root.mft");
let _ = runtime
.request_publication_point_repo(&ca, 0)
.expect("request repo");
let snapshot = runtime.transport_prefetch_snapshot();
assert_eq!(snapshot.requests.len(), 1);
assert_eq!(
snapshot.requests[0].repo_identity.rsync_base_uri,
"rsync://example.test/repo/"
);
assert_eq!(
snapshot.requests[0].rsync_failure_scope_uri.as_deref(),
Some("rsync://example.test/")
);
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
SyncPreference::RrdpThenRsync,
);
let _ = runtime
.request_publication_point_repo(&ca, 0)
.expect("request repo without recording");
assert!(runtime.transport_prefetch_snapshot().requests.is_empty());
}
#[test]
fn phase1_runtime_prefetch_dispatches_and_later_request_waits_on_same_task() {
let count = Arc::new(AtomicUsize::new(0));
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
CountingSuccessTransportExecutor {
count: Arc::clone(&count),
},
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let ca = sample_ca("rsync://example.test/repo/root.mft");
let mut recorder =
crate::parallel::transport_prefetch::TransportPrefetchRecorder::default();
recorder.record_registered_request(
&super::Phase1RepoSyncRuntime::<CountingSuccessTransportExecutor>::build_identity(&ca),
&super::Phase1RepoSyncRuntime::<CountingSuccessTransportExecutor>::build_requester(&ca),
0,
"rsync://example.test/repo/".to_string(),
Some("rsync://example.test/".to_string()),
SyncPreference::RrdpThenRsync,
);
let snapshot = recorder.snapshot(SyncPreference::RrdpThenRsync);
let stats = runtime
.prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
.expect("prefetch transport requests");
assert_eq!(stats.loaded_requests, 1);
assert_eq!(stats.enqueued_tasks, 1);
let status = runtime
.request_publication_point_repo(&ca, 0)
.expect("request after prefetch");
assert!(matches!(
status,
super::RepoSyncRequestStatus::Pending {
state: RepoRuntimeState::WaitingRrdp,
..
}
));
let _ = runtime
.recv_repo_result_timeout(Duration::from_secs(1))
.expect("repo event")
.expect("event");
assert_eq!(count.load(Ordering::SeqCst), 1);
}
#[test]
fn phase1_runtime_does_not_persist_prefetch_only_requests() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|base: &str| base.to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let snapshot = TransportPrefetchSnapshot::new(
SyncPreference::RrdpThenRsync,
vec![crate::parallel::transport_prefetch::TransportPrefetchRequest::from_registered_request(
&crate::parallel::types::RepoIdentity::new(
Some("https://example.test/notify.xml".to_string()),
"rsync://example.test/repo/",
),
&crate::parallel::types::RepoRequester::with_tal_rir(
"arin",
"arin",
"rsync://example.test/repo/root.mft",
"rsync://example.test/repo/",
"arin:root",
),
0,
"rsync://example.test/repo/".to_string(),
Some("rsync://example.test/".to_string()),
SyncPreference::RrdpThenRsync,
)],
);
let stats = runtime
.prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
.expect("prefetch transport requests");
assert_eq!(stats.enqueued_tasks, 1);
assert!(
runtime.transport_prefetch_snapshot().requests.is_empty(),
"prefetch-only requests should not be carried forward forever"
);
}
#[test]
fn phase1_runtime_prefetch_skips_requests_when_scope_resolver_differs() {
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
SuccessTransportExecutor,
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new_with_failure_scope_and_prefetch_recording(
coordinator,
pool,
Arc::new(|_base: &str| "rsync://example.test/different/".to_string()),
Arc::new(|_base: &str| Some("rsync://example.test/".to_string())),
SyncPreference::RrdpThenRsync,
true,
);
let snapshot = TransportPrefetchSnapshot::new(
SyncPreference::RrdpThenRsync,
vec![crate::parallel::transport_prefetch::TransportPrefetchRequest::from_registered_request(
&crate::parallel::types::RepoIdentity::new(
Some("https://example.test/notify.xml".to_string()),
"rsync://example.test/repo/",
),
&crate::parallel::types::RepoRequester::with_tal_rir(
"arin",
"arin",
"rsync://example.test/repo/root.mft",
"rsync://example.test/repo/",
"arin:root",
),
0,
"rsync://example.test/repo/".to_string(),
Some("rsync://example.test/".to_string()),
SyncPreference::RrdpThenRsync,
)],
);
let stats = runtime
.prefetch_transport_requests(&snapshot, time::OffsetDateTime::UNIX_EPOCH)
.expect("prefetch transport requests");
assert_eq!(stats.loaded_requests, 1);
assert_eq!(stats.enqueued_tasks, 0);
assert_eq!(stats.skipped_incompatible, 1);
}
#[test]
fn phase1_runtime_transitions_rrdp_failure_to_rsync_success() {
let rrdp_count = Arc::new(AtomicUsize::new(0));
let rsync_count = Arc::new(AtomicUsize::new(0));
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
FailRrdpThenSucceedRsyncExecutor {
rrdp_count: Arc::clone(&rrdp_count),
rsync_count: Arc::clone(&rsync_count),
},
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|_base: &str| "rsync://example.test/module/".to_string()),
SyncPreference::RrdpThenRsync,
);
let outcome = runtime
.sync_publication_point_repo(&sample_ca("rsync://example.test/repo/root.mft"))
.expect("sync repo");
assert!(outcome.repo_sync_ok);
assert_eq!(outcome.repo_sync_source.as_deref(), Some("rsync"));
assert_eq!(
outcome.repo_sync_phase.as_deref(),
Some("rrdp_failed_rsync_ok")
);
assert_eq!(rrdp_count.load(Ordering::SeqCst), 1);
assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
}
#[test]
fn phase1_runtime_terminal_failure_keeps_rsync_failure_duration() {
let rrdp_count = Arc::new(AtomicUsize::new(0));
let rsync_count = Arc::new(AtomicUsize::new(0));
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
FailRrdpThenFailRsyncExecutor {
rrdp_count: Arc::clone(&rrdp_count),
rsync_count: Arc::clone(&rsync_count),
},
)
.expect("pool");
let runtime = Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|_base: &str| "rsync://example.test/module/".to_string()),
SyncPreference::RrdpThenRsync,
);
let outcome = runtime
.sync_publication_point_repo(&sample_ca("rsync://example.test/repo/root.mft"))
.expect("sync repo");
assert!(!outcome.repo_sync_ok);
assert_eq!(
outcome.repo_sync_phase.as_deref(),
Some("rrdp_failed_rsync_failed")
);
assert_eq!(outcome.repo_sync_duration_ms, 12);
assert_eq!(rrdp_count.load(Ordering::SeqCst), 1);
assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
}
#[test]
fn phase1_runtime_prefetch_submits_transport_task_before_consumption() {
let rrdp_count = Arc::new(AtomicUsize::new(0));
let rsync_count = Arc::new(AtomicUsize::new(0));
let coordinator = GlobalRunCoordinator::new(
ParallelPhase1Config::default(),
vec![TalInputSpec::from_url("https://example.test/arin.tal")],
);
let pool = RepoTransportWorkerPool::new(
RepoWorkerPoolConfig { max_workers: 1 },
FailRrdpThenSucceedRsyncExecutor {
rrdp_count: Arc::clone(&rrdp_count),
rsync_count: Arc::clone(&rsync_count),
},
)
.expect("pool");
let runtime = Arc::new(Phase1RepoSyncRuntime::new(
coordinator,
pool,
Arc::new(|_base: &str| "rsync://example.test/module/".to_string()),
SyncPreference::RrdpThenRsync,
));
let child = DiscoveredChildCaInstance {
handle: sample_ca("rsync://example.test/repo/child.mft"),
discovered_from: crate::audit::DiscoveredFrom {
parent_manifest_rsync_uri: "rsync://example.test/repo/root.mft".to_string(),
child_ca_certificate_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
child_ca_certificate_sha256_hex: "00".repeat(32),
},
child_entry_projection: None,
};
runtime
.prefetch_discovered_children(std::slice::from_ref(&child))
.expect("prefetch");
let started = Instant::now();
while rrdp_count.load(Ordering::SeqCst) == 0 && started.elapsed() < Duration::from_secs(1) {
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(rrdp_count.load(Ordering::SeqCst), 1);
let outcome = runtime
.sync_publication_point_repo(&child.handle)
.expect("sync child repo");
assert!(outcome.repo_sync_ok);
assert_eq!(rsync_count.load(Ordering::SeqCst), 1);
}
}