Skip to content

fix(wasm): avoid std::time::Instant::now() and expand testing#625

Open
pmarks wants to merge 4 commits into
apache:mainfrom
pmarks:pmarks/wasm-fixes-tests
Open

fix(wasm): avoid std::time::Instant::now() and expand testing#625
pmarks wants to merge 4 commits into
apache:mainfrom
pmarks:pmarks/wasm-fixes-tests

Conversation

@pmarks

@pmarks pmarks commented Jan 26, 2026

Copy link
Copy Markdown

Proposed fix to #624 - opening for discussion, I'm happy to rework this.

Currently retry and throttle can crash on WASM due to hitting std::time::Instant::now(), and MultiPart upload relies on tokio::task::JoinSet, which doesn't work on WASM.

TODO: figure out a unit test that actually exercises RetryableRequest in WASM. Existing tests can't run because they need MockServer, which needs TcpListener.

Which issue does this PR close?

Closes #624

Rationale for this change

Solidify WASM support by fixing a couple small issues and greatly expanding WASM test coverage.

What changes are included in this PR?

  • Use n0-futures, which re-exports tokio types on native, and supplies browser-compatible versions on wasm32. Used to get a replacement for tokio::task::JoinSet, used for MultiPart uploads
  • Use sleep() from gloo-timers on wasm instead of tokio::time::sleep which relies on std::Instant::now()
  • To test WASM, we use #[wasm_bindgen_test] instead of #[tokio::test] to mark many existing tests. We can conditionally import the appropriate macro, renaming the macro as async_test then decorate the tests with #[async_test]. These tests will now run under the existing WASM CI.

Are there any user-facing changes?

The main Error enum will change for the WASM build - it will now expose n0_future::task::JoinError instead of tokio::task::JoinError - this is a breaking change.

Currently retry and throttle can crash on WASM due to hitting std::time::Instant::now(), and MultiPart upload relies on tokio::task::JoinSet, which doesn't work on WASM.  So do the following:

- Use n0-futures, which re-exports tokio types on native, and supplies browser-compatible versions on wasm32.  Used to get a replacement for tokio::task::JoinSet, used for MultiPart uploads
- Use sleep() from gloo-timers on wasm instead of tokio::time::sleep which relies on std::Instant::now()
- To test WASM, we use #[wasm_bindgen_test] instead of #[tokio::test] to mark many existing tests.  We can conditionally import the appropriate macro, renaming the macro as `async_test` then decorate the tests with #[async_test]. These tests will now run under the existing WASM CI.
@pmarks
pmarks marked this pull request as ready for review January 27, 2026 08:36
Comment thread src/util.rs
pub(crate) async fn sleep(duration: Duration) {
use send_wrapper::SendWrapper;
if !duration.is_zero() {
SendWrapper::new(gloo_timers::future::sleep(duration)).await

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner future is not actually Send because it contains JS objects. SendWrapper makes it Send, with dynamic checking that it is not used on another thread. This works for standard wasm32-unknown-unknown, and will crash 'safely' if used across threads somehow with Web Workers, so we can figure out how to fix that later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if perhaps we should create our own type that is Send whenever the target arch is not wasm32?

I.e.

//! `MaybeSend` and `MaybeSync` traits for cross-platform async code.
//!
//! Reference: <https://github.com/iced-rs/iced/blob/master/futures/src/maybe.rs>

#[cfg(not(target_arch = "wasm32"))]
mod platform {
    /// A marker trait that enforces `Send` only on native platforms.
    ///
    pub use core::marker::Send as MaybeSend;
    /// A marker trait that enforces `Sync` only on native platforms.
    ///
    pub use core::marker::Sync as MaybeSync;
}

#[cfg(target_arch = "wasm32")]
mod platform {
    /// A marker trait that enforces `Send` only on native platforms.
    pub trait MaybeSend {}

    impl<T> MaybeSend for T {}

    /// A marker trait that enforces `Sync` only on native platforms.
    pub trait MaybeSync {}

    impl<T> MaybeSync for T {}
}

pub use platform::{MaybeSend, MaybeSync};

from https://github.com/zarrs/zarrs/blob/77aeab716ea219049f372ea750eecce4b72fbb7f/zarrs_plugin/src/maybe.rs#L1-L28

@kylebarron kylebarron left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable to me

@crepererum

Copy link
Copy Markdown
Contributor

This is a rather high-level question for wasm32-unknown-unknown: are we assuming that this is ALWAYS a web browser / JavaScript-driven target? Because technically wasm32-unknown-unknown is -- according to the Rust docs -- a "no host" target. So this code could in theory also run within a micro kernel or alongside another WASM module in a freestanding wasmtime execution.

@kylebarron

Copy link
Copy Markdown
Member

Perhaps we should have a web named feature flag, and not automatically assume that wasm32-unknown-unknown will be run in the browser? E.g. https://github.com/cloudflare/workers-rs uses wasm32-unknown-unknown as a compilation target while I'm guessing it doesn't provide web primitives (though I don't know for sure)

@crepererum

Copy link
Copy Markdown
Contributor

Yeah, I would feel more comfortable w/ an explicit web feature. I think that would make it clearer (esp. if we document that, maybe in our top-level crate docs).

@alamb

alamb commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

I am working on organizing a release

Wha do we want to do with this PR?

@kevinjqliu

Copy link
Copy Markdown
Contributor

btw theres a new "feature flags" section in the rust docs

//! # Feature Flags
//!
//! The feature set is layered so that you can pick a provider independently
//! of its HTTP transport:
//!
//! * `cloud-base` holds the shared provider implementation (XML/JSON parsing,
//! credentials, retry, etc.) and intentionally does *not* depend on
//! `reqwest`.
//! * `reqwest` enables the built-in [`reqwest`]-based [`HttpConnector`].
//! * `<provider>-base` (`aws-base`, `azure-base`, `gcp-base`, `http-base`)
//! adds the per-provider logic on top of `cloud-base` without pulling in
//! `reqwest`.
//! * `<provider>` (`aws`, `azure`, `gcp`, `http`) is the batteries-included
//! alias for `<provider>-base` + `reqwest` and is the typical choice.
//!
//! ## Provider features
//!
//! | Feature | Enables | Notes |
//! | --- | --- | --- |
//! | `aws` | `aws-base` + `reqwest` | Amazon S3 with the built-in HTTP transport. |
//! | `azure` | `azure-base` + `reqwest` | Azure Blob Storage with the built-in HTTP transport. |
//! | `gcp` | `gcp-base` + `reqwest` | Google Cloud Storage with the built-in HTTP transport. |
//! | `http` | `http-base` + `reqwest` | HTTP/WebDAV with the built-in HTTP transport. |
//! | `aws-base` | provider only | S3 provider without `reqwest`; supply your own [`HttpConnector`]. |
//! | `azure-base` | provider only | Azure provider without `reqwest`; supply your own [`HttpConnector`]. |
//! | `gcp-base` | provider only | GCS provider without `reqwest`; supply your own [`HttpConnector`]. |
//! | `http-base` | provider only | HTTP/WebDAV provider without `reqwest`; supply your own [`HttpConnector`]. |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash in http in WASM due to hitting std::time::Instant::now()

5 participants