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}