1use std::{
4 borrow::Borrow,
5 collections::HashMap,
6 fmt,
7 num::NonZeroUsize,
8 path::Path,
9 str::FromStr,
10 sync::{
11 Arc, Mutex,
12 atomic::{AtomicBool, Ordering},
13 mpsc::{self, SyncSender},
14 },
15 thread::{self, JoinHandle},
16};
17
18use arc_swap::{ArcSwap, ArcSwapOption};
19use thiserror::Error;
20
21use crate::{
22 WriterState,
23 domain::{DomainError, ObjectType, RelationName},
24 error::ZanzibarError,
25 eval::{self, EvaluationError, EvaluationLimits},
26 model::{
27 CheckRequest, CheckResponse, ExpandRequest, ExpandResponse, ExpandedUserset,
28 LookupObjectPermissions, LookupObjectPermissionsRequest, LookupPermissions,
29 LookupPermissionsRequest, LookupResources, LookupResourcesRequest, LookupSubjects,
30 LookupSubjectsRequest, NamespaceConfig, Object, PermissionSubjects, Relation,
31 RelationTuple, User,
32 },
33 policy::{self, PolicyIoError, PolicyText},
34 relationship::{Precondition, RelationshipMutation, StoreError},
35 revision::{Consistency, ConsistencyError, ConsistencyToken, default_retained_snapshots},
36 runtime::{EngineState, SharedEngineState},
37 schema::{SchemaError, SchemaSource},
38 snapshot::{IndexProfile, SnapshotIoError, SnapshotLoadOptions, SnapshotSaveOptions},
39};
40
41const DEFAULT_WRITER_QUEUE_CAPACITY: usize = 1024;
42const MAX_TENANT_ID_BYTES: usize = 128;
43
44macro_rules! enter_api_span {
45 ($operation:literal) => {
46 #[cfg(feature = "tracing")]
47 let _span_guard = tracing::debug_span!("zanzibar.engine", operation = $operation).entered();
48 };
49}
50
51#[derive(Debug)]
57pub struct ZanzibarEngine {
58 state: SharedEngineState,
59 writer: WriterActor,
60}
61
62impl ZanzibarEngine {
63 #[must_use]
65 pub fn builder() -> ZanzibarEngineBuilder {
66 ZanzibarEngineBuilder::new()
67 }
68
69 pub fn check(&self, request: CheckRequest) -> Result<CheckResponse, EngineError> {
76 enter_api_span!("check");
77 request.validate()?;
78 let (snapshot, limits) = self.snapshot_for_consistency(request.consistency)?;
79 let object_type = ObjectType::try_from(request.object.namespace.as_str())?;
80 let relation_name = RelationName::try_from(request.relation.0.as_str())?;
81 let relation_definition = snapshot
82 .schema()
83 .resolver()
84 .relation(&object_type, &relation_name)?;
85 let allowed = eval::check_prepared_with_snapshot(
86 &snapshot,
87 &request.object,
88 &request.relation,
89 &request.user,
90 relation_definition,
91 limits,
92 )?
93 .is_allowed();
94 Ok(CheckResponse { allowed })
95 }
96
97 pub fn check_relation(
104 &self,
105 object: &Object,
106 relation: &Relation,
107 user: &User,
108 ) -> Result<bool, EngineError> {
109 self.check_relation_with_consistency(object, relation, user, Consistency::Latest)
110 }
111
112 pub fn check_relation_with_consistency(
119 &self,
120 object: &Object,
121 relation: &Relation,
122 user: &User,
123 consistency: Consistency,
124 ) -> Result<bool, EngineError> {
125 Ok(self
126 .check(CheckRequest::new(
127 object.clone(),
128 relation.clone(),
129 user.clone(),
130 consistency,
131 ))?
132 .allowed)
133 }
134
135 pub fn check_with_consistency(
144 &self,
145 object: &Object,
146 relation: &Relation,
147 user: &User,
148 consistency: Consistency,
149 ) -> Result<bool, EngineError> {
150 self.check_relation_with_consistency(object, relation, user, consistency)
151 }
152
153 pub fn expand(&self, request: ExpandRequest) -> Result<ExpandResponse, EngineError> {
160 enter_api_span!("expand");
161 request.validate()?;
162 let (snapshot, limits) = self.snapshot_for_consistency(request.consistency)?;
163 let object_type = ObjectType::try_from(request.object.namespace.as_str())?;
164 let relation_name = RelationName::try_from(request.relation.0.as_str())?;
165 snapshot
166 .schema()
167 .resolver()
168 .relation(&object_type, &relation_name)?;
169 let expanded =
170 eval::expand_with_snapshot(&snapshot, &request.object, &request.relation, limits)?;
171 Ok(ExpandResponse { expanded })
172 }
173
174 pub fn expand_relation(
181 &self,
182 object: &Object,
183 relation: &Relation,
184 ) -> Result<ExpandedUserset, EngineError> {
185 Ok(self
186 .expand(ExpandRequest::new(
187 object.clone(),
188 relation.clone(),
189 Consistency::Latest,
190 ))?
191 .expanded)
192 }
193
194 pub fn lookup_resources(
200 &self,
201 request: impl Borrow<LookupResourcesRequest>,
202 ) -> Result<LookupResources, EngineError> {
203 self.lookup_resources_with_consistency(request, Consistency::Latest)
204 }
205
206 pub fn lookup_resources_with_consistency(
213 &self,
214 request: impl Borrow<LookupResourcesRequest>,
215 consistency: Consistency,
216 ) -> Result<LookupResources, EngineError> {
217 enter_api_span!("lookup_resources");
218 request.borrow().validate()?;
219 let (snapshot, limits) = self.snapshot_for_consistency(consistency)?;
220 Self::ensure_subject_reverse_lookup_supported(&snapshot, "lookup_resources")?;
221 let request = request.borrow();
222 Ok(eval::lookup_resources_with_snapshot(
223 &snapshot, request, limits,
224 )?)
225 }
226
227 pub fn lookup_subjects(
233 &self,
234 request: impl Borrow<LookupSubjectsRequest>,
235 ) -> Result<LookupSubjects, EngineError> {
236 self.lookup_subjects_with_consistency(request, Consistency::Latest)
237 }
238
239 pub fn lookup_subjects_with_consistency(
246 &self,
247 request: impl Borrow<LookupSubjectsRequest>,
248 consistency: Consistency,
249 ) -> Result<LookupSubjects, EngineError> {
250 enter_api_span!("lookup_subjects");
251 request.borrow().validate()?;
252 let (snapshot, limits) = self.snapshot_for_consistency(consistency)?;
253 let request = request.borrow();
254 Ok(eval::lookup_subjects_with_snapshot(
255 &snapshot, request, limits,
256 )?)
257 }
258
259 #[cfg(feature = "bench-internals")]
265 pub fn bench_relationship_delta_stats(
266 &self,
267 consistency: Consistency,
268 ) -> Result<crate::relationship::StoreViewDeltaStats, EngineError> {
269 let (snapshot, _) = self.snapshot_for_consistency(consistency)?;
270 Ok(snapshot.relationships().delta_stats())
271 }
272
273 #[cfg(feature = "bench-internals")]
279 pub fn bench_relationship_posting_histograms(
280 &self,
281 consistency: Consistency,
282 ) -> Result<crate::relationship::StorePostingHistograms, EngineError> {
283 let (snapshot, _) = self.snapshot_for_consistency(consistency)?;
284 Ok(snapshot.relationships().posting_histograms())
285 }
286
287 pub fn write_relationships(
294 &self,
295 mutations: impl IntoIterator<Item = RelationshipMutation>,
296 ) -> Result<ConsistencyToken, EngineError> {
297 enter_api_span!("write_relationships");
298 self.write_relationships_with_preconditions(mutations, [])
299 }
300
301 pub fn apply_relationship_mutations(
311 &self,
312 mutations: impl IntoIterator<Item = RelationshipMutation>,
313 preconditions: impl IntoIterator<Item = Precondition>,
314 ) -> Result<ConsistencyToken, EngineError> {
315 self.write_relationships_with_preconditions(mutations, preconditions)
316 }
317
318 pub fn write_tuple(&self, tuple: impl Borrow<RelationTuple>) -> Result<(), EngineError> {
327 self.write_tuple_with_token(tuple.borrow()).map(drop)
328 }
329
330 pub fn write_tuple_with_token(
336 &self,
337 tuple: &RelationTuple,
338 ) -> Result<ConsistencyToken, EngineError> {
339 let relationship = crate::domain::Relationship::try_from(tuple)?;
340 self.write_relationships([RelationshipMutation::Touch(relationship)])
341 }
342
343 pub fn delete_tuple(&self, tuple: &RelationTuple) -> Result<(), EngineError> {
350 let relationship = crate::domain::Relationship::try_from(tuple)?;
351 self.write_relationships([RelationshipMutation::Delete(relationship)])
352 .map(drop)
353 }
354
355 pub fn delete_tuple_with_token(
362 &self,
363 tuple: &RelationTuple,
364 ) -> Result<ConsistencyToken, EngineError> {
365 let relationship = crate::domain::Relationship::try_from(tuple)?;
366 self.write_relationships([RelationshipMutation::Delete(relationship)])
367 }
368
369 pub fn create_relationship(&self, relationship: &str) -> Result<ConsistencyToken, EngineError> {
376 enter_api_span!("create_relationship");
377 self.write_relationships([RelationshipMutation::create(relationship)?])
378 }
379
380 pub fn touch_relationship(&self, relationship: &str) -> Result<ConsistencyToken, EngineError> {
387 enter_api_span!("touch_relationship");
388 self.write_relationships([RelationshipMutation::touch(relationship)?])
389 }
390
391 pub fn delete_relationship(&self, relationship: &str) -> Result<ConsistencyToken, EngineError> {
398 enter_api_span!("delete_relationship");
399 self.write_relationships([RelationshipMutation::delete(relationship)?])
400 }
401
402 pub fn write_relationships_with_preconditions(
409 &self,
410 mutations: impl IntoIterator<Item = RelationshipMutation>,
411 preconditions: impl IntoIterator<Item = Precondition>,
412 ) -> Result<ConsistencyToken, EngineError> {
413 enter_api_span!("write_relationships_with_preconditions");
414 self.submit_write("write_relationships", |response| {
415 WriterCommand::WriteRelationships {
416 mutations: mutations.into_iter().collect(),
417 preconditions: preconditions.into_iter().collect(),
418 response,
419 }
420 })
421 }
422
423 pub fn apply_schema(&self, source: SchemaSource<'_>) -> Result<ConsistencyToken, EngineError> {
429 enter_api_span!("apply_schema");
430 self.submit_write("apply_schema", |response| WriterCommand::ApplySchema {
431 text: source.text.to_string(),
432 response,
433 })
434 }
435
436 pub fn add_dsl(&self, dsl: &str) -> Result<(), EngineError> {
442 self.add_dsl_with_token(dsl).map(drop)
443 }
444
445 pub fn add_dsl_with_token(&self, dsl: &str) -> Result<ConsistencyToken, EngineError> {
451 self.apply_schema(SchemaSource {
452 name: Some("dsl"),
453 text: dsl,
454 })
455 }
456
457 pub fn apply_namespace_config(
463 &self,
464 config: NamespaceConfig,
465 ) -> Result<ConsistencyToken, EngineError> {
466 self.apply_namespace_configs([config])
467 }
468
469 pub fn add_config_with_token(
475 &self,
476 config: NamespaceConfig,
477 ) -> Result<ConsistencyToken, EngineError> {
478 self.apply_namespace_config(config)
479 }
480
481 pub fn apply_namespace_configs(
487 &self,
488 configs: impl IntoIterator<Item = NamespaceConfig>,
489 ) -> Result<ConsistencyToken, EngineError> {
490 let configs = configs.into_iter().collect();
491 self.submit_write("apply_namespace_configs", |response| {
492 WriterCommand::ApplyNamespaceConfigs { configs, response }
493 })
494 }
495
496 pub fn replace_schema(
503 &self,
504 source: SchemaSource<'_>,
505 ) -> Result<ConsistencyToken, EngineError> {
506 enter_api_span!("replace_schema");
507 self.submit_write("replace_schema", |response| WriterCommand::ReplaceSchema {
508 text: source.text.to_string(),
509 response,
510 })
511 }
512
513 pub fn delete_namespace(&self, namespace: &str) -> Result<ConsistencyToken, EngineError> {
520 enter_api_span!("delete_namespace");
521 self.submit_write("delete_namespace", |response| {
522 WriterCommand::DeleteNamespace {
523 namespace: namespace.to_string(),
524 response,
525 }
526 })
527 }
528
529 pub fn delete_relation(
536 &self,
537 namespace: &str,
538 relation: &str,
539 ) -> Result<ConsistencyToken, EngineError> {
540 enter_api_span!("delete_relation");
541 self.submit_write("delete_relation", |response| {
542 WriterCommand::DeleteRelation {
543 namespace: namespace.to_string(),
544 relation: relation.to_string(),
545 response,
546 }
547 })
548 }
549
550 pub fn from_policy_text(policy: &PolicyText) -> Result<Self, EngineError> {
556 enter_api_span!("from_policy_text");
557 let engine = Self::builder().build();
558 engine.apply_policy_text(policy)?;
559 Ok(engine)
560 }
561
562 pub fn apply_policy_text(&self, policy: &PolicyText) -> Result<ConsistencyToken, EngineError> {
568 enter_api_span!("apply_policy_text");
569 let policy = policy.clone();
570 self.submit_write("apply_policy_text", |response| {
571 WriterCommand::ApplyPolicyText { policy, response }
572 })
573 }
574
575 pub fn save_snapshot_from_policy_text(
581 path: impl AsRef<Path>,
582 policy: &PolicyText,
583 options: SnapshotSaveOptions,
584 ) -> Result<(), PolicyIoError> {
585 policy::save_snapshot_from_policy_text(path.as_ref(), policy, options)
586 }
587
588 pub fn export_policy_text(&self) -> Result<PolicyText, EngineError> {
594 enter_api_span!("export_policy_text");
595 let snapshot = self.latest_snapshot()?;
596 Ok(policy::export_policy_text(
597 snapshot.configs(),
598 snapshot.relationships().rows(),
599 ))
600 }
601
602 pub fn export_policy_files(&self, directory: impl AsRef<Path>) -> Result<(), PolicyIoError> {
608 enter_api_span!("export_policy_files");
609 let snapshot = self
610 .latest_snapshot()
611 .map_err(|_| PolicyIoError::Zanzibar {
612 source: ZanzibarError::SchemaRequired,
613 })?;
614 let policy =
615 policy::export_policy_text(snapshot.configs(), snapshot.relationships().rows());
616 policy::write_policy_files(directory.as_ref(), &policy)
617 }
618
619 pub fn lookup_permissions(
625 &self,
626 request: impl Borrow<LookupPermissionsRequest>,
627 ) -> Result<LookupPermissions, EngineError> {
628 enter_api_span!("lookup_permissions");
629 let request = request.borrow();
630 request.validate()?;
631 let (snapshot, limits) = self.snapshot_for_consistency(request.consistency.clone())?;
632 let object_type = ObjectType::try_from(request.resource.namespace.as_str())?;
633 snapshot.schema().resolver().namespace(&object_type)?;
634 let mut permissions = Vec::new();
635 let mut check_context = eval::EvaluationContext::new_with_request_memo(&snapshot, limits);
636 for relation_definition in snapshot
637 .schema()
638 .resolver()
639 .sorted_relations(&object_type)?
640 {
641 let relation = Relation(relation_definition.name().as_str().to_string());
642 check_context.reset_for_reuse();
643 if check_context
644 .check_prepared(
645 &request.resource,
646 &relation,
647 &request.subject,
648 relation_definition,
649 )?
650 .is_allowed()
651 {
652 permissions.push(relation);
653 }
654 }
655 Ok(LookupPermissions { permissions })
656 }
657
658 pub fn lookup_object_permissions(
664 &self,
665 request: impl Borrow<LookupObjectPermissionsRequest>,
666 ) -> Result<LookupObjectPermissions, EngineError> {
667 enter_api_span!("lookup_object_permissions");
668 let request = request.borrow();
669 request.validate()?;
670 let (snapshot, limits) = self.snapshot_for_consistency(request.consistency.clone())?;
671 let object_type = ObjectType::try_from(request.resource.namespace.as_str())?;
672 snapshot.schema().resolver().namespace(&object_type)?;
673 let subject_type = crate::domain::SubjectType::try_from(request.subject_type.as_str())?;
674 if subject_type.as_str() != "user" {
675 let subject_object_type = ObjectType::try_from(subject_type.as_str())?;
676 snapshot
677 .schema()
678 .resolver()
679 .namespace(&subject_object_type)?;
680 }
681
682 let mut permissions = Vec::new();
683 for relation_definition in snapshot
684 .schema()
685 .resolver()
686 .sorted_relations(&object_type)?
687 {
688 let permission = Relation(relation_definition.name().as_str().to_string());
689 let subjects = eval::lookup_subjects_with_snapshot(
690 &snapshot,
691 &LookupSubjectsRequest {
692 resource: request.resource.clone(),
693 permission: permission.clone(),
694 subject_type: request.subject_type.clone(),
695 },
696 limits,
697 )?
698 .subjects;
699 if !subjects.is_empty() {
700 permissions.push(PermissionSubjects {
701 permission,
702 subjects,
703 });
704 }
705 }
706 Ok(LookupObjectPermissions { permissions })
707 }
708
709 pub fn save_snapshot(
716 &self,
717 path: impl AsRef<Path>,
718 options: SnapshotSaveOptions,
719 ) -> Result<(), SnapshotIoError> {
720 enter_api_span!("save_snapshot");
721 let snapshot = self.latest_snapshot().map_err(|error| match error {
722 EngineError::SchemaRequired => SnapshotIoError::Format {
723 reason: "schema snapshot is required before saving",
724 },
725 _ => SnapshotIoError::Format {
726 reason: "engine state unavailable during snapshot save",
727 },
728 })?;
729 crate::snapshot::save_snapshot_file(path.as_ref(), &snapshot, options)
730 }
731
732 pub fn load_snapshot(
738 path: impl AsRef<Path>,
739 options: SnapshotLoadOptions,
740 ) -> Result<Self, SnapshotIoError> {
741 enter_api_span!("load_snapshot");
742 let state = Arc::new(ArcSwapOption::empty());
743 let writer_state =
744 WriterState::load_snapshot_with_publisher(path, options, Arc::clone(&state))?;
745 Ok(Self {
746 state,
747 writer: WriterActor::start(writer_state, default_writer_queue_capacity()),
748 })
749 }
750
751 fn submit_write(
752 &self,
753 operation: &'static str,
754 build_command: impl FnOnce(WriteResponseSender) -> WriterCommand,
755 ) -> Result<ConsistencyToken, EngineError> {
756 let (sender, receiver) = mpsc::sync_channel(1);
757 self.writer.send(build_command(sender), operation)?;
758 receiver
759 .recv()
760 .map_err(|_| EngineError::WriterUnavailable { operation })?
761 .map_err(EngineError::from)
762 }
763
764 fn current_state(&self) -> Result<Arc<EngineState>, EngineError> {
765 self.state.load_full().ok_or(EngineError::SchemaRequired)
766 }
767
768 fn latest_snapshot(&self) -> Result<Arc<crate::revision::PublishedSnapshot>, EngineError> {
769 Ok(self.current_state()?.latest_snapshot())
770 }
771
772 fn snapshot_for_consistency(
773 &self,
774 consistency: Consistency,
775 ) -> Result<(Arc<crate::revision::PublishedSnapshot>, EvaluationLimits), EngineError> {
776 let state = self.current_state()?;
777 let limits = state.evaluation_limits();
778 let snapshot = state.snapshot_for_consistency(consistency)?;
779 Ok((snapshot, limits))
780 }
781
782 fn ensure_subject_reverse_lookup_supported(
783 snapshot: &crate::revision::PublishedSnapshot,
784 operation: &'static str,
785 ) -> Result<(), EngineError> {
786 let profile = snapshot.relationships().index_profile();
787 if profile.supports_subject_reverse_lookup() {
788 return Ok(());
789 }
790 Err(EngineError::UnsupportedIndexProfile { operation, profile })
791 }
792}
793
794impl Default for ZanzibarEngine {
795 fn default() -> Self {
796 Self::builder().build()
797 }
798}
799
800#[derive(Debug, Clone, Copy)]
802pub struct ZanzibarEngineBuilder {
803 retained_snapshots: NonZeroUsize,
804 evaluation_limits: EvaluationLimits,
805 writer_queue_capacity: NonZeroUsize,
806}
807
808impl ZanzibarEngineBuilder {
809 #[must_use]
811 pub fn new() -> Self {
812 Self {
813 retained_snapshots: default_retained_snapshots(),
814 evaluation_limits: EvaluationLimits::default(),
815 writer_queue_capacity: default_writer_queue_capacity(),
816 }
817 }
818
819 #[must_use]
821 pub fn retained_snapshots(mut self, retained_snapshots: NonZeroUsize) -> Self {
822 self.retained_snapshots = retained_snapshots;
823 self
824 }
825
826 #[must_use]
828 pub fn evaluation_limits(mut self, evaluation_limits: EvaluationLimits) -> Self {
829 self.evaluation_limits = evaluation_limits;
830 self
831 }
832
833 #[must_use]
835 pub fn writer_queue_capacity(mut self, writer_queue_capacity: NonZeroUsize) -> Self {
836 self.writer_queue_capacity = writer_queue_capacity;
837 self
838 }
839
840 #[must_use]
842 pub fn build(self) -> ZanzibarEngine {
843 let state = Arc::new(ArcSwapOption::empty());
844 let writer_state = WriterState::with_snapshot_retention_and_publisher(
845 self.retained_snapshots,
846 Arc::clone(&state),
847 )
848 .with_evaluation_limits(self.evaluation_limits);
849 ZanzibarEngine {
850 state,
851 writer: WriterActor::start(writer_state, self.writer_queue_capacity),
852 }
853 }
854}
855
856impl Default for ZanzibarEngineBuilder {
857 fn default() -> Self {
858 Self::new()
859 }
860}
861
862#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
864pub struct TenantId(String);
865
866impl TenantId {
867 pub fn new(value: impl Into<String>) -> Result<Self, TenantIdError> {
873 let value = value.into();
874 validate_tenant_id(&value)?;
875 Ok(Self(value))
876 }
877
878 #[must_use]
880 pub fn as_str(&self) -> &str {
881 &self.0
882 }
883}
884
885impl fmt::Display for TenantId {
886 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
887 formatter.write_str(&self.0)
888 }
889}
890
891impl FromStr for TenantId {
892 type Err = TenantIdError;
893
894 fn from_str(value: &str) -> Result<Self, Self::Err> {
895 Self::new(value)
896 }
897}
898
899impl TryFrom<&str> for TenantId {
900 type Error = TenantIdError;
901
902 fn try_from(value: &str) -> Result<Self, Self::Error> {
903 Self::new(value)
904 }
905}
906
907#[derive(Debug, Clone, PartialEq, Eq, Error)]
909pub enum TenantIdError {
910 #[error("tenant id must not be empty")]
912 Empty,
913 #[error("tenant id exceeds maximum byte length {max_bytes}")]
915 TooLong {
916 max_bytes: usize,
918 },
919 #[error("tenant id contains invalid byte at offset {offset}")]
921 InvalidByte {
922 offset: usize,
924 },
925}
926
927#[derive(Debug)]
929pub struct ZanzibarTenantShards {
930 shards: Arc<ArcSwap<ShardMap>>,
931 create_gate: Mutex<()>,
932 builder: ZanzibarEngineBuilder,
933}
934
935impl ZanzibarTenantShards {
936 #[must_use]
938 pub fn new(builder: ZanzibarEngineBuilder) -> Self {
939 Self {
940 shards: Arc::new(ArcSwap::from_pointee(ShardMap::default())),
941 create_gate: Mutex::new(()),
942 builder,
943 }
944 }
945
946 #[must_use]
948 pub fn get(&self, tenant: &TenantId) -> Option<Arc<ZanzibarEngine>> {
949 self.shards.load_full().engines.get(tenant).cloned()
950 }
951
952 #[must_use]
954 pub fn get_or_create(&self, tenant: TenantId) -> Arc<ZanzibarEngine> {
955 if let Some(engine) = self.get(&tenant) {
956 return engine;
957 }
958 let _guard = self
959 .create_gate
960 .lock()
961 .unwrap_or_else(std::sync::PoisonError::into_inner);
962 if let Some(engine) = self.get(&tenant) {
963 return engine;
964 }
965 let mut next = (*self.shards.load_full()).clone();
966 let engine = Arc::new(self.builder.build());
967 next.engines.insert(tenant, Arc::clone(&engine));
968 self.shards.store(Arc::new(next));
969 engine
970 }
971
972 #[must_use]
974 pub fn tenants(&self) -> Vec<TenantId> {
975 let mut tenants = self
976 .shards
977 .load_full()
978 .engines
979 .keys()
980 .cloned()
981 .collect::<Vec<_>>();
982 tenants.sort();
983 tenants
984 }
985}
986
987impl Default for ZanzibarTenantShards {
988 fn default() -> Self {
989 Self::new(ZanzibarEngine::builder())
990 }
991}
992
993#[derive(Debug, Error, PartialEq)]
995pub enum EngineError {
996 #[error(transparent)]
998 Domain(DomainError),
999
1000 #[error(transparent)]
1002 Schema(SchemaError),
1003
1004 #[error(transparent)]
1006 Store(StoreError),
1007
1008 #[error(transparent)]
1010 Consistency(ConsistencyError),
1011
1012 #[error(transparent)]
1014 Evaluation(EvaluationError),
1015
1016 #[error("namespace '{namespace}' not found")]
1018 NamespaceNotFound {
1019 namespace: String,
1021 },
1022
1023 #[error("relation '{relation}' not found in namespace '{namespace}'")]
1025 RelationNotFound {
1026 namespace: String,
1028 relation: String,
1030 },
1031
1032 #[error("schema parse error: {message}")]
1034 ParseError {
1035 message: String,
1037 },
1038
1039 #[error("storage error: {message}")]
1041 StorageError {
1042 message: String,
1044 },
1045
1046 #[error("schema must be loaded before this operation")]
1048 SchemaRequired,
1049
1050 #[error("engine writer actor unavailable during {operation}")]
1052 WriterUnavailable {
1053 operation: &'static str,
1055 },
1056
1057 #[error("index profile {profile:?} does not support {operation}")]
1059 UnsupportedIndexProfile {
1060 operation: &'static str,
1062 profile: IndexProfile,
1064 },
1065}
1066
1067impl From<ZanzibarError> for EngineError {
1068 fn from(error: ZanzibarError) -> Self {
1069 match error {
1070 ZanzibarError::NamespaceNotFound(namespace) => Self::NamespaceNotFound { namespace },
1071 ZanzibarError::RelationNotFound(relation, namespace) => Self::RelationNotFound {
1072 namespace,
1073 relation,
1074 },
1075 ZanzibarError::ParseError(message) => Self::ParseError { message },
1076 ZanzibarError::StorageError(message) => Self::StorageError { message },
1077 ZanzibarError::SchemaRequired => Self::SchemaRequired,
1078 ZanzibarError::Domain(error) => Self::Domain(error),
1079 ZanzibarError::Schema(error) => Self::Schema(error),
1080 ZanzibarError::Store(error) => Self::Store(error),
1081 ZanzibarError::Consistency(error) => Self::Consistency(error),
1082 ZanzibarError::Evaluation(error) => Self::Evaluation(error),
1083 }
1084 }
1085}
1086
1087impl From<EngineError> for ZanzibarError {
1088 fn from(error: EngineError) -> Self {
1089 match error {
1090 EngineError::NamespaceNotFound { namespace } => Self::NamespaceNotFound(namespace),
1091 EngineError::RelationNotFound {
1092 namespace,
1093 relation,
1094 } => Self::RelationNotFound(relation, namespace),
1095 EngineError::ParseError { message } => Self::ParseError(message),
1096 EngineError::StorageError { message } => Self::StorageError(message),
1097 EngineError::SchemaRequired => Self::SchemaRequired,
1098 EngineError::Domain(error) => Self::Domain(error),
1099 EngineError::Schema(error) => Self::Schema(error),
1100 EngineError::Store(error) => Self::Store(error),
1101 EngineError::Consistency(error) => Self::Consistency(error),
1102 EngineError::Evaluation(error) => Self::Evaluation(error),
1103 EngineError::WriterUnavailable { operation } => Self::StorageError(format!(
1104 "engine writer actor unavailable during {operation}",
1105 )),
1106 EngineError::UnsupportedIndexProfile { operation, profile } => Self::StorageError(
1107 format!("index profile {profile:?} does not support {operation}"),
1108 ),
1109 }
1110 }
1111}
1112
1113impl From<DomainError> for EngineError {
1114 fn from(error: DomainError) -> Self {
1115 Self::Domain(error)
1116 }
1117}
1118
1119impl From<SchemaError> for EngineError {
1120 fn from(error: SchemaError) -> Self {
1121 Self::Schema(error)
1122 }
1123}
1124
1125impl From<StoreError> for EngineError {
1126 fn from(error: StoreError) -> Self {
1127 Self::Store(error)
1128 }
1129}
1130
1131impl From<ConsistencyError> for EngineError {
1132 fn from(error: ConsistencyError) -> Self {
1133 Self::Consistency(error)
1134 }
1135}
1136
1137impl From<EvaluationError> for EngineError {
1138 fn from(error: EvaluationError) -> Self {
1139 Self::Evaluation(error)
1140 }
1141}
1142
1143type WriteResponseSender = SyncSender<Result<ConsistencyToken, ZanzibarError>>;
1144
1145enum WriterCommand {
1146 WriteRelationships {
1147 mutations: Vec<RelationshipMutation>,
1148 preconditions: Vec<Precondition>,
1149 response: WriteResponseSender,
1150 },
1151 ApplySchema {
1152 text: String,
1153 response: WriteResponseSender,
1154 },
1155 ApplyNamespaceConfigs {
1156 configs: Vec<NamespaceConfig>,
1157 response: WriteResponseSender,
1158 },
1159 ReplaceSchema {
1160 text: String,
1161 response: WriteResponseSender,
1162 },
1163 DeleteNamespace {
1164 namespace: String,
1165 response: WriteResponseSender,
1166 },
1167 DeleteRelation {
1168 namespace: String,
1169 relation: String,
1170 response: WriteResponseSender,
1171 },
1172 ApplyPolicyText {
1173 policy: PolicyText,
1174 response: WriteResponseSender,
1175 },
1176 Shutdown,
1177}
1178
1179struct WriterActor {
1180 sender: SyncSender<WriterCommand>,
1181 handle: Mutex<Option<JoinHandle<()>>>,
1182 shutdown: AtomicBool,
1183}
1184
1185impl WriterActor {
1186 fn start(mut state: WriterState, queue_capacity: NonZeroUsize) -> Self {
1187 let (sender, receiver) = mpsc::sync_channel(queue_capacity.get());
1188 let handle = thread::spawn(move || {
1189 while let Ok(command) = receiver.recv() {
1190 match command {
1191 WriterCommand::WriteRelationships {
1192 mutations,
1193 preconditions,
1194 response,
1195 } => {
1196 drop(
1197 response
1198 .send(state.apply_relationship_mutations(mutations, preconditions)),
1199 );
1200 }
1201 WriterCommand::ApplySchema { text, response } => {
1202 drop(response.send(state.add_dsl_with_token(&text)));
1203 }
1204 WriterCommand::ApplyNamespaceConfigs { configs, response } => {
1205 drop(response.send(state.apply_namespace_configs(configs)));
1206 }
1207 WriterCommand::ReplaceSchema { text, response } => {
1208 drop(response.send(state.replace_dsl_with_token(&text)));
1209 }
1210 WriterCommand::DeleteNamespace {
1211 namespace,
1212 response,
1213 } => {
1214 drop(response.send(state.delete_namespace(&namespace)));
1215 }
1216 WriterCommand::DeleteRelation {
1217 namespace,
1218 relation,
1219 response,
1220 } => {
1221 drop(response.send(state.delete_relation(&namespace, &relation)));
1222 }
1223 WriterCommand::ApplyPolicyText { policy, response } => {
1224 drop(response.send(state.apply_policy_text(&policy)));
1225 }
1226 WriterCommand::Shutdown => break,
1227 }
1228 }
1229 });
1230 Self {
1231 sender,
1232 handle: Mutex::new(Some(handle)),
1233 shutdown: AtomicBool::new(false),
1234 }
1235 }
1236
1237 fn send(&self, command: WriterCommand, operation: &'static str) -> Result<(), EngineError> {
1238 if self.shutdown.load(Ordering::Acquire) {
1239 return Err(EngineError::WriterUnavailable { operation });
1240 }
1241 self.sender
1242 .send(command)
1243 .map_err(|_| EngineError::WriterUnavailable { operation })
1244 }
1245}
1246
1247impl fmt::Debug for WriterActor {
1248 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1249 formatter
1250 .debug_struct("WriterActor")
1251 .field("sender", &"<sync sender>")
1252 .field("handle", &"<join handle>")
1253 .finish()
1254 }
1255}
1256
1257impl Drop for WriterActor {
1258 fn drop(&mut self) {
1259 if !self.shutdown.swap(true, Ordering::AcqRel) {
1260 drop(self.sender.try_send(WriterCommand::Shutdown));
1261 }
1262 if let Ok(mut handle) = self.handle.lock()
1263 && let Some(handle) = handle.take()
1264 {
1265 drop(handle.join());
1266 }
1267 }
1268}
1269
1270#[derive(Debug, Clone, Default)]
1271struct ShardMap {
1272 engines: HashMap<TenantId, Arc<ZanzibarEngine>>,
1273}
1274
1275fn default_writer_queue_capacity() -> NonZeroUsize {
1276 match NonZeroUsize::new(DEFAULT_WRITER_QUEUE_CAPACITY) {
1277 Some(value) => value,
1278 None => NonZeroUsize::MIN,
1279 }
1280}
1281
1282fn validate_tenant_id(value: &str) -> Result<(), TenantIdError> {
1283 if value.is_empty() {
1284 return Err(TenantIdError::Empty);
1285 }
1286 if value.len() > MAX_TENANT_ID_BYTES {
1287 return Err(TenantIdError::TooLong {
1288 max_bytes: MAX_TENANT_ID_BYTES,
1289 });
1290 }
1291 for (offset, byte) in value.bytes().enumerate() {
1292 if !(byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) {
1293 return Err(TenantIdError::InvalidByte { offset });
1294 }
1295 }
1296 Ok(())
1297}