|
45 | 45 | //! ```
|
46 | 46 | use std::collections::HashMap;
|
47 | 47 | use std::marker::PhantomData;
|
| 48 | +use std::sync::atomic::AtomicU64; |
| 49 | +use std::sync::atomic::Ordering; |
48 | 50 |
|
49 | 51 | use super::*;
|
50 | 52 | use crate::attrs::AttrValue;
|
@@ -333,6 +335,8 @@ static LAYERS: LazyLock<Arc<RwLock<Layers>>> = LazyLock::new(|| {
|
333 | 335 | Arc::new(RwLock::new(layers))
|
334 | 336 | });
|
335 | 337 |
|
| 338 | +static OVERRIDE_TOKEN_SEQ: AtomicU64 = AtomicU64::new(1); |
| 339 | + |
336 | 340 | /// Acquire the global configuration lock.
|
337 | 341 | ///
|
338 | 342 | /// This lock serializes all mutations of the global
|
@@ -569,38 +573,67 @@ impl ConfigLock {
|
569 | 573 | key: crate::attrs::Key<T>,
|
570 | 574 | value: T,
|
571 | 575 | ) -> ConfigValueGuard<'a, T> {
|
572 |
| - // Write into the single TestOverride layer (create if |
573 |
| - // needed). |
574 |
| - let (prev_in_layer, orig_env) = { |
575 |
| - let mut guard = LAYERS.write().unwrap(); |
576 |
| - let layer_attrs = ensure_test_override_layer_mut(&mut guard); |
577 |
| - // Save any previous override for this key in the the |
578 |
| - // TestOverride layer. |
579 |
| - let prev = layer_attrs.remove_value(key); |
580 |
| - // Set new override value. |
581 |
| - layer_attrs.set(key, value.clone()); |
582 |
| - // Mirror env var. |
583 |
| - let orig_env = if let Some(cfg) = key.attrs().get(crate::config::CONFIG) { |
584 |
| - if let Some(env_var) = &cfg.env_name { |
585 |
| - let orig = std::env::var(env_var).ok(); |
586 |
| - // SAFETY: this path is used only in tests under ConfigLock |
587 |
| - unsafe { |
588 |
| - std::env::set_var(env_var, value.display()); |
589 |
| - } |
590 |
| - Some((env_var.clone(), orig)) |
591 |
| - } else { |
592 |
| - None |
593 |
| - } |
| 576 | + let token = OVERRIDE_TOKEN_SEQ.fetch_add(1, Ordering::Relaxed); |
| 577 | + |
| 578 | + let mut g = LAYERS.write().unwrap(); |
| 579 | + |
| 580 | + // Ensure TestOverride layer exists. |
| 581 | + let idx = if let Some(i) = test_override_index(&g) { |
| 582 | + i |
| 583 | + } else { |
| 584 | + g.ordered.push(Layer::TestOverride { |
| 585 | + attrs: Attrs::new(), |
| 586 | + stacks: HashMap::new(), |
| 587 | + }); |
| 588 | + g.ordered.sort_by_key(|l| priority(layer_source(l))); |
| 589 | + test_override_index(&g).expect("just inserted TestOverride layer") |
| 590 | + }; |
| 591 | + |
| 592 | + // Mutably access TestOverride's attrs + stacks. |
| 593 | + let (attrs, stacks) = match &mut g.ordered[idx] { |
| 594 | + Layer::TestOverride { attrs, stacks } => (attrs, stacks), |
| 595 | + _ => unreachable!(), |
| 596 | + }; |
| 597 | + |
| 598 | + // Compute env var (if any) for this key once. |
| 599 | + let (env_var, env_str) = if let Some(cfg) = key.attrs().get(crate::config::CONFIG) { |
| 600 | + if let Some(name) = &cfg.env_name { |
| 601 | + (Some(name.clone()), value.display()) |
594 | 602 | } else {
|
595 |
| - None |
596 |
| - }; |
597 |
| - (prev, orig_env) |
| 603 | + (None, String::new()) |
| 604 | + } |
| 605 | + } else { |
| 606 | + (None, String::new()) |
598 | 607 | };
|
599 | 608 |
|
| 609 | + // Get per-key stack (by declared name). |
| 610 | + let key_name = key.name(); |
| 611 | + let stack = stacks.entry(key_name).or_insert_with(|| OverrideStack { |
| 612 | + env_var: env_var.clone(), |
| 613 | + saved_env: env_var.as_ref().and_then(|n| std::env::var(n).ok()), |
| 614 | + frames: Vec::new(), |
| 615 | + }); |
| 616 | + |
| 617 | + // Push the new frame. |
| 618 | + let boxed: Box<dyn crate::attrs::SerializableValue> = Box::new(value.clone()); |
| 619 | + stack.frames.push(OverrideFrame { |
| 620 | + token, |
| 621 | + value: boxed, |
| 622 | + env_str, |
| 623 | + }); |
| 624 | + |
| 625 | + // Make this frame the active value in TestOverride attrs. |
| 626 | + attrs.set(key, value.clone()); |
| 627 | + |
| 628 | + // Update process env to reflect new top-of-stack. |
| 629 | + if let (Some(var), Some(top)) = (stack.env_var.as_ref(), stack.frames.last()) { |
| 630 | + // SAFETY: Under global ConfigLock during tests. |
| 631 | + unsafe { std::env::set_var(var, &top.env_str) } |
| 632 | + } |
| 633 | + |
600 | 634 | ConfigValueGuard {
|
601 | 635 | key,
|
602 |
| - orig: prev_in_layer, // previous value for this key *inside* TestOverride layer |
603 |
| - orig_env, |
| 636 | + token, |
604 | 637 | _phantom: PhantomData,
|
605 | 638 | }
|
606 | 639 | }
|
@@ -629,55 +662,96 @@ impl Drop for ConfigLock {
|
629 | 662 | /// A guard that restores a single configuration value when dropped
|
630 | 663 | pub struct ConfigValueGuard<'a, T: 'static> {
|
631 | 664 | key: crate::attrs::Key<T>,
|
632 |
| - orig: Option<Box<dyn crate::attrs::SerializableValue>>, |
633 |
| - orig_env: Option<(String, Option<String>)>, |
| 665 | + token: u64, |
634 | 666 | // This is here so we can hold onto a 'a lifetime.
|
635 | 667 | _phantom: PhantomData<&'a ()>,
|
636 | 668 | }
|
637 | 669 |
|
638 | 670 | /// When a [`ConfigValueGuard`] is dropped, it restores the
|
639 | 671 | /// configuration state for the key it was guarding:
|
640 | 672 | ///
|
641 |
| -/// - If there was a previous override for this key in the |
642 |
| -/// [`Source::TestOverride`] layer, that value is reinserted. |
643 |
| -/// - If this guard was the only override for the key, the entry is |
644 |
| -/// removed from the layer entirely (leaving underlying layers or |
645 |
| -/// defaults to apply). |
646 |
| -/// - If the key declared a `CONFIG.env_name` (via `@meta(CONFIG = |
647 |
| -/// ConfigAttr { env_name: Some(...), .. })`), the corresponding |
648 |
| -/// process environment variable is restored to its original value |
649 |
| -/// (or removed if it didn't exist). |
| 673 | +/// <insert text here> |
650 | 674 | ///
|
651 | 675 | /// This ensures that overrides applied via
|
652 | 676 | /// [`ConfigLock::override_key`] are always reverted cleanly when the
|
653 | 677 | /// guard is dropped, without leaking state into subsequent tests or
|
654 | 678 | /// callers.
|
655 | 679 | impl<T: 'static> Drop for ConfigValueGuard<'_, T> {
|
656 | 680 | fn drop(&mut self) {
|
657 |
| - let mut guard = LAYERS.write().unwrap(); |
658 |
| - |
659 |
| - if let Some(i) = test_override_index(&guard) { |
660 |
| - let layer_attrs = &mut layer_attrs_mut(&mut guard.ordered[i]); |
| 681 | + let mut g = LAYERS.write().unwrap(); |
| 682 | + let i = if let Some(i) = test_override_index(&g) { |
| 683 | + i |
| 684 | + } else { |
| 685 | + return; |
| 686 | + }; |
661 | 687 |
|
662 |
| - if let Some(prev) = self.orig.take() { |
663 |
| - layer_attrs.insert_value(self.key, prev); |
664 |
| - } else { |
665 |
| - // remove without needing T: AttrValue |
666 |
| - let _ = layer_attrs.remove_value(self.key); |
667 |
| - } |
668 |
| - } |
| 688 | + // Access TestOverride internals |
| 689 | + let (attrs, stacks) = match &mut g.ordered[i] { |
| 690 | + Layer::TestOverride { attrs, stacks } => (attrs, stacks), |
| 691 | + _ => unreachable!("TestOverride index points to non-TestOverride layer"), |
| 692 | + }; |
669 | 693 |
|
670 |
| - if let Some((k, v)) = self.orig_env.take() { |
671 |
| - // SAFETY: only ever used in single-threaded test code and |
672 |
| - // serialized by the global ConfigLock to avoid races |
673 |
| - // between tests. |
674 |
| - unsafe { |
675 |
| - if let Some(v) = v { |
676 |
| - std::env::set_var(k, v); |
| 694 | + let key_name = self.key.name(); |
| 695 | + |
| 696 | + // We need a tiny scope for the &mut borrow of the stack so we |
| 697 | + // can call `stacks.remove(key_name)` afterward if it becomes |
| 698 | + // empty. |
| 699 | + let mut remove_empty_stack = false; |
| 700 | + let mut restore_env_var: Option<String> = None; |
| 701 | + let mut restore_env_to: Option<String> = None; |
| 702 | + |
| 703 | + if let Some(stack) = stacks.get_mut(key_name) { |
| 704 | + // Find this guard's frame by token. |
| 705 | + if let Some(pos) = stack.frames.iter().position(|f| f.token == self.token) { |
| 706 | + let is_top = pos + 1 == stack.frames.len(); |
| 707 | + |
| 708 | + if is_top { |
| 709 | + // Pop the active frame |
| 710 | + stack.frames.pop(); |
| 711 | + |
| 712 | + if let Some(new_top) = stack.frames.last() { |
| 713 | + // New top becomes active: update attrs and env. |
| 714 | + attrs.insert_value(self.key, (*new_top.value).cloned()); |
| 715 | + if let Some(var) = stack.env_var.as_ref() { |
| 716 | + // SAFETY: Under global ConfigLock during tests. |
| 717 | + unsafe { std::env::set_var(var, &new_top.env_str) } |
| 718 | + } |
| 719 | + } else { |
| 720 | + // Stack empty: remove key now, and after we |
| 721 | + // drop the&mut borrow of the stack, restore |
| 722 | + // env and remove the stack entry. |
| 723 | + let _ = attrs.remove_value(self.key); |
| 724 | + |
| 725 | + // Capture restoration details while we still have access to the stack. |
| 726 | + if let Some(var) = stack.env_var.as_ref() { |
| 727 | + restore_env_var = Some(var.clone()); |
| 728 | + restore_env_to = stack.saved_env.clone(); // None => unset |
| 729 | + } |
| 730 | + remove_empty_stack = true |
| 731 | + } |
677 | 732 | } else {
|
678 |
| - std::env::remove_var(&k); |
| 733 | + // Out-of-order drop: remove only that frame: active top stays |
| 734 | + stack.frames.remove(pos); |
| 735 | + // No changes to attrs or env here. |
| 736 | + } |
| 737 | + } // else: token already handled; nothing to do |
| 738 | + } // &must stack borrow ends here |
| 739 | + |
| 740 | + // If we emptied the stack for this key, restore env and drop |
| 741 | + // the stack entry. |
| 742 | + if remove_empty_stack { |
| 743 | + if let Some(var) = restore_env_var.as_ref() { |
| 744 | + // SAFETY: Under global ConfigLock during tests. |
| 745 | + unsafe { |
| 746 | + if let Some(val) = restore_env_to.as_ref() { |
| 747 | + std::env::set_var(var, val); |
| 748 | + } else { |
| 749 | + std::env::remove_var(var); |
| 750 | + } |
679 | 751 | }
|
680 | 752 | }
|
| 753 | + // Now it's safe to remove the stack from the map. |
| 754 | + let _ = stacks.remove(key_name); |
681 | 755 | }
|
682 | 756 | }
|
683 | 757 | }
|
@@ -962,4 +1036,113 @@ mod tests {
|
962 | 1036 | "attrs() should exclude keys without @meta(CONFIG = ...)"
|
963 | 1037 | );
|
964 | 1038 | }
|
| 1039 | + |
| 1040 | + #[test] |
| 1041 | + fn test_testoverride_multiple_stacked_overrides_lifo() { |
| 1042 | + let lock = lock(); |
| 1043 | + reset_to_defaults(); |
| 1044 | + |
| 1045 | + // Baseline sanity. |
| 1046 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30)); |
| 1047 | + |
| 1048 | + // Start from a clean env so we can assert restoration to "unset". |
| 1049 | + // SAFETY: single-threaded tests. |
| 1050 | + unsafe { |
| 1051 | + std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT"); |
| 1052 | + } |
| 1053 | + assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err()); |
| 1054 | + |
| 1055 | + // Stack A: 40s (becomes top) |
| 1056 | + let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40)); |
| 1057 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40)); |
| 1058 | + { |
| 1059 | + let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(); |
| 1060 | + assert_eq!( |
| 1061 | + humantime::parse_duration(&s).unwrap(), |
| 1062 | + Duration::from_secs(40) |
| 1063 | + ); |
| 1064 | + } |
| 1065 | + |
| 1066 | + // Stack B: 50s (new top) |
| 1067 | + let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50)); |
| 1068 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(50)); |
| 1069 | + { |
| 1070 | + let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(); |
| 1071 | + assert_eq!( |
| 1072 | + humantime::parse_duration(&s).unwrap(), |
| 1073 | + Duration::from_secs(50) |
| 1074 | + ); |
| 1075 | + } |
| 1076 | + |
| 1077 | + // Drop B first → should reveal A (LIFO) |
| 1078 | + std::mem::drop(guard_b); |
| 1079 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40)); |
| 1080 | + { |
| 1081 | + let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(); |
| 1082 | + assert_eq!( |
| 1083 | + humantime::parse_duration(&s).unwrap(), |
| 1084 | + Duration::from_secs(40) |
| 1085 | + ); |
| 1086 | + } |
| 1087 | + |
| 1088 | + // Drop A → should restore default and unset env. |
| 1089 | + std::mem::drop(guard_a); |
| 1090 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30)); |
| 1091 | + assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err()); |
| 1092 | + } |
| 1093 | + |
| 1094 | + #[test] |
| 1095 | + fn test_testoverride_out_of_order_drop_keeps_top_stable() { |
| 1096 | + let lock = lock(); |
| 1097 | + reset_to_defaults(); |
| 1098 | + |
| 1099 | + // Clean env baseline. |
| 1100 | + // SAFETY: single-threaded tests. |
| 1101 | + unsafe { |
| 1102 | + std::env::remove_var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT"); |
| 1103 | + } |
| 1104 | + assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err()); |
| 1105 | + |
| 1106 | + // Push three frames in order: A=40s, B=50s, C=70s (C is top). |
| 1107 | + let guard_a = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(40)); |
| 1108 | + let guard_b = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(50)); |
| 1109 | + let guard_c = lock.override_key(MESSAGE_DELIVERY_TIMEOUT, Duration::from_secs(70)); |
| 1110 | + |
| 1111 | + // Top is C. |
| 1112 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70)); |
| 1113 | + { |
| 1114 | + let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(); |
| 1115 | + assert_eq!( |
| 1116 | + humantime::parse_duration(&s).unwrap(), |
| 1117 | + Duration::from_secs(70) |
| 1118 | + ); |
| 1119 | + } |
| 1120 | + |
| 1121 | + // Drop the *middle* frame (B) first → top must remain C, env unchanged. |
| 1122 | + std::mem::drop(guard_b); |
| 1123 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(70)); |
| 1124 | + { |
| 1125 | + let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(); |
| 1126 | + assert_eq!( |
| 1127 | + humantime::parse_duration(&s).unwrap(), |
| 1128 | + Duration::from_secs(70) |
| 1129 | + ); |
| 1130 | + } |
| 1131 | + |
| 1132 | + // Now drop C → A becomes top, env follows A. |
| 1133 | + std::mem::drop(guard_c); |
| 1134 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(40)); |
| 1135 | + { |
| 1136 | + let s = std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").unwrap(); |
| 1137 | + assert_eq!( |
| 1138 | + humantime::parse_duration(&s).unwrap(), |
| 1139 | + Duration::from_secs(40) |
| 1140 | + ); |
| 1141 | + } |
| 1142 | + |
| 1143 | + // Drop A → restore default and clear env. |
| 1144 | + std::mem::drop(guard_a); |
| 1145 | + assert_eq!(get(MESSAGE_DELIVERY_TIMEOUT), Duration::from_secs(30)); |
| 1146 | + assert!(std::env::var("HYPERACTOR_MESSAGE_DELIVERY_TIMEOUT").is_err()); |
| 1147 | + } |
965 | 1148 | }
|
0 commit comments