Skip to main content

opendev_runtime/
lazy_init.rs

1//! Lazy initialization for expensive subsystems (#50).
2//!
3//! Uses `tokio::sync::OnceCell` to defer initialization of heavy subsystems
4//! (LSP, MCP, embeddings) until first use rather than at startup.
5
6use std::fmt;
7use std::future::Future;
8use std::sync::Arc;
9
10use tokio::sync::OnceCell;
11use tracing::debug;
12
13/// A lazily-initialized subsystem value.
14///
15/// The inner `T` is initialized on the first call to [`LazySubsystem::get`]
16/// or [`LazySubsystem::get_or_try_init`].  Subsequent calls return the
17/// cached value without re-running the initializer.
18///
19/// This is a thin, ergonomic wrapper around `tokio::sync::OnceCell` that
20/// adds logging and a human-readable subsystem name.
21pub struct LazySubsystem<T: Send + Sync + 'static> {
22    name: &'static str,
23    cell: Arc<OnceCell<T>>,
24}
25
26impl<T: Send + Sync + 'static> LazySubsystem<T> {
27    /// Create a new lazy subsystem with the given human-readable `name`.
28    pub fn new(name: &'static str) -> Self {
29        Self {
30            name,
31            cell: Arc::new(OnceCell::new()),
32        }
33    }
34
35    /// Get the value, initializing it with `init` if necessary.
36    ///
37    /// The `init` future runs at most once, even under concurrent access.
38    pub async fn get<F, Fut>(&self, init: F) -> &T
39    where
40        F: FnOnce() -> Fut,
41        Fut: Future<Output = T>,
42    {
43        self.cell
44            .get_or_init(|| async {
45                debug!("Lazy-initializing subsystem: {}", self.name);
46                let value = init().await;
47                debug!("Subsystem {} initialized", self.name);
48                value
49            })
50            .await
51    }
52
53    /// Get the value, initializing with a fallible `init` if necessary.
54    ///
55    /// If `init` returns an error, the cell remains uninitialized and future
56    /// calls will retry.
57    pub async fn get_or_try_init<F, Fut, E>(&self, init: F) -> Result<&T, E>
58    where
59        F: FnOnce() -> Fut,
60        Fut: Future<Output = Result<T, E>>,
61    {
62        let name = self.name;
63        self.cell
64            .get_or_try_init(|| async {
65                debug!("Lazy-initializing subsystem (fallible): {name}");
66                let value = init().await?;
67                debug!("Subsystem {name} initialized");
68                Ok(value)
69            })
70            .await
71    }
72
73    /// Check whether the subsystem has been initialized.
74    pub fn is_initialized(&self) -> bool {
75        self.cell.initialized()
76    }
77
78    /// Return the value if already initialized, without triggering init.
79    pub fn try_get(&self) -> Option<&T> {
80        self.cell.get()
81    }
82
83    /// The human-readable name of this subsystem.
84    pub fn name(&self) -> &'static str {
85        self.name
86    }
87}
88
89impl<T: Send + Sync + 'static> Clone for LazySubsystem<T> {
90    fn clone(&self) -> Self {
91        Self {
92            name: self.name,
93            cell: Arc::clone(&self.cell),
94        }
95    }
96}
97
98impl<T: Send + Sync + fmt::Debug + 'static> fmt::Debug for LazySubsystem<T> {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        f.debug_struct("LazySubsystem")
101            .field("name", &self.name)
102            .field("initialized", &self.is_initialized())
103            .finish()
104    }
105}
106
107// ---------------------------------------------------------------------------
108// Convenience type aliases for the three named subsystems
109// ---------------------------------------------------------------------------
110
111/// Lazy-init wrapper for the LSP subsystem.
112pub type LazyLsp<T> = LazySubsystem<T>;
113
114/// Lazy-init wrapper for the MCP subsystem.
115pub type LazyMcp<T> = LazySubsystem<T>;
116
117/// Lazy-init wrapper for the embeddings subsystem.
118pub type LazyEmbeddings<T> = LazySubsystem<T>;
119
120/// Create standard lazy wrappers for LSP, MCP, and embeddings.
121pub fn create_lazy_subsystems<L, M, E>() -> (LazyLsp<L>, LazyMcp<M>, LazyEmbeddings<E>)
122where
123    L: Send + Sync + 'static,
124    M: Send + Sync + 'static,
125    E: Send + Sync + 'static,
126{
127    (
128        LazySubsystem::new("LSP"),
129        LazySubsystem::new("MCP"),
130        LazySubsystem::new("Embeddings"),
131    )
132}
133
134// ---------------------------------------------------------------------------
135// SyncLazy — for non-async contexts using std::sync::OnceLock
136// ---------------------------------------------------------------------------
137
138/// A synchronous lazy-init wrapper using `std::sync::OnceLock`.
139///
140/// Useful for subsystems that can be initialized without async.
141pub struct SyncLazy<T: Send + Sync + 'static> {
142    name: &'static str,
143    cell: std::sync::OnceLock<T>,
144}
145
146impl<T: Send + Sync + 'static> SyncLazy<T> {
147    /// Create a new synchronous lazy subsystem.
148    pub const fn new(name: &'static str) -> Self {
149        Self {
150            name,
151            cell: std::sync::OnceLock::new(),
152        }
153    }
154
155    /// Get the value, initializing with `init` if necessary.
156    pub fn get_or_init(&self, init: impl FnOnce() -> T) -> &T {
157        self.cell.get_or_init(|| {
158            debug!("Sync lazy-init: {}", self.name);
159            init()
160        })
161    }
162
163    /// Check whether initialized.
164    pub fn is_initialized(&self) -> bool {
165        self.cell.get().is_some()
166    }
167
168    /// Return the value if already initialized.
169    pub fn try_get(&self) -> Option<&T> {
170        self.cell.get()
171    }
172
173    /// The human-readable name.
174    pub fn name(&self) -> &'static str {
175        self.name
176    }
177}
178
179impl<T: Send + Sync + fmt::Debug + 'static> fmt::Debug for SyncLazy<T> {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        f.debug_struct("SyncLazy")
182            .field("name", &self.name)
183            .field("initialized", &self.is_initialized())
184            .finish()
185    }
186}
187
188#[cfg(test)]
189#[path = "lazy_init_tests.rs"]
190mod tests;