saluki_core/runtime/state/resources/
mod.rs

1//! External resource management.
2
3use std::{
4    any::{Any, TypeId},
5    fmt, mem,
6    ops::{Deref, DerefMut},
7    sync::{Arc, Mutex},
8};
9
10use async_trait::async_trait;
11use saluki_common::collections::FastHashMap;
12use saluki_error::GenericError;
13use serde::Serialize;
14use snafu::Snafu;
15use stringtheory::MetaString;
16use tracing::{debug, warn};
17
18use crate::{runtime::process::Id as ProcessId, support::SubsystemIdentifier};
19
20mod api;
21pub use self::api::{ResourceRegistryAPIHandler, ResourceRegistryState};
22
23mod worker;
24pub use self::worker::ResourceRegistryWorker;
25
26#[cfg(test)]
27mod tests;
28
29/// The kind of an external resource.
30///
31/// Deliberately a closed set: the registry coordinates a known, bounded collection of scarce things, so introducing a
32/// kind is a design decision that belongs here rather than in a downstream crate. This names the kind only; it implies
33/// no dependency on the types that implement it.
34#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ResourceKind {
37    /// A bound network socket.
38    Socket,
39
40    /// A synthetic kind for exercising cross-kind behavior in tests.
41    #[cfg(test)]
42    Test,
43}
44
45impl ResourceKind {
46    /// Returns the string representation of this kind.
47    pub const fn as_str(&self) -> &'static str {
48        match self {
49            Self::Socket => "socket",
50            #[cfg(test)]
51            Self::Test => "test",
52        }
53    }
54}
55
56impl fmt::Display for ResourceKind {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        f.write_str(self.as_str())
59    }
60}
61
62/// A specification naming one resource, and the blueprint for creating it.
63///
64/// A specification describes *what* to create without creating it, so it can be built and compared long before the
65/// underlying resource exists. It also names exactly one resource type, which is what keeps two callers from acquiring
66/// the same resource as two different types: there is no way to express the mismatch.
67#[async_trait]
68pub trait ResourceSpecification: Clone + fmt::Debug + Send + Sync + 'static {
69    /// Type of the resource this specification creates.
70    type Resource: Send + 'static;
71
72    /// Kind of the resource this specification names.
73    const KIND: ResourceKind;
74
75    /// Returns the key for the resource this specification names.
76    ///
77    /// Keys must be unique within their kind: two specifications of the same kind that yield the same key are
78    /// understood to name the same underlying scarce thing and will conflict with each other. Keys of different kinds
79    /// never collide, so a key only has to distinguish a resource from its siblings.
80    ///
81    /// Only the parts of a specification that identify the underlying thing belong in the key. Settings that do not
82    /// change *which* resource is named should be left out, so that two specifications differing only in their
83    /// settings are correctly recognized as naming the same resource.
84    fn key(&self) -> MetaString;
85
86    /// Creates the resource.
87    ///
88    /// A resource that is only meaningful as a set of underlying handles -- several sockets bound to one address with
89    /// `SO_REUSEPORT`, say -- holds all of them itself. Creating them together in a single call is what makes them
90    /// atomic: if any one fails, this returns an error and nothing is registered.
91    ///
92    /// # Errors
93    ///
94    /// If the resource can't be created, an error is returned and nothing is registered.
95    async fn create(&self) -> Result<Self::Resource, GenericError>;
96
97    /// Prepares a returning resource for its next holder.
98    ///
99    /// Called when a lease is dropped, before the resource becomes available again. Implement this only for a resource
100    /// that accumulates state over the course of a single lease and must start clean for the next one -- a listener
101    /// tracking how many of its pre-bound sockets it has handed out, for example. Everything the resource is *for*,
102    /// such as the sockets themselves, must survive: the point of the registry is that it outlives its holders.
103    ///
104    /// Defaults to doing nothing, which is right for a resource that carries no per-lease state.
105    ///
106    /// This is deliberately infallible. A resource that can't be made fit for reuse should be
107    /// [`discard`][ResourceLease::discard]ed by its holder instead, so the next acquisition builds a fresh one.
108    fn reset(_resource: &mut Self::Resource) {}
109}
110
111/// Erases the specification type so [`Entry`] can reset a resource it only knows as `dyn Any`.
112fn reset_shim<S: ResourceSpecification>(resource: &mut (dyn Any + Send)) {
113    match resource.downcast_mut::<S::Resource>() {
114        Some(resource) => S::reset(resource),
115        None => unreachable!("entry only ever holds the resource type its specification names"),
116    }
117}
118
119/// An error that occurred while acquiring a resource.
120#[derive(Debug, Snafu)]
121#[snafu(context(suffix(false)))]
122pub enum AcquireError {
123    /// The resource is already leased by something else in this process.
124    #[snafu(display(
125        "{} resource '{}' is already leased by '{}' (acquired by process {})",
126        kind,
127        key,
128        owner,
129        acquisition_process_id.as_usize()
130    ))]
131    AlreadyLeased {
132        /// Kind of the resource.
133        kind: ResourceKind,
134
135        /// Key of the resource.
136        key: MetaString,
137
138        /// Rendered identity of the subsystem holding the resource.
139        ///
140        /// This is the authoritative answer to who holds the resource. Rendered rather than kept as a
141        /// [`SubsystemIdentifier`], which stores enough segments inline to make this error large enough to slow down
142        /// every `Result` that carries it.
143        owner: MetaString,
144
145        /// Identifier of the process that acquired the resource.
146        acquisition_process_id: ProcessId,
147    },
148
149    /// The key is registered, but holds a different type of resource.
150    ///
151    /// Two specifications of the same kind produced the same key while naming different resource types, which means
152    /// their keys are not as unique as they need to be.
153    #[snafu(display(
154        "{} resource '{}' is registered as `{}`, but was requested as `{}`",
155        kind,
156        key,
157        existing_type,
158        requested_type
159    ))]
160    TypeMismatch {
161        /// Kind of the resource.
162        kind: ResourceKind,
163
164        /// Key of the resource.
165        key: MetaString,
166
167        /// Type the resource was registered as.
168        existing_type: &'static str,
169
170        /// Type the resource was requested as.
171        requested_type: &'static str,
172    },
173
174    /// The resource could not be created.
175    #[snafu(display("failed to create {} resource '{}': {}", kind, key, source))]
176    CreationFailed {
177        /// Kind of the resource.
178        kind: ResourceKind,
179
180        /// Key of the resource.
181        key: MetaString,
182
183        /// Source of the error.
184        source: GenericError,
185    },
186}
187
188/// Identifies one registry entry.
189///
190/// Keying on the kind as well as the key means a kind namespaces its own keys, so two unrelated resource families can
191/// never collide on a coincidentally equal key. It stays deliberately coarser than the resource type: two
192/// representations of the same scarce thing must collide, and keying by type would let each of them claim it.
193#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
194struct EntryKey {
195    kind: ResourceKind,
196    key: MetaString,
197}
198
199impl EntryKey {
200    fn new<S: ResourceSpecification>(spec: &S) -> Self {
201        Self {
202            kind: S::KIND,
203            key: spec.key(),
204        }
205    }
206}
207
208impl fmt::Display for EntryKey {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        write!(f, "{}:{}", self.kind, self.key)
211    }
212}
213
214/// Identity of whatever currently holds a resource.
215#[derive(Clone, Debug)]
216struct LeaseInfo {
217    owner: SubsystemIdentifier,
218    acquisition_process_id: ProcessId,
219}
220
221impl LeaseInfo {
222    fn from_owner(owner: &SubsystemIdentifier) -> Self {
223        Self {
224            owner: owner.clone(),
225            acquisition_process_id: ProcessId::current(),
226        }
227    }
228}
229
230/// RAII guard to disarm in-progress leases when resource creation fails to run to completion.
231///
232/// [`ResourceRegistry::acquire`] marks an entry as [`EntryState::Creating`] before awaiting
233/// [`ResourceSpecification::create`], and
234/// that await is a cancellation point: a supervisor aborts a worker that is still initializing when shutdown arrives,
235/// so a builder's acquisition can be dropped partway through. Without this guard the claim would outlive the
236/// acquisition and block the key for the life of the process.
237struct ClaimCreationGuard<'a> {
238    registry: &'a ResourceRegistry,
239    key: &'a EntryKey,
240    armed: bool,
241}
242
243impl<'a> ClaimCreationGuard<'a> {
244    /// Creates a new guard for the given key in the armed state.
245    fn from_key(registry: &'a ResourceRegistry, key: &'a EntryKey) -> Self {
246        Self {
247            registry,
248            key,
249            armed: true,
250        }
251    }
252
253    /// Disarm and consume the guard.
254    fn disarm(mut self) {
255        self.armed = false;
256    }
257}
258
259impl Drop for ClaimCreationGuard<'_> {
260    fn drop(&mut self) {
261        if !self.armed {
262            return;
263        }
264
265        let mut state = self.registry.inner.lock().unwrap();
266
267        // Only take back a claim that is still ours to take back.
268        if matches!(
269            state.entries.get(self.key).map(|entry| &entry.state),
270            Some(EntryState::Creating(_))
271        ) {
272            debug!(key = %self.key, "Resource creation was cancelled. Releasing the claim on its key.");
273            state.entries.remove(self.key);
274        }
275    }
276}
277
278/// Lifecycle state of a registry entry.
279enum EntryState {
280    /// The resource is held by the registry and can be acquired.
281    Idle(Box<dyn Any + Send>),
282
283    /// Creation is in flight. The entry holds nothing yet, but is already spoken for.
284    Creating(LeaseInfo),
285
286    /// The resource is lent out.
287    Leased(LeaseInfo),
288}
289
290impl EntryState {
291    fn as_str(&self) -> &'static str {
292        match self {
293            Self::Idle(_) => "idle",
294            Self::Creating(_) => "creating",
295            Self::Leased(_) => "leased",
296        }
297    }
298
299    fn holder(&self) -> Option<&LeaseInfo> {
300        match self {
301            Self::Idle(_) => None,
302            Self::Creating(info) | Self::Leased(info) => Some(info),
303        }
304    }
305}
306
307/// One resource registered under a single key.
308struct Entry {
309    type_id: TypeId,
310    type_name: &'static str,
311    reset: fn(&mut (dyn Any + Send)),
312    spec_desc: String,
313    state: EntryState,
314    acquisitions: u64,
315}
316
317impl Entry {
318    fn new<S: ResourceSpecification>(spec: &S, lease_info: LeaseInfo) -> Self {
319        Self {
320            type_id: TypeId::of::<S::Resource>(),
321            type_name: std::any::type_name::<S::Resource>(),
322            reset: reset_shim::<S>,
323            spec_desc: format!("{:?}", spec),
324            state: EntryState::Creating(lease_info),
325            acquisitions: 0,
326        }
327    }
328
329    /// Hands out an idle resource, or explains why it can't.
330    fn lease<S: ResourceSpecification>(
331        &mut self, registry: &ResourceRegistry, key: &EntryKey, new_spec: &S, lease_info: LeaseInfo,
332    ) -> Result<ResourceLease<S::Resource>, AcquireError> {
333        if self.type_id != TypeId::of::<S::Resource>() {
334            return Err(AcquireError::TypeMismatch {
335                kind: S::KIND,
336                key: key.key.clone(),
337                existing_type: self.type_name,
338                requested_type: std::any::type_name::<S::Resource>(),
339            });
340        }
341
342        if let Some(holder) = self.state.holder() {
343            return Err(AcquireError::AlreadyLeased {
344                kind: S::KIND,
345                key: key.key.clone(),
346                owner: MetaString::from(holder.owner.to_string()),
347                acquisition_process_id: holder.acquisition_process_id,
348            });
349        }
350
351        // The key identifies the resource, so a specification differing only in its settings still names this same
352        // resource. Hand back what exists rather than rebuilding it, but say so, since the new settings have no effect.
353        let new_spec_desc = format!("{:?}", new_spec);
354        if self.spec_desc != new_spec_desc {
355            warn!(
356                %key,
357                existing = %self.spec_desc,
358                requested = %new_spec_desc,
359                "Resource acquired with a different specification than it was created with. Using the existing resource; \
360                 the requested specification has no effect."
361            );
362        }
363
364        // `holder` returned `None` just above, so the entry is idle and holds its value.
365        let value = match mem::replace(&mut self.state, EntryState::Leased(lease_info)) {
366            EntryState::Idle(value) => value,
367            _ => unreachable!("entry without a holder is idle"),
368        };
369
370        self.acquisitions += 1;
371
372        Ok(ResourceLease {
373            value: Some(*value.downcast::<S::Resource>().expect("entry type checked above")),
374            registry: registry.clone(),
375            key: key.clone(),
376        })
377    }
378}
379
380#[derive(Default)]
381struct RegistryState {
382    entries: FastHashMap<EntryKey, Entry>,
383}
384
385impl RegistryState {
386    fn snapshot(&self) -> Vec<ResourceStatus> {
387        let mut statuses = self
388            .entries
389            .iter()
390            .map(|(key, entry)| ResourceStatus {
391                kind: key.kind,
392                key: key.key.to_string(),
393                spec: entry.spec_desc.clone(),
394                state: entry.state.as_str(),
395                owner: entry.state.holder().map(|info| info.owner.to_string()),
396                acquisition_process_id: entry.state.holder().map(|info| info.acquisition_process_id.as_usize()),
397                acquisitions: entry.acquisitions,
398            })
399            .collect::<Vec<_>>();
400        statuses.sort_by(|a, b| (a.kind, &a.key).cmp(&(b.kind, &b.key)));
401
402        statuses
403    }
404}
405
406/// A registry for scarce, externally backed resources.
407///
408/// In many cases, data planes will have to interact with the outside world by way of exposing network endpoints, or
409/// exposing files, and so on... referred to here as "resources." These resources are unique, or are conceptually meant
410/// to be unique: there should be no other OS processes trying to take ownership of them, and only one part of the code
411/// in the data plane should own them.
412///
413/// A [`ResourceRegistry`] owns those resources on behalf of the entire data plane and lends them out. A child process
414/// never owns a resource, but instead holds a [`ResourceLease`]. When the lease drops -- including when the child
415/// process holding it dies -- the resource returns to the registry intact and still live, ready for the next acquirer.
416/// Since the registry outlives the components that use its resources, a component can be torn down and rebuilt without
417/// the underlying resource being automatically released back to the operating system due to typical Rust drop
418/// semantics.
419///
420/// # Groups
421///
422/// Resources are named by a [`ResourceSpecification`], which provides the blueprint for how to create a particular
423/// resource, such as a network socket, when a caller attempts to acquire it. Resource specifications are generally tied
424/// one-to-one with a particular type.
425///
426/// The specification provides both the information necessary to properly determine one unique resource from
427/// another, as well as a mechanism for consistent creation of potentially complex resources, including asynchronous
428/// initialization.
429///
430/// # Keys and conflicts
431///
432/// Entries are keyed by kind and key together, deliberately *not* by resource type. Two different Rust types can
433/// easily describe the same scarce thing -- a connection-oriented listener and a general one over the same address --
434/// and keying by type would let each of them claim it. Kind is coarse enough that such representations still collide,
435/// while keeping unrelated resource families from colliding on a coincidentally equal key. A key therefore only has to
436/// be unique within its own kind.
437#[derive(Clone, Default)]
438pub struct ResourceRegistry {
439    inner: Arc<Mutex<RegistryState>>,
440}
441
442impl ResourceRegistry {
443    /// Creates an empty registry.
444    pub fn new() -> Self {
445        Self::default()
446    }
447
448    /// Acquires the resource named by `spec`, creating it if it isn't registered yet.
449    ///
450    /// `owner` identifies the subsystem taking the lease and is recorded, alongside the current process identifier, for
451    /// accounting.
452    ///
453    /// If the resource is already registered, the existing one is handed back rather than a new one being created. This
454    /// is the mechanism by which a resource outlives the components that use it.
455    ///
456    /// # Errors
457    ///
458    /// If the resource is already leased, if the key is registered to a different type of resource, or if creation
459    /// fails, an error is returned.
460    pub async fn acquire<S: ResourceSpecification>(
461        &self, owner: &SubsystemIdentifier, spec: S,
462    ) -> Result<ResourceLease<S::Resource>, AcquireError> {
463        let key = EntryKey::new(&spec);
464        let lease_info = LeaseInfo::from_owner(owner);
465
466        // Attempt to lease the resource if it's already registered.
467        //
468        // Otherwise, start the registration process by insert an uninitialized entry that gives us lease ownership
469        // prior to actually creating the resource and finalizing it.
470        {
471            let mut state = self.inner.lock().unwrap();
472            if let Some(entry) = state.entries.get_mut(&key) {
473                return entry.lease(self, &key, &spec, lease_info);
474            }
475
476            let new_entry = Entry::new(&spec, lease_info.clone());
477            state.entries.insert(key.clone(), new_entry);
478        }
479
480        // Create the resource.
481        //
482        // We establish a "creation guard" which is a drop guard that ensures we remove our pending entry if we fail to
483        // create the resource, including if this asynchronous call is cancelled, so that we don't permanently tie up
484        // the resource in an uninitialized state.
485        let claim_guard = ClaimCreationGuard::from_key(self, &key);
486        let created = spec.create().await;
487        claim_guard.disarm();
488
489        let mut state = self.inner.lock().unwrap();
490        match created {
491            Ok(value) => {
492                let entry = state
493                    .entries
494                    .get_mut(&key)
495                    .expect("entry was inserted before creation and is only removed by this function");
496
497                entry.acquisitions += 1;
498                entry.state = EntryState::Leased(lease_info);
499
500                debug!(%key, %owner, "Created resource.");
501
502                Ok(ResourceLease {
503                    value: Some(value),
504                    registry: self.clone(),
505                    key,
506                })
507            }
508            Err(source) => {
509                // Drop the claim so that a later acquire can retry.
510                state.entries.remove(&key);
511                Err(AcquireError::CreationFailed {
512                    kind: S::KIND,
513                    key: key.key,
514                    source,
515                })
516            }
517        }
518    }
519
520    /// Returns a snapshot of every registered resource, ordered by key.
521    pub fn snapshot(&self) -> Vec<ResourceStatus> {
522        let state = self.inner.lock().unwrap();
523        state.snapshot()
524    }
525
526    /// Creates an API handler for reporting the state of all registered resources.
527    pub fn api_handler(&self) -> ResourceRegistryAPIHandler {
528        ResourceRegistryAPIHandler::from_registry(self.clone())
529    }
530
531    /// Creates a [`ResourceRegistryWorker`] that publishes the registry over the control plane.
532    pub fn worker(&self) -> ResourceRegistryWorker {
533        ResourceRegistryWorker::new(self.clone())
534    }
535
536    /// Returns a resource to the registry, marking its entry idle.
537    fn return_value(&self, key: &EntryKey, mut value: Box<dyn Any + Send>) {
538        let mut state = self.inner.lock().unwrap();
539        if let Some(entry) = state.entries.get_mut(key) {
540            debug!(%key, "Resource returned to registry.");
541
542            // Reset on the way in rather than on the way out, so the registry is never holding a half-consumed
543            // resource that a snapshot could observe.
544            (entry.reset)(&mut *value);
545            entry.state = EntryState::Idle(value);
546        }
547    }
548
549    /// Drops a resource instead of returning it, so that the next acquisition creates a fresh one.
550    fn discard_value(&self, key: &EntryKey) {
551        let mut state = self.inner.lock().unwrap();
552        if state.entries.remove(key).is_some() {
553            debug!(%key, "Resource discarded; it will be recreated on the next acquisition.");
554        }
555    }
556}
557
558/// Reported state of a single resource.
559#[derive(Clone, Debug, Serialize)]
560pub struct ResourceStatus {
561    /// Kind of the resource.
562    pub kind: ResourceKind,
563
564    /// Key the resource is registered under, unique within its kind.
565    pub key: String,
566
567    /// Rendered specification the resource was created from.
568    pub spec: String,
569
570    /// Lifecycle state of the resource: `idle`, `creating`, or `leased`.
571    pub state: &'static str,
572
573    /// Subsystem holding the resource, if any.
574    ///
575    /// This is the authoritative answer to who holds the resource.
576    pub owner: Option<String>,
577
578    /// Process that acquired the resource, if any.
579    ///
580    /// This is the process that ran the acquisition, which is not necessarily the one using the resource now: a
581    /// component acquires while it is being built, and only afterwards does it get a process of its own.
582    ///
583    /// Use [`owner`][Self::owner] to identify the holder.
584    pub acquisition_process_id: Option<usize>,
585
586    /// Number of times the resource has been acquired.
587    pub acquisitions: u64,
588}
589
590/// An exclusive lease on a resource.
591///
592/// Dereferences to the resource itself. Dropping the lease returns the resource to the registry still live, so a lease
593/// is a loan, never ownership. See [`ResourceRegistry`] for the full model.
594pub struct ResourceLease<R: Send + 'static> {
595    value: Option<R>,
596    registry: ResourceRegistry,
597    key: EntryKey,
598}
599
600impl<R: Send + 'static> ResourceLease<R> {
601    /// Returns the kind of this resource.
602    pub fn kind(&self) -> ResourceKind {
603        self.key.kind
604    }
605
606    /// Returns the key this resource is registered under, unique within its kind.
607    pub fn key(&self) -> &MetaString {
608        &self.key.key
609    }
610
611    /// Returns the resource to the registry.
612    ///
613    /// Equivalent to dropping the lease; useful where the return should be obvious at the call site.
614    pub fn release(self) {}
615
616    /// Drops the resource instead of returning it, so the next acquisition creates a fresh one.
617    ///
618    /// Use this when the resource has hit an error it can't recover from and handing it to the next acquirer would pass
619    /// the problem along.
620    pub fn discard(mut self) {
621        // Dropping the value here is the point: it is what releases the underlying resource.
622        let _ = self.value.take();
623        self.registry.discard_value(&self.key);
624    }
625}
626
627impl<R: Send + 'static> Deref for ResourceLease<R> {
628    type Target = R;
629
630    fn deref(&self) -> &Self::Target {
631        self.value.as_ref().expect("lease holds its value until dropped")
632    }
633}
634
635impl<R: Send + 'static> DerefMut for ResourceLease<R> {
636    fn deref_mut(&mut self) -> &mut Self::Target {
637        self.value.as_mut().expect("lease holds its value until dropped")
638    }
639}
640
641impl<R: Send + 'static> Drop for ResourceLease<R> {
642    fn drop(&mut self) {
643        if let Some(value) = self.value.take() {
644            self.registry.return_value(&self.key, Box::new(value));
645        }
646    }
647}
648
649impl<R: Send + fmt::Debug + 'static> fmt::Debug for ResourceLease<R> {
650    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
651        f.debug_struct("ResourceLease")
652            .field("key", &self.key)
653            .field("value", &self.value)
654            .finish()
655    }
656}