GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
tests/style_registry_reentrancy.rs (4.7K)
1 //! No style getter may read the registry while it already holds a read of it.
2 //!
3 //! `STYLE_REGISTRY` is a `std::sync::RwLock`, and std's lock queues new readers
4 //! behind a WAITING writer. A getter written as
5 //!
6 //! ```ignore
7 //! get_style_registry().read().unwrap().get_float("toggle_corner_radius")
8 //! .unwrap_or_else(control_corner_radius)
9 //! ```
10 //!
11 //! keeps its guard alive to the end of the statement, so the fallback's own
12 //! read is a second read on the same thread. A writer that arrives between
13 //! the two waits for the first, the second waits for the writer, and every
14 //! reader in the process then queues behind both. That hung
15 //! `cargo test -p cce-designer` (2026-09-25): ~40 test threads parked in
16 //! `RwLock::read_contended` on `STYLE_REGISTRY`, the writer being the
17 //! designer's `reload_config` (one write per config line, on every
18 //! `State::new`) and the reader any unset corner radius.
19 //!
20 //! An integration test on purpose: here cce-ui is built WITHOUT `cfg(test)`,
21 //! as it is inside every app's suite, so the per-thread style overlay the unit
22 //! tests get is out of the way and the getters hit the shared lock.
23 //!
24 //! The stress runs in a CHILD process — this binary re-executed — because a
25 //! deadlock on the process-wide registry cannot be recovered from in-process:
26 //! the parent kills the child at a deadline and fails, rather than hanging.
27
28 use std::process::{Command, Stdio};
29 use std::sync::atomic::{AtomicBool, Ordering};
30 use std::sync::Arc;
31 use std::time::{Duration, Instant};
32
33 use cce_ui::layout;
34
35 const CHILD_ENV: &str = "CCE_UI_REENTRANCY_CHILD";
36
37 /// Every getter whose unset slot falls back to another style getter — the
38 /// shape that nests. With an empty config each of them takes the fallback.
39 const FALLBACK_GETTERS: &[fn() -> f32] = &[
40 layout::toggle_corner_radius,
41 layout::slider_corner_radius,
42 layout::color_selector_preview_corner_radius,
43 layout::color_selector_corner_radius,
44 layout::button_corner_radius,
45 layout::spinbox_corner_radius,
46 layout::textbox_corner_radius,
47 layout::list_corner_radius,
48 layout::tree_corner_radius,
49 layout::font_selector_corner_radius,
50 layout::menu_corner_radius,
51 layout::dropdown_corner_radius,
52 ];
53
54 /// The child: one thread takes and drops the write lock as fast as it can,
55 /// the others call every fallback getter. Against a nested read this parks
56 /// within milliseconds; without one it runs out its time and exits.
57 fn hammer() {
58 // Initialise (reload_config runs once) before the race starts, so the
59 // writer is the only writer.
60 for g in FALLBACK_GETTERS {
61 g();
62 }
63 let stop = Arc::new(AtomicBool::new(false));
64 let writer = {
65 let stop = stop.clone();
66 std::thread::spawn(move || {
67 while !stop.load(Ordering::Relaxed) {
68 drop(layout::get_style_registry().write().unwrap());
69 }
70 })
71 };
72 let readers: Vec<_> = (0..4)
73 .map(|_| {
74 std::thread::spawn(|| {
75 let until = Instant::now() + Duration::from_secs(2);
76 while Instant::now() < until {
77 for g in FALLBACK_GETTERS {
78 std::hint::black_box(g());
79 }
80 }
81 })
82 })
83 .collect();
84 for r in readers {
85 r.join().unwrap();
86 }
87 stop.store(true, Ordering::Relaxed);
88 writer.join().unwrap();
89 }
90
91 #[test]
92 fn fallback_getters_never_nest_a_registry_read() {
93 if std::env::var_os(CHILD_ENV).is_some() {
94 hammer();
95 return;
96 }
97
98 // An empty config, so no slot is set and every getter falls back. The
99 // directory is never created; an absent config.kdl reads as no config.
100 let config_home = std::env::temp_dir().join(format!("cce-ui-reentrancy-{}", std::process::id()));
101 let mut child = Command::new(std::env::current_exe().unwrap())
102 .args(["--exact", "fallback_getters_never_nest_a_registry_read", "--test-threads=1", "-q"])
103 .env(CHILD_ENV, "1")
104 .env("XDG_CONFIG_HOME", &config_home)
105 .stdout(Stdio::null())
106 .stderr(Stdio::inherit())
107 .spawn()
108 .expect("re-execute the test binary");
109
110 let deadline = Instant::now() + Duration::from_secs(30);
111 loop {
112 if let Some(status) = child.try_wait().unwrap() {
113 assert!(status.success(), "the stress child failed: {status}");
114 return;
115 }
116 if Instant::now() > deadline {
117 let _ = child.kill();
118 let _ = child.wait();
119 panic!(
120 "style getters deadlocked on STYLE_REGISTRY: a getter read the registry while \
121 holding a read of it, and a queued writer parked both"
122 );
123 }
124 std::thread::sleep(Duration::from_millis(50));
125 }
126 }