1use std::marker::PhantomData;
2use std::num::NonZeroU64;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(1);
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
8#[error("runtime identity space is exhausted")]
9pub struct IdExhausted;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
12#[error("runtime identity must be nonzero")]
13pub struct InvalidRuntimeId;
14
15macro_rules! scalar_id {
16 ($name:ident) => {
17 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
18 pub struct $name(NonZeroU64);
19
20 impl $name {
21 pub fn get(self) -> u64 {
22 self.0.get()
23 }
24 }
25
26 impl private::Sealed for $name {
27 fn from_nonzero(value: NonZeroU64) -> Self {
28 Self(value)
29 }
30 }
31 };
32}
33
34scalar_id!(TaskId);
35scalar_id!(ScopeId);
36scalar_id!(WaitId);
37scalar_id!(WaitGeneration);
38scalar_id!(OperationId);
39scalar_id!(SettlementSeq);
40scalar_id!(CompletionKind);
41
42impl TaskId {
43 pub fn try_from_raw(raw: u64) -> Result<Self, InvalidRuntimeId> {
44 NonZeroU64::new(raw).map(Self).ok_or(InvalidRuntimeId)
45 }
46}
47
48impl CompletionKind {
49 pub fn try_from_raw(raw: u64) -> Result<Self, InvalidRuntimeId> {
50 NonZeroU64::new(raw).map(Self).ok_or(InvalidRuntimeId)
51 }
52}
53
54#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55pub struct RuntimeId(NonZeroU64);
56
57impl RuntimeId {
58 pub(crate) fn allocate() -> Result<Self, IdExhausted> {
59 allocate_atomic(&NEXT_RUNTIME_ID).map(Self)
60 }
61
62 pub fn get(self) -> u64 {
63 self.0.get()
64 }
65}
66
67#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
73pub struct RuntimeTaskId {
74 runtime: RuntimeId,
75 task: TaskId,
76}
77
78impl RuntimeTaskId {
79 pub fn new(runtime: RuntimeId, task: TaskId) -> Self {
80 Self { runtime, task }
81 }
82
83 pub fn runtime(self) -> RuntimeId {
84 self.runtime
85 }
86
87 pub fn task(self) -> TaskId {
88 self.task
89 }
90}
91
92fn allocate_atomic(counter: &AtomicU64) -> Result<NonZeroU64, IdExhausted> {
93 let raw = counter
94 .fetch_update(
95 Ordering::Relaxed,
96 Ordering::Relaxed,
97 |current| match current {
98 0 => None,
99 u64::MAX => Some(0),
100 value => Some(value + 1),
101 },
102 )
103 .map_err(|_| IdExhausted)?;
104 NonZeroU64::new(raw).ok_or(IdExhausted)
105}
106
107mod private {
108 use super::RuntimeId;
109 use std::num::NonZeroU64;
110
111 pub trait Sealed: Sized {
112 fn from_nonzero(value: NonZeroU64) -> Self;
113 }
114
115 pub trait ScopedSealed: Sized {
116 fn from_parts(runtime: RuntimeId, local: NonZeroU64) -> Self;
117 }
118}
119
120#[doc(hidden)]
121pub trait RuntimeIdType: private::Sealed {}
122
123impl<T: private::Sealed> RuntimeIdType for T {}
124
125#[derive(Clone, Debug)]
126pub struct IdCounter<I> {
127 next: Option<NonZeroU64>,
128 marker: PhantomData<fn() -> I>,
129}
130
131impl<I: RuntimeIdType> Default for IdCounter<I> {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137impl<I: RuntimeIdType> IdCounter<I> {
138 pub fn new() -> Self {
139 Self {
140 next: NonZeroU64::new(1),
141 marker: PhantomData,
142 }
143 }
144
145 pub fn allocate(&mut self) -> Result<I, IdExhausted> {
146 let current = self.next.ok_or(IdExhausted)?;
147 self.next = current.get().checked_add(1).and_then(NonZeroU64::new);
148 Ok(I::from_nonzero(current))
149 }
150
151 pub fn is_exhausted(&self) -> bool {
152 self.next.is_none()
153 }
154
155 #[cfg(test)]
156 fn starting_at(next: u64) -> Self {
157 Self {
158 next: NonZeroU64::new(next),
159 marker: PhantomData,
160 }
161 }
162}
163
164macro_rules! scoped_id {
165 ($name:ident) => {
166 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
167 pub struct $name {
168 runtime: RuntimeId,
169 local: NonZeroU64,
170 }
171
172 impl $name {
173 pub fn runtime(self) -> RuntimeId {
174 self.runtime
175 }
176
177 pub fn local(self) -> u64 {
178 self.local.get()
179 }
180
181 pub fn get(self) -> u64 {
182 self.local.get()
183 }
184 }
185 };
186}
187
188scoped_id!(RootId);
189scoped_id!(PromiseId);
190scoped_id!(ChannelId);
191scoped_id!(ResourceGateId);
192
193#[doc(hidden)]
194pub trait RuntimeScopedIdType: private::ScopedSealed {}
195
196impl<T: private::ScopedSealed> RuntimeScopedIdType for T {}
197
198macro_rules! scoped_id_type {
199 ($name:ident) => {
200 impl private::ScopedSealed for $name {
201 fn from_parts(runtime: RuntimeId, local: NonZeroU64) -> Self {
202 Self { runtime, local }
203 }
204 }
205 };
206}
207
208scoped_id_type!(RootId);
209scoped_id_type!(PromiseId);
210scoped_id_type!(ChannelId);
211scoped_id_type!(ResourceGateId);
212
213#[derive(Debug)]
214pub struct RuntimeScopedIdCounter<I> {
215 runtime: RuntimeId,
216 local: IdCounter<NonZeroU64>,
217 marker: PhantomData<fn() -> I>,
218}
219
220impl private::Sealed for NonZeroU64 {
221 fn from_nonzero(value: NonZeroU64) -> Self {
222 value
223 }
224}
225
226impl<I: RuntimeScopedIdType> RuntimeScopedIdCounter<I> {
227 pub fn new(runtime: RuntimeId) -> Self {
233 Self {
234 runtime,
235 local: IdCounter::new(),
236 marker: PhantomData,
237 }
238 }
239
240 pub fn allocate(&mut self) -> Result<I, IdExhausted> {
241 self.local
242 .allocate()
243 .map(|local| <I as private::ScopedSealed>::from_parts(self.runtime, local))
244 }
245
246 pub fn is_exhausted(&self) -> bool {
247 self.local.is_exhausted()
248 }
249}
250
251#[doc(hidden)]
256pub struct RuntimeScopedIdIssuers {
257 root: RuntimeScopedIdCounter<RootId>,
258 promise: RuntimeScopedIdCounter<PromiseId>,
259 channel: RuntimeScopedIdCounter<ChannelId>,
260}
261
262impl RuntimeScopedIdIssuers {
263 pub(crate) fn new(runtime: RuntimeId) -> Self {
264 Self {
265 root: RuntimeScopedIdCounter::new(runtime),
266 promise: RuntimeScopedIdCounter::new(runtime),
267 channel: RuntimeScopedIdCounter::new(runtime),
268 }
269 }
270
271 #[doc(hidden)]
272 pub fn into_parts(
273 self,
274 ) -> (
275 RuntimeScopedIdCounter<RootId>,
276 RuntimeScopedIdCounter<PromiseId>,
277 RuntimeScopedIdCounter<ChannelId>,
278 ) {
279 (self.root, self.promise, self.channel)
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::{SemaError, Value};
287 use std::collections::BTreeMap;
288
289 #[test]
290 fn counter_starts_at_one() {
291 let mut counter = IdCounter::<TaskId>::new();
292 assert_eq!(counter.allocate().expect("ID available").get(), 1);
293 }
294
295 #[test]
296 fn counter_issues_max_once_then_stays_exhausted() {
297 let mut counter = IdCounter::<TaskId>::starting_at(u64::MAX);
298 assert_eq!(
299 counter.allocate().expect("last ID available").get(),
300 u64::MAX
301 );
302 assert_eq!(counter.allocate(), Err(IdExhausted));
303 assert_eq!(counter.allocate(), Err(IdExhausted));
304 }
305
306 #[test]
307 fn condition_ids_are_emitted_as_integers() {
308 let big: u64 = 1_000_000_000_000;
313 let operation_id = IdCounter::<OperationId>::starting_at(big)
314 .allocate()
315 .expect("large operation ID");
316 let condition =
317 SemaError::timeout_condition("timed out", "runtime/wait", big, Some(operation_id));
318 let expected = BTreeMap::from([
319 (Value::keyword("type"), Value::keyword("timeout")),
320 (Value::keyword("message"), Value::string("timed out")),
321 (Value::keyword("operation"), Value::string("runtime/wait")),
322 (Value::keyword("duration-ms"), Value::int(big as i64)),
323 (Value::keyword("operation-id"), Value::int(big as i64)),
324 ]);
325
326 assert!(matches!(condition, SemaError::Condition(value) if value == Value::map(expected)));
327 }
328
329 #[test]
330 fn atomic_allocator_issues_max_once_then_stays_exhausted() {
331 let counter = AtomicU64::new(u64::MAX);
332 assert_eq!(
333 allocate_atomic(&counter).expect("last ID available").get(),
334 u64::MAX
335 );
336 assert_eq!(allocate_atomic(&counter), Err(IdExhausted));
337 assert_eq!(allocate_atomic(&counter), Err(IdExhausted));
338 }
339
340 #[test]
341 fn runtime_ids_are_process_global_and_unique() {
342 let first = RuntimeId::allocate().expect("runtime ID available");
343 let second = RuntimeId::allocate().expect("runtime ID available");
344 assert!(first < second);
345 }
346
347 #[test]
348 fn scoped_ids_include_runtime_and_local_identity() {
349 let runtime = RuntimeId::allocate().expect("runtime ID available");
350 let mut counter = RuntimeScopedIdCounter::<PromiseId>::new(runtime);
351 let id = counter.allocate().expect("promise ID available");
352 assert_eq!(id.runtime(), runtime);
353 assert_eq!(id.local(), 1);
354 }
355
356 #[test]
357 fn scoped_ids_with_equal_locals_are_distinct_across_runtimes() {
358 let first_runtime = RuntimeId::allocate().expect("runtime ID available");
359 let second_runtime = RuntimeId::allocate().expect("runtime ID available");
360
361 macro_rules! assert_scoped_identity {
362 ($id_type:ty) => {{
363 let mut first = RuntimeScopedIdCounter::<$id_type>::new(first_runtime);
364 let mut second = RuntimeScopedIdCounter::<$id_type>::new(second_runtime);
365 let first_id = first.allocate().expect("scoped ID available");
366 let second_id = second.allocate().expect("scoped ID available");
367
368 assert_eq!(first_id.local(), 1);
369 assert_eq!(second_id.local(), 1);
370 assert_ne!(first_id, second_id);
371 }};
372 }
373
374 assert_scoped_identity!(RootId);
375 assert_scoped_identity!(PromiseId);
376 assert_scoped_identity!(ChannelId);
377 assert_scoped_identity!(ResourceGateId);
378 }
379
380 #[test]
381 fn runtime_task_ids_with_equal_local_tasks_are_distinct() {
382 let first_runtime = RuntimeId::allocate().expect("runtime ID available");
383 let second_runtime = RuntimeId::allocate().expect("runtime ID available");
384 let local_task = TaskId::try_from_raw(7).expect("task ID is nonzero");
385
386 let first = RuntimeTaskId::new(first_runtime, local_task);
387 let second = RuntimeTaskId::new(second_runtime, local_task);
388
389 assert_eq!(first.runtime(), first_runtime);
390 assert_eq!(first.task(), local_task);
391 assert_ne!(first, second);
392 }
393
394 #[test]
395 fn every_identity_has_the_required_value_traits() {
396 fn assert_traits<T: Copy + Clone + std::fmt::Debug + Eq + Ord + std::hash::Hash>() {}
397
398 assert_traits::<RuntimeId>();
399 assert_traits::<RuntimeTaskId>();
400 assert_traits::<RootId>();
401 assert_traits::<TaskId>();
402 assert_traits::<ScopeId>();
403 assert_traits::<PromiseId>();
404 assert_traits::<ChannelId>();
405 assert_traits::<ResourceGateId>();
406 assert_traits::<WaitId>();
407 assert_traits::<WaitGeneration>();
408 assert_traits::<OperationId>();
409 assert_traits::<SettlementSeq>();
410 assert_traits::<CompletionKind>();
411 }
412}