use acton_ern::Ern;
use crate::actor::{Escalation, RestartLimiterConfig, RestartPolicy, SupervisionStrategy};
use crate::common::{BrokerRef, ParentRef};
use crate::traits::ActorHandleInterface;
pub const MAX_SUPERVISION_DEPTH: usize = 10;
#[derive(Default, Debug, Clone)]
pub struct ActorConfig {
id: Ern,
pub(crate) broker: Option<BrokerRef>,
parent: Option<ParentRef>,
inbox_capacity: Option<usize>,
restart_policy: RestartPolicy,
supervision_strategy: SupervisionStrategy,
escalation: Escalation,
restart_limiter_config: Option<RestartLimiterConfig>,
}
impl ActorConfig {
#[must_use]
pub fn new(id: Ern, broker: Option<BrokerRef>) -> Self {
Self {
id,
broker,
parent: None,
inbox_capacity: None,
restart_policy: RestartPolicy::default(),
supervision_strategy: SupervisionStrategy::default(),
escalation: Escalation::default(),
restart_limiter_config: None,
}
}
#[must_use]
pub const fn with_inbox_capacity(mut self, capacity: usize) -> Self {
self.inbox_capacity = Some(capacity);
self
}
#[must_use]
pub const fn with_restart_policy(mut self, policy: RestartPolicy) -> Self {
self.restart_policy = policy;
self
}
pub fn new_with_name(name: impl Into<String>) -> anyhow::Result<Self> {
Ok(Self::new(Ern::with_root(name.into())?, None))
}
pub fn for_supervised_child(
name: impl Into<String>,
parent: ParentRef,
broker: Option<BrokerRef>,
) -> anyhow::Result<Self> {
let parent_id = parent.id();
let depth = parent_id.parts().len();
if depth >= MAX_SUPERVISION_DEPTH {
let name = name.into();
return Err(anyhow::anyhow!(
"supervision depth limit reached: {parent_id} is already \
{depth} levels deep, and the maximum supervision depth is \
{MAX_SUPERVISION_DEPTH}, so '{name}' cannot be added beneath it"
));
}
let id = parent_id.add_part(name.into())?;
Ok(Self {
id,
broker,
parent: Some(parent),
inbox_capacity: None,
restart_policy: RestartPolicy::default(),
supervision_strategy: SupervisionStrategy::default(),
escalation: Escalation::default(),
restart_limiter_config: None,
})
}
#[inline]
#[must_use]
pub fn id(&self) -> Ern {
self.id.clone()
}
#[inline]
pub(crate) const fn get_broker(&self) -> Option<&BrokerRef> {
self.broker.as_ref()
}
#[inline]
pub(crate) const fn parent(&self) -> Option<&ParentRef> {
self.parent.as_ref()
}
#[inline]
pub(crate) const fn inbox_capacity(&self) -> Option<usize> {
self.inbox_capacity
}
#[inline]
pub(crate) const fn restart_policy(&self) -> RestartPolicy {
self.restart_policy
}
#[must_use]
pub const fn with_supervision_strategy(mut self, strategy: SupervisionStrategy) -> Self {
self.supervision_strategy = strategy;
self
}
#[inline]
pub(crate) const fn supervision_strategy(&self) -> SupervisionStrategy {
self.supervision_strategy
}
#[must_use]
pub const fn with_escalation(mut self, escalation: Escalation) -> Self {
self.escalation = escalation;
self
}
#[inline]
pub(crate) const fn escalation(&self) -> Escalation {
self.escalation
}
#[must_use]
pub const fn with_restart_limiter(mut self, config: RestartLimiterConfig) -> Self {
self.restart_limiter_config = Some(config);
self
}
#[inline]
pub(crate) const fn restart_limiter_config(&self) -> Option<&RestartLimiterConfig> {
self.restart_limiter_config.as_ref()
}
}
#[cfg(test)]
mod supervised_child_identity_tests {
use super::*;
use crate::common::ActorHandle;
fn parent_handle() -> ActorHandle {
handle_at_depth(0)
}
fn handle_at_depth(depth: usize) -> ActorHandle {
ActorHandle::new(ern_at_depth(depth), tokio::sync::mpsc::channel(8).0)
}
fn ern_at_depth(depth: usize) -> Ern {
(0..depth).fold(
Ern::with_root("pool").expect("'pool' is a valid Ern root"),
|ern, level| {
ern.add_part(format!("level{level}"))
.expect("depth is within what acton-ern allows")
},
)
}
#[test]
fn a_child_at_the_depth_limit_is_refused_by_name() {
let deep = ern_at_depth(MAX_SUPERVISION_DEPTH);
let error = ActorConfig::for_supervised_child(
"one_too_many",
ActorHandle::new(deep.clone(), tokio::sync::mpsc::channel(8).0),
None,
)
.expect_err("the parent is already at the depth limit");
let message = error.to_string();
assert!(
message.contains("supervision depth"),
"the refusal should name supervision depth rather than acton-ern's \
generic message about parts, but said: {message}"
);
assert!(
message.contains("one_too_many"),
"the refusal should name the child that was refused: {message}"
);
assert!(
deep.add_part("one_too_many").is_err(),
"acton-ern's own cap must sit exactly at MAX_SUPERVISION_DEPTH"
);
}
#[test]
fn a_child_one_below_the_depth_limit_is_still_allowed() {
let parent = handle_at_depth(MAX_SUPERVISION_DEPTH - 1);
let config = ActorConfig::for_supervised_child("last_level", parent, None)
.expect("one level remains beneath the limit");
assert_eq!(config.id().parts().len(), MAX_SUPERVISION_DEPTH);
let id = config.id().to_string();
assert!(id.ends_with("last_level"), "{id}");
}
#[test]
fn the_same_parent_and_name_always_yield_the_same_identifier() {
let parent = parent_handle();
let first = ActorConfig::for_supervised_child("worker", parent.clone(), None)
.expect("'worker' is a valid name");
let second = ActorConfig::for_supervised_child("worker", parent, None)
.expect("'worker' is a valid name");
assert_eq!(first.id(), second.id());
}
#[test]
fn different_names_under_one_parent_stay_distinct() {
let parent = parent_handle();
let first = ActorConfig::for_supervised_child("reader", parent.clone(), None)
.expect("valid name");
let second = ActorConfig::for_supervised_child("writer", parent, None)
.expect("valid name");
assert_ne!(first.id(), second.id());
}
#[test]
fn the_same_name_under_different_parents_stays_distinct() {
let first = ActorConfig::for_supervised_child("worker", parent_handle(), None)
.expect("valid name");
let second = ActorConfig::for_supervised_child("worker", parent_handle(), None)
.expect("valid name");
assert_ne!(
first.id(),
second.id(),
"each parent has its own generated root"
);
}
#[test]
fn the_child_identifier_reads_as_the_parent_then_the_name() {
let parent = parent_handle();
let parent_id = parent.id();
let config = ActorConfig::for_supervised_child("worker", parent, None)
.expect("valid name");
let child_id = config.id().to_string();
assert!(child_id.starts_with(&parent_id.to_string()), "{child_id}");
assert!(child_id.ends_with("worker"), "{child_id}");
}
#[test]
fn the_ordinary_constructors_keep_minting_fresh_identifiers() {
let first = ActorConfig::new_with_name("worker").expect("valid name");
let second = ActorConfig::new_with_name("worker").expect("valid name");
assert_ne!(first.id(), second.id());
}
}