1use std::sync::{Arc, Mutex, MutexGuard};
8use std::time::{Duration, SystemTime};
9
10use alopex_cluster::{
11 authorize_remote_read, LocalReadAuthorizationRecheck, ReadDelegationVerifier,
12 RemoteReadAuthorizationEnvelope, RequestId, VerifiedPeerIdentity,
13};
14use alopex_sql::distributed_read::{PreparedResult, PreparedResultStream};
15use dashmap::DashMap;
16use serde::Serialize;
17use uuid::Uuid;
18
19use crate::error::{Result, ServerError};
20use crate::session::{SessionId, SessionManager};
21
22pub type ReadExecutionId = RequestId;
29
30const READ_EXECUTION_SUMMARY_VERSION: u32 = 1;
31
32#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct ReadExecutionOwner {
40 profile: String,
41 session_id: Option<SessionId>,
42}
43
44impl ReadExecutionOwner {
45 pub fn new(profile: impl Into<String>, session_id: Option<SessionId>) -> Result<Self> {
46 let profile = profile.into();
47 if profile.trim().is_empty() {
48 return Err(ServerError::Unauthorized(
49 "distributed read requires an authenticated profile".into(),
50 ));
51 }
52 Ok(Self {
53 profile,
54 session_id,
55 })
56 }
57
58 fn permits(&self, profile: Option<&str>) -> bool {
59 profile == Some(self.profile.as_str())
60 }
61
62 pub fn session_id(&self) -> Option<&SessionId> {
63 self.session_id.as_ref()
64 }
65}
66
67#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct ReadExecutionPlanSummary {
70 pub requested_mode: String,
71 pub effective_mode: String,
72 pub metadata_version: u64,
73 pub ranges: Vec<String>,
74 pub freshness: String,
75 pub retry_count: u32,
76 pub failover_count: u32,
77}
78
79impl ReadExecutionPlanSummary {
80 pub fn validate(&self) -> Result<()> {
81 if self.requested_mode.trim().is_empty()
82 || self.effective_mode.trim().is_empty()
83 || self.freshness.trim().is_empty()
84 {
85 return Err(ServerError::BadRequest(
86 "distributed read summary requires requested/effective mode and freshness".into(),
87 ));
88 }
89 if self.ranges.is_empty() || self.ranges.iter().any(|range| range.trim().is_empty()) {
90 return Err(ServerError::BadRequest(
91 "distributed read summary requires at least one range".into(),
92 ));
93 }
94 Ok(())
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
100#[serde(rename_all = "snake_case")]
101pub enum ReadExecutionOutcome {
102 Preparing,
103 Success,
104 RetryableFailure,
105 TerminalFailure,
106 Cancelled,
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
111pub struct ReadExecutionSummary {
112 pub schema_version: u32,
113 pub execution_id: ReadExecutionId,
114 pub outcome: ReadExecutionOutcome,
115 pub requested_mode: String,
116 pub effective_mode: String,
117 pub metadata_version: u64,
118 pub ranges: Vec<String>,
119 pub freshness: String,
120 pub retry_count: u32,
121 pub failover_count: u32,
122 pub resource_outcome: String,
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub row_count: Option<usize>,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub reason: Option<String>,
127}
128
129impl ReadExecutionSummary {
130 fn preparing(execution_id: ReadExecutionId, plan: ReadExecutionPlanSummary) -> Self {
131 Self {
132 schema_version: READ_EXECUTION_SUMMARY_VERSION,
133 execution_id,
134 outcome: ReadExecutionOutcome::Preparing,
135 requested_mode: plan.requested_mode,
136 effective_mode: plan.effective_mode,
137 metadata_version: plan.metadata_version,
138 ranges: plan.ranges,
139 freshness: plan.freshness,
140 retry_count: plan.retry_count,
141 failover_count: plan.failover_count,
142 resource_outcome: "preparing".into(),
143 row_count: None,
144 reason: None,
145 }
146 }
147}
148
149#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
151pub struct ReadCancellation {
152 pub summary: ReadExecutionSummary,
153 pub peer_cleanup_deliveries: usize,
156 pub already_terminal: bool,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq)]
162pub enum ReadExecutionFailure {
163 Retryable(String),
164 Terminal(String),
165}
166
167pub type PeerCancellation = Arc<dyn Fn(&ReadExecutionId) + Send + Sync>;
171
172#[derive(Clone, Default)]
176pub struct DistributedReadRegistry {
177 entries: Arc<DashMap<ReadExecutionId, Arc<Mutex<ReadExecutionEntry>>>>,
178}
179
180struct ReadExecutionEntry {
181 owner: ReadExecutionOwner,
182 summary: ReadExecutionSummary,
183 prepared: Option<PreparedResult>,
184 peer_cancellations: Vec<PeerCancellation>,
185 updated_at: SystemTime,
186}
187
188pub struct PreparedReadLease {
191 registry: DistributedReadRegistry,
192 execution_id: ReadExecutionId,
193 entry: Arc<Mutex<ReadExecutionEntry>>,
194 stream: PreparedResultStream,
195 closed: bool,
196}
197
198impl PreparedReadLease {
199 pub fn columns(&self) -> Vec<alopex_sql::executor::ColumnInfo> {
200 let entry = lock_entry(&self.entry);
201 entry
202 .prepared
203 .as_ref()
204 .map(|result| result.columns().to_vec())
205 .unwrap_or_default()
206 }
207
208 pub fn preview_stream(&self) -> Option<PreparedResultStream> {
211 let entry = lock_entry(&self.entry);
212 (entry.summary.outcome == ReadExecutionOutcome::Success)
213 .then(|| {
214 entry
215 .prepared
216 .as_ref()
217 .map(PreparedResult::open_prepared_stream)
218 })
219 .flatten()
220 }
221
222 pub fn next_row(&mut self) -> Option<Vec<alopex_sql::storage::SqlValue>> {
226 if self.summary().outcome != ReadExecutionOutcome::Success {
227 return None;
228 }
229 self.stream.next_row()
230 }
231
232 pub fn summary(&self) -> ReadExecutionSummary {
233 lock_entry(&self.entry).summary.clone()
234 }
235
236 pub fn finish(mut self) {
240 self.registry
241 .finish_delivery(&self.execution_id, &self.entry);
242 self.closed = true;
243 }
244}
245
246impl Drop for PreparedReadLease {
247 fn drop(&mut self) {
248 if !self.closed {
249 let _ = self.registry.cancel_unchecked(&self.execution_id);
250 }
251 }
252}
253
254impl DistributedReadRegistry {
255 pub fn new() -> Self {
256 Self::default()
257 }
258
259 pub fn register_with_id(
263 &self,
264 execution_id: ReadExecutionId,
265 owner: ReadExecutionOwner,
266 plan: ReadExecutionPlanSummary,
267 peer_cancellations: Vec<PeerCancellation>,
268 ) -> Result<()> {
269 plan.validate()?;
270 if self.entries.contains_key(&execution_id) {
271 return Err(ServerError::Conflict(format!(
272 "distributed read execution '{}' is already registered",
273 execution_id.as_str()
274 )));
275 }
276 let entry = Arc::new(Mutex::new(ReadExecutionEntry {
277 owner,
278 summary: ReadExecutionSummary::preparing(execution_id.clone(), plan),
279 prepared: None,
280 peer_cancellations,
281 updated_at: SystemTime::now(),
282 }));
283 if self.entries.insert(execution_id.clone(), entry).is_some() {
284 return Err(ServerError::Conflict(format!(
285 "distributed read execution '{}' is already registered",
286 execution_id.as_str()
287 )));
288 }
289 Ok(())
290 }
291
292 pub fn register(
295 &self,
296 owner: ReadExecutionOwner,
297 plan: ReadExecutionPlanSummary,
298 peer_cancellations: Vec<PeerCancellation>,
299 ) -> Result<ReadExecutionId> {
300 let execution_id = RequestId::new(Uuid::new_v4().to_string());
301 self.register_with_id(execution_id.clone(), owner, plan, peer_cancellations)?;
302 Ok(execution_id)
303 }
304
305 pub fn publish_prepared(
308 &self,
309 execution_id: &ReadExecutionId,
310 prepared: PreparedResult,
311 ) -> Result<ReadExecutionSummary> {
312 let entry = self.entry(execution_id)?;
313 let mut entry = lock_entry(&entry);
314 match entry.summary.outcome {
315 ReadExecutionOutcome::Preparing => {
316 entry.summary.outcome = ReadExecutionOutcome::Success;
317 entry.summary.resource_outcome = "prepared".into();
318 entry.summary.row_count = Some(prepared.row_count());
319 entry.summary.reason = None;
320 entry.prepared = Some(prepared);
321 entry.updated_at = SystemTime::now();
322 Ok(entry.summary.clone())
323 }
324 ReadExecutionOutcome::Cancelled => Err(ServerError::Conflict(
325 "distributed read was cancelled before preparation completed".into(),
326 )),
327 _ => Err(ServerError::Conflict(
328 "distributed read no longer accepts a prepared result".into(),
329 )),
330 }
331 }
332
333 pub fn fail(
336 &self,
337 execution_id: &ReadExecutionId,
338 failure: ReadExecutionFailure,
339 ) -> Result<ReadExecutionSummary> {
340 let entry = self.entry(execution_id)?;
341 let (summary, callbacks) = {
342 let mut entry = lock_entry(&entry);
343 if !matches!(
344 entry.summary.outcome,
345 ReadExecutionOutcome::Preparing | ReadExecutionOutcome::Success
346 ) {
347 return Ok(entry.summary.clone());
348 }
349 let (outcome, reason) = match failure {
350 ReadExecutionFailure::Retryable(reason) => {
351 (ReadExecutionOutcome::RetryableFailure, reason)
352 }
353 ReadExecutionFailure::Terminal(reason) => {
354 (ReadExecutionOutcome::TerminalFailure, reason)
355 }
356 };
357 entry.summary.outcome = outcome;
358 entry.summary.resource_outcome = "released".into();
359 entry.summary.reason = Some(reason);
360 entry.prepared = None;
361 entry.updated_at = SystemTime::now();
362 let callbacks = std::mem::take(&mut entry.peer_cancellations);
363 (entry.summary.clone(), callbacks)
364 };
365 deliver_cancellations(execution_id, callbacks);
366 Ok(summary)
367 }
368
369 pub fn cancel(
370 &self,
371 execution_id: &ReadExecutionId,
372 requester_profile: Option<&str>,
373 ) -> Result<ReadCancellation> {
374 let entry = self.entry(execution_id)?;
375 {
376 let entry = lock_entry(&entry);
377 if !entry.owner.permits(requester_profile) {
378 return Err(ServerError::Unauthorized(
379 "distributed read belongs to a different authenticated profile".into(),
380 ));
381 }
382 }
383 Ok(self.cancel_entry(execution_id, &entry))
384 }
385
386 pub fn summary(
387 &self,
388 execution_id: &ReadExecutionId,
389 requester_profile: Option<&str>,
390 ) -> Result<ReadExecutionSummary> {
391 let entry = self.entry(execution_id)?;
392 let entry = lock_entry(&entry);
393 if !entry.owner.permits(requester_profile) {
394 return Err(ServerError::Unauthorized(
395 "distributed read belongs to a different authenticated profile".into(),
396 ));
397 }
398 Ok(entry.summary.clone())
399 }
400
401 pub fn open_prepared(
403 &self,
404 execution_id: &ReadExecutionId,
405 requester_profile: Option<&str>,
406 ) -> Result<PreparedReadLease> {
407 let entry = self.entry(execution_id)?;
408 let prepared = {
409 let entry = lock_entry(&entry);
410 if !entry.owner.permits(requester_profile) {
411 return Err(ServerError::Unauthorized(
412 "distributed read belongs to a different authenticated profile".into(),
413 ));
414 }
415 if entry.summary.outcome != ReadExecutionOutcome::Success {
416 return Err(ServerError::Conflict(format!(
417 "distributed read is {:?}, not prepared",
418 entry.summary.outcome
419 )));
420 }
421 entry.prepared.clone().ok_or_else(|| {
422 ServerError::Conflict("distributed read result has already been released".into())
423 })?
424 };
425 Ok(PreparedReadLease {
426 registry: self.clone(),
427 execution_id: execution_id.clone(),
428 entry,
429 stream: prepared.open_prepared_stream(),
430 closed: false,
431 })
432 }
433
434 pub fn cleanup_terminal_before(&self, cutoff: SystemTime) {
437 let removable = self
438 .entries
439 .iter()
440 .filter_map(|entry| {
441 let state = lock_entry(entry.value());
442 (state.updated_at <= cutoff && state.prepared.is_none())
443 .then(|| entry.key().clone())
444 })
445 .collect::<Vec<_>>();
446 for execution_id in removable {
447 self.entries.remove(&execution_id);
448 }
449 }
450
451 pub fn cleanup_after(&self, ttl: Duration) {
452 let cutoff = SystemTime::now()
453 .checked_sub(ttl)
454 .unwrap_or(SystemTime::UNIX_EPOCH);
455 self.cleanup_terminal_before(cutoff);
456 }
457
458 #[cfg(test)]
459 fn active_count(&self) -> usize {
460 self.entries.len()
461 }
462
463 fn entry(&self, execution_id: &ReadExecutionId) -> Result<Arc<Mutex<ReadExecutionEntry>>> {
464 self.entries
465 .get(execution_id)
466 .map(|entry| entry.value().clone())
467 .ok_or_else(|| {
468 ServerError::NotFound(format!(
469 "distributed read execution '{}' was not found",
470 execution_id.as_str()
471 ))
472 })
473 }
474
475 fn cancel_unchecked(&self, execution_id: &ReadExecutionId) -> Result<ReadCancellation> {
476 let entry = self.entry(execution_id)?;
477 Ok(self.cancel_entry(execution_id, &entry))
478 }
479
480 fn cancel_entry(
481 &self,
482 execution_id: &ReadExecutionId,
483 entry: &Arc<Mutex<ReadExecutionEntry>>,
484 ) -> ReadCancellation {
485 let (summary, callbacks, already_terminal) = {
486 let mut entry = lock_entry(entry);
487 if !matches!(
488 entry.summary.outcome,
489 ReadExecutionOutcome::Preparing | ReadExecutionOutcome::Success
490 ) || entry.prepared.is_none()
491 && entry.summary.outcome == ReadExecutionOutcome::Success
492 {
493 (entry.summary.clone(), Vec::new(), true)
494 } else {
495 entry.summary.outcome = ReadExecutionOutcome::Cancelled;
496 entry.summary.resource_outcome = "released".into();
497 entry.summary.reason = Some("cancelled".into());
498 entry.prepared = None;
499 entry.updated_at = SystemTime::now();
500 let callbacks = std::mem::take(&mut entry.peer_cancellations);
501 (entry.summary.clone(), callbacks, false)
502 }
503 };
504 let peer_cleanup_deliveries = callbacks.len();
505 deliver_cancellations(execution_id, callbacks);
506 ReadCancellation {
507 summary,
508 peer_cleanup_deliveries,
509 already_terminal,
510 }
511 }
512
513 fn finish_delivery(
514 &self,
515 _execution_id: &ReadExecutionId,
516 entry: &Arc<Mutex<ReadExecutionEntry>>,
517 ) {
518 let mut entry = lock_entry(entry);
519 if entry.summary.outcome == ReadExecutionOutcome::Success {
520 entry.prepared = None;
521 entry.peer_cancellations.clear();
522 entry.summary.resource_outcome = "released".into();
523 entry.updated_at = SystemTime::now();
524 }
525 }
526}
527
528fn lock_entry(entry: &Arc<Mutex<ReadExecutionEntry>>) -> MutexGuard<'_, ReadExecutionEntry> {
529 entry
530 .lock()
531 .unwrap_or_else(|poisoned| poisoned.into_inner())
532}
533
534fn deliver_cancellations(execution_id: &ReadExecutionId, callbacks: Vec<PeerCancellation>) {
535 for callback in callbacks {
536 callback(execution_id);
537 }
538}
539
540#[derive(Clone)]
542pub struct RemoteReadWorkerAuthorizer {
543 local_authorizer: Arc<dyn LocalReadAuthorizationRecheck>,
544}
545
546impl RemoteReadWorkerAuthorizer {
547 pub fn new(local_authorizer: Arc<dyn LocalReadAuthorizationRecheck>) -> Self {
549 Self { local_authorizer }
550 }
551
552 pub fn authorize(
555 &self,
556 peer: VerifiedPeerIdentity,
557 envelope: &RemoteReadAuthorizationEnvelope,
558 now_ms: u64,
559 verifier: &dyn ReadDelegationVerifier,
560 ) -> Result<()> {
561 authorize_remote_read(
562 peer,
563 envelope,
564 now_ms,
565 verifier,
566 self.local_authorizer.as_ref(),
567 )
568 .map_err(|error| {
569 ServerError::Unauthorized(format!("remote read delegation rejected: {error}"))
570 })
571 }
572
573 pub async fn begin_authorized_worker_session(
578 &self,
579 sessions: &SessionManager,
580 peer: VerifiedPeerIdentity,
581 envelope: &RemoteReadAuthorizationEnvelope,
582 now_ms: u64,
583 verifier: &dyn ReadDelegationVerifier,
584 ) -> Result<SessionId> {
585 self.authorize(peer, envelope, now_ms, verifier)?;
586 sessions
587 .create_authenticated_session(envelope.credential.subject.clone())
588 .await
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use std::collections::BTreeSet;
595 use std::sync::atomic::{AtomicUsize, Ordering};
596 use std::time::Duration;
597
598 use alopex_cluster::{
599 AuthenticatedSubject, ClusterId, LocalReadAuthorizationRequest, NodeId, RangeId,
600 ReadDelegationCredential, ReadOperationScope, RequestId,
601 };
602 use alopex_core::ReadAtPoint;
603 use alopex_sql::distributed_read::{
604 AssemblerRow, AssemblyPlan, DistributedReadBudget, GlobalResultAssembler,
605 RangeAssemblerInput, RangeAssemblerPayload, RangeTerminal, ResultPresentation,
606 RowMergePlan,
607 };
608 use alopex_sql::executor::ColumnInfo;
609 use alopex_sql::planner::ResolvedType;
610 use alopex_sql::storage::SqlValue;
611 use sha2::{Digest, Sha256};
612
613 use super::*;
614 use crate::auth::{LocalReadAuthorizationPolicy, ServerLocalReadAuthorizationRecheck};
615 use crate::session::{SessionConfig, TransactionFactory};
616
617 struct DigestVerifier;
618
619 impl ReadDelegationVerifier for DigestVerifier {
620 fn verify(&self, _key_id: &str, payload: &[u8], signature: &[u8]) -> bool {
621 signature == Sha256::digest(payload).as_slice()
622 }
623 }
624
625 struct TableSevenPolicy;
626
627 impl LocalReadAuthorizationPolicy for TableSevenPolicy {
628 fn authorize_local_read(
629 &self,
630 request: &LocalReadAuthorizationRequest,
631 ) -> std::result::Result<(), String> {
632 (request.subject.as_str() == "user-a"
633 && request.table_id == 7
634 && request.range_id == RangeId::new("range-a"))
635 .then_some(())
636 .ok_or_else(|| "not permitted by local table/range policy".into())
637 }
638 }
639
640 fn envelope() -> RemoteReadAuthorizationEnvelope {
641 let mut credential = ReadDelegationCredential {
642 issuer: NodeId::new("gateway-a"),
643 cluster_id: ClusterId::new("cluster-a"),
644 subject: AuthenticatedSubject::new("user-a"),
645 operation: ReadOperationScope::Select,
646 table_id: 7,
647 allowed_ranges: BTreeSet::from([RangeId::new("range-a")]),
648 query_digest: "query-a".into(),
649 request_id: RequestId::new("request-a"),
650 read_fence_digest: "fence-a".into(),
651 audience: NodeId::new("gateway-a"),
652 read_at: ReadAtPoint::new(4, 3, 2, 1),
653 issued_at_ms: 10,
654 expires_at_ms: 20,
655 key_id: "test-key".into(),
656 signature: Vec::new(),
657 };
658 credential.signature = Sha256::digest(credential.signed_payload().unwrap()).to_vec();
659 RemoteReadAuthorizationEnvelope {
660 range_id: RangeId::new("range-a"),
661 table_id: 7,
662 operation: ReadOperationScope::Select,
663 request_id: RequestId::new("request-a"),
664 query_digest: "query-a".into(),
665 read_fence_digest: "fence-a".into(),
666 read_at: ReadAtPoint::new(4, 3, 2, 1),
667 credential,
668 }
669 }
670
671 fn authorizer() -> RemoteReadWorkerAuthorizer {
672 RemoteReadWorkerAuthorizer::new(Arc::new(ServerLocalReadAuthorizationRecheck::new(
673 Arc::new(TableSevenPolicy),
674 )))
675 }
676
677 fn peer() -> VerifiedPeerIdentity {
678 VerifiedPeerIdentity::new("gateway-a", "cluster-a")
679 }
680
681 fn session_manager() -> SessionManager {
682 let factory: TransactionFactory = Arc::new(|| {
683 Box::pin(async {
684 Err(ServerError::Internal(
685 "test factory must not create a transaction".into(),
686 ))
687 })
688 });
689 SessionManager::new(
690 SessionConfig {
691 ttl: Duration::from_secs(60),
692 },
693 factory,
694 )
695 }
696
697 fn execution_plan() -> ReadExecutionPlanSummary {
698 ReadExecutionPlanSummary {
699 requested_mode: "strong".into(),
700 effective_mode: "strong".into(),
701 metadata_version: 41,
702 ranges: vec!["range-a".into()],
703 freshness: "current_committed_prefix".into(),
704 retry_count: 0,
705 failover_count: 0,
706 }
707 }
708
709 fn execution_owner() -> ReadExecutionOwner {
710 ReadExecutionOwner::new("profile-a", None).unwrap()
711 }
712
713 fn prepared_result() -> PreparedResult {
714 let columns = vec![ColumnInfo::new("name", ResolvedType::Text)];
715 let plan = AssemblyPlan::Rows(RowMergePlan {
716 presentation: ResultPresentation {
717 columns: columns.clone(),
718 distinct: false,
719 order: Vec::new(),
720 final_order_key_indexes: Vec::new(),
721 offset: 0,
722 limit: None,
723 },
724 });
725 let mut assembler = GlobalResultAssembler::new(
726 vec!["range-a".into()],
727 plan,
728 DistributedReadBudget::default(),
729 )
730 .unwrap();
731 assembler
732 .push_range(RangeAssemblerInput {
733 range_id: "range-a".into(),
734 columns,
735 payloads: vec![RangeAssemblerPayload::Rows(vec![AssemblerRow {
736 values: vec![SqlValue::Text("prepared".into())],
737 order_keys: Vec::new(),
738 row_key: 1,
739 }])],
740 terminal: RangeTerminal::Completed {
741 cleanup_acknowledged: true,
742 },
743 })
744 .unwrap();
745 assembler.prepare().unwrap()
746 }
747
748 #[tokio::test]
749 async fn worker_injection_creates_a_session_bound_to_the_locally_allowed_subject() {
750 let sessions = session_manager();
751 let session = authorizer()
752 .begin_authorized_worker_session(&sessions, peer(), &envelope(), 15, &DigestVerifier)
753 .await
754 .unwrap();
755 assert_eq!(
756 sessions
757 .authenticated_subject(&session)
758 .await
759 .unwrap()
760 .as_str(),
761 "user-a"
762 );
763 }
764
765 #[tokio::test]
766 async fn scope_tamper_is_rejected_without_creating_a_worker_session() {
767 let sessions = session_manager();
768 let mut tampered = envelope();
769 tampered.range_id = RangeId::new("range-b");
770 let result = authorizer()
771 .begin_authorized_worker_session(&sessions, peer(), &tampered, 15, &DigestVerifier)
772 .await;
773 assert!(matches!(result, Err(ServerError::Unauthorized(_))));
774 assert_eq!(sessions.active_session_count(), 0);
775 }
776
777 #[tokio::test]
778 async fn valid_transport_cannot_broaden_a_locally_denied_table_scope() {
779 let sessions = session_manager();
780 let mut denied = envelope();
781 denied.credential.table_id = 8;
782 denied.table_id = 8;
783 denied.credential.signature =
784 Sha256::digest(denied.credential.signed_payload().unwrap()).to_vec();
785 let result = authorizer()
786 .begin_authorized_worker_session(&sessions, peer(), &denied, 15, &DigestVerifier)
787 .await;
788 assert!(matches!(result, Err(ServerError::Unauthorized(_))));
789 assert_eq!(sessions.active_session_count(), 0);
790 }
791
792 #[test]
793 fn cancellation_is_owner_bound_idempotent_and_delivered_once_outside_registry_state() {
794 let registry = DistributedReadRegistry::new();
795 let execution_id = RequestId::new("read-cancel");
796 let deliveries = Arc::new(AtomicUsize::new(0));
797 let observed_id = Arc::new(std::sync::Mutex::new(None));
798 let callback_deliveries = deliveries.clone();
799 let callback_id = observed_id.clone();
800 registry
801 .register_with_id(
802 execution_id.clone(),
803 execution_owner(),
804 execution_plan(),
805 vec![Arc::new(move |id| {
806 callback_deliveries.fetch_add(1, Ordering::SeqCst);
807 *callback_id.lock().unwrap() = Some(id.clone());
808 })],
809 )
810 .unwrap();
811
812 assert!(matches!(
813 registry.cancel(&execution_id, Some("profile-b")),
814 Err(ServerError::Unauthorized(_))
815 ));
816 assert_eq!(deliveries.load(Ordering::SeqCst), 0);
817
818 let cancelled = registry.cancel(&execution_id, Some("profile-a")).unwrap();
819 assert_eq!(cancelled.summary.outcome, ReadExecutionOutcome::Cancelled);
820 assert_eq!(cancelled.peer_cleanup_deliveries, 1);
821 assert!(!cancelled.already_terminal);
822 assert_eq!(deliveries.load(Ordering::SeqCst), 1);
823 assert_eq!(observed_id.lock().unwrap().as_ref(), Some(&execution_id));
824
825 let repeated = registry.cancel(&execution_id, Some("profile-a")).unwrap();
826 assert_eq!(repeated.summary.outcome, ReadExecutionOutcome::Cancelled);
827 assert_eq!(repeated.peer_cleanup_deliveries, 0);
828 assert!(repeated.already_terminal);
829 assert_eq!(deliveries.load(Ordering::SeqCst), 1);
830 assert!(matches!(
831 registry.open_prepared(&execution_id, Some("profile-a")),
832 Err(ServerError::Conflict(_))
833 ));
834 }
835
836 #[test]
837 fn only_prepared_results_open_and_disconnect_uses_the_same_peer_cleanup_once() {
838 let registry = DistributedReadRegistry::new();
839 let execution_id = RequestId::new("read-prepared");
840 let deliveries = Arc::new(AtomicUsize::new(0));
841 let callback_deliveries = deliveries.clone();
842 registry
843 .register_with_id(
844 execution_id.clone(),
845 execution_owner(),
846 execution_plan(),
847 vec![Arc::new(move |_| {
848 callback_deliveries.fetch_add(1, Ordering::SeqCst);
849 })],
850 )
851 .unwrap();
852
853 assert!(matches!(
854 registry.open_prepared(&execution_id, Some("profile-a")),
855 Err(ServerError::Conflict(_))
856 ));
857 let summary = registry
858 .publish_prepared(&execution_id, prepared_result())
859 .unwrap();
860 assert_eq!(summary.outcome, ReadExecutionOutcome::Success);
861 assert_eq!(summary.row_count, Some(1));
862
863 let mut lease = registry
864 .open_prepared(&execution_id, Some("profile-a"))
865 .unwrap();
866 assert_eq!(
867 lease.next_row(),
868 Some(vec![SqlValue::Text("prepared".into())])
869 );
870 drop(lease);
872
873 assert_eq!(deliveries.load(Ordering::SeqCst), 1);
874 assert_eq!(
875 registry
876 .summary(&execution_id, Some("profile-a"))
877 .unwrap()
878 .outcome,
879 ReadExecutionOutcome::Cancelled
880 );
881 registry.cleanup_terminal_before(SystemTime::now());
882 assert_eq!(registry.active_count(), 0);
883 }
884}