use std::error::Error;
use std::fmt;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{Arc, Mutex, PoisonError, Weak};
use std::time::{Duration, SystemTime};
use crate::{
BoxedStream, ChunkAttemptOutcome, ChunkCount, ChunkSize, Clock, CodecId, CodecVersion,
ComponentStateEnvelope, ComponentStreamIdentity, DefaultComponentCodec, ItemStream,
RestartabilityDeclaration, StateCodecError, StateLimits, StateSchemaId, StateSchemaVersion,
StreamCloseContext, StreamCloseError, StreamCloseOutcome, StreamOpenContext, StreamOpenError,
StreamOpenOutcome, StreamStateContract, StreamUpdateContext, StreamUpdateError,
VersionedStateCodec,
};
pub trait CompletionPolicy: Send + Sync {
fn begin_chunk(&self) {}
fn is_complete(&self, items_read: ChunkCount) -> bool;
fn end_chunk(&self, _outcome: ChunkAttemptOutcome) {}
fn fingerprint(&self) -> String {
std::any::type_name::<Self>().to_owned()
}
fn stream_registrations(
&self,
) -> Vec<(
ComponentStreamIdentity,
Arc<BoxedStream>,
StreamStateContract,
)> {
Vec::new()
}
#[doc(hidden)]
fn composite_depth(&self) -> usize {
0
}
}
#[derive(Clone, Copy, Debug)]
pub struct ItemCountCompletionPolicy(ChunkSize);
impl ItemCountCompletionPolicy {
#[must_use]
pub const fn new(size: ChunkSize) -> Self {
Self(size)
}
#[must_use]
pub const fn size(self) -> ChunkSize {
self.0
}
}
impl CompletionPolicy for ItemCountCompletionPolicy {
fn is_complete(&self, items_read: ChunkCount) -> bool {
items_read.get() >= u64::from(self.0.get())
}
fn fingerprint(&self) -> String {
format!("count/{}", self.0.get())
}
}
const MIN_TIME_THRESHOLD: Duration = Duration::from_millis(1);
const MAX_TIME_THRESHOLD: Duration = Duration::from_hours(24);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ChunkTimeThreshold(Duration);
impl ChunkTimeThreshold {
pub fn new(value: Duration) -> Result<Self, CompletionPolicyError> {
if !(MIN_TIME_THRESHOLD..=MAX_TIME_THRESHOLD).contains(&value) {
return Err(CompletionPolicyError::InvalidTimeThreshold);
}
Ok(Self(value))
}
#[must_use]
pub const fn get(self) -> Duration {
self.0
}
}
pub struct TimeCompletionPolicy {
clock: Arc<dyn Clock>,
threshold: ChunkTimeThreshold,
started: Mutex<Option<SystemTime>>,
}
impl TimeCompletionPolicy {
#[must_use]
pub fn new(clock: Arc<dyn Clock>, threshold: ChunkTimeThreshold) -> Self {
Self {
clock,
threshold,
started: Mutex::new(None),
}
}
}
impl CompletionPolicy for TimeCompletionPolicy {
fn begin_chunk(&self) {
let mut started = self.started.lock().unwrap_or_else(PoisonError::into_inner);
*started = Some(self.clock.now());
}
fn is_complete(&self, _items_read: ChunkCount) -> bool {
let started = self.started.lock().unwrap_or_else(PoisonError::into_inner);
let Some(started) = *started else {
return false;
};
let elapsed = self
.clock
.now()
.duration_since(started)
.unwrap_or(Duration::ZERO);
elapsed >= self.threshold.get()
}
fn fingerprint(&self) -> String {
format!("time/{}", self.threshold.get().as_nanos())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CompositeMode {
Any,
All,
}
pub const MAX_COMPOSITE_MEMBERS: usize = 32;
pub const MAX_COMPOSITE_DEPTH: usize = 8;
pub struct CompositeCompletionPolicy {
members: Vec<Arc<dyn CompletionPolicy>>,
mode: CompositeMode,
depth: usize,
}
impl fmt::Debug for CompositeCompletionPolicy {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CompositeCompletionPolicy")
.field("mode", &self.mode)
.field("members", &self.members.len())
.field("depth", &self.depth)
.finish()
}
}
impl CompositeCompletionPolicy {
pub fn new(
mode: CompositeMode,
members: Vec<Arc<dyn CompletionPolicy>>,
) -> Result<Self, CompletionPolicyError> {
if members.is_empty() {
return Err(CompletionPolicyError::EmptyComposite);
}
if members.len() > MAX_COMPOSITE_MEMBERS {
return Err(CompletionPolicyError::TooManyMembers {
max: MAX_COMPOSITE_MEMBERS,
});
}
let depth = 1 + members
.iter()
.map(|member| member.composite_depth())
.max()
.unwrap_or(0);
if depth > MAX_COMPOSITE_DEPTH {
return Err(CompletionPolicyError::CompositeTooDeep {
max: MAX_COMPOSITE_DEPTH,
});
}
Ok(Self {
members,
mode,
depth,
})
}
}
impl CompletionPolicy for CompositeCompletionPolicy {
fn begin_chunk(&self) {
let mut panicked = None;
for (index, member) in self.members.iter().enumerate() {
match catch_unwind(AssertUnwindSafe(|| member.begin_chunk())) {
Ok(()) => {}
Err(payload) => {
panicked = Some((index, payload));
break;
}
}
}
if let Some((panicked_at, payload)) = panicked {
for member in &self.members[..panicked_at] {
let _ = catch_unwind(AssertUnwindSafe(|| {
member.end_chunk(ChunkAttemptOutcome::Unknown);
}));
}
std::panic::resume_unwind(payload);
}
}
fn is_complete(&self, items_read: ChunkCount) -> bool {
match self.mode {
CompositeMode::Any => self
.members
.iter()
.any(|member| member.is_complete(items_read)),
CompositeMode::All => self
.members
.iter()
.all(|member| member.is_complete(items_read)),
}
}
fn end_chunk(&self, outcome: ChunkAttemptOutcome) {
let mut first_panic = None;
for member in &self.members {
if let Err(payload) = catch_unwind(AssertUnwindSafe(|| member.end_chunk(outcome))) {
first_panic.get_or_insert(payload);
}
}
if let Some(payload) = first_panic {
std::panic::resume_unwind(payload);
}
}
fn fingerprint(&self) -> String {
use std::fmt::Write as _;
let mut members = String::new();
for member in &self.members {
let fingerprint = member.fingerprint();
let _ = write!(members, "{}:{fingerprint}", fingerprint.len());
}
format!("composite/{:?}/[{members}]", self.mode)
}
fn stream_registrations(
&self,
) -> Vec<(
ComponentStreamIdentity,
Arc<BoxedStream>,
StreamStateContract,
)> {
self.members
.iter()
.flat_map(|member| member.stream_registrations())
.collect()
}
fn composite_depth(&self) -> usize {
self.depth
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CompletionPolicyError {
InvalidTimeThreshold,
EmptyComposite,
TooManyMembers {
max: usize,
},
CompositeTooDeep {
max: usize,
},
InvalidAdaptiveBounds,
}
impl fmt::Display for CompletionPolicyError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidTimeThreshold => {
formatter.write_str("completion time threshold must be within 1 ms..=24 h")
}
Self::EmptyComposite => {
formatter.write_str("a composite completion policy requires at least one member")
}
Self::TooManyMembers { max } => write!(
formatter,
"a composite completion policy accepts at most {max} members"
),
Self::CompositeTooDeep { max } => write!(
formatter,
"a composite completion policy nests at most {max} levels deep"
),
Self::InvalidAdaptiveBounds => formatter
.write_str("adaptive completion policy minimum bound exceeds its maximum bound"),
}
}
}
impl Error for CompletionPolicyError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AdaptiveBounds {
min: ChunkSize,
max: ChunkSize,
}
impl AdaptiveBounds {
pub fn new(min: ChunkSize, max: ChunkSize) -> Result<Self, CompletionPolicyError> {
if min.get() > max.get() {
return Err(CompletionPolicyError::InvalidAdaptiveBounds);
}
Ok(Self { min, max })
}
#[must_use]
pub const fn min(self) -> ChunkSize {
self.min
}
#[must_use]
pub const fn max(self) -> ChunkSize {
self.max
}
fn clamp(self, target: u32) -> ChunkSize {
let clamped = target.clamp(self.min.get(), self.max.get());
ChunkSize::new(clamped).unwrap_or(self.min)
}
}
#[derive(Clone, Copy)]
struct AdaptiveDecision {
target: ChunkSize,
}
const ADAPTIVE_SCHEMA: &str = "oxide-batch.completion.adaptive-decision";
const ADAPTIVE_CODEC: &str = "oxide-batch.completion.adaptive-decision-codec";
#[derive(Clone, Copy)]
struct AdaptiveDecisionSchema;
impl VersionedStateCodec<AdaptiveDecision> for AdaptiveDecisionSchema {
fn schema_id(&self) -> &StateSchemaId {
static SCHEMA: std::sync::OnceLock<StateSchemaId> = std::sync::OnceLock::new();
#[allow(
clippy::unwrap_used,
reason = "fixed literal schema identity cannot fail validation"
)]
SCHEMA.get_or_init(|| StateSchemaId::new(ADAPTIVE_SCHEMA).unwrap())
}
fn current_version(&self) -> StateSchemaVersion {
#[allow(
clippy::unwrap_used,
reason = "fixed literal schema version cannot fail validation"
)]
StateSchemaVersion::new(1).unwrap()
}
fn encode(&self, value: &AdaptiveDecision) -> Result<Vec<u8>, StateCodecError> {
serde_json::to_vec(&serde_json::json!({ "target_items": value.target.get() }))
.map_err(|_| StateCodecError::InvalidPayload)
}
fn decode(&self, payload: &[u8]) -> Result<AdaptiveDecision, StateCodecError> {
let value: serde_json::Value =
serde_json::from_slice(payload).map_err(|_| StateCodecError::InvalidPayload)?;
let target_items = u32::try_from(
value
.get("target_items")
.and_then(serde_json::Value::as_u64)
.ok_or(StateCodecError::InvalidPayload)?,
)
.map_err(|_| StateCodecError::InvalidPayload)?;
let target = ChunkSize::new(target_items).map_err(|_| StateCodecError::InvalidPayload)?;
Ok(AdaptiveDecision { target })
}
}
fn adaptive_decision_codec() -> DefaultComponentCodec<AdaptiveDecisionSchema> {
#[allow(
clippy::unwrap_used,
reason = "fixed literal identities cannot fail validation"
)]
DefaultComponentCodec::new(
AdaptiveDecisionSchema,
CodecId::new(ADAPTIVE_CODEC).unwrap(),
CodecVersion::new(1).unwrap(),
RestartabilityDeclaration::Restartable,
)
}
struct AdaptiveInterior {
confirmed: ChunkSize,
pending: Option<ChunkSize>,
chunk_started: Option<SystemTime>,
}
pub struct AdaptiveCompletionPolicy {
bounds: AdaptiveBounds,
target_duration: ChunkTimeThreshold,
clock: Arc<dyn Clock>,
limits: StateLimits,
identity: ComponentStreamIdentity,
state: Mutex<AdaptiveInterior>,
self_weak: Weak<AdaptiveCompletionPolicy>,
}
impl AdaptiveCompletionPolicy {
#[must_use]
pub fn new(
identity: ComponentStreamIdentity,
bounds: AdaptiveBounds,
target_duration: ChunkTimeThreshold,
clock: Arc<dyn Clock>,
) -> Arc<Self> {
Arc::new_cyclic(|self_weak| Self {
bounds,
target_duration,
clock,
limits: StateLimits::default(),
identity,
state: Mutex::new(AdaptiveInterior {
confirmed: bounds.min(),
pending: None,
chunk_started: None,
}),
self_weak: self_weak.clone(),
})
}
#[must_use]
pub const fn identity(&self) -> &ComponentStreamIdentity {
&self.identity
}
pub(crate) fn stream_state_contract() -> StreamStateContract {
StreamStateContract::new(adaptive_decision_codec())
}
#[must_use]
pub fn current_target(&self) -> ChunkSize {
self.state
.lock()
.unwrap_or_else(PoisonError::into_inner)
.confirmed
}
}
impl CompletionPolicy for AdaptiveCompletionPolicy {
fn begin_chunk(&self) {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state.pending = None;
state.chunk_started = Some(self.clock.now());
}
fn is_complete(&self, items_read: ChunkCount) -> bool {
let target = self.current_target();
items_read.get() >= u64::from(target.get())
}
fn end_chunk(&self, outcome: ChunkAttemptOutcome) {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let pending = state.pending.take();
if let (ChunkAttemptOutcome::Committed, Some(pending)) = (outcome, pending) {
state.confirmed = pending;
}
}
fn fingerprint(&self) -> String {
format!(
"adaptive/{}/{}/{}",
self.bounds.min().get(),
self.bounds.max().get(),
self.target_duration.get().as_nanos()
)
}
fn stream_registrations(
&self,
) -> Vec<(
ComponentStreamIdentity,
Arc<BoxedStream>,
StreamStateContract,
)> {
let Some(strong) = self.self_weak.upgrade() else {
return Vec::new();
};
vec![(
self.identity.clone(),
Arc::new(BoxedStream::new(AdaptiveCompletionStream(strong))),
Self::stream_state_contract(),
)]
}
}
struct AdaptiveCompletionStream(Arc<AdaptiveCompletionPolicy>);
impl ItemStream for AdaptiveCompletionStream {
async fn open(
&self,
context: StreamOpenContext<'_>,
) -> Result<StreamOpenOutcome, StreamOpenError> {
self.0.open(context).await
}
async fn update(
&self,
context: StreamUpdateContext<'_>,
) -> Result<ComponentStateEnvelope, StreamUpdateError> {
self.0.update(context).await
}
async fn close(
&self,
context: StreamCloseContext<'_>,
) -> Result<StreamCloseOutcome, StreamCloseError> {
self.0.close(context).await
}
}
impl ItemStream for AdaptiveCompletionPolicy {
async fn open(
&self,
context: StreamOpenContext<'_>,
) -> Result<StreamOpenOutcome, StreamOpenError> {
let Some(inherited) = context.inherited_state() else {
return Ok(StreamOpenOutcome::Initial);
};
let codec = adaptive_decision_codec();
let decoded: AdaptiveDecision = inherited
.decode(&codec)
.map_err(|_| StreamOpenError::new())?;
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state.confirmed = decoded.target;
state.pending = None;
Ok(StreamOpenOutcome::Restored)
}
async fn update(
&self,
_context: StreamUpdateContext<'_>,
) -> Result<ComponentStateEnvelope, StreamUpdateError> {
let next_target = {
let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let elapsed = state.chunk_started.map(|started| {
self.clock
.now()
.duration_since(started)
.unwrap_or(Duration::ZERO)
});
adjust_target(
state.confirmed,
elapsed,
self.target_duration.get(),
self.bounds,
)
};
{
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state.pending = Some(next_target);
}
let codec = adaptive_decision_codec();
ComponentStateEnvelope::encode(
self.identity.clone(),
&AdaptiveDecision {
target: next_target,
},
&codec,
self.limits,
)
.map_err(|_| StreamUpdateError::new())
}
async fn close(
&self,
_context: StreamCloseContext<'_>,
) -> Result<StreamCloseOutcome, StreamCloseError> {
Ok(StreamCloseOutcome::Closed)
}
}
fn adjust_target(
confirmed: ChunkSize,
observed: Option<Duration>,
target_duration: Duration,
bounds: AdaptiveBounds,
) -> ChunkSize {
let Some(observed) = observed else {
return confirmed;
};
let tolerance = target_duration / 5;
let step = (confirmed.get() / 8).max(1);
if observed < target_duration.saturating_sub(tolerance) {
bounds.clamp(confirmed.get().saturating_add(step))
} else if observed > target_duration.saturating_add(tolerance) {
bounds.clamp(confirmed.get().saturating_sub(step))
} else {
confirmed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_policy_boundaries() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let policy = ItemCountCompletionPolicy::new(ChunkSize::new(3).unwrap());
assert!(!policy.is_complete(ChunkCount::new(0)));
assert!(!policy.is_complete(ChunkCount::new(2)));
assert!(policy.is_complete(ChunkCount::new(3)));
assert!(policy.is_complete(ChunkCount::new(4)));
}
#[test]
fn time_threshold_rejects_out_of_bounds() {
assert!(ChunkTimeThreshold::new(Duration::ZERO).is_err());
assert!(ChunkTimeThreshold::new(Duration::from_hours(25)).is_err());
assert!(ChunkTimeThreshold::new(Duration::from_secs(1)).is_ok());
}
#[test]
#[allow(clippy::unwrap_used, reason = "test asserts the exact Err variant")]
fn composite_rejects_empty_and_oversized() {
assert_eq!(
CompositeCompletionPolicy::new(CompositeMode::Any, Vec::new()).unwrap_err(),
CompletionPolicyError::EmptyComposite
);
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let member: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(1).unwrap()));
let too_many = std::iter::repeat_with(|| Arc::clone(&member))
.take(MAX_COMPOSITE_MEMBERS + 1)
.collect();
assert!(matches!(
CompositeCompletionPolicy::new(CompositeMode::Any, too_many),
Err(CompletionPolicyError::TooManyMembers { max }) if max == MAX_COMPOSITE_MEMBERS
));
}
#[test]
#[allow(clippy::unwrap_used, reason = "test constructs known-valid policies")]
fn composite_any_and_all_semantics() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let small: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(2).unwrap()));
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let large: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(5).unwrap()));
let any = CompositeCompletionPolicy::new(
CompositeMode::Any,
vec![Arc::clone(&small), Arc::clone(&large)],
)
.unwrap();
assert!(any.is_complete(ChunkCount::new(2)));
assert!(!any.is_complete(ChunkCount::new(1)));
let all = CompositeCompletionPolicy::new(CompositeMode::All, vec![small, large]).unwrap();
assert!(!all.is_complete(ChunkCount::new(2)));
assert!(all.is_complete(ChunkCount::new(5)));
}
#[test]
#[allow(clippy::unwrap_used, reason = "test asserts the exact Err variant")]
fn adaptive_bounds_reject_min_above_max() {
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let (min, max) = (ChunkSize::new(10).unwrap(), ChunkSize::new(5).unwrap());
assert_eq!(
AdaptiveBounds::new(min, max).unwrap_err(),
CompletionPolicyError::InvalidAdaptiveBounds
);
}
#[test]
fn adjust_target_moves_toward_faster_chunks_when_slow() {
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let bounds =
AdaptiveBounds::new(ChunkSize::new(1).unwrap(), ChunkSize::new(100).unwrap()).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let confirmed = ChunkSize::new(16).unwrap();
let slow = adjust_target(
confirmed,
Some(Duration::from_secs(2)),
Duration::from_secs(1),
bounds,
);
assert!(
slow.get() < confirmed.get(),
"slow chunk should shrink the target"
);
let fast = adjust_target(
confirmed,
Some(Duration::from_millis(100)),
Duration::from_secs(1),
bounds,
);
assert!(
fast.get() > confirmed.get(),
"fast chunk should grow the target"
);
let on_target = adjust_target(
confirmed,
Some(Duration::from_secs(1)),
Duration::from_secs(1),
bounds,
);
assert_eq!(on_target.get(), confirmed.get());
}
#[test]
fn adjust_target_stays_within_bounds() {
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let bounds =
AdaptiveBounds::new(ChunkSize::new(4).unwrap(), ChunkSize::new(8).unwrap()).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let confirmed = ChunkSize::new(8).unwrap();
let still_fast = adjust_target(
confirmed,
Some(Duration::from_millis(1)),
Duration::from_secs(1),
bounds,
);
assert_eq!(still_fast.get(), 8, "must not exceed the configured max");
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let confirmed_min = ChunkSize::new(4).unwrap();
let still_slow = adjust_target(
confirmed_min,
Some(Duration::from_secs(1000)),
Duration::from_secs(1),
bounds,
);
assert_eq!(still_slow.get(), 4, "must not go below the configured min");
}
struct ManualClock(Mutex<SystemTime>);
impl ManualClock {
fn new(start: SystemTime) -> Self {
Self(Mutex::new(start))
}
fn advance(&self, delta: Duration) {
let mut now = self.0.lock().unwrap_or_else(PoisonError::into_inner);
*now += delta;
}
}
impl Clock for ManualClock {
fn now(&self) -> SystemTime {
*self.0.lock().unwrap_or_else(PoisonError::into_inner)
}
}
fn decode_target(envelope: &ComponentStateEnvelope) -> ChunkSize {
let codec = adaptive_decision_codec();
#[allow(
clippy::unwrap_used,
reason = "test decodes a value this test just encoded"
)]
let decoded: AdaptiveDecision = envelope.decode(&codec).unwrap();
decoded.target
}
#[test]
#[allow(clippy::unwrap_used, reason = "test builds a controlled nesting chain")]
fn composite_depth_boundary_and_overflow() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let mut current: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(1).unwrap()));
for depth in 1..=MAX_COMPOSITE_DEPTH {
let composite =
CompositeCompletionPolicy::new(CompositeMode::Any, vec![current]).unwrap();
assert_eq!(composite.composite_depth(), depth);
current = Arc::new(composite);
}
assert!(matches!(
CompositeCompletionPolicy::new(CompositeMode::Any, vec![current]),
Err(CompletionPolicyError::CompositeTooDeep { max }) if max == MAX_COMPOSITE_DEPTH
));
}
#[test]
#[allow(clippy::unwrap_used, reason = "test builds a controlled nesting chain")]
fn composite_depth_bounded_via_indirect_nesting() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let leaf: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(1).unwrap()));
let mut deep = Arc::clone(&leaf);
for _ in 0..MAX_COMPOSITE_DEPTH - 1 {
deep =
Arc::new(CompositeCompletionPolicy::new(CompositeMode::Any, vec![deep]).unwrap());
}
let at_boundary =
CompositeCompletionPolicy::new(CompositeMode::All, vec![leaf.clone(), deep.clone()])
.unwrap();
assert_eq!(at_boundary.composite_depth(), MAX_COMPOSITE_DEPTH);
let one_level_deeper =
Arc::new(CompositeCompletionPolicy::new(CompositeMode::Any, vec![deep]).unwrap());
assert!(matches!(
CompositeCompletionPolicy::new(CompositeMode::All, vec![leaf, one_level_deeper]),
Err(CompletionPolicyError::CompositeTooDeep { max }) if max == MAX_COMPOSITE_DEPTH
));
}
#[test]
#[allow(
clippy::unwrap_used,
reason = "test drives a controlled adaptive policy"
)]
fn adaptive_update_never_mutates_confirmed_and_end_chunk_gates_promotion() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid identity")]
let identity = ComponentStreamIdentity::new("test.adaptive").unwrap();
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let bounds =
AdaptiveBounds::new(ChunkSize::new(1).unwrap(), ChunkSize::new(100).unwrap()).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid threshold")]
let target_duration = ChunkTimeThreshold::new(Duration::from_secs(1)).unwrap();
let clock = Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH));
let policy = AdaptiveCompletionPolicy::new(
identity,
bounds,
target_duration,
Arc::clone(&clock) as Arc<dyn Clock>,
);
let (_source, stop) = crate::StopSource::new();
let initial = policy.current_target();
policy.begin_chunk();
clock.advance(Duration::from_millis(50));
#[allow(clippy::unwrap_used, reason = "test drives a known-valid update")]
let candidate_1 =
futures_executor::block_on(policy.update(StreamUpdateContext::new(&stop))).unwrap();
assert_eq!(
policy.current_target(),
initial,
"update must never mutate the confirmed target before commit"
);
policy.end_chunk(ChunkAttemptOutcome::RolledBack);
assert_eq!(
policy.current_target(),
initial,
"a rolled-back attempt must leave confirmed exactly as it was"
);
policy.begin_chunk();
clock.advance(Duration::from_millis(50));
#[allow(clippy::unwrap_used, reason = "test drives a known-valid update")]
let candidate_2 =
futures_executor::block_on(policy.update(StreamUpdateContext::new(&stop))).unwrap();
assert_eq!(
decode_target(&candidate_1),
decode_target(&candidate_2),
"a replayed attempt must recompute the identical candidate"
);
assert_ne!(
decode_target(&candidate_2).get(),
initial.get(),
"the fast chunk should have produced a larger candidate than the baseline"
);
policy.end_chunk(ChunkAttemptOutcome::Committed);
assert_eq!(
policy.current_target(),
decode_target(&candidate_2),
"a committed attempt must promote its candidate to confirmed"
);
policy.begin_chunk();
#[allow(clippy::unwrap_used, reason = "test drives a known-valid update")]
let _candidate_3 =
futures_executor::block_on(policy.update(StreamUpdateContext::new(&stop))).unwrap();
let confirmed_after_commit = policy.current_target();
policy.end_chunk(ChunkAttemptOutcome::Unknown);
assert_eq!(
policy.current_target(),
confirmed_after_commit,
"an unknown commit outcome must not promote a speculative candidate"
);
}
#[test]
#[allow(
clippy::unwrap_used,
reason = "test drives a controlled adaptive policy"
)]
fn adaptive_open_restores_confirmed_and_discards_stale_pending() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid identity")]
let identity = ComponentStreamIdentity::new("test.adaptive").unwrap();
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let bounds =
AdaptiveBounds::new(ChunkSize::new(1).unwrap(), ChunkSize::new(100).unwrap()).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid threshold")]
let target_duration = ChunkTimeThreshold::new(Duration::from_secs(1)).unwrap();
let clock = Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH));
let policy = AdaptiveCompletionPolicy::new(
identity.clone(),
bounds,
target_duration,
Arc::clone(&clock) as Arc<dyn Clock>,
);
let (_source, stop) = crate::StopSource::new();
policy.begin_chunk();
clock.advance(Duration::from_millis(1));
#[allow(clippy::unwrap_used, reason = "test drives a known-valid update")]
let _pending =
futures_executor::block_on(policy.update(StreamUpdateContext::new(&stop))).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let restored_target = ChunkSize::new(42).unwrap();
let codec = adaptive_decision_codec();
#[allow(clippy::unwrap_used, reason = "test builds a known-valid envelope")]
let inherited = ComponentStateEnvelope::encode(
identity,
&AdaptiveDecision {
target: restored_target,
},
&codec,
StateLimits::default(),
)
.unwrap();
#[allow(clippy::unwrap_used, reason = "test drives a known-valid open")]
futures_executor::block_on(policy.open(StreamOpenContext::new(Some(&inherited), &stop)))
.unwrap();
assert_eq!(
policy.current_target(),
restored_target,
"open must restore the durable envelope's target"
);
policy.end_chunk(ChunkAttemptOutcome::Committed);
assert_eq!(
policy.current_target(),
restored_target,
"a pending candidate predating `open` must never be promoted"
);
}
#[test]
#[allow(clippy::unwrap_used, reason = "test constructs known-valid policies")]
fn fingerprint_is_deterministic_for_identical_configuration() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let a = ItemCountCompletionPolicy::new(ChunkSize::new(7).unwrap());
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let b = ItemCountCompletionPolicy::new(ChunkSize::new(7).unwrap());
assert_eq!(a.fingerprint(), b.fingerprint());
#[allow(clippy::unwrap_used, reason = "test literal is a valid threshold")]
let threshold = ChunkTimeThreshold::new(Duration::from_secs(3)).unwrap();
let clock_a: Arc<dyn Clock> = Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH));
let clock_b: Arc<dyn Clock> = Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH));
let time_a = TimeCompletionPolicy::new(clock_a, threshold);
let time_b = TimeCompletionPolicy::new(clock_b, threshold);
assert_eq!(
time_a.fingerprint(),
time_b.fingerprint(),
"the injected clock is runtime state, not configuration"
);
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let bounds =
AdaptiveBounds::new(ChunkSize::new(2).unwrap(), ChunkSize::new(9).unwrap()).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid identity")]
let adaptive_a = AdaptiveCompletionPolicy::new(
ComponentStreamIdentity::new("a").unwrap(),
bounds,
threshold,
Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH)),
);
#[allow(clippy::unwrap_used, reason = "test literal is a valid identity")]
let adaptive_b = AdaptiveCompletionPolicy::new(
ComponentStreamIdentity::new("b").unwrap(),
bounds,
threshold,
Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH)),
);
assert_eq!(
adaptive_a.fingerprint(),
adaptive_b.fingerprint(),
"identity and clock are runtime wiring, not configuration"
);
}
#[test]
#[allow(clippy::unwrap_used, reason = "test constructs known-valid policies")]
fn fingerprint_changes_with_configuration() {
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let count_5 = ItemCountCompletionPolicy::new(ChunkSize::new(5).unwrap());
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let count_6 = ItemCountCompletionPolicy::new(ChunkSize::new(6).unwrap());
assert_ne!(count_5.fingerprint(), count_6.fingerprint());
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let small: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(2).unwrap()));
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let large: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(5).unwrap()));
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
let any = CompositeCompletionPolicy::new(
CompositeMode::Any,
vec![Arc::clone(&small), Arc::clone(&large)],
)
.unwrap();
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
let all = CompositeCompletionPolicy::new(CompositeMode::All, vec![small, large]).unwrap();
assert_ne!(
any.fingerprint(),
all.fingerprint(),
"Any vs All must fingerprint differently even with the same members"
);
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let bounds_a =
AdaptiveBounds::new(ChunkSize::new(1).unwrap(), ChunkSize::new(10).unwrap()).unwrap();
#[allow(clippy::unwrap_used, reason = "test literals are valid ChunkSizes")]
let bounds_b =
AdaptiveBounds::new(ChunkSize::new(1).unwrap(), ChunkSize::new(20).unwrap()).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid threshold")]
let threshold = ChunkTimeThreshold::new(Duration::from_secs(1)).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid identity")]
let adaptive_a = AdaptiveCompletionPolicy::new(
ComponentStreamIdentity::new("same").unwrap(),
bounds_a,
threshold,
Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH)),
);
#[allow(clippy::unwrap_used, reason = "test literal is a valid identity")]
let adaptive_b = AdaptiveCompletionPolicy::new(
ComponentStreamIdentity::new("same").unwrap(),
bounds_b,
threshold,
Arc::new(ManualClock::new(SystemTime::UNIX_EPOCH)),
);
assert_ne!(
adaptive_a.fingerprint(),
adaptive_b.fingerprint(),
"a bounds change must change the fingerprint"
);
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let nested_leaf: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(2).unwrap()));
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
let inner_any =
CompositeCompletionPolicy::new(CompositeMode::Any, vec![Arc::clone(&nested_leaf)])
.unwrap();
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
let inner_all =
CompositeCompletionPolicy::new(CompositeMode::All, vec![nested_leaf]).unwrap();
#[allow(clippy::unwrap_used, reason = "test literal is a valid ChunkSize")]
let sibling: Arc<dyn CompletionPolicy> =
Arc::new(ItemCountCompletionPolicy::new(ChunkSize::new(3).unwrap()));
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
let outer_a = CompositeCompletionPolicy::new(
CompositeMode::All,
vec![Arc::new(inner_any), Arc::clone(&sibling)],
)
.unwrap();
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
let outer_b =
CompositeCompletionPolicy::new(CompositeMode::All, vec![Arc::new(inner_all), sibling])
.unwrap();
assert_ne!(
outer_a.fingerprint(),
outer_b.fingerprint(),
"a mode change nested inside a composite must change the outer fingerprint"
);
}
struct FixedFingerprint(&'static str);
impl CompletionPolicy for FixedFingerprint {
fn is_complete(&self, _items_read: ChunkCount) -> bool {
false
}
fn fingerprint(&self) -> String {
self.0.to_owned()
}
}
#[test]
#[allow(clippy::unwrap_used, reason = "test constructs known-valid composites")]
fn composite_fingerprint_encoding_is_injective_across_member_splits() {
let split_a: Vec<Arc<dyn CompletionPolicy>> = vec![
Arc::new(FixedFingerprint("a")),
Arc::new(FixedFingerprint("b,c")),
];
let split_b: Vec<Arc<dyn CompletionPolicy>> = vec![
Arc::new(FixedFingerprint("a,b")),
Arc::new(FixedFingerprint("c")),
];
let composite_a = CompositeCompletionPolicy::new(CompositeMode::Any, split_a).unwrap();
let composite_b = CompositeCompletionPolicy::new(CompositeMode::Any, split_b).unwrap();
assert_ne!(
composite_a.fingerprint(),
composite_b.fingerprint(),
"different member splits must never collide onto the same \
composite fingerprint, even when a member's own fingerprint \
contains the naive join delimiter"
);
}
struct RecordingMember {
begins: Arc<std::sync::atomic::AtomicUsize>,
ends: Arc<Mutex<Vec<ChunkAttemptOutcome>>>,
}
impl RecordingMember {
fn new() -> (
Self,
Arc<std::sync::atomic::AtomicUsize>,
Arc<Mutex<Vec<ChunkAttemptOutcome>>>,
) {
let begins = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let ends = Arc::new(Mutex::new(Vec::new()));
(
Self {
begins: Arc::clone(&begins),
ends: Arc::clone(&ends),
},
begins,
ends,
)
}
}
impl CompletionPolicy for RecordingMember {
fn begin_chunk(&self) {
self.begins
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
fn is_complete(&self, _items_read: ChunkCount) -> bool {
false
}
fn end_chunk(&self, outcome: ChunkAttemptOutcome) {
self.ends
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(outcome);
}
}
struct PanickingMember {
panic_in_begin: bool,
panic_in_end: bool,
}
impl CompletionPolicy for PanickingMember {
fn begin_chunk(&self) {
assert!(!self.panic_in_begin, "member begin_chunk secret");
}
fn is_complete(&self, _items_read: ChunkCount) -> bool {
false
}
fn end_chunk(&self, _outcome: ChunkAttemptOutcome) {
assert!(!self.panic_in_end, "member end_chunk secret");
}
}
#[test]
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
fn composite_begin_panic_still_ends_every_member_that_already_began() {
let (before, before_begins, before_ends) = RecordingMember::new();
let (after, after_begins, _after_ends) = RecordingMember::new();
let panicking = PanickingMember {
panic_in_begin: true,
panic_in_end: false,
};
let composite = CompositeCompletionPolicy::new(
CompositeMode::All,
vec![
Arc::new(before) as Arc<dyn CompletionPolicy>,
Arc::new(panicking),
Arc::new(after),
],
)
.unwrap();
let result = catch_unwind(AssertUnwindSafe(|| composite.begin_chunk()));
assert!(
result.is_err(),
"the composite's begin_chunk must still panic, matching a \
non-composite policy's panicking begin_chunk"
);
assert_eq!(
before_begins.load(std::sync::atomic::Ordering::SeqCst),
1,
"the member positioned before the panicking one must have begun"
);
assert_eq!(
*before_ends.lock().unwrap_or_else(PoisonError::into_inner),
vec![ChunkAttemptOutcome::Unknown],
"a member that already began before a sibling's begin_chunk \
panicked must still receive its own matching end_chunk -- with \
Unknown, since this attempt never validly began"
);
assert_eq!(
after_begins.load(std::sync::atomic::Ordering::SeqCst),
0,
"a member positioned after the panicking one must never have \
begun at all"
);
}
#[test]
#[allow(
clippy::unwrap_used,
reason = "test constructs a known-valid composite"
)]
fn composite_end_panic_still_ends_every_remaining_member() {
let (before, _before_begins, before_ends) = RecordingMember::new();
let (after, _after_begins, after_ends) = RecordingMember::new();
let panicking = PanickingMember {
panic_in_begin: false,
panic_in_end: true,
};
let composite = CompositeCompletionPolicy::new(
CompositeMode::All,
vec![
Arc::new(before) as Arc<dyn CompletionPolicy>,
Arc::new(panicking),
Arc::new(after),
],
)
.unwrap();
composite.begin_chunk();
let result = catch_unwind(AssertUnwindSafe(|| {
composite.end_chunk(ChunkAttemptOutcome::Committed);
}));
assert!(
result.is_err(),
"the composite's end_chunk must still panic, matching a \
non-composite policy's panicking end_chunk"
);
assert_eq!(
*before_ends.lock().unwrap_or_else(PoisonError::into_inner),
vec![ChunkAttemptOutcome::Committed],
"a member before the panicking one must receive its end_chunk"
);
assert_eq!(
*after_ends.lock().unwrap_or_else(PoisonError::into_inner),
vec![ChunkAttemptOutcome::Committed],
"a member positioned *after* the panicking one must still \
receive its own end_chunk call -- one member's panic must \
never suppress a sibling's matching end"
);
}
}