~radicle-link/dev

link-async: new crate v1 PROPOSED

Kim Altintop: 1
 link-async: new crate

 22 files changed, 193 insertions(+), 109 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~radicle-link/dev/patches/26191/mbox | git am -3
Learn more about email & git

[PATCH] link-async: new crate Export this patch

Move the executor abstractions to a new crate. Also provide timer utils
(timeout, sleep, interval), as timers are in fact very executor-specific.

In the process, remove all use of `futures-timer` which heretically
spawns an ambient thread. Also ensure only one version of the `rand`
crate is used in the workspace.

Signed-off-by: Kim Altintop <kim@eagain.st>
---

I needed a timeout, so I cleansed this long-standing issue.

In my naivete, I had not realised that timers are meticulously optimised,
integral ingredients of async runtimes, almost their sole raison d'etre. I had
fallen prey to the heretics who, in the name of "ease of use" and similar wildly
irrelevant claims, had thrown all memory efficiency overboard by SPAWNING
THREADS without telling.

It's a dangerous world. Those ECMAScript people are lurking everywhere, and what
they want is TO ALLOCATE. We shall not be fooled!

Published-As: https://git.sr.ht/~kim/radicle-link/refs/patches/link-async/v1

 Cargo.toml                                    |  1 +
 librad/Cargo.toml                             |  8 +-
 librad/src/git/storage/fetcher.rs             |  6 +-
 librad/src/lib.rs                             |  1 -
 librad/src/net/peer.rs                        |  9 +-
 librad/src/net/peer/storage.rs                |  6 +-
 librad/src/net/protocol.rs                    |  4 +-
 librad/src/net/protocol/event.rs              |  6 +-
 .../src/net/protocol/membership/periodic.rs   | 72 +++-----------
 librad/src/net/protocol/state.rs              |  4 +-
 librad/src/net/quic/endpoint.rs               |  6 +-
 librad/src/net/upgrade.rs                     | 19 +---
 link-async/Cargo.toml                         | 25 +++++
 link-async/src/lib.rs                         | 10 ++
 .../executor.rs => link-async/src/spawn.rs    |  5 +-
 link-async/src/time.rs                        | 99 +++++++++++++++++++
 test/Cargo.toml                               |  5 +-
 test/src/librad/keys.rs                       |  2 +-
 test/src/test/unit.rs                         |  1 +
 test/src/test/unit/librad.rs                  |  1 -
 test/src/test/unit/librad/net/upgrade.rs      |  8 +-
 .../{librad/executor.rs => link_async.rs}     |  4 +-
 22 files changed, 193 insertions(+), 109 deletions(-)
 create mode 100644 link-async/Cargo.toml
 create mode 100644 link-async/src/lib.rs
 rename librad/src/executor.rs => link-async/src/spawn.rs (98%)
 create mode 100644 link-async/src/time.rs
 rename test/src/test/unit/{librad/executor.rs => link_async.rs} (84%)

diff --git a/Cargo.toml b/Cargo.toml
index d121f58b..70a7b2c9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
  "git-helpers",
  "git-trailers",
  "librad",
  "link-async",
  "link-canonical",
  "link-canonical-derive",
  "link-crypto",
diff --git a/librad/Cargo.toml b/librad/Cargo.toml
index 9c01af26..ac2c577d 100644
--- a/librad/Cargo.toml
+++ b/librad/Cargo.toml
@@ -19,7 +19,6 @@ dashmap = "4.0"
directories = "3.0"
futures = "0.3"
futures_codec = "0.4"
futures-timer = "3.0"
globset = "0.4"
governor = ">=0.3.2"
if-watch = "0.2"
@@ -40,8 +39,8 @@ picky-asn1 = "0.3.2"
picky-asn1-der = "0.2.5"
picky-asn1-x509 = "0.6.0"
priority-queue = "1.0"
rand = "0.7"
rand_pcg = "0.2"
rand = "0.8"
rand_pcg = "0.3.1"
regex = "1.3"
rustc-hash = "1.1"
serde_bytes = "0.11"
@@ -86,6 +85,9 @@ version = "0.7"
default-features = false
features = ["tls-rustls"]

[dependencies.link-async]
path = "../link-async"

[dependencies.link-canonical]
path = "../link-canonical"

diff --git a/librad/src/git/storage/fetcher.rs b/librad/src/git/storage/fetcher.rs
index f585d7ab..195d9962 100644
--- a/librad/src/git/storage/fetcher.rs
+++ b/librad/src/git/storage/fetcher.rs
@@ -14,13 +14,13 @@ use std::{

use dashmap::DashMap;
use git_ext::RefLike;
use link_async::Spawner;
use rustc_hash::FxHasher;
use thiserror::Error;
use url::Url;

use super::{PoolError, Storage};
use crate::{
    executor,
    git::{
        fetch::{self, FetchResult, Fetchspecs, RemoteHeads},
        p2p::url::GitUrlRef,
@@ -279,7 +279,7 @@ pub mod error {
/// strategy increases the sleep interval after each attempt, so is biased
/// towards more recent requests for the same resource.
pub async fn retrying<P, B, E, F, A>(
    spawner: &executor::Spawner,
    spawner: &Spawner,
    pool: &P,
    builder: B,
    timeout: Duration,
@@ -303,7 +303,7 @@ where
    }

    async fn go<P, B, F, A, E>(
        spawner: &executor::Spawner,
        spawner: &Spawner,
        pool: &P,
        builder: B,
        f: F,
diff --git a/librad/src/lib.rs b/librad/src/lib.rs
index 9079955c..b33f99af 100644
--- a/librad/src/lib.rs
+++ b/librad/src/lib.rs
@@ -32,7 +32,6 @@ pub extern crate radicle_data as data;
pub extern crate radicle_git_ext as git_ext;
pub extern crate radicle_std_ext as std_ext;

pub mod executor;
pub mod git;
pub mod internal;
pub mod net;
diff --git a/librad/src/net/peer.rs b/librad/src/net/peer.rs
index 1db1fcd5..80dc6bb8 100644
--- a/librad/src/net/peer.rs
+++ b/librad/src/net/peer.rs
@@ -6,11 +6,10 @@
use std::{net::SocketAddr, sync::Arc, time::Duration};

use futures::{future, StreamExt as _, TryFutureExt as _, TryStreamExt as _};
use futures_timer::Delay;
use link_async::Spawner;

use super::protocol::{self, gossip};
use crate::{
    executor,
    git::{self, storage::Fetchers, Urn},
    PeerId,
    Signer,
@@ -93,7 +92,7 @@ pub struct Peer<S> {
    peer_store: PeerStorage,
    user_store: git::storage::Pool<git::storage::Storage>,
    caches: protocol::Caches,
    spawner: Arc<executor::Spawner>,
    spawner: Arc<Spawner>,
}

impl<S> Peer<S>
@@ -101,7 +100,7 @@ where
    S: Signer + Clone,
{
    pub fn new(config: Config<S>) -> Result<Self, error::Init> {
        let spawner = executor::Spawner::from_current()
        let spawner = Spawner::from_current()
            .map(Arc::new)
            .ok_or(error::Init::Runtime)?;
        let phone = protocol::TinCans::default();
@@ -182,7 +181,7 @@ where
        let events = self.subscribe();
        let providers = futures::stream::select(
            futures::stream::once(async move {
                Delay::new(timeout).await;
                link_async::sleep(timeout).await;
                Err("timed out")
            }),
            {
diff --git a/librad/src/net/peer/storage.rs b/librad/src/net/peer/storage.rs
index 2e84aac9..1f2b9c46 100644
--- a/librad/src/net/peer/storage.rs
+++ b/librad/src/net/peer/storage.rs
@@ -8,10 +8,10 @@ use std::{net::SocketAddr, sync::Arc, time::Duration};
use crypto::peer::Originates;
use either::Either::{self, Left, Right};
use git_ext::{self as ext, reference};
use link_async::Spawner;
use nonzero_ext::nonzero;

use crate::{
    executor,
    git::{
        replication,
        storage::{self, fetcher, Pool, PoolError, PooledRef, ReadOnlyStorage as _},
@@ -40,12 +40,12 @@ pub struct Storage {
    config: Config,
    urns: cache::urns::Filter,
    limits: Arc<RateLimiter<Keyed<(PeerId, Urn)>>>,
    spawner: Arc<executor::Spawner>,
    spawner: Arc<Spawner>,
}

impl Storage {
    pub fn new(
        spawner: Arc<executor::Spawner>,
        spawner: Arc<Spawner>,
        pool: Pool<storage::Storage>,
        config: Config,
        urns: cache::urns::Filter,
diff --git a/librad/src/net/protocol.rs b/librad/src/net/protocol.rs
index b7219d3a..c158e59c 100644
--- a/librad/src/net/protocol.rs
+++ b/librad/src/net/protocol.rs
@@ -7,6 +7,7 @@ use std::{fmt::Debug, future::Future, net::SocketAddr, sync::Arc};

use async_stream::stream;
use futures::{stream::BoxStream, StreamExt};
use link_async::Spawner;
use nonempty::NonEmpty;
use nonzero_ext::nonzero;
use rand_pcg::Pcg64Mcg;
@@ -19,7 +20,6 @@ use super::{
    Network,
};
use crate::{
    executor,
    git::{self, p2p::transport::GitStreamFactory, replication, storage},
    paths::Paths,
    rate_limit::RateLimiter,
@@ -147,7 +147,7 @@ impl<S> LocalAddr for Bound<S> {
}

pub async fn bind<Sign, Store>(
    spawner: Arc<executor::Spawner>,
    spawner: Arc<Spawner>,
    phone: TinCans,
    config: Config,
    signer: Sign,
diff --git a/librad/src/net/protocol/event.rs b/librad/src/net/protocol/event.rs
index 61867022..d178cf38 100644
--- a/librad/src/net/protocol/event.rs
+++ b/librad/src/net/protocol/event.rs
@@ -90,8 +90,7 @@ pub mod upstream {

    use std::time::Duration;

    use futures::{FutureExt as _, StreamExt as _};
    use futures_timer::Delay;
    use futures::{pin_mut, FutureExt as _, StreamExt as _};
    use thiserror::Error;

    use crate::net::protocol::{PeerInfo, RecvError};
@@ -168,7 +167,8 @@ pub mod upstream {
        S: futures::Stream<Item = Result<Upstream, RecvError>> + Unpin,
        P: Fn(&Upstream) -> bool,
    {
        let mut timeout = Delay::new(timeout).fuse();
        let timeout = link_async::sleep(timeout).fuse();
        pin_mut!(timeout);
        let mut events = events.fuse();
        loop {
            futures::select! {
diff --git a/librad/src/net/protocol/membership/periodic.rs b/librad/src/net/protocol/membership/periodic.rs
index 6c8e28eb..5c8066f0 100644
--- a/librad/src/net/protocol/membership/periodic.rs
+++ b/librad/src/net/protocol/membership/periodic.rs
@@ -3,20 +3,14 @@
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

use std::{
    fmt::Debug,
    pin::Pin,
    task::{Context, Poll},
    time::Duration,
};
use std::{fmt::Debug, time::Duration};

use futures::{
    future::{self, FutureExt as _},
    future,
    stream::{self, StreamExt as _},
    Stream,
};
use futures_timer::Delay;
use rand::Rng as _;
use link_async::interval;

use super::{Hpv, Shuffle};
use crate::net::{protocol::info::PeerInfo, quic::MAX_IDLE_TIMEOUT};
@@ -35,7 +29,7 @@ where
{
    let params = hpv.params();

    let shuffle = Interval::new(params.shuffle_interval, Duration::from_secs(5)).filter_map({
    let shuffle = interval(params.shuffle_interval, Duration::from_secs(5)).filter_map({
        let hpv = hpv.clone();
        move |_| {
            let p = hpv.shuffle().map(Periodic::Shuffle);
@@ -46,18 +40,17 @@ where
        }
    });

    let promote =
        Interval::new(params.promote_interval, Duration::from_secs(5)).filter_map(move |_| {
            let candidates = hpv.choose_passive_to_promote();
            if candidates.is_empty() {
                tracing::debug!("nothing to promote");
                future::ready(None)
            } else {
                future::ready(Some(Periodic::RandomPromotion { candidates }))
            }
        });
    let promote = interval(params.promote_interval, Duration::from_secs(5)).filter_map(move |_| {
        let candidates = hpv.choose_passive_to_promote();
        if candidates.is_empty() {
            tracing::debug!("nothing to promote");
            future::ready(None)
        } else {
            future::ready(Some(Periodic::RandomPromotion { candidates }))
        }
    });

    let tickle = Interval::new(MAX_IDLE_TIMEOUT.div_f32(2.0), Duration::from_secs(5))
    let tickle = interval(MAX_IDLE_TIMEOUT.div_f32(2.0), Duration::from_secs(5))
        .filter_map(|_| future::ready(Some(Periodic::Tickle)));

    // Wrapping the `select` calls is the most effective to combine the three
@@ -65,40 +58,3 @@ where
    // incur significant overhead.
    stream::select(stream::select(promote, shuffle), tickle)
}

struct Interval {
    delay: Delay,
    duration: Duration,
    jitter: Duration,
}

impl Interval {
    fn new(duration: Duration, jitter: Duration) -> Self {
        Self {
            delay: Delay::new(duration),
            duration,
            jitter,
        }
    }
}

impl futures::Stream for Interval {
    type Item = ();

    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
        if let Poll::Ready(()) = self.delay.poll_unpin(cx) {
            let mut rng = rand::thread_rng();
            let jitter = Duration::from_secs(rng.gen_range(0, self.jitter.as_secs()));
            let delay = if rng.gen() {
                self.duration.saturating_add(jitter)
            } else {
                self.duration.saturating_sub(jitter)
            };
            self.get_mut().delay.reset(delay);

            return Poll::Ready(Some(()));
        }

        Poll::Pending
    }
}
diff --git a/librad/src/net/protocol/state.rs b/librad/src/net/protocol/state.rs
index 9f5acba2..f162ceeb 100644
--- a/librad/src/net/protocol/state.rs
+++ b/librad/src/net/protocol/state.rs
@@ -6,6 +6,7 @@
use std::{net::SocketAddr, ops::Deref, sync::Arc};

use futures::future::TryFutureExt as _;
use link_async::Spawner;
use nonzero_ext::nonzero;
use rand_pcg::Pcg64Mcg;
use tracing::Instrument as _;
@@ -24,7 +25,6 @@ use super::{
    TinCans,
};
use crate::{
    executor,
    git::{
        p2p::transport::{GitStream, GitStreamFactory},
        replication,
@@ -55,7 +55,7 @@ pub(super) struct State<S> {
    pub phone: TinCans,
    pub config: StateConfig,
    pub caches: cache::Caches,
    pub spawner: Arc<executor::Spawner>,
    pub spawner: Arc<Spawner>,
    pub limits: RateLimits,
}

diff --git a/librad/src/net/quic/endpoint.rs b/librad/src/net/quic/endpoint.rs
index bb61c133..c8d29154 100644
--- a/librad/src/net/quic/endpoint.rs
+++ b/librad/src/net/quic/endpoint.rs
@@ -13,6 +13,7 @@ use std::{

use futures::stream::{BoxStream, StreamExt as _, TryStreamExt as _};
use if_watch::IfWatcher;
use link_async::Spawner;
use nonempty::NonEmpty;
use parking_lot::RwLock;
use quinn::{NewConnection, TransportConfig};
@@ -20,7 +21,6 @@ use socket2::{Domain, Protocol, Socket, Type};

use super::{BoxedIncomingStreams, Connection, Conntrack, Error, Result};
use crate::{
    executor,
    net::{
        connection::{CloseReason, LocalAddr, LocalPeer},
        tls,
@@ -69,7 +69,7 @@ pub struct Endpoint<const R: usize> {
impl<const R: usize> Endpoint<R> {
    pub async fn bind<'a, S>(
        signer: S,
        spawner: &executor::Spawner,
        spawner: &Spawner,
        listen_addr: SocketAddr,
        advertised_addrs: Option<NonEmpty<SocketAddr>>,
        network: Network,
@@ -214,7 +214,7 @@ fn bind_socket(listen_addr: SocketAddr) -> Result<UdpSocket> {

#[tracing::instrument(skip(spawner, listen_addrs))]
async fn ifwatch(
    spawner: &executor::Spawner,
    spawner: &Spawner,
    bound_addr: SocketAddr,
    listen_addrs: Weak<RwLock<BTreeSet<SocketAddr>>>,
) -> io::Result<()> {
diff --git a/librad/src/net/upgrade.rs b/librad/src/net/upgrade.rs
index 1cc1e8ee..745ee7bb 100644
--- a/librad/src/net/upgrade.rs
+++ b/librad/src/net/upgrade.rs
@@ -17,11 +17,10 @@ use std::{
};

use futures::{
    future::{self, TryFutureExt as _},
    future::TryFutureExt as _,
    io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt},
    task::{Context, Poll},
};
use futures_timer::Delay;
use thiserror::Error;

use crate::git::p2p::transport::GitStream;
@@ -273,19 +272,9 @@ where
    let recv = async {
        let mut buf = [0u8; UPGRADE_REQUEST_ENCODING_LEN];
        {
            let timeout = async {
                Delay::new(RECV_UPGRADE_TIMEOUT).await;
                Err(ErrorSource::Timeout)
            };
            let recv = async { Ok(incoming.read_exact(&mut buf).await?) };

            futures::pin_mut!(timeout);
            futures::pin_mut!(recv);

            future::try_select(timeout, recv)
                .map_ok(|ok| future::Either::factor_first(ok).0)
                .map_err(|er| future::Either::factor_first(er).0)
                .await?;
            link_async::timeout(RECV_UPGRADE_TIMEOUT, incoming.read_exact(&mut buf))
                .map_err(|link_async::Elapsed| ErrorSource::Timeout)
                .await??;
        }

        Ok(minicbor::decode(&buf)?)
diff --git a/link-async/Cargo.toml b/link-async/Cargo.toml
new file mode 100644
index 00000000..6d6e3b91
--- /dev/null
+++ b/link-async/Cargo.toml
@@ -0,0 +1,25 @@
[package]
name = "link-async"
version = "0.1.0"
authors = ["Kim Altintop <kim@eagain.st>"]
edition = "2018"
license = "GPL-3.0-or-later"

[lib]
doctest = false
test = false

[dependencies]
blocking = "1.0"
futures-util = "0.3"
rand = "0.8"
thiserror = "1.0"
tracing = "0.1"

[dependencies.tokio]
version = "1.1"
features = ["time"]

[dependencies.tokio-stream]
version = "0.1.8"
features = ["time"]
diff --git a/link-async/src/lib.rs b/link-async/src/lib.rs
new file mode 100644
index 00000000..0ee9841a
--- /dev/null
+++ b/link-async/src/lib.rs
@@ -0,0 +1,10 @@
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

mod spawn;
pub use spawn::{Cancelled, JoinError, Spawner, Stats, Task};

mod time;
pub use time::{interval, sleep, timeout, Elapsed};
diff --git a/librad/src/executor.rs b/link-async/src/spawn.rs
similarity index 98%
rename from librad/src/executor.rs
rename to link-async/src/spawn.rs
index 12527115..cb123b4d 100644
--- a/librad/src/executor.rs
+++ b/link-async/src/spawn.rs
@@ -1,4 +1,5 @@
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
// Copyright © 2021 The Radicle Foundation <hello@radicle.foundation>
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.
@@ -15,7 +16,7 @@ use std::{
    task::{Context, Poll},
};

use futures::FutureExt as _;
use futures_util::FutureExt as _;
use thiserror::Error;
use tracing::Instrument as _;

diff --git a/link-async/src/time.rs b/link-async/src/time.rs
new file mode 100644
index 00000000..e110ec3e
--- /dev/null
+++ b/link-async/src/time.rs
@@ -0,0 +1,99 @@
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

use std::{
    future::Future,
    pin::Pin,
    task::{Context, Poll},
    time::Duration,
};

use futures_util::{FutureExt as _, Stream};
use thiserror::Error;

#[derive(Debug, Error)]
#[error("timeout elapsed")]
pub struct Elapsed;

/// Requires a [`Future`] to complete before the specified duration elapsed.
///
/// If the future completes before the duration elapsed, then the completed
/// value is returned. Otherwise, an error is returned and the future is
/// dropped.
///
/// # Cancellation
///
/// No special measures are taken to cancel the supplied [`Future`] -- it is
/// simply dropped if either the timeout elapsed or the future returned by
/// calling [`timeout`] is dropped. That is, it is the caller's responsibility
/// to ensure cancellation-safety of the provided future.
///
/// It is not currently possible to cancel the timeout, and get back the future
/// for further scheduling.
pub async fn timeout<F, T>(after: Duration, f: F) -> Result<T, Elapsed>
where
    F: Future<Output = T>,
{
    tokio::time::timeout(after, f).await.map_err(|_| Elapsed)
}

/// Wait until `duration` has elapsed.
///
/// No work is performed while awaiting on the sleep future to complete.
/// Awaiting the future can be expected to not return for _at least_ `duration`,
/// but not precisely at the point it elapsed.
///
/// # Cancellation
///
/// A sleep can be cancelled by dropping its future.
pub async fn sleep(duration: Duration) {
    tokio::time::sleep(duration).await
}

/// The [`Stream`] created by [`interval`].
pub struct Interval {
    snooze: Pin<Box<tokio::time::Sleep>>,
    period: Duration,
    jitter: Duration,
}

/// Create a [`Stream`] which yields every `period`.
///
/// Whenever `period` elapses, a new period is calculated by either adding or
/// subtracting a duration between zero and `jitter` to the configured `period`
/// duration. The granularity for jitter is one second.
///
/// # Cancellation
///
/// An interval can be cancelled by dropping it.
pub fn interval(period: Duration, jitter: Duration) -> Interval {
    Interval {
        snooze: Box::pin(tokio::time::sleep(period)),
        period,
        jitter,
    }
}

impl Stream for Interval {
    type Item = ();

    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
        use rand::Rng as _;

        self.snooze.poll_unpin(cx).map(|()| {
            let mut rng = rand::thread_rng();
            let jitter = Duration::from_secs(rng.gen_range(0..=self.jitter.as_secs()));
            let delay = if rng.gen() {
                self.period.saturating_add(jitter)
            } else {
                self.period.saturating_sub(jitter)
            };
            let deadline = tokio::time::Instant::now() + delay;
            self.snooze.as_mut().reset(deadline);

            Some(())
        })
    }
}
diff --git a/test/Cargo.toml b/test/Cargo.toml
index a10a1555..8ab0a03c 100644
--- a/test/Cargo.toml
+++ b/test/Cargo.toml
@@ -63,6 +63,9 @@ path = "../git-trailers"
[dependencies.librad]
path = "../librad"

[dependencies.link-async]
path = "../link-async"

[dependencies.link-canonical]
path = "../link-canonical"
features = [ "derive" ]
@@ -96,7 +99,7 @@ path = "../git-ext"
path = "../git-helpers"

[dependencies.rand]
version = "0.7"
version = "0.8"
features = [ "small_rng" ]

# Note: this MUST always match the exact patch version `quinn` uses
diff --git a/test/src/librad/keys.rs b/test/src/librad/keys.rs
index 862714e8..41152e05 100644
--- a/test/src/librad/keys.rs
+++ b/test/src/librad/keys.rs
@@ -6,7 +6,7 @@
use std::{fmt, ops::Deref};

use proptest::prelude::*;
use rand::{rngs::SmallRng, SeedableRng as _};
use rand::{rngs::SmallRng, Rng as _, SeedableRng as _};
use zeroize::Zeroize;

use librad::{PeerId, PublicKey, SecretKey};
diff --git a/test/src/test/unit.rs b/test/src/test/unit.rs
index abf4483b..8cfbbc7f 100644
--- a/test/src/test/unit.rs
+++ b/test/src/test/unit.rs
@@ -6,6 +6,7 @@
mod git_ext;
mod git_trailers;
mod librad;
mod link_async;
mod link_git_protocol;
mod node_lib;
mod rad_clib;
diff --git a/test/src/test/unit/librad.rs b/test/src/test/unit/librad.rs
index 272dd1ff..bb67355e 100644
--- a/test/src/test/unit/librad.rs
+++ b/test/src/test/unit/librad.rs
@@ -4,7 +4,6 @@
// Linking Exception. For full terms see the included LICENSE file.

mod canonical;
mod executor;
mod git;
mod identities;
mod keys;
diff --git a/test/src/test/unit/librad/net/upgrade.rs b/test/src/test/unit/librad/net/upgrade.rs
index 441f521d..715df4d5 100644
--- a/test/src/test/unit/librad/net/upgrade.rs
+++ b/test/src/test/unit/librad/net/upgrade.rs
@@ -49,17 +49,17 @@ async fn test_upgrade(
    .map(|(_, upgrade)| upgrade)
}

#[async_test]
#[tokio::test]
async fn upgrade_gossip() {
    assert_matches!(test_upgrade(Git).await, Ok(SomeUpgraded::Git(_)))
}

#[async_test]
#[tokio::test]
async fn upgrade_git() {
    assert_matches!(test_upgrade(Gossip).await, Ok(SomeUpgraded::Gossip(_)))
}

#[async_test]
#[tokio::test]
async fn upgrade_membership() {
    assert_matches!(
        test_upgrade(Membership).await,
@@ -67,7 +67,7 @@ async fn upgrade_membership() {
    )
}

#[async_test]
#[tokio::test]
async fn upgrade_interrogation() {
    assert_matches!(
        test_upgrade(Interrogation).await,
diff --git a/test/src/test/unit/librad/executor.rs b/test/src/test/unit/link_async.rs
similarity index 84%
rename from test/src/test/unit/librad/executor.rs
rename to test/src/test/unit/link_async.rs
index caec1a6e..827ce09c 100644
--- a/test/src/test/unit/librad/executor.rs
+++ b/test/src/test/unit/link_async.rs
@@ -3,14 +3,14 @@
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

use librad::executor;
use link_async::Spawner;
use tokio::runtime::Runtime;

#[test]
#[should_panic(expected = "task has failed")]
fn unhelpful_panic() {
    Runtime::new().unwrap().block_on(async {
        let spawner = executor::Spawner::from_current().unwrap();
        let spawner = Spawner::from_current().unwrap();
        spawner
            .blocking(|| panic!("you won't see this unless `--nocapture`"))
            .await
--
2.33.1
This patch has merge conflicts with master due to the recently merged
collaborative objects stuff. The fix is straightforward. Is it
acceptable as maintainer to just fix the conflicts in the merge commit
and push to master or should I ask for a re-roll?

Alex
Merged to master

Published-At: https://github.com/alexjg/radicle-link
Master-At: 2afd0f77ecf2cc124fb2dc3adc88d856cf5a4e2b
good to me looks