1use boxology_contract::{
3 CallContext, CapabilityDescriptor, Detail, ErasedCallError, ErasedTarget, ExposureLevel,
4 SlotValue, call_guarded,
5};
6use std::{future::Future, pin::Pin, sync::Arc};
7use tokio_util::sync::CancellationToken;
8pub type TransportTaskTracker = tokio_util::task::TaskTracker;
10pub type TransportJoinFuture = Pin<Box<dyn Future<Output = Result<(), Detail>> + Send + 'static>>;
12#[derive(Clone)]
14pub struct TransportExposure {
15 descriptor: &'static CapabilityDescriptor,
16 level: ExposureLevel,
17 target: Arc<dyn ErasedTarget>,
18}
19#[allow(dead_code)]
20impl TransportExposure {
21 pub(crate) fn new(
22 descriptor: &'static CapabilityDescriptor,
23 level: ExposureLevel,
24 target: Arc<dyn ErasedTarget>,
25 ) -> Self {
26 Self {
27 descriptor,
28 level,
29 target,
30 }
31 }
32 pub fn descriptor(&self) -> &'static CapabilityDescriptor {
34 self.descriptor
35 }
36 pub fn level(&self) -> ExposureLevel {
38 self.level
39 }
40 pub fn dispatch<'a>(
42 &'a self,
43 context: CallContext,
44 input: SlotValue,
45 ) -> Pin<Box<dyn Future<Output = Result<SlotValue, ErasedCallError>> + Send + 'a>> {
46 call_guarded(self.target.as_ref(), self.descriptor.id(), context, input)
47 }
48}
49pub struct TransportRuntime<C>
51where
52 C: Send + Sync + 'static,
53{
54 exposures: Arc<[TransportExposure]>,
55 tracker: TransportTaskTracker,
56 config: Arc<C>,
57 activation: CancellationToken,
58}
59impl<C: Send + Sync + 'static> Clone for TransportRuntime<C> {
60 fn clone(&self) -> Self {
61 Self {
62 exposures: self.exposures.clone(),
63 tracker: self.tracker.clone(),
64 config: self.config.clone(),
65 activation: self.activation.clone(),
66 }
67 }
68}
69#[allow(dead_code)]
70impl<C: Send + Sync + 'static> TransportRuntime<C> {
71 pub(crate) fn new(
72 exposures: Arc<[TransportExposure]>,
73 tracker: TransportTaskTracker,
74 config: Arc<C>,
75 activation: CancellationToken,
76 ) -> Self {
77 Self {
78 exposures,
79 tracker,
80 config,
81 activation,
82 }
83 }
84 pub fn exposures(&self) -> &[TransportExposure] {
86 &self.exposures
87 }
88 pub fn tracker(&self) -> &TransportTaskTracker {
90 &self.tracker
91 }
92 pub fn config(&self) -> &C {
94 &self.config
95 }
96 pub fn is_active(&self) -> bool {
98 self.activation.is_cancelled()
99 }
100 pub async fn wait_until_active(&self) {
102 self.activation.cancelled().await;
103 }
104 pub(crate) fn activate(&self) {
105 self.activation.cancel();
106 }
107}
108pub trait TransportBinding: Send + Sync + 'static {
113 type Config: Send + Sync + 'static;
115 type Handle: TransportHandle;
117 fn config(&self) -> Arc<Self::Config>;
119 fn conform(
121 &self,
122 descriptor: &CapabilityDescriptor,
123 level: ExposureLevel,
124 ) -> Result<(), Detail>;
125 fn prepare(&self, descriptors: &[&'static CapabilityDescriptor]) -> Result<(), Detail>;
127 fn start(&self, runtime: TransportRuntime<Self::Config>) -> Result<Self::Handle, Detail>;
129}
130pub trait TransportHandle: Send + Sync + 'static {
132 fn stop_intake(&self);
134 fn cancel_tasks(&self);
136 fn abort_tasks(&self);
138 fn join_tasks(self: Box<Self>) -> TransportJoinFuture;
140}
141#[cfg(test)]
142mod tests {
143 use super::{TransportTaskTracker as Tracker, *};
144 use boxology_contract::{
145 BoxId, Caller, CancelToken, CapabilityId, CapabilityName, CapabilityShape, ContractValue,
146 Idempotency, TraceContext, TypeDescriptor,
147 };
148 use std::future::{Future, ready};
149 use std::sync::Mutex;
150 use std::task::{Context, Poll, Waker};
151 fn descriptor() -> &'static CapabilityDescriptor {
152 Box::leak(Box::new(CapabilityDescriptor::new(
153 CapabilityId::new(
154 BoxId::new("transport-test").unwrap(),
155 CapabilityName::new("call").unwrap(),
156 ),
157 TypeDescriptor::bool(),
158 TypeDescriptor::bool(),
159 TypeDescriptor::bool(),
160 CapabilityShape::Unary,
161 ExposureLevel::External,
162 Idempotency::None,
163 None,
164 )))
165 }
166 fn context() -> CallContext {
167 CallContext::new(
168 Caller::Anonymous,
169 None,
170 CancelToken::new(),
171 TraceContext::empty(),
172 None,
173 )
174 }
175 fn poll_once<F: Future + ?Sized>(future: Pin<&mut F>) -> Poll<F::Output> {
176 future.poll(&mut Context::from_waker(Waker::noop()))
177 }
178 #[derive(Default)]
179 struct Target(Mutex<Vec<CapabilityId>>);
180 impl ErasedTarget for Target {
181 fn call<'a>(
182 &'a self,
183 capability: &'a CapabilityId,
184 _context: CallContext,
185 input: SlotValue,
186 ) -> Pin<Box<dyn Future<Output = Result<SlotValue, ErasedCallError>> + Send + 'a>> {
187 self.0.lock().unwrap().push(capability.clone());
188 let result = match input {
189 SlotValue::Null => Ok(SlotValue::Null),
190 SlotValue::Missing => Err(ErasedCallError::Unavailable(Detail::new("test_call"))),
191 SlotValue::Value(_) => panic!("guarded transport panic"),
192 };
193 Box::pin(ready(result))
194 }
195 }
196 struct Config(u8);
197 struct Handle;
198 struct Binding;
199 impl TransportHandle for Handle {
200 fn stop_intake(&self) {}
201 fn cancel_tasks(&self) {}
202 fn abort_tasks(&self) {}
203 fn join_tasks(self: Box<Self>) -> TransportJoinFuture {
204 Box::pin(ready(Ok(())))
205 }
206 }
207 impl TransportBinding for Binding {
208 type Config = Config;
209 type Handle = Handle;
210 fn config(&self) -> Arc<Config> {
211 Arc::new(Config(0))
212 }
213 fn conform(
214 &self,
215 _descriptor: &CapabilityDescriptor,
216 _level: ExposureLevel,
217 ) -> Result<(), Detail> {
218 Ok(())
219 }
220 fn prepare(&self, _: &[&'static CapabilityDescriptor]) -> Result<(), Detail> {
221 Ok(())
222 }
223 fn start(&self, _: TransportRuntime<Config>) -> Result<Handle, Detail> {
224 Ok(Handle)
225 }
226 }
227 #[test]
228 fn transport_carriers_preserve_dispatch_sharing_and_activation_contracts() {
229 fn bounds<T: Send + Sync + 'static>() {}
230 bounds::<TransportExposure>();
231 bounds::<TransportRuntime<Config>>();
232 bounds::<Tracker>();
233 let descriptor = descriptor();
234 let target = Arc::new(Target::default());
235 let exposure = TransportExposure::new(descriptor, ExposureLevel::Internal, target.clone());
236 assert!(std::ptr::eq(exposure.descriptor(), descriptor));
237 assert_eq!(exposure.level(), ExposureLevel::Internal);
238 let mut success = exposure.dispatch(context(), SlotValue::Null);
239 let result = poll_once(success.as_mut());
240 assert_eq!(result, Poll::Ready(Ok(SlotValue::Null)));
241 let mut failure = exposure.dispatch(context(), SlotValue::Missing);
242 let result = poll_once(failure.as_mut());
243 let expected = ErasedCallError::Unavailable(Detail::new("test_call"));
244 assert_eq!(result, Poll::Ready(Err(expected)));
245 let mut panic = exposure.dispatch(context(), SlotValue::Value(ContractValue::bool(true)));
246 let Poll::Ready(Err(ErasedCallError::Internal(detail))) = poll_once(panic.as_mut()) else {
247 panic!("transport panic escaped guarded dispatch")
248 };
249 assert_eq!(detail.code(), "panic");
250 assert_eq!(*target.0.lock().unwrap(), vec![descriptor.id().clone(); 3]);
251 let runtime = TransportRuntime::new(
252 Arc::from([exposure.clone()]),
253 Tracker::new(),
254 Binding.config(),
255 CancellationToken::new(),
256 );
257 let clone = runtime.clone();
258 let same_tracker = Tracker::ptr_eq(&runtime.tracker, &clone.tracker);
259 assert!(same_tracker);
260 assert!(Arc::ptr_eq(&runtime.config, &clone.config));
261 assert_eq!(runtime.config().0, 0);
262 assert!(Arc::ptr_eq(&runtime.exposures, &clone.exposures));
263 let token = clone.tracker().token();
264 assert_eq!(runtime.tracker().len(), 1);
265 drop(token);
266 assert!(runtime.tracker().is_empty());
267 let mut active = Box::pin(runtime.wait_until_active());
268 assert!(!runtime.is_active() && matches!(poll_once(active.as_mut()), Poll::Pending));
269 clone.activate();
270 assert!(runtime.is_active() && matches!(poll_once(active.as_mut()), Poll::Ready(())));
271 }
272}