1#![forbid(unsafe_code)]
2use 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
59pub use serde;
62
63pub 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
76pub type RememberEntitiesStoreFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
78
79pub type RememberEntitiesStoreResult<T> = Result<T, RememberEntitiesStoreError>;
81
82pub type ShardingResult<T> = Result<T, ShardingError>;
84
85#[derive(Debug, thiserror::Error)]
87pub enum ShardingError {
88 #[error("invalid sharding config: {0}")]
90 InvalidConfig(String),
91 #[error("sharding region stopped")]
93 Stopped,
94 #[error("no eligible shard owner: {0}")]
96 NoEligibleShardOwner(String),
97 #[error("entity type is not registered: {0}")]
99 EntityTypeNotRegistered(String),
100 #[error("shard allocation buffer overflow for {type_name}/{shard_id}")]
102 BufferOverflow { type_name: String, shard_id: String },
103 #[error("sharding request timed out after {0:?}")]
105 Timeout(Duration),
106 #[error("sharding codec failed: {0}")]
108 Codec(String),
109 #[error("node session failed: {0}")]
111 Session(String),
112 #[error("entity actor failed: {0}")]
114 Actor(String),
115 #[error("remember-entities store failed for {type_name}/{shard_id}: {message}")]
117 RememberEntities {
118 type_name: String,
120 shard_id: String,
122 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#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
135#[error("{message}")]
136pub struct RememberEntitiesStoreError {
137 message: String,
138}
139
140impl RememberEntitiesStoreError {
141 #[must_use]
143 pub fn new(message: impl Into<String>) -> Self {
144 Self {
145 message: message.into(),
146 }
147 }
148
149 #[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
162pub trait RememberEntitiesStore: Send + Sync + 'static {
170 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 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 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 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 fn flush<'a>(&'a self) -> RememberEntitiesStoreFuture<'a, RememberEntitiesStoreResult<()>> {
205 Box::pin(async { Ok(()) })
206 }
207}
208
209#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
211pub enum RememberEntitiesFailurePolicy {
212 #[default]
214 FailOpen,
215 FailClosed,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum RememberEntitiesOperation {
222 RecordStarted,
224 RecordStopped,
226 ListEntities,
228 ListShards,
230}
231
232#[derive(Debug, Clone, PartialEq, Eq)]
234pub struct RememberEntitiesEvent {
235 pub type_name: String,
237 pub shard_id: String,
239 pub entity_id: Option<String>,
241 pub operation: RememberEntitiesOperation,
243 pub policy: RememberEntitiesFailurePolicy,
245 pub message: String,
247}
248
249#[derive(Clone)]
251pub struct RememberEntitiesConfig {
252 store: Option<Arc<dyn RememberEntitiesStore>>,
253 pub queue_capacity: usize,
255 pub failure_policy: RememberEntitiesFailurePolicy,
257 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 #[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 #[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 #[must_use]
302 pub fn is_enabled(&self) -> bool {
303 self.store.is_some()
304 }
305
306 #[must_use]
308 pub const fn with_queue_capacity(mut self, capacity: usize) -> Self {
309 self.queue_capacity = capacity;
310 self
311 }
312
313 #[must_use]
315 pub const fn with_failure_policy(mut self, policy: RememberEntitiesFailurePolicy) -> Self {
316 self.failure_policy = policy;
317 self
318 }
319
320 #[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#[derive(Debug, Default, Clone)]
334pub struct InMemoryStore {
335 entities: Arc<Mutex<BTreeMap<RememberShardKey, BTreeSet<String>>>>,
336}
337
338impl InMemoryStore {
339 #[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#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
425pub enum FileStoreFsyncPolicy {
426 Never,
428 OnCompaction,
430 #[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#[derive(Debug, Clone)]
455pub struct FileStore {
456 dir: PathBuf,
457 fsync_policy: FileStoreFsyncPolicy,
458}
459
460impl FileStore {
461 #[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 #[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#[derive(Debug, Clone)]
735pub struct ShardingConfig {
736 pub num_shards: u64,
738 pub allocation_buffer: usize,
741 pub request_timeout: Duration,
743 pub command_buffer: usize,
745 pub role_constraint: Option<String>,
747 pub agent_role: String,
749 pub coordinator_tick: Duration,
751 pub rebalance_per_round: usize,
756 pub passivation_idle_timeout: Option<Duration>,
760 pub handoff_drain_delay: Duration,
762 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
790pub enum RebalanceReason {
791 DeadOwner,
793 GracefulSpread,
795}
796
797#[derive(Debug, Clone, PartialEq, Eq)]
799pub struct ShardMovement {
800 pub type_name: String,
802 pub shard_id: String,
804 pub from_node: String,
806 pub to_node: String,
808 pub generation: u64,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq)]
814pub struct RebalanceRound {
815 pub round: u64,
817 pub reason: RebalanceReason,
819 pub movements: Vec<ShardMovement>,
821}
822
823#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
825pub struct ShardEnvelope<M> {
826 pub entity_id: String,
828 pub message: M,
830}
831
832impl<M> ShardEnvelope<M> {
833 #[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
843pub trait ShardExtractor<M>: Send + Sync + 'static {
845 fn entity_id<'a>(&self, envelope: &'a ShardEnvelope<M>) -> &'a str {
847 &envelope.entity_id
848 }
849
850 fn shard_id(&self, entity_id: &str) -> String;
852}
853
854#[derive(Debug, Clone)]
869pub struct DefaultShardExtractor {
870 num_shards: u64,
871}
872
873impl DefaultShardExtractor {
874 #[must_use]
876 pub fn new(num_shards: u64) -> Self {
877 Self {
878 num_shards: num_shards.max(1),
879 }
880 }
881
882 #[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#[derive(Debug, Clone)]
909pub struct EntityContext {
910 pub type_name: String,
912 pub entity_id: String,
914}
915
916pub trait EntityBehavior<M>: Send + 'static {
918 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
931pub type ActorEntitySpawnFuture<M> =
933 Pin<Box<dyn Future<Output = ShardingResult<ActorEntityHandle<M>>> + Send + 'static>>;
934
935pub trait ActorEntity<M>: Send + 'static {
942 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
957pub 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 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 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 pub fn tell(&self, message: M) -> ShardingResult<()> {
1039 (self.tell)(message)
1040 }
1041
1042 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 #[must_use]
1057 pub fn is_stopped(&self) -> bool {
1058 (self.is_stopped)()
1059 }
1060}
1061
1062pub 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#[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 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
1241pub struct Sharding;
1243
1244impl Sharding {
1245 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
1335pub struct SingleNodeSharding {
1340 region: ShardingHandle,
1341 agent: Option<ClusterAgentHandle>,
1342}
1343
1344impl SingleNodeSharding {
1345 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 #[must_use]
1373 pub fn region(&self) -> &ShardingHandle {
1374 &self.region
1375 }
1376
1377 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#[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 #[must_use]
1415 pub fn node_id(&self) -> &str {
1416 &self.self_node
1417 }
1418
1419 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 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 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 #[must_use]
1523 pub fn subscribe_remember_entities_events(&self) -> broadcast::Receiver<RememberEntitiesEvent> {
1524 self.remember_events.subscribe()
1525 }
1526
1527 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 #[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 #[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 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 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 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
1646pub 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 #[must_use]
1697 pub fn entity_id(&self) -> &str {
1698 &self.entity_id
1699 }
1700
1701 #[must_use]
1703 pub fn shard_id(&self) -> &str {
1704 &self.shard_id
1705 }
1706
1707 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 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 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 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 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 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 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 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 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 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}