Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,51 @@ jobs:
- name: cargo check --lib
run: cargo check -p pagedb --target ${{ matrix.target }} --lib ${{ matrix.features }}

# ─────────────────────────────────────────────────────────────────────────
# Runtime wasm coverage. The `wasm` job above only compiles: a wall-clock
# read inside the txn layer compiles fine on wasm32 and panics at the first
# commit ("time not implemented on this platform"), which is invisible to a
# check. This job runs both smoke crates under node — the `opfs` build, where
# a clock exists, and the crate that builds `pagedb` without `opfs` at all,
# where it does not and age retention has to be refused.
wasm-tests:
name: WASM / node smoke (wasm32)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Install Rust + target
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
targets: wasm32-unknown-unknown

- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
prefix-key: wasm-smoke

# The runner must match the wasm-bindgen version the lockfile resolves,
# so read it from `cargo metadata` instead of pinning a number here.
- name: Install wasm-bindgen-test-runner
run: |
version=$(cargo metadata --format-version 1 --locked \
| python3 -c "import json,sys; d=json.load(sys.stdin); print(next(p['version'] for p in d['packages'] if p['name']=='wasm-bindgen'))")
cargo install wasm-bindgen-cli --version "$version" --locked

- name: Run wasm smoke tests (node)
env:
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER: wasm-bindgen-test-runner
run: cargo test -p pagedb-wasm-smoke --target wasm32-unknown-unknown --test commit_smoke

# Same target, the other configuration: `pagedb-wasm-smoke-no-clock`
# depends on `pagedb` without `opfs`, so `clock_available()` is false
# there. This is what runs the refusal — which `wasm-smoke` cannot reach,
# because its own dependency on `opfs` fixes the feature for every test
# target in that build.
- name: Run no-clock wasm smoke tests (node)
env:
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER: wasm-bindgen-test-runner
run: cargo test -p pagedb-wasm-smoke-no-clock --target wasm32-unknown-unknown --test refusal

# ─────────────────────────────────────────────────────────────────────────
features:
name: Feature matrix (${{ matrix.flags }})
Expand Down
125 changes: 125 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["benchmarks/engine-comparison"]
members = ["benchmarks/engine-comparison", "wasm-smoke", "wasm-smoke-no-clock"]
default-members = ["."]
resolver = "3"

Expand Down
18 changes: 12 additions & 6 deletions src/btree/tree/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,15 +620,21 @@ mod tests {
tree.free_page(id);
}

// The loop is functional coverage on every target; the bound is a
// wall-clock regression guard, so it only runs where a clock exists.
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
let start = std::time::Instant::now();
for _ in 0..N {
tree.allocate_page();
}
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(10),
"{N} allocations against {N} held-back frees took {elapsed:?} — \
allocation is scanning the freed list again"
);
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_secs(10),
"{N} allocations against {N} held-back frees took {elapsed:?} — \
allocation is scanning the freed list again"
);
}
}
}
75 changes: 75 additions & 0 deletions src/clock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Wall-clock access.
//!
//! `wasm32-unknown-unknown` has no std clock: `SystemTime::now()` panics with
//! "time not implemented on this platform". The OPFS build reads the host
//! clock through `js_sys` instead; every other target, `wasm32-wasip1`
//! included, uses std.
//!
//! A wasm build *without* the JS bindings has no clock at all, and that case is
//! represented rather than papered over: [`unix_seconds`] returns `None`, and
//! [`clock_available`] lets a caller refuse a policy that a frozen clock would
//! silently neuter. Returning `0` instead would be worse than useless — the
//! commit-history ordering tolerates a zero timestamp, but the age-based
//! retention policy reads it as "nothing is older than the threshold" and stops
//! pruning, so `RetainPolicy::Age` would quietly behave as `Unbounded`.

/// Seconds since the Unix epoch, or `None` when this build has no clock.
///
/// `None` on `wasm32-unknown-unknown` without the `opfs` feature. Every other
/// target answers, `wasm32-wasip1` included.
#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "opfs"))]
pub(crate) fn unix_seconds() -> Option<u64> {
Some((js_sys::Date::now() / 1000.0) as u64)
}

/// No clock in this configuration: `wasm32-unknown-unknown` without the JS
/// bindings. Callers either tolerate the absence (commit-history ordering keeps
/// its total order without timestamps) or refuse the configuration up front
/// (age retention) — see [`clock_available`].
#[cfg(all(target_arch = "wasm32", target_os = "unknown", not(feature = "opfs")))]
pub(crate) fn unix_seconds() -> Option<u64> {
None
}

#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
// Clippy sees only this arm's body, where the answer is always `Some`, and
// calls the wrapper unnecessary. It is not: the `wasm32-unknown-unknown` arm
// above returns `None`, and one signature across the three arms is what lets a
// caller ask "is there a clock" without a second, drift-prone cfg.
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn unix_seconds() -> Option<u64> {
Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
)
}

/// Whether this build can read a wall clock at all.
///
/// A caller that needs time to *mean* something refuses its configuration when
/// this is false; a caller that only needs a total order can read
/// [`unix_seconds`]'s absence as "no timestamp".
pub(crate) const fn clock_available() -> bool {
cfg!(not(all(
target_arch = "wasm32",
target_os = "unknown",
not(feature = "opfs")
)))
}

// The three arms above are selected by a cfg triple, and a cfg that is always
// true compiles into a configuration nobody builds. This pins the direction a
// build can get wrong silently: a `wasm32-unknown-unknown` build without the JS
// bindings that reports a clock anyway, which would leave `Age` unrefused. The
// other direction — `opfs` present, clock absent — is caught at runtime by the
// smoke tests, which commit under age retention on exactly that build. Both
// configurations are compiled in CI: `wasm-smoke` brings `opfs`, and
// `wasm-smoke-no-clock` is the build that has none.
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
const _: () = assert!(
cfg!(feature = "opfs") || !clock_available(),
"a wasm32-unknown-unknown build without `opfs` must report no clock"
);
9 changes: 9 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,15 @@ pub enum PagedbError {
)]
HeaderCapabilityUnsupported { unknown_flags: u32 },

/// A policy that prunes against `now` was requested on a build with no
/// clock, where a zero stand-in would silently prune nothing. Refused at
/// open, before the store is touched.
#[error(
"retain policy {policy} needs a wall clock and this target has none — \
use Unbounded or Count, or build with the `opfs` feature"
)]
RetainPolicyNeedsClock { policy: &'static str },

/// The caller opened the store with a different page size than it was
/// created with.
///
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// must not be able to reach around.
pub(crate) mod btree;
pub(crate) mod catalog;
pub(crate) mod clock;
pub(crate) mod compaction;
pub(crate) mod crypto;
pub(crate) mod diag;
Expand Down
Loading