Skip to main content

hyperactor_remote/
token.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! Opaque rendezvous tokens for actor reference exchange.
10//!
11//! A token is a capability to contact a rendezvous actor. The creator can hand
12//! the token out through any external mechanism; holders can join and exchange
13//! typed actor references with the creator.
14//!
15//! The protocol has three roles:
16//!
17//! - The creator calls [`create`] with its own typed ref, a private
18//!   [`PortRef<Joined<J>>`] that receives join notifications, and [`Options`].
19//!   `create` spawns a supervised child rendezvous actor and returns a
20//!   serializable [`Token<C, J>`] that points at that child.
21//! - The token holder calls [`Token::join`] with its typed ref and a private
22//!   [`PortRef<JoinResult<C>>`]. The call only sends a join request; the
23//!   outcome is delivered asynchronously on that result port.
24//! - The rendezvous actor delivers [`Joined<J>`] to the creator and
25//!   [`JoinResult<C>`] to the joiner. With [`Policy::Multi`], every join is
26//!   accepted while the rendezvous actor is alive. With [`Policy::Once`], the
27//!   first join is accepted, and later joins receive
28//!   [`JoinResult::Rejected`].
29//!
30//! Tokens serialize as a single base64-encoded compact JSON string. The JSON
31//! payload includes the rendezvous actor ref and the type URIs for `C`, `J`,
32//! and [`RendezvousLike<C, J>`], so deserialization rejects tokens used with
33//! incompatible peer types. [`std::fmt::Display`] prints the canonical token,
34//! followed by `#`, followed by the compact JSON payload for inspection.
35//! [`FromStr`] accepts either the canonical token or this display form; the
36//! suffix after `#` is informational and ignored.
37
38use std::fmt;
39use std::str::FromStr;
40
41use base64::Engine as _;
42use base64::prelude::BASE64_STANDARD;
43use hyperactor::Actor;
44use hyperactor::ActorRef;
45use hyperactor::Context;
46use hyperactor::Endpoint as _;
47use hyperactor::HandleClient;
48use hyperactor::Handler;
49#[cfg(test)]
50use hyperactor::Instance;
51use hyperactor::PortRef;
52use hyperactor::RefClient;
53use hyperactor::context;
54use serde::Deserialize;
55use serde::Serialize;
56use serde::de::DeserializeOwned;
57use typeuri::Named;
58
59/// Type that can be exchanged through a rendezvous token.
60pub trait TokenPeer:
61    Named + Serialize + DeserializeOwned + Send + Sync + fmt::Debug + 'static
62{
63}
64
65impl<T> TokenPeer for T where
66    T: Named + Serialize + DeserializeOwned + Send + Sync + fmt::Debug + 'static
67{
68}
69
70/// Token join options.
71#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
72pub struct Options {
73    /// Policy that governs how many joins are accepted.
74    pub policy: Policy,
75}
76wirevalue::register_type!(Options);
77
78impl Default for Options {
79    fn default() -> Self {
80        Self {
81            policy: Policy::Multi,
82        }
83    }
84}
85
86/// Token join policy.
87#[derive(Clone, Copy, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
88pub enum Policy {
89    /// Accept every join while the rendezvous actor is alive.
90    Multi,
91    /// Accept the first join, then reject later joins.
92    Once,
93}
94wirevalue::register_type!(Policy);
95
96/// Notification delivered to the token creator when a peer joins.
97#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
98#[serde(bound(serialize = "P: TokenPeer"))]
99#[serde(bound(deserialize = "P: TokenPeer"))]
100pub struct Joined<P: TokenPeer> {
101    /// The peer that joined.
102    pub peer: P,
103}
104
105/// Result delivered to a joiner after it attempts to join.
106#[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
107#[serde(bound(serialize = "P: TokenPeer"))]
108#[serde(bound(deserialize = "P: TokenPeer"))]
109pub enum JoinResult<P: TokenPeer> {
110    /// The join was accepted, and the peer ref is available.
111    Joined {
112        /// The peer on the other side of the rendezvous.
113        peer: P,
114    },
115    /// The join was rejected by rendezvous policy.
116    Rejected {
117        /// Concise lowercase rejection reason.
118        reason: String,
119    },
120}
121
122/// Request sent to a rendezvous actor by a token holder.
123#[derive(
124    Clone,
125    Debug,
126    Serialize,
127    Deserialize,
128    Named,
129    Handler,
130    HandleClient,
131    RefClient
132)]
133#[serde(bound(serialize = "C: TokenPeer, J: TokenPeer"))]
134#[serde(bound(deserialize = "C: TokenPeer, J: TokenPeer"))]
135pub struct Join<C: TokenPeer, J: TokenPeer> {
136    /// Joiner ref to deliver to the token creator.
137    pub joiner: J,
138    /// Port that receives the join result.
139    pub result: PortRef<JoinResult<C>>,
140}
141
142hyperactor::behavior!(RendezvousLike<C, J>, Join<C, J>);
143
144/// Create a rendezvous token owned by `this`.
145///
146/// The token is backed by a child actor, so it is supervised by the creator.
147/// The creator receives [`Joined`] notifications on `creator_joined`; joiners
148/// receive [`JoinResult`] on the result port they pass to [`Token::join`].
149pub fn create<A, C, J>(
150    this: &impl context::Actor<A = A>,
151    creator: C,
152    creator_joined: PortRef<Joined<J>>,
153    options: Options,
154) -> anyhow::Result<Token<C, J>>
155where
156    A: Actor,
157    C: TokenPeer + Clone,
158    J: TokenPeer,
159{
160    let rendezvous = this
161        .instance()
162        .spawn(Rendezvous::new(creator, creator_joined, options));
163    Ok(Token::new(rendezvous.bind::<RendezvousLike<C, J>>()))
164}
165
166/// Opaque rendezvous token.
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct Token<C: TokenPeer, J: TokenPeer> {
169    rendezvous: ActorRef<RendezvousLike<C, J>>,
170}
171
172impl<C: TokenPeer, J: TokenPeer> Token<C, J> {
173    /// Create a token from a rendezvous actor ref.
174    pub fn new(rendezvous: ActorRef<RendezvousLike<C, J>>) -> Self {
175        Self { rendezvous }
176    }
177
178    /// The rendezvous actor ref contained by this token.
179    pub fn rendezvous(&self) -> &ActorRef<RendezvousLike<C, J>> {
180        &self.rendezvous
181    }
182
183    /// Send a join request to the rendezvous actor.
184    pub fn join(
185        &self,
186        cx: &impl hyperactor::context::Actor,
187        joiner: J,
188        result: PortRef<JoinResult<C>>,
189    ) -> anyhow::Result<()> {
190        (&self.rendezvous).post(cx, Join { joiner, result });
191        Ok(())
192    }
193
194    fn payload(&self) -> TokenPayload<C, J> {
195        TokenPayload {
196            creator_type_uri: C::typename().to_string(),
197            joiner_type_uri: J::typename().to_string(),
198            rendezvous_type_uri: <RendezvousLike<C, J> as Named>::typename().to_string(),
199            rendezvous: self.rendezvous.clone(),
200        }
201    }
202
203    fn encode(&self) -> Result<String, serde_json::Error> {
204        Ok(BASE64_STANDARD.encode(serde_json::to_string(&self.payload())?))
205    }
206
207    fn compact_json(&self) -> Result<String, serde_json::Error> {
208        serde_json::to_string(&self.payload())
209    }
210}
211
212impl<C, J> Serialize for Token<C, J>
213where
214    C: TokenPeer,
215    J: TokenPeer,
216{
217    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
218    where
219        S: serde::Serializer,
220    {
221        serializer.serialize_str(&self.encode().map_err(serde::ser::Error::custom)?)
222    }
223}
224
225impl<'de, C, J> Deserialize<'de> for Token<C, J>
226where
227    C: TokenPeer,
228    J: TokenPeer,
229{
230    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
231    where
232        D: serde::Deserializer<'de>,
233    {
234        let token = String::deserialize(deserializer)?;
235        let json = BASE64_STANDARD
236            .decode(token)
237            .map_err(serde::de::Error::custom)?;
238        let payload: TokenPayload<C, J> =
239            serde_json::from_slice(&json).map_err(serde::de::Error::custom)?;
240        payload.validate().map_err(serde::de::Error::custom)?;
241        Ok(Self {
242            rendezvous: payload.rendezvous,
243        })
244    }
245}
246
247impl<C, J> FromStr for Token<C, J>
248where
249    C: TokenPeer,
250    J: TokenPeer,
251{
252    type Err = anyhow::Error;
253
254    fn from_str(token: &str) -> Result<Self, Self::Err> {
255        let token = token.split_once('#').map_or(token, |(token, _)| token);
256        let json = BASE64_STANDARD.decode(token)?;
257        let payload: TokenPayload<C, J> = serde_json::from_slice(&json)?;
258        payload.validate()?;
259        Ok(Self {
260            rendezvous: payload.rendezvous,
261        })
262    }
263}
264
265impl<C, J> fmt::Display for Token<C, J>
266where
267    C: TokenPeer,
268    J: TokenPeer,
269{
270    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271        let token = self.encode().map_err(|_| fmt::Error)?;
272        let json = self.compact_json().map_err(|_| fmt::Error)?;
273        write!(f, "{}#{}", token, json)
274    }
275}
276
277#[derive(Debug)]
278struct Rendezvous<C: TokenPeer, J: TokenPeer> {
279    creator: C,
280    creator_joined: PortRef<Joined<J>>,
281    options: Options,
282    joined: bool,
283}
284
285impl<C: TokenPeer, J: TokenPeer> Rendezvous<C, J> {
286    fn new(creator: C, creator_joined: PortRef<Joined<J>>, options: Options) -> Self {
287        Self {
288            creator,
289            creator_joined,
290            options,
291            joined: false,
292        }
293    }
294}
295
296#[async_trait::async_trait]
297impl<C: TokenPeer, J: TokenPeer> Actor for Rendezvous<C, J> {}
298
299#[async_trait::async_trait]
300impl<C: TokenPeer + Clone, J: TokenPeer> Handler<Join<C, J>> for Rendezvous<C, J> {
301    async fn handle(&mut self, cx: &Context<Self>, message: Join<C, J>) -> anyhow::Result<()> {
302        if self.options.policy == Policy::Once && self.joined {
303            message.result.post(
304                cx,
305                JoinResult::Rejected {
306                    reason: "token already joined".to_string(),
307                },
308            );
309            return Ok(());
310        }
311
312        (&self.creator_joined).post(
313            cx,
314            Joined {
315                peer: message.joiner,
316            },
317        );
318
319        message.result.post(
320            cx,
321            JoinResult::Joined {
322                peer: self.creator.clone(),
323            },
324        );
325        self.joined = true;
326        Ok(())
327    }
328}
329
330#[derive(Clone, Debug, Serialize, Deserialize)]
331#[serde(bound(serialize = "C: TokenPeer, J: TokenPeer"))]
332#[serde(bound(deserialize = "C: TokenPeer, J: TokenPeer"))]
333struct TokenPayload<C: TokenPeer, J: TokenPeer> {
334    creator_type_uri: String,
335    joiner_type_uri: String,
336    rendezvous_type_uri: String,
337    rendezvous: ActorRef<RendezvousLike<C, J>>,
338}
339
340impl<C: TokenPeer, J: TokenPeer> TokenPayload<C, J> {
341    fn validate(&self) -> Result<(), TokenTypeError> {
342        check_type_uri("creator", &self.creator_type_uri, C::typename())?;
343        check_type_uri("joiner", &self.joiner_type_uri, J::typename())?;
344        check_type_uri(
345            "rendezvous",
346            &self.rendezvous_type_uri,
347            <RendezvousLike<C, J> as Named>::typename(),
348        )
349    }
350}
351
352#[derive(Debug, thiserror::Error)]
353#[error("{kind} type uri mismatch: expected {expected}, got {actual}")]
354struct TokenTypeError {
355    kind: &'static str,
356    expected: &'static str,
357    actual: String,
358}
359
360fn check_type_uri(
361    kind: &'static str,
362    actual: &str,
363    expected: &'static str,
364) -> Result<(), TokenTypeError> {
365    if actual == expected {
366        return Ok(());
367    }
368    Err(TokenTypeError {
369        kind,
370        expected,
371        actual: actual.to_string(),
372    })
373}
374
375#[cfg(test)]
376mod tests {
377    use hyperactor::Actor;
378    use hyperactor::Context;
379    use hyperactor::PortHandle;
380    use hyperactor::Proc;
381    use hyperactor::testing::process_assertion::assert_termination;
382
383    use super::*;
384
385    #[derive(Debug)]
386    #[hyperactor::export]
387    struct RendezvousStub;
388
389    #[async_trait::async_trait]
390    impl Actor for RendezvousStub {}
391
392    #[async_trait::async_trait]
393    impl Handler<Join<CreatorRef, JoinerRef>> for RendezvousStub {
394        async fn handle(
395            &mut self,
396            _cx: &Context<Self>,
397            _message: Join<CreatorRef, JoinerRef>,
398        ) -> anyhow::Result<()> {
399            Ok(())
400        }
401    }
402
403    #[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
404    struct CreatorRef;
405
406    #[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
407    struct JoinerRef;
408
409    #[derive(Clone, Debug, Serialize, Deserialize, Named, PartialEq, Eq)]
410    struct OtherJoinerRef;
411
412    #[derive(
413        Clone,
414        Debug,
415        Serialize,
416        Deserialize,
417        Named,
418        PartialEq,
419        Eq,
420        PartialOrd,
421        Ord
422    )]
423    enum MultiJoinerRef {
424        One,
425        Two,
426        Three,
427    }
428
429    #[derive(Debug)]
430    struct CreatorActor {
431        creator_joined: PortHandle<Joined<JoinerRef>>,
432        token_out: PortHandle<Token<CreatorRef, JoinerRef>>,
433    }
434
435    #[async_trait::async_trait]
436    impl Actor for CreatorActor {
437        async fn init(&mut self, this: &Instance<Self>) -> anyhow::Result<()> {
438            let token = create(
439                this,
440                CreatorRef,
441                self.creator_joined.bind(),
442                Options::default(),
443            )?;
444            self.token_out.post(this, token);
445            Ok(())
446        }
447    }
448
449    #[tokio::test]
450    async fn test_create_delivers_join_to_both_sides() {
451        let proc = Proc::isolated();
452        let creator = proc.client("creator");
453        let (creator_joined, mut creator_joined_rx) = creator.open_port::<Joined<JoinerRef>>();
454        let token = create(
455            &creator,
456            CreatorRef,
457            creator_joined.bind(),
458            Options::default(),
459        )
460        .unwrap();
461        let joiner = proc.client("joiner");
462        let (join_result, mut join_result_rx) = joiner.open_port::<JoinResult<CreatorRef>>();
463
464        token.join(&joiner, JoinerRef, join_result.bind()).unwrap();
465
466        assert_eq!(creator_joined_rx.recv().await.unwrap().peer, JoinerRef);
467        assert_eq!(
468            join_result_rx.recv().await.unwrap(),
469            JoinResult::Joined { peer: CreatorRef }
470        );
471    }
472
473    #[tokio::test]
474    async fn test_once_token_rejects_later_joins() {
475        let proc = Proc::isolated();
476        let creator = proc.client("creator");
477        let (creator_joined, mut creator_joined_rx) = creator.open_port::<Joined<JoinerRef>>();
478        let token = create(
479            &creator,
480            CreatorRef,
481            creator_joined.bind(),
482            Options {
483                policy: Policy::Once,
484            },
485        )
486        .unwrap();
487        let joiner = proc.client("joiner");
488        let (first_result, mut first_result_rx) = joiner.open_port::<JoinResult<CreatorRef>>();
489        let (second_result, mut second_result_rx) = joiner.open_port::<JoinResult<CreatorRef>>();
490
491        token.join(&joiner, JoinerRef, first_result.bind()).unwrap();
492        token
493            .join(&joiner, JoinerRef, second_result.bind())
494            .unwrap();
495
496        assert_eq!(creator_joined_rx.recv().await.unwrap().peer, JoinerRef);
497        assert_eq!(
498            first_result_rx.recv().await.unwrap(),
499            JoinResult::Joined { peer: CreatorRef }
500        );
501        assert_eq!(
502            second_result_rx.recv().await.unwrap(),
503            JoinResult::Rejected {
504                reason: "token already joined".to_string()
505            }
506        );
507    }
508
509    #[tokio::test]
510    async fn test_multi_token_accepts_every_join() {
511        let proc = Proc::isolated();
512        let creator = proc.client("creator");
513        let (creator_joined, mut creator_joined_rx) = creator.open_port::<Joined<MultiJoinerRef>>();
514        let token = create(
515            &creator,
516            CreatorRef,
517            creator_joined.bind(),
518            Options {
519                policy: Policy::Multi,
520            },
521        )
522        .unwrap();
523
524        let joiner1 = proc.client("joiner1");
525        let joiner2 = proc.client("joiner2");
526        let joiner3 = proc.client("joiner3");
527
528        let (r1, mut r1_rx) = joiner1.open_port::<JoinResult<CreatorRef>>();
529        let (r2, mut r2_rx) = joiner2.open_port::<JoinResult<CreatorRef>>();
530        let (r3, mut r3_rx) = joiner3.open_port::<JoinResult<CreatorRef>>();
531
532        token
533            .join(&joiner1, MultiJoinerRef::One, r1.bind())
534            .unwrap();
535        token
536            .join(&joiner2, MultiJoinerRef::Two, r2.bind())
537            .unwrap();
538        token
539            .join(&joiner3, MultiJoinerRef::Three, r3.bind())
540            .unwrap();
541
542        let mut joined = vec![
543            creator_joined_rx.recv().await.unwrap().peer,
544            creator_joined_rx.recv().await.unwrap().peer,
545            creator_joined_rx.recv().await.unwrap().peer,
546        ];
547        joined.sort();
548
549        assert_eq!(
550            joined,
551            vec![
552                MultiJoinerRef::One,
553                MultiJoinerRef::Two,
554                MultiJoinerRef::Three,
555            ]
556        );
557
558        assert_eq!(
559            r1_rx.recv().await.unwrap(),
560            JoinResult::Joined { peer: CreatorRef }
561        );
562        assert_eq!(
563            r2_rx.recv().await.unwrap(),
564            JoinResult::Joined { peer: CreatorRef }
565        );
566        assert_eq!(
567            r3_rx.recv().await.unwrap(),
568            JoinResult::Joined { peer: CreatorRef }
569        );
570    }
571
572    #[tokio::test]
573    async fn test_token_serializes_as_single_base64_json_string() {
574        let proc = Proc::isolated();
575        let rendezvous = proc.spawn(RendezvousStub);
576        let token = Token::<CreatorRef, JoinerRef>::new(
577            rendezvous.bind::<RendezvousLike<CreatorRef, JoinerRef>>(),
578        );
579
580        let serialized = serde_json::to_string(&token).unwrap();
581        let encoded: String = serde_json::from_str(&serialized).unwrap();
582        assert!(!encoded.contains('#'));
583
584        let json = BASE64_STANDARD.decode(encoded).unwrap();
585        let payload: serde_json::Value = serde_json::from_slice(&json).unwrap();
586        assert_eq!(
587            payload["creator_type_uri"],
588            <CreatorRef as Named>::typename()
589        );
590        assert_eq!(payload["joiner_type_uri"], <JoinerRef as Named>::typename());
591        assert_eq!(
592            payload["rendezvous_type_uri"],
593            <RendezvousLike<CreatorRef, JoinerRef> as Named>::typename()
594        );
595
596        rendezvous.stop("test").unwrap();
597        rendezvous.await;
598    }
599
600    #[tokio::test]
601    async fn test_token_display_includes_compact_json_suffix() {
602        let proc = Proc::isolated();
603        let rendezvous = proc.spawn(RendezvousStub);
604        let token = Token::<CreatorRef, JoinerRef>::new(
605            rendezvous.bind::<RendezvousLike<CreatorRef, JoinerRef>>(),
606        );
607
608        let display = token.to_string();
609        let (encoded, json) = display.split_once('#').unwrap();
610        assert_eq!(encoded, token.encode().unwrap());
611        assert_eq!(json, token.compact_json().unwrap());
612
613        rendezvous.stop("test").unwrap();
614        rendezvous.await;
615    }
616
617    #[tokio::test]
618    async fn test_token_from_str_accepts_display_suffix() {
619        let proc = Proc::isolated();
620        let rendezvous = proc.spawn(RendezvousStub);
621        let token = Token::<CreatorRef, JoinerRef>::new(
622            rendezvous.bind::<RendezvousLike<CreatorRef, JoinerRef>>(),
623        );
624
625        let encoded = token.encode().unwrap();
626        let parsed_encoded: Token<CreatorRef, JoinerRef> = encoded.parse().unwrap();
627        let parsed_display: Token<CreatorRef, JoinerRef> = token.to_string().parse().unwrap();
628        let parsed_ignored_suffix: Token<CreatorRef, JoinerRef> =
629            format!("{encoded}#not json").parse().unwrap();
630
631        assert_eq!(parsed_encoded, token);
632        assert_eq!(parsed_display, token);
633        assert_eq!(parsed_ignored_suffix, token);
634
635        rendezvous.stop("test").unwrap();
636        rendezvous.await;
637    }
638
639    #[tokio::test]
640    async fn test_token_rejects_incompatible_type_parameters() {
641        let proc = Proc::isolated();
642        let rendezvous = proc.spawn(RendezvousStub);
643        let token = Token::<CreatorRef, JoinerRef>::new(
644            rendezvous.bind::<RendezvousLike<CreatorRef, JoinerRef>>(),
645        );
646        let serialized = serde_json::to_string(&token).unwrap();
647
648        let result = serde_json::from_str::<Token<CreatorRef, OtherJoinerRef>>(&serialized);
649
650        assert!(result.is_err());
651
652        rendezvous.stop("test").unwrap();
653        rendezvous.await;
654    }
655
656    #[test]
657    fn test_token_from_str_rejects_corrupt_base64() {
658        let result: Result<Token<CreatorRef, JoinerRef>, _> = "%%%".parse();
659        assert!(result.is_err());
660    }
661
662    #[test]
663    fn test_token_from_str_rejects_invalid_json() {
664        let encoded = BASE64_STANDARD.encode(b"not json at all");
665        let result: Result<Token<CreatorRef, JoinerRef>, _> = encoded.parse();
666        assert!(result.is_err());
667    }
668
669    #[tokio::test]
670    async fn test_token_from_str_rejects_mismatched_type_uris() {
671        let proc = Proc::isolated();
672        let rendezvous = proc.spawn(RendezvousStub);
673        let token = Token::<CreatorRef, JoinerRef>::new(
674            rendezvous.bind::<RendezvousLike<CreatorRef, JoinerRef>>(),
675        );
676
677        let encoded = token.encode().unwrap();
678        let result: Result<Token<CreatorRef, OtherJoinerRef>, _> = encoded.parse();
679        assert!(result.is_err());
680
681        rendezvous.stop("test").unwrap();
682        rendezvous.await;
683    }
684
685    #[tokio::test]
686    async fn test_join_after_creator_stops_exits() {
687        assert_termination(
688            || async {
689                let proc = Proc::isolated();
690                let inst = proc.client("inst");
691                let (creator_joined, _creator_joined_rx) = inst.open_port::<Joined<JoinerRef>>();
692                let (token_out, mut token_out_rx) =
693                    inst.open_port::<Token<CreatorRef, JoinerRef>>();
694
695                let creator_handle = proc.spawn(CreatorActor {
696                    creator_joined,
697                    token_out,
698                });
699
700                let token = token_out_rx.recv().await.unwrap();
701
702                // Stop the creator; its supervised rendezvous actor dies with
703                // it.
704                creator_handle.stop("test").unwrap();
705                tokio::time::timeout(std::time::Duration::from_secs(5), creator_handle)
706                    .await
707                    .unwrap();
708
709                let joiner = proc.client("joiner");
710                let (result, _result_rx) = joiner.open_port::<JoinResult<CreatorRef>>();
711                token.join(&joiner, JoinerRef, result.bind()).unwrap();
712
713                tokio::time::sleep(std::time::Duration::from_secs(30)).await;
714            },
715            1,
716        )
717        .await
718        .unwrap();
719    }
720}