1use crate::error::FaucetError;
19use crate::state::StateStore;
20
21pub const KEY_SCHEME_VERSION: u32 = 3;
32
33pub const KEY_SEPARATOR: &str = "::";
35
36pub const COMPONENT_MAX: usize = 64;
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41#[non_exhaustive]
42pub enum KeySuffix {
43 Sla,
45 Profiling,
47 RollbackIndex,
49 RollbackRun(String),
51 Status,
53 Lease,
55 Replication,
57 Backfill(String),
59 ServeBackfill(String),
61}
62
63impl KeySuffix {
64 pub fn is_pipeline_level(&self) -> bool {
66 matches!(
67 self,
68 Self::Replication | Self::Backfill(_) | Self::ServeBackfill(_)
69 )
70 }
71
72 fn render_into(&self, out: &mut String) {
73 let (marker, tail) = match self {
74 Self::Sla => ("__sla__", None),
75 Self::Profiling => ("__profiling__", None),
76 Self::RollbackIndex => ("__rollback__", None),
77 Self::RollbackRun(run) => ("__rollback__", Some(run.as_str())),
78 Self::Status => ("__status__", None),
79 Self::Lease => ("__lease__", None),
80 Self::Replication => ("__replication__", None),
81 Self::Backfill(h) => ("__backfill__", Some(h.as_str())),
82 Self::ServeBackfill(h) => ("__serve_backfill__", Some(h.as_str())),
83 };
84 push(out, marker);
85 if let Some(tail) = tail {
86 push(out, tail);
87 }
88 }
89
90 pub fn render(&self) -> String {
92 let mut s = String::new();
93 self.render_into(&mut s);
94 s
95 }
96
97 pub fn tail(&self) -> String {
99 format!("{KEY_SEPARATOR}{}", self.render())
100 }
101}
102
103fn push(out: &mut String, seg: &str) {
104 if !out.is_empty() {
105 out.push_str(KEY_SEPARATOR);
106 }
107 out.push_str(seg);
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Hash)]
111enum Sub {
112 Parent(String),
114 Encoded(String),
116 Typed { typed: String, text: String },
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122enum Head {
123 Parts {
124 tenant: Option<String>,
125 pipeline: String,
126 row: Option<String>,
127 sub: Option<Sub>,
128 },
129 Rendered(String),
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Hash)]
135pub struct StateKey {
136 head: Head,
137 shard: Option<String>,
138 suffix: Option<KeySuffix>,
139}
140
141impl StateKey {
142 pub fn pipeline(pipeline: impl Into<String>) -> Self {
144 Self {
145 head: Head::Parts {
146 tenant: None,
147 pipeline: pipeline.into(),
148 row: None,
149 sub: None,
150 },
151 shard: None,
152 suffix: None,
153 }
154 }
155
156 pub fn invocation(pipeline: impl Into<String>, row: impl Into<String>) -> Self {
158 Self::pipeline(pipeline).with_row(row)
159 }
160
161 pub fn from_rendered(base: impl Into<String>) -> Self {
164 Self {
165 head: Head::Rendered(base.into()),
166 shard: None,
167 suffix: None,
168 }
169 }
170
171 pub fn with_tenant(mut self, tenant: Option<impl Into<String>>) -> Self {
173 if let Head::Parts { tenant: t, .. } = &mut self.head {
174 *t = tenant.map(Into::into);
175 }
176 self
177 }
178
179 pub fn with_row(mut self, id: impl Into<String>) -> Self {
181 if let Head::Parts { row, .. } = &mut self.head {
182 *row = Some(id.into());
183 }
184 self
185 }
186
187 pub fn with_parent_key(mut self, raw: impl Into<String>) -> Self {
192 if let Head::Parts { sub, .. } = &mut self.head {
193 *sub = Some(Sub::Parent(raw.into()));
194 }
195 self
196 }
197
198 pub fn with_parent_value(mut self, value: &serde_json::Value) -> Self {
203 use serde_json::Value;
204 let (typed, text) = match value {
205 Value::String(s) => (typed_string(s), s.clone()),
206 other => {
207 let tag = match other {
208 Value::Number(_) => 'n',
209 Value::Bool(_) => 'b',
210 Value::Null => 'z',
211 Value::Array(_) => 'a',
212 _ => 'o',
213 };
214 let text = other.to_string();
215 (encode_component(&format!("_{tag}.{text}"), true), text)
216 }
217 };
218 if let Head::Parts { sub, .. } = &mut self.head {
219 *sub = Some(Sub::Typed { typed, text });
220 }
221 self
222 }
223
224 pub fn with_sub(mut self, encoded: impl Into<String>) -> Self {
226 if let Head::Parts { sub, .. } = &mut self.head {
227 *sub = Some(Sub::Encoded(encoded.into()));
228 }
229 self
230 }
231
232 pub fn with_shard(mut self, shard: impl Into<String>) -> Self {
234 self.shard = Some(shard.into());
235 self
236 }
237
238 pub fn with_suffix(mut self, suffix: KeySuffix) -> Self {
240 self.suffix = Some(suffix);
241 self
242 }
243
244 pub fn tenant(&self) -> Option<&str> {
246 match &self.head {
247 Head::Parts { tenant, .. } => tenant.as_deref(),
248 Head::Rendered(_) => None,
249 }
250 }
251
252 pub fn pipeline_name(&self) -> Option<&str> {
254 match &self.head {
255 Head::Parts { pipeline, .. } => Some(pipeline),
256 Head::Rendered(_) => None,
257 }
258 }
259
260 pub fn row(&self) -> Option<&str> {
262 match &self.head {
263 Head::Parts { row, .. } => row.as_deref(),
264 Head::Rendered(_) => None,
265 }
266 }
267
268 pub fn sub(&self) -> Option<String> {
270 match &self.head {
271 Head::Parts { sub: Some(s), .. } => Some(render_sub(s, KEY_SCHEME_VERSION)),
272 _ => None,
273 }
274 }
275
276 pub fn shard(&self) -> Option<&str> {
278 self.shard.as_deref()
279 }
280
281 pub fn suffix(&self) -> Option<&KeySuffix> {
283 self.suffix.as_ref()
284 }
285
286 pub fn namespace(&self) -> String {
289 match &self.head {
290 Head::Parts {
291 tenant, pipeline, ..
292 } => {
293 let mut s = String::new();
294 if let Some(t) = tenant {
295 push(&mut s, t);
296 }
297 push(&mut s, pipeline);
298 s
299 }
300 Head::Rendered(base) => base.clone(),
301 }
302 }
303
304 pub fn children_prefix(&self) -> String {
306 format!("{}{KEY_SEPARATOR}", self.render())
307 }
308
309 pub fn render(&self) -> String {
311 self.render_scheme(KEY_SCHEME_VERSION)
312 }
313
314 pub fn render_scheme(&self, version: u32) -> String {
316 let mut s = match &self.head {
317 Head::Parts { row, sub, .. } => {
318 let mut s = self.namespace();
319 if let Some(row) = row {
320 push(&mut s, row);
321 }
322 if let Some(sub) = sub {
323 push(&mut s, &render_sub(sub, version));
324 }
325 s
326 }
327 Head::Rendered(base) => base.clone(),
328 };
329 if let Some(shard) = &self.shard {
330 push(&mut s, shard);
331 }
332 if let Some(suffix) = &self.suffix {
333 suffix.render_into(&mut s);
334 }
335 s
336 }
337
338 pub fn legacy_renderings(&self) -> Vec<String> {
342 let current = self.render();
343 let mut out: Vec<String> = Vec::new();
344 for version in (1..KEY_SCHEME_VERSION).rev() {
345 let old = self.render_scheme(version);
346 if old != current
347 && !out.contains(&old)
348 && crate::state::validate_state_key(&old).is_ok()
349 {
350 out.push(old);
351 }
352 }
353 out
354 }
355
356 pub fn parse(tenant: Option<&str>, pipeline: &str, key: &str) -> Option<Self> {
360 let ns = StateKey::pipeline(pipeline).with_tenant(tenant).namespace();
361 let rest = key.strip_prefix(&ns)?.strip_prefix(KEY_SEPARATOR)?;
362 if rest.is_empty() {
363 return None;
364 }
365 let segs: Vec<&str> = rest.split(KEY_SEPARATOR).collect();
366 let join = |s: &[&str]| s.join(KEY_SEPARATOR);
367 let base = StateKey::pipeline(pipeline).with_tenant(tenant);
368 match segs[0] {
369 "__replication__" if segs.len() == 1 => {
370 return Some(base.with_suffix(KeySuffix::Replication));
371 }
372 "__backfill__" if segs.len() > 1 => {
373 return Some(base.with_suffix(KeySuffix::Backfill(join(&segs[1..]))));
374 }
375 "__serve_backfill__" if segs.len() > 1 => {
376 return Some(base.with_suffix(KeySuffix::ServeBackfill(join(&segs[1..]))));
377 }
378 _ => {}
379 }
380 let marker = segs.iter().enumerate().skip(1).find_map(|(i, s)| {
381 let suffix = match *s {
382 "__sla__" if i + 1 == segs.len() => KeySuffix::Sla,
383 "__profiling__" if i + 1 == segs.len() => KeySuffix::Profiling,
384 "__status__" if i + 1 == segs.len() => KeySuffix::Status,
385 "__lease__" if i + 1 == segs.len() => KeySuffix::Lease,
386 "__rollback__" if i + 1 < segs.len() => {
387 KeySuffix::RollbackRun(join(&segs[i + 1..]))
388 }
389 "__rollback__" => KeySuffix::RollbackIndex,
390 _ => return None,
391 };
392 Some((i, suffix))
393 });
394 let end = marker.as_ref().map_or(segs.len(), |(i, _)| *i);
395 let row_end = if segs[0] == "backfill" && end > 1 {
397 end
398 } else {
399 1
400 };
401 let mut parsed = base.with_row(join(&segs[..row_end]));
402 if end > row_end {
403 parsed = parsed.with_sub(join(&segs[row_end..end]));
404 }
405 if let Some((_, suffix)) = marker {
406 parsed = parsed.with_suffix(suffix);
407 }
408 Some(parsed)
409 }
410}
411
412fn render_sub(sub: &Sub, version: u32) -> String {
413 match sub {
414 Sub::Parent(raw) if version >= 3 => typed_string(raw),
415 Sub::Parent(raw) if version >= 2 => encode_component(raw, true),
416 Sub::Parent(raw) => raw.clone(),
417 Sub::Typed { typed, .. } if version >= 3 => typed.clone(),
418 Sub::Typed { text, .. } if version >= 2 => encode_component(text, true),
419 Sub::Typed { text, .. } => text.clone(),
420 Sub::Encoded(s) => s.clone(),
421 }
422}
423
424const TYPE_TAGS: &[char] = &['s', 'n', 'b', 'z', 'a', 'o'];
426
427fn typed_string(raw: &str) -> String {
431 let mut chars = raw.chars();
432 let tag_like = chars.next() == Some('_')
433 && chars.next().is_some_and(|c| TYPE_TAGS.contains(&c))
434 && chars.next() == Some('.');
435 if tag_like {
436 encode_component(&format!("_s.{raw}"), true)
437 } else {
438 encode_component(raw, true)
439 }
440}
441
442impl std::fmt::Display for StateKey {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 f.write_str(&self.render())
445 }
446}
447
448pub fn encode_component(raw: &str, allow_separators: bool) -> String {
457 let plain = |c: char| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.');
458 let legal = |c: char| plain(c) || (allow_separators && matches!(c, ':' | '/'));
459 if !raw.is_empty()
460 && raw.len() <= COMPONENT_MAX
461 && raw.chars().all(legal)
462 && !raw.contains(KEY_SEPARATOR)
463 && !raw.starts_with(':')
464 && !raw.ends_with(':')
465 && (!raw.contains('/')
466 || raw
467 .split('/')
468 .all(|seg| !seg.is_empty() && !seg.starts_with('.')))
469 {
470 return raw.to_owned();
471 }
472 let readable: String = raw.chars().filter(|c| plain(*c)).take(32).collect();
473 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
474 for b in raw.as_bytes() {
475 hash ^= u64::from(*b);
476 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
477 }
478 format!("{readable}.h{hash:016x}")
479}
480
481pub fn backfill_unit_row(range_hash: &str, unit: &str) -> String {
484 ["backfill", range_hash, unit].join(KEY_SEPARATOR)
485}
486
487#[derive(Debug, Clone, PartialEq, Eq)]
489#[non_exhaustive]
490pub enum KeyMigration {
491 Current,
493 Fresh,
495 Moved {
497 from: String,
499 },
500}
501
502pub async fn migrate_state_key(
512 store: &dyn StateStore,
513 key: &str,
514 candidates: &[String],
515) -> Result<KeyMigration, FaucetError> {
516 if candidates.is_empty() {
517 return Ok(KeyMigration::Fresh);
518 }
519 if store.get(key).await?.is_some() {
520 return Ok(KeyMigration::Current);
521 }
522 for old in candidates.iter().filter(|c| c.as_str() != key) {
523 let Some(value) = store.get(old).await? else {
524 continue;
525 };
526 if !store.compare_and_put(key, None, &value).await? {
527 metrics::counter!("faucet_state_key_migrations_total", "outcome" => "raced")
528 .increment(1);
529 return Ok(KeyMigration::Current);
530 }
531 if let Err(e) = store.delete(old).await {
532 tracing::warn!(key = %old, error = %e, "legacy state key could not be removed");
533 }
534 metrics::counter!("faucet_state_key_migrations_total", "outcome" => "moved").increment(1);
535 tracing::info!(from = %old, to = key, "moved state from the key an earlier release used");
536 return Ok(KeyMigration::Moved { from: old.clone() });
537 }
538 Ok(KeyMigration::Fresh)
539}
540
541#[cfg(test)]
542mod tests {
543 #[test]
544 fn typed_parent_keys_tag_null_and_arrays() {
545 let inv = || StateKey::invocation("p", "r");
546 let null = inv().with_parent_value(&json!(null)).render();
547 assert!(null.starts_with("p::r::_z."), "{null}");
548 let arr = inv().with_parent_value(&json!([1])).render();
549 assert!(arr.starts_with("p::r::_a."), "{arr}");
550 assert_ne!(null, arr);
551 crate::state::validate_state_key(&arr).unwrap();
552 }
553 use super::*;
554 use crate::state::MemoryStateStore;
555 use serde_json::json;
556
557 #[test]
558 fn a_path_like_parent_key_always_renders_a_valid_state_key() {
559 for raw in ["exports/2026/", "a//b", "a/./b", "a/../b", "x/.cfg", "/abs"] {
560 let key = StateKey::invocation("p", "files")
561 .with_parent_key(raw)
562 .render();
563 crate::state::validate_state_key(&key).unwrap_or_else(|e| panic!("{raw}: {e}"));
564 assert!(key.contains(".h"), "{raw} is hashed: {key}");
565 }
566 assert_eq!(
568 StateKey::invocation("p", "files")
569 .with_parent_key("exports/2026")
570 .render(),
571 "p::files::exports/2026"
572 );
573 }
574
575 #[test]
576 fn renders_every_kind() {
577 let inv = StateKey::invocation("p", "r");
578 assert_eq!(inv.render(), "p::r");
579 assert_eq!(StateKey::pipeline("p").render(), "p");
580 assert_eq!(
581 StateKey::invocation("p", "r")
582 .with_tenant(Some("acme"))
583 .render(),
584 "acme::p::r"
585 );
586 assert_eq!(inv.clone().with_parent_key("42").render(), "p::r::42");
587 assert_eq!(inv.clone().with_sub("a:1/b:2").render(), "p::r::a:1/b:2");
588 assert_eq!(inv.clone().with_shard("s3").render(), "p::r::s3");
589 let cases = [
590 (KeySuffix::Sla, "p::r::__sla__"),
591 (KeySuffix::Profiling, "p::r::__profiling__"),
592 (KeySuffix::RollbackIndex, "p::r::__rollback__"),
593 (KeySuffix::RollbackRun("x".into()), "p::r::__rollback__::x"),
594 (KeySuffix::Status, "p::r::__status__"),
595 (KeySuffix::Lease, "p::r::__lease__"),
596 ];
597 for (suffix, want) in cases {
598 assert!(!suffix.is_pipeline_level());
599 assert_eq!(inv.clone().with_suffix(suffix).render(), want);
600 }
601 for (suffix, want) in [
602 (KeySuffix::Replication, "p::__replication__"),
603 (KeySuffix::Backfill("h".into()), "p::__backfill__::h"),
604 (
605 KeySuffix::ServeBackfill("h".into()),
606 "p::__serve_backfill__::h",
607 ),
608 ] {
609 assert!(suffix.is_pipeline_level());
610 assert_eq!(StateKey::pipeline("p").with_suffix(suffix).render(), want);
611 }
612 assert_eq!(
613 StateKey::from_rendered("p::r::7")
614 .with_suffix(KeySuffix::Status)
615 .to_string(),
616 "p::r::7::__status__"
617 );
618 assert_eq!(
619 KeySuffix::RollbackRun("x".into()).render(),
620 "__rollback__::x"
621 );
622 assert_eq!(inv.children_prefix(), "p::r::");
623 assert_eq!(KeySuffix::Lease.tail(), "::__lease__");
624 }
625
626 #[test]
627 fn accessors_report_components() {
628 let k = StateKey::invocation("p", "r")
629 .with_tenant(Some("t"))
630 .with_parent_key("a b")
631 .with_shard("s")
632 .with_suffix(KeySuffix::Lease);
633 assert_eq!(k.tenant(), Some("t"));
634 assert_eq!(k.pipeline_name(), Some("p"));
635 assert_eq!(k.row(), Some("r"));
636 assert_eq!(k.sub(), Some(encode_component("a b", true)));
637 assert_eq!(k.shard(), Some("s"));
638 assert_eq!(k.suffix(), Some(&KeySuffix::Lease));
639 assert_eq!(k.namespace(), "t::p");
640 let r = StateKey::from_rendered("x::y")
641 .with_tenant(Some("ignored"))
642 .with_row("ignored")
643 .with_parent_key("ignored")
644 .with_sub("ignored");
645 assert_eq!(r.render(), "x::y");
646 assert_eq!(
647 (r.tenant(), r.pipeline_name(), r.row(), r.sub()),
648 (None, None, None, None)
649 );
650 assert_eq!(r.namespace(), "x::y");
651 assert_eq!(StateKey::invocation("p", "r").sub(), None);
652 }
653
654 #[test]
655 fn parent_keys_are_encoded_and_remember_the_verbatim_scheme() {
656 let k = StateKey::invocation("p", "r").with_parent_key("a b");
657 assert_ne!(k.render(), "p::r::a b");
658 assert_eq!(k.render_scheme(1), "p::r::a b");
659 assert!(k.legacy_renderings().is_empty());
661 let k = StateKey::invocation("p", "r").with_parent_key("x::y");
662 assert_eq!(k.legacy_renderings(), vec!["p::r::x::y".to_string()]);
663 let k = StateKey::invocation("p", "r").with_parent_key("plain");
664 assert!(k.legacy_renderings().is_empty());
665 }
666
667 #[test]
668 fn typed_parent_keys_keep_numbers_and_strings_apart() {
669 let inv = || StateKey::invocation("p", "r");
670 let num = inv().with_parent_value(&json!(1));
671 let text = inv().with_parent_value(&json!("1"));
672 assert_eq!(num.render(), "p::r::_n.1");
673 assert_eq!(text.render(), "p::r::1");
674 assert_ne!(num.render(), text.render());
675 assert_eq!(num.render_scheme(2), "p::r::1");
677 assert_eq!(num.legacy_renderings(), vec!["p::r::1".to_string()]);
678 assert!(
679 text.legacy_renderings().is_empty(),
680 "a plain string never moves"
681 );
682 assert_eq!(text, inv().with_parent_value(&json!("1")));
684 assert_eq!(text.render(), inv().with_parent_key("1").render());
685 let fake = inv().with_parent_value(&json!("_n.1"));
687 assert_eq!(fake.render(), "p::r::_s._n.1");
688 assert_eq!(fake.legacy_renderings(), vec!["p::r::_n.1".to_string()]);
689 assert_eq!(inv().with_parent_key("_n.1").render(), "p::r::_s._n.1");
690 for (v, want) in [
691 (json!(true), "p::r::_b.true"),
692 (json!(-2.5), "p::r::_n.-2.5"),
693 ] {
694 assert_eq!(inv().with_parent_value(&v).render(), want);
695 }
696 let obj = inv().with_parent_value(&json!({"a": 1})).render();
697 assert!(obj.starts_with("p::r::_o.a1.h"), "{obj}");
698 crate::state::validate_state_key(&obj).unwrap();
699 assert_eq!(num.render_scheme(1), "p::r::1");
700 }
701
702 #[test]
703 fn encode_component_keeps_legal_values_and_hashes_the_rest() {
704 assert_eq!(encode_component("cust-42_a.b", false), "cust-42_a.b");
705 assert_eq!(encode_component("a/b:c", true), "a/b:c");
706 assert_ne!(encode_component("a/b:c", false), "a/b:c");
707 for raw in ["", "x::y", ":x", "y:", "émoji", &"z".repeat(65)] {
708 let c = encode_component(raw, true);
709 assert!(c.contains(".h"), "{raw:?} → {c}");
710 crate::state::validate_state_key(&format!("p::r::{c}")).unwrap();
711 }
712 }
713
714 #[test]
715 fn parse_round_trips_every_kind() {
716 let keys = [
717 "p::r",
718 "p::r::42",
719 "p::r::a:1/b:2",
720 "p::r::__sla__",
721 "p::r::__profiling__",
722 "p::r::__rollback__",
723 "p::r::__rollback__::run1",
724 "p::r::7::__status__",
725 "p::r::__lease__",
726 "p::__replication__",
727 "p::__backfill__::abc",
728 "p::__serve_backfill__::abc",
729 "p::backfill::h::2026-01-01",
730 "p::backfill::h::u1::__status__",
731 "p::backfill",
732 ];
733 for key in keys {
734 let k = StateKey::parse(None, "p", key).unwrap_or_else(|| panic!("{key}"));
735 assert_eq!(k.render(), key);
736 }
737 assert_eq!(backfill_unit_row("h", "u"), "backfill::h::u");
738 let k = StateKey::parse(None, "p", "p::backfill::h::u1::__lease__").unwrap();
739 assert_eq!(k.row(), Some("backfill::h::u1"));
740 assert_eq!(k.suffix(), Some(&KeySuffix::Lease));
741 let k = StateKey::parse(Some("t"), "p", "t::p::r::9").unwrap();
742 assert_eq!(
743 (k.tenant(), k.row(), k.sub()),
744 (Some("t"), Some("r"), Some("9".into()))
745 );
746 assert!(StateKey::parse(None, "p", "q::r").is_none());
747 assert!(StateKey::parse(None, "p", "p::").is_none());
748 assert!(StateKey::parse(None, "p", "px::r").is_none());
749 }
750
751 #[tokio::test]
752 async fn migration_moves_once_and_never_clobbers() {
753 let store = MemoryStateStore::new();
754 assert_eq!(
755 migrate_state_key(&store, "p::new", &[]).await.unwrap(),
756 KeyMigration::Fresh
757 );
758 assert_eq!(
759 migrate_state_key(&store, "p::new", &["p::old".into()])
760 .await
761 .unwrap(),
762 KeyMigration::Fresh
763 );
764 store.put("p::old", &json!({"page": 3})).await.unwrap();
765 assert_eq!(
766 migrate_state_key(&store, "p::new", &["p::new".into(), "p::old".into()])
767 .await
768 .unwrap(),
769 KeyMigration::Moved {
770 from: "p::old".into()
771 }
772 );
773 assert_eq!(store.get("p::new").await.unwrap(), Some(json!({"page": 3})));
774 assert_eq!(store.get("p::old").await.unwrap(), None);
775 store.put("p::old", &json!(1)).await.unwrap();
776 assert_eq!(
777 migrate_state_key(&store, "p::new", &["p::old".into()])
778 .await
779 .unwrap(),
780 KeyMigration::Current
781 );
782 assert_eq!(store.get("p::new").await.unwrap(), Some(json!({"page": 3})));
783 }
784
785 struct Racing(MemoryStateStore);
786
787 #[async_trait::async_trait]
788 impl StateStore for Racing {
789 async fn get(&self, key: &str) -> Result<Option<serde_json::Value>, FaucetError> {
790 self.0.get(key).await
791 }
792 async fn put(&self, key: &str, value: &serde_json::Value) -> Result<(), FaucetError> {
793 self.0.put(key, value).await
794 }
795 async fn delete(&self, key: &str) -> Result<(), FaucetError> {
796 self.0.delete(key).await
797 }
798 async fn compare_and_put(
799 &self,
800 _key: &str,
801 _expected: Option<&serde_json::Value>,
802 _value: &serde_json::Value,
803 ) -> Result<bool, FaucetError> {
804 Ok(false)
805 }
806 }
807
808 struct Sticky(MemoryStateStore);
810
811 #[async_trait::async_trait]
812 impl StateStore for Sticky {
813 async fn get(&self, key: &str) -> Result<Option<serde_json::Value>, FaucetError> {
814 self.0.get(key).await
815 }
816 async fn put(&self, key: &str, value: &serde_json::Value) -> Result<(), FaucetError> {
817 self.0.put(key, value).await
818 }
819 async fn delete(&self, _key: &str) -> Result<(), FaucetError> {
820 Err(FaucetError::State("read-only".into()))
821 }
822 }
823
824 #[tokio::test]
828 async fn a_legacy_key_that_cannot_be_removed_still_moves() {
829 let store = Sticky(MemoryStateStore::new());
830 store.put("p::old", &json!(2)).await.unwrap();
831 migrate_state_key(&store, "p::new", &["p::old".into()])
832 .await
833 .unwrap();
834 assert_eq!(store.get("p::new").await.unwrap(), Some(json!(2)));
835 assert_eq!(store.get("p::old").await.unwrap(), Some(json!(2)));
836 let racing = Racing(MemoryStateStore::new());
837 racing.put("k", &json!(1)).await.unwrap();
838 racing.delete("k").await.unwrap();
839 assert_eq!(racing.get("k").await.unwrap(), None);
840 }
841
842 #[tokio::test]
843 async fn a_concurrent_writer_wins_the_race() {
844 let store = Racing(MemoryStateStore::new());
845 store.put("p::old", &json!(1)).await.unwrap();
846 assert_eq!(
847 migrate_state_key(&store, "p::new", &["p::old".into()])
848 .await
849 .unwrap(),
850 KeyMigration::Current
851 );
852 assert_eq!(store.get("p::old").await.unwrap(), Some(json!(1)));
853 }
854}