Skip to main content

datum_cluster_sharding/
lib.rs

1#![forbid(unsafe_code)]
2//! Cluster sharding core for Datum.
3//!
4//! This crate implements the v0.10 sharding subset: envelopes and shard
5//! extraction, an oldest-member shard coordinator, one region per node,
6//! first-message entity actor startup, bounded buffering while resolving
7//! unallocated shards, rebalance-on-membership-change with bounded handoff
8//! buffering, and peer-region forwarding over long-lived C2 DCP shard pipes
9//! from `datum-agent`.
10//!
11//! Delivery is at-most-once per attempt. Ordering is preserved for messages to
12//! one entity from one sender that awaits each `tell`/`ask` call. There is no
13//! persistent allocation store in v0.10. When remember-entities is enabled for
14//! an entity type, entity ids are recorded through a write-behind
15//! [`RememberEntitiesStore`] and are respawned when their shard starts; in-memory
16//! entity state is not migrated. Passivation removes the entity id from the
17//! remember store, matching Akka Cluster Sharding's rule that passivated
18//! entities are not remembered. Coordinator takeover rebuilds the allocation
19//! table from surviving regions and keeps the highest generation for each shard;
20//! any shard that still points at a non-eligible member is reallocated by the
21//! new coordinator.
22
23use std::{
24    cell::RefCell,
25    collections::{BTreeMap, BTreeSet, VecDeque},
26    fmt,
27    fs::{self, File, OpenOptions},
28    future::Future,
29    io::{BufRead, BufReader, Write},
30    marker::PhantomData,
31    panic::{AssertUnwindSafe, catch_unwind},
32    path::{Path, PathBuf},
33    pin::Pin,
34    sync::{
35        Arc, Mutex,
36        atomic::{AtomicU64, Ordering},
37    },
38    time::Duration,
39};
40
41use datum_agent::{
42    AGENT_ROLE, ClusterAgent, ClusterAgentConfig, ClusterAgentHandle, NodeSessionManagerHandle,
43    dcp::{
44        CompleteShardingAsk, DcpJobFactories, ForwardShardEnvelopes, RememberShardAllocations,
45        ResponseStatus, ShardAllocation, ShardAllocationEntry, ShardAllocationRequest,
46        ShardAllocationTable, ShardEnvelopeAck, ShardEnvelopeBatchResult, ShardEnvelopeWire,
47        ShardingViewProvider,
48    },
49};
50use datum_cluster::{ClusterState, Member, MemberState, Signal};
51use ractor::{Actor, ActorProcessingErr, ActorRef, ActorStatus};
52use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
53use tokio::{
54    sync::{RwLock, broadcast, mpsc, oneshot, watch},
55    task::JoinHandle,
56    time::Instant,
57};
58
59/// Serde re-export for defining sharding protocol messages without adding a
60/// second direct Serde dependency.
61pub use serde;
62
63/// The `datum-cluster-sharding` crate version.
64pub const VERSION: &str = env!("CARGO_PKG_VERSION");
65
66type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
67type PendingAskMap = Arc<Mutex<BTreeMap<u64, oneshot::Sender<Result<Vec<u8>, ShardingError>>>>>;
68type EntityRegistry = Arc<RwLock<BTreeMap<String, Arc<dyn DynEntityType>>>>;
69type AllocationCache = Arc<RwLock<BTreeMap<ShardKey, Allocation>>>;
70type MovingShardSet = Arc<RwLock<BTreeSet<ShardKey>>>;
71type FailedRememberShards = Arc<RwLock<BTreeMap<ShardKey, String>>>;
72
73const ALLOCATION_REPLICATION_ATTEMPTS: usize = 8;
74const ALLOCATION_REPLICATION_RETRY_DELAY: Duration = Duration::from_millis(50);
75
76/// Future returned by [`RememberEntitiesStore`] methods.
77pub type RememberEntitiesStoreFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
78
79/// Result alias for remember-entities stores.
80pub type RememberEntitiesStoreResult<T> = Result<T, RememberEntitiesStoreError>;
81
82/// Result alias for `datum-cluster-sharding`.
83pub type ShardingResult<T> = Result<T, ShardingError>;
84
85/// Errors returned by the sharding core.
86#[derive(Debug, thiserror::Error)]
87pub enum ShardingError {
88    /// Invalid sharding configuration or entity registration.
89    #[error("invalid sharding config: {0}")]
90    InvalidConfig(String),
91    /// The sharding region has stopped.
92    #[error("sharding region stopped")]
93    Stopped,
94    /// A shard has no eligible owner.
95    #[error("no eligible shard owner: {0}")]
96    NoEligibleShardOwner(String),
97    /// The entity type is not registered on this region.
98    #[error("entity type is not registered: {0}")]
99    EntityTypeNotRegistered(String),
100    /// Allocation buffering exceeded its configured bound.
101    #[error("shard allocation buffer overflow for {type_name}/{shard_id}")]
102    BufferOverflow { type_name: String, shard_id: String },
103    /// A request exceeded its bounded wait.
104    #[error("sharding request timed out after {0:?}")]
105    Timeout(Duration),
106    /// Serialization failed.
107    #[error("sharding codec failed: {0}")]
108    Codec(String),
109    /// A peer node-session request failed.
110    #[error("node session failed: {0}")]
111    Session(String),
112    /// Entity actor spawn or send failed.
113    #[error("entity actor failed: {0}")]
114    Actor(String),
115    /// Remember-entities storage failed for a shard.
116    #[error("remember-entities store failed for {type_name}/{shard_id}: {message}")]
117    RememberEntities {
118        /// Entity type name.
119        type_name: String,
120        /// Shard id.
121        shard_id: String,
122        /// Failure details.
123        message: String,
124    },
125}
126
127impl ShardingError {
128    fn codec(error: impl std::fmt::Display) -> Self {
129        Self::Codec(error.to_string())
130    }
131}
132
133/// Error returned by a [`RememberEntitiesStore`].
134#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
135#[error("{message}")]
136pub struct RememberEntitiesStoreError {
137    message: String,
138}
139
140impl RememberEntitiesStoreError {
141    /// Create a store error from a displayable message.
142    #[must_use]
143    pub fn new(message: impl Into<String>) -> Self {
144        Self {
145            message: message.into(),
146        }
147    }
148
149    /// Error message.
150    #[must_use]
151    pub fn message(&self) -> &str {
152        &self.message
153    }
154}
155
156impl From<std::io::Error> for RememberEntitiesStoreError {
157    fn from(error: std::io::Error) -> Self {
158        Self::new(error.to_string())
159    }
160}
161
162/// Store SPI for remembered entity ids.
163///
164/// Writes are invoked by a bounded write-behind worker, not directly on the
165/// entity message hot path. `list_entities_for_shard` is used when a shard
166/// starts locally so remembered entities can be spawned without waiting for the
167/// next user message. `list_shards` has a default empty implementation and is
168/// used by the built-in stores to recover shards after a node restart.
169pub trait RememberEntitiesStore: Send + Sync + 'static {
170    /// Record that `entity_id` has started in `type_name`/`shard_id`.
171    fn record_entity_started<'a>(
172        &'a self,
173        type_name: &'a str,
174        shard_id: &'a str,
175        entity_id: &'a str,
176    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>>;
177
178    /// Record that `entity_id` has stopped/passivated in `type_name`/`shard_id`.
179    fn record_entity_stopped<'a>(
180        &'a self,
181        type_name: &'a str,
182        shard_id: &'a str,
183        entity_id: &'a str,
184    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>>;
185
186    /// Return remembered entity ids for one shard.
187    fn list_entities_for_shard<'a>(
188        &'a self,
189        type_name: &'a str,
190        shard_id: &'a str,
191    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<Vec<String>>>;
192
193    /// Return shards known to the store for one entity type.
194    fn list_shards<'a>(
195        &'a self,
196        _type_name: &'a str,
197    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<Vec<String>>> {
198        Box::pin(async { Ok(Vec::new()) })
199    }
200
201    /// Synchronise outstanding writes so a resolved `RememberEntitiesRuntime::flush`
202    /// makes prior starts/stops visible to a reader that opens the store after the
203    /// flush resolves (e.g., a node restart).
204    fn flush<'a>(&'a self) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>> {
205        Box::pin(async { Ok(()) })
206    }
207}
208
209/// Availability policy when remember-entities storage fails.
210#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
211pub enum RememberEntitiesFailurePolicy {
212    /// Keep routing entity messages and publish a shard-level error event.
213    #[default]
214    FailOpen,
215    /// Mark the shard failed after a store error; future messages fail fast.
216    FailClosed,
217}
218
219/// Remember-entities operation attached to a failure event.
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum RememberEntitiesOperation {
222    /// A started entity id was being recorded.
223    RecordStarted,
224    /// A stopped/passivated entity id was being recorded.
225    RecordStopped,
226    /// Remembered entities were being listed for shard startup.
227    ListEntities,
228    /// Remembered shards were being listed for restart recovery.
229    ListShards,
230}
231
232/// Shard-level remember-entities error event.
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct RememberEntitiesEvent {
235    /// Entity type name.
236    pub type_name: String,
237    /// Shard id, if the failing operation is shard-scoped.
238    pub shard_id: String,
239    /// Entity id, if the failing operation is entity-scoped.
240    pub entity_id: Option<String>,
241    /// Operation that failed.
242    pub operation: RememberEntitiesOperation,
243    /// Configured failure policy at the time of failure.
244    pub policy: RememberEntitiesFailurePolicy,
245    /// Store or queue failure details.
246    pub message: String,
247}
248
249/// Remember-entities configuration for a sharding region.
250#[derive(Clone)]
251pub struct RememberEntitiesConfig {
252    store: Option<Arc<dyn RememberEntitiesStore>>,
253    /// Bounded write-behind queue capacity.
254    pub queue_capacity: usize,
255    /// Store failure behavior.
256    pub failure_policy: RememberEntitiesFailurePolicy,
257    /// Bounded event-feed capacity for remember-entities errors.
258    pub event_buffer: usize,
259}
260
261impl fmt::Debug for RememberEntitiesConfig {
262    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263        formatter
264            .debug_struct("RememberEntitiesConfig")
265            .field("enabled", &self.store.is_some())
266            .field("queue_capacity", &self.queue_capacity)
267            .field("failure_policy", &self.failure_policy)
268            .field("event_buffer", &self.event_buffer)
269            .finish()
270    }
271}
272
273impl Default for RememberEntitiesConfig {
274    fn default() -> Self {
275        Self::disabled()
276    }
277}
278
279impl RememberEntitiesConfig {
280    /// Disable remember-entities. This is the default.
281    #[must_use]
282    pub const fn disabled() -> Self {
283        Self {
284            store: None,
285            queue_capacity: 4096,
286            failure_policy: RememberEntitiesFailurePolicy::FailOpen,
287            event_buffer: 1024,
288        }
289    }
290
291    /// Enable remember-entities with `store`.
292    #[must_use]
293    pub fn with_store(store: Arc<dyn RememberEntitiesStore>) -> Self {
294        Self {
295            store: Some(store),
296            ..Self::disabled()
297        }
298    }
299
300    /// Return whether a store is configured.
301    #[must_use]
302    pub fn is_enabled(&self) -> bool {
303        self.store.is_some()
304    }
305
306    /// Set the write-behind queue capacity.
307    #[must_use]
308    pub const fn with_queue_capacity(mut self, capacity: usize) -> Self {
309        self.queue_capacity = capacity;
310        self
311    }
312
313    /// Set the store failure policy.
314    #[must_use]
315    pub const fn with_failure_policy(mut self, policy: RememberEntitiesFailurePolicy) -> Self {
316        self.failure_policy = policy;
317        self
318    }
319
320    /// Set the remember error event buffer.
321    #[must_use]
322    pub const fn with_event_buffer(mut self, capacity: usize) -> Self {
323        self.event_buffer = capacity;
324        self
325    }
326}
327
328/// In-memory remembered entity store.
329///
330/// This store is intended for tests and single-process development. It survives
331/// shard rebalance when all regions share the same `InMemoryStore` value, but it
332/// is process memory and does not survive process death.
333#[derive(Debug, Default, Clone)]
334pub struct InMemoryStore {
335    entities: Arc<Mutex<BTreeMap<RememberShardKey, BTreeSet<String>>>>,
336}
337
338impl InMemoryStore {
339    /// Create an empty in-memory store.
340    #[must_use]
341    pub fn new() -> Self {
342        Self::default()
343    }
344}
345
346impl RememberEntitiesStore for InMemoryStore {
347    fn record_entity_started<'a>(
348        &'a self,
349        type_name: &'a str,
350        shard_id: &'a str,
351        entity_id: &'a str,
352    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>> {
353        Box::pin(async move {
354            let mut locked = self
355                .entities
356                .lock()
357                .map_err(|_| RememberEntitiesStoreError::new("in-memory store poisoned"))?;
358            locked
359                .entry(RememberShardKey::new(type_name, shard_id))
360                .or_default()
361                .insert(entity_id.to_owned());
362            Ok(())
363        })
364    }
365
366    fn record_entity_stopped<'a>(
367        &'a self,
368        type_name: &'a str,
369        shard_id: &'a str,
370        entity_id: &'a str,
371    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>> {
372        Box::pin(async move {
373            let mut locked = self
374                .entities
375                .lock()
376                .map_err(|_| RememberEntitiesStoreError::new("in-memory store poisoned"))?;
377            let key = RememberShardKey::new(type_name, shard_id);
378            if let Some(entities) = locked.get_mut(&key) {
379                entities.remove(entity_id);
380                if entities.is_empty() {
381                    locked.remove(&key);
382                }
383            }
384            Ok(())
385        })
386    }
387
388    fn list_entities_for_shard<'a>(
389        &'a self,
390        type_name: &'a str,
391        shard_id: &'a str,
392    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<Vec<String>>> {
393        Box::pin(async move {
394            let locked = self
395                .entities
396                .lock()
397                .map_err(|_| RememberEntitiesStoreError::new("in-memory store poisoned"))?;
398            Ok(locked
399                .get(&RememberShardKey::new(type_name, shard_id))
400                .map(|entities| entities.iter().cloned().collect())
401                .unwrap_or_default())
402        })
403    }
404
405    fn list_shards<'a>(
406        &'a self,
407        type_name: &'a str,
408    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<Vec<String>>> {
409        Box::pin(async move {
410            let locked = self
411                .entities
412                .lock()
413                .map_err(|_| RememberEntitiesStoreError::new("in-memory store poisoned"))?;
414            Ok(locked
415                .keys()
416                .filter(|key| key.type_name == type_name)
417                .map(|key| key.shard_id.clone())
418                .collect())
419        })
420    }
421}
422
423/// File-store fsync policy.
424#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
425pub enum FileStoreFsyncPolicy {
426    /// Do not call `fsync`; durability is delegated to the operating system.
427    Never,
428    /// Fsync compacted logs, but not append records.
429    OnCompaction,
430    /// Fsync every append and compaction. This is the default durability level.
431    #[default]
432    Always,
433}
434
435impl FileStoreFsyncPolicy {
436    const fn sync_append(self) -> bool {
437        matches!(self, Self::Always)
438    }
439
440    const fn sync_compaction(self) -> bool {
441        matches!(self, Self::OnCompaction | Self::Always)
442    }
443}
444
445/// Per-shard append-log remember-entities store.
446///
447/// Each shard is stored as `<dir>/<hex(type_name)>/<hex(shard_id)>.log`.
448/// Starts append one record. Stops/passivation rewrite the shard log with the
449/// remaining live entity ids, which compacts tombstones immediately. With
450/// [`FileStoreFsyncPolicy::Always`], each append and compaction is fsynced
451/// before the store method returns to the write-behind worker; because writes
452/// are still write-behind, a delivered message is acknowledged before the store
453/// record is necessarily durable.
454#[derive(Debug, Clone)]
455pub struct FileStore {
456    dir: PathBuf,
457    fsync_policy: FileStoreFsyncPolicy,
458}
459
460impl FileStore {
461    /// Create a file store rooted at `dir`.
462    #[must_use]
463    pub fn new(dir: impl Into<PathBuf>) -> Self {
464        Self {
465            dir: dir.into(),
466            fsync_policy: FileStoreFsyncPolicy::default(),
467        }
468    }
469
470    /// Set the fsync policy.
471    #[must_use]
472    pub const fn with_fsync_policy(mut self, policy: FileStoreFsyncPolicy) -> Self {
473        self.fsync_policy = policy;
474        self
475    }
476}
477
478impl RememberEntitiesStore for FileStore {
479    fn record_entity_started<'a>(
480        &'a self,
481        type_name: &'a str,
482        shard_id: &'a str,
483        entity_id: &'a str,
484    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>> {
485        let path = shard_log_path(&self.dir, type_name, shard_id);
486        let entity_id = entity_id.to_owned();
487        let fsync_policy = self.fsync_policy;
488        Box::pin(async move {
489            tokio::task::spawn_blocking(move || {
490                append_file_store_record(&path, 'S', &entity_id, fsync_policy)
491            })
492            .await
493            .map_err(|error| RememberEntitiesStoreError::new(error.to_string()))?
494        })
495    }
496
497    fn record_entity_stopped<'a>(
498        &'a self,
499        type_name: &'a str,
500        shard_id: &'a str,
501        entity_id: &'a str,
502    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>> {
503        let path = shard_log_path(&self.dir, type_name, shard_id);
504        let entity_id = entity_id.to_owned();
505        let fsync_policy = self.fsync_policy;
506        Box::pin(async move {
507            tokio::task::spawn_blocking(move || {
508                let mut entities = read_file_store_entities(&path)?;
509                entities.remove(&entity_id);
510                compact_file_store_log(&path, &entities, fsync_policy)
511            })
512            .await
513            .map_err(|error| RememberEntitiesStoreError::new(error.to_string()))?
514        })
515    }
516
517    fn list_entities_for_shard<'a>(
518        &'a self,
519        type_name: &'a str,
520        shard_id: &'a str,
521    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<Vec<String>>> {
522        let path = shard_log_path(&self.dir, type_name, shard_id);
523        Box::pin(async move {
524            tokio::task::spawn_blocking(move || {
525                read_file_store_entities(&path).map(|entities| entities.into_iter().collect())
526            })
527            .await
528            .map_err(|error| RememberEntitiesStoreError::new(error.to_string()))?
529        })
530    }
531
532    fn list_shards<'a>(
533        &'a self,
534        type_name: &'a str,
535    ) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<Vec<String>>> {
536        let type_dir = self.dir.join(hex_encode(type_name.as_bytes()));
537        Box::pin(async move {
538            tokio::task::spawn_blocking(move || list_file_store_shards(&type_dir))
539                .await
540                .map_err(|error| RememberEntitiesStoreError::new(error.to_string()))?
541        })
542    }
543
544    fn flush<'a>(&'a self) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>> {
545        let dir = self.dir.clone();
546        Box::pin(async move {
547            tokio::task::spawn_blocking(move || {
548                let mut last_error: Option<std::io::Error> = None;
549                fn sync_dir(path: &std::path::Path, last_error: &mut Option<std::io::Error>) {
550                    let Ok(entries) = std::fs::read_dir(path) else {
551                        return;
552                    };
553                    for entry in entries.flatten() {
554                        let entry_path = entry.path();
555                        if entry_path.is_dir() {
556                            sync_dir(&entry_path, last_error);
557                        } else if entry_path.extension().and_then(|e| e.to_str()) == Some("log")
558                            && let Ok(file) = std::fs::File::open(&entry_path)
559                            && let Err(error) = file.sync_data()
560                        {
561                            *last_error = Some(error);
562                        }
563                    }
564                }
565                sync_dir(&dir, &mut last_error);
566                if let Some(error) = last_error {
567                    Err(RememberEntitiesStoreError::new(error.to_string()))
568                } else {
569                    Ok(())
570                }
571            })
572            .await
573            .map_err(|error| RememberEntitiesStoreError::new(error.to_string()))?
574        })
575    }
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
579struct RememberShardKey {
580    type_name: String,
581    shard_id: String,
582}
583
584impl RememberShardKey {
585    fn new(type_name: &str, shard_id: &str) -> Self {
586        Self {
587            type_name: type_name.to_owned(),
588            shard_id: shard_id.to_owned(),
589        }
590    }
591}
592
593fn shard_log_path(root: &Path, type_name: &str, shard_id: &str) -> PathBuf {
594    root.join(hex_encode(type_name.as_bytes()))
595        .join(format!("{}.log", hex_encode(shard_id.as_bytes())))
596}
597
598fn append_file_store_record(
599    path: &Path,
600    op: char,
601    entity_id: &str,
602    fsync_policy: FileStoreFsyncPolicy,
603) -> RememberEntitiesStoreResult<()> {
604    if let Some(parent) = path.parent() {
605        fs::create_dir_all(parent)?;
606    }
607    let mut file = OpenOptions::new().create(true).append(true).open(path)?;
608    writeln!(file, "{op}\t{}", hex_encode(entity_id.as_bytes()))?;
609    if fsync_policy.sync_append() {
610        file.sync_all()?;
611    }
612    Ok(())
613}
614
615fn compact_file_store_log(
616    path: &Path,
617    entities: &BTreeSet<String>,
618    fsync_policy: FileStoreFsyncPolicy,
619) -> RememberEntitiesStoreResult<()> {
620    if let Some(parent) = path.parent() {
621        fs::create_dir_all(parent)?;
622    }
623    let tmp_path = path.with_extension("log.tmp");
624    {
625        let mut file = File::create(&tmp_path)?;
626        for entity_id in entities {
627            writeln!(file, "S\t{}", hex_encode(entity_id.as_bytes()))?;
628        }
629        if fsync_policy.sync_compaction() {
630            file.sync_all()?;
631        }
632    }
633    fs::rename(&tmp_path, path)?;
634    Ok(())
635}
636
637fn read_file_store_entities(path: &Path) -> RememberEntitiesStoreResult<BTreeSet<String>> {
638    let file = match File::open(path) {
639        Ok(file) => file,
640        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
641            return Ok(BTreeSet::new());
642        }
643        Err(error) => return Err(error.into()),
644    };
645    let mut entities = BTreeSet::new();
646    for line in BufReader::new(file).lines() {
647        let line = line?;
648        if line.trim().is_empty() {
649            continue;
650        }
651        let Some((op, encoded_entity)) = line.split_once('\t') else {
652            return Err(RememberEntitiesStoreError::new(
653                "malformed remember-entities log record",
654            ));
655        };
656        let entity_id = String::from_utf8(hex_decode(encoded_entity)?)
657            .map_err(|error| RememberEntitiesStoreError::new(error.to_string()))?;
658        match op {
659            "S" => {
660                entities.insert(entity_id);
661            }
662            "T" => {
663                entities.remove(&entity_id);
664            }
665            _ => {
666                return Err(RememberEntitiesStoreError::new(
667                    "unknown remember-entities log operation",
668                ));
669            }
670        }
671    }
672    Ok(entities)
673}
674
675fn list_file_store_shards(type_dir: &Path) -> RememberEntitiesStoreResult<Vec<String>> {
676    let entries = match fs::read_dir(type_dir) {
677        Ok(entries) => entries,
678        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
679        Err(error) => return Err(error.into()),
680    };
681    let mut shards = Vec::new();
682    for entry in entries {
683        let entry = entry?;
684        let path = entry.path();
685        if path.extension().and_then(|extension| extension.to_str()) != Some("log") {
686            continue;
687        }
688        let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
689            continue;
690        };
691        let shard_id = String::from_utf8(hex_decode(stem)?)
692            .map_err(|error| RememberEntitiesStoreError::new(error.to_string()))?;
693        shards.push(shard_id);
694    }
695    shards.sort();
696    shards.dedup();
697    Ok(shards)
698}
699
700fn hex_encode(bytes: &[u8]) -> String {
701    const HEX: &[u8; 16] = b"0123456789abcdef";
702    let mut encoded = String::with_capacity(bytes.len() * 2);
703    for byte in bytes {
704        encoded.push(HEX[(byte >> 4) as usize] as char);
705        encoded.push(HEX[(byte & 0x0f) as usize] as char);
706    }
707    encoded
708}
709
710fn hex_decode(encoded: &str) -> RememberEntitiesStoreResult<Vec<u8>> {
711    let bytes = encoded.as_bytes();
712    if !bytes.len().is_multiple_of(2) {
713        return Err(RememberEntitiesStoreError::new("odd-length hex string"));
714    }
715    let mut decoded = Vec::with_capacity(bytes.len() / 2);
716    for pair in bytes.chunks_exact(2) {
717        let high = hex_value(pair[0])?;
718        let low = hex_value(pair[1])?;
719        decoded.push((high << 4) | low);
720    }
721    Ok(decoded)
722}
723
724fn hex_value(byte: u8) -> RememberEntitiesStoreResult<u8> {
725    match byte {
726        b'0'..=b'9' => Ok(byte - b'0'),
727        b'a'..=b'f' => Ok(byte - b'a' + 10),
728        b'A'..=b'F' => Ok(byte - b'A' + 10),
729        _ => Err(RememberEntitiesStoreError::new("invalid hex digit")),
730    }
731}
732
733/// Configuration for one sharding region.
734#[derive(Debug, Clone)]
735pub struct ShardingConfig {
736    /// Default shard count used by [`DefaultShardExtractor`].
737    pub num_shards: u64,
738    /// Maximum envelopes buffered per shard while an allocation request is in
739    /// flight.
740    pub allocation_buffer: usize,
741    /// Bounded wait for coordinator, peer-region, and ask requests.
742    pub request_timeout: Duration,
743    /// Region command queue capacity.
744    pub command_buffer: usize,
745    /// Optional role that shard-owning nodes must advertise.
746    pub role_constraint: Option<String>,
747    /// Cluster-agent role used to discover C2-capable peers.
748    pub agent_role: String,
749    /// Coordinator takeover/rebuild poll interval.
750    pub coordinator_tick: Duration,
751    /// Maximum graceful rebalance moves the coordinator may initiate per round.
752    ///
753    /// Dead-owner reallocations are not capped; this limit applies to
754    /// coordinator-initiated spread after a node joins.
755    pub rebalance_per_round: usize,
756    /// Optional idle timeout after which a local entity actor is passivated.
757    ///
758    /// The next message for the entity respawns it on demand.
759    pub passivation_idle_timeout: Option<Duration>,
760    /// Delay before a region drains envelopes buffered for a moving shard.
761    pub handoff_drain_delay: Duration,
762    /// Remember-entities store and write-behind policy.
763    ///
764    /// Disabled by default. Entity types must also be registered through
765    /// [`ShardingHandle::register_remembered_entity_type`] before their ids are
766    /// written to or recovered from the store.
767    pub remember_entities: RememberEntitiesConfig,
768}
769
770impl Default for ShardingConfig {
771    fn default() -> Self {
772        Self {
773            num_shards: 128,
774            allocation_buffer: 1024,
775            request_timeout: Duration::from_millis(750),
776            command_buffer: 65_536,
777            role_constraint: None,
778            agent_role: AGENT_ROLE.to_owned(),
779            coordinator_tick: Duration::from_millis(50),
780            rebalance_per_round: 10,
781            passivation_idle_timeout: None,
782            handoff_drain_delay: Duration::from_millis(1),
783            remember_entities: RememberEntitiesConfig::default(),
784        }
785    }
786}
787
788/// Coordinator rebalance reason.
789#[derive(Debug, Clone, Copy, PartialEq, Eq)]
790pub enum RebalanceReason {
791    /// The previous owner is no longer an eligible shard owner.
792    DeadOwner,
793    /// A live shard was moved to spread load after membership changed.
794    GracefulSpread,
795}
796
797/// One shard movement recorded by the active coordinator.
798#[derive(Debug, Clone, PartialEq, Eq)]
799pub struct ShardMovement {
800    /// Entity type name.
801    pub type_name: String,
802    /// Shard id.
803    pub shard_id: String,
804    /// Previous owner.
805    pub from_node: String,
806    /// New owner.
807    pub to_node: String,
808    /// Allocation generation assigned to this movement.
809    pub generation: u64,
810}
811
812/// One coordinator rebalance round.
813#[derive(Debug, Clone, PartialEq, Eq)]
814pub struct RebalanceRound {
815    /// Monotonic coordinator-local round id.
816    pub round: u64,
817    /// Why the round moved shards.
818    pub reason: RebalanceReason,
819    /// Shards moved in this round.
820    pub movements: Vec<ShardMovement>,
821}
822
823/// User-facing sharding envelope.
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
825pub struct ShardEnvelope<M> {
826    /// Stable entity id.
827    pub entity_id: String,
828    /// User message.
829    pub message: M,
830}
831
832impl<M> ShardEnvelope<M> {
833    /// Create a new envelope.
834    #[must_use]
835    pub fn new(entity_id: impl Into<String>, message: M) -> Self {
836        Self {
837            entity_id: entity_id.into(),
838            message,
839        }
840    }
841}
842
843/// Extracts entity and shard ids, mirroring Akka's entity/shard extractors.
844pub trait ShardExtractor<M>: Send + Sync + 'static {
845    /// Return the entity id for `envelope`.
846    fn entity_id<'a>(&self, envelope: &'a ShardEnvelope<M>) -> &'a str {
847        &envelope.entity_id
848    }
849
850    /// Return the shard id for `entity_id`.
851    fn shard_id(&self, entity_id: &str) -> String;
852}
853
854/// Hash-based default extractor: FNV-1a 64-bit `(entity_id) % num_shards`.
855///
856/// # Cross-node shard stability contract
857///
858/// The FNV-1a 64-bit hash used here is **version-stable forever** (public-domain
859/// constants). Every Datum node that runs the same `num_shards` MUST produce identical
860/// `shard_id` values for the same `entity_id`, regardless of Rust compiler version,
861/// target OS/arch, or Datum release. This prevents split-entity faults in
862/// mixed-version clusters during rolling upgrades. The golden test
863/// `golden_shard_map_is_stable` pins known entity-id-to-shard mappings; any hash
864/// change will fail that test loudly.
865///
866/// A one-time re-mapping of existing entities onto new shards after the switch from
867/// `std::DefaultHasher` (SipHash) to FNV-1a is expected and acceptable pre-1.0.
868#[derive(Debug, Clone)]
869pub struct DefaultShardExtractor {
870    num_shards: u64,
871}
872
873impl DefaultShardExtractor {
874    /// Create a default extractor with `num_shards`.
875    #[must_use]
876    pub fn new(num_shards: u64) -> Self {
877        Self {
878            num_shards: num_shards.max(1),
879        }
880    }
881
882    /// Configured shard count.
883    #[must_use]
884    pub const fn num_shards(&self) -> u64 {
885        self.num_shards
886    }
887}
888
889impl<M> ShardExtractor<M> for DefaultShardExtractor {
890    fn shard_id(&self, entity_id: &str) -> String {
891        (fnv1a_64(entity_id.as_bytes()) % self.num_shards).to_string()
892    }
893}
894
895const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
896const FNV_PRIME: u64 = 0x100000001b3;
897
898fn fnv1a_64(data: &[u8]) -> u64 {
899    let mut hash = FNV_OFFSET_BASIS;
900    for &byte in data {
901        hash ^= byte as u64;
902        hash = hash.wrapping_mul(FNV_PRIME);
903    }
904    hash
905}
906
907/// Context passed to an entity factory.
908#[derive(Debug, Clone)]
909pub struct EntityContext {
910    /// Entity type name.
911    pub type_name: String,
912    /// Entity id.
913    pub entity_id: String,
914}
915
916/// Handler owned by one Ractor entity actor.
917pub trait EntityBehavior<M>: Send + 'static {
918    /// Handle one message.
919    fn handle(&mut self, context: &EntityContext, message: M) -> ShardingResult<()>;
920}
921
922impl<M, F> EntityBehavior<M> for F
923where
924    F: FnMut(&EntityContext, M) -> ShardingResult<()> + Send + 'static,
925{
926    fn handle(&mut self, context: &EntityContext, message: M) -> ShardingResult<()> {
927        self(context, message)
928    }
929}
930
931/// Future that spawns one actor-backed sharding entity.
932pub type ActorEntitySpawnFuture<M> =
933    Pin<Box<dyn Future<Output = ShardingResult<ActorEntityHandle<M>>> + Send + 'static>>;
934
935/// Spawnable actor entity created by an [`EntityContext`] factory.
936///
937/// This is the actor-native registration seam corresponding to Akka's
938/// `EntityContext<M> => Behavior<M>` shape. The returned
939/// [`ActorEntityHandle`] supplies the typed ingress used by the shard and the
940/// lifecycle controls used for passivation and handoff.
941pub trait ActorEntity<M>: Send + 'static {
942    /// Spawn the entity actor and return its typed ingress.
943    fn spawn(self: Box<Self>) -> ActorEntitySpawnFuture<M>;
944}
945
946impl<M, F, Fut> ActorEntity<M> for F
947where
948    M: 'static,
949    F: FnOnce() -> Fut + Send + 'static,
950    Fut: Future<Output = ShardingResult<ActorEntityHandle<M>>> + Send + 'static,
951{
952    fn spawn(self: Box<Self>) -> ActorEntitySpawnFuture<M> {
953        Box::pin((*self)())
954    }
955}
956
957/// Typed ingress and lifecycle controls for a spawned [`ActorEntity`].
958///
959/// [`Self::from_actor_ref`] adapts a Ractor actor whose mailbox message may
960/// wrap the sharded message. This lets actor behaviors keep private runtime
961/// protocol messages while exposing only `M` at the sharding boundary.
962pub struct ActorEntityHandle<M> {
963    tell: Arc<dyn Fn(M) -> ShardingResult<()> + Send + Sync>,
964    stop: Arc<dyn Fn() + Send + Sync>,
965    begin_quiesce: Arc<dyn Fn() + Send + Sync>,
966    wait_stopped: Arc<dyn Fn() -> BoxFuture<'static, ()> + Send + Sync>,
967    is_stopped: Arc<dyn Fn() -> bool + Send + Sync>,
968}
969
970impl<M> Clone for ActorEntityHandle<M> {
971    fn clone(&self) -> Self {
972        Self {
973            tell: Arc::clone(&self.tell),
974            stop: Arc::clone(&self.stop),
975            begin_quiesce: Arc::clone(&self.begin_quiesce),
976            wait_stopped: Arc::clone(&self.wait_stopped),
977            is_stopped: Arc::clone(&self.is_stopped),
978        }
979    }
980}
981
982impl<M> ActorEntityHandle<M> {
983    fn is_same_entity(&self, other: &Self) -> bool {
984        Arc::ptr_eq(&self.tell, &other.tell)
985    }
986}
987
988impl<M> ActorEntityHandle<M>
989where
990    M: Send + 'static,
991{
992    /// Adapt a Ractor actor ref into a sharding entity ingress.
993    ///
994    /// `make_message` maps each sharded `M` into the actor's mailbox protocol.
995    /// Passivation and shard handoff stop the supplied actor ref. Once that
996    /// actor begins draining or stopping, the next sharding delivery creates a
997    /// fresh entity from the registered factory.
998    pub fn from_actor_ref<A, F>(actor: ActorRef<A>, make_message: F) -> Self
999    where
1000        A: ractor::Message,
1001        F: Fn(M) -> A + Send + Sync + 'static,
1002    {
1003        let tell_actor = actor.clone();
1004        let quiesce_actor = actor.clone();
1005        let wait_actor = actor.clone();
1006        let status_actor = actor.clone();
1007        Self {
1008            tell: Arc::new(move |message| {
1009                tell_actor.cast(make_message(message)).map_err(|_error| {
1010                    ShardingError::Actor("actor entity ingress is stopped".to_owned())
1011                })
1012            }),
1013            stop: Arc::new(move || actor.stop(Some("sharding entity stopped".to_owned()))),
1014            begin_quiesce: Arc::new(move || {
1015                // `drain` changes the actor status before queuing its FIFO drain
1016                // marker. New casts are rejected while messages accepted before
1017                // the marker finish ahead of actor termination.
1018                let _ = quiesce_actor.drain();
1019            }),
1020            wait_stopped: Arc::new(move || {
1021                let actor = wait_actor.clone();
1022                Box::pin(async move {
1023                    while actor.get_status() != ActorStatus::Stopped {
1024                        let _ = actor.wait(None).await;
1025                    }
1026                })
1027            }),
1028            is_stopped: Arc::new(move || {
1029                matches!(
1030                    status_actor.get_status(),
1031                    ActorStatus::Draining | ActorStatus::Stopping | ActorStatus::Stopped
1032                )
1033            }),
1034        }
1035    }
1036
1037    /// Deliver one sharded message to the entity actor's mailbox.
1038    pub fn tell(&self, message: M) -> ShardingResult<()> {
1039        (self.tell)(message)
1040    }
1041
1042    /// Stop the entity actor for passivation or shard handoff.
1043    pub fn stop(&self) {
1044        (self.stop)();
1045    }
1046
1047    fn begin_quiesce(&self) {
1048        (self.begin_quiesce)();
1049    }
1050
1051    async fn wait_stopped(&self) {
1052        (self.wait_stopped)().await;
1053    }
1054
1055    /// Return whether the entity actor has begun stopping.
1056    #[must_use]
1057    pub fn is_stopped(&self) -> bool {
1058        (self.is_stopped)()
1059    }
1060}
1061
1062/// Reply port for sharded asks.
1063///
1064/// The port serializes as an opaque reply target when a message crosses a C2
1065/// session. The receiving entity can call `send` exactly like a local reply
1066/// port; the region returns the serialized reply to the originating region.
1067pub struct ReplyPort<T> {
1068    target: WireReplyTarget,
1069    local_node: String,
1070    commands: mpsc::Sender<RegionCommand>,
1071    sessions: NodeSessionManagerHandle,
1072    pending_asks: PendingAskMap,
1073    _marker: PhantomData<T>,
1074}
1075
1076impl<T> std::fmt::Debug for ReplyPort<T> {
1077    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1078        formatter
1079            .debug_struct("ReplyPort")
1080            .field("origin_node", &self.target.origin_node)
1081            .field("request_id", &self.target.request_id)
1082            .finish()
1083    }
1084}
1085
1086/// Error returned when a sharded reply cannot be sent.
1087#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1088pub struct ReplySendError;
1089
1090impl std::fmt::Display for ReplySendError {
1091    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1092        formatter.write_str("sharding reply receiver dropped")
1093    }
1094}
1095
1096impl std::error::Error for ReplySendError {}
1097
1098impl<T> ReplyPort<T> {
1099    fn new(
1100        target: WireReplyTarget,
1101        local_node: String,
1102        commands: mpsc::Sender<RegionCommand>,
1103        sessions: NodeSessionManagerHandle,
1104        pending_asks: PendingAskMap,
1105    ) -> Self {
1106        Self {
1107            target,
1108            local_node,
1109            commands,
1110            sessions,
1111            pending_asks,
1112            _marker: PhantomData,
1113        }
1114    }
1115}
1116
1117impl<T> ReplyPort<T>
1118where
1119    T: Serialize + Send + 'static,
1120{
1121    /// Send the ask reply.
1122    pub fn send(self, reply: T) -> Result<(), ReplySendError> {
1123        let payload = encode(&reply).map_err(|_error| ReplySendError)?;
1124        if self.target.origin_node == self.local_node {
1125            complete_pending_ask(
1126                &self.pending_asks,
1127                self.target.request_id,
1128                true,
1129                payload,
1130                String::new(),
1131            );
1132            return Ok(());
1133        }
1134        let response = CompleteShardingAsk {
1135            request_id: self.target.request_id,
1136            ok: true,
1137            payload: payload.clone(),
1138            message: String::new(),
1139        };
1140        if self
1141            .sessions
1142            .try_complete_sharding_ask_pipe(&self.target.origin_node, response)
1143            .is_ok()
1144        {
1145            return Ok(());
1146        }
1147        self.commands
1148            .try_send(RegionCommand::SendAskReply {
1149                target: self.target,
1150                ok: true,
1151                payload,
1152                message: String::new(),
1153            })
1154            .map_err(|_error| ReplySendError)
1155    }
1156}
1157
1158impl<T> Serialize for ReplyPort<T> {
1159    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1160    where
1161        S: Serializer,
1162    {
1163        self.target.serialize(serializer)
1164    }
1165}
1166
1167impl<'de, T> Deserialize<'de> for ReplyPort<T> {
1168    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1169    where
1170        D: Deserializer<'de>,
1171    {
1172        let target = WireReplyTarget::deserialize(deserializer)?;
1173        let context = decode_context().ok_or_else(|| {
1174            serde::de::Error::custom("ReplyPort decoded outside sharding delivery context")
1175        })?;
1176        Ok(Self::new(
1177            target,
1178            context.local_node,
1179            context.commands,
1180            context.sessions,
1181            context.pending_asks,
1182        ))
1183    }
1184}
1185
1186#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1187struct WireReplyTarget {
1188    origin_node: String,
1189    request_id: u64,
1190}
1191
1192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1193enum WirePayload {
1194    Message(Vec<u8>),
1195    Passivate,
1196}
1197
1198const WIRE_PAYLOAD_MAGIC: &[u8] = b"\0datum-sharding-v2\0";
1199
1200fn encode_wire_payload(payload: WirePayload) -> ShardingResult<Vec<u8>> {
1201    let mut encoded = WIRE_PAYLOAD_MAGIC.to_vec();
1202    encoded.extend(encode(&payload)?);
1203    Ok(encoded)
1204}
1205
1206fn decode_wire_payload(payload: Vec<u8>) -> ShardingResult<WirePayload> {
1207    if let Some(encoded) = payload.strip_prefix(WIRE_PAYLOAD_MAGIC) {
1208        return decode(encoded);
1209    }
1210    Ok(WirePayload::Message(payload))
1211}
1212
1213#[derive(Clone)]
1214struct ReplyContext {
1215    local_node: String,
1216    commands: mpsc::Sender<RegionCommand>,
1217    sessions: NodeSessionManagerHandle,
1218    pending_asks: PendingAskMap,
1219}
1220
1221thread_local! {
1222    static DECODE_CONTEXT: RefCell<Option<ReplyContext>> = const { RefCell::new(None) };
1223}
1224
1225fn decode_context() -> Option<ReplyContext> {
1226    DECODE_CONTEXT.with(|context| context.borrow().clone())
1227}
1228
1229fn with_decode_context<T>(
1230    reply_context: ReplyContext,
1231    f: impl FnOnce() -> ShardingResult<T>,
1232) -> ShardingResult<T> {
1233    DECODE_CONTEXT.with(|context| {
1234        let previous = context.replace(Some(reply_context));
1235        let result = f();
1236        context.replace(previous);
1237        result
1238    })
1239}
1240
1241/// Sharding entrypoint.
1242pub struct Sharding;
1243
1244impl Sharding {
1245    /// Start a sharding region on an already running cluster-aware agent.
1246    pub fn init(
1247        cluster_handle: &ClusterAgentHandle,
1248        config: ShardingConfig,
1249    ) -> ShardingResult<ShardingHandle> {
1250        validate_config(&config)?;
1251        let (commands, receiver) = mpsc::channel(config.command_buffer.max(1));
1252        let pending_asks = Arc::new(Mutex::new(BTreeMap::new()));
1253        let entity_types = Arc::new(RwLock::new(BTreeMap::new()));
1254        let allocation_cache = Arc::new(RwLock::new(BTreeMap::new()));
1255        let moving_shards = Arc::new(RwLock::new(BTreeSet::new()));
1256        let tasks = Arc::new(Mutex::new(Vec::new()));
1257        let (remember_events, _) = broadcast::channel(config.remember_entities.event_buffer.max(1));
1258        let remember = config.remember_entities.store.as_ref().map(|store| {
1259            let (runtime, task) = RememberEntitiesRuntime::start(
1260                Arc::clone(store),
1261                &config.remember_entities,
1262                remember_events.clone(),
1263            );
1264            tasks.lock().expect("sharding tasks poisoned").push(task);
1265            runtime
1266        });
1267        let state = RegionState {
1268            self_node: cluster_handle.cluster().node_id().to_owned(),
1269            cluster_state: cluster_handle.cluster().state(),
1270            sessions: cluster_handle.sessions().clone(),
1271            config: config.clone(),
1272            entity_types: Arc::clone(&entity_types),
1273            allocation_cache: Arc::clone(&allocation_cache),
1274            moving_shards: Arc::clone(&moving_shards),
1275            remember: remember.clone(),
1276            allocations: BTreeMap::new(),
1277            pending_allocations: BTreeMap::new(),
1278            allocation_inflight: BTreeMap::new(),
1279            handoffs: BTreeMap::new(),
1280            pending_asks: Arc::clone(&pending_asks),
1281            active_coordinator: false,
1282            rebuilding: false,
1283            next_generation: 1,
1284            next_rebalance_round: 1,
1285            rebalance_rounds: VecDeque::new(),
1286            commands: commands.clone(),
1287        };
1288
1289        let provider = Arc::new(ShardingProvider {
1290            commands: commands.clone(),
1291            pending_asks: Arc::clone(&pending_asks),
1292            timeout: config.request_timeout,
1293        });
1294        cluster_handle.server().set_sharding_view(provider);
1295
1296        let manager = tokio::spawn(run_region_manager(state, receiver));
1297        let tick_commands = commands.clone();
1298        let tick_interval = config.coordinator_tick;
1299        let tick = tokio::spawn(async move {
1300            let mut interval = tokio::time::interval(tick_interval);
1301            loop {
1302                interval.tick().await;
1303                if tick_commands.send(RegionCommand::Tick).await.is_err() {
1304                    break;
1305                }
1306            }
1307        });
1308        {
1309            let mut locked = tasks.lock().expect("sharding tasks poisoned");
1310            locked.push(manager);
1311            locked.push(tick);
1312        }
1313
1314        Ok(ShardingHandle {
1315            commands,
1316            sessions: cluster_handle.sessions().clone(),
1317            entity_types,
1318            allocation_cache,
1319            moving_shards,
1320            cluster_state: cluster_handle.cluster().state(),
1321            agent_role: config.agent_role.clone(),
1322            role_constraint: config.role_constraint.clone(),
1323            self_node: cluster_handle.cluster().node_id().to_owned(),
1324            default_extractor: Arc::new(DefaultShardExtractor::new(config.num_shards)),
1325            request_timeout: config.request_timeout,
1326            next_request_id: Arc::new(AtomicU64::new(1)),
1327            pending_asks,
1328            remember,
1329            remember_events,
1330            tasks,
1331        })
1332    }
1333}
1334
1335/// In-process single-node sharding host for local development and tests.
1336///
1337/// This owns both the cluster-aware agent and its sharding region, avoiding a
1338/// separate direct `datum-agent` dependency for single-node users.
1339pub struct SingleNodeSharding {
1340    region: ShardingHandle,
1341    agent: Option<ClusterAgentHandle>,
1342}
1343
1344impl SingleNodeSharding {
1345    /// Start one seedless cluster node and initialize a sharding region on it.
1346    pub async fn start(node_id: impl Into<String>, config: ShardingConfig) -> ShardingResult<Self> {
1347        let node_id = node_id.into();
1348        let mut agent_config = ClusterAgentConfig::default();
1349        agent_config.cluster.node_id = node_id.clone();
1350        agent_config.dcp.node_id = node_id;
1351        let agent = ClusterAgent::start(agent_config, DcpJobFactories::new())
1352            .await
1353            .map_err(|error| {
1354                ShardingError::Actor(format!(
1355                    "single-node cluster agent failed to start: {error}"
1356                ))
1357            })?;
1358        let region = match Sharding::init(&agent, config) {
1359            Ok(region) => region,
1360            Err(error) => {
1361                let _shutdown = agent.shutdown().await;
1362                return Err(error);
1363            }
1364        };
1365        Ok(Self {
1366            region,
1367            agent: Some(agent),
1368        })
1369    }
1370
1371    /// Borrow the running sharding region.
1372    #[must_use]
1373    pub fn region(&self) -> &ShardingHandle {
1374        &self.region
1375    }
1376
1377    /// Stop the sharding region and its owned cluster agent.
1378    pub async fn shutdown(mut self) -> ShardingResult<()> {
1379        self.region.shutdown().await;
1380        if let Some(agent) = self.agent.take() {
1381            agent.shutdown().await.map_err(|error| {
1382                ShardingError::Actor(format!(
1383                    "single-node cluster agent failed to shut down: {error}"
1384                ))
1385            })?;
1386        }
1387        Ok(())
1388    }
1389}
1390
1391/// Running sharding region handle.
1392#[derive(Clone)]
1393pub struct ShardingHandle {
1394    commands: mpsc::Sender<RegionCommand>,
1395    sessions: NodeSessionManagerHandle,
1396    entity_types: EntityRegistry,
1397    allocation_cache: AllocationCache,
1398    moving_shards: MovingShardSet,
1399    cluster_state: Signal<ClusterState>,
1400    agent_role: String,
1401    role_constraint: Option<String>,
1402    self_node: String,
1403    default_extractor: Arc<DefaultShardExtractor>,
1404    request_timeout: Duration,
1405    next_request_id: Arc<AtomicU64>,
1406    pending_asks: PendingAskMap,
1407    remember: Option<RememberEntitiesRuntime>,
1408    remember_events: broadcast::Sender<RememberEntitiesEvent>,
1409    tasks: Arc<Mutex<Vec<JoinHandle<()>>>>,
1410}
1411
1412impl ShardingHandle {
1413    /// Local cluster node id for this region.
1414    #[must_use]
1415    pub fn node_id(&self) -> &str {
1416        &self.self_node
1417    }
1418
1419    /// Register an entity type with a Ractor-backed factory.
1420    pub async fn register_entity_type<M, F, B>(
1421        &self,
1422        type_name: impl Into<String>,
1423        factory: F,
1424    ) -> ShardingResult<()>
1425    where
1426        M: Serialize + DeserializeOwned + Send + 'static,
1427        F: Fn(EntityContext) -> B + Send + Sync + 'static,
1428        B: EntityBehavior<M>,
1429    {
1430        let type_name = type_name.into();
1431        if type_name.trim().is_empty() {
1432            return Err(ShardingError::InvalidConfig(
1433                "entity type name must not be empty".to_owned(),
1434            ));
1435        }
1436        let runtime = EntityTypeState::<M>::new(type_name.clone(), factory);
1437        self.register_entity_type_runtime(type_name, runtime).await
1438    }
1439
1440    /// Register an entity type whose factory creates a spawnable actor entity.
1441    ///
1442    /// The factory mirrors Akka Cluster Sharding's
1443    /// `EntityContext<M> => Behavior<M>` boundary: it is invoked once for each
1444    /// entity incarnation, and [`ActorEntity::spawn`] returns the typed mailbox
1445    /// ingress used for routing `M`. Passivation and handoff stop that actor. A
1446    /// self-stopped actor remains stopped until the next delivery, which creates
1447    /// a new incarnation without retrying the message that caused the stop.
1448    pub async fn register_actor_entity_type<M, F, A>(
1449        &self,
1450        type_name: impl Into<String>,
1451        factory: F,
1452    ) -> ShardingResult<()>
1453    where
1454        M: Serialize + DeserializeOwned + Send + 'static,
1455        F: Fn(EntityContext) -> A + Send + Sync + 'static,
1456        A: ActorEntity<M>,
1457    {
1458        let type_name = type_name.into();
1459        if type_name.trim().is_empty() {
1460            return Err(ShardingError::InvalidConfig(
1461                "entity type name must not be empty".to_owned(),
1462            ));
1463        }
1464        let runtime = ActorEntityTypeState::<M>::new(type_name.clone(), factory);
1465        self.register_entity_type_runtime(type_name, runtime).await
1466    }
1467
1468    /// Register an entity type with remember-entities enabled.
1469    ///
1470    /// The region must be configured with [`RememberEntitiesConfig::with_store`]
1471    /// or registration fails. Remembered entities are spawned automatically when
1472    /// their shard starts locally after rebalance, restart recovery, or
1473    /// coordinator takeover. Passivation removes the id from the remember store;
1474    /// ordinary actor failure/restart and node stop do not.
1475    pub async fn register_remembered_entity_type<M, F, B>(
1476        &self,
1477        type_name: impl Into<String>,
1478        factory: F,
1479    ) -> ShardingResult<()>
1480    where
1481        M: Serialize + DeserializeOwned + Send + 'static,
1482        F: Fn(EntityContext) -> B + Send + Sync + 'static,
1483        B: EntityBehavior<M>,
1484    {
1485        if self.remember.is_none() {
1486            return Err(ShardingError::InvalidConfig(
1487                "remember-entities store must be configured before registering a remembered entity type"
1488                    .to_owned(),
1489            ));
1490        }
1491        let type_name = type_name.into();
1492        if type_name.trim().is_empty() {
1493            return Err(ShardingError::InvalidConfig(
1494                "entity type name must not be empty".to_owned(),
1495            ));
1496        }
1497        let runtime = EntityTypeState::<M>::new_remembered(type_name.clone(), factory);
1498        self.register_entity_type_runtime(type_name, runtime).await
1499    }
1500
1501    async fn register_entity_type_runtime<R>(
1502        &self,
1503        type_name: String,
1504        runtime: R,
1505    ) -> ShardingResult<()>
1506    where
1507        R: DynEntityType + 'static,
1508    {
1509        let (reply, receiver) = oneshot::channel();
1510        self.commands
1511            .send(RegionCommand::RegisterType {
1512                type_name,
1513                runtime: Arc::new(runtime),
1514                reply,
1515            })
1516            .await
1517            .map_err(|_| ShardingError::Stopped)?;
1518        receiver.await.map_err(|_| ShardingError::Stopped)?
1519    }
1520
1521    /// Subscribe to remember-entities store failure events.
1522    #[must_use]
1523    pub fn subscribe_remember_entities_events(&self) -> broadcast::Receiver<RememberEntitiesEvent> {
1524        self.remember_events.subscribe()
1525    }
1526
1527    /// Wait until previously queued remember-entities writes have been processed.
1528    pub async fn flush_remember_entities(&self) -> ShardingResult<()> {
1529        if let Some(remember) = &self.remember {
1530            remember.flush().await
1531        } else {
1532            Ok(())
1533        }
1534    }
1535
1536    /// Return an entity ref using the configured default extractor.
1537    #[must_use]
1538    pub fn entity_ref<M>(
1539        &self,
1540        type_name: impl Into<String>,
1541        entity_id: impl Into<String>,
1542    ) -> EntityRef<M>
1543    where
1544        M: Serialize + Send + 'static,
1545    {
1546        self.entity_ref_with_extractor(type_name, entity_id, Arc::clone(&self.default_extractor))
1547    }
1548
1549    /// Return an entity ref using a custom extractor.
1550    #[must_use]
1551    pub fn entity_ref_with_extractor<M, E>(
1552        &self,
1553        type_name: impl Into<String>,
1554        entity_id: impl Into<String>,
1555        extractor: Arc<E>,
1556    ) -> EntityRef<M>
1557    where
1558        M: Serialize + Send + 'static,
1559        E: ShardExtractor<M>,
1560    {
1561        let entity_id = entity_id.into();
1562        let shard_id = extractor.shard_id(&entity_id);
1563        EntityRef {
1564            type_name: type_name.into(),
1565            entity_id,
1566            shard_id,
1567            commands: self.commands.clone(),
1568            sessions: self.sessions.clone(),
1569            entity_types: Arc::clone(&self.entity_types),
1570            allocation_cache: Arc::clone(&self.allocation_cache),
1571            moving_shards: Arc::clone(&self.moving_shards),
1572            cluster_state: self.cluster_state.clone(),
1573            agent_role: self.agent_role.clone(),
1574            role_constraint: self.role_constraint.clone(),
1575            self_node: self.self_node.clone(),
1576            request_timeout: self.request_timeout,
1577            next_request_id: Arc::clone(&self.next_request_id),
1578            pending_asks: Arc::clone(&self.pending_asks),
1579            remember: self.remember.clone(),
1580            _marker: PhantomData,
1581        }
1582    }
1583
1584    /// Return the cached allocation table known by this region.
1585    pub async fn allocation_table(
1586        &self,
1587        type_name: impl Into<String>,
1588    ) -> ShardingResult<ShardAllocationTable> {
1589        let (reply, receiver) = oneshot::channel();
1590        self.commands
1591            .send(RegionCommand::GetAllocations {
1592                type_name: type_name.into(),
1593                reply,
1594            })
1595            .await
1596            .map_err(|_| ShardingError::Stopped)?;
1597        receiver.await.map_err(|_| ShardingError::Stopped)?
1598    }
1599
1600    /// Return rebalance rounds recorded by this region when it was coordinator.
1601    pub async fn rebalance_rounds(&self) -> ShardingResult<Vec<RebalanceRound>> {
1602        let (reply, receiver) = oneshot::channel();
1603        self.commands
1604            .send(RegionCommand::GetRebalanceRounds { reply })
1605            .await
1606            .map_err(|_| ShardingError::Stopped)?;
1607        receiver.await.map_err(|_| ShardingError::Stopped)
1608    }
1609
1610    /// Stop local sharding tasks. Dropping the cluster agent also clears the DCP
1611    /// provider.
1612    pub async fn shutdown(&self) {
1613        let _ = self.flush_remember_entities().await;
1614        let _ = self.commands.send(RegionCommand::Shutdown).await;
1615        if let Some(remember) = &self.remember {
1616            remember.shutdown().await;
1617        }
1618        let tasks = {
1619            let mut locked = self.tasks.lock().expect("sharding tasks poisoned");
1620            std::mem::take(&mut *locked)
1621        };
1622        for task in tasks {
1623            task.abort();
1624        }
1625    }
1626
1627    #[cfg(test)]
1628    pub(crate) async fn test_live_entity_count(
1629        &self,
1630        type_name: &str,
1631        shard_id: &str,
1632    ) -> ShardingResult<usize> {
1633        let runtime = self
1634            .entity_types
1635            .read()
1636            .await
1637            .get(type_name)
1638            .cloned()
1639            .ok_or_else(|| ShardingError::EntityTypeNotRegistered(type_name.to_owned()))?;
1640        Ok(runtime
1641            .live_entity_count_for_shard(shard_id.to_owned())
1642            .await)
1643    }
1644}
1645
1646/// Reference to one entity.
1647pub struct EntityRef<M> {
1648    type_name: String,
1649    entity_id: String,
1650    shard_id: String,
1651    commands: mpsc::Sender<RegionCommand>,
1652    sessions: NodeSessionManagerHandle,
1653    entity_types: EntityRegistry,
1654    allocation_cache: AllocationCache,
1655    moving_shards: MovingShardSet,
1656    cluster_state: Signal<ClusterState>,
1657    agent_role: String,
1658    role_constraint: Option<String>,
1659    self_node: String,
1660    request_timeout: Duration,
1661    next_request_id: Arc<AtomicU64>,
1662    pending_asks: PendingAskMap,
1663    remember: Option<RememberEntitiesRuntime>,
1664    _marker: PhantomData<M>,
1665}
1666
1667impl<M> Clone for EntityRef<M> {
1668    fn clone(&self) -> Self {
1669        Self {
1670            type_name: self.type_name.clone(),
1671            entity_id: self.entity_id.clone(),
1672            shard_id: self.shard_id.clone(),
1673            commands: self.commands.clone(),
1674            sessions: self.sessions.clone(),
1675            entity_types: Arc::clone(&self.entity_types),
1676            allocation_cache: Arc::clone(&self.allocation_cache),
1677            moving_shards: Arc::clone(&self.moving_shards),
1678            cluster_state: self.cluster_state.clone(),
1679            agent_role: self.agent_role.clone(),
1680            role_constraint: self.role_constraint.clone(),
1681            self_node: self.self_node.clone(),
1682            request_timeout: self.request_timeout,
1683            next_request_id: Arc::clone(&self.next_request_id),
1684            pending_asks: Arc::clone(&self.pending_asks),
1685            remember: self.remember.clone(),
1686            _marker: PhantomData,
1687        }
1688    }
1689}
1690
1691impl<M> EntityRef<M>
1692where
1693    M: Serialize + Send + 'static,
1694{
1695    /// Entity id.
1696    #[must_use]
1697    pub fn entity_id(&self) -> &str {
1698        &self.entity_id
1699    }
1700
1701    /// Shard id.
1702    #[must_use]
1703    pub fn shard_id(&self) -> &str {
1704        &self.shard_id
1705    }
1706
1707    /// Send one message at-most-once.
1708    pub async fn tell(&self, message: M) -> ShardingResult<()> {
1709        let payload = encode(&message)?;
1710        let key = ShardKey {
1711            type_name: self.type_name.clone(),
1712            shard_id: self.shard_id.clone(),
1713        };
1714        if !self.moving_shards.read().await.contains(&key)
1715            && let Some(allocation) = self.allocation_cache.read().await.get(&key).cloned()
1716            && self.allocation_owner_is_eligible(&allocation)
1717        {
1718            return self
1719                .route_allocated(&key, allocation, self.entity_id.clone(), payload)
1720                .await;
1721        }
1722        self.allocation_cache.write().await.remove(&key);
1723        let (reply, receiver) = oneshot::channel();
1724        self.commands
1725            .send(RegionCommand::Route {
1726                type_name: self.type_name.clone(),
1727                shard_id: self.shard_id.clone(),
1728                entity_id: self.entity_id.clone(),
1729                payload,
1730                reply,
1731            })
1732            .await
1733            .map_err(|_| ShardingError::Stopped)?;
1734        receiver.await.map_err(|_| ShardingError::Stopped)?
1735    }
1736
1737    /// Passivate this entity if it is currently running.
1738    ///
1739    /// The entity actor is stopped and removed from the local runtime on its
1740    /// current owner. A later message respawns the entity on demand. If the
1741    /// shard is moving, passivation is ordered through the same bounded handoff
1742    /// buffer as user messages.
1743    pub async fn passivate(&self) -> ShardingResult<()> {
1744        let key = ShardKey {
1745            type_name: self.type_name.clone(),
1746            shard_id: self.shard_id.clone(),
1747        };
1748        if !self.moving_shards.read().await.contains(&key)
1749            && let Some(allocation) = self.allocation_cache.read().await.get(&key).cloned()
1750            && self.allocation_owner_is_eligible(&allocation)
1751        {
1752            return self
1753                .passivate_allocated(&key, allocation, self.entity_id.clone())
1754                .await;
1755        }
1756        self.allocation_cache.write().await.remove(&key);
1757        let (reply, receiver) = oneshot::channel();
1758        self.commands
1759            .send(RegionCommand::Passivate {
1760                type_name: self.type_name.clone(),
1761                shard_id: self.shard_id.clone(),
1762                entity_id: self.entity_id.clone(),
1763                reply,
1764            })
1765            .await
1766            .map_err(|_| ShardingError::Stopped)?;
1767        receiver.await.map_err(|_| ShardingError::Stopped)?
1768    }
1769
1770    fn allocation_owner_is_eligible(&self, allocation: &Allocation) -> bool {
1771        if allocation.node_id == self.self_node {
1772            return true;
1773        }
1774        let snapshot = self.cluster_state.get();
1775        is_eligible_node(
1776            &snapshot,
1777            &allocation.node_id,
1778            &self.agent_role,
1779            self.role_constraint.as_deref(),
1780        )
1781    }
1782
1783    async fn route_allocated(
1784        &self,
1785        key: &ShardKey,
1786        allocation: Allocation,
1787        entity_id: String,
1788        payload: Vec<u8>,
1789    ) -> ShardingResult<()> {
1790        if allocation.node_id == self.self_node {
1791            if type_remembers_from_registry(&self.entity_types, &key.type_name).await
1792                && let Some(remember) = &self.remember
1793            {
1794                remember.ensure_shard_open(key).await?;
1795            }
1796            let record_entity_id = entity_id.clone();
1797            let delivery = deliver_from_registry(
1798                &self.entity_types,
1799                &key.type_name,
1800                key.shard_id.clone(),
1801                entity_id,
1802                payload,
1803                ReplyContext {
1804                    local_node: self.self_node.clone(),
1805                    commands: self.commands.clone(),
1806                    sessions: self.sessions.clone(),
1807                    pending_asks: Arc::clone(&self.pending_asks),
1808                },
1809            )
1810            .await?;
1811            if delivery.remember_entities
1812                && delivery.entity_started
1813                && let Some(remember) = &self.remember
1814            {
1815                remember.record_started(key, &record_entity_id).await?;
1816            }
1817            return Ok(());
1818        }
1819        let batch = ForwardShardEnvelopes {
1820            type_name: key.type_name.clone(),
1821            envelopes: vec![ShardEnvelopeWire {
1822                entity_id,
1823                shard_id: key.shard_id.clone(),
1824                payload: encode_wire_payload(WirePayload::Message(payload))?,
1825            }],
1826        };
1827        self.sessions
1828            .forward_shard_pipe_envelopes(&allocation.node_id, batch)
1829            .await
1830            .map_err(ShardingError::Session)
1831    }
1832
1833    async fn passivate_allocated(
1834        &self,
1835        key: &ShardKey,
1836        allocation: Allocation,
1837        entity_id: String,
1838    ) -> ShardingResult<()> {
1839        if allocation.node_id == self.self_node {
1840            let (remember_entities, stored_shard_id) =
1841                passivate_from_registry(&self.entity_types, &key.type_name, &entity_id).await?;
1842            if remember_entities && let Some(remember) = &self.remember {
1843                let key = ShardKey {
1844                    type_name: key.type_name.clone(),
1845                    shard_id: stored_shard_id.unwrap_or_else(|| key.shard_id.clone()),
1846                };
1847                remember.record_stopped(&key, &entity_id).await?;
1848            }
1849            return Ok(());
1850        }
1851        let batch = ForwardShardEnvelopes {
1852            type_name: key.type_name.clone(),
1853            envelopes: vec![ShardEnvelopeWire {
1854                entity_id,
1855                shard_id: key.shard_id.clone(),
1856                payload: encode_wire_payload(WirePayload::Passivate)?,
1857            }],
1858        };
1859        self.sessions
1860            .forward_shard_pipe_envelopes(&allocation.node_id, batch)
1861            .await
1862            .map_err(ShardingError::Session)
1863    }
1864
1865    /// Send one message that carries a sharding [`ReplyPort`].
1866    pub async fn ask<R, F>(&self, timeout: Duration, make_message: F) -> ShardingResult<R>
1867    where
1868        R: Serialize + DeserializeOwned + Send + 'static,
1869        F: FnOnce(ReplyPort<R>) -> M,
1870    {
1871        let request_id = self.next_request_id.fetch_add(1, Ordering::Relaxed);
1872        let (sender, receiver) = oneshot::channel();
1873        self.pending_asks
1874            .lock()
1875            .expect("pending asks poisoned")
1876            .insert(request_id, sender);
1877
1878        let port = ReplyPort::new(
1879            WireReplyTarget {
1880                origin_node: self.self_node.clone(),
1881                request_id,
1882            },
1883            self.self_node.clone(),
1884            self.commands.clone(),
1885            self.sessions.clone(),
1886            Arc::clone(&self.pending_asks),
1887        );
1888        let message = make_message(port);
1889        if let Err(error) = self.tell(message).await {
1890            self.pending_asks
1891                .lock()
1892                .expect("pending asks poisoned")
1893                .remove(&request_id);
1894            return Err(error);
1895        }
1896
1897        let timeout = if timeout.is_zero() {
1898            self.request_timeout
1899        } else {
1900            timeout
1901        };
1902        let bytes = match tokio::time::timeout(timeout, receiver).await {
1903            Ok(Ok(Ok(bytes))) => bytes,
1904            Ok(Ok(Err(error))) => return Err(error),
1905            Ok(Err(_closed)) => return Err(ShardingError::Stopped),
1906            Err(_elapsed) => {
1907                self.pending_asks
1908                    .lock()
1909                    .expect("pending asks poisoned")
1910                    .remove(&request_id);
1911                return Err(ShardingError::Timeout(timeout));
1912            }
1913        };
1914        decode(&bytes)
1915    }
1916}
1917
1918trait DynEntityType: Send + Sync {
1919    fn remember_entities(&self) -> bool;
1920
1921    fn deliver(
1922        &self,
1923        shard_id: String,
1924        entity_id: String,
1925        payload: Vec<u8>,
1926        context: ReplyContext,
1927    ) -> BoxFuture<'_, ShardingResult<DeliveryOutcome>>;
1928
1929    fn ensure_started(
1930        &self,
1931        entity_id: String,
1932        shard_id: String,
1933    ) -> BoxFuture<'_, ShardingResult<bool>>;
1934
1935    fn passivate(&self, entity_id: String) -> BoxFuture<'_, ShardingResult<Option<String>>>;
1936
1937    fn passivate_idle(
1938        &self,
1939        idle_timeout: Duration,
1940        now: Instant,
1941    ) -> BoxFuture<'_, Vec<PassivatedEntity>>;
1942
1943    fn stop_entities_for_shard(&self, shard_id: String) -> BoxFuture<'_, ()>;
1944
1945    #[cfg(test)]
1946    fn live_entity_count_for_shard(&self, shard_id: String) -> BoxFuture<'_, usize>;
1947}
1948
1949struct EntityTypeState<M> {
1950    type_name: String,
1951    remember_entities: bool,
1952    factory: Arc<dyn Fn(EntityContext) -> Box<dyn EntityBehavior<M>> + Send + Sync>,
1953    entities: tokio::sync::Mutex<BTreeMap<String, EntityCell<M>>>,
1954}
1955
1956struct EntityCell<M> {
1957    actor: ActorRef<M>,
1958    handle: ractor::concurrency::JoinHandle<()>,
1959    shard_id: String,
1960    last_seen: Instant,
1961}
1962
1963struct DeliveryOutcome {
1964    entity_started: bool,
1965}
1966
1967struct PassivatedEntity {
1968    entity_id: String,
1969    shard_id: String,
1970}
1971
1972impl<M> EntityTypeState<M>
1973where
1974    M: Serialize + DeserializeOwned + Send + 'static,
1975{
1976    fn new<F, B>(type_name: String, factory: F) -> Self
1977    where
1978        F: Fn(EntityContext) -> B + Send + Sync + 'static,
1979        B: EntityBehavior<M>,
1980    {
1981        Self::with_remember_entities(type_name, factory, false)
1982    }
1983
1984    fn new_remembered<F, B>(type_name: String, factory: F) -> Self
1985    where
1986        F: Fn(EntityContext) -> B + Send + Sync + 'static,
1987        B: EntityBehavior<M>,
1988    {
1989        Self::with_remember_entities(type_name, factory, true)
1990    }
1991
1992    fn with_remember_entities<F, B>(type_name: String, factory: F, remember_entities: bool) -> Self
1993    where
1994        F: Fn(EntityContext) -> B + Send + Sync + 'static,
1995        B: EntityBehavior<M>,
1996    {
1997        let factory = Arc::new(move |context: EntityContext| {
1998            Box::new(factory(context)) as Box<dyn EntityBehavior<M>>
1999        });
2000        Self {
2001            type_name,
2002            remember_entities,
2003            factory,
2004            entities: tokio::sync::Mutex::new(BTreeMap::new()),
2005        }
2006    }
2007
2008    async fn actor_for(
2009        &self,
2010        entity_id: &str,
2011        shard_id: &str,
2012    ) -> ShardingResult<(ActorRef<M>, bool)> {
2013        let mut locked = self.entities.lock().await;
2014        if let Some(cell) = locked.get_mut(entity_id) {
2015            cell.last_seen = Instant::now();
2016            cell.shard_id = shard_id.to_owned();
2017            return Ok((cell.actor.clone(), false));
2018        }
2019
2020        let context = EntityContext {
2021            type_name: self.type_name.clone(),
2022            entity_id: entity_id.to_owned(),
2023        };
2024        let actor = EntityActor {
2025            factory: Arc::clone(&self.factory),
2026            context,
2027        };
2028        let (actor_ref, handle) = Actor::spawn(None, actor, ())
2029            .await
2030            .map_err(|error| ShardingError::Actor(error.to_string()))?;
2031        locked.insert(
2032            entity_id.to_owned(),
2033            EntityCell {
2034                actor: actor_ref.clone(),
2035                handle,
2036                shard_id: shard_id.to_owned(),
2037                last_seen: Instant::now(),
2038            },
2039        );
2040        Ok((actor_ref, true))
2041    }
2042
2043    async fn remove_actor(&self, entity_id: &str) -> Option<String> {
2044        let removed = self.entities.lock().await.remove(entity_id);
2045        if let Some(cell) = removed {
2046            let shard_id = cell.shard_id;
2047            cell.actor.stop(Some("passivated".to_owned()));
2048            cell.handle.abort();
2049            return Some(shard_id);
2050        }
2051        None
2052    }
2053
2054    async fn remove_idle_actors(
2055        &self,
2056        idle_timeout: Duration,
2057        now: Instant,
2058    ) -> Vec<PassivatedEntity> {
2059        let idle = {
2060            let locked = self.entities.lock().await;
2061            locked
2062                .iter()
2063                .filter(|(_entity_id, cell)| now.duration_since(cell.last_seen) >= idle_timeout)
2064                .map(|(entity_id, _cell)| entity_id.clone())
2065                .collect::<Vec<_>>()
2066        };
2067        let mut removed = Vec::new();
2068        for entity_id in idle {
2069            if let Some(shard_id) = self.remove_actor(&entity_id).await {
2070                removed.push(PassivatedEntity {
2071                    entity_id,
2072                    shard_id,
2073                });
2074            }
2075        }
2076        removed
2077    }
2078
2079    async fn stop_entities_for_shard(&self, shard_id: &str) {
2080        let to_stop = {
2081            let locked = self.entities.lock().await;
2082            locked
2083                .iter()
2084                .filter(|(_entity_id, cell)| cell.shard_id == shard_id)
2085                .map(|(entity_id, _cell)| entity_id.clone())
2086                .collect::<Vec<_>>()
2087        };
2088        for entity_id in to_stop {
2089            let removed = self.entities.lock().await.remove(&entity_id);
2090            if let Some(cell) = removed {
2091                cell.actor.stop(Some("handoff".to_owned()));
2092                cell.handle.abort();
2093            }
2094        }
2095    }
2096
2097    #[cfg(test)]
2098    async fn live_entity_count_for_shard(&self, shard_id: &str) -> usize {
2099        self.entities
2100            .lock()
2101            .await
2102            .values()
2103            .filter(|cell| cell.shard_id == shard_id)
2104            .count()
2105    }
2106}
2107
2108impl<M> DynEntityType for EntityTypeState<M>
2109where
2110    M: Serialize + DeserializeOwned + Send + 'static,
2111{
2112    fn remember_entities(&self) -> bool {
2113        self.remember_entities
2114    }
2115
2116    fn deliver(
2117        &self,
2118        shard_id: String,
2119        entity_id: String,
2120        payload: Vec<u8>,
2121        context: ReplyContext,
2122    ) -> BoxFuture<'_, ShardingResult<DeliveryOutcome>> {
2123        Box::pin(async move {
2124            let message = with_decode_context(context, || decode::<M>(&payload))?;
2125            let (actor, entity_started) = self.actor_for(&entity_id, &shard_id).await?;
2126            match actor.cast(message) {
2127                Ok(()) => Ok(DeliveryOutcome { entity_started }),
2128                Err(_error) => {
2129                    self.remove_actor(&entity_id).await;
2130                    Err(ShardingError::Actor(format!(
2131                        "entity actor unavailable: {}/{}",
2132                        self.type_name, entity_id
2133                    )))
2134                }
2135            }
2136        })
2137    }
2138
2139    fn ensure_started(
2140        &self,
2141        entity_id: String,
2142        shard_id: String,
2143    ) -> BoxFuture<'_, ShardingResult<bool>> {
2144        Box::pin(async move {
2145            let (_actor, started) = self.actor_for(&entity_id, &shard_id).await?;
2146            Ok(started)
2147        })
2148    }
2149
2150    fn passivate(&self, entity_id: String) -> BoxFuture<'_, ShardingResult<Option<String>>> {
2151        Box::pin(async move { Ok(self.remove_actor(&entity_id).await) })
2152    }
2153
2154    fn passivate_idle(
2155        &self,
2156        idle_timeout: Duration,
2157        now: Instant,
2158    ) -> BoxFuture<'_, Vec<PassivatedEntity>> {
2159        Box::pin(async move { self.remove_idle_actors(idle_timeout, now).await })
2160    }
2161
2162    fn stop_entities_for_shard(&self, shard_id: String) -> BoxFuture<'_, ()> {
2163        Box::pin(async move { self.stop_entities_for_shard(&shard_id).await })
2164    }
2165
2166    #[cfg(test)]
2167    fn live_entity_count_for_shard(&self, shard_id: String) -> BoxFuture<'_, usize> {
2168        Box::pin(async move { self.live_entity_count_for_shard(&shard_id).await })
2169    }
2170}
2171
2172struct ActorEntityTypeState<M> {
2173    type_name: String,
2174    factory: Arc<dyn Fn(EntityContext) -> Box<dyn ActorEntity<M>> + Send + Sync>,
2175    entities: tokio::sync::Mutex<BTreeMap<String, ActorEntityCell<M>>>,
2176}
2177
2178struct ActorEntityCell<M> {
2179    actor: ActorEntityHandle<M>,
2180    shard_id: String,
2181    last_seen: Instant,
2182    quiescence: Option<watch::Receiver<bool>>,
2183}
2184
2185struct ActorEntityQuiescence<M> {
2186    actor: ActorEntityHandle<M>,
2187    shard_id: String,
2188    completion: watch::Receiver<bool>,
2189    owner: Option<watch::Sender<bool>>,
2190}
2191
2192impl<M> ActorEntityQuiescence<M>
2193where
2194    M: Send + 'static,
2195{
2196    async fn wait(mut self) {
2197        if let Some(owner) = self.owner.take() {
2198            self.actor.wait_stopped().await;
2199            owner.send_replace(true);
2200            return;
2201        }
2202
2203        loop {
2204            if *self.completion.borrow() {
2205                return;
2206            }
2207            if self.completion.changed().await.is_err() {
2208                // The task that began quiescence was cancelled. The actor is
2209                // still draining, so this waiter takes ownership of observing
2210                // its actual termination.
2211                self.actor.wait_stopped().await;
2212                return;
2213            }
2214        }
2215    }
2216}
2217
2218impl<M> ActorEntityTypeState<M>
2219where
2220    M: Serialize + DeserializeOwned + Send + 'static,
2221{
2222    fn new<F, A>(type_name: String, factory: F) -> Self
2223    where
2224        F: Fn(EntityContext) -> A + Send + Sync + 'static,
2225        A: ActorEntity<M>,
2226    {
2227        let factory = Arc::new(move |context: EntityContext| {
2228            Box::new(factory(context)) as Box<dyn ActorEntity<M>>
2229        });
2230        Self {
2231            type_name,
2232            factory,
2233            entities: tokio::sync::Mutex::new(BTreeMap::new()),
2234        }
2235    }
2236
2237    async fn actor_for(
2238        &self,
2239        entity_id: &str,
2240        shard_id: &str,
2241    ) -> ShardingResult<(ActorEntityHandle<M>, bool)> {
2242        loop {
2243            let mut locked = self.entities.lock().await;
2244            if let Some(cell) = locked.get_mut(entity_id) {
2245                if cell.quiescence.is_none() && !cell.actor.is_stopped() {
2246                    cell.last_seen = Instant::now();
2247                    cell.shard_id = shard_id.to_owned();
2248                    return Ok((cell.actor.clone(), false));
2249                }
2250                let expected = cell.actor.clone();
2251                let quiescence = Self::begin_quiescence(cell);
2252                drop(locked);
2253                quiescence.wait().await;
2254                self.remove_quiesced_actor_if(entity_id, &expected).await;
2255                continue;
2256            }
2257
2258            let context = EntityContext {
2259                type_name: self.type_name.clone(),
2260                entity_id: entity_id.to_owned(),
2261            };
2262            let actor = (self.factory)(context).spawn().await?;
2263            locked.insert(
2264                entity_id.to_owned(),
2265                ActorEntityCell {
2266                    actor: actor.clone(),
2267                    shard_id: shard_id.to_owned(),
2268                    last_seen: Instant::now(),
2269                    quiescence: None,
2270                },
2271            );
2272            return Ok((actor, true));
2273        }
2274    }
2275
2276    async fn remove_actor(&self, entity_id: &str) -> Option<String> {
2277        let (expected, quiescence) = {
2278            let mut locked = self.entities.lock().await;
2279            let cell = locked.get_mut(entity_id)?;
2280            (cell.actor.clone(), Self::begin_quiescence(cell))
2281        };
2282        let shard_id = quiescence.shard_id.clone();
2283        quiescence.wait().await;
2284        self.remove_quiesced_actor_if(entity_id, &expected).await;
2285        Some(shard_id)
2286    }
2287
2288    async fn remove_actor_if(&self, entity_id: &str, expected: &ActorEntityHandle<M>) {
2289        let quiescence = {
2290            let mut locked = self.entities.lock().await;
2291            locked.get_mut(entity_id).and_then(|cell| {
2292                cell.actor
2293                    .is_same_entity(expected)
2294                    .then(|| Self::begin_quiescence(cell))
2295            })
2296        };
2297        if let Some(quiescence) = quiescence {
2298            quiescence.wait().await;
2299            self.remove_quiesced_actor_if(entity_id, expected).await;
2300        }
2301    }
2302
2303    fn begin_quiescence(cell: &mut ActorEntityCell<M>) -> ActorEntityQuiescence<M> {
2304        if let Some(completion) = &cell.quiescence {
2305            return ActorEntityQuiescence {
2306                actor: cell.actor.clone(),
2307                shard_id: cell.shard_id.clone(),
2308                completion: completion.clone(),
2309                owner: None,
2310            };
2311        }
2312
2313        let (owner, completion) = watch::channel(false);
2314        cell.quiescence = Some(completion.clone());
2315        cell.actor.begin_quiesce();
2316        ActorEntityQuiescence {
2317            actor: cell.actor.clone(),
2318            shard_id: cell.shard_id.clone(),
2319            completion,
2320            owner: Some(owner),
2321        }
2322    }
2323
2324    async fn remove_quiesced_actor_if(&self, entity_id: &str, expected: &ActorEntityHandle<M>) {
2325        let mut locked = self.entities.lock().await;
2326        let matches = locked
2327            .get(entity_id)
2328            .is_some_and(|cell| cell.actor.is_same_entity(expected));
2329        if matches {
2330            locked.remove(entity_id);
2331        }
2332    }
2333
2334    async fn tell_if_current(
2335        &self,
2336        entity_id: &str,
2337        expected: &ActorEntityHandle<M>,
2338        message: M,
2339    ) -> bool {
2340        let locked = self.entities.lock().await;
2341        let Some(cell) = locked.get(entity_id) else {
2342            return false;
2343        };
2344        if !cell.actor.is_same_entity(expected)
2345            || cell.quiescence.is_some()
2346            || cell.actor.is_stopped()
2347        {
2348            return false;
2349        }
2350
2351        if expected.tell(message).is_err() {
2352            return false;
2353        }
2354
2355        // This is both the cast serialization point and the post-cast
2356        // generation/lifecycle check. Runtime-controlled passivation and
2357        // handoff take the same map lock before marking the cell quiescent, so
2358        // they cannot queue a drain marker between this cast and this check.
2359        // Once the lock is released, draining preserves every message queued
2360        // before its FIFO marker. An actor-controlled priority self-stop can
2361        // still begin just after this check and discard an accepted message;
2362        // that narrow boundary retains EntityRef's documented at-most-once
2363        // delivery semantics because the runtime has no processing ack.
2364        locked
2365            .get(entity_id)
2366            .is_some_and(|cell| cell.actor.is_same_entity(expected) && cell.quiescence.is_none())
2367    }
2368
2369    async fn remove_idle_actors(
2370        &self,
2371        idle_timeout: Duration,
2372        now: Instant,
2373    ) -> Vec<PassivatedEntity> {
2374        let idle = {
2375            let locked = self.entities.lock().await;
2376            locked
2377                .iter()
2378                .filter(|(_entity_id, cell)| now.duration_since(cell.last_seen) >= idle_timeout)
2379                .map(|(entity_id, _cell)| entity_id.clone())
2380                .collect::<Vec<_>>()
2381        };
2382        let mut removed = Vec::new();
2383        for entity_id in idle {
2384            let candidate = {
2385                let mut locked = self.entities.lock().await;
2386                locked.get_mut(&entity_id).and_then(|cell| {
2387                    (cell.quiescence.is_none()
2388                        && now.duration_since(cell.last_seen) >= idle_timeout)
2389                        .then(|| (cell.actor.clone(), Self::begin_quiescence(cell)))
2390                })
2391            };
2392            if let Some((expected, quiescence)) = candidate {
2393                let shard_id = quiescence.shard_id.clone();
2394                quiescence.wait().await;
2395                self.remove_quiesced_actor_if(&entity_id, &expected).await;
2396                removed.push(PassivatedEntity {
2397                    entity_id,
2398                    shard_id,
2399                });
2400            }
2401        }
2402        removed
2403    }
2404
2405    async fn stop_entities_for_shard(&self, shard_id: &str) {
2406        let to_stop = {
2407            let locked = self.entities.lock().await;
2408            locked
2409                .iter()
2410                .filter(|(_entity_id, cell)| cell.shard_id == shard_id)
2411                .map(|(entity_id, _cell)| entity_id.clone())
2412                .collect::<Vec<_>>()
2413        };
2414        for entity_id in to_stop {
2415            let _removed = self.remove_actor(&entity_id).await;
2416        }
2417    }
2418
2419    #[cfg(test)]
2420    async fn live_entity_count_for_shard(&self, shard_id: &str) -> usize {
2421        self.entities
2422            .lock()
2423            .await
2424            .values()
2425            .filter(|cell| cell.shard_id == shard_id && !cell.actor.is_stopped())
2426            .count()
2427    }
2428}
2429
2430impl<M> DynEntityType for ActorEntityTypeState<M>
2431where
2432    M: Serialize + DeserializeOwned + Send + 'static,
2433{
2434    fn remember_entities(&self) -> bool {
2435        false
2436    }
2437
2438    fn deliver(
2439        &self,
2440        shard_id: String,
2441        entity_id: String,
2442        payload: Vec<u8>,
2443        context: ReplyContext,
2444    ) -> BoxFuture<'_, ShardingResult<DeliveryOutcome>> {
2445        Box::pin(async move {
2446            let mut entity_started = false;
2447            loop {
2448                let (actor, started) = self.actor_for(&entity_id, &shard_id).await?;
2449                entity_started |= started;
2450                let message = with_decode_context(context.clone(), || decode::<M>(&payload))?;
2451                if self.tell_if_current(&entity_id, &actor, message).await {
2452                    return Ok(DeliveryOutcome { entity_started });
2453                }
2454                self.remove_actor_if(&entity_id, &actor).await;
2455            }
2456        })
2457    }
2458
2459    fn ensure_started(
2460        &self,
2461        _entity_id: String,
2462        _shard_id: String,
2463    ) -> BoxFuture<'_, ShardingResult<bool>> {
2464        Box::pin(async { Ok(false) })
2465    }
2466
2467    fn passivate(&self, entity_id: String) -> BoxFuture<'_, ShardingResult<Option<String>>> {
2468        Box::pin(async move { Ok(self.remove_actor(&entity_id).await) })
2469    }
2470
2471    fn passivate_idle(
2472        &self,
2473        idle_timeout: Duration,
2474        now: Instant,
2475    ) -> BoxFuture<'_, Vec<PassivatedEntity>> {
2476        Box::pin(async move { self.remove_idle_actors(idle_timeout, now).await })
2477    }
2478
2479    fn stop_entities_for_shard(&self, shard_id: String) -> BoxFuture<'_, ()> {
2480        Box::pin(async move { self.stop_entities_for_shard(&shard_id).await })
2481    }
2482
2483    #[cfg(test)]
2484    fn live_entity_count_for_shard(&self, shard_id: String) -> BoxFuture<'_, usize> {
2485        Box::pin(async move { self.live_entity_count_for_shard(&shard_id).await })
2486    }
2487}
2488
2489struct RegistryDelivery {
2490    remember_entities: bool,
2491    entity_started: bool,
2492}
2493
2494async fn deliver_from_registry(
2495    entity_types: &EntityRegistry,
2496    type_name: &str,
2497    shard_id: String,
2498    entity_id: String,
2499    payload: Vec<u8>,
2500    context: ReplyContext,
2501) -> ShardingResult<RegistryDelivery> {
2502    let runtime = entity_types
2503        .read()
2504        .await
2505        .get(type_name)
2506        .cloned()
2507        .ok_or_else(|| ShardingError::EntityTypeNotRegistered(type_name.to_owned()))?;
2508    let remember_entities = runtime.remember_entities();
2509    let outcome = runtime
2510        .deliver(shard_id, entity_id, payload, context)
2511        .await?;
2512    Ok(RegistryDelivery {
2513        remember_entities,
2514        entity_started: outcome.entity_started,
2515    })
2516}
2517
2518async fn ensure_started_from_registry(
2519    entity_types: &EntityRegistry,
2520    type_name: &str,
2521    shard_id: String,
2522    entity_id: String,
2523) -> ShardingResult<bool> {
2524    let runtime = entity_types
2525        .read()
2526        .await
2527        .get(type_name)
2528        .cloned()
2529        .ok_or_else(|| ShardingError::EntityTypeNotRegistered(type_name.to_owned()))?;
2530    if !runtime.remember_entities() {
2531        return Ok(false);
2532    }
2533    runtime.ensure_started(entity_id, shard_id).await
2534}
2535
2536async fn type_remembers_from_registry(entity_types: &EntityRegistry, type_name: &str) -> bool {
2537    entity_types
2538        .read()
2539        .await
2540        .get(type_name)
2541        .is_some_and(|runtime| runtime.remember_entities())
2542}
2543
2544async fn passivate_from_registry(
2545    entity_types: &EntityRegistry,
2546    type_name: &str,
2547    entity_id: &str,
2548) -> ShardingResult<(bool, Option<String>)> {
2549    let runtime = entity_types
2550        .read()
2551        .await
2552        .get(type_name)
2553        .cloned()
2554        .ok_or_else(|| ShardingError::EntityTypeNotRegistered(type_name.to_owned()))?;
2555    let remember_entities = runtime.remember_entities();
2556    let shard_id = runtime.passivate(entity_id.to_owned()).await?;
2557    Ok((remember_entities, shard_id))
2558}
2559
2560struct EntityActor<M> {
2561    factory: Arc<dyn Fn(EntityContext) -> Box<dyn EntityBehavior<M>> + Send + Sync>,
2562    context: EntityContext,
2563}
2564
2565struct EntityActorState<M> {
2566    factory: Arc<dyn Fn(EntityContext) -> Box<dyn EntityBehavior<M>> + Send + Sync>,
2567    context: EntityContext,
2568    behavior: Box<dyn EntityBehavior<M>>,
2569}
2570
2571impl<M> Actor for EntityActor<M>
2572where
2573    M: Send + 'static,
2574{
2575    type Msg = M;
2576    type State = EntityActorState<M>;
2577    type Arguments = ();
2578
2579    async fn pre_start(
2580        &self,
2581        _myself: ActorRef<Self::Msg>,
2582        _args: Self::Arguments,
2583    ) -> Result<Self::State, ActorProcessingErr> {
2584        Ok(EntityActorState {
2585            factory: Arc::clone(&self.factory),
2586            context: self.context.clone(),
2587            behavior: (self.factory)(self.context.clone()),
2588        })
2589    }
2590
2591    async fn handle(
2592        &self,
2593        _myself: ActorRef<Self::Msg>,
2594        message: Self::Msg,
2595        state: &mut Self::State,
2596    ) -> Result<(), ActorProcessingErr> {
2597        let result = catch_unwind(AssertUnwindSafe(|| {
2598            state.behavior.handle(&state.context, message)
2599        }));
2600        match result {
2601            Ok(Ok(())) => Ok(()),
2602            Ok(Err(_)) | Err(_) => {
2603                state.behavior = (state.factory)(state.context.clone());
2604                Ok(())
2605            }
2606        }
2607    }
2608}
2609
2610#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2611struct ShardKey {
2612    type_name: String,
2613    shard_id: String,
2614}
2615
2616#[derive(Clone)]
2617struct RememberEntitiesRuntime {
2618    store: Arc<dyn RememberEntitiesStore>,
2619    writer: mpsc::Sender<RememberWrite>,
2620    failed_shards: FailedRememberShards,
2621    events: broadcast::Sender<RememberEntitiesEvent>,
2622    policy: RememberEntitiesFailurePolicy,
2623}
2624
2625enum RememberWrite {
2626    Started { key: ShardKey, entity_id: String },
2627    Stopped { key: ShardKey, entity_id: String },
2628    Flush { reply: oneshot::Sender<()> },
2629    Shutdown { reply: oneshot::Sender<()> },
2630}
2631
2632impl RememberEntitiesRuntime {
2633    fn start(
2634        store: Arc<dyn RememberEntitiesStore>,
2635        config: &RememberEntitiesConfig,
2636        events: broadcast::Sender<RememberEntitiesEvent>,
2637    ) -> (Self, JoinHandle<()>) {
2638        let (writer, receiver) = mpsc::channel(config.queue_capacity.max(1));
2639        let failed_shards = Arc::new(RwLock::new(BTreeMap::new()));
2640        let runtime = Self {
2641            store: Arc::clone(&store),
2642            writer,
2643            failed_shards: Arc::clone(&failed_shards),
2644            events: events.clone(),
2645            policy: config.failure_policy,
2646        };
2647        let task = tokio::spawn(run_remember_writer(
2648            store,
2649            receiver,
2650            failed_shards,
2651            events,
2652            config.failure_policy,
2653        ));
2654        (runtime, task)
2655    }
2656
2657    async fn ensure_shard_open(&self, key: &ShardKey) -> ShardingResult<()> {
2658        if self.policy == RememberEntitiesFailurePolicy::FailOpen {
2659            return Ok(());
2660        }
2661        if let Some(message) = self.failed_shards.read().await.get(key).cloned() {
2662            return Err(ShardingError::RememberEntities {
2663                type_name: key.type_name.clone(),
2664                shard_id: key.shard_id.clone(),
2665                message,
2666            });
2667        }
2668        Ok(())
2669    }
2670
2671    async fn record_started(&self, key: &ShardKey, entity_id: &str) -> ShardingResult<()> {
2672        self.ensure_shard_open(key).await?;
2673        self.try_enqueue(
2674            key,
2675            Some(entity_id),
2676            RememberEntitiesOperation::RecordStarted,
2677            RememberWrite::Started {
2678                key: key.clone(),
2679                entity_id: entity_id.to_owned(),
2680            },
2681        )
2682        .await
2683    }
2684
2685    async fn record_stopped(&self, key: &ShardKey, entity_id: &str) -> ShardingResult<()> {
2686        self.ensure_shard_open(key).await?;
2687        self.enqueue_blocking(
2688            key,
2689            Some(entity_id),
2690            RememberEntitiesOperation::RecordStopped,
2691            RememberWrite::Stopped {
2692                key: key.clone(),
2693                entity_id: entity_id.to_owned(),
2694            },
2695        )
2696        .await
2697    }
2698
2699    async fn list_entities(&self, key: &ShardKey) -> ShardingResult<Vec<String>> {
2700        self.ensure_shard_open(key).await?;
2701        match self
2702            .store
2703            .list_entities_for_shard(&key.type_name, &key.shard_id)
2704            .await
2705        {
2706            Ok(entities) => Ok(entities),
2707            Err(error) => {
2708                self.report_failure(
2709                    key,
2710                    None,
2711                    RememberEntitiesOperation::ListEntities,
2712                    error.to_string(),
2713                )
2714                .await;
2715                Err(ShardingError::RememberEntities {
2716                    type_name: key.type_name.clone(),
2717                    shard_id: key.shard_id.clone(),
2718                    message: error.to_string(),
2719                })
2720            }
2721        }
2722    }
2723
2724    async fn list_shards(&self, type_name: &str) -> ShardingResult<Vec<String>> {
2725        match self.store.list_shards(type_name).await {
2726            Ok(shards) => Ok(shards),
2727            Err(error) => {
2728                let key = ShardKey {
2729                    type_name: type_name.to_owned(),
2730                    shard_id: String::new(),
2731                };
2732                self.report_failure(
2733                    &key,
2734                    None,
2735                    RememberEntitiesOperation::ListShards,
2736                    error.to_string(),
2737                )
2738                .await;
2739                Err(ShardingError::RememberEntities {
2740                    type_name: type_name.to_owned(),
2741                    shard_id: String::new(),
2742                    message: error.to_string(),
2743                })
2744            }
2745        }
2746    }
2747
2748    async fn flush(&self) -> ShardingResult<()> {
2749        let (reply, receiver) = oneshot::channel();
2750        self.writer
2751            .send(RememberWrite::Flush { reply })
2752            .await
2753            .map_err(|_| ShardingError::Stopped)?;
2754        receiver.await.map_err(|_| ShardingError::Stopped)?;
2755        Ok(())
2756    }
2757
2758    async fn shutdown(&self) {
2759        let (reply, receiver) = oneshot::channel();
2760        if self
2761            .writer
2762            .send(RememberWrite::Shutdown { reply })
2763            .await
2764            .is_ok()
2765        {
2766            let _ = receiver.await;
2767        }
2768    }
2769
2770    async fn try_enqueue(
2771        &self,
2772        key: &ShardKey,
2773        entity_id: Option<&str>,
2774        operation: RememberEntitiesOperation,
2775        write: RememberWrite,
2776    ) -> ShardingResult<()> {
2777        match self.writer.try_send(write) {
2778            Ok(()) => Ok(()),
2779            Err(error) => {
2780                let message = error.to_string();
2781                self.report_failure(
2782                    key,
2783                    entity_id.map(ToOwned::to_owned),
2784                    operation,
2785                    message.clone(),
2786                )
2787                .await;
2788                if self.policy == RememberEntitiesFailurePolicy::FailClosed {
2789                    Err(ShardingError::RememberEntities {
2790                        type_name: key.type_name.clone(),
2791                        shard_id: key.shard_id.clone(),
2792                        message,
2793                    })
2794                } else {
2795                    Ok(())
2796                }
2797            }
2798        }
2799    }
2800
2801    async fn enqueue_blocking(
2802        &self,
2803        key: &ShardKey,
2804        entity_id: Option<&str>,
2805        operation: RememberEntitiesOperation,
2806        write: RememberWrite,
2807    ) -> ShardingResult<()> {
2808        match self.writer.send(write).await {
2809            Ok(()) => Ok(()),
2810            Err(_error) => {
2811                self.report_failure(
2812                    key,
2813                    entity_id.map(ToOwned::to_owned),
2814                    operation,
2815                    "remember-entities writer stopped".to_owned(),
2816                )
2817                .await;
2818                if self.policy == RememberEntitiesFailurePolicy::FailClosed {
2819                    Err(ShardingError::RememberEntities {
2820                        type_name: key.type_name.clone(),
2821                        shard_id: key.shard_id.clone(),
2822                        message: "remember-entities writer stopped".to_owned(),
2823                    })
2824                } else {
2825                    Ok(())
2826                }
2827            }
2828        }
2829    }
2830
2831    async fn report_failure(
2832        &self,
2833        key: &ShardKey,
2834        entity_id: Option<String>,
2835        operation: RememberEntitiesOperation,
2836        message: String,
2837    ) {
2838        if self.policy == RememberEntitiesFailurePolicy::FailClosed {
2839            self.failed_shards
2840                .write()
2841                .await
2842                .insert(key.clone(), message.clone());
2843        }
2844        let _ = self.events.send(RememberEntitiesEvent {
2845            type_name: key.type_name.clone(),
2846            shard_id: key.shard_id.clone(),
2847            entity_id,
2848            operation,
2849            policy: self.policy,
2850            message,
2851        });
2852    }
2853}
2854
2855async fn run_remember_writer(
2856    store: Arc<dyn RememberEntitiesStore>,
2857    mut receiver: mpsc::Receiver<RememberWrite>,
2858    failed_shards: FailedRememberShards,
2859    events: broadcast::Sender<RememberEntitiesEvent>,
2860    policy: RememberEntitiesFailurePolicy,
2861) {
2862    while let Some(write) = receiver.recv().await {
2863        match write {
2864            RememberWrite::Started { key, entity_id } => {
2865                if let Err(error) = store
2866                    .record_entity_started(&key.type_name, &key.shard_id, &entity_id)
2867                    .await
2868                {
2869                    report_remember_writer_failure(
2870                        &failed_shards,
2871                        &events,
2872                        policy,
2873                        key,
2874                        Some(entity_id),
2875                        RememberEntitiesOperation::RecordStarted,
2876                        error.to_string(),
2877                    )
2878                    .await;
2879                }
2880            }
2881            RememberWrite::Stopped { key, entity_id } => {
2882                if let Err(error) = store
2883                    .record_entity_stopped(&key.type_name, &key.shard_id, &entity_id)
2884                    .await
2885                {
2886                    report_remember_writer_failure(
2887                        &failed_shards,
2888                        &events,
2889                        policy,
2890                        key,
2891                        Some(entity_id),
2892                        RememberEntitiesOperation::RecordStopped,
2893                        error.to_string(),
2894                    )
2895                    .await;
2896                }
2897            }
2898            RememberWrite::Flush { reply } => {
2899                if let Err(error) = store.flush().await {
2900                    report_remember_writer_failure(
2901                        &failed_shards,
2902                        &events,
2903                        policy,
2904                        ShardKey {
2905                            type_name: String::new(),
2906                            shard_id: String::new(),
2907                        },
2908                        None,
2909                        RememberEntitiesOperation::ListEntities,
2910                        error.to_string(),
2911                    )
2912                    .await;
2913                }
2914                let _ = reply.send(());
2915            }
2916            RememberWrite::Shutdown { reply } => {
2917                while let Ok(write) = receiver.try_recv() {
2918                    match write {
2919                        RememberWrite::Started { key, entity_id } => {
2920                            let _ = store
2921                                .record_entity_started(&key.type_name, &key.shard_id, &entity_id)
2922                                .await;
2923                        }
2924                        RememberWrite::Stopped { key, entity_id } => {
2925                            let _ = store
2926                                .record_entity_stopped(&key.type_name, &key.shard_id, &entity_id)
2927                                .await;
2928                        }
2929                        RememberWrite::Flush { reply } | RememberWrite::Shutdown { reply } => {
2930                            let _ = reply.send(());
2931                        }
2932                    }
2933                }
2934                let _ = reply.send(());
2935                break;
2936            }
2937        }
2938    }
2939}
2940
2941async fn report_remember_writer_failure(
2942    failed_shards: &FailedRememberShards,
2943    events: &broadcast::Sender<RememberEntitiesEvent>,
2944    policy: RememberEntitiesFailurePolicy,
2945    key: ShardKey,
2946    entity_id: Option<String>,
2947    operation: RememberEntitiesOperation,
2948    message: String,
2949) {
2950    if policy == RememberEntitiesFailurePolicy::FailClosed {
2951        failed_shards
2952            .write()
2953            .await
2954            .insert(key.clone(), message.clone());
2955    }
2956    let _ = events.send(RememberEntitiesEvent {
2957        type_name: key.type_name,
2958        shard_id: key.shard_id,
2959        entity_id,
2960        operation,
2961        policy,
2962        message,
2963    });
2964}
2965
2966#[derive(Debug, Clone)]
2967struct Allocation {
2968    node_id: String,
2969    generation: u64,
2970}
2971
2972enum BufferedEnvelope {
2973    Message {
2974        entity_id: String,
2975        payload: Vec<u8>,
2976        reply: oneshot::Sender<ShardingResult<()>>,
2977    },
2978    Passivate {
2979        entity_id: String,
2980        reply: oneshot::Sender<ShardingResult<()>>,
2981    },
2982}
2983
2984struct HandoffState {
2985    allocation: Allocation,
2986    buffer: VecDeque<BufferedEnvelope>,
2987}
2988
2989struct PendingEnvelope {
2990    entity_id: String,
2991    payload: Vec<u8>,
2992    reply: oneshot::Sender<ShardingResult<()>>,
2993}
2994
2995struct InflightAllocation {
2996    started_at: Instant,
2997}
2998
2999struct RegionState {
3000    self_node: String,
3001    cluster_state: Signal<ClusterState>,
3002    sessions: NodeSessionManagerHandle,
3003    config: ShardingConfig,
3004    entity_types: EntityRegistry,
3005    allocation_cache: AllocationCache,
3006    moving_shards: MovingShardSet,
3007    remember: Option<RememberEntitiesRuntime>,
3008    allocations: BTreeMap<ShardKey, Allocation>,
3009    pending_allocations: BTreeMap<ShardKey, VecDeque<PendingEnvelope>>,
3010    allocation_inflight: BTreeMap<ShardKey, InflightAllocation>,
3011    handoffs: BTreeMap<ShardKey, HandoffState>,
3012    pending_asks: PendingAskMap,
3013    active_coordinator: bool,
3014    rebuilding: bool,
3015    next_generation: u64,
3016    next_rebalance_round: u64,
3017    rebalance_rounds: VecDeque<RebalanceRound>,
3018    commands: mpsc::Sender<RegionCommand>,
3019}
3020
3021enum RegionCommand {
3022    RegisterType {
3023        type_name: String,
3024        runtime: Arc<dyn DynEntityType>,
3025        reply: oneshot::Sender<ShardingResult<()>>,
3026    },
3027    Route {
3028        type_name: String,
3029        shard_id: String,
3030        entity_id: String,
3031        payload: Vec<u8>,
3032        reply: oneshot::Sender<ShardingResult<()>>,
3033    },
3034    Passivate {
3035        type_name: String,
3036        shard_id: String,
3037        entity_id: String,
3038        reply: oneshot::Sender<ShardingResult<()>>,
3039    },
3040    ForwardedEnvelope {
3041        type_name: String,
3042        envelope: ShardEnvelopeWire,
3043        reply: oneshot::Sender<ShardingResult<()>>,
3044    },
3045    AllocateShard {
3046        type_name: String,
3047        shard_id: String,
3048        reply: oneshot::Sender<ShardingResult<ShardAllocation>>,
3049    },
3050    AllocationResolved {
3051        key: ShardKey,
3052        result: ShardingResult<ShardAllocation>,
3053    },
3054    RememberAllocations {
3055        table: ShardAllocationTable,
3056        reply: oneshot::Sender<ShardingResult<()>>,
3057    },
3058    GetAllocations {
3059        type_name: String,
3060        reply: oneshot::Sender<ShardingResult<ShardAllocationTable>>,
3061    },
3062    GetRebalanceRounds {
3063        reply: oneshot::Sender<Vec<RebalanceRound>>,
3064    },
3065    RebuildResolved {
3066        tables: Vec<ShardAllocationTable>,
3067    },
3068    HandoffReady {
3069        key: ShardKey,
3070    },
3071    SendAskReply {
3072        target: WireReplyTarget,
3073        ok: bool,
3074        payload: Vec<u8>,
3075        message: String,
3076    },
3077    Tick,
3078    Shutdown,
3079}
3080
3081async fn run_region_manager(mut state: RegionState, mut receiver: mpsc::Receiver<RegionCommand>) {
3082    while let Some(command) = receiver.recv().await {
3083        match command {
3084            RegionCommand::RegisterType {
3085                type_name,
3086                runtime,
3087                reply,
3088            } => {
3089                let recover_type_name = type_name.clone();
3090                state.entity_types.write().await.insert(type_name, runtime);
3091                state
3092                    .recover_known_remembered_shards_for_type(recover_type_name)
3093                    .await;
3094                let _ = reply.send(Ok(()));
3095            }
3096            RegionCommand::Route {
3097                type_name,
3098                shard_id,
3099                entity_id,
3100                payload,
3101                reply,
3102            } => {
3103                let result = state
3104                    .route(type_name, shard_id, entity_id, payload, reply)
3105                    .await;
3106                if let Err((error, reply)) = result {
3107                    let _ = reply.send(Err(error));
3108                }
3109            }
3110            RegionCommand::Passivate {
3111                type_name,
3112                shard_id,
3113                entity_id,
3114                reply,
3115            } => {
3116                let result = state.passivate(type_name, shard_id, entity_id, reply).await;
3117                if let Err((error, reply)) = result {
3118                    let _ = reply.send(Err(error));
3119                }
3120            }
3121            RegionCommand::ForwardedEnvelope {
3122                type_name,
3123                envelope,
3124                reply,
3125            } => {
3126                let result = state.forwarded_envelope(type_name, envelope, reply).await;
3127                if let Err((error, reply)) = result {
3128                    let _ = reply.send(Err(error));
3129                }
3130            }
3131            RegionCommand::AllocateShard {
3132                type_name,
3133                shard_id,
3134                reply,
3135            } => {
3136                let result = state.allocate_for_coordinator(type_name, shard_id).await;
3137                let _ = reply.send(result);
3138            }
3139            RegionCommand::AllocationResolved { key, result } => {
3140                state.apply_allocation_result(key, result).await;
3141            }
3142            RegionCommand::RememberAllocations { table, reply } => {
3143                state.merge_table(table).await;
3144                let _ = reply.send(Ok(()));
3145            }
3146            RegionCommand::GetAllocations { type_name, reply } => {
3147                let _ = reply.send(Ok(state.table_for_type(&type_name)));
3148            }
3149            RegionCommand::GetRebalanceRounds { reply } => {
3150                let rounds: Vec<_> = state.rebalance_rounds.iter().cloned().collect();
3151                let _ = reply.send(rounds);
3152            }
3153            RegionCommand::RebuildResolved { tables } => {
3154                for table in tables {
3155                    state.merge_table(table).await;
3156                }
3157                state.rebuilding = false;
3158            }
3159            RegionCommand::HandoffReady { key } => {
3160                state.drain_handoff(key).await;
3161            }
3162            RegionCommand::SendAskReply {
3163                target,
3164                ok,
3165                payload,
3166                message,
3167            } => {
3168                state.send_ask_reply(target, ok, payload, message).await;
3169            }
3170            RegionCommand::Tick => state.tick().await,
3171            RegionCommand::Shutdown => break,
3172        }
3173    }
3174}
3175
3176impl RegionState {
3177    async fn route(
3178        &mut self,
3179        type_name: String,
3180        shard_id: String,
3181        entity_id: String,
3182        payload: Vec<u8>,
3183        reply: oneshot::Sender<ShardingResult<()>>,
3184    ) -> Result<(), (ShardingError, oneshot::Sender<ShardingResult<()>>)> {
3185        if !self.entity_types.read().await.contains_key(&type_name) {
3186            return Err((ShardingError::EntityTypeNotRegistered(type_name), reply));
3187        }
3188        let key = ShardKey {
3189            type_name,
3190            shard_id,
3191        };
3192        if self.handoffs.contains_key(&key) {
3193            return self.buffer_handoff(
3194                key,
3195                BufferedEnvelope::Message {
3196                    entity_id,
3197                    payload,
3198                    reply,
3199                },
3200            );
3201        }
3202        if let Some(allocation) = self.allocations.get(&key).cloned() {
3203            if !self.allocation_owner_is_eligible(&allocation) {
3204                self.allocations.remove(&key);
3205                self.allocation_cache.write().await.remove(&key);
3206            } else {
3207                if allocation.node_id == self.self_node {
3208                    let result = self.deliver_local(&key, entity_id, payload).await;
3209                    let _ = reply.send(result);
3210                } else {
3211                    self.spawn_remote_forward(key, allocation.node_id, entity_id, payload, reply);
3212                }
3213                return Ok(());
3214            }
3215        }
3216
3217        let pending = self.pending_allocations.entry(key.clone()).or_default();
3218        if pending.len() >= self.config.allocation_buffer {
3219            return Err((
3220                ShardingError::BufferOverflow {
3221                    type_name: key.type_name,
3222                    shard_id: key.shard_id,
3223                },
3224                reply,
3225            ));
3226        }
3227        pending.push_back(PendingEnvelope {
3228            entity_id,
3229            payload,
3230            reply,
3231        });
3232        self.start_allocation_if_needed(key).await;
3233        Ok(())
3234    }
3235
3236    async fn passivate(
3237        &mut self,
3238        type_name: String,
3239        shard_id: String,
3240        entity_id: String,
3241        reply: oneshot::Sender<ShardingResult<()>>,
3242    ) -> Result<(), (ShardingError, oneshot::Sender<ShardingResult<()>>)> {
3243        if !self.entity_types.read().await.contains_key(&type_name) {
3244            return Err((ShardingError::EntityTypeNotRegistered(type_name), reply));
3245        }
3246        let key = ShardKey {
3247            type_name,
3248            shard_id,
3249        };
3250        if self.handoffs.contains_key(&key) {
3251            return self.buffer_handoff(key, BufferedEnvelope::Passivate { entity_id, reply });
3252        }
3253        let Some(allocation) = self.allocations.get(&key).cloned() else {
3254            let result = self
3255                .record_remembered_stopped_if_needed(&key, &entity_id)
3256                .await;
3257            let _ = reply.send(result);
3258            return Ok(());
3259        };
3260        if !self.allocation_owner_is_eligible(&allocation) {
3261            self.allocations.remove(&key);
3262            self.allocation_cache.write().await.remove(&key);
3263            let result = self
3264                .record_remembered_stopped_if_needed(&key, &entity_id)
3265                .await;
3266            let _ = reply.send(result);
3267            return Ok(());
3268        }
3269        if allocation.node_id == self.self_node {
3270            let result = self.passivate_local(&key, &entity_id).await;
3271            let _ = reply.send(result);
3272        } else {
3273            self.spawn_remote_passivate(key, allocation.node_id, entity_id, reply);
3274        }
3275        Ok(())
3276    }
3277
3278    async fn forwarded_envelope(
3279        &mut self,
3280        type_name: String,
3281        envelope: ShardEnvelopeWire,
3282        reply: oneshot::Sender<ShardingResult<()>>,
3283    ) -> Result<(), (ShardingError, oneshot::Sender<ShardingResult<()>>)> {
3284        let payload = match decode_wire_payload(envelope.payload) {
3285            Ok(payload) => payload,
3286            Err(error) => return Err((error, reply)),
3287        };
3288        match payload {
3289            WirePayload::Message(payload) => {
3290                self.route(
3291                    type_name,
3292                    envelope.shard_id,
3293                    envelope.entity_id,
3294                    payload,
3295                    reply,
3296                )
3297                .await
3298            }
3299            WirePayload::Passivate => {
3300                self.passivate(type_name, envelope.shard_id, envelope.entity_id, reply)
3301                    .await
3302            }
3303        }
3304    }
3305
3306    fn buffer_handoff(
3307        &mut self,
3308        key: ShardKey,
3309        envelope: BufferedEnvelope,
3310    ) -> Result<(), (ShardingError, oneshot::Sender<ShardingResult<()>>)> {
3311        let Some(handoff) = self.handoffs.get_mut(&key) else {
3312            return match envelope {
3313                BufferedEnvelope::Message { reply, .. }
3314                | BufferedEnvelope::Passivate { reply, .. } => Err((ShardingError::Stopped, reply)),
3315            };
3316        };
3317        if handoff.buffer.len() >= self.config.allocation_buffer {
3318            let error = ShardingError::BufferOverflow {
3319                type_name: key.type_name,
3320                shard_id: key.shard_id,
3321            };
3322            return match envelope {
3323                BufferedEnvelope::Message { reply, .. }
3324                | BufferedEnvelope::Passivate { reply, .. } => Err((error, reply)),
3325            };
3326        }
3327        handoff.buffer.push_back(envelope);
3328        Ok(())
3329    }
3330
3331    async fn drain_handoff(&mut self, key: ShardKey) {
3332        let Some(mut handoff) = self.handoffs.remove(&key) else {
3333            return;
3334        };
3335        let losing_owner = handoff.allocation.node_id != self.self_node;
3336        if losing_owner {
3337            let runtimes: Vec<_> = self.entity_types.read().await.values().cloned().collect();
3338            for runtime in runtimes {
3339                runtime.stop_entities_for_shard(key.shard_id.clone()).await;
3340            }
3341        }
3342        while let Some(envelope) = handoff.buffer.pop_front() {
3343            let _ = self
3344                .route_buffered_to_owner(&key, &handoff.allocation.node_id, envelope)
3345                .await;
3346        }
3347        self.moving_shards.write().await.remove(&key);
3348    }
3349
3350    async fn route_buffered_to_owner(
3351        &self,
3352        key: &ShardKey,
3353        owner: &str,
3354        envelope: BufferedEnvelope,
3355    ) -> ShardingResult<()> {
3356        match envelope {
3357            BufferedEnvelope::Message {
3358                entity_id,
3359                payload,
3360                reply,
3361            } => {
3362                let result = self.route_to_owner(key, owner, entity_id, payload).await;
3363                let ok = result.is_ok();
3364                let _ = reply.send(result);
3365                if ok {
3366                    Ok(())
3367                } else {
3368                    Err(ShardingError::Stopped)
3369                }
3370            }
3371            BufferedEnvelope::Passivate { entity_id, reply } => {
3372                let result = self.passivate_to_owner(key, owner, entity_id).await;
3373                let ok = result.is_ok();
3374                let _ = reply.send(result);
3375                if ok {
3376                    Ok(())
3377                } else {
3378                    Err(ShardingError::Stopped)
3379                }
3380            }
3381        }
3382    }
3383
3384    async fn start_handoff(&mut self, key: ShardKey, allocation: Allocation) {
3385        self.handoffs
3386            .entry(key.clone())
3387            .and_modify(|handoff| {
3388                handoff.allocation = allocation.clone();
3389            })
3390            .or_insert_with(|| HandoffState {
3391                allocation,
3392                buffer: VecDeque::new(),
3393            });
3394        self.moving_shards.write().await.insert(key.clone());
3395        let commands = self.commands.clone();
3396        let delay = self.config.handoff_drain_delay;
3397        tokio::spawn(async move {
3398            if !delay.is_zero() {
3399                tokio::time::sleep(delay).await;
3400            }
3401            let _ = commands.send(RegionCommand::HandoffReady { key }).await;
3402        });
3403    }
3404
3405    fn allocation_owner_is_eligible(&self, allocation: &Allocation) -> bool {
3406        if allocation.node_id == self.self_node {
3407            return true;
3408        }
3409        let snapshot = self.cluster_state.get();
3410        is_eligible_node(
3411            &snapshot,
3412            &allocation.node_id,
3413            &self.config.agent_role,
3414            self.config.role_constraint.as_deref(),
3415        )
3416    }
3417
3418    async fn drain_pending_for_allocation(&mut self, key: &ShardKey) {
3419        let Some(allocation) = self.allocations.get(key).cloned() else {
3420            return;
3421        };
3422        let pending = self.pending_allocations.remove(key).unwrap_or_default();
3423        for envelope in pending {
3424            let result = self
3425                .route_to_owner(
3426                    key,
3427                    &allocation.node_id,
3428                    envelope.entity_id,
3429                    envelope.payload,
3430                )
3431                .await;
3432            let _ = envelope.reply.send(result);
3433        }
3434    }
3435
3436    async fn invalidate_noneligible_allocations(&mut self) {
3437        let stale = self
3438            .allocations
3439            .iter()
3440            .filter(|(_key, allocation)| !self.allocation_owner_is_eligible(allocation))
3441            .map(|(key, _allocation)| key.clone())
3442            .collect::<Vec<_>>();
3443        for key in stale {
3444            self.allocations.remove(&key);
3445            self.allocation_cache.write().await.remove(&key);
3446        }
3447    }
3448
3449    async fn passivate_idle_entities(&mut self) {
3450        let Some(idle_timeout) = self.config.passivation_idle_timeout else {
3451            return;
3452        };
3453        let runtimes = self
3454            .entity_types
3455            .read()
3456            .await
3457            .iter()
3458            .map(|(type_name, runtime)| (type_name.clone(), Arc::clone(runtime)))
3459            .collect::<Vec<_>>();
3460        let now = Instant::now();
3461        for (type_name, runtime) in runtimes {
3462            let passivated = runtime.passivate_idle(idle_timeout, now).await;
3463            if runtime.remember_entities()
3464                && let Some(remember) = &self.remember
3465            {
3466                for entity in passivated {
3467                    let key = ShardKey {
3468                        type_name: type_name.clone(),
3469                        shard_id: entity.shard_id,
3470                    };
3471                    let _ = remember.record_stopped(&key, &entity.entity_id).await;
3472                }
3473            }
3474        }
3475    }
3476
3477    async fn type_remembers(&self, type_name: &str) -> bool {
3478        type_remembers_from_registry(&self.entity_types, type_name).await
3479    }
3480
3481    async fn record_remembered_stopped_if_needed(
3482        &self,
3483        key: &ShardKey,
3484        entity_id: &str,
3485    ) -> ShardingResult<()> {
3486        if self.type_remembers(&key.type_name).await
3487            && let Some(remember) = &self.remember
3488        {
3489            remember.record_stopped(key, entity_id).await?;
3490        }
3491        Ok(())
3492    }
3493
3494    async fn recover_known_remembered_shards_for_type(&mut self, type_name: String) {
3495        if !self.type_remembers(&type_name).await {
3496            return;
3497        }
3498        let Some(remember) = self.remember.clone() else {
3499            return;
3500        };
3501        let Ok(shards) = remember.list_shards(&type_name).await else {
3502            return;
3503        };
3504        for shard_id in shards {
3505            self.start_allocation_if_needed(ShardKey {
3506                type_name: type_name.clone(),
3507                shard_id,
3508            })
3509            .await;
3510        }
3511        self.recover_local_shards_for_type(&type_name).await;
3512    }
3513
3514    async fn recover_local_shards_for_type(&self, type_name: &str) {
3515        let keys = self
3516            .allocations
3517            .iter()
3518            .filter(|(key, allocation)| {
3519                key.type_name == type_name && allocation.node_id == self.self_node
3520            })
3521            .map(|(key, _allocation)| key.clone())
3522            .collect::<Vec<_>>();
3523        for key in keys {
3524            self.spawn_remembered_recovery(key).await;
3525        }
3526    }
3527
3528    async fn maybe_recover_local_shard(&self, key: &ShardKey, allocation: &Allocation) {
3529        if allocation.node_id == self.self_node {
3530            self.spawn_remembered_recovery(key.clone()).await;
3531        }
3532    }
3533
3534    async fn spawn_remembered_recovery(&self, key: ShardKey) {
3535        if !self.type_remembers(&key.type_name).await {
3536            return;
3537        }
3538        let Some(remember) = self.remember.clone() else {
3539            return;
3540        };
3541        let entity_types = Arc::clone(&self.entity_types);
3542        tokio::spawn(async move {
3543            let Ok(entity_ids) = remember.list_entities(&key).await else {
3544                return;
3545            };
3546            for entity_id in entity_ids {
3547                let _ = ensure_started_from_registry(
3548                    &entity_types,
3549                    &key.type_name,
3550                    key.shard_id.clone(),
3551                    entity_id,
3552                )
3553                .await;
3554            }
3555        });
3556    }
3557
3558    async fn start_allocation_if_needed(&mut self, key: ShardKey) {
3559        if self.allocation_inflight.contains_key(&key) {
3560            return;
3561        }
3562        self.allocation_inflight.insert(
3563            key.clone(),
3564            InflightAllocation {
3565                started_at: Instant::now(),
3566            },
3567        );
3568
3569        if self.is_local_coordinator() {
3570            let result = self
3571                .allocate_for_coordinator(key.type_name.clone(), key.shard_id.clone())
3572                .await;
3573            self.apply_allocation_result(key, result).await;
3574            return;
3575        }
3576
3577        let Some(coordinator) = self.coordinator_node_id() else {
3578            self.apply_allocation_result(
3579                key,
3580                Err(ShardingError::NoEligibleShardOwner(
3581                    "no coordinator is available".to_owned(),
3582                )),
3583            )
3584            .await;
3585            return;
3586        };
3587        let sessions = self.sessions.clone();
3588        let commands = self.commands.clone();
3589        let timeout = self.config.request_timeout;
3590        tokio::spawn(async move {
3591            let result = sessions
3592                .allocate_shard(
3593                    &coordinator,
3594                    key.type_name.clone(),
3595                    key.shard_id.clone(),
3596                    timeout,
3597                )
3598                .await
3599                .map_err(ShardingError::Session);
3600            let _ = commands
3601                .send(RegionCommand::AllocationResolved { key, result })
3602                .await;
3603        });
3604    }
3605
3606    async fn apply_allocation_result(
3607        &mut self,
3608        key: ShardKey,
3609        result: ShardingResult<ShardAllocation>,
3610    ) {
3611        self.allocation_inflight.remove(&key);
3612        match result {
3613            Ok(allocation) => {
3614                let allocation = Allocation {
3615                    node_id: allocation.node_id,
3616                    generation: allocation.generation,
3617                };
3618                self.allocations.insert(key.clone(), allocation.clone());
3619                self.allocation_cache
3620                    .write()
3621                    .await
3622                    .insert(key.clone(), allocation.clone());
3623                self.maybe_recover_local_shard(&key, &allocation).await;
3624                self.drain_pending_for_allocation(&key).await;
3625            }
3626            Err(error) => {
3627                let pending = self.pending_allocations.remove(&key).unwrap_or_default();
3628                let message = error.to_string();
3629                for envelope in pending {
3630                    let _ = envelope
3631                        .reply
3632                        .send(Err(ShardingError::Session(message.clone())));
3633                }
3634            }
3635        }
3636    }
3637
3638    async fn route_to_owner(
3639        &self,
3640        key: &ShardKey,
3641        owner: &str,
3642        entity_id: String,
3643        payload: Vec<u8>,
3644    ) -> ShardingResult<()> {
3645        if owner == self.self_node {
3646            return self.deliver_local(key, entity_id, payload).await;
3647        }
3648        let batch = ForwardShardEnvelopes {
3649            type_name: key.type_name.clone(),
3650            envelopes: vec![ShardEnvelopeWire {
3651                entity_id,
3652                shard_id: key.shard_id.clone(),
3653                payload: encode_wire_payload(WirePayload::Message(payload))?,
3654            }],
3655        };
3656        self.sessions
3657            .forward_shard_pipe_envelopes(owner, batch)
3658            .await
3659            .map_err(ShardingError::Session)?;
3660        Ok(())
3661    }
3662
3663    fn spawn_remote_forward(
3664        &self,
3665        key: ShardKey,
3666        owner: String,
3667        entity_id: String,
3668        payload: Vec<u8>,
3669        reply: oneshot::Sender<ShardingResult<()>>,
3670    ) {
3671        let sessions = self.sessions.clone();
3672        tokio::spawn(async move {
3673            let batch = ForwardShardEnvelopes {
3674                type_name: key.type_name,
3675                envelopes: vec![ShardEnvelopeWire {
3676                    entity_id,
3677                    shard_id: key.shard_id,
3678                    payload: match encode_wire_payload(WirePayload::Message(payload)) {
3679                        Ok(payload) => payload,
3680                        Err(error) => {
3681                            let _ = reply.send(Err(error));
3682                            return;
3683                        }
3684                    },
3685                }],
3686            };
3687            let result = sessions
3688                .forward_shard_pipe_envelopes(&owner, batch)
3689                .await
3690                .map_err(ShardingError::Session);
3691            let _ = reply.send(result);
3692        });
3693    }
3694
3695    async fn passivate_to_owner(
3696        &self,
3697        key: &ShardKey,
3698        owner: &str,
3699        entity_id: String,
3700    ) -> ShardingResult<()> {
3701        if owner == self.self_node {
3702            return self.passivate_local(key, &entity_id).await;
3703        }
3704        let batch = ForwardShardEnvelopes {
3705            type_name: key.type_name.clone(),
3706            envelopes: vec![ShardEnvelopeWire {
3707                entity_id,
3708                shard_id: key.shard_id.clone(),
3709                payload: encode_wire_payload(WirePayload::Passivate)?,
3710            }],
3711        };
3712        self.sessions
3713            .forward_shard_pipe_envelopes(owner, batch)
3714            .await
3715            .map_err(ShardingError::Session)?;
3716        Ok(())
3717    }
3718
3719    fn spawn_remote_passivate(
3720        &self,
3721        key: ShardKey,
3722        owner: String,
3723        entity_id: String,
3724        reply: oneshot::Sender<ShardingResult<()>>,
3725    ) {
3726        let sessions = self.sessions.clone();
3727        tokio::spawn(async move {
3728            let payload = match encode_wire_payload(WirePayload::Passivate) {
3729                Ok(payload) => payload,
3730                Err(error) => {
3731                    let _ = reply.send(Err(error));
3732                    return;
3733                }
3734            };
3735            let batch = ForwardShardEnvelopes {
3736                type_name: key.type_name,
3737                envelopes: vec![ShardEnvelopeWire {
3738                    entity_id,
3739                    shard_id: key.shard_id,
3740                    payload,
3741                }],
3742            };
3743            let result = sessions
3744                .forward_shard_pipe_envelopes(&owner, batch)
3745                .await
3746                .map_err(ShardingError::Session);
3747            let _ = reply.send(result);
3748        });
3749    }
3750
3751    async fn deliver_local(
3752        &self,
3753        key: &ShardKey,
3754        entity_id: String,
3755        payload: Vec<u8>,
3756    ) -> ShardingResult<()> {
3757        if self.type_remembers(&key.type_name).await
3758            && let Some(remember) = &self.remember
3759        {
3760            remember.ensure_shard_open(key).await?;
3761        }
3762        let record_entity_id = entity_id.clone();
3763        let delivery = deliver_from_registry(
3764            &self.entity_types,
3765            &key.type_name,
3766            key.shard_id.clone(),
3767            entity_id,
3768            payload,
3769            ReplyContext {
3770                local_node: self.self_node.clone(),
3771                commands: self.commands.clone(),
3772                sessions: self.sessions.clone(),
3773                pending_asks: Arc::clone(&self.pending_asks),
3774            },
3775        )
3776        .await?;
3777        if delivery.remember_entities
3778            && delivery.entity_started
3779            && let Some(remember) = &self.remember
3780        {
3781            remember.record_started(key, &record_entity_id).await?;
3782        }
3783        Ok(())
3784    }
3785
3786    async fn passivate_local(&self, key: &ShardKey, entity_id: &str) -> ShardingResult<()> {
3787        let (remember_entities, stored_shard_id) =
3788            passivate_from_registry(&self.entity_types, &key.type_name, entity_id).await?;
3789        if remember_entities && let Some(remember) = &self.remember {
3790            let key = ShardKey {
3791                type_name: key.type_name.clone(),
3792                shard_id: stored_shard_id.unwrap_or_else(|| key.shard_id.clone()),
3793            };
3794            remember.record_stopped(&key, entity_id).await?;
3795        }
3796        Ok(())
3797    }
3798
3799    async fn allocate_for_coordinator(
3800        &mut self,
3801        type_name: String,
3802        shard_id: String,
3803    ) -> ShardingResult<ShardAllocation> {
3804        if !self.is_local_coordinator() {
3805            return Err(ShardingError::Session(
3806                "this node is not the sharding coordinator".to_owned(),
3807            ));
3808        }
3809        let key = ShardKey {
3810            type_name: type_name.clone(),
3811            shard_id: shard_id.clone(),
3812        };
3813        if let Some(allocation) = self.allocations.get(&key)
3814            && self.allocation_owner_is_eligible(allocation)
3815        {
3816            return Ok(ShardAllocation {
3817                type_name,
3818                shard_id,
3819                node_id: allocation.node_id.clone(),
3820                generation: allocation.generation,
3821            });
3822        }
3823        self.allocations.remove(&key);
3824        self.allocation_cache.write().await.remove(&key);
3825
3826        let target = self.choose_least_shards_owner(&type_name)?;
3827        let generation = self.next_generation;
3828        self.next_generation = self.next_generation.saturating_add(1).max(1);
3829        let allocation = Allocation {
3830            node_id: target.clone(),
3831            generation,
3832        };
3833        self.allocations.insert(key.clone(), allocation.clone());
3834        self.allocation_cache
3835            .write()
3836            .await
3837            .insert(key.clone(), allocation.clone());
3838        self.maybe_recover_local_shard(&key, &allocation).await;
3839        self.replicate_allocations(type_name.clone()).await;
3840        Ok(ShardAllocation {
3841            type_name,
3842            shard_id,
3843            node_id: target,
3844            generation,
3845        })
3846    }
3847
3848    fn choose_least_shards_owner(&self, type_name: &str) -> ShardingResult<String> {
3849        let snapshot = self.cluster_state.get();
3850        let mut candidates = eligible_members(&snapshot, &self.config)
3851            .into_iter()
3852            .map(|member| member.node_id.clone())
3853            .collect::<Vec<_>>();
3854        candidates.sort();
3855        candidates
3856            .into_iter()
3857            .min_by(|left, right| {
3858                let left_count = self.shards_on_node(type_name, left);
3859                let right_count = self.shards_on_node(type_name, right);
3860                left_count.cmp(&right_count).then_with(|| left.cmp(right))
3861            })
3862            .ok_or_else(|| {
3863                ShardingError::NoEligibleShardOwner(format!(
3864                    "no Up members with role {}",
3865                    self.config.agent_role
3866                ))
3867            })
3868    }
3869
3870    fn shards_on_node(&self, type_name: &str, node_id: &str) -> usize {
3871        self.allocations
3872            .iter()
3873            .filter(|(key, allocation)| key.type_name == type_name && allocation.node_id == node_id)
3874            .count()
3875    }
3876
3877    async fn replicate_allocations(&self, type_name: String) {
3878        let table = self.table_for_type(&type_name);
3879        let peers = self.peer_node_ids();
3880        for node_id in peers {
3881            let sessions = self.sessions.clone();
3882            let table = table.clone();
3883            let timeout = self.config.request_timeout;
3884            tokio::spawn(async move {
3885                replicate_allocations_to_peer(sessions, node_id, table, timeout).await;
3886            });
3887        }
3888    }
3889
3890    fn table_for_type(&self, type_name: &str) -> ShardAllocationTable {
3891        ShardAllocationTable {
3892            entries: self
3893                .allocations
3894                .iter()
3895                .filter(|(key, _allocation)| type_name.is_empty() || key.type_name == type_name)
3896                .map(|(key, allocation)| ShardAllocationEntry {
3897                    type_name: key.type_name.clone(),
3898                    shard_id: key.shard_id.clone(),
3899                    node_id: allocation.node_id.clone(),
3900                    generation: allocation.generation,
3901                })
3902                .collect(),
3903        }
3904    }
3905
3906    async fn merge_table(&mut self, table: ShardAllocationTable) {
3907        for entry in table.entries {
3908            let key = ShardKey {
3909                type_name: entry.type_name,
3910                shard_id: entry.shard_id,
3911            };
3912            let existing = self.allocations.get(&key).cloned();
3913            let replace = existing
3914                .as_ref()
3915                .map(|existing| {
3916                    entry.generation > existing.generation
3917                        || (entry.generation == existing.generation
3918                            && entry.node_id > existing.node_id)
3919                })
3920                .unwrap_or(true);
3921            if replace {
3922                self.next_generation = self.next_generation.max(entry.generation.saturating_add(1));
3923                let allocation = Allocation {
3924                    node_id: entry.node_id,
3925                    generation: entry.generation,
3926                };
3927                let owner_changed = existing
3928                    .as_ref()
3929                    .is_some_and(|existing| existing.node_id != allocation.node_id);
3930                self.allocations.insert(key.clone(), allocation.clone());
3931                self.allocation_cache
3932                    .write()
3933                    .await
3934                    .insert(key.clone(), allocation.clone());
3935                self.maybe_recover_local_shard(&key, &allocation).await;
3936                if owner_changed {
3937                    self.start_handoff(key.clone(), allocation).await;
3938                } else {
3939                    self.drain_pending_for_allocation(&key).await;
3940                }
3941            }
3942        }
3943    }
3944
3945    async fn send_ask_reply(
3946        &mut self,
3947        target: WireReplyTarget,
3948        ok: bool,
3949        payload: Vec<u8>,
3950        message: String,
3951    ) {
3952        if target.origin_node == self.self_node {
3953            complete_pending_ask(&self.pending_asks, target.request_id, ok, payload, message);
3954            return;
3955        }
3956        let response = CompleteShardingAsk {
3957            request_id: target.request_id,
3958            ok,
3959            payload,
3960            message,
3961        };
3962        let _ = self
3963            .sessions
3964            .complete_sharding_ask_pipe(&target.origin_node, response)
3965            .await;
3966    }
3967
3968    async fn tick(&mut self) {
3969        self.passivate_idle_entities().await;
3970        let is_coordinator = self.is_local_coordinator();
3971        if !is_coordinator {
3972            self.active_coordinator = false;
3973            self.rebuilding = false;
3974            self.invalidate_noneligible_allocations().await;
3975            return;
3976        }
3977        self.fail_stale_allocations().await;
3978        if self.rebuilding {
3979            return;
3980        }
3981        if self.active_coordinator {
3982            self.rebalance_dead_owners().await;
3983            self.rebalance_graceful_spread().await;
3984            return;
3985        }
3986        self.active_coordinator = true;
3987        self.rebuilding = true;
3988        let peers = self.peer_node_ids();
3989        let sessions = self.sessions.clone();
3990        let commands = self.commands.clone();
3991        let timeout = self.config.request_timeout;
3992        let local_table = self.table_for_type("");
3993        tokio::spawn(async move {
3994            let mut tables = vec![local_table];
3995            let mut pending = Vec::new();
3996            for node_id in peers {
3997                let sessions = sessions.clone();
3998                pending.push(tokio::spawn(async move {
3999                    sessions
4000                        .get_shard_allocations(&node_id, String::new(), timeout)
4001                        .await
4002                }));
4003            }
4004            for task in pending {
4005                if let Ok(Ok(table)) = task.await {
4006                    tables.push(table);
4007                }
4008            }
4009            let _ = commands
4010                .send(RegionCommand::RebuildResolved { tables })
4011                .await;
4012        });
4013    }
4014
4015    async fn rebalance_dead_owners(&mut self) {
4016        let keys = self
4017            .allocations
4018            .iter()
4019            .filter(|(_key, allocation)| !self.allocation_owner_is_eligible(allocation))
4020            .map(|(key, _allocation)| key.clone())
4021            .collect::<Vec<_>>();
4022        let mut movements = Vec::new();
4023        for key in keys {
4024            let Ok(target) = self.choose_least_shards_owner(&key.type_name) else {
4025                continue;
4026            };
4027            if let Some(movement) = self.move_shard(&key, target).await {
4028                movements.push(movement);
4029            }
4030        }
4031        self.record_rebalance_round(RebalanceReason::DeadOwner, movements)
4032            .await;
4033    }
4034
4035    async fn rebalance_graceful_spread(&mut self) {
4036        let cap = self.config.rebalance_per_round;
4037        if cap == 0 {
4038            return;
4039        }
4040        let candidates = self.eligible_node_ids();
4041        if candidates.len() < 2 {
4042            return;
4043        }
4044        let types = self
4045            .allocations
4046            .keys()
4047            .map(|key| key.type_name.clone())
4048            .collect::<BTreeSet<_>>();
4049        let mut movements = Vec::new();
4050        let mut moved_keys = BTreeSet::new();
4051        for type_name in types {
4052            loop {
4053                if movements.len() >= cap {
4054                    self.record_rebalance_round(RebalanceReason::GracefulSpread, movements)
4055                        .await;
4056                    return;
4057                }
4058                let counts = self.shard_counts_for_type(&type_name, &candidates);
4059                let Some((source, source_count)) = counts
4060                    .iter()
4061                    .max_by(|left, right| left.1.cmp(right.1).then_with(|| left.0.cmp(right.0)))
4062                    .map(|(node, count)| (node.clone(), *count))
4063                else {
4064                    break;
4065                };
4066                let Some((target, target_count)) = counts
4067                    .iter()
4068                    .min_by(|left, right| left.1.cmp(right.1).then_with(|| left.0.cmp(right.0)))
4069                    .map(|(node, count)| (node.clone(), *count))
4070                else {
4071                    break;
4072                };
4073                if source_count <= target_count.saturating_add(1) {
4074                    break;
4075                }
4076                let Some(key) = self
4077                    .allocations
4078                    .iter()
4079                    .filter(|(key, allocation)| {
4080                        key.type_name == type_name
4081                            && allocation.node_id == source
4082                            && !moved_keys.contains(*key)
4083                    })
4084                    .map(|(key, _allocation)| key.clone())
4085                    .next()
4086                else {
4087                    break;
4088                };
4089                moved_keys.insert(key.clone());
4090                if let Some(movement) = self.move_shard(&key, target).await {
4091                    movements.push(movement);
4092                } else {
4093                    break;
4094                }
4095            }
4096        }
4097        self.record_rebalance_round(RebalanceReason::GracefulSpread, movements)
4098            .await;
4099    }
4100
4101    async fn move_shard(&mut self, key: &ShardKey, target: String) -> Option<ShardMovement> {
4102        let existing = self.allocations.get(key)?.clone();
4103        if existing.node_id == target {
4104            return None;
4105        }
4106        let generation = self.next_generation;
4107        self.next_generation = self.next_generation.saturating_add(1).max(1);
4108        let allocation = Allocation {
4109            node_id: target.clone(),
4110            generation,
4111        };
4112        self.allocations.insert(key.clone(), allocation.clone());
4113        self.allocation_cache
4114            .write()
4115            .await
4116            .insert(key.clone(), allocation.clone());
4117        self.maybe_recover_local_shard(key, &allocation).await;
4118        self.start_handoff(key.clone(), allocation).await;
4119        Some(ShardMovement {
4120            type_name: key.type_name.clone(),
4121            shard_id: key.shard_id.clone(),
4122            from_node: existing.node_id,
4123            to_node: target,
4124            generation,
4125        })
4126    }
4127}
4128
4129fn push_bounded_round(queue: &mut VecDeque<RebalanceRound>, round: RebalanceRound) {
4130    const MAX_ROUNDS: usize = 1024;
4131    if queue.len() >= MAX_ROUNDS {
4132        queue.pop_front();
4133    }
4134    queue.push_back(round);
4135}
4136
4137impl RegionState {
4138    async fn record_rebalance_round(
4139        &mut self,
4140        reason: RebalanceReason,
4141        movements: Vec<ShardMovement>,
4142    ) {
4143        if movements.is_empty() {
4144            return;
4145        }
4146        let affected_types = movements
4147            .iter()
4148            .map(|movement| movement.type_name.clone())
4149            .collect::<BTreeSet<_>>();
4150        let round = RebalanceRound {
4151            round: self.next_rebalance_round,
4152            reason,
4153            movements,
4154        };
4155        self.next_rebalance_round = self.next_rebalance_round.saturating_add(1).max(1);
4156        push_bounded_round(&mut self.rebalance_rounds, round);
4157        for type_name in affected_types {
4158            self.replicate_allocations(type_name).await;
4159        }
4160    }
4161
4162    fn eligible_node_ids(&self) -> Vec<String> {
4163        let snapshot = self.cluster_state.get();
4164        let mut nodes = eligible_members(&snapshot, &self.config)
4165            .into_iter()
4166            .map(|member| member.node_id.clone())
4167            .collect::<Vec<_>>();
4168        nodes.sort();
4169        nodes
4170    }
4171
4172    fn shard_counts_for_type(
4173        &self,
4174        type_name: &str,
4175        candidates: &[String],
4176    ) -> BTreeMap<String, usize> {
4177        let mut counts = candidates
4178            .iter()
4179            .map(|node_id| (node_id.clone(), 0_usize))
4180            .collect::<BTreeMap<_, _>>();
4181        for (key, allocation) in &self.allocations {
4182            if key.type_name == type_name
4183                && let Some(count) = counts.get_mut(&allocation.node_id)
4184            {
4185                *count += 1;
4186            }
4187        }
4188        counts
4189    }
4190
4191    async fn fail_stale_allocations(&mut self) {
4192        let timeout = self.config.request_timeout;
4193        let now = Instant::now();
4194        let stale = self
4195            .allocation_inflight
4196            .iter()
4197            .filter(|(_key, inflight)| now.duration_since(inflight.started_at) > timeout)
4198            .map(|(key, _inflight)| key.clone())
4199            .collect::<Vec<_>>();
4200        for key in stale {
4201            self.apply_allocation_result(key, Err(ShardingError::Timeout(timeout)))
4202                .await;
4203        }
4204    }
4205
4206    fn peer_node_ids(&self) -> Vec<String> {
4207        let snapshot = self.cluster_state.get();
4208        eligible_members(&snapshot, &self.config)
4209            .into_iter()
4210            .filter(|member| member.node_id != self.self_node)
4211            .map(|member| member.node_id.clone())
4212            .collect()
4213    }
4214
4215    fn is_local_coordinator(&self) -> bool {
4216        self.cluster_state
4217            .get()
4218            .placement_coordinator()
4219            .is_some_and(|member| member.node_id == self.self_node)
4220    }
4221
4222    fn coordinator_node_id(&self) -> Option<String> {
4223        self.cluster_state
4224            .get()
4225            .placement_coordinator()
4226            .map(|member| member.node_id.clone())
4227    }
4228}
4229
4230async fn replicate_allocations_to_peer(
4231    sessions: NodeSessionManagerHandle,
4232    node_id: String,
4233    table: ShardAllocationTable,
4234    timeout: Duration,
4235) {
4236    for attempt in 1..=ALLOCATION_REPLICATION_ATTEMPTS {
4237        if sessions
4238            .remember_shard_allocations(&node_id, table.clone(), timeout)
4239            .await
4240            .is_ok()
4241        {
4242            return;
4243        }
4244        if attempt < ALLOCATION_REPLICATION_ATTEMPTS {
4245            tokio::time::sleep(ALLOCATION_REPLICATION_RETRY_DELAY).await;
4246        }
4247    }
4248}
4249
4250fn eligible_members<'a>(snapshot: &'a ClusterState, config: &ShardingConfig) -> Vec<&'a Member> {
4251    snapshot
4252        .members
4253        .values()
4254        .filter(|member| member.state == MemberState::Up && !member.unreachable)
4255        .filter(|member| member.has_role(&config.agent_role))
4256        .filter(|member| {
4257            config
4258                .role_constraint
4259                .as_deref()
4260                .is_none_or(|role| member.has_role(role))
4261        })
4262        .collect()
4263}
4264
4265fn is_eligible_node(
4266    snapshot: &ClusterState,
4267    node_id: &str,
4268    agent_role: &str,
4269    role_constraint: Option<&str>,
4270) -> bool {
4271    snapshot.member(node_id).is_some_and(|member| {
4272        member.state == MemberState::Up
4273            && !member.unreachable
4274            && member.has_role(agent_role)
4275            && role_constraint.is_none_or(|role| member.has_role(role))
4276    })
4277}
4278
4279struct ShardingProvider {
4280    commands: mpsc::Sender<RegionCommand>,
4281    pending_asks: PendingAskMap,
4282    timeout: Duration,
4283}
4284
4285impl ShardingViewProvider for ShardingProvider {
4286    fn allocate_shard(
4287        &self,
4288        request: ShardAllocationRequest,
4289        timeout: Duration,
4290    ) -> datum_agent::dcp::server::ClusterViewFuture<'_, ShardAllocation> {
4291        Box::pin(async move {
4292            let (reply, receiver) = oneshot::channel();
4293            self.commands
4294                .send(RegionCommand::AllocateShard {
4295                    type_name: request.type_name,
4296                    shard_id: request.shard_id,
4297                    reply,
4298                })
4299                .await
4300                .map_err(|_| dcp_error(ResponseStatus::Failed, "sharding region stopped"))?;
4301            wait_dcp(timeout, receiver).await
4302        })
4303    }
4304
4305    fn remember_shard_allocations(
4306        &self,
4307        request: RememberShardAllocations,
4308    ) -> datum_agent::dcp::server::ClusterViewFuture<'_, ()> {
4309        Box::pin(async move {
4310            let table = request.table.unwrap_or_else(|| ShardAllocationTable {
4311                entries: Vec::new(),
4312            });
4313            let (reply, receiver) = oneshot::channel();
4314            self.commands
4315                .send(RegionCommand::RememberAllocations { table, reply })
4316                .await
4317                .map_err(|_| dcp_error(ResponseStatus::Failed, "sharding region stopped"))?;
4318            wait_dcp(self.timeout, receiver).await
4319        })
4320    }
4321
4322    fn get_shard_allocations(
4323        &self,
4324        type_name: String,
4325        timeout: Duration,
4326    ) -> datum_agent::dcp::server::ClusterViewFuture<'_, ShardAllocationTable> {
4327        Box::pin(async move {
4328            let (reply, receiver) = oneshot::channel();
4329            self.commands
4330                .send(RegionCommand::GetAllocations { type_name, reply })
4331                .await
4332                .map_err(|_| dcp_error(ResponseStatus::Failed, "sharding region stopped"))?;
4333            wait_dcp(timeout, receiver).await
4334        })
4335    }
4336
4337    fn forward_shard_envelopes(
4338        &self,
4339        request: ForwardShardEnvelopes,
4340        timeout: Duration,
4341    ) -> datum_agent::dcp::server::ClusterViewFuture<'_, ShardEnvelopeBatchResult> {
4342        Box::pin(async move {
4343            let timeout = if timeout.is_zero() {
4344                self.timeout
4345            } else {
4346                timeout
4347            };
4348            let mut acks = Vec::with_capacity(request.envelopes.len());
4349            for envelope in request.envelopes {
4350                let entity_id = envelope.entity_id.clone();
4351                let shard_id = envelope.shard_id.clone();
4352                let (reply, receiver) = oneshot::channel();
4353                let send_result = self
4354                    .commands
4355                    .send(RegionCommand::ForwardedEnvelope {
4356                        type_name: request.type_name.clone(),
4357                        envelope,
4358                        reply,
4359                    })
4360                    .await
4361                    .map_err(|_| dcp_error(ResponseStatus::Failed, "sharding region stopped"));
4362                let result = match send_result {
4363                    Ok(()) => wait_dcp(timeout, receiver)
4364                        .await
4365                        .map_err(|error| error.to_string()),
4366                    Err(error) => Err(error.to_string()),
4367                };
4368                acks.push(ShardEnvelopeAck {
4369                    entity_id,
4370                    shard_id,
4371                    ok: result.is_ok(),
4372                    message: result.err().unwrap_or_default(),
4373                });
4374            }
4375            Ok(ShardEnvelopeBatchResult { acks })
4376        })
4377    }
4378
4379    fn complete_sharding_ask(
4380        &self,
4381        request: CompleteShardingAsk,
4382    ) -> datum_agent::dcp::server::ClusterViewFuture<'_, ()> {
4383        Box::pin(async move {
4384            complete_pending_ask(
4385                &self.pending_asks,
4386                request.request_id,
4387                request.ok,
4388                request.payload,
4389                request.message,
4390            );
4391            Ok(())
4392        })
4393    }
4394}
4395
4396async fn wait_dcp<T>(
4397    timeout: Duration,
4398    receiver: oneshot::Receiver<ShardingResult<T>>,
4399) -> datum_agent::dcp::DcpResult<T> {
4400    tokio::time::timeout(timeout, receiver)
4401        .await
4402        .map_err(|_| {
4403            dcp_error(
4404                ResponseStatus::DeadlineExceeded,
4405                "sharding request timed out",
4406            )
4407        })?
4408        .map_err(|_| dcp_error(ResponseStatus::Failed, "sharding region stopped"))?
4409        .map_err(|error| dcp_error(ResponseStatus::Failed, error.to_string()))
4410}
4411
4412fn complete_pending_ask(
4413    pending_asks: &PendingAskMap,
4414    request_id: u64,
4415    ok: bool,
4416    payload: Vec<u8>,
4417    message: String,
4418) {
4419    let sender = pending_asks
4420        .lock()
4421        .expect("pending asks poisoned")
4422        .remove(&request_id);
4423    if let Some(sender) = sender {
4424        let result = if ok {
4425            Ok(payload)
4426        } else {
4427            Err(ShardingError::Session(message))
4428        };
4429        let _ = sender.send(result);
4430    }
4431}
4432
4433fn dcp_error(status: ResponseStatus, message: impl Into<String>) -> datum_agent::dcp::DcpError {
4434    datum_agent::dcp::DcpError::Response {
4435        status,
4436        message: message.into(),
4437    }
4438}
4439
4440fn validate_config(config: &ShardingConfig) -> ShardingResult<()> {
4441    if config.num_shards == 0 {
4442        return Err(ShardingError::InvalidConfig(
4443            "num_shards must be non-zero".to_owned(),
4444        ));
4445    }
4446    if config.allocation_buffer == 0 {
4447        return Err(ShardingError::InvalidConfig(
4448            "allocation_buffer must be non-zero".to_owned(),
4449        ));
4450    }
4451    if config.request_timeout.is_zero() {
4452        return Err(ShardingError::InvalidConfig(
4453            "request_timeout must be non-zero".to_owned(),
4454        ));
4455    }
4456    if config.agent_role.trim().is_empty() {
4457        return Err(ShardingError::InvalidConfig(
4458            "agent_role must not be empty".to_owned(),
4459        ));
4460    }
4461    if config
4462        .passivation_idle_timeout
4463        .is_some_and(|timeout| timeout.is_zero())
4464    {
4465        return Err(ShardingError::InvalidConfig(
4466            "passivation_idle_timeout must be non-zero when configured".to_owned(),
4467        ));
4468    }
4469    if config.remember_entities.is_enabled() && config.remember_entities.queue_capacity == 0 {
4470        return Err(ShardingError::InvalidConfig(
4471            "remember_entities.queue_capacity must be non-zero when configured".to_owned(),
4472        ));
4473    }
4474    if config.remember_entities.event_buffer == 0 {
4475        return Err(ShardingError::InvalidConfig(
4476            "remember_entities.event_buffer must be non-zero".to_owned(),
4477        ));
4478    }
4479    Ok(())
4480}
4481
4482fn encode<T: Serialize>(value: &T) -> ShardingResult<Vec<u8>> {
4483    bincode::serde::encode_to_vec(value, bincode::config::standard()).map_err(ShardingError::codec)
4484}
4485
4486fn decode<T: DeserializeOwned>(payload: &[u8]) -> ShardingResult<T> {
4487    let (value, read): (T, usize) =
4488        bincode::serde::decode_from_slice(payload, bincode::config::standard())
4489            .map_err(ShardingError::codec)?;
4490    if read != payload.len() {
4491        return Err(ShardingError::Codec(
4492            "trailing bytes in sharding payload".to_owned(),
4493        ));
4494    }
4495    Ok(value)
4496}
4497
4498#[cfg(test)]
4499mod tests {
4500    use super::*;
4501
4502    #[tokio::test]
4503    async fn actor_entity_self_stop_waits_for_next_delivery_before_respawn() {
4504        use std::sync::atomic::AtomicUsize;
4505
4506        struct SelfStoppingActor;
4507
4508        impl Actor for SelfStoppingActor {
4509            type Msg = u8;
4510            type State = ();
4511            type Arguments = ();
4512
4513            async fn pre_start(
4514                &self,
4515                _myself: ActorRef<Self::Msg>,
4516                _args: Self::Arguments,
4517            ) -> Result<Self::State, ActorProcessingErr> {
4518                Ok(())
4519            }
4520
4521            async fn handle(
4522                &self,
4523                myself: ActorRef<Self::Msg>,
4524                message: Self::Msg,
4525                _state: &mut Self::State,
4526            ) -> Result<(), ActorProcessingErr> {
4527                if message == 0 {
4528                    myself.stop(None);
4529                }
4530                Ok(())
4531            }
4532        }
4533
4534        let starts = Arc::new(AtomicUsize::new(0));
4535        let runtime = ActorEntityTypeState::new("actor-stop-test".to_owned(), {
4536            let starts = Arc::clone(&starts);
4537            move |_context| {
4538                let starts = Arc::clone(&starts);
4539                move || async move {
4540                    starts.fetch_add(1, Ordering::SeqCst);
4541                    let (actor, _join) = Actor::spawn(None, SelfStoppingActor, ())
4542                        .await
4543                        .map_err(|error| ShardingError::Actor(error.to_string()))?;
4544                    Ok(ActorEntityHandle::from_actor_ref(actor, |message| message))
4545                }
4546            }
4547        });
4548
4549        let (first, started) = runtime.actor_for("entity-1", "0").await.unwrap();
4550        assert!(started);
4551        first.tell(0).unwrap();
4552        tokio::time::timeout(Duration::from_secs(1), async {
4553            while !first.is_stopped() {
4554                tokio::task::yield_now().await;
4555            }
4556        })
4557        .await
4558        .expect("self-stopped actor did not terminate");
4559        assert_eq!(starts.load(Ordering::SeqCst), 1);
4560
4561        let (_second, started) = runtime.actor_for("entity-1", "0").await.unwrap();
4562        assert!(started, "the next delivery must create a new incarnation");
4563        assert_eq!(starts.load(Ordering::SeqCst), 2);
4564        runtime.remove_actor("entity-1").await;
4565    }
4566
4567    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
4568    async fn actor_entity_idle_sweep_rechecks_activity_before_removal() {
4569        use tokio::{sync::Notify, time::timeout};
4570
4571        #[derive(Serialize, Deserialize)]
4572        enum TestMessage {
4573            Block,
4574            Record,
4575        }
4576
4577        struct Control {
4578            started: Notify,
4579            release: Notify,
4580            recorded: Notify,
4581        }
4582
4583        struct BlockingActor {
4584            control: Arc<Control>,
4585        }
4586
4587        impl Actor for BlockingActor {
4588            type Msg = TestMessage;
4589            type State = ();
4590            type Arguments = ();
4591
4592            async fn pre_start(
4593                &self,
4594                _myself: ActorRef<Self::Msg>,
4595                _args: Self::Arguments,
4596            ) -> Result<Self::State, ActorProcessingErr> {
4597                Ok(())
4598            }
4599
4600            async fn handle(
4601                &self,
4602                _myself: ActorRef<Self::Msg>,
4603                message: Self::Msg,
4604                _state: &mut Self::State,
4605            ) -> Result<(), ActorProcessingErr> {
4606                match message {
4607                    TestMessage::Block => {
4608                        self.control.started.notify_one();
4609                        self.control.release.notified().await;
4610                    }
4611                    TestMessage::Record => self.control.recorded.notify_one(),
4612                }
4613                Ok(())
4614            }
4615        }
4616
4617        fn control() -> Arc<Control> {
4618            Arc::new(Control {
4619                started: Notify::new(),
4620                release: Notify::new(),
4621                recorded: Notify::new(),
4622            })
4623        }
4624
4625        const BOUND: Duration = Duration::from_secs(1);
4626        let first_control = control();
4627        let target_control = control();
4628        let runtime = Arc::new(ActorEntityTypeState::new(
4629            "actor-idle-sweep-test".to_owned(),
4630            {
4631                let first_control = Arc::clone(&first_control);
4632                let target_control = Arc::clone(&target_control);
4633                move |context: EntityContext| {
4634                    let control = if context.entity_id == "a" {
4635                        Arc::clone(&first_control)
4636                    } else {
4637                        Arc::clone(&target_control)
4638                    };
4639                    move || async move {
4640                        let (actor, _join) = Actor::spawn(None, BlockingActor { control }, ())
4641                            .await
4642                            .map_err(|error| ShardingError::Actor(error.to_string()))?;
4643                        Ok(ActorEntityHandle::from_actor_ref(actor, |message| message))
4644                    }
4645                }
4646            },
4647        ));
4648
4649        let (first, _) = runtime.actor_for("a", "0").await.unwrap();
4650        let (target, _) = runtime.actor_for("b", "0").await.unwrap();
4651        first.tell(TestMessage::Block).unwrap();
4652        target.tell(TestMessage::Block).unwrap();
4653        timeout(BOUND, first_control.started.notified())
4654            .await
4655            .expect("first actor did not begin its blocking delivery");
4656        timeout(BOUND, target_control.started.notified())
4657            .await
4658            .expect("target actor did not begin its blocking delivery");
4659
4660        let sweep_now = Instant::now();
4661        let sweeping_runtime = Arc::clone(&runtime);
4662        let sweep = tokio::spawn(async move {
4663            sweeping_runtime
4664                .remove_idle_actors(Duration::from_nanos(1), sweep_now)
4665                .await
4666        });
4667        timeout(BOUND, async {
4668            while !first.is_stopped() {
4669                tokio::task::yield_now().await;
4670            }
4671        })
4672        .await
4673        .expect("idle sweep did not begin quiescing the first candidate");
4674
4675        let (target, started) = runtime.actor_for("b", "0").await.unwrap();
4676        assert!(
4677            !started,
4678            "activity must use the existing target incarnation"
4679        );
4680        target.tell(TestMessage::Record).unwrap();
4681
4682        first_control.release.notify_one();
4683        let removed = timeout(BOUND, sweep)
4684            .await
4685            .expect("idle sweep did not finish")
4686            .expect("idle sweep task failed");
4687        assert!(removed.iter().any(|entity| entity.entity_id == "a"));
4688        assert!(
4689            removed.iter().all(|entity| entity.entity_id != "b"),
4690            "the removal-time last_seen check must retain the concurrently active entity"
4691        );
4692
4693        target_control.release.notify_one();
4694        timeout(BOUND, target_control.recorded.notified())
4695            .await
4696            .expect("the concurrent delivery was silently dropped");
4697        runtime.remove_actor("b").await;
4698    }
4699
4700    fn generation_wins(
4701        entry_gen: u64,
4702        entry_node: &str,
4703        existing_gen: u64,
4704        existing_node: &str,
4705    ) -> bool {
4706        entry_gen > existing_gen || (entry_gen == existing_gen && entry_node > existing_node)
4707    }
4708
4709    #[test]
4710    fn merge_table_generation_tie_break_converges() {
4711        // Verify that when two entries have the same generation but different
4712        // node_ids, merge_table always picks the lexicographically greater
4713        // node_id, regardless of the order the entries are merged.
4714        assert!(generation_wins(5, "node-b", 5, "node-a"));
4715        assert!(!generation_wins(5, "node-a", 5, "node-b"));
4716        assert!(generation_wins(10, "node-a", 5, "node-b"));
4717        assert!(!generation_wins(5, "node-a", 10, "node-b"));
4718        assert!(!generation_wins(5, "node-a", 5, "node-a"));
4719
4720        let (generation, a, b) = (42, "coord-0", "coord-1");
4721        let ab = generation_wins(generation, b, generation, a);
4722        let ba = !generation_wins(generation, a, generation, b);
4723        assert!(ab, "b should win over a under generation(42) tie");
4724        assert!(ba, "a should NOT win over b under same-generation tie");
4725        assert_eq!(ab, ba);
4726    }
4727
4728    #[test]
4729    fn golden_shard_map_is_stable() {
4730        let extractor = DefaultShardExtractor::new(16);
4731        #[rustfmt::skip]
4732        let cases: &[(&str, u64)] = &[
4733            ("entity-0",               15),
4734            ("entity-1",               12),
4735            ("entity-100",             12),
4736            ("entity-999",             10),
4737            ("a",                      12),
4738            ("hello",                  11),
4739            ("",                       5),
4740            ("datum-sharding",         3),
4741            ("very-long-entity-id-that-exercises-fnv-loop-0123456789", 8),
4742        ];
4743        for &(entity_id, expected) in cases {
4744            let shard: String = ShardExtractor::<u8>::shard_id(&extractor, entity_id);
4745            assert_eq!(
4746                shard.parse::<u64>().unwrap(),
4747                expected,
4748                "shard mapping changed for {entity_id:?}: expected {expected}, got {shard}"
4749            );
4750        }
4751    }
4752
4753    #[test]
4754    fn push_bounded_round_caps_at_max_rounds() {
4755        const MAX_ROUNDS: usize = 1024;
4756
4757        let new_round = |r: u64| RebalanceRound {
4758            round: r,
4759            reason: RebalanceReason::DeadOwner,
4760            movements: vec![ShardMovement {
4761                type_name: "test".to_owned(),
4762                shard_id: format!("shard-{r}"),
4763                from_node: "old".to_owned(),
4764                to_node: "new".to_owned(),
4765                generation: r,
4766            }],
4767        };
4768
4769        let mut queue = VecDeque::new();
4770        assert_eq!(queue.len(), 0);
4771
4772        for r in 0..512 {
4773            push_bounded_round(&mut queue, new_round(r));
4774        }
4775        assert_eq!(queue.len(), 512);
4776
4777        for r in 512..1024 {
4778            push_bounded_round(&mut queue, new_round(r));
4779        }
4780        assert_eq!(queue.len(), 1024);
4781        assert_eq!(queue.front().unwrap().round, 0);
4782
4783        for r in 1024..2050 {
4784            push_bounded_round(&mut queue, new_round(r));
4785        }
4786        assert_eq!(queue.len(), 1024);
4787        assert_eq!(queue.front().unwrap().round, 2050 - MAX_ROUNDS as u64);
4788        assert_eq!(queue.back().unwrap().round, 2049);
4789
4790        for r in 2050..3050 {
4791            push_bounded_round(&mut queue, new_round(r));
4792        }
4793        assert_eq!(queue.len(), 1024);
4794        assert_eq!(queue.front().unwrap().round, 3050 - MAX_ROUNDS as u64);
4795        assert_eq!(queue.back().unwrap().round, 3049);
4796    }
4797
4798    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
4799    async fn handoff_stops_live_entities_on_losing_owner() {
4800        use datum_agent::{
4801            ClusterAgent, ClusterAgentConfig, ClusterAgentHandle, NodeSessionConfig,
4802            dcp::{DcpJobFactories, DcpServerConfig, DcpTcpServerConfig},
4803        };
4804        use datum_cluster::ClusterConfig;
4805        use std::time::Duration;
4806
4807        const TYPE: &str = "handoff-entity-test";
4808
4809        #[derive(serde::Serialize, serde::Deserialize)]
4810        enum Msg {
4811            Ping,
4812        }
4813        struct TestEntity;
4814        impl crate::EntityBehavior<Msg> for TestEntity {
4815            fn handle(
4816                &mut self,
4817                _context: &crate::EntityContext,
4818                _message: Msg,
4819            ) -> crate::ShardingResult<()> {
4820                Ok(())
4821            }
4822        }
4823
4824        fn agent_config(node_id: &str, seeds: Vec<std::net::SocketAddr>) -> ClusterAgentConfig {
4825            ClusterAgentConfig {
4826                cluster: ClusterConfig {
4827                    node_id: node_id.to_owned(),
4828                    seed_nodes: seeds,
4829                    bind_addr: "127.0.0.1:0".parse().unwrap(),
4830                    advertise_addr: "127.0.0.1:0".parse().unwrap(),
4831                    gossip_interval: Duration::from_millis(60),
4832                    probe_timeout: Duration::from_millis(15),
4833                    suspect_timeout: Duration::from_secs(2),
4834                    downing_timeout: Duration::from_millis(150),
4835                    remove_down_after: Duration::from_secs(20),
4836                    event_buffer: 512,
4837                    ..ClusterConfig::default()
4838                },
4839                dcp: DcpServerConfig {
4840                    node_id: node_id.to_owned(),
4841                    tcp: Some(DcpTcpServerConfig {
4842                        addr: "127.0.0.1:0".parse().unwrap(),
4843                    }),
4844                    ..DcpServerConfig::default()
4845                },
4846                sessions: NodeSessionConfig {
4847                    request_timeout: Duration::from_secs(2),
4848                    command_buffer: 256,
4849                    reconnect_min_backoff: Duration::from_millis(20),
4850                    reconnect_max_backoff: Duration::from_millis(100),
4851                    ..NodeSessionConfig::default()
4852                },
4853                ..ClusterAgentConfig::default()
4854            }
4855        }
4856
4857        let mut agents = Vec::new();
4858        for i in 0..2 {
4859            let seeds: Vec<_> = agents
4860                .first()
4861                .map(|a: &ClusterAgentHandle| vec![a.cluster().advertise_addr()])
4862                .unwrap_or_default();
4863            agents.push(
4864                ClusterAgent::start(
4865                    agent_config(&format!("hent-{i}"), seeds),
4866                    DcpJobFactories::new(),
4867                )
4868                .await
4869                .expect("agent"),
4870            );
4871        }
4872        tokio::time::sleep(Duration::from_millis(500)).await;
4873
4874        let sharding_config = ShardingConfig {
4875            num_shards: 8,
4876            rebalance_per_round: 8,
4877            handoff_drain_delay: Duration::from_millis(10),
4878            ..ShardingConfig::default()
4879        };
4880
4881        let mut handles = Vec::new();
4882        for agent in &agents {
4883            let handle = Sharding::init(agent, sharding_config.clone()).expect("sharding");
4884            handle
4885                .register_entity_type(TYPE, |_| TestEntity)
4886                .await
4887                .expect("register");
4888            handles.push(handle);
4889        }
4890
4891        // Start entities to populate shards.
4892        for entity in 0..80 {
4893            handles[0]
4894                .entity_ref::<Msg>(TYPE, format!("e-{entity}"))
4895                .tell(Msg::Ping)
4896                .await
4897                .expect("tell");
4898        }
4899
4900        // Find a shard owned by node 0 that has live entities.
4901        let table0 = handles[0].allocation_table(TYPE).await.expect("table");
4902        let node0 = agents[0].cluster().node_id().to_owned();
4903        let mut target_shard = None;
4904        for entry in &table0.entries {
4905            if entry.node_id == node0 {
4906                let count = handles[0]
4907                    .test_live_entity_count(TYPE, &entry.shard_id)
4908                    .await
4909                    .expect("count");
4910                if count > 0 {
4911                    target_shard = Some(entry.shard_id.clone());
4912                    break;
4913                }
4914            }
4915        }
4916        let target_shard =
4917            target_shard.expect("node 0 must own at least one shard with live entities");
4918
4919        let before_count = handles[0]
4920            .test_live_entity_count(TYPE, &target_shard)
4921            .await
4922            .expect("before count");
4923        assert!(
4924            before_count > 0,
4925            "node 0 must have live entities for shard {target_shard} before rebalance"
4926        );
4927
4928        // Add a 3rd node to trigger graceful rebalance.
4929        let seed = agents[0].cluster().advertise_addr();
4930        agents.push(
4931            ClusterAgent::start(agent_config("hent-2", vec![seed]), DcpJobFactories::new())
4932                .await
4933                .expect("agent 3"),
4934        );
4935        let handle3 = Sharding::init(&agents[2], sharding_config).expect("sharding 3");
4936        handle3
4937            .register_entity_type(TYPE, |_| TestEntity)
4938            .await
4939            .expect("register 3");
4940        handles.push(handle3);
4941        tokio::time::sleep(Duration::from_millis(500)).await;
4942
4943        // Wait for rebalance to move shards to the new node.
4944        let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
4945        let mut moved = false;
4946        while tokio::time::Instant::now() < deadline && !moved {
4947            if let Ok(table) = handles[0].allocation_table(TYPE).await {
4948                moved = table
4949                    .entries
4950                    .iter()
4951                    .any(|e| e.shard_id == target_shard && e.node_id != node0);
4952            }
4953            tokio::time::sleep(Duration::from_millis(20)).await;
4954        }
4955        assert!(
4956            moved,
4957            "shard {target_shard} must move off node 0 after rebalance"
4958        );
4959
4960        let after_count = handles[0]
4961            .test_live_entity_count(TYPE, &target_shard)
4962            .await
4963            .expect("after count");
4964        assert_eq!(
4965            after_count, 0,
4966            "node 0 must hold zero live entities for shard {target_shard} after handoff, got {after_count}"
4967        );
4968
4969        for h in &handles {
4970            h.shutdown().await;
4971        }
4972        for a in agents {
4973            let _ = a.shutdown().await;
4974        }
4975    }
4976}