1use std::collections::{BTreeMap, HashMap};
29
30use serde::{Deserialize, Serialize};
31
32use crate::{
33 proto::command::HealthCheckConfig,
34 secret::{
35 ProtectedHeaderChange, ProtectedHeaderJournal, ProtectedHeaderSet, ProtectedHeaderVersion,
36 Resolution, VersionExhausted,
37 },
38};
39
40pub type WorkerId = u32;
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45pub enum Phase {
46 Preparing,
48 Committing,
50 Aborting,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Order {
57 Commit,
58 Abort,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum Outcome {
64 Committed,
66 Aborted,
68 Incoherent(Vec<WorkerId>),
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum Step {
76 Wait,
78 Send(Order, Vec<WorkerId>),
80 Done(Outcome),
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
86pub enum BeginError {
87 #[error("a protected health check change needs at least one worker")]
88 NoWorker,
89 #[error(transparent)]
90 Exhausted(#[from] VersionExhausted),
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96pub struct Transaction {
97 cluster_id: String,
98 version: ProtectedHeaderVersion,
99 phase: Phase,
100 workers: BTreeMap<WorkerId, bool>,
102 unrecoverable: Vec<WorkerId>,
104 finished: bool,
105}
106
107impl Transaction {
108 pub fn begin(
111 journal: &mut ProtectedHeaderJournal,
112 cluster_id: &str,
113 change: ProtectedHeaderChange,
114 workers: impl IntoIterator<Item = WorkerId>,
115 ) -> Result<Self, BeginError> {
116 let workers: BTreeMap<WorkerId, bool> = workers.into_iter().map(|w| (w, false)).collect();
117 if workers.is_empty() {
118 return Err(BeginError::NoWorker);
119 }
120 let version = journal.propose(cluster_id, change)?;
121 Ok(Self {
122 cluster_id: cluster_id.to_owned(),
123 version,
124 phase: Phase::Preparing,
125 workers,
126 unrecoverable: Vec::new(),
127 finished: false,
128 })
129 }
130
131 pub fn cluster_id(&self) -> &str {
132 &self.cluster_id
133 }
134
135 pub fn version(&self) -> ProtectedHeaderVersion {
136 self.version
137 }
138
139 pub fn phase(&self) -> Phase {
140 self.phase
141 }
142
143 pub fn is_finished(&self) -> bool {
144 self.finished
145 }
146
147 pub fn workers(&self) -> Vec<WorkerId> {
149 self.workers.keys().copied().collect()
150 }
151
152 pub fn prepared(&mut self, worker: WorkerId, version: ProtectedHeaderVersion) -> Step {
154 if !self.acknowledge(Phase::Preparing, worker, version) {
155 return Step::Wait;
156 }
157 if self.all_acknowledged() {
158 self.enter(Phase::Committing);
159 return Step::Send(Order::Commit, self.workers());
160 }
161 Step::Wait
162 }
163
164 pub fn committed(
166 &mut self,
167 journal: &mut ProtectedHeaderJournal,
168 worker: WorkerId,
169 version: ProtectedHeaderVersion,
170 ) -> Step {
171 if !self.acknowledge(Phase::Committing, worker, version) {
172 return Step::Wait;
173 }
174 if self.all_acknowledged() {
175 self.finished = true;
176 let _ = journal.accept(&self.cluster_id, self.version);
179 return Step::Done(Outcome::Committed);
180 }
181 Step::Wait
182 }
183
184 pub fn failed(
188 &mut self,
189 journal: &mut ProtectedHeaderJournal,
190 worker: WorkerId,
191 version: ProtectedHeaderVersion,
192 ) -> Step {
193 if self.finished || version != self.version || !self.workers.contains_key(&worker) {
194 return Step::Wait;
195 }
196 match self.phase {
197 Phase::Preparing | Phase::Committing => self.begin_abort(),
198 Phase::Aborting => {
199 if !self.unrecoverable.contains(&worker) {
200 self.unrecoverable.push(worker);
201 }
202 self.workers.insert(worker, true);
203 self.finish_abort(journal)
204 }
205 }
206 }
207
208 pub fn aborted(
210 &mut self,
211 journal: &mut ProtectedHeaderJournal,
212 worker: WorkerId,
213 version: ProtectedHeaderVersion,
214 ) -> Step {
215 if !self.acknowledge(Phase::Aborting, worker, version) {
216 return Step::Wait;
217 }
218 self.finish_abort(journal)
219 }
220
221 pub fn timed_out(&mut self, journal: &mut ProtectedHeaderJournal) -> Step {
224 if self.finished {
225 return Step::Wait;
226 }
227 match self.phase {
228 Phase::Preparing | Phase::Committing => self.begin_abort(),
229 Phase::Aborting => {
230 let silent: Vec<WorkerId> = self
231 .workers
232 .iter()
233 .filter(|(_, acknowledged)| !**acknowledged)
234 .map(|(worker, _)| *worker)
235 .collect();
236 for worker in silent {
237 if !self.unrecoverable.contains(&worker) {
238 self.unrecoverable.push(worker);
239 }
240 self.workers.insert(worker, true);
241 }
242 self.finish_abort(journal)
243 }
244 }
245 }
246
247 fn acknowledge(
250 &mut self,
251 phase: Phase,
252 worker: WorkerId,
253 version: ProtectedHeaderVersion,
254 ) -> bool {
255 if self.finished || self.phase != phase || version != self.version {
256 return false;
257 }
258 match self.workers.get_mut(&worker) {
259 Some(acknowledged) if !*acknowledged => {
260 *acknowledged = true;
261 true
262 }
263 _ => false,
264 }
265 }
266
267 fn all_acknowledged(&self) -> bool {
268 self.workers.values().all(|acknowledged| *acknowledged)
269 }
270
271 fn enter(&mut self, phase: Phase) {
272 self.phase = phase;
273 for acknowledged in self.workers.values_mut() {
274 *acknowledged = false;
275 }
276 }
277
278 fn begin_abort(&mut self) -> Step {
281 self.enter(Phase::Aborting);
282 Step::Send(Order::Abort, self.workers())
283 }
284
285 fn finish_abort(&mut self, journal: &mut ProtectedHeaderJournal) -> Step {
286 if !self.all_acknowledged() {
287 return Step::Wait;
288 }
289 self.finished = true;
290 journal.reject(&self.cluster_id, self.version);
291 if self.unrecoverable.is_empty() {
292 Step::Done(Outcome::Aborted)
293 } else {
294 Step::Done(Outcome::Incoherent(self.unrecoverable.to_owned()))
295 }
296 }
297}
298
299#[derive(Clone)]
301pub struct Payload {
302 pub config: HealthCheckConfig,
303 pub headers: ProtectedHeaderSet,
304}
305
306#[derive(Clone, Default)]
308struct Held {
309 version: Option<ProtectedHeaderVersion>,
310 payload: Option<Payload>,
311}
312
313#[derive(Default)]
314struct ClusterVersions {
315 applied: Held,
316 previous: Held,
318 staged: BTreeMap<ProtectedHeaderVersion, Option<Payload>>,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
323pub enum StageError {
324 #[error("protected health check version is older than the one already applied")]
325 Stale,
326}
327
328#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum Commit {
331 Applied,
333 Superseded,
335 Unknown,
337}
338
339#[derive(Default)]
342pub struct WorkerStore {
343 clusters: HashMap<String, ClusterVersions>,
344}
345
346impl WorkerStore {
347 pub fn prepare(
351 &mut self,
352 cluster: &str,
353 version: ProtectedHeaderVersion,
354 payload: Option<Payload>,
355 ) -> Result<(), StageError> {
356 let versions = self.clusters.entry(cluster.to_owned()).or_default();
357 if versions
358 .applied
359 .version
360 .is_some_and(|applied| version <= applied)
361 {
362 return Err(StageError::Stale);
363 }
364 versions.staged.entry(version).or_insert(payload);
365 Ok(())
366 }
367
368 pub fn commit(&mut self, cluster: &str, version: ProtectedHeaderVersion) -> Commit {
372 let Some(versions) = self.clusters.get_mut(cluster) else {
373 return Commit::Unknown;
374 };
375 let Some(payload) = versions.staged.remove(&version) else {
376 return Commit::Unknown;
377 };
378 let held = Held {
379 version: Some(version),
380 payload,
381 };
382 if versions
383 .applied
384 .version
385 .is_none_or(|applied| version > applied)
386 {
387 versions.previous = std::mem::replace(&mut versions.applied, held);
388 Commit::Applied
389 } else {
390 if versions.previous.version.is_none_or(|kept| version > kept) {
391 versions.previous = held;
392 }
393 Commit::Superseded
394 }
395 }
396
397 pub fn abort(&mut self, cluster: &str, version: ProtectedHeaderVersion) -> Resolution {
400 let Some(versions) = self.clusters.get_mut(cluster) else {
401 return Resolution::Ignored;
402 };
403 let mut changed = versions.staged.remove(&version).is_some();
404 if versions.applied.version == Some(version) {
405 versions.applied = std::mem::take(&mut versions.previous);
406 changed = true;
407 }
408 self.forget_if_empty(cluster);
409 if changed {
410 Resolution::Applied
411 } else {
412 Resolution::Ignored
413 }
414 }
415
416 pub fn release(&mut self, cluster: &str, version: ProtectedHeaderVersion) {
419 if let Some(versions) = self.clusters.get_mut(cluster)
420 && versions.applied.version == Some(version)
421 {
422 versions.previous = Held::default();
423 }
424 self.forget_if_empty(cluster);
425 }
426
427 pub fn effective(&self, cluster: &str) -> Option<&Payload> {
429 self.clusters.get(cluster)?.applied.payload.as_ref()
430 }
431
432 pub fn remove_cluster(&mut self, cluster: &str) {
434 self.clusters.remove(cluster);
435 }
436
437 fn forget_if_empty(&mut self, cluster: &str) {
438 if self.clusters.get(cluster).is_some_and(|v| {
439 v.applied.version.is_none() && v.previous.version.is_none() && v.staged.is_empty()
440 }) {
441 self.clusters.remove(cluster);
442 }
443 }
444}
445
446impl std::fmt::Debug for WorkerStore {
447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 f.debug_struct("WorkerStore")
449 .field("clusters", &self.clusters.len())
450 .field(
451 "staged",
452 &self
453 .clusters
454 .values()
455 .map(|v| v.staged.len())
456 .sum::<usize>(),
457 )
458 .finish()
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use zeroize::Zeroizing;
465
466 use super::*;
467 use crate::{
468 proto::command::HealthCheckHeaderValue,
469 secret::{FingerprintKey, ProtectedHeader, SecretValue},
470 };
471
472 const SENTINEL: &str = "SYNTHETIC-TXN-SECRET-77aa";
473
474 fn set(value: &str) -> ProtectedHeaderSet {
475 let key = FingerprintKey::from_parts(Zeroizing::new([5; 32]), [6; 16]);
476 let mut carrier = HealthCheckHeaderValue {
477 name: "authorization".to_owned(),
478 value: value.as_bytes().to_vec(),
479 };
480 let secret = SecretValue::take_from_ingress(&mut carrier).unwrap();
481 ProtectedHeaderSet::new(vec![ProtectedHeader::new(
482 &key,
483 "authorization".to_owned(),
484 secret,
485 )])
486 }
487
488 fn replace(value: &str) -> ProtectedHeaderChange {
489 ProtectedHeaderChange::Replace(set(value))
490 }
491
492 fn payload(value: &str) -> Payload {
493 Payload {
494 config: HealthCheckConfig::default(),
495 headers: set(value),
496 }
497 }
498
499 fn held_ptr(journal: &ProtectedHeaderJournal, cluster: &str) -> Option<Vec<u8>> {
500 journal
501 .effective(cluster)
502 .and_then(|s| s.headers().first())
503 .map(|h| h.value().expose_secret_bytes().to_vec())
504 }
505
506 fn commit_all(txn: &mut Transaction, journal: &mut ProtectedHeaderJournal) -> Step {
508 let version = txn.version();
509 let mut last = Step::Wait;
510 for worker in txn.workers() {
511 txn.prepared(worker, version);
512 }
513 for worker in txn.workers() {
514 last = txn.committed(journal, worker, version);
515 }
516 last
517 }
518
519 #[test]
520 fn begin_without_worker_touches_nothing() {
521 let mut journal = ProtectedHeaderJournal::default();
522 let error = Transaction::begin(&mut journal, "app", replace("a"), []).unwrap_err();
523 assert_eq!(error, BeginError::NoWorker);
524 assert!(journal.effective("app").is_none());
525 let txn = Transaction::begin(&mut journal, "app", replace("a"), [1]).unwrap();
527 assert_eq!(txn.phase(), Phase::Preparing);
528 assert!(journal.effective("app").is_some());
529 }
530
531 #[test]
532 fn all_workers_commit_confirms_the_journal() {
533 let mut journal = ProtectedHeaderJournal::default();
534 let mut txn = Transaction::begin(&mut journal, "app", replace("a"), [1, 2]).unwrap();
535 let version = txn.version();
536
537 assert_eq!(txn.prepared(1, version), Step::Wait);
538 assert_eq!(
539 txn.prepared(2, version),
540 Step::Send(Order::Commit, vec![1, 2])
541 );
542 assert_eq!(txn.phase(), Phase::Committing);
543 assert_eq!(txn.committed(&mut journal, 1, version), Step::Wait);
544 assert!(!txn.is_finished());
546 assert_eq!(
547 txn.committed(&mut journal, 2, version),
548 Step::Done(Outcome::Committed)
549 );
550 assert!(txn.is_finished());
551 assert_eq!(journal.reject("app", version), Resolution::Ignored);
553 assert!(journal.effective("app").is_some());
554 }
555
556 #[test]
557 fn zero_acknowledgement_failure_aborts_everyone_and_drops_the_proposal() {
558 let mut journal = ProtectedHeaderJournal::default();
559 let mut txn = Transaction::begin(&mut journal, "app", replace("a"), [1, 2]).unwrap();
560 let version = txn.version();
561
562 assert_eq!(
563 txn.failed(&mut journal, 1, version),
564 Step::Send(Order::Abort, vec![1, 2])
565 );
566 assert_eq!(txn.aborted(&mut journal, 1, version), Step::Wait);
567 assert_eq!(
568 txn.aborted(&mut journal, 2, version),
569 Step::Done(Outcome::Aborted)
570 );
571 assert!(journal.effective("app").is_none(), "no value is kept");
572 }
573
574 #[test]
575 fn one_success_one_failure_never_commits() {
576 let mut journal = ProtectedHeaderJournal::default();
577 let mut txn = Transaction::begin(&mut journal, "app", replace("a"), [1, 2]).unwrap();
578 let version = txn.version();
579 assert_eq!(txn.prepared(1, version), Step::Wait);
580 assert_eq!(
581 txn.failed(&mut journal, 2, version),
582 Step::Send(Order::Abort, vec![1, 2])
583 );
584 assert_eq!(txn.prepared(2, version), Step::Wait);
586 assert_eq!(txn.committed(&mut journal, 1, version), Step::Wait);
587 txn.aborted(&mut journal, 1, version);
588 assert_eq!(
589 txn.aborted(&mut journal, 2, version),
590 Step::Done(Outcome::Aborted)
591 );
592 assert!(journal.effective("app").is_none());
593 }
594
595 #[test]
596 fn failure_after_a_partial_commit_aborts_and_restores() {
597 let mut journal = ProtectedHeaderJournal::default();
598 let mut txn = Transaction::begin(&mut journal, "app", replace("a"), [1, 2]).unwrap();
599 let version = txn.version();
600 txn.prepared(1, version);
601 txn.prepared(2, version);
602 assert_eq!(txn.committed(&mut journal, 1, version), Step::Wait);
603 assert_eq!(
605 txn.timed_out(&mut journal),
606 Step::Send(Order::Abort, vec![1, 2])
607 );
608 assert_eq!(txn.phase(), Phase::Aborting);
609 assert_eq!(txn.committed(&mut journal, 2, version), Step::Wait);
611 txn.aborted(&mut journal, 1, version);
612 assert_eq!(
613 txn.aborted(&mut journal, 2, version),
614 Step::Done(Outcome::Aborted)
615 );
616 assert!(journal.effective("app").is_none());
617 }
618
619 #[test]
620 fn a_failed_abort_is_reported_not_hidden() {
621 let mut journal = ProtectedHeaderJournal::default();
622 let mut txn = Transaction::begin(&mut journal, "app", replace("a"), [1, 2, 3]).unwrap();
623 let version = txn.version();
624 txn.failed(&mut journal, 1, version);
625 assert_eq!(txn.aborted(&mut journal, 1, version), Step::Wait);
626 assert_eq!(txn.failed(&mut journal, 2, version), Step::Wait);
628 assert_eq!(
629 txn.timed_out(&mut journal),
630 Step::Done(Outcome::Incoherent(vec![2, 3]))
631 );
632 assert!(journal.effective("app").is_none(), "main never commits");
633 }
634
635 #[test]
636 fn stale_foreign_and_duplicate_acknowledgements_change_nothing() {
637 let mut journal = ProtectedHeaderJournal::default();
638 let mut first = Transaction::begin(&mut journal, "app", replace("a"), [1]).unwrap();
639 let old = first.version();
640 assert!(matches!(
641 commit_all(&mut first, &mut journal),
642 Step::Done(_)
643 ));
644
645 let mut second = Transaction::begin(&mut journal, "app", replace("b"), [1, 2]).unwrap();
646 let version = second.version();
647 let before = second.clone();
648 assert_eq!(second.prepared(1, old), Step::Wait, "older version");
649 assert_eq!(second.prepared(9, version), Step::Wait, "foreign worker");
650 assert_eq!(second.committed(&mut journal, 1, version), Step::Wait);
651 assert_eq!(second.aborted(&mut journal, 1, version), Step::Wait);
652 assert_eq!(second.failed(&mut journal, 1, old), Step::Wait);
653 assert_eq!(second, before);
654 assert_eq!(second.prepared(1, version), Step::Wait);
655 assert_eq!(second.prepared(1, version), Step::Wait, "duplicate");
656 assert_eq!(
657 second.prepared(2, version),
658 Step::Send(Order::Commit, vec![1, 2])
659 );
660 let done = first.clone();
662 assert_eq!(first.failed(&mut journal, 1, old), Step::Wait);
663 assert_eq!(first.timed_out(&mut journal), Step::Wait);
664 assert_eq!(first, done);
665 assert!(journal.effective("app").is_some());
666 }
667
668 #[test]
669 fn a_then_b_completes_in_either_order_and_the_newest_wins() {
670 for a_first in [true, false] {
671 let mut journal = ProtectedHeaderJournal::default();
672 let mut a = Transaction::begin(&mut journal, "app", replace("a"), [1]).unwrap();
673 let mut b = Transaction::begin(&mut journal, "app", replace("b"), [1]).unwrap();
674 let b_ptr = held_ptr(&journal, "app");
675 if a_first {
676 commit_all(&mut a, &mut journal);
677 commit_all(&mut b, &mut journal);
678 } else {
679 commit_all(&mut b, &mut journal);
680 commit_all(&mut a, &mut journal);
681 }
682 assert!(a.is_finished() && b.is_finished());
683 assert_eq!(held_ptr(&journal, "app"), b_ptr, "B stays effective");
684 assert_eq!(journal.reject("app", a.version()), Resolution::Ignored);
686 assert_eq!(journal.reject("app", b.version()), Resolution::Ignored);
687 }
688 }
689
690 #[test]
691 fn an_aborted_ancestor_leaves_a_live_descendant_alone() {
692 let mut journal = ProtectedHeaderJournal::default();
693 let mut a = Transaction::begin(&mut journal, "app", replace("a"), [1]).unwrap();
694 let mut b = Transaction::begin(&mut journal, "app", replace("b"), [1]).unwrap();
695 let b_ptr = held_ptr(&journal, "app");
696 let version = a.version();
697 a.failed(&mut journal, 1, version);
698 assert_eq!(
699 a.aborted(&mut journal, 1, version),
700 Step::Done(Outcome::Aborted)
701 );
702 assert_eq!(held_ptr(&journal, "app"), b_ptr);
703 assert!(matches!(
704 commit_all(&mut b, &mut journal),
705 Step::Done(Outcome::Committed)
706 ));
707 }
708
709 #[test]
710 fn a_persisted_transaction_carries_no_value() {
711 let mut journal = ProtectedHeaderJournal::default();
712 let txn = Transaction::begin(&mut journal, "app", replace(SENTINEL), [1, 2]).unwrap();
713 let json = serde_json::to_string(&txn).unwrap();
714 assert!(!json.contains(SENTINEL), "{json}");
715 assert!(!json.contains("authorization"), "{json}");
716 assert!(!format!("{txn:?}").contains(SENTINEL));
717 assert!(!format!("{journal:?}").contains(SENTINEL));
718 let back: Transaction = serde_json::from_str(&json).unwrap();
719 assert_eq!(back, txn);
720 }
721
722 fn versions(count: usize) -> Vec<ProtectedHeaderVersion> {
723 let mut journal = ProtectedHeaderJournal::default();
724 (0..count)
725 .map(|_| journal.propose("v", ProtectedHeaderChange::Clear).unwrap())
726 .collect()
727 }
728
729 fn ptr(store: &WorkerStore, cluster: &str) -> Option<Vec<u8>> {
730 store
731 .effective(cluster)
732 .and_then(|p| p.headers.headers().first())
733 .map(|h| h.value().expose_secret_bytes().to_vec())
734 }
735
736 #[test]
737 fn worker_stages_without_applying_and_commit_applies() {
738 let v = versions(1)[0];
739 let mut store = WorkerStore::default();
740 store.prepare("app", v, Some(payload("a"))).unwrap();
741 assert!(store.effective("app").is_none(), "staged is not applied");
742 assert_eq!(store.commit("app", v), Commit::Applied);
743 assert!(store.effective("app").is_some());
744 assert_eq!(store.commit("app", v), Commit::Unknown, "already resolved");
745 }
746
747 #[test]
748 fn worker_abort_drops_staged_and_restores_the_applied_predecessor() {
749 let v = versions(2);
750 let mut store = WorkerStore::default();
751 store.prepare("app", v[0], Some(payload("a"))).unwrap();
752 store.commit("app", v[0]);
753 let before = ptr(&store, "app");
754
755 store.prepare("app", v[1], Some(payload("b"))).unwrap();
756 assert_eq!(store.abort("app", v[1]), Resolution::Applied);
757 assert_eq!(ptr(&store, "app"), before);
758
759 store.prepare("app", v[1], Some(payload("b"))).unwrap();
760 store.commit("app", v[1]);
761 assert_ne!(ptr(&store, "app"), before);
762 assert_eq!(store.abort("app", v[1]), Resolution::Applied);
764 assert_eq!(ptr(&store, "app"), before);
765 assert_eq!(store.abort("app", v[1]), Resolution::Ignored, "repeat");
766 }
767
768 #[test]
769 fn worker_never_lets_an_older_version_overwrite_a_newer_one() {
770 let v = versions(2);
771 let mut store = WorkerStore::default();
772 store.prepare("app", v[0], Some(payload("a"))).unwrap();
773 store.prepare("app", v[1], Some(payload("b"))).unwrap();
774 assert_eq!(store.commit("app", v[1]), Commit::Applied);
775 let newest = ptr(&store, "app");
776 assert_eq!(store.commit("app", v[0]), Commit::Superseded);
777 assert_eq!(ptr(&store, "app"), newest);
778 assert_eq!(
779 store.prepare("app", v[0], Some(payload("a"))),
780 Err(StageError::Stale)
781 );
782 assert_eq!(store.abort("app", v[1]), Resolution::Applied);
784 assert!(store.effective("app").is_some(), "A is restored, not lost");
785 assert_ne!(ptr(&store, "app"), newest);
786 }
787
788 #[test]
789 fn worker_release_forgets_the_predecessor() {
790 let v = versions(2);
791 let mut store = WorkerStore::default();
792 store.prepare("app", v[0], Some(payload("a"))).unwrap();
793 store.commit("app", v[0]);
794 store.prepare("app", v[1], Some(payload("b"))).unwrap();
795 store.commit("app", v[1]);
796 let newest = ptr(&store, "app");
797 store.release("app", v[1]);
798 assert_eq!(store.abort("app", v[0]), Resolution::Ignored);
800 assert_eq!(ptr(&store, "app"), newest);
801 assert_eq!(store.abort("app", v[1]), Resolution::Applied);
804 assert!(store.effective("app").is_none());
805 }
806
807 #[test]
808 fn worker_clear_and_removal_erase_future_values() {
809 let v = versions(3);
810 let mut store = WorkerStore::default();
811 store.prepare("app", v[0], Some(payload("a"))).unwrap();
812 store.commit("app", v[0]);
813 let held = store.effective("app").unwrap().headers.clone();
814 assert_eq!(held.len(), 1);
815
816 let cleared = Payload {
817 config: HealthCheckConfig::default(),
818 headers: ProtectedHeaderSet::new(Vec::new()),
819 };
820 store.prepare("app", v[1], Some(cleared)).unwrap();
821 store.commit("app", v[1]);
822 assert!(store.effective("app").unwrap().headers.is_empty());
823 assert_eq!(
825 held.headers()[0].value().expose_secret_bytes(),
826 b"a".as_slice()
827 );
828
829 store.prepare("app", v[2], None).unwrap();
830 store.commit("app", v[2]);
831 assert!(store.effective("app").is_none());
832 store.remove_cluster("app");
833 assert_eq!(
834 format!("{store:?}"),
835 "WorkerStore { clusters: 0, staged: 0 }"
836 );
837 }
838
839 #[test]
840 fn worker_debug_prints_counts_only() {
841 let v = versions(1)[0];
842 let mut store = WorkerStore::default();
843 store.prepare("app", v, Some(payload(SENTINEL))).unwrap();
844 store.commit("app", v);
845 let shown = format!("{store:?}");
846 assert!(!shown.contains(SENTINEL) && !shown.contains("authorization"));
847 }
848}