1use std::collections::BTreeMap;
22use std::fmt;
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::sync::{Arc, Mutex};
25use std::time::Duration;
26
27use rustev_contract::canonical::{canonical_bytes, tagged_digest};
28use rustev_contract::ids::ArtifactId;
29use rustev_contract::remote::{
30 AgeBucket, ConnectionObservation, ConnectionReuse, EvictionReason, Multiplex,
31};
32use serde::Serialize;
33use tokio::sync::Notify;
34use tokio::time::Instant;
35
36use crate::budget::Budget;
37
38pub const POOL_KEY_SCHEMA: &str = "rustev.pool-key/1";
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
43#[serde(rename_all = "snake_case")]
44pub enum OnExhaustion {
45 Refuse,
47 Queue { max_wait_ms: u64 },
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
54#[serde(deny_unknown_fields)]
55pub struct PoolPolicy {
56 pub max_connections: u32,
57 pub max_per_origin: u32,
58 pub max_streams: u32,
59 pub max_idle: u32,
60 pub idle_lifetime_ms: u64,
61 pub max_lifetime_ms: u64,
62 pub max_uses: u64,
63 pub on_exhaustion: OnExhaustion,
64}
65
66impl PoolPolicy {
67 pub fn validate(&self) -> Result<(), String> {
69 let positive = [
70 ("max_connections", self.max_connections as u64),
71 ("max_per_origin", self.max_per_origin as u64),
72 ("max_streams", self.max_streams as u64),
73 ("max_idle", self.max_idle as u64),
74 ("idle_lifetime_ms", self.idle_lifetime_ms),
75 ("max_lifetime_ms", self.max_lifetime_ms),
76 ("max_uses", self.max_uses),
77 ];
78 if let Some((name, _)) = positive.iter().find(|(_, v)| *v == 0) {
79 return Err(format!("pool bound `{name}` must be positive"));
80 }
81 if self.max_streams != 1 {
82 return Err("the HTTP/1.1 binding carries one stream per connection".into());
83 }
84 if self.max_per_origin > self.max_connections || self.max_idle > self.max_connections {
85 return Err("per-origin and idle bounds cannot exceed the total".into());
86 }
87 if let OnExhaustion::Queue { max_wait_ms: 0 } = self.on_exhaustion {
88 return Err("a queue needs a positive wait".into());
89 }
90 Ok(())
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
98#[serde(rename_all = "snake_case")]
99pub enum PoolBinding {
100 #[default]
101 Disabled,
102 Enabled {
103 policy: PoolPolicy,
104 key_schema: String,
105 },
106}
107
108impl PoolBinding {
109 pub fn is_disabled(&self) -> bool {
110 matches!(self, PoolBinding::Disabled)
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
118pub struct KeyMaterial {
119 pub schema: String,
120 pub endpoint_class: String,
121 pub authority: String,
123 pub protocol: String,
125 pub proxy_route: String,
127 pub tls: Option<String>,
130 pub credential: String,
132 pub privacy: BTreeMap<String, String>,
134 pub principal_posture: String,
136 pub principal: Option<String>,
138 pub binding: Option<ArtifactId>,
139}
140
141pub fn credential_identity(secret: Option<&str>) -> String {
143 match secret {
144 None => "none".into(),
145 Some(s) => tagged_digest("rustev.credential-identity/1", s.as_bytes()),
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct PoolKey {
152 pub digest: Option<String>,
155 pub origin: String,
156}
157
158impl KeyMaterial {
159 pub fn key(&self) -> PoolKey {
162 let known = self.tls.is_some() && self.binding.is_some() && self.schema == POOL_KEY_SCHEMA;
163 let digest = known
164 .then(|| canonical_bytes(self).ok())
165 .flatten()
166 .map(|b| tagged_digest(POOL_KEY_SCHEMA, &b));
167 PoolKey {
168 digest,
169 origin: self.authority.clone(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct Exhausted;
177
178struct Meta {
179 id: u64,
180 key: Option<String>,
181 origin: String,
182 created: Instant,
183 uses: u64,
184 last_used: Instant,
185}
186
187struct Idle<C> {
188 meta: Meta,
189 conn: C,
190}
191
192struct State<C> {
193 idle: Vec<Idle<C>>,
194 per_origin: BTreeMap<String, u32>,
196 open: u32,
197 evictions: BTreeMap<String, u64>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Default)]
202pub struct PoolStats {
203 pub open: u32,
204 pub idle: u32,
205 pub opened: u64,
206 pub reused: u64,
207 pub evictions: BTreeMap<String, u64>,
208}
209
210pub trait Pooled: Send + 'static {
212 fn is_closed(&self) -> bool;
213}
214
215pub struct ConnectionPool<C> {
217 policy: PoolPolicy,
218 state: Mutex<State<C>>,
219 room: Notify,
220 next_id: AtomicU64,
221 opened: AtomicU64,
222 reused: AtomicU64,
223}
224
225impl<C> fmt::Debug for ConnectionPool<C> {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 f.debug_struct("ConnectionPool")
228 .field("policy", &self.policy)
229 .finish_non_exhaustive()
230 }
231}
232
233fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
234 m.lock().unwrap_or_else(|e| e.into_inner())
235}
236
237fn reason_name(r: EvictionReason) -> String {
238 serde_json::to_value(r)
239 .ok()
240 .and_then(|v| v.as_str().map(str::to_string))
241 .unwrap_or_default()
242}
243
244fn ms(d: Duration) -> u64 {
245 u64::try_from(d.as_millis()).unwrap_or(u64::MAX)
246}
247
248pub struct Lease<C: Pooled> {
251 pool: Arc<ConnectionPool<C>>,
252 meta: Option<Meta>,
253 conn: Option<C>,
254 reused: bool,
255}
256
257impl<C: Pooled> Lease<C> {
258 pub fn conn(&self) -> Option<&C> {
260 self.conn.as_ref()
261 }
262
263 pub fn is_reused(&self) -> bool {
264 self.reused
265 }
266
267 pub fn conn_mut(&mut self) -> Option<&mut C> {
268 self.conn.as_mut()
269 }
270
271 pub fn fill(&mut self, conn: C) {
273 self.conn = Some(conn);
274 }
275
276 pub fn observation(&self, key_digest: &str) -> Option<ConnectionObservation> {
278 let m = self.meta.as_ref()?;
279 Some(ConnectionObservation {
280 reuse: if self.reused {
281 ConnectionReuse::Reused
282 } else {
283 ConnectionReuse::Fresh
284 },
285 instance_id: format!("conn-{}", m.id),
286 age_bucket: AgeBucket::of_ms(ms(Instant::now().saturating_duration_since(m.created))),
287 use_count: m.uses,
288 multiplex: Multiplex::None,
289 key_digest: key_digest.to_string(),
290 eviction: None,
291 })
292 }
293}
294
295impl<C: Pooled> Drop for Lease<C> {
296 fn drop(&mut self) {
297 if let Some(m) = self.meta.take() {
298 self.pool.close(&m, EvictionReason::Failed);
299 }
300 }
301}
302
303impl<C: Pooled> ConnectionPool<C> {
304 pub fn new(policy: PoolPolicy) -> Result<Arc<Self>, String> {
306 policy.validate()?;
307 Ok(Arc::new(ConnectionPool {
308 policy,
309 state: Mutex::new(State {
310 idle: vec![],
311 per_origin: BTreeMap::new(),
312 open: 0,
313 evictions: BTreeMap::new(),
314 }),
315 room: Notify::new(),
316 next_id: AtomicU64::new(1),
317 opened: AtomicU64::new(0),
318 reused: AtomicU64::new(0),
319 }))
320 }
321
322 pub fn policy(&self) -> &PoolPolicy {
323 &self.policy
324 }
325
326 pub fn binding(&self) -> PoolBinding {
328 PoolBinding::Enabled {
329 policy: self.policy,
330 key_schema: POOL_KEY_SCHEMA.into(),
331 }
332 }
333
334 pub fn stats(&self) -> PoolStats {
335 let st = lock(&self.state);
336 PoolStats {
337 open: st.open,
338 idle: st.idle.len() as u32,
339 opened: self.opened.load(Ordering::SeqCst),
340 reused: self.reused.load(Ordering::SeqCst),
341 evictions: st.evictions.clone(),
342 }
343 }
344
345 fn count(st: &mut State<C>, reason: EvictionReason) {
346 *st.evictions.entry(reason_name(reason)).or_default() += 1;
347 }
348
349 fn close(&self, m: &Meta, reason: EvictionReason) {
350 let mut st = lock(&self.state);
351 Self::forget(&mut st, m);
352 Self::count(&mut st, reason);
353 drop(st);
354 self.room.notify_waiters();
355 }
356
357 fn forget(st: &mut State<C>, m: &Meta) {
358 st.open = st.open.saturating_sub(1);
359 if let Some(n) = st.per_origin.get_mut(&m.origin) {
360 *n = n.saturating_sub(1);
361 if *n == 0 {
362 st.per_origin.remove(&m.origin);
363 }
364 }
365 }
366
367 fn sweep(&self, st: &mut State<C>, now: Instant) {
370 let idle_life = Duration::from_millis(self.policy.idle_lifetime_ms);
371 let life = Duration::from_millis(self.policy.max_lifetime_ms);
372 let mut keep = Vec::with_capacity(st.idle.len());
373 for i in std::mem::take(&mut st.idle) {
374 let reason = if i.conn.is_closed() {
375 Some(EvictionReason::Closed)
376 } else if now.saturating_duration_since(i.meta.created) >= life {
377 Some(EvictionReason::LifetimeExpired)
378 } else if now.saturating_duration_since(i.meta.last_used) >= idle_life {
379 Some(EvictionReason::IdleExpired)
380 } else {
381 None
382 };
383 match reason {
384 Some(r) => {
385 Self::forget(st, &i.meta);
386 Self::count(st, r);
387 }
388 None => keep.push(i),
389 }
390 }
391 st.idle = keep;
392 }
393
394 fn try_acquire(self: &Arc<Self>, key: &PoolKey) -> Option<Lease<C>> {
395 let now = Instant::now();
396 let mut st = lock(&self.state);
397 self.sweep(&mut st, now);
398 if let Some(digest) = &key.digest
400 && let Some(at) = st
401 .idle
402 .iter()
403 .position(|i| i.meta.key.as_deref() == Some(digest.as_str()))
404 {
405 let Idle { mut meta, conn } = st.idle.remove(at);
406 meta.uses += 1;
407 meta.last_used = now;
408 self.reused.fetch_add(1, Ordering::SeqCst);
409 return Some(Lease {
410 pool: self.clone(),
411 meta: Some(meta),
412 conn: Some(conn),
413 reused: true,
414 });
415 }
416 let origin_open = st.per_origin.get(&key.origin).copied().unwrap_or(0);
417 if origin_open >= self.policy.max_per_origin {
418 let at = st.idle.iter().position(|i| i.meta.origin == key.origin)?;
420 let old = st.idle.remove(at);
421 Self::forget(&mut st, &old.meta);
422 Self::count(&mut st, EvictionReason::Capacity);
423 }
424 if st.open >= self.policy.max_connections {
425 let at = (0..st.idle.len())
428 .min_by_key(|&i| (st.idle[i].meta.last_used, st.idle[i].meta.id))?;
429 let old = st.idle.remove(at);
430 Self::forget(&mut st, &old.meta);
431 Self::count(&mut st, EvictionReason::Capacity);
432 }
433 st.open += 1;
434 *st.per_origin.entry(key.origin.clone()).or_default() += 1;
435 self.opened.fetch_add(1, Ordering::SeqCst);
436 Some(Lease {
437 pool: self.clone(),
438 meta: Some(Meta {
439 id: self.next_id.fetch_add(1, Ordering::SeqCst),
440 key: key.digest.clone(),
441 origin: key.origin.clone(),
442 created: now,
443 uses: 1,
444 last_used: now,
445 }),
446 conn: None,
447 reused: false,
448 })
449 }
450
451 pub async fn acquire(
455 self: &Arc<Self>,
456 key: &PoolKey,
457 budget: &Budget,
458 ) -> Result<Lease<C>, Exhausted> {
459 let wait = match self.policy.on_exhaustion {
460 OnExhaustion::Refuse => None,
461 OnExhaustion::Queue { max_wait_ms } => Some(Budget::from_ms(max_wait_ms).min(*budget)),
462 };
463 loop {
464 let room = self.room.notified();
465 tokio::pin!(room);
466 room.as_mut().enable();
467 if let Some(l) = self.try_acquire(key) {
468 return Ok(l);
469 }
470 let Some(w) = wait else {
471 return Err(Exhausted);
472 };
473 tokio::select! {
474 _ = &mut room => {}
475 _ = w.expiry() => return Err(Exhausted),
476 }
477 }
478 }
479
480 pub fn release(&self, mut lease: Lease<C>, reusable: bool) -> Option<EvictionReason> {
484 let meta = lease.meta.take()?;
485 let conn = lease.conn.take();
486 let now = Instant::now();
487 let reason = match &conn {
488 None => Some(EvictionReason::Failed),
489 Some(_) if !reusable => Some(EvictionReason::Failed),
490 Some(c) if c.is_closed() => Some(EvictionReason::Closed),
491 Some(_) if meta.key.is_none() => Some(EvictionReason::Closed),
492 Some(_) if meta.uses >= self.policy.max_uses => Some(EvictionReason::MaxUses),
493 Some(_)
494 if now.saturating_duration_since(meta.created)
495 >= Duration::from_millis(self.policy.max_lifetime_ms) =>
496 {
497 Some(EvictionReason::LifetimeExpired)
498 }
499 Some(_) => None,
500 };
501 let mut st = lock(&self.state);
502 match (reason, conn) {
503 (Some(r), _) => {
504 Self::forget(&mut st, &meta);
505 Self::count(&mut st, r);
506 drop(st);
507 self.room.notify_waiters();
508 Some(r)
509 }
510 (None, Some(conn)) => {
511 if st.idle.len() as u32 >= self.policy.max_idle {
512 let at = (0..st.idle.len())
514 .min_by_key(|&i| (st.idle[i].meta.last_used, st.idle[i].meta.id))
515 .expect("max_idle is positive");
516 let old = st.idle.remove(at);
517 Self::forget(&mut st, &old.meta);
518 Self::count(&mut st, EvictionReason::IdleCapacity);
519 }
520 let mut meta = meta;
521 meta.last_used = now;
522 st.idle.push(Idle { meta, conn });
523 drop(st);
524 self.room.notify_waiters();
525 None
526 }
527 (None, None) => unreachable!("no connection is always a reason"),
528 }
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use super::*;
535 use std::sync::atomic::AtomicBool;
536
537 #[derive(Clone, Default)]
539 struct Fake(Arc<AtomicBool>);
540
541 impl Pooled for Fake {
542 fn is_closed(&self) -> bool {
543 self.0.load(Ordering::SeqCst)
544 }
545 }
546
547 fn policy() -> PoolPolicy {
548 PoolPolicy {
549 max_connections: 3,
550 max_per_origin: 2,
551 max_streams: 1,
552 max_idle: 2,
553 idle_lifetime_ms: 100,
554 max_lifetime_ms: 1_000,
555 max_uses: 3,
556 on_exhaustion: OnExhaustion::Refuse,
557 }
558 }
559
560 fn key(d: &str, origin: &str) -> PoolKey {
561 PoolKey {
562 digest: Some(d.into()),
563 origin: origin.into(),
564 }
565 }
566
567 async fn take(p: &Arc<ConnectionPool<Fake>>, k: &PoolKey) -> Lease<Fake> {
568 let mut l = p.acquire(k, &Budget::from_ms(10_000)).await.unwrap();
569 if l.conn().is_none() {
570 l.fill(Fake::default());
571 }
572 l
573 }
574
575 #[test]
576 fn every_bound_must_be_positive_and_consistent() {
577 assert!(policy().validate().is_ok());
578 let cases: Vec<fn(&mut PoolPolicy)> = vec![
579 |p| p.max_connections = 0,
580 |p| p.max_per_origin = 0,
581 |p| p.max_streams = 0,
582 |p| p.max_streams = 2,
583 |p| p.max_idle = 0,
584 |p| p.idle_lifetime_ms = 0,
585 |p| p.max_lifetime_ms = 0,
586 |p| p.max_uses = 0,
587 |p| p.max_per_origin = 4,
588 |p| p.max_idle = 4,
589 |p| p.on_exhaustion = OnExhaustion::Queue { max_wait_ms: 0 },
590 ];
591 for c in cases {
592 let mut p = policy();
593 c(&mut p);
594 assert!(p.validate().is_err(), "{p:?}");
595 assert!(ConnectionPool::<Fake>::new(p).is_err());
596 }
597 }
598
599 #[tokio::test(flavor = "current_thread", start_paused = true)]
600 async fn a_connection_is_reused_only_under_its_exact_key() {
601 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
602 let a = key("A", "o");
603 let l = take(&p, &a).await;
604 assert!(!l.is_reused());
605 assert_eq!(p.release(l, true), None);
606 let b = take(&p, &key("B", "o")).await;
608 assert!(!b.is_reused());
609 let again = take(&p, &a).await;
610 assert!(again.is_reused());
611 assert_eq!(again.observation("A").unwrap().use_count, 2);
612 let unknown = PoolKey {
614 digest: None,
615 origin: "u".into(),
616 };
617 let u = take(&p, &unknown).await;
618 assert_eq!(p.release(u, true), Some(EvictionReason::Closed));
619 let stats = p.stats();
620 assert_eq!((stats.opened, stats.reused), (3, 1));
621 }
622
623 #[tokio::test(flavor = "current_thread", start_paused = true)]
624 async fn lifetimes_uses_and_closure_evict() {
625 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
626 let a = key("A", "o");
627 let l = take(&p, &a).await;
629 p.release(l, true);
630 tokio::time::advance(Duration::from_millis(100)).await;
631 assert!(!take(&p, &a).await.is_reused());
632 assert_eq!(p.stats().evictions["idle_expired"], 1);
633 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
635 let mut last = None;
636 for n in 1..=3 {
637 let l = take(&p, &a).await;
638 assert_eq!(l.is_reused(), n > 1);
639 last = p.release(l, true);
640 }
641 assert_eq!(last, Some(EvictionReason::MaxUses));
642 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
644 let l = take(&p, &a).await;
645 tokio::time::advance(Duration::from_millis(1_000)).await;
646 assert_eq!(p.release(l, true), Some(EvictionReason::LifetimeExpired));
647 let l = take(&p, &a).await;
649 p.release(l, true);
650 for _ in 0..11 {
651 tokio::time::advance(Duration::from_millis(95)).await;
652 let l = take(&p, &a).await;
653 if !l.is_reused() {
654 break;
655 }
656 p.release(l, true);
657 }
658 assert!(p.stats().evictions.contains_key("lifetime_expired"));
659 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
661 let l = take(&p, &a).await;
662 let flag = l.conn().unwrap().0.clone();
663 p.release(l, true);
664 flag.store(true, Ordering::SeqCst);
665 assert!(!take(&p, &a).await.is_reused());
666 assert_eq!(p.stats().evictions["closed"], 1);
667 let l = take(&p, &key("F", "o")).await;
669 assert_eq!(p.release(l, false), Some(EvictionReason::Failed));
670 }
671
672 #[tokio::test(flavor = "current_thread", start_paused = true)]
673 async fn idle_total_and_origin_bounds_hold_and_exhaustion_refuses_or_queues() {
674 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
675 let (x, y, z) = (
677 take(&p, &key("X", "o1")).await,
678 take(&p, &key("Y", "o1")).await,
679 take(&p, &key("Z", "o2")).await,
680 );
681 p.release(x, true);
682 tokio::time::advance(Duration::from_millis(1)).await;
683 p.release(y, true);
684 p.release(z, true);
685 assert_eq!(p.stats().idle, 2);
686 assert_eq!(p.stats().evictions["idle_capacity"], 1);
687 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
689 let _a = take(&p, &key("A", "o1")).await;
690 let _b = take(&p, &key("B", "o1")).await;
691 assert!(
692 p.acquire(&key("C", "o1"), &Budget::from_ms(10))
693 .await
694 .is_err()
695 );
696 let _c = take(&p, &key("C", "o2")).await;
698 assert!(
699 p.acquire(&key("D", "o3"), &Budget::from_ms(10))
700 .await
701 .is_err()
702 );
703 let p = ConnectionPool::<Fake>::new(policy()).unwrap();
705 let a = take(&p, &key("A", "o1")).await;
706 let _b = take(&p, &key("B", "o2")).await;
707 let _c = take(&p, &key("C", "o3")).await;
708 p.release(a, true);
709 let d = take(&p, &key("D", "o4")).await;
710 assert!(!d.is_reused());
711 assert_eq!(p.stats().evictions["capacity"], 1);
712
713 let mut q = policy();
715 q.max_connections = 1;
716 q.max_per_origin = 1;
717 q.max_idle = 1;
718 q.on_exhaustion = OnExhaustion::Queue { max_wait_ms: 50 };
719 let p = ConnectionPool::<Fake>::new(q).unwrap();
720 let held = take(&p, &key("A", "o")).await;
721 let t0 = Instant::now();
722 assert!(
723 p.acquire(&key("B", "o"), &Budget::from_ms(10_000))
724 .await
725 .is_err()
726 );
727 assert_eq!(t0.elapsed(), Duration::from_millis(50));
728 let t0 = Instant::now();
729 assert!(
730 p.acquire(&key("B", "o"), &Budget::from_ms(20))
731 .await
732 .is_err()
733 );
734 assert_eq!(
735 t0.elapsed(),
736 Duration::from_millis(20),
737 "never past the budget"
738 );
739 let p2 = p.clone();
740 tokio::spawn(async move {
741 tokio::time::sleep(Duration::from_millis(30)).await;
742 drop(held);
743 });
744 let got = p2.acquire(&key("B", "o"), &Budget::from_ms(10_000)).await;
745 assert!(got.is_ok());
746 let _ = p;
747 }
748
749 #[test]
750 fn every_key_member_changes_the_key_and_unknown_material_disables_reuse() {
751 let base = KeyMaterial {
752 schema: POOL_KEY_SCHEMA.into(),
753 endpoint_class: "c".into(),
754 authority: "http://127.0.0.1:1".into(),
755 protocol: "rustev.remote/1 http/1.1".into(),
756 proxy_route: "direct".into(),
757 tls: Some("plaintext".into()),
758 credential: credential_identity(Some("SYNTHETIC-A")),
759 privacy: BTreeMap::new(),
760 principal_posture: "omitted".into(),
761 principal: None,
762 binding: Some(
763 ArtifactId::parse(
764 "sha256:00000000000000000000000000000000000000000000000000000000000000aa",
765 )
766 .unwrap(),
767 ),
768 };
769 let k = base.key().digest.unwrap();
770 let variants: Vec<KeyMaterial> =
771 vec![
772 KeyMaterial { endpoint_class: "x".into(), ..base.clone() },
773 KeyMaterial { authority: "http://127.0.0.1:2".into(), ..base.clone() },
774 KeyMaterial { protocol: "rustev.remote/2 http/1.1".into(), ..base.clone() },
775 KeyMaterial { proxy_route: "via".into(), ..base.clone() },
776 KeyMaterial { tls: Some("webpki-roots".into()), ..base.clone() },
777 KeyMaterial { credential: credential_identity(Some("SYNTHETIC-B")), ..base.clone() },
778 KeyMaterial { credential: credential_identity(None), ..base.clone() },
779 KeyMaterial { privacy: BTreeMap::from([("k".into(), "v".into())]), ..base.clone() },
780 KeyMaterial { principal_posture: "host_operated m/1 s".into(), ..base.clone() },
781 KeyMaterial { principal: Some("d".into()), ..base.clone() },
782 KeyMaterial {
783 binding: Some(
784 ArtifactId::parse(
785 "sha256:00000000000000000000000000000000000000000000000000000000000000bb",
786 )
787 .unwrap(),
788 ),
789 ..base.clone()
790 },
791 ];
792 for v in variants {
793 assert_ne!(v.key().digest.unwrap(), k, "{v:?}");
794 }
795 assert!(!credential_identity(Some("SYNTHETIC-A")).contains("SYNTHETIC"));
796 for unknown in [
797 KeyMaterial {
798 tls: None,
799 ..base.clone()
800 },
801 KeyMaterial {
802 binding: None,
803 ..base.clone()
804 },
805 KeyMaterial {
806 schema: "rustev.pool-key/0".into(),
807 ..base.clone()
808 },
809 ] {
810 assert_eq!(unknown.key().digest, None);
811 }
812 }
813}