1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
//! [`Capabilities`] -- the per-node consumer API for resolved capability
//! bindings, with `require_*` and `optional_*` accessors.
use super::{Error, ResolvedLocalEntry, ResolvedSharedEntry};
use std::any::TypeId;
use std::collections::HashMap;
/// Per-node capability bindings resolved from the
/// [`CapabilityRegistry`](super::CapabilityRegistry).
///
/// Passed to node factories as `&Capabilities`. Provides type-safe
/// access to extension capabilities via the [`ExtensionCapability`]
/// sealed trait.
///
/// [`ExtensionCapability`]: crate::capability::ExtensionCapability
pub struct Capabilities {
local: HashMap<TypeId, ResolvedLocalEntry>,
shared: HashMap<TypeId, ResolvedSharedEntry>,
}
impl Capabilities {
/// Creates a new `Capabilities` from resolved entries.
pub(crate) fn new(
local: HashMap<TypeId, ResolvedLocalEntry>,
shared: HashMap<TypeId, ResolvedSharedEntry>,
) -> Self {
Capabilities { local, shared }
}
/// Creates an empty `Capabilities` (no bindings).
#[must_use]
pub fn empty() -> Self {
Capabilities {
local: HashMap::new(),
shared: HashMap::new(),
}
}
fn missing_entry_error<C: crate::capability::ExtensionCapability>(
&self,
requested_execution_model: &'static str,
) -> Error {
let id = TypeId::of::<C>();
let extension = self
.local
.get(&id)
.map(|entry| &entry.extension_id)
.or_else(|| self.shared.get(&id).map(|entry| &entry.extension_id));
if let Some(extension) = extension {
let available_execution_model =
match (self.local.contains_key(&id), self.shared.contains_key(&id)) {
(true, false) => "local",
(false, true) => "shared",
(true, true) => "local and shared",
(false, false) => "none",
};
Error::CapabilityExecutionModelMismatch {
capability: C::name().to_owned(),
extension: extension.clone(),
requested_execution_model,
available_execution_model,
}
} else {
Error::CapabilityNotBound {
capability: C::name().to_owned(),
execution_model: requested_execution_model,
}
}
}
/// Resolve a **required** local capability.
///
/// Returns `Box<dyn C::Local>` -- a fresh local trait object minted
/// for this consumer. If the capability was registered by a
/// shared-only extension, the `SharedAsLocal` adapter is returned
/// transparently -- the caller always gets a local trait object.
///
/// Each consumer gets its own boxed instance, mirroring
/// [`Self::require_shared`]. Capabilities that need to share state
/// across consumers (across calls or across nodes that bound the
/// same instance) must use `Rc<RefCell<T>>` (local) or
/// `Arc<Mutex<T>>` (shared) fields explicitly -- there is no
/// implicit fan-out via `Rc::clone`.
///
/// # One-shot contract
///
/// Each resolved entry is one-shot per node: a `require_local`
/// claim consumes the local entry, and a `require_shared` claim
/// consumes the shared entry. Node factories are expected to
/// call each accessor at most once at construction and store the
/// returned handle (wrapping it in `Rc<...>` themselves if they need
/// to fan out within the node).
///
/// The contract is **per-binding**, not per-execution-model: a
/// node claims a binding at most once, regardless of which
/// accessor (`require_local`, `require_shared`, `optional_local`,
/// `optional_shared`) is used. Claiming one execution model
/// invalidates the other on the same node, so a subsequent call
/// to the alternative accessor returns
/// [`Error::CapabilityAlreadyConsumed`].
///
/// This holds uniformly across both binding shapes:
/// - **SharedAsLocal fallback** (extension registered only a
/// shared variant): there is one underlying produce closure,
/// so the shared entry's `Cell::take()` is the single guard.
/// - **Native dual** (extension registered both native local and
/// native shared variants): the two resolved entries are
/// distinct, but a successful claim on either side also takes
/// (and drops, unrun) the alternative entry's produce closure
/// on this node, so the per-binding contract holds without an
/// auxiliary flag.
///
/// # Errors
///
/// - [`Error::CapabilityNotBound`] if no extension is bound to this
/// capability for this node. Either add the binding to the
/// node's capability declaration or switch to
/// [`Self::optional_local`].
/// - [`Error::CapabilityAlreadyConsumed`] if the capability was
/// already claimed on this node.
/// - [`Error::CapabilityExecutionModelMismatch`] if a binding was
/// declared but the extension provides only a local implementation.
///
/// # Panics
///
/// Panics on a type-erasure downcast mismatch -- this indicates a
/// registry bug (the `#[capability]` proc macro guarantees the
/// stored entry's concrete type matches `C::Local`).
pub fn require_local<C: crate::capability::ExtensionCapability>(
&self,
) -> Result<Box<C::Local>, Error> {
let id = TypeId::of::<C>();
// Native local path. `Cell::take()` is the one-shot guard.
if let Some(entry) = self.local.get(&id) {
let produce = entry
.produce
.take()
.ok_or_else(|| Error::CapabilityAlreadyConsumed {
capability: C::name().to_owned(),
})?;
let box_any = produce();
let trait_object = box_any
.downcast::<Box<C::Local>>()
.map(|b| *b)
.unwrap_or_else(|_| {
panic!(
"BUG: capability '{}': local entry type mismatch in registry",
C::name(),
)
});
entry.tracker_consumed.set(true);
// Per-binding one-shot: invalidate the native-shared
// alternative on this node so a subsequent
// `require_shared`/`optional_shared` returns
// `CapabilityAlreadyConsumed`. In the SharedAsLocal
// fallback there is no separate native local entry, so
// this branch does not run for that path -- the shared
// entry's `Cell::take()` below is the single guard.
if let Some(shared_entry) = self.shared.get(&id) {
let _ = shared_entry.produce.take();
}
return Ok(trait_object);
}
// SharedAsLocal fallback. The same `Cell::take()` on the
// shared entry is the binding's one-shot guard, so claiming
// the local-via-fallback accessor here naturally consumes
// the native shared accessor too -- a subsequent
// `require_shared` returns [`Error::CapabilityAlreadyConsumed`].
let entry = self
.shared
.get(&id)
.ok_or_else(|| self.missing_entry_error::<C>("local"))?;
let produce = entry
.produce
.take()
.ok_or_else(|| Error::CapabilityAlreadyConsumed {
capability: C::name().to_owned(),
})?;
let box_any = (entry.adapt_as_local)(produce());
let trait_object = box_any
.downcast::<Box<C::Local>>()
.map(|b| *b)
.unwrap_or_else(|_| {
panic!(
"BUG: capability '{}': SharedAsLocal adapter type mismatch in registry",
C::name(),
)
});
entry.tracker_consumed.set(true);
Ok(trait_object)
}
/// Resolve a **required** shared capability.
///
/// Returns `Box<dyn C::Shared>` -- the extension's `Send + Sync`
/// capability implementation produced for this node.
///
/// # One-shot contract
///
/// See [`Self::require_local`].
///
/// # Errors
///
/// - [`Error::CapabilityNotBound`] if no extension is bound to this
/// capability for this node. Either add the binding to the
/// node's capability declaration or switch to
/// [`Self::optional_shared`].
/// - [`Error::CapabilityAlreadyConsumed`] if the capability was
/// already claimed on this node.
///
/// # Panics
///
/// Panics on a type-erasure downcast mismatch -- this indicates a
/// registry bug (the `#[capability]` proc macro guarantees the
/// stored entry's concrete type matches `C::Shared`).
pub fn require_shared<C: crate::capability::ExtensionCapability>(
&self,
) -> Result<Box<C::Shared>, Error> {
let id = TypeId::of::<C>();
let entry = self
.shared
.get(&id)
.ok_or_else(|| self.missing_entry_error::<C>("shared"))?;
let produce = entry
.produce
.take()
.ok_or_else(|| Error::CapabilityAlreadyConsumed {
capability: C::name().to_owned(),
})?;
let trait_object = produce()
.downcast::<Box<C::Shared>>()
.map(|b| *b)
.unwrap_or_else(|_| {
panic!(
"BUG: capability '{}': shared entry type mismatch in registry",
C::name(),
)
});
entry.tracker_consumed.set(true);
// Per-binding one-shot: invalidate the native-local
// alternative on this node so a subsequent
// `require_local`/`optional_local` returns
// `CapabilityAlreadyConsumed`. In the SharedAsLocal fallback
// (no native local entry) this is a no-op -- the shared
// entry's `Cell::take()` above already serves both sides.
if let Some(local_entry) = self.local.get(&id) {
let _ = local_entry.produce.take();
}
Ok(trait_object)
}
/// Resolve an **optional** local capability.
///
/// Returns `Ok(Some(_))` if bound, `Ok(None)` if the capability
/// was not configured for this node.
///
/// # One-shot contract
///
/// See [`Self::require_local`].
///
/// # Errors
///
/// Returns [`Error::CapabilityAlreadyConsumed`] if the capability was
/// already claimed on this node. A shared-only implementation remains
/// usable through the `SharedAsLocal` adapter.
///
/// # Panics
///
/// Panics on a type-erasure downcast mismatch (registry bug).
pub fn optional_local<C: crate::capability::ExtensionCapability>(
&self,
) -> Result<Option<Box<C::Local>>, Error> {
let id = TypeId::of::<C>();
// A shared entry can satisfy a local request through SharedAsLocal.
if !self.local.contains_key(&id) && !self.shared.contains_key(&id) {
return Ok(None);
}
self.require_local::<C>().map(Some)
}
/// Resolve an **optional** shared capability.
///
/// Returns `Ok(Some(_))` if bound, `Ok(None)` if the capability
/// was not configured for this node.
///
/// # One-shot contract
///
/// See [`Self::require_local`].
///
/// # Errors
///
/// - Returns [`Error::CapabilityAlreadyConsumed`] if the capability was
/// already claimed on this node.
/// - Returns [`Error::CapabilityExecutionModelMismatch`] if a binding was
/// declared but the extension cannot satisfy the shared execution model.
///
/// # Panics
///
/// Panics on a type-erasure downcast mismatch (registry bug).
pub fn optional_shared<C: crate::capability::ExtensionCapability>(
&self,
) -> Result<Option<Box<C::Shared>>, Error> {
let id = TypeId::of::<C>();
if !self.local.contains_key(&id) && !self.shared.contains_key(&id) {
return Ok(None);
}
self.require_shared::<C>().map(Some)
}
}
impl std::fmt::Debug for Capabilities {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Entry values are type-erased; only the shape is printable.
f.debug_struct("Capabilities")
.field("local_bindings", &self.local.len())
.field("shared_bindings", &self.shared.len())
.finish()
}
}