lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit cc57eabbc1e8bcd58bdd03d2a4d1ba46b4695a15
parent 437789fe6181ce636ac223ccef143d819c7488b9
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 12:40:45 +0000

runtime: validate delivery authority

- seal delivery topology and receipts behind validated construction
- enforce bounded queue lifecycle and preserve partial outcomes
- require verified inbound events with canonical transport evidence
- bind outbox profile validation, generated authority, and coverage

Diffstat:
Mcontracts/coverage-profiles.toml | 5+++++
Mcrates/outbox/contracts/migration_authority_v1.manifest.json | 8++++----
Mcrates/outbox/contracts/migration_authority_v1.manifest.sha256 | 2+-
Mcrates/outbox/src/error.rs | 3+++
Mcrates/outbox/src/store.rs | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mcrates/runtime/README | 2++
Mcrates/runtime/src/lib.rs | 1+
Mcrates/runtime/src/transport.rs | 1985+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
8 files changed, 1580 insertions(+), 526 deletions(-)

diff --git a/contracts/coverage-profiles.toml b/contracts/coverage-profiles.toml @@ -28,6 +28,11 @@ no_default_features = false features = ["blossom"] test_threads = 1 +[profiles.crates."radroots_runtime"] +no_default_features = true +features = ["transport", "transport-workers"] +test_threads = 1 + [profiles.crates."radroots_replica_sync"] no_default_features = false features = ["legacy-ingest"] diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.json b/crates/outbox/contracts/migration_authority_v1.manifest.json @@ -233,10 +233,10 @@ }, { "file": { - "byte_length": 11269, + "byte_length": 11376, "hash_algorithm": "sha256_bytes_v1", "path": "crates/outbox/src/error.rs", - "sha256": "496eca49b17431890251224b9d1818370a8174056f2dab57002f224dbfdb917d" + "sha256": "c54e9396c48629aecb5a2b8958ff83bb28f5a803b0e4efba734321917d42afe2" }, "role": "outbox_error_surface" }, @@ -278,10 +278,10 @@ }, { "file": { - "byte_length": 320734, + "byte_length": 323369, "hash_algorithm": "sha256_bytes_v1", "path": "crates/outbox/src/store.rs", - "sha256": "2fbc9690e98bc651953e081a57265bd9778c8dd124c9b26a02668c3bc0f14314" + "sha256": "2cfd5994923edd45e990f56a5e021152016611ba70e297858e187bb840a17216" }, "role": "store_integration" }, diff --git a/crates/outbox/contracts/migration_authority_v1.manifest.sha256 b/crates/outbox/contracts/migration_authority_v1.manifest.sha256 @@ -1 +1 @@ -6ba646dbd786b2b0a5b90adc915c5221c7b8e7c29d2ce99942e39469431f1494 +7756d99889ee5875664d1c831c0a430d3352dac2ab958b9abf4bdb8cd5a360a4 diff --git a/crates/outbox/src/error.rs b/crates/outbox/src/error.rs @@ -39,6 +39,9 @@ pub enum RadrootsOutboxError { #[error("transport profile id cannot be empty")] EmptyTransportProfileId, + #[error("transport profile id is not a bounded canonical identifier")] + InvalidTransportProfileId, + #[error("trade mutation drafts require the semantic trade mutation outbox API")] TradeMutationRequiresSemanticOutbox, diff --git a/crates/outbox/src/store.rs b/crates/outbox/src/store.rs @@ -1834,12 +1834,21 @@ fn prepare_delivery_plan( event_id: &str, input: &RadrootsOutboxDeliveryPlanInput, ) -> Result<PreparedDeliveryPlan, RadrootsOutboxError> { - if input.transport_profile_id.trim().is_empty() { + if input.transport_profile_id.is_empty() { return Err(RadrootsOutboxError::EmptyTransportProfileId); } + if input.transport_profile_id != input.transport_profile_id.trim() + || input + .transport_profile_id + .chars() + .any(|character| character.is_whitespace() || character.is_control()) + || input.transport_profile_id.len() + > radroots_transport::RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES + { + return Err(RadrootsOutboxError::InvalidTransportProfileId); + } let targets = input.targets.clone(); - let is_no_wait = input.satisfaction_policy == RadrootsTransportSatisfactionPolicy::no_wait(); - if targets.is_empty() && !is_no_wait { + if targets.is_empty() { return Err(RadrootsOutboxError::EmptyDeliveryTargets); } validate_unique_targets(&targets)?; @@ -1873,7 +1882,7 @@ fn prepare_delivery_plan( input.target_policy_version, ); Ok(PreparedDeliveryPlan { - transport_profile_id: input.transport_profile_id.trim().to_owned(), + transport_profile_id: input.transport_profile_id.clone(), target_policy_fingerprint, target_policy_version: input.target_policy_version, satisfaction_policy, @@ -3670,6 +3679,16 @@ mod tests { #[test] fn validation_and_persisted_value_parsers_reject_malformed_contract_data() { + let truly_empty_profile = RadrootsOutboxDeliveryPlanInput::new( + "", + 1, + RadrootsTransportSatisfactionPolicy::no_wait(), + vec![nostr_target(NOSTR_PRIMARY_WSS)], + ); + assert!(matches!( + prepare_delivery_plan("event-empty-profile", &truly_empty_profile), + Err(RadrootsOutboxError::EmptyTransportProfileId) + )); let empty_profile = RadrootsOutboxDeliveryPlanInput::new( " ", 1, @@ -3678,8 +3697,42 @@ mod tests { ); assert!(matches!( prepare_delivery_plan("event-empty-profile", &empty_profile), - Err(RadrootsOutboxError::EmptyTransportProfileId) + Err(RadrootsOutboxError::InvalidTransportProfileId) + )); + let whitespace_profile = RadrootsOutboxDeliveryPlanInput::new( + "transport nostr.local", + 1, + RadrootsTransportSatisfactionPolicy::no_wait(), + vec![nostr_target(NOSTR_PRIMARY_WSS)], + ); + assert!(matches!( + prepare_delivery_plan("event-whitespace-profile", &whitespace_profile), + Err(RadrootsOutboxError::InvalidTransportProfileId) )); + let oversized_profile = RadrootsOutboxDeliveryPlanInput::new( + "x".repeat(radroots_transport::RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES + 1), + 1, + RadrootsTransportSatisfactionPolicy::no_wait(), + vec![nostr_target(NOSTR_PRIMARY_WSS)], + ); + assert!(matches!( + prepare_delivery_plan("event-oversized-profile", &oversized_profile), + Err(RadrootsOutboxError::InvalidTransportProfileId) + )); + let exact_profile_id = + "x".repeat(radroots_transport::RADROOTS_TRANSPORT_IDENTIFIER_MAX_BYTES); + let exact_profile = RadrootsOutboxDeliveryPlanInput::new( + exact_profile_id.clone(), + 1, + RadrootsTransportSatisfactionPolicy::no_wait(), + vec![nostr_target(NOSTR_PRIMARY_WSS)], + ); + assert_eq!( + prepare_delivery_plan("event-exact-profile", &exact_profile) + .expect("exact profile boundary") + .transport_profile_id, + exact_profile_id + ); for (stored, required_count) in [ ("no_wait", 1), @@ -3913,7 +3966,10 @@ mod tests { RadrootsTransportSatisfactionPolicy::no_wait(), Vec::new(), ); - assert!(prepare_delivery_plan("event-no-wait", &empty_no_wait).is_ok()); + assert!(matches!( + prepare_delivery_plan("event-no-wait", &empty_no_wait), + Err(RadrootsOutboxError::EmptyDeliveryTargets) + )); } fn delivery_plan(targets: Vec<RadrootsTransportTarget>) -> RadrootsOutboxDeliveryPlanInput { @@ -5679,19 +5735,27 @@ mod tests { #[tokio::test] async fn enqueue_rejects_empty_delivery_targets_before_persistence() { let outbox = RadrootsOutbox::open_memory().await.expect("open"); - let draft = generic_draft(hex_64('a').as_str(), "hello"); - - let err = outbox - .enqueue_operation(RadrootsOutboxOperationInput::new( - "publish_post", - draft, - delivery_plan(Vec::new()), - 1_000, - )) - .await - .expect_err("empty targets"); + for policy in [ + RadrootsTransportSatisfactionPolicy::all_accepted(), + RadrootsTransportSatisfactionPolicy::no_wait(), + ] { + let err = outbox + .enqueue_operation(RadrootsOutboxOperationInput::new( + "publish_post", + generic_draft(hex_64('a').as_str(), "hello"), + RadrootsOutboxDeliveryPlanInput::new( + "transport.nostr.local", + 1, + policy, + Vec::new(), + ), + 1_000, + )) + .await + .expect_err("empty targets"); - assert!(matches!(err, RadrootsOutboxError::EmptyDeliveryTargets)); + assert!(matches!(err, RadrootsOutboxError::EmptyDeliveryTargets)); + } assert_eq!(table_count(&outbox, "outbox_operations").await, 0); assert_eq!(table_count(&outbox, "outbox_event").await, 0); assert_eq!(table_count(&outbox, "outbox_delivery_plan").await, 0); diff --git a/crates/runtime/README b/crates/runtime/README @@ -10,6 +10,8 @@ and service helpers for the `radroots` core libraries. crates; * service bootstrap, shutdown signal, backoff, and tracing initialization utilities; + * optional validated transport delivery jobs, bounded worker admission, partial + delivery receipts, and signature-verified inbound observation authority; * an optional CLI layer for configuration parsing and process startup. ## Copyright diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs @@ -44,6 +44,7 @@ pub use tracing::{ }; #[cfg(feature = "transport")] pub use transport::{ + RADROOTS_RUNTIME_DELIVERY_QUEUE_DEFAULT_CAPACITY, RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY, RadrootsRuntimeBoundedQueue, RadrootsRuntimeQueueStatus, RadrootsRuntimeQueueTask, RadrootsRuntimeTransportDispatchRequest, RadrootsRuntimeTransportError, RadrootsRuntimeTransportFuture, RadrootsRuntimeTransportPayload, diff --git a/crates/runtime/src/transport.rs b/crates/runtime/src/transport.rs @@ -2,6 +2,8 @@ use std::collections::{BTreeMap, VecDeque}; use std::future::Future; use std::pin::Pin; use std::sync::Arc; +#[cfg(feature = "transport-workers")] +use std::sync::{Mutex, MutexGuard}; use radroots_event::{draft::RadrootsVerifiedSignedEvent, wire::RadrootsNip01EventWire}; #[cfg(feature = "transport-workers")] @@ -9,13 +11,17 @@ use radroots_transport::RadrootsTransportTargetReceipt; use radroots_transport::{ RadrootsTransport, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportKind, RadrootsTransportPayload, - RadrootsTransportSatisfactionPolicy, RadrootsTransportTarget, RadrootsTransportTargetSet, + RadrootsTransportSatisfactionPolicy, RadrootsTransportSatisfactionPolicyKind, + RadrootsTransportTarget, RadrootsTransportTargetSet, }; use thiserror::Error; pub type RadrootsRuntimeTransportFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, RadrootsRuntimeTransportError>> + Send + 'a>>; +pub const RADROOTS_RUNTIME_DELIVERY_QUEUE_DEFAULT_CAPACITY: usize = 64; +pub const RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY: usize = 1_024; + #[derive(Debug, Error)] pub enum RadrootsRuntimeTransportError { #[error("transport `{0}` is not registered")] @@ -30,11 +36,52 @@ pub enum RadrootsRuntimeTransportError { #[error("transport target error: {0}")] TransportTarget(String), - #[error("runtime queue capacity must be greater than zero")] - InvalidQueueCapacity, + #[error("runtime queue capacity {actual} is outside the supported range 1..={max}")] + InvalidQueueCapacity { actual: usize, max: usize }, + + #[error("runtime queue capacity {capacity} is exhausted")] + QueueCapacityExhausted { capacity: usize }, + + #[error("runtime queue is shut down")] + QueueShutdown, + + #[error("runtime queue sequence space is exhausted")] + QueueSequenceExhausted, + + #[error("runtime queue completion has no matching in-flight task")] + QueueCompletionUnmatched, + + #[error("runtime queue state lock is poisoned")] + QueueStatePoisoned, + + #[error("runtime {field} id must be positive, received {value}")] + InvalidIdentifier { field: &'static str, value: i64 }, + + #[error("runtime {field} timestamp must not be negative, received {value}")] + InvalidTimestamp { field: &'static str, value: i64 }, + + #[error("runtime delivery job must contain at least one plan")] + EmptyDeliveryJob, + + #[error("runtime delivery plan must contain at least one target")] + EmptyDeliveryPlan, - #[error("inbound observation for event `{event_id}` is not verified")] - InboundObservationUnverified { event_id: String }, + #[error("runtime delivery job contains duplicate plan id {delivery_plan_id}")] + DuplicateDeliveryPlanId { delivery_plan_id: i64 }, + + #[error("runtime delivery plan contains duplicate target id {delivery_target_id}")] + DuplicateDeliveryTargetId { delivery_target_id: i64 }, + + #[error( + "runtime delivery target {delivery_target_id} has incoherent initial status {status:?}" + )] + InvalidDeliveryTargetInitialStatus { + delivery_target_id: i64, + status: RadrootsTransportDeliveryTargetStatus, + }, + + #[error("runtime lease {record_id} has incoherent claimed/recovered state")] + InvalidLeaseState { record_id: i64 }, #[error( "inbound observation event `{observation_event_id}` does not match signed event `{signed_event_id}`" @@ -46,6 +93,26 @@ pub enum RadrootsRuntimeTransportError { #[error("transport `{kind}` failed: {message}")] Transport { kind: String, message: String }, + + #[cfg(feature = "transport-workers")] + #[error("transport `{kind}` failed after a partial delivery outcome: {message}")] + PartialDelivery { + kind: String, + message: String, + partial_receipt: Box<RadrootsRuntimeDeliveryJobReceipt>, + }, +} + +impl RadrootsRuntimeTransportError { + #[cfg(feature = "transport-workers")] + pub fn partial_delivery_receipt(&self) -> Option<&RadrootsRuntimeDeliveryJobReceipt> { + match self { + Self::PartialDelivery { + partial_receipt, .. + } => Some(partial_receipt), + _ => None, + } + } } impl From<RadrootsTransportError> for RadrootsRuntimeTransportError { @@ -102,11 +169,11 @@ fn verify_signed_event_raw_json_matches_event( #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeTransportDispatchRequest { - pub request_id: String, - pub payload: RadrootsRuntimeTransportPayload, - pub target_set: RadrootsTransportTargetSet, - pub satisfaction_policy: RadrootsTransportSatisfactionPolicy, - pub now_ms: i64, + request_id: String, + payload: RadrootsRuntimeTransportPayload, + target_set: RadrootsTransportTargetSet, + satisfaction_policy: RadrootsTransportSatisfactionPolicy, + now_ms: i64, } impl RadrootsRuntimeTransportDispatchRequest { @@ -121,8 +188,16 @@ impl RadrootsRuntimeTransportDispatchRequest { return Err(RadrootsRuntimeTransportError::EmptyDispatchTargets); } let target_set = RadrootsTransportTargetSet::new(targets)?; + let request_id = request_id.into(); + RadrootsTransportDeliveryRequest::new( + request_id.clone(), + payload.transport_payload()?, + target_set.clone(), + satisfaction_policy.clone(), + )? + .try_with_now_ms(now_ms)?; Ok(Self { - request_id: request_id.into(), + request_id, payload, target_set, satisfaction_policy, @@ -141,6 +216,22 @@ impl RadrootsRuntimeTransportDispatchRequest { )? .try_with_now_ms(self.now_ms)?) } + + pub fn request_id(&self) -> &str { + self.request_id.as_str() + } + + pub fn target_set(&self) -> &RadrootsTransportTargetSet { + &self.target_set + } + + pub fn satisfaction_policy(&self) -> &RadrootsTransportSatisfactionPolicy { + &self.satisfaction_policy + } + + pub fn now_ms(&self) -> i64 { + self.now_ms + } } #[derive(Clone, Default)] @@ -183,22 +274,54 @@ impl RadrootsRuntimeTransportRegistry { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeQueueStatus { - pub capacity: usize, - pub queued: usize, - pub in_flight: usize, - pub shutdown: bool, + capacity: usize, + queued: usize, + in_flight: usize, + shutdown: bool, +} + +impl RadrootsRuntimeQueueStatus { + pub fn capacity(&self) -> usize { + self.capacity + } + + pub fn queued(&self) -> usize { + self.queued + } + + pub fn in_flight(&self) -> usize { + self.in_flight + } + + pub fn is_shutdown(&self) -> bool { + self.shutdown + } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeQueueTask<T> { - pub sequence: u64, - pub payload: T, + sequence: u64, + payload: T, +} + +impl<T> RadrootsRuntimeQueueTask<T> { + pub fn sequence(&self) -> u64 { + self.sequence + } + + pub fn payload(&self) -> &T { + &self.payload + } + + pub fn into_payload(self) -> T { + self.payload + } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeBoundedQueue<T> { capacity: usize, - next_sequence: u64, + next_sequence: Option<u64>, in_flight: usize, shutdown: bool, queue: VecDeque<RadrootsRuntimeQueueTask<T>>, @@ -206,37 +329,69 @@ pub struct RadrootsRuntimeBoundedQueue<T> { impl<T> RadrootsRuntimeBoundedQueue<T> { pub fn new(capacity: usize) -> Result<Self, RadrootsRuntimeTransportError> { - if capacity == 0 { - return Err(RadrootsRuntimeTransportError::InvalidQueueCapacity); + if !(1..=RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY).contains(&capacity) { + return Err(RadrootsRuntimeTransportError::InvalidQueueCapacity { + actual: capacity, + max: RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY, + }); } Ok(Self { capacity, - next_sequence: 1, + next_sequence: Some(1), in_flight: 0, shutdown: false, queue: VecDeque::new(), }) } - pub fn try_enqueue(&mut self, payload: T) -> Option<u64> { - if self.shutdown || self.queue.len() >= self.capacity { - return None; + pub fn try_enqueue(&mut self, payload: T) -> Result<u64, RadrootsRuntimeTransportError> { + if self.shutdown { + return Err(RadrootsRuntimeTransportError::QueueShutdown); } - let sequence = self.next_sequence; - self.next_sequence += 1; + let admitted = self.queue.len().checked_add(self.in_flight).ok_or( + RadrootsRuntimeTransportError::QueueCapacityExhausted { + capacity: self.capacity, + }, + )?; + if admitted >= self.capacity { + return Err(RadrootsRuntimeTransportError::QueueCapacityExhausted { + capacity: self.capacity, + }); + } + let sequence = self + .next_sequence + .ok_or(RadrootsRuntimeTransportError::QueueSequenceExhausted)?; + self.next_sequence = sequence.checked_add(1); self.queue .push_back(RadrootsRuntimeQueueTask { sequence, payload }); - Some(sequence) + Ok(sequence) } - pub fn pop(&mut self) -> Option<RadrootsRuntimeQueueTask<T>> { - let task = self.queue.pop_front()?; - self.in_flight += 1; - Some(task) + pub fn pop( + &mut self, + ) -> Result<Option<RadrootsRuntimeQueueTask<T>>, RadrootsRuntimeTransportError> { + if self.queue.is_empty() { + return Ok(None); + } + let next_in_flight = self.in_flight.checked_add(1).ok_or( + RadrootsRuntimeTransportError::QueueCapacityExhausted { + capacity: self.capacity, + }, + )?; + let task = self + .queue + .pop_front() + .ok_or(RadrootsRuntimeTransportError::QueueCompletionUnmatched)?; + self.in_flight = next_in_flight; + Ok(Some(task)) } - pub fn complete_task(&mut self) { - self.in_flight = self.in_flight.saturating_sub(1); + pub fn complete_task(&mut self) -> Result<(), RadrootsRuntimeTransportError> { + self.in_flight = self + .in_flight + .checked_sub(1) + .ok_or(RadrootsRuntimeTransportError::QueueCompletionUnmatched)?; + Ok(()) } pub fn shutdown(&mut self) { @@ -255,29 +410,81 @@ impl<T> RadrootsRuntimeBoundedQueue<T> { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeDeliveryTarget { - pub delivery_target_id: i64, - pub target: RadrootsTransportTarget, - pub status: RadrootsTransportDeliveryTargetStatus, + delivery_target_id: i64, + target: RadrootsTransportTarget, + status: RadrootsTransportDeliveryTargetStatus, } impl RadrootsRuntimeDeliveryTarget { - pub fn ready(delivery_target_id: i64, target: RadrootsTransportTarget) -> Self { - Self { + pub fn ready( + delivery_target_id: i64, + target: RadrootsTransportTarget, + ) -> Result<Self, RadrootsRuntimeTransportError> { + Self::with_initial_status( delivery_target_id, target, - status: RadrootsTransportDeliveryTargetStatus::Pending, - } + RadrootsTransportDeliveryTargetStatus::Pending, + ) } pub fn deferred_until_implemented( delivery_target_id: i64, target: RadrootsTransportTarget, - ) -> Self { - Self { + ) -> Result<Self, RadrootsRuntimeTransportError> { + Self::with_initial_status( + delivery_target_id, + target, + RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, + ) + } + + pub fn retryable( + delivery_target_id: i64, + target: RadrootsTransportTarget, + ) -> Result<Self, RadrootsRuntimeTransportError> { + Self::with_initial_status( delivery_target_id, target, - status: RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, + RadrootsTransportDeliveryTargetStatus::FailedRetryable, + ) + } + + fn with_initial_status( + delivery_target_id: i64, + target: RadrootsTransportTarget, + status: RadrootsTransportDeliveryTargetStatus, + ) -> Result<Self, RadrootsRuntimeTransportError> { + validate_positive_id("delivery target", delivery_target_id)?; + if !matches!( + status, + RadrootsTransportDeliveryTargetStatus::Pending + | RadrootsTransportDeliveryTargetStatus::FailedRetryable + | RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + ) { + return Err( + RadrootsRuntimeTransportError::InvalidDeliveryTargetInitialStatus { + delivery_target_id, + status, + }, + ); } + Ok(Self { + delivery_target_id, + target, + status, + }) + } + + pub fn delivery_target_id(&self) -> i64 { + self.delivery_target_id + } + + pub fn target(&self) -> &RadrootsTransportTarget { + &self.target + } + + pub fn status(&self) -> RadrootsTransportDeliveryTargetStatus { + self.status } pub fn is_ready_for_attempt(&self) -> bool { @@ -287,43 +494,175 @@ impl RadrootsRuntimeDeliveryTarget { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeDeliveryPlan { - pub delivery_plan_id: i64, - pub satisfaction_policy: RadrootsTransportSatisfactionPolicy, - pub targets: Vec<RadrootsRuntimeDeliveryTarget>, + delivery_plan_id: i64, + satisfaction_policy: RadrootsTransportSatisfactionPolicy, + targets: Vec<RadrootsRuntimeDeliveryTarget>, + required_target_count: usize, +} + +impl RadrootsRuntimeDeliveryPlan { + pub fn new( + delivery_plan_id: i64, + satisfaction_policy: RadrootsTransportSatisfactionPolicy, + targets: Vec<RadrootsRuntimeDeliveryTarget>, + ) -> Result<Self, RadrootsRuntimeTransportError> { + validate_positive_id("delivery plan", delivery_plan_id)?; + if targets.is_empty() { + return Err(RadrootsRuntimeTransportError::EmptyDeliveryPlan); + } + let mut target_ids = std::collections::BTreeSet::new(); + for target in &targets { + if !target_ids.insert(target.delivery_target_id) { + return Err(RadrootsRuntimeTransportError::DuplicateDeliveryTargetId { + delivery_target_id: target.delivery_target_id, + }); + } + } + let target_set = RadrootsTransportTargetSet::new( + targets.iter().map(|target| target.target.clone()).collect(), + )?; + satisfaction_policy.validate_for_target_set(&target_set)?; + let delivery_capable_target_count = targets + .iter() + .filter(|target| !target.status.is_deferred_until_implemented()) + .count(); + let satisfaction_target_count = if delivery_capable_target_count == 0 { + target_set.len() + } else { + delivery_capable_target_count + }; + let required_target_count = + satisfaction_policy.required_target_count(satisfaction_target_count)?; + Ok(Self { + delivery_plan_id, + satisfaction_policy, + targets, + required_target_count, + }) + } + + pub fn delivery_plan_id(&self) -> i64 { + self.delivery_plan_id + } + + pub fn satisfaction_policy(&self) -> &RadrootsTransportSatisfactionPolicy { + &self.satisfaction_policy + } + + pub fn targets(&self) -> &[RadrootsRuntimeDeliveryTarget] { + &self.targets + } + + pub fn required_target_count(&self) -> usize { + self.required_target_count + } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeDeliveryJob { - pub outbox_event_id: i64, - pub payload: RadrootsRuntimeTransportPayload, - pub plans: Vec<RadrootsRuntimeDeliveryPlan>, - pub now_ms: i64, + outbox_event_id: i64, + payload: RadrootsRuntimeTransportPayload, + plans: Vec<RadrootsRuntimeDeliveryPlan>, + now_ms: i64, +} + +impl RadrootsRuntimeDeliveryJob { + pub fn new( + outbox_event_id: i64, + payload: RadrootsRuntimeTransportPayload, + plans: Vec<RadrootsRuntimeDeliveryPlan>, + now_ms: i64, + ) -> Result<Self, RadrootsRuntimeTransportError> { + validate_positive_id("outbox event", outbox_event_id)?; + validate_timestamp("delivery job", now_ms)?; + payload.transport_payload()?; + if plans.is_empty() { + return Err(RadrootsRuntimeTransportError::EmptyDeliveryJob); + } + let mut plan_ids = std::collections::BTreeSet::new(); + for plan in &plans { + if !plan_ids.insert(plan.delivery_plan_id) { + return Err(RadrootsRuntimeTransportError::DuplicateDeliveryPlanId { + delivery_plan_id: plan.delivery_plan_id, + }); + } + } + Ok(Self { + outbox_event_id, + payload, + plans, + now_ms, + }) + } + + pub fn outbox_event_id(&self) -> i64 { + self.outbox_event_id + } + + pub fn payload(&self) -> &RadrootsRuntimeTransportPayload { + &self.payload + } + + pub fn plans(&self) -> &[RadrootsRuntimeDeliveryPlan] { + &self.plans + } + + pub fn now_ms(&self) -> i64 { + self.now_ms + } +} + +fn validate_positive_id( + field: &'static str, + value: i64, +) -> Result<(), RadrootsRuntimeTransportError> { + if value <= 0 { + return Err(RadrootsRuntimeTransportError::InvalidIdentifier { field, value }); + } + Ok(()) +} + +fn validate_timestamp( + field: &'static str, + value: i64, +) -> Result<(), RadrootsRuntimeTransportError> { + if value < 0 { + return Err(RadrootsRuntimeTransportError::InvalidTimestamp { field, value }); + } + Ok(()) } #[cfg(feature = "transport-workers")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeDeliveryWorkerConfig { - pub bounded_queue_capacity: usize, + bounded_queue_capacity: usize, +} + +#[cfg(feature = "transport-workers")] +impl RadrootsRuntimeDeliveryWorkerConfig { + pub fn new(bounded_queue_capacity: usize) -> Result<Self, RadrootsRuntimeTransportError> { + RadrootsRuntimeBoundedQueue::<()>::new(bounded_queue_capacity)?; + Ok(Self { + bounded_queue_capacity, + }) + } + + pub fn bounded_queue_capacity(&self) -> usize { + self.bounded_queue_capacity + } } #[cfg(feature = "transport-workers")] impl Default for RadrootsRuntimeDeliveryWorkerConfig { fn default() -> Self { Self { - bounded_queue_capacity: 64, + bounded_queue_capacity: RADROOTS_RUNTIME_DELIVERY_QUEUE_DEFAULT_CAPACITY, } } } #[cfg(feature = "transport-workers")] #[derive(Clone, Debug, PartialEq, Eq)] -struct RadrootsRuntimeDeliveryTargetState { - target: RadrootsTransportTarget, - status: RadrootsTransportDeliveryTargetStatus, -} - -#[cfg(feature = "transport-workers")] -#[derive(Clone, Debug, PartialEq, Eq)] pub enum RadrootsRuntimeDeliveryPlanSatisfactionState { Satisfied, Unsatisfied, @@ -332,70 +671,106 @@ pub enum RadrootsRuntimeDeliveryPlanSatisfactionState { #[cfg(feature = "transport-workers")] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeDeliveryPlanReceipt { - pub delivery_plan_id: i64, - pub satisfaction_policy: RadrootsTransportSatisfactionPolicy, - pub target_count: usize, - pub attempted_target_count: usize, - pub required_target_count: usize, - pub satisfied_target_count: usize, - pub satisfaction_state: RadrootsRuntimeDeliveryPlanSatisfactionState, - pub target_receipts: Vec<RadrootsTransportTargetReceipt>, + delivery_plan_id: i64, + satisfaction_policy: RadrootsTransportSatisfactionPolicy, + targets: Vec<RadrootsRuntimeDeliveryTarget>, + attempted_target_count: usize, + required_target_count: usize, + satisfied_target_count: usize, + satisfaction_state: RadrootsRuntimeDeliveryPlanSatisfactionState, + target_receipts: Vec<RadrootsTransportTargetReceipt>, } #[cfg(feature = "transport-workers")] impl RadrootsRuntimeDeliveryPlanReceipt { fn from_target_states( - delivery_plan_id: i64, - satisfaction_policy: RadrootsTransportSatisfactionPolicy, - target_states: &[RadrootsRuntimeDeliveryTargetState], + plan: &RadrootsRuntimeDeliveryPlan, + targets: Vec<RadrootsRuntimeDeliveryTarget>, target_receipts: Vec<RadrootsTransportTargetReceipt>, - ) -> Result<Self, RadrootsRuntimeTransportError> { - let required_target_count = - satisfaction_policy.required_target_count(target_states.len())?; + ) -> Self { let satisfied_target_count = - satisfied_target_count_for_policy(&satisfaction_policy, target_states); - let satisfaction_state = - if target_states_satisfy_policy(&satisfaction_policy, target_states)? { - RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied - } else { - RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied - }; - Ok(Self { - delivery_plan_id, - satisfaction_policy, - target_count: target_states.len(), + satisfied_target_count_for_policy(&plan.satisfaction_policy, &targets); + let satisfaction_state = if target_states_satisfy_policy( + &plan.satisfaction_policy, + plan.required_target_count, + &targets, + ) { + RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied + } else { + RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied + }; + Self { + delivery_plan_id: plan.delivery_plan_id, + satisfaction_policy: plan.satisfaction_policy.clone(), + targets, attempted_target_count: target_receipts.len(), - required_target_count, + required_target_count: plan.required_target_count, satisfied_target_count, satisfaction_state, target_receipts, - }) + } } pub fn is_satisfied(&self) -> bool { self.satisfaction_state == RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied } + + pub fn delivery_plan_id(&self) -> i64 { + self.delivery_plan_id + } + + pub fn satisfaction_policy(&self) -> &RadrootsTransportSatisfactionPolicy { + &self.satisfaction_policy + } + + pub fn targets(&self) -> &[RadrootsRuntimeDeliveryTarget] { + &self.targets + } + + pub fn target_count(&self) -> usize { + self.targets.len() + } + + pub fn attempted_target_count(&self) -> usize { + self.attempted_target_count + } + + pub fn required_target_count(&self) -> usize { + self.required_target_count + } + + pub fn satisfied_target_count(&self) -> usize { + self.satisfied_target_count + } + + pub fn satisfaction_state(&self) -> &RadrootsRuntimeDeliveryPlanSatisfactionState { + &self.satisfaction_state + } + + pub fn target_receipts(&self) -> &[RadrootsTransportTargetReceipt] { + &self.target_receipts + } } #[cfg(feature = "transport-workers")] fn satisfied_target_count_for_policy( policy: &RadrootsTransportSatisfactionPolicy, - target_states: &[RadrootsRuntimeDeliveryTargetState], + targets: &[RadrootsRuntimeDeliveryTarget], ) -> usize { let Some(class) = policy.target_satisfaction_class() else { return 0; }; match policy.required_target_fingerprints() { - None => target_states + None => targets .iter() - .filter(|state| state.status.counts_as_satisfied(class)) + .filter(|target| target.status.counts_as_satisfied(class)) .count(), - Some(targets) => targets + Some(required_targets) => required_targets .iter() .filter(|required| { - target_states.iter().any(|state| { - state.target.fingerprint() == *required - && state.status.counts_as_satisfied(class) + targets.iter().any(|target| { + target.target.fingerprint() == *required + && target.status.counts_as_satisfied(class) }) }) .count(), @@ -405,20 +780,15 @@ fn satisfied_target_count_for_policy( #[cfg(feature = "transport-workers")] fn target_states_satisfy_policy( policy: &RadrootsTransportSatisfactionPolicy, - target_states: &[RadrootsRuntimeDeliveryTargetState], -) -> Result<bool, RadrootsRuntimeTransportError> { - if policy.target_satisfaction_class().is_none() { - return Ok(true); + required_target_count: usize, + targets: &[RadrootsRuntimeDeliveryTarget], +) -> bool { + if policy.kind() == RadrootsTransportSatisfactionPolicyKind::NoWait { + return true; } match policy.required_target_fingerprints() { - None => Ok(policy.is_satisfied_by( - target_states.len(), - satisfied_target_count_for_policy(policy, target_states), - )?), - Some(targets) => { - policy.required_target_count(target_states.len())?; - Ok(satisfied_target_count_for_policy(policy, target_states) == targets.len()) - } + None => satisfied_target_count_for_policy(policy, targets) >= required_target_count, + Some(required) => satisfied_target_count_for_policy(policy, targets) == required.len(), } } @@ -435,17 +805,105 @@ fn dispatch_satisfaction_policy( #[cfg(feature = "transport-workers")] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeDeliveryJobReceipt { - pub outbox_event_id: i64, - pub dispatch_count: usize, - pub plan_receipts: Vec<RadrootsRuntimeDeliveryPlanReceipt>, - pub all_plans_satisfied: bool, - pub target_receipts: Vec<RadrootsTransportTargetReceipt>, + outbox_event_id: i64, + dispatch_count: usize, + plan_receipts: Vec<RadrootsRuntimeDeliveryPlanReceipt>, + all_plans_satisfied: bool, + target_receipts: Vec<RadrootsTransportTargetReceipt>, +} + +#[cfg(feature = "transport-workers")] +impl RadrootsRuntimeDeliveryJobReceipt { + fn new( + outbox_event_id: i64, + dispatch_count: usize, + plan_receipts: Vec<RadrootsRuntimeDeliveryPlanReceipt>, + ) -> Self { + let all_plans_satisfied = !plan_receipts.is_empty() + && plan_receipts + .iter() + .all(RadrootsRuntimeDeliveryPlanReceipt::is_satisfied); + let target_receipts = plan_receipts + .iter() + .flat_map(|receipt| receipt.target_receipts.iter().cloned()) + .collect(); + Self { + outbox_event_id, + dispatch_count, + plan_receipts, + all_plans_satisfied, + target_receipts, + } + } + + pub fn outbox_event_id(&self) -> i64 { + self.outbox_event_id + } + + pub fn dispatch_count(&self) -> usize { + self.dispatch_count + } + + pub fn plan_receipts(&self) -> &[RadrootsRuntimeDeliveryPlanReceipt] { + &self.plan_receipts + } + + pub fn all_plans_satisfied(&self) -> bool { + self.all_plans_satisfied + } + + pub fn target_receipts(&self) -> &[RadrootsTransportTargetReceipt] { + &self.target_receipts + } } #[cfg(feature = "transport-workers")] pub struct RadrootsRuntimeDeliveryWorker<'a> { registry: &'a RadrootsRuntimeTransportRegistry, - config: RadrootsRuntimeDeliveryWorkerConfig, + queue_capacity: usize, + queue: Mutex<RadrootsRuntimeBoundedQueue<()>>, +} + +#[cfg(feature = "transport-workers")] +struct RadrootsRuntimeQueueAdmission<'a> { + queue: &'a Mutex<RadrootsRuntimeBoundedQueue<()>>, + active: bool, +} + +#[cfg(feature = "transport-workers")] +impl RadrootsRuntimeQueueAdmission<'_> { + fn complete(mut self) -> Result<(), RadrootsRuntimeTransportError> { + lock_runtime_queue(self.queue)?.complete_task()?; + self.active = false; + Ok(()) + } +} + +#[cfg(feature = "transport-workers")] +impl Drop for RadrootsRuntimeQueueAdmission<'_> { + fn drop(&mut self) { + if !self.active { + return; + } + match self.queue.lock() { + Ok(mut queue) => { + let _ = queue.complete_task(); + } + Err(poisoned) => { + let mut queue = poisoned.into_inner(); + let _ = queue.complete_task(); + } + } + } +} + +#[cfg(feature = "transport-workers")] +fn lock_runtime_queue( + queue: &Mutex<RadrootsRuntimeBoundedQueue<()>>, +) -> Result<MutexGuard<'_, RadrootsRuntimeBoundedQueue<()>>, RadrootsRuntimeTransportError> { + queue + .lock() + .map_err(|_| RadrootsRuntimeTransportError::QueueStatePoisoned) } #[cfg(feature = "transport-workers")] @@ -453,128 +911,317 @@ impl<'a> RadrootsRuntimeDeliveryWorker<'a> { pub fn new( registry: &'a RadrootsRuntimeTransportRegistry, config: RadrootsRuntimeDeliveryWorkerConfig, - ) -> Self { - Self { registry, config } + ) -> Result<Self, RadrootsRuntimeTransportError> { + let queue = RadrootsRuntimeBoundedQueue::new(config.bounded_queue_capacity)?; + Ok(Self { + registry, + queue_capacity: config.bounded_queue_capacity, + queue: Mutex::new(queue), + }) } pub fn queue_capacity(&self) -> usize { - self.config.bounded_queue_capacity + self.queue_capacity + } + + pub fn queue_status( + &self, + ) -> Result<RadrootsRuntimeQueueStatus, RadrootsRuntimeTransportError> { + Ok(lock_runtime_queue(&self.queue)?.status()) + } + + fn admit(&self) -> Result<RadrootsRuntimeQueueAdmission<'_>, RadrootsRuntimeTransportError> { + let mut queue = lock_runtime_queue(&self.queue)?; + let sequence = queue.try_enqueue(())?; + let task = queue + .pop()? + .ok_or(RadrootsRuntimeTransportError::QueueCompletionUnmatched)?; + if task.sequence() != sequence { + queue.complete_task()?; + return Err(RadrootsRuntimeTransportError::QueueCompletionUnmatched); + } + drop(queue); + Ok(RadrootsRuntimeQueueAdmission { + queue: &self.queue, + active: true, + }) } pub async fn execute_job( &self, job: RadrootsRuntimeDeliveryJob, ) -> Result<RadrootsRuntimeDeliveryJobReceipt, RadrootsRuntimeTransportError> { - let mut target_receipts = Vec::new(); + let admission = self.admit()?; + let result = self.execute_admitted_job(job).await; + admission.complete()?; + result + } + + async fn execute_admitted_job( + &self, + job: RadrootsRuntimeDeliveryJob, + ) -> Result<RadrootsRuntimeDeliveryJobReceipt, RadrootsRuntimeTransportError> { + let outbox_event_id = job.outbox_event_id; + let payload = job.payload; + let now_ms = job.now_ms; let mut plan_receipts = Vec::new(); let mut dispatch_count = 0usize; for plan in job.plans { - let delivery_plan_id = plan.delivery_plan_id; - let satisfaction_policy = plan.satisfaction_policy.clone(); - let dispatch_satisfaction_policy = dispatch_satisfaction_policy(&satisfaction_policy); - let mut target_states = plan - .targets + let dispatch_satisfaction_policy = + dispatch_satisfaction_policy(&plan.satisfaction_policy); + let mut target_states = plan.targets.clone(); + let target_positions = target_states .iter() - .map(|target| { - ( - target.target.fingerprint().as_str().to_owned(), - RadrootsRuntimeDeliveryTargetState { - target: target.target.clone(), - status: target.status, - }, - ) - }) + .enumerate() + .map(|(index, target)| (target.target.fingerprint().as_str().to_owned(), index)) .collect::<BTreeMap<_, _>>(); let mut by_kind = BTreeMap::<RadrootsTransportKind, Vec<RadrootsRuntimeDeliveryTarget>>::new(); - for target in plan - .targets - .into_iter() - .filter(RadrootsRuntimeDeliveryTarget::is_ready_for_attempt) - { - by_kind - .entry(target.target.kind().clone()) - .or_default() - .push(target); + if plan.satisfaction_policy.kind() != RadrootsTransportSatisfactionPolicyKind::NoWait { + for target in plan + .targets + .iter() + .filter(|target| target.is_ready_for_attempt()) + .cloned() + { + by_kind + .entry(target.target.kind().clone()) + .or_default() + .push(target); + } } let mut plan_target_receipts = Vec::new(); for (kind, targets) in by_kind { - let transport = self.registry.transport(&kind)?; + let transport = match self.registry.transport(&kind) { + Ok(transport) => transport, + Err(error) => { + return Err(partial_delivery_error( + &kind, + error.to_string(), + &plan, + PartialDeliveryState { + outbox_event_id, + dispatch_count, + completed_plan_receipts: plan_receipts, + current_targets: target_states, + current_target_receipts: plan_target_receipts, + }, + )); + } + }; let transport_targets = targets .iter() .map(|target| target.target.clone()) .collect::<Vec<_>>(); - let request = RadrootsRuntimeTransportDispatchRequest::new( + let request = match RadrootsRuntimeTransportDispatchRequest::new( format!( "outbox-event-{}-plan-{}", - job.outbox_event_id, plan.delivery_plan_id + outbox_event_id, plan.delivery_plan_id ), - job.payload.clone(), + payload.clone(), transport_targets, dispatch_satisfaction_policy.clone(), - job.now_ms, - )?; - let delivery_request = request.transport_delivery_request()?; - let receipt = - transport - .deliver(delivery_request.clone()) - .await - .map_err(|error| RadrootsRuntimeTransportError::Transport { - kind: kind.canonical_label(), - message: error.to_string(), - })?; - receipt - .validate_for_request(&delivery_request) - .map_err(|error| RadrootsRuntimeTransportError::Transport { - kind: kind.canonical_label(), - message: error.to_string(), - })?; - for target_receipt in receipt.target_receipts().iter().cloned() { - target_states.insert( - target_receipt.target().fingerprint().as_str().to_owned(), - RadrootsRuntimeDeliveryTargetState { - target: target_receipt.target().clone(), - status: target_receipt.status(), + now_ms, + ) { + Ok(request) => request, + Err(error) => { + return Err(partial_delivery_error( + &kind, + error.to_string(), + &plan, + PartialDeliveryState { + outbox_event_id, + dispatch_count, + completed_plan_receipts: plan_receipts, + current_targets: target_states, + current_target_receipts: plan_target_receipts, + }, + )); + } + }; + let delivery_request = match request.transport_delivery_request() { + Ok(request) => request, + Err(error) => { + return Err(partial_delivery_error( + &kind, + error.to_string(), + &plan, + PartialDeliveryState { + outbox_event_id, + dispatch_count, + completed_plan_receipts: plan_receipts, + current_targets: target_states, + current_target_receipts: plan_target_receipts, + }, + )); + } + }; + dispatch_count += 1; + let receipt = match transport.deliver(delivery_request.clone()).await { + Ok(receipt) => receipt, + Err(error) => { + return Err(partial_delivery_error( + &kind, + error.to_string(), + &plan, + PartialDeliveryState { + outbox_event_id, + dispatch_count, + completed_plan_receipts: plan_receipts, + current_targets: target_states, + current_target_receipts: plan_target_receipts, + }, + )); + } + }; + if let Err(error) = receipt.validate_for_request(&delivery_request) { + return Err(partial_delivery_error( + &kind, + error.to_string(), + &plan, + PartialDeliveryState { + outbox_event_id, + dispatch_count, + completed_plan_receipts: plan_receipts, + current_targets: target_states, + current_target_receipts: plan_target_receipts, }, - ); + )); + } + for target_receipt in receipt.target_receipts().iter().cloned() { + let Some(index) = target_positions + .get(target_receipt.target().fingerprint().as_str()) + .copied() + else { + return Err(partial_delivery_error( + &kind, + "delivery receipt target is not present in the validated plan" + .to_owned(), + &plan, + PartialDeliveryState { + outbox_event_id, + dispatch_count, + completed_plan_receipts: plan_receipts, + current_targets: target_states, + current_target_receipts: plan_target_receipts, + }, + )); + }; + target_states[index].status = target_receipt.status(); plan_target_receipts.push(target_receipt); } - dispatch_count += 1; } - let final_target_states = target_states.into_values().collect::<Vec<_>>(); let plan_receipt = RadrootsRuntimeDeliveryPlanReceipt::from_target_states( - delivery_plan_id, - satisfaction_policy, - &final_target_states, + &plan, + target_states, plan_target_receipts, - )?; - target_receipts.extend(plan_receipt.target_receipts.iter().cloned()); + ); plan_receipts.push(plan_receipt); } - let all_plans_satisfied = plan_receipts - .iter() - .all(RadrootsRuntimeDeliveryPlanReceipt::is_satisfied); - Ok(RadrootsRuntimeDeliveryJobReceipt { - outbox_event_id: job.outbox_event_id, + Ok(RadrootsRuntimeDeliveryJobReceipt::new( + outbox_event_id, dispatch_count, plan_receipts, - all_plans_satisfied, - target_receipts, - }) + )) + } +} + +#[cfg(feature = "transport-workers")] +struct PartialDeliveryState { + outbox_event_id: i64, + dispatch_count: usize, + completed_plan_receipts: Vec<RadrootsRuntimeDeliveryPlanReceipt>, + current_targets: Vec<RadrootsRuntimeDeliveryTarget>, + current_target_receipts: Vec<RadrootsTransportTargetReceipt>, +} + +#[cfg(feature = "transport-workers")] +fn partial_delivery_error( + kind: &RadrootsTransportKind, + message: String, + current_plan: &RadrootsRuntimeDeliveryPlan, + mut state: PartialDeliveryState, +) -> RadrootsRuntimeTransportError { + state + .completed_plan_receipts + .push(RadrootsRuntimeDeliveryPlanReceipt::from_target_states( + current_plan, + state.current_targets, + state.current_target_receipts, + )); + RadrootsRuntimeTransportError::PartialDelivery { + kind: kind.canonical_label(), + message, + partial_receipt: Box::new(RadrootsRuntimeDeliveryJobReceipt::new( + state.outbox_event_id, + state.dispatch_count, + state.completed_plan_receipts, + )), } } #[cfg(feature = "transport-workers")] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeLeaseRecord { - pub record_id: i64, - pub claimed: bool, - pub claim_expires_at_ms: i64, - pub recovered: bool, + record_id: i64, + claimed: bool, + claim_expires_at_ms: i64, + recovered: bool, +} + +#[cfg(feature = "transport-workers")] +impl RadrootsRuntimeLeaseRecord { + pub fn claimed( + record_id: i64, + claim_expires_at_ms: i64, + ) -> Result<Self, RadrootsRuntimeTransportError> { + validate_positive_id("lease record", record_id)?; + validate_timestamp("lease expiry", claim_expires_at_ms)?; + Ok(Self { + record_id, + claimed: true, + claim_expires_at_ms, + recovered: false, + }) + } + + pub fn record_id(&self) -> i64 { + self.record_id + } + + pub fn is_claimed(&self) -> bool { + self.claimed + } + + pub fn claim_expires_at_ms(&self) -> i64 { + self.claim_expires_at_ms + } + + pub fn is_recovered(&self) -> bool { + self.recovered + } + + fn validate(&self) -> Result<(), RadrootsRuntimeTransportError> { + validate_positive_id("lease record", self.record_id)?; + validate_timestamp("lease expiry", self.claim_expires_at_ms)?; + if self.claimed && self.recovered { + return Err(RadrootsRuntimeTransportError::InvalidLeaseState { + record_id: self.record_id, + }); + } + Ok(()) + } } #[cfg(feature = "transport-workers")] -pub fn recover_expired_leases(leases: &mut [RadrootsRuntimeLeaseRecord], now_ms: i64) -> usize { +pub fn recover_expired_leases( + leases: &mut [RadrootsRuntimeLeaseRecord], + now_ms: i64, +) -> Result<usize, RadrootsRuntimeTransportError> { + validate_timestamp("lease recovery", now_ms)?; + for lease in leases.iter() { + lease.validate()?; + } let mut recovered = 0usize; for lease in leases { if lease.claimed && lease.claim_expires_at_ms <= now_ms { @@ -583,47 +1230,48 @@ pub fn recover_expired_leases(leases: &mut [RadrootsRuntimeLeaseRecord], now_ms: recovered += 1; } } - recovered + Ok(recovered) } #[cfg(feature = "transport-workers")] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsRuntimeInboundObservation { - pub event_id: String, - pub verified: bool, - pub transport_kind: RadrootsTransportKind, - pub endpoint_uri: String, - pub observed_at_ms: i64, + event_id: String, + transport_target: RadrootsTransportTarget, + observed_at_ms: i64, } #[cfg(feature = "transport-workers")] impl RadrootsRuntimeInboundObservation { pub fn verified_signed_event( event: &RadrootsVerifiedSignedEvent, - transport_kind: RadrootsTransportKind, - endpoint_uri: impl Into<String>, + transport_target: RadrootsTransportTarget, observed_at_ms: i64, - ) -> Self { - Self { + ) -> Result<Self, RadrootsRuntimeTransportError> { + validate_timestamp("inbound observation", observed_at_ms)?; + Ok(Self { event_id: event.signed_event().id_str().to_owned(), - verified: true, - transport_kind, - endpoint_uri: endpoint_uri.into(), + transport_target, observed_at_ms, - } + }) + } + + pub fn event_id(&self) -> &str { + self.event_id.as_str() + } + + pub fn transport_target(&self) -> &RadrootsTransportTarget { + &self.transport_target + } + + pub fn observed_at_ms(&self) -> i64 { + self.observed_at_ms } pub fn require_verified_for_signed_event( &self, event: &RadrootsVerifiedSignedEvent, ) -> Result<(), RadrootsRuntimeTransportError> { - if !self.verified { - return Err( - RadrootsRuntimeTransportError::InboundObservationUnverified { - event_id: self.event_id.clone(), - }, - ); - } if self.event_id != event.signed_event().id_str() { return Err( RadrootsRuntimeTransportError::InboundObservationEventMismatch { @@ -663,9 +1311,11 @@ where #[cfg(test)] mod tests { use super::{ - RadrootsRuntimeBoundedQueue, RadrootsRuntimeTransportDispatchRequest, - RadrootsRuntimeTransportError, RadrootsRuntimeTransportFuture, - RadrootsRuntimeTransportPayload, RadrootsRuntimeTransportRegistry, + RADROOTS_RUNTIME_DELIVERY_QUEUE_DEFAULT_CAPACITY, + RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY, RadrootsRuntimeBoundedQueue, + RadrootsRuntimeTransportDispatchRequest, RadrootsRuntimeTransportError, + RadrootsRuntimeTransportFuture, RadrootsRuntimeTransportPayload, + RadrootsRuntimeTransportRegistry, }; #[cfg(feature = "transport-workers")] use super::{ @@ -681,8 +1331,7 @@ mod tests { wire::RadrootsNip01EventWire, }; use radroots_transport::{ - RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportCapabilityAvailability, - RadrootsTransportCapabilityMaturity, RadrootsTransportDeliveryReceipt, + RadrootsTransport, RadrootsTransportCapabilities, RadrootsTransportDeliveryReceipt, RadrootsTransportDeliveryRequest, RadrootsTransportDeliveryTargetStatus, RadrootsTransportError, RadrootsTransportFetchReceipt, RadrootsTransportFetchRequest, RadrootsTransportFuture, RadrootsTransportImplementationState, RadrootsTransportKind, @@ -690,6 +1339,10 @@ mod tests { RadrootsTransportSatisfactionPolicy, RadrootsTransportStatus, RadrootsTransportTarget, RadrootsTransportTargetReceipt, RadrootsTransportTargetSet, }; + #[cfg(feature = "transport-reticulum")] + use radroots_transport::{ + RadrootsTransportCapabilityAvailability, RadrootsTransportCapabilityMaturity, + }; #[cfg(feature = "transport-workers")] use std::sync::{Arc, Mutex}; @@ -882,6 +1535,42 @@ mod tests { } } + #[cfg(feature = "transport-workers")] + struct PendingTransport; + + #[cfg(feature = "transport-workers")] + impl RadrootsTransport for PendingTransport { + fn transport_kind(&self) -> RadrootsTransportKind { + RadrootsTransportKind::Nostr + } + + fn status<'a>(&'a self) -> RadrootsTransportFuture<'a, RadrootsTransportStatus> { + Box::pin(async { + RadrootsTransportStatus::new( + RadrootsTransportKind::Nostr, + true, + RadrootsTransportImplementationState::Real, + true, + "pending transport fixture", + ) + }) + } + + fn deliver<'a>( + &'a self, + _request: RadrootsTransportDeliveryRequest, + ) -> RadrootsTransportFuture<'a, RadrootsTransportDeliveryReceipt> { + Box::pin(std::future::pending()) + } + + fn fetch<'a>( + &'a self, + _request: RadrootsTransportFetchRequest, + ) -> RadrootsTransportFuture<'a, RadrootsTransportFetchReceipt> { + Box::pin(async { Err(RadrootsTransportError::UnsupportedOperation) }) + } + } + fn target(kind: RadrootsTransportKind, uri: &str) -> RadrootsTransportTarget { RadrootsTransportTarget::new(kind, uri).expect("target") } @@ -894,6 +1583,57 @@ mod tests { } #[cfg(feature = "transport-workers")] + fn delivery_target( + delivery_target_id: i64, + kind: RadrootsTransportKind, + uri: &str, + ) -> RadrootsRuntimeDeliveryTarget { + RadrootsRuntimeDeliveryTarget::ready(delivery_target_id, target(kind, uri)) + .expect("delivery target") + } + + #[cfg(feature = "transport-workers")] + fn deferred_delivery_target( + delivery_target_id: i64, + kind: RadrootsTransportKind, + uri: &str, + ) -> RadrootsRuntimeDeliveryTarget { + RadrootsRuntimeDeliveryTarget::deferred_until_implemented( + delivery_target_id, + target(kind, uri), + ) + .expect("deferred delivery target") + } + + #[cfg(feature = "transport-workers")] + fn delivery_plan( + satisfaction_policy: RadrootsTransportSatisfactionPolicy, + targets: Vec<RadrootsRuntimeDeliveryTarget>, + ) -> RadrootsRuntimeDeliveryPlan { + RadrootsRuntimeDeliveryPlan::new(7, satisfaction_policy, targets).expect("delivery plan") + } + + #[cfg(feature = "transport-workers")] + fn delivery_job( + plans: Vec<RadrootsRuntimeDeliveryPlan>, + now_ms: i64, + ) -> RadrootsRuntimeDeliveryJob { + RadrootsRuntimeDeliveryJob::new(42, opaque_payload(), plans, now_ms).expect("delivery job") + } + + #[cfg(feature = "transport-workers")] + fn delivery_worker( + registry: &RadrootsRuntimeTransportRegistry, + capacity: usize, + ) -> RadrootsRuntimeDeliveryWorker<'_> { + RadrootsRuntimeDeliveryWorker::new( + registry, + RadrootsRuntimeDeliveryWorkerConfig::new(capacity).expect("worker config"), + ) + .expect("delivery worker") + } + + #[cfg(feature = "transport-workers")] fn signed_event() -> RadrootsVerifiedSignedEvent { let raw_json = r#"{"id":"fb3f42caf9db337a7f1c0d49cd8ba5191f08dc1c419ed0640f7ea48a924e3bf3","pubkey":"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","created_at":1781632860,"kind":1,"tags":[],"content":"The first strawberries are ready.","sig":"dba0a86fee54304c2b419742f186e74d7edca5fc7234c8aa294651de9bc2f16bf829d46f36ec759a767c4ccd1841a73243eae89afd5f6c89b2243491bfbb5f50"}"#.to_owned(); let wire = @@ -936,8 +1676,11 @@ mod tests { ) -> RadrootsRuntimeTransportFuture<'a, ()> { Box::pin(async move { assert_eq!(event.signed_event().id_str(), self.expected_event_id); - assert_eq!(observation.event_id, self.expected_event_id); - assert!(observation.verified); + assert_eq!(observation.event_id(), self.expected_event_id); + assert_eq!( + observation.transport_target().uri().as_str(), + "wss://relay.example" + ); Ok(()) }) } @@ -1035,7 +1778,7 @@ mod tests { #[cfg(feature = "transport-reticulum")] #[tokio::test] - async fn registry_dispatches_reticulum_transport_without_success() { + async fn registry_preserves_default_reticulum_reject_behavior() { let mut registry = RadrootsRuntimeTransportRegistry::new(); registry .register(radroots_transport_reticulum::RadrootsReticulumTransport::default()) @@ -1096,23 +1839,86 @@ mod tests { ); assert_eq!( receipt.target_receipts()[0].status(), - RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented + RadrootsTransportDeliveryTargetStatus::FailedRetryable ); } #[test] fn bounded_queue_tracks_capacity_inflight_and_shutdown() { let mut queue = RadrootsRuntimeBoundedQueue::new(2).expect("queue"); - assert_eq!(queue.try_enqueue("a"), Some(1)); - assert_eq!(queue.try_enqueue("b"), Some(2)); - assert_eq!(queue.try_enqueue("c"), None); - let task = queue.pop().expect("task"); - assert_eq!(task.sequence, 1); - assert_eq!(queue.status().in_flight, 1); - queue.complete_task(); + assert_eq!(queue.try_enqueue("a").expect("enqueue"), 1); + assert_eq!(queue.try_enqueue("b").expect("enqueue"), 2); + assert!(matches!( + queue.try_enqueue("c"), + Err(RadrootsRuntimeTransportError::QueueCapacityExhausted { capacity: 2 }) + )); + let task = queue.pop().expect("pop").expect("task"); + assert_eq!(task.sequence(), 1); + assert_eq!(task.payload(), &"a"); + assert_eq!(queue.status().in_flight(), 1); + assert!(matches!( + queue.try_enqueue("c"), + Err(RadrootsRuntimeTransportError::QueueCapacityExhausted { capacity: 2 }) + )); + queue.complete_task().expect("complete"); + assert_eq!(queue.try_enqueue("c").expect("enqueue"), 3); queue.shutdown(); - assert_eq!(queue.try_enqueue("d"), None); - assert!(queue.status().shutdown); + assert!(matches!( + queue.try_enqueue("d"), + Err(RadrootsRuntimeTransportError::QueueShutdown) + )); + assert!(queue.status().is_shutdown()); + assert!(matches!( + RadrootsRuntimeBoundedQueue::<()>::new(0), + Err(RadrootsRuntimeTransportError::InvalidQueueCapacity { actual: 0, .. }) + )); + assert!(matches!( + RadrootsRuntimeBoundedQueue::<()>::new( + RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY + 1 + ), + Err(RadrootsRuntimeTransportError::InvalidQueueCapacity { .. }) + )); + RadrootsRuntimeBoundedQueue::<()>::new(RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY) + .expect("maximum queue"); + } + + #[test] + fn bounded_queue_reports_sequence_exhaustion_and_unmatched_completion() { + let mut queue = RadrootsRuntimeBoundedQueue::new(1).expect("queue"); + assert!(matches!( + queue.complete_task(), + Err(RadrootsRuntimeTransportError::QueueCompletionUnmatched) + )); + queue.next_sequence = Some(u64::MAX); + assert_eq!(queue.try_enqueue("last").expect("last sequence"), u64::MAX); + let task = queue.pop().expect("pop").expect("task"); + assert_eq!(task.sequence(), u64::MAX); + queue.complete_task().expect("complete"); + assert!(matches!( + queue.try_enqueue("overflow"), + Err(RadrootsRuntimeTransportError::QueueSequenceExhausted) + )); + } + + #[cfg(feature = "transport-workers")] + #[test] + fn delivery_worker_config_uses_governed_capacity_range() { + assert_eq!( + RadrootsRuntimeDeliveryWorkerConfig::default().bounded_queue_capacity(), + RADROOTS_RUNTIME_DELIVERY_QUEUE_DEFAULT_CAPACITY + ); + RadrootsRuntimeDeliveryWorkerConfig::new(RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY) + .expect("maximum worker capacity"); + assert!(matches!( + RadrootsRuntimeDeliveryWorkerConfig::new(0), + Err(RadrootsRuntimeTransportError::InvalidQueueCapacity { actual: 0, .. }) + )); + assert!(matches!( + RadrootsRuntimeDeliveryWorkerConfig::new( + RADROOTS_RUNTIME_DELIVERY_QUEUE_MAX_CAPACITY + 1, + ), + Err(RadrootsRuntimeTransportError::InvalidQueueCapacity { .. }) + )); } #[cfg(feature = "transport-workers")] @@ -1125,52 +1931,43 @@ mod tests { RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); - let ready = RadrootsRuntimeDeliveryTarget::ready( - 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), - ); - let deferred = RadrootsRuntimeDeliveryTarget::deferred_until_implemented( - 2, - target(RadrootsTransportKind::Reticulum, "reticulum:local"), - ); + let worker = delivery_worker(&registry, 8); + let ready = delivery_target(1, RadrootsTransportKind::Nostr, "wss://relay.example"); + let deferred = + deferred_delivery_target(2, RadrootsTransportKind::Reticulum, "reticulum:local"); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_accepted(), - targets: vec![ready, deferred], - }], - now_ms: 1_000, - }) + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::any_accepted(), + vec![ready, deferred], + )], + 1_000, + )) .await .expect("worker receipt"); assert_eq!(worker.queue_capacity(), 8); - assert_eq!(receipt.dispatch_count, 1); - assert!(receipt.all_plans_satisfied); - assert_eq!(receipt.plan_receipts.len(), 1); - assert_eq!(receipt.plan_receipts[0].delivery_plan_id, 7); - assert_eq!(receipt.plan_receipts[0].target_count, 2); - assert_eq!(receipt.plan_receipts[0].attempted_target_count, 1); - assert_eq!(receipt.plan_receipts[0].required_target_count, 1); - assert_eq!(receipt.plan_receipts[0].satisfied_target_count, 1); + assert_eq!(receipt.dispatch_count(), 1); + assert!(receipt.all_plans_satisfied()); + assert_eq!(receipt.plan_receipts().len(), 1); + let plan_receipt = &receipt.plan_receipts()[0]; + assert_eq!(plan_receipt.delivery_plan_id(), 7); + assert_eq!(plan_receipt.target_count(), 2); + assert_eq!(plan_receipt.targets()[0].delivery_target_id(), 1); + assert_eq!(plan_receipt.targets()[1].delivery_target_id(), 2); + assert_eq!(plan_receipt.attempted_target_count(), 1); + assert_eq!(plan_receipt.required_target_count(), 1); + assert_eq!(plan_receipt.satisfied_target_count(), 1); assert_eq!( - receipt.plan_receipts[0].satisfaction_state, - RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied + plan_receipt.satisfaction_state(), + &RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied ); - assert_eq!(receipt.target_receipts.len(), 1); + assert_eq!(receipt.target_receipts().len(), 1); assert_eq!( - receipt.target_receipts[0].status(), + receipt.target_receipts()[0].status(), RadrootsTransportDeliveryTargetStatus::Accepted ); + assert_eq!(worker.queue_status().expect("queue status").in_flight(), 0); } #[cfg(feature = "transport-workers")] @@ -1182,30 +1979,23 @@ mod tests { RadrootsTransportOutcomeKind::Accepted, ); registry.register(transport).expect("register"); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); + let worker = delivery_worker(&registry, 8); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_accepted(), - targets: vec![RadrootsRuntimeDeliveryTarget::ready( + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::any_accepted(), + vec![delivery_target( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + RadrootsTransportKind::Nostr, + "wss://relay.example", )], - }], - now_ms: 987_654, - }) + )], + 987_654, + )) .await .expect("worker receipt"); - assert_eq!(receipt.dispatch_count, 1); + assert_eq!(receipt.dispatch_count(), 1); assert_eq!( captured_now_ms.lock().expect("now_ms capture").as_slice(), &[987_654] @@ -1223,33 +2013,29 @@ mod tests { registry .register(ForgedReceiptTransport { forged }) .expect("register"); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); + let worker = delivery_worker(&registry, 8); let error = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_accepted(), - targets: vec![RadrootsRuntimeDeliveryTarget::ready( + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::any_accepted(), + vec![delivery_target( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + RadrootsTransportKind::Nostr, + "wss://relay.example", )], - }], - now_ms: 1_000, - }) + )], + 1_000, + )) .await .expect_err("forged receipt rejected"); assert!(matches!( error, - RadrootsRuntimeTransportError::Transport { .. } + RadrootsRuntimeTransportError::PartialDelivery { .. } )); + let partial = error.partial_delivery_receipt().expect("partial receipt"); + assert_eq!(partial.dispatch_count(), 1); + assert!(partial.target_receipts().is_empty()); } } @@ -1257,35 +2043,32 @@ mod tests { #[tokio::test] async fn delivery_worker_reports_no_wait_satisfied_without_dispatch() { let registry = RadrootsRuntimeTransportRegistry::new(); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); + let worker = delivery_worker(&registry, 8); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::no_wait(), - targets: Vec::new(), - }], - now_ms: 1_000, - }) + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::no_wait(), + vec![delivery_target( + 1, + RadrootsTransportKind::Nostr, + "wss://relay.example", + )], + )], + 1_000, + )) .await .expect("worker receipt"); - assert_eq!(receipt.dispatch_count, 0); - assert!(receipt.all_plans_satisfied); - assert_eq!(receipt.plan_receipts[0].target_count, 0); - assert_eq!(receipt.plan_receipts[0].attempted_target_count, 0); - assert_eq!(receipt.plan_receipts[0].required_target_count, 0); - assert_eq!(receipt.plan_receipts[0].satisfied_target_count, 0); + assert_eq!(receipt.dispatch_count(), 0); + assert!(receipt.all_plans_satisfied()); + let plan_receipt = &receipt.plan_receipts()[0]; + assert_eq!(plan_receipt.target_count(), 1); + assert_eq!(plan_receipt.attempted_target_count(), 0); + assert_eq!(plan_receipt.required_target_count(), 0); + assert_eq!(plan_receipt.satisfied_target_count(), 0); assert_eq!( - receipt.plan_receipts[0].satisfaction_state, - RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied + plan_receipt.satisfaction_state(), + &RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied ); } @@ -1299,50 +2082,41 @@ mod tests { RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); + let worker = delivery_worker(&registry, 8); let required_target = target(RadrootsTransportKind::Reticulum, "reticulum:local"); let required_fingerprint = required_target.fingerprint().clone(); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::required_targets( + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::required_targets( RadrootsTransportSatisfactionClass::Accepted, vec![required_fingerprint], ) .expect("required target policy"), - targets: vec![ - RadrootsRuntimeDeliveryTarget::ready( - 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), - ), + vec![ + delivery_target(1, RadrootsTransportKind::Nostr, "wss://relay.example"), RadrootsRuntimeDeliveryTarget::deferred_until_implemented( 2, required_target, - ), + ) + .expect("deferred target"), ], - }], - now_ms: 1_000, - }) + )], + 1_000, + )) .await .expect("worker receipt"); - assert_eq!(receipt.dispatch_count, 1); - assert!(!receipt.all_plans_satisfied); - assert_eq!(receipt.plan_receipts[0].target_count, 2); - assert_eq!(receipt.plan_receipts[0].attempted_target_count, 1); - assert_eq!(receipt.plan_receipts[0].required_target_count, 1); - assert_eq!(receipt.plan_receipts[0].satisfied_target_count, 0); + assert_eq!(receipt.dispatch_count(), 1); + assert!(!receipt.all_plans_satisfied()); + let plan_receipt = &receipt.plan_receipts()[0]; + assert_eq!(plan_receipt.target_count(), 2); + assert_eq!(plan_receipt.attempted_target_count(), 1); + assert_eq!(plan_receipt.required_target_count(), 1); + assert_eq!(plan_receipt.satisfied_target_count(), 0); assert_eq!( - receipt.plan_receipts[0].satisfaction_state, - RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied + plan_receipt.satisfaction_state(), + &RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied ); } @@ -1356,36 +2130,29 @@ mod tests { RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); + let worker = delivery_worker(&registry, 8); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_delivered(), - targets: vec![RadrootsRuntimeDeliveryTarget::ready( + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::any_delivered(), + vec![delivery_target( 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), + RadrootsTransportKind::Nostr, + "wss://relay.example", )], - }], - now_ms: 1_000, - }) + )], + 1_000, + )) .await .expect("worker receipt"); - assert_eq!(receipt.dispatch_count, 1); - assert!(!receipt.all_plans_satisfied); - assert_eq!(receipt.plan_receipts[0].required_target_count, 1); - assert_eq!(receipt.plan_receipts[0].satisfied_target_count, 0); + assert_eq!(receipt.dispatch_count(), 1); + assert!(!receipt.all_plans_satisfied()); + assert_eq!(receipt.plan_receipts()[0].required_target_count(), 1); + assert_eq!(receipt.plan_receipts()[0].satisfied_target_count(), 0); assert_eq!( - receipt.plan_receipts[0].satisfaction_state, - RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied + receipt.plan_receipts()[0].satisfaction_state(), + &RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied ); } @@ -1399,45 +2166,31 @@ mod tests { RadrootsTransportOutcomeKind::Accepted, )) .expect("register"); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); + let worker = delivery_worker(&registry, 8); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::quorum_accepted(2) - .expect("valid quorum"), - targets: vec![ - RadrootsRuntimeDeliveryTarget::ready( - 1, - target(RadrootsTransportKind::Nostr, "wss://relay-a.example"), - ), - RadrootsRuntimeDeliveryTarget::ready( - 2, - target(RadrootsTransportKind::Nostr, "wss://relay-b.example"), - ), + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::quorum_accepted(2).expect("valid quorum"), + vec![ + delivery_target(1, RadrootsTransportKind::Nostr, "wss://relay-a.example"), + delivery_target(2, RadrootsTransportKind::Nostr, "wss://relay-b.example"), ], - }], - now_ms: 1_000, - }) + )], + 1_000, + )) .await .expect("worker receipt"); - assert_eq!(receipt.dispatch_count, 1); - assert!(receipt.all_plans_satisfied); - assert_eq!(receipt.plan_receipts[0].target_count, 2); - assert_eq!(receipt.plan_receipts[0].attempted_target_count, 2); - assert_eq!(receipt.plan_receipts[0].required_target_count, 2); - assert_eq!(receipt.plan_receipts[0].satisfied_target_count, 2); + assert_eq!(receipt.dispatch_count(), 1); + assert!(receipt.all_plans_satisfied()); + let plan_receipt = &receipt.plan_receipts()[0]; + assert_eq!(plan_receipt.target_count(), 2); + assert_eq!(plan_receipt.attempted_target_count(), 2); + assert_eq!(plan_receipt.required_target_count(), 2); + assert_eq!(plan_receipt.satisfied_target_count(), 2); assert_eq!( - receipt.plan_receipts[0].satisfaction_state, - RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied + plan_receipt.satisfaction_state(), + &RadrootsRuntimeDeliveryPlanSatisfactionState::Satisfied ); } @@ -1456,107 +2209,309 @@ mod tests { )) .expect("register"); } - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); + let worker = delivery_worker(&registry, 8); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::quorum_accepted(2) - .expect("valid quorum"), - targets: vec![ - RadrootsRuntimeDeliveryTarget::ready( - 1, - target(RadrootsTransportKind::Nostr, "wss://relay.example"), - ), - RadrootsRuntimeDeliveryTarget::ready( - 2, - target(RadrootsTransportKind::Reticulum, "reticulum:local"), - ), + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::quorum_accepted(2).expect("valid quorum"), + vec![ + delivery_target(1, RadrootsTransportKind::Nostr, "wss://relay.example"), + delivery_target(2, RadrootsTransportKind::Reticulum, "reticulum:local"), ], - }], - now_ms: 1_000, - }) + )], + 1_000, + )) .await .expect("worker receipt"); - assert_eq!(receipt.dispatch_count, 2); - assert!(receipt.all_plans_satisfied); - assert_eq!(receipt.plan_receipts[0].target_count, 2); - assert_eq!(receipt.plan_receipts[0].attempted_target_count, 2); - assert_eq!(receipt.plan_receipts[0].required_target_count, 2); - assert_eq!(receipt.plan_receipts[0].satisfied_target_count, 2); + assert_eq!(receipt.dispatch_count(), 2); + assert!(receipt.all_plans_satisfied()); + let plan_receipt = &receipt.plan_receipts()[0]; + assert_eq!(plan_receipt.target_count(), 2); + assert_eq!(plan_receipt.attempted_target_count(), 2); + assert_eq!(plan_receipt.required_target_count(), 2); + assert_eq!(plan_receipt.satisfied_target_count(), 2); } #[cfg(feature = "transport-workers")] #[tokio::test] async fn delivery_worker_reports_reticulum_targets_unsatisfied_without_retry_failure() { let registry = RadrootsRuntimeTransportRegistry::new(); - let worker = RadrootsRuntimeDeliveryWorker::new( - &registry, - RadrootsRuntimeDeliveryWorkerConfig { - bounded_queue_capacity: 8, - }, - ); - let reticulum_target = RadrootsRuntimeDeliveryTarget { - delivery_target_id: 1, - target: target(RadrootsTransportKind::Reticulum, "reticulum:local"), - status: RadrootsTransportDeliveryTargetStatus::DeferredUntilImplemented, - }; + let worker = delivery_worker(&registry, 8); + let reticulum_target = + deferred_delivery_target(1, RadrootsTransportKind::Reticulum, "reticulum:local"); let receipt = worker - .execute_job(RadrootsRuntimeDeliveryJob { - outbox_event_id: 42, - payload: opaque_payload(), - plans: vec![RadrootsRuntimeDeliveryPlan { - delivery_plan_id: 7, - satisfaction_policy: RadrootsTransportSatisfactionPolicy::any_accepted(), - targets: vec![reticulum_target], - }], - now_ms: 1_000, - }) + .execute_job(delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::any_accepted(), + vec![reticulum_target], + )], + 1_000, + )) .await .expect("worker receipt"); - assert_eq!(receipt.dispatch_count, 0); - assert!(!receipt.all_plans_satisfied); - assert_eq!(receipt.plan_receipts[0].target_count, 1); - assert_eq!(receipt.plan_receipts[0].attempted_target_count, 0); - assert_eq!(receipt.plan_receipts[0].required_target_count, 1); - assert_eq!(receipt.plan_receipts[0].satisfied_target_count, 0); + assert_eq!(receipt.dispatch_count(), 0); + assert!(!receipt.all_plans_satisfied()); + let plan_receipt = &receipt.plan_receipts()[0]; + assert_eq!(plan_receipt.target_count(), 1); + assert_eq!(plan_receipt.attempted_target_count(), 0); + assert_eq!(plan_receipt.required_target_count(), 1); + assert_eq!(plan_receipt.satisfied_target_count(), 0); assert_eq!( - receipt.plan_receipts[0].satisfaction_state, - RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied + plan_receipt.satisfaction_state(), + &RadrootsRuntimeDeliveryPlanSatisfactionState::Unsatisfied + ); + assert!(receipt.target_receipts().is_empty()); + } + + #[cfg(feature = "transport-workers")] + #[test] + fn delivery_topology_rejects_invalid_ids_empty_and_duplicate_authority() { + let nostr = target(RadrootsTransportKind::Nostr, "wss://relay.example"); + assert!(matches!( + RadrootsRuntimeDeliveryTarget::ready(0, nostr.clone()), + Err(RadrootsRuntimeTransportError::InvalidIdentifier { .. }) + )); + assert!(matches!( + RadrootsRuntimeDeliveryTarget::with_initial_status( + 1, + nostr.clone(), + RadrootsTransportDeliveryTargetStatus::Accepted, + ), + Err(RadrootsRuntimeTransportError::InvalidDeliveryTargetInitialStatus { .. }) + )); + assert!(matches!( + RadrootsRuntimeDeliveryPlan::new( + 7, + RadrootsTransportSatisfactionPolicy::no_wait(), + Vec::new(), + ), + Err(RadrootsRuntimeTransportError::EmptyDeliveryPlan) + )); + + let first = RadrootsRuntimeDeliveryTarget::ready(1, nostr.clone()).expect("target"); + let duplicate_id = RadrootsRuntimeDeliveryTarget::ready( + 1, + target(RadrootsTransportKind::Nostr, "wss://relay-2.example"), + ) + .expect("target"); + assert!(matches!( + RadrootsRuntimeDeliveryPlan::new( + 7, + RadrootsTransportSatisfactionPolicy::all_accepted(), + vec![first.clone(), duplicate_id], + ), + Err(RadrootsRuntimeTransportError::DuplicateDeliveryTargetId { .. }) + )); + + let duplicate_fingerprint = + RadrootsRuntimeDeliveryTarget::ready(2, nostr.clone()).expect("target"); + assert!(matches!( + RadrootsRuntimeDeliveryPlan::new( + 7, + RadrootsTransportSatisfactionPolicy::all_accepted(), + vec![first.clone(), duplicate_fingerprint], + ), + Err(RadrootsRuntimeTransportError::TransportTarget(_)) + )); + + let absent = target(RadrootsTransportKind::Nostr, "wss://absent.example"); + let required_absent = RadrootsTransportSatisfactionPolicy::required_targets( + RadrootsTransportSatisfactionClass::Accepted, + vec![absent.fingerprint().clone()], + ) + .expect("required policy"); + assert!(matches!( + RadrootsRuntimeDeliveryPlan::new(7, required_absent, vec![first.clone()]), + Err(RadrootsRuntimeTransportError::TransportTarget(_)) + )); + + let plan = RadrootsRuntimeDeliveryPlan::new( + 7, + RadrootsTransportSatisfactionPolicy::any_accepted(), + vec![first], + ) + .expect("plan"); + let ready = delivery_target(1, RadrootsTransportKind::Nostr, "wss://relay.example"); + let deferred = + deferred_delivery_target(2, RadrootsTransportKind::Reticulum, "reticulum:local"); + assert_eq!( + RadrootsRuntimeDeliveryPlan::new( + 8, + RadrootsTransportSatisfactionPolicy::all_accepted(), + vec![ready.clone(), deferred.clone()], + ) + .expect("delivery-capable all policy") + .required_target_count(), + 1 + ); + assert!(matches!( + RadrootsRuntimeDeliveryPlan::new( + 8, + RadrootsTransportSatisfactionPolicy::quorum_accepted(2).expect("quorum policy"), + vec![ready, deferred], + ), + Err(RadrootsRuntimeTransportError::TransportTarget(_)) + )); + assert!(matches!( + RadrootsRuntimeDeliveryJob::new(42, opaque_payload(), Vec::new(), 1_000), + Err(RadrootsRuntimeTransportError::EmptyDeliveryJob) + )); + assert!(matches!( + RadrootsRuntimeDeliveryJob::new(42, opaque_payload(), vec![plan.clone(), plan], 1_000,), + Err(RadrootsRuntimeTransportError::DuplicateDeliveryPlanId { .. }) + )); + assert!(matches!( + RadrootsRuntimeDeliveryJob::new(0, opaque_payload(), Vec::new(), 1_000), + Err(RadrootsRuntimeTransportError::InvalidIdentifier { .. }) + )); + assert!(matches!( + RadrootsRuntimeDeliveryJob::new(42, opaque_payload(), Vec::new(), -1), + Err(RadrootsRuntimeTransportError::InvalidTimestamp { .. }) + )); + } + + #[cfg(feature = "transport-workers")] + #[test] + fn delivery_topology_accepts_exact_target_boundary_and_rejects_one_over() { + let targets = (0..radroots_transport::RADROOTS_TRANSPORT_TARGET_MAX_COUNT) + .map(|index| { + delivery_target( + i64::try_from(index + 1).expect("target id"), + RadrootsTransportKind::Nostr, + format!("wss://relay-{index}.example").as_str(), + ) + }) + .collect::<Vec<_>>(); + RadrootsRuntimeDeliveryPlan::new( + 7, + RadrootsTransportSatisfactionPolicy::all_accepted(), + targets.clone(), + ) + .expect("exact target boundary"); + + let mut one_over = targets; + one_over.push(delivery_target( + 17, + RadrootsTransportKind::Nostr, + "wss://relay-over.example", + )); + assert!(matches!( + RadrootsRuntimeDeliveryPlan::new( + 7, + RadrootsTransportSatisfactionPolicy::all_accepted(), + one_over, + ), + Err(RadrootsRuntimeTransportError::TransportTarget(_)) + )); + } + + #[cfg(feature = "transport-workers")] + #[tokio::test] + async fn delivery_worker_returns_completed_receipts_after_later_transport_failure() { + let mut registry = RadrootsRuntimeTransportRegistry::new(); + registry + .register(StaticTransport::new( + RadrootsTransportKind::Nostr, + RadrootsTransportOutcomeKind::Accepted, + )) + .expect("register"); + let worker = delivery_worker(&registry, 8); + let job = delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::all_accepted(), + vec![ + delivery_target(1, RadrootsTransportKind::Nostr, "wss://relay.example"), + delivery_target(2, RadrootsTransportKind::Reticulum, "reticulum:local"), + ], + )], + 1_000, + ); + + let error = worker + .execute_job(job) + .await + .expect_err("later transport failure"); + let partial = error.partial_delivery_receipt().expect("partial receipt"); + assert_eq!(partial.outbox_event_id(), 42); + assert_eq!(partial.dispatch_count(), 1); + assert_eq!(partial.plan_receipts().len(), 1); + assert_eq!(partial.target_receipts().len(), 1); + assert_eq!( + partial.target_receipts()[0].status(), + RadrootsTransportDeliveryTargetStatus::Accepted ); - assert!(receipt.target_receipts.is_empty()); + assert_eq!(partial.plan_receipts()[0].targets().len(), 2); + assert!(!partial.all_plans_satisfied()); + } + + #[cfg(feature = "transport-workers")] + #[tokio::test] + async fn delivery_worker_queue_bounds_admission_and_releases_cancelled_work() { + let mut registry = RadrootsRuntimeTransportRegistry::new(); + registry.register(PendingTransport).expect("register"); + let worker = delivery_worker(&registry, 1); + let job = || { + delivery_job( + vec![delivery_plan( + RadrootsTransportSatisfactionPolicy::any_accepted(), + vec![delivery_target( + 1, + RadrootsTransportKind::Nostr, + "wss://relay.example", + )], + )], + 1_000, + ) + }; + let mut first = Box::pin(worker.execute_job(job())); + let waker = std::task::Waker::noop(); + let mut context = std::task::Context::from_waker(waker); + assert!(matches!( + std::future::Future::poll(first.as_mut(), &mut context), + std::task::Poll::Pending + )); + assert_eq!(worker.queue_status().expect("queue status").in_flight(), 1); + + assert!(matches!( + worker.execute_job(job()).await, + Err(RadrootsRuntimeTransportError::QueueCapacityExhausted { capacity: 1 }) + )); + drop(first); + assert_eq!(worker.queue_status().expect("queue status").in_flight(), 0); + assert_eq!(worker.queue_status().expect("queue status").queued(), 0); } #[cfg(feature = "transport-workers")] #[test] fn lease_recovery_marks_expired_claims() { let mut leases = vec![ - RadrootsRuntimeLeaseRecord { - record_id: 1, - claimed: true, - claim_expires_at_ms: 900, - recovered: false, - }, - RadrootsRuntimeLeaseRecord { - record_id: 2, - claimed: true, - claim_expires_at_ms: 1_100, - recovered: false, - }, + RadrootsRuntimeLeaseRecord::claimed(1, 900).expect("lease"), + RadrootsRuntimeLeaseRecord::claimed(2, 1_100).expect("lease"), + ]; + assert_eq!( + recover_expired_leases(&mut leases, 1_000).expect("recovery"), + 1 + ); + assert!(!leases[0].is_claimed()); + assert!(leases[0].is_recovered()); + assert!(leases[1].is_claimed()); + assert!(matches!( + recover_expired_leases(&mut leases, -1), + Err(RadrootsRuntimeTransportError::InvalidTimestamp { .. }) + )); + + let mut invalid = vec![ + RadrootsRuntimeLeaseRecord::claimed(3, 500).expect("lease"), + RadrootsRuntimeLeaseRecord::claimed(4, 600).expect("lease"), ]; - assert_eq!(recover_expired_leases(&mut leases, 1_000), 1); - assert!(!leases[0].claimed); - assert!(leases[0].recovered); - assert!(leases[1].claimed); + invalid[1].recovered = true; + assert!(matches!( + recover_expired_leases(&mut invalid, 1_000), + Err(RadrootsRuntimeTransportError::InvalidLeaseState { record_id: 4 }) + )); + assert!(invalid[0].is_claimed()); } #[cfg(feature = "transport-workers")] @@ -1569,36 +2524,60 @@ mod tests { let observation = RadrootsRuntimeInboundObservation::verified_signed_event( &event, - RadrootsTransportKind::Nostr, - "wss://relay.example", + target(RadrootsTransportKind::Nostr, "wss://relay.example"), 1_000, - ); + ) + .expect("verified observation"); record_verified_inbound_observation(&sink, event.clone(), observation) .await .expect("record observation"); - let unverified = RadrootsRuntimeInboundObservation { - event_id: event.signed_event().id_str().to_owned(), - verified: false, - transport_kind: RadrootsTransportKind::Nostr, - endpoint_uri: "wss://relay.example".to_owned(), - observed_at_ms: 1_000, - }; assert!(matches!( - record_verified_inbound_observation(&sink, event.clone(), unverified).await, - Err(RadrootsRuntimeTransportError::InboundObservationUnverified { .. }) + RadrootsRuntimeInboundObservation::verified_signed_event( + &event, + target(RadrootsTransportKind::Nostr, "wss://relay.example"), + -1, + ), + Err(RadrootsRuntimeTransportError::InvalidTimestamp { .. }) )); - let mismatched = RadrootsRuntimeInboundObservation { - event_id: "a".repeat(64), - verified: true, - transport_kind: RadrootsTransportKind::Nostr, - endpoint_uri: "wss://relay.example".to_owned(), - observed_at_ms: 1_000, - }; + let exact_endpoint = + "x".repeat(radroots_transport::RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES); + RadrootsRuntimeInboundObservation::verified_signed_event( + &event, + target(RadrootsTransportKind::Local, exact_endpoint.as_str()), + 1_000, + ) + .expect("exact endpoint boundary"); + assert!(RadrootsTransportTarget::new(RadrootsTransportKind::Local, "").is_err()); + assert!( + RadrootsTransportTarget::new( + RadrootsTransportKind::Local, + "x".repeat(radroots_transport::RADROOTS_TRANSPORT_ENDPOINT_URI_MAX_BYTES + 1), + ) + .is_err() + ); + + let mut mismatched = RadrootsRuntimeInboundObservation::verified_signed_event( + &event, + target(RadrootsTransportKind::Nostr, "wss://relay.example"), + 1_000, + ) + .expect("verified observation"); + mismatched.event_id = "a".repeat(64); assert!(matches!( - record_verified_inbound_observation(&sink, event, mismatched).await, + record_verified_inbound_observation(&sink, event.clone(), mismatched).await, Err(RadrootsRuntimeTransportError::InboundObservationEventMismatch { .. }) )); + + let invalid_raw = event + .signed_event() + .raw_json() + .replace(event.signed_event().wire().sig.as_str(), &"0".repeat(128)); + let invalid_wire = + RadrootsNip01EventWire::parse_json(&invalid_raw).expect("invalid signature wire"); + let invalid_signed = RadrootsSignedEvent::from_wire_verified_id(invalid_wire, invalid_raw) + .expect("id-valid event"); + assert!(invalid_signed.verify_signature().is_err()); } }