From 97411e90049539d264ea9eda0efd2802548dae6b Mon Sep 17 00:00:00 2001
From: Cristian C
Date: Mon, 24 Aug 2026 21:23:18 +0300
Subject: [PATCH 01/50] ci: add Rust dependency cooldown gate (#713)
Adds a seven-day publication cooldown for newly locked crates.io
versions. Runs it as a reusable, credential-free PR gate and documents
the dependency update workflow.
---------
Co-authored-by: Codex GPT-5.6 Sol
Co-authored-by: Claude Fable 5
---
.cargo/config.toml | 5 +
.../rust-dependency-cooldown/action.yml | 25 ++
.../actions/rust-dependency-cooldown/check.py | 254 ++++++++++++++++++
.../first-party-crates.txt | 10 +
.github/workflows/build-s2-lite.yml | 2 +-
.github/workflows/ci.yml | 29 +-
.github/workflows/release-cli.yml | 2 +-
.../workflows/rust-dependency-cooldown.yml | 25 ++
AGENTS.md | 9 +
README.md | 20 ++
justfile | 14 +-
11 files changed, 377 insertions(+), 18 deletions(-)
create mode 100644 .cargo/config.toml
create mode 100644 .github/actions/rust-dependency-cooldown/action.yml
create mode 100644 .github/actions/rust-dependency-cooldown/check.py
create mode 100644 .github/actions/rust-dependency-cooldown/first-party-crates.txt
create mode 100644 .github/workflows/rust-dependency-cooldown.yml
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 00000000..f27aa58b
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+[unstable]
+min-publish-age = true
+
+[registry]
+global-min-publish-age = "7 days"
diff --git a/.github/actions/rust-dependency-cooldown/action.yml b/.github/actions/rust-dependency-cooldown/action.yml
new file mode 100644
index 00000000..72093d91
--- /dev/null
+++ b/.github/actions/rust-dependency-cooldown/action.yml
@@ -0,0 +1,25 @@
+name: Rust dependency cooldown gate
+description: Reject new crates.io versions that are inside the publication cooldown
+
+inputs:
+ base-sha:
+ description: Pull request base commit
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Check dependency publish age
+ shell: bash
+ env:
+ ACTION_PATH: ${{ github.action_path }}
+ BASE_SHA: ${{ inputs.base-sha }}
+ run: |
+ env -i \
+ HOME="$RUNNER_TEMP" \
+ PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
+ python3 "$ACTION_PATH/check.py" \
+ --repo-root "$GITHUB_WORKSPACE" \
+ --config "$GITHUB_WORKSPACE/.cargo/config.toml" \
+ --allowlist "$ACTION_PATH/first-party-crates.txt" \
+ "$BASE_SHA"
diff --git a/.github/actions/rust-dependency-cooldown/check.py b/.github/actions/rust-dependency-cooldown/check.py
new file mode 100644
index 00000000..535e2884
--- /dev/null
+++ b/.github/actions/rust-dependency-cooldown/check.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python3
+"""Reject newly locked crates.io versions that are too new."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import subprocess
+import sys
+import time
+import tomllib
+import urllib.error
+import urllib.request
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+
+
+ACTION_ROOT = Path(__file__).resolve().parent
+REPO_ROOT = Path.cwd()
+CONFIG = REPO_ROOT / ".cargo" / "config.toml"
+ALLOWLIST = ACTION_ROOT / "first-party-crates.txt"
+CRATES_IO_SOURCE = "registry+https://github.com/rust-lang/crates.io-index"
+INDEX_BASE = "https://index.crates.io"
+USER_AGENT = "s2 minimum-publish-age check (github.com/s2-streamstore/s2)"
+RETRY_ATTEMPTS = 4
+RETRY_BASE_DELAY_SECONDS = 2
+UNIT_SECONDS = {
+ "second": 1,
+ "seconds": 1,
+ "minute": 60,
+ "minutes": 60,
+ "hour": 3600,
+ "hours": 3600,
+ "day": 86400,
+ "days": 86400,
+ "week": 604800,
+ "weeks": 604800,
+ "month": 2592000,
+ "months": 2592000,
+}
+RELEVANT_PATHS = (
+ ":(glob)**/Cargo.lock",
+ ".cargo/config.toml",
+ ".github/actions/rust-dependency-cooldown",
+ ".github/workflows/rust-dependency-cooldown.yml",
+)
+
+
+def minimum_age() -> tuple[timedelta, str]:
+ with CONFIG.open("rb") as config_file:
+ raw = tomllib.load(config_file).get("registry", {}).get("global-min-publish-age")
+ if not isinstance(raw, str):
+ raise ValueError(f"registry.global-min-publish-age is not set in {CONFIG}")
+ value = raw.strip()
+ if value == "0":
+ return timedelta(0), value
+ match = re.fullmatch(r"(\d+)\s+(\w+)", value)
+ if match is None or match.group(2) not in UNIT_SECONDS:
+ raise ValueError(f"cannot parse global-min-publish-age = {value!r}")
+ return timedelta(seconds=int(match.group(1)) * UNIT_SECONDS[match.group(2)]), value
+
+
+def allowed_crates() -> set[str]:
+ return {
+ name
+ for line in ALLOWLIST.read_text().splitlines()
+ if (name := line.split("#", 1)[0].strip())
+ }
+
+
+def crates_io_versions(lock_text: str) -> set[tuple[str, str]]:
+ packages = tomllib.loads(lock_text).get("package", [])
+ return {
+ (package["name"], package["version"])
+ for package in packages
+ if package.get("source") == CRATES_IO_SOURCE
+ }
+
+
+def run_git(*args: str) -> subprocess.CompletedProcess[str]:
+ return subprocess.run(
+ ["git", *args],
+ cwd=REPO_ROOT,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+
+
+def lock_at(revision: str, path: str) -> str | None:
+ result = run_git("show", f"{revision}:{path}")
+ return result.stdout if result.returncode == 0 else None
+
+
+def has_relevant_changes(base_ref: str) -> bool:
+ result = run_git("diff", "--quiet", base_ref, "HEAD", "--", *RELEVANT_PATHS)
+ if result.returncode == 1:
+ return True
+ if result.returncode != 0:
+ raise RuntimeError(
+ f"cannot compare committed Rust dependency cooldown paths: {result.stderr.strip()}"
+ )
+
+ result = run_git("diff", "--quiet", "HEAD", "--", *RELEVANT_PATHS)
+ if result.returncode == 1:
+ return True
+ if result.returncode != 0:
+ raise RuntimeError(
+ f"cannot compare working Rust dependency cooldown paths: {result.stderr.strip()}"
+ )
+
+ result = run_git("ls-files", "--others", "--exclude-standard", "--", *RELEVANT_PATHS)
+ if result.returncode:
+ raise RuntimeError(
+ f"cannot list untracked Rust dependency cooldown paths: {result.stderr.strip()}"
+ )
+ return bool(result.stdout.strip())
+
+
+def index_url(name: str) -> str:
+ normalized = name.lower()
+ if len(normalized) == 1:
+ path = f"1/{normalized}"
+ elif len(normalized) == 2:
+ path = f"2/{normalized}"
+ elif len(normalized) == 3:
+ path = f"3/{normalized[0]}/{normalized}"
+ else:
+ path = f"{normalized[:2]}/{normalized[2:4]}/{normalized}"
+ return f"{INDEX_BASE}/{path}"
+
+
+def fetch_index(name: str) -> str:
+ request = urllib.request.Request(index_url(name), headers={"User-Agent": USER_AGENT})
+ last_error: Exception | None = None
+ for attempt in range(1, RETRY_ATTEMPTS + 1):
+ if attempt > 1:
+ time.sleep(RETRY_BASE_DELAY_SECONDS * (attempt - 1))
+ try:
+ with urllib.request.urlopen(request, timeout=30) as response:
+ raw = response.read()
+ except urllib.error.HTTPError as error:
+ if error.code < 500 and error.code != 429:
+ raise RuntimeError(f"cannot read the crates.io index for {name}: {error}") from error
+ last_error = error
+ continue
+ except (urllib.error.URLError, TimeoutError) as error:
+ last_error = error
+ continue
+ try:
+ return raw.decode()
+ except UnicodeDecodeError as error:
+ raise RuntimeError(f"cannot read the crates.io index for {name}: {error}") from error
+ raise RuntimeError(
+ f"cannot read the crates.io index for {name} after {RETRY_ATTEMPTS} attempts: {last_error}"
+ ) from last_error
+
+
+def publication_times(name: str) -> dict[str, datetime]:
+ times: dict[str, datetime] = {}
+ for line in fetch_index(name).splitlines():
+ if not line.strip():
+ continue
+ entry = json.loads(line)
+ if pubtime := entry.get("pubtime"):
+ times[entry["vers"]] = datetime.fromisoformat(pubtime.replace("Z", "+00:00"))
+ return times
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--repo-root",
+ type=Path,
+ default=Path.cwd(),
+ help="repository whose lock files are checked",
+ )
+ parser.add_argument("--config", type=Path, help="Cargo configuration to read")
+ parser.add_argument("--allowlist", type=Path, help="exact first-party crate names")
+ parser.add_argument(
+ "base_ref",
+ nargs="?",
+ help="check only crates.io versions not present at this Git ref",
+ )
+ return parser.parse_args()
+
+
+def main() -> int:
+ global ALLOWLIST, CONFIG, REPO_ROOT
+
+ args = parse_args()
+ REPO_ROOT = args.repo_root.resolve()
+ CONFIG = (args.config or REPO_ROOT / ".cargo" / "config.toml").resolve()
+ ALLOWLIST = (args.allowlist or ACTION_ROOT / "first-party-crates.txt").resolve()
+ try:
+ if args.base_ref and run_git("rev-parse", "--verify", "--quiet", args.base_ref).returncode:
+ raise ValueError(f"base ref {args.base_ref!r} is not available")
+ if args.base_ref and not has_relevant_changes(args.base_ref):
+ print("No Rust dependency cooldown files changed; check skipped.")
+ return 0
+
+ age_limit, age_text = minimum_age()
+ allowlist = allowed_crates()
+
+ lockfile_result = run_git(
+ "ls-files", "--cached", "--others", "--exclude-standard", "*Cargo.lock"
+ )
+ if lockfile_result.returncode:
+ raise RuntimeError(f"cannot list Cargo.lock files: {lockfile_result.stderr.strip()}")
+ lockfiles = lockfile_result.stdout.splitlines()
+ now = datetime.now(timezone.utc)
+ violations: list[str] = []
+ checked = 0
+ index_cache: dict[str, dict[str, datetime]] = {}
+
+ for lockfile in sorted(lockfiles):
+ proposed = crates_io_versions((REPO_ROOT / lockfile).read_text())
+ baseline: set[tuple[str, str]] = set()
+ if args.base_ref and (text := lock_at(args.base_ref, lockfile)) is not None:
+ baseline = crates_io_versions(text)
+
+ for name, version in sorted(proposed - baseline):
+ if name in allowlist:
+ continue
+ if name not in index_cache:
+ index_cache[name] = publication_times(name)
+ pubtime = index_cache[name].get(version)
+ if pubtime is None:
+ raise RuntimeError(f"no publication time for {name} {version}")
+ checked += 1
+ crate_age = now - pubtime
+ if crate_age < age_limit:
+ violations.append(
+ f"{name} {version} ({lockfile}): published "
+ f"{crate_age.total_seconds() / 86400:.1f} days ago; "
+ f"minimum is {age_text}"
+ )
+ except (OSError, ValueError, RuntimeError, json.JSONDecodeError, tomllib.TOMLDecodeError) as error:
+ print(f"minimum-publish-age error: {error}", file=sys.stderr)
+ return 2
+
+ if violations:
+ print("New crates.io versions are inside the publication cooldown:", file=sys.stderr)
+ for violation in violations:
+ print(f" - {violation}", file=sys.stderr)
+ return 1
+
+ print(f"Checked {checked} new crates.io version(s); all are at least {age_text} old.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/.github/actions/rust-dependency-cooldown/first-party-crates.txt b/.github/actions/rust-dependency-cooldown/first-party-crates.txt
new file mode 100644
index 00000000..f624704e
--- /dev/null
+++ b/.github/actions/rust-dependency-cooldown/first-party-crates.txt
@@ -0,0 +1,10 @@
+# Crates that bypass only the minimum publish age.
+# Use exact crate names. Only add crates that S2 publishes and controls.
+s2-api
+s2-cli
+s2-common
+s2-lite
+s2-resource-spec
+s2-sdk
+s2-storage
+s2-testcontainers
diff --git a/.github/workflows/build-s2-lite.yml b/.github/workflows/build-s2-lite.yml
index bc294910..aedcb617 100644
--- a/.github/workflows/build-s2-lite.yml
+++ b/.github/workflows/build-s2-lite.yml
@@ -53,7 +53,7 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build s2-lite
- run: cargo build --profile ci -p s2-lite
+ run: cargo build --locked --profile ci -p s2-lite
- name: Upload binary
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e29387d..7c8a043e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,6 +10,12 @@ env:
CARGO_TERM_COLOR: always
jobs:
+ rust-dependency-cooldown:
+ name: Rust dependency cooldown gate
+ permissions:
+ contents: read
+ uses: $/.github/workflows/rust-dependency-cooldown.yml
+
changes:
name: Detect Changes
runs-on: ubuntu-latest
@@ -32,7 +38,7 @@ jobs:
cli-schema-drift:
name: CLI Schema Drift
- needs: [changes]
+ needs: [changes, rust-dependency-cooldown]
if: needs.changes.outputs.apply_schema == 'true'
runs-on: ubuntu-latest
steps:
@@ -40,7 +46,7 @@ jobs:
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Generate apply schema
- run: cargo run -q -p s2-cli -- apply --schema > /tmp/apply.schema.json
+ run: cargo run --locked -q -p s2-cli -- apply --schema > /tmp/apply.schema.json
- name: Check for schema drift
run: diff -u cli/schema.json /tmp/apply.schema.json
@@ -86,6 +92,7 @@ jobs:
clippy:
name: Clippy
+ needs: rust-dependency-cooldown
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -99,7 +106,7 @@ jobs:
uses: arduino/setup-protoc@c65c819552d16ad3c9b72d9dfd5ba5237b9c906b # v3.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- - run: cargo clippy --workspace --all-features --all-targets -- -D warnings --allow deprecated
+ - run: cargo clippy --locked --workspace --all-features --all-targets -- -D warnings --allow deprecated
- name: Clippy (simulator)
env:
RUSTFLAGS: --cfg tokio_unstable
@@ -107,6 +114,7 @@ jobs:
test:
name: Tests
+ needs: rust-dependency-cooldown
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -119,10 +127,11 @@ jobs:
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: taiki-e/install-action@d5f9268ff7620505a81ada10ddf18cdd72240185 # nextest
- - run: cargo nextest run --workspace --all-features --exclude s2-sdk --exclude s2-testcontainers -E 'not (package(s2-cli) & binary(integration))'
+ - run: cargo nextest run --locked --workspace --all-features --exclude s2-sdk --exclude s2-testcontainers -E 'not (package(s2-cli) & binary(integration))'
testcontainers:
name: Testcontainers
+ needs: rust-dependency-cooldown
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -156,10 +165,11 @@ jobs:
run: |
docker run --rm ghcr.io/s2-streamstore/s2:${{ steps.image-version.outputs.value }} --version \
| grep -F "rev $S2_GIT_REV"
- - run: cargo test -p s2-testcontainers
+ - run: cargo test --locked -p s2-testcontainers
simulation:
name: Simulation Tests
+ needs: rust-dependency-cooldown
# arm64, deliberately: on x86_64 Linux, fastant (via slatedb -> foyer) runs
# a pre-main TSC calibration loop that spins forever under mad-turmoil's
# interposed clock_gettime, hanging the simulator at startup. On aarch64
@@ -302,6 +312,7 @@ jobs:
build-server:
name: Build s2-lite
+ needs: rust-dependency-cooldown
uses: ./.github/workflows/build-s2-lite.yml
with:
ref: ${{ github.sha }}
@@ -346,7 +357,7 @@ jobs:
"repo": "${{ github.repository }}",
"ref": "${{ github.sha }}",
"lang": "rust",
- "test_cmd": "cargo test -p s2-sdk --all-features -- --skip access_token --skip metrics"
+ "test_cmd": "cargo test --locked -p s2-sdk --all-features -- --skip access_token --skip metrics"
}
]
@@ -360,11 +371,11 @@ jobs:
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Check docs
- run: cargo doc -p s2-sdk --all-features --no-deps
+ run: cargo doc --locked -p s2-sdk --all-features --no-deps
env:
RUSTDOCFLAGS: "-D warnings"
- name: Run tests
- run: cargo test -p s2-sdk --all-features
+ run: cargo test --locked -p s2-sdk --all-features
env:
S2_ACCESS_TOKEN: ${{ secrets.S2_ACCESS_TOKEN_FOR_RUST_SDK_TESTS }}
@@ -384,6 +395,6 @@ jobs:
"repo": "${{ github.repository }}",
"ref": "${{ github.sha }}",
"lang": "rust",
- "test_cmd": "cargo test -p s2-cli --test integration -j 1"
+ "test_cmd": "cargo test --locked -p s2-cli --test integration -j 1"
}
]
diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml
index cf06b9c7..42fddca5 100644
--- a/.github/workflows/release-cli.yml
+++ b/.github/workflows/release-cli.yml
@@ -138,7 +138,7 @@ jobs:
# an official release artifact for install-channel detection.
S2_BUILD_CHANNEL: release
S2_GIT_REV: ${{ steps.source_revision.outputs.value }}
- run: ${{ matrix.builder || 'cargo' }} build --release --package s2-cli --target ${{ matrix.target }}
+ run: ${{ matrix.builder || 'cargo' }} build --locked --release --package s2-cli --target ${{ matrix.target }}
- name: Verify source revision stamp
shell: bash
env:
diff --git a/.github/workflows/rust-dependency-cooldown.yml b/.github/workflows/rust-dependency-cooldown.yml
new file mode 100644
index 00000000..067b8edc
--- /dev/null
+++ b/.github/workflows/rust-dependency-cooldown.yml
@@ -0,0 +1,25 @@
+name: Rust dependency cooldown gate
+
+on:
+ workflow_call:
+
+permissions:
+ contents: read
+
+jobs:
+ rust-dependency-cooldown:
+ name: Rust dependency cooldown gate
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout repository
+ if: github.event_name == 'pull_request'
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ fetch-depth: 2
+ persist-credentials: false
+ - name: Check Rust dependency cooldown
+ if: github.event_name == 'pull_request'
+ uses: $/.github/actions/rust-dependency-cooldown
+ with:
+ base-sha: HEAD^1
diff --git a/AGENTS.md b/AGENTS.md
index 0c2419c1..e2ad9644 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -3,3 +3,12 @@
- Formatting: run `just fmt`
- Tests: run `just test`
- PR title + description become the squashed commit message at merge time; use conventional commit format
+
+## Cargo Dependency Safety
+
+- Use `--locked` for Cargo commands that build, check, test, run, document, fetch, or read metadata.
+- The simulator is temporarily exempt until its separate lockfile is regenerated.
+- Use `cargo +nightly add`, `cargo +nightly update`, `cargo +nightly remove`, or `cargo +nightly generate-lockfile` for dependency changes. The repository Cargo configuration applies the publication cooldown.
+- Do not use the stable forms of these dependency commands.
+- Do not edit dependency declarations or `Cargo.lock` directly.
+- Do not install Cargo tools as part of a coding task.
diff --git a/README.md b/README.md
index 1bac9c63..fdee5d52 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,26 @@ This repository contains:
- **[s2-lite](lite/)** - An open source, self-hostable server implementation of the [S2 API](https://s2.dev/docs/api)
- **[s2-sdk](sdk/)** - The official Rust SDK for S2
+## Development
+
+Use the nightly Cargo dependency commands so that the repository publication cooldown applies:
+
+```bash
+cargo +nightly add
+cargo +nightly update
+cargo +nightly update -p
+cargo +nightly remove
+cargo +nightly generate-lockfile
+```
+
+Use `--locked` with normal build, check, test, run, document, fetch, and metadata commands. The simulator is temporarily exempt until its separate lockfile is regenerated. The pull request dependency check verifies every proposed lock-file change before Rust build jobs start.
+
+Install the repository Cargo tools from Homebrew bottles:
+
+```bash
+brew install cargo-deny cargo-nextest
+```
+
## Installation
### Homebrew (macOS/Linux)
diff --git a/justfile b/justfile
index f3a9cad7..7fbc7058 100644
--- a/justfile
+++ b/justfile
@@ -12,7 +12,7 @@ build *args: sync
# Run clippy linter
clippy *args: sync
- cargo clippy --workspace --all-features --all-targets {{args}} -- -D warnings --allow deprecated
+ cargo clippy --locked --workspace --all-features --all-targets {{args}} -- -D warnings --allow deprecated
# Run clippy on the simulator (separate workspace)
sim-clippy *args:
@@ -20,7 +20,7 @@ sim-clippy *args:
# Ensure cargo-deny is installed
_ensure-deny:
- @cargo deny --version > /dev/null 2>&1 || cargo install cargo-deny
+ @cargo deny --version > /dev/null 2>&1 || (echo "cargo-deny is required; run: brew install cargo-deny" && exit 1)
# Run cargo-deny checks
deny *args: _ensure-deny
@@ -37,20 +37,20 @@ fmt: _ensure-nightly
# Ensure cargo-nextest is installed
_ensure-nextest:
- @cargo nextest --version > /dev/null 2>&1 || cargo install cargo-nextest
+ @cargo nextest --version > /dev/null 2>&1 || (echo "cargo-nextest is required; run: brew install cargo-nextest" && exit 1)
# Run tests with nextest (excludes Docker-backed and live integration tests)
test *args: sync _ensure-nextest
- cargo nextest run --workspace --all-features --exclude s2-testcontainers -E 'not ((package(s2-cli) & binary(integration)) or (package(s2-sdk) & (binary(account_ops) or binary(basin_ops) or binary(metrics_ops) or binary(stream_ops))))' {{args}}
+ cargo nextest run --locked --workspace --all-features --exclude s2-testcontainers -E 'not ((package(s2-cli) & binary(integration)) or (package(s2-sdk) & (binary(account_ops) or binary(basin_ops) or binary(metrics_ops) or binary(stream_ops))))' {{args}}
# Run CLI integration tests (requires s2 lite server running)
test-cli-integration: sync _ensure-nextest
S2_ACCESS_TOKEN=test S2_ACCOUNT_ENDPOINT=http://localhost S2_BASIN_ENDPOINT=http://localhost \
- cargo nextest run -p s2-cli --test integration
+ cargo nextest run --locked -p s2-cli --test integration
# Run SDK integration tests (requires S2_ACCESS_TOKEN and optional custom endpoints)
test-sdk-integration: sync _ensure-nextest
- cargo nextest run -p s2-sdk --test account_ops --test basin_ops --test metrics_ops --test stream_ops
+ cargo nextest run --locked -p s2-sdk --test account_ops --test basin_ops --test metrics_ops --test stream_ops
# Verify Cargo.lock is up-to-date
check-locked:
@@ -67,7 +67,7 @@ clean:
# Run s2-lite
lite *args:
- cargo run --release -p s2-cli -- lite {{args}}
+ cargo run --locked --release -p s2-cli -- lite {{args}}
# Run the s2-lite deterministic simulation (e.g. `just sim smoke --seed 42`,
# `just sim linearizable --seed 42 --clients 3 --ops-per-client 100`)
From a5ac6ac5a0c943d1617b7d0197eb4b035e44eb5a Mon Sep 17 00:00:00 2001
From: Shikhar Bhushan
Date: Tue, 25 Aug 2026 07:09:14 -0700
Subject: [PATCH 02/50] docs: refine descriptions
Signed-off-by: Shikhar Bhushan
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index fdee5d52..9c3eb14e 100644
--- a/README.md
+++ b/README.md
@@ -25,9 +25,9 @@
[s2.dev](https://s2.dev) is a serverless datastore for real-time, streaming data.
This repository contains:
-- **[s2-cli](cli/)** - The official S2 command-line interface
-- **[s2-lite](lite/)** - An open source, self-hostable server implementation of the [S2 API](https://s2.dev/docs/api)
-- **[s2-sdk](sdk/)** - The official Rust SDK for S2
+- **[s2-cli](cli/)** - Command-line interface for S2
+- **[s2-lite](lite/)** - Open source, self-hostable server implementation of the [S2 API](https://s2.dev/docs/api)
+- **[s2-sdk](sdk/)** - Rust SDK for S2
## Development
From 91e1cf3fa35c979b07213061d70d22a0bde7a929 Mon Sep 17 00:00:00 2001
From: Mehul Arora
Date: Tue, 25 Aug 2026 22:56:07 +0530
Subject: [PATCH 03/50] fix(sdk): require http2 (#710)
---
Cargo.lock | 4 +++
cli/Cargo.toml | 3 ++
cli/tests/cli.rs | 81 ++++++++++++++++++++++++++++++++++-------------
sdk/Cargo.toml | 1 +
sdk/src/client.rs | 9 ++++--
sdk/src/error.rs | 12 ++++++-
sim/Cargo.lock | 3 +-
7 files changed, 87 insertions(+), 26 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 39735717..154d4b0c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4949,7 +4949,10 @@ dependencies = [
"dirs",
"fs2",
"futures",
+ "http-body-util",
"humantime",
+ "hyper",
+ "hyper-util",
"indicatif",
"json_to_table",
"keyring",
@@ -5083,6 +5086,7 @@ dependencies = [
"bytes",
"futures-core",
"futures-util",
+ "h2",
"http 1.5.0",
"http-body 1.1.0",
"http-body-util",
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index abbd4eac..a3d1cdbc 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -71,6 +71,9 @@ serde_json = { workspace = true }
[dev-dependencies]
assert_cmd = "2.2"
+http-body-util = "0.1"
+hyper = { version = "1", features = ["http2", "server"] }
+hyper-util = { version = "0.1", features = ["http2", "server", "tokio"] }
predicates = "3.1"
proptest = { workspace = true }
rstest = { workspace = true }
diff --git a/cli/tests/cli.rs b/cli/tests/cli.rs
index 9fdc0716..2baac327 100644
--- a/cli/tests/cli.rs
+++ b/cli/tests/cli.rs
@@ -1,7 +1,9 @@
use std::{
- io::{Read as _, Write as _},
+ convert::Infallible,
net::TcpListener,
+ sync::{Arc, Mutex},
thread::JoinHandle,
+ time::Duration,
};
use assert_cmd::Command;
@@ -29,31 +31,17 @@ struct TestServer {
}
impl TestServer {
+ /// Serves one HTTP/2 request and returns the request line and headers it
+ /// saw. The client speaks h2 with prior knowledge over cleartext.
fn start() -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server");
let endpoint = format!("http://{}", listener.local_addr().expect("server address"));
let handle = std::thread::spawn(move || {
- let (mut stream, _) = listener.accept().expect("accept request");
- let mut request = Vec::new();
- let mut buffer = [0_u8; 4096];
- loop {
- let bytes_read = stream.read(&mut buffer).expect("read request");
- if bytes_read == 0 {
- break;
- }
- request.extend_from_slice(&buffer[..bytes_read]);
- if request.windows(4).any(|window| window == b"\r\n\r\n") {
- break;
- }
- }
- let body = r#"{"basins":[],"has_more":false}"#;
- write!(
- stream,
- "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
- body.len()
- )
- .expect("write response");
- String::from_utf8(request).expect("request is UTF-8")
+ tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .expect("test runtime")
+ .block_on(serve_one_request(listener))
});
Self { endpoint, handle }
}
@@ -63,6 +51,55 @@ impl TestServer {
}
}
+const TEST_SERVER_TIMEOUT: Duration = Duration::from_secs(10);
+
+async fn serve_one_request(listener: TcpListener) -> String {
+ listener
+ .set_nonblocking(true)
+ .expect("non-blocking listener");
+ let listener = tokio::net::TcpListener::from_std(listener).expect("tokio listener");
+ let (stream, _) = tokio::time::timeout(TEST_SERVER_TIMEOUT, listener.accept())
+ .await
+ .expect("timed out waiting for a connection")
+ .expect("accept connection");
+
+ let observed = Arc::new(Mutex::new(None));
+ let captured = observed.clone();
+ let service = hyper::service::service_fn(move |req: hyper::Request| {
+ let captured = captured.clone();
+ async move {
+ let mut rendered = format!("{} {}\r\n", req.method(), req.uri());
+ for (name, value) in req.headers() {
+ rendered.push_str(name.as_str());
+ rendered.push_str(": ");
+ rendered.push_str(value.to_str().unwrap_or_default());
+ rendered.push_str("\r\n");
+ }
+ *captured.lock().expect("capture request") = Some(rendered);
+
+ let body = r#"{"basins":[],"has_more":false}"#;
+ Ok::<_, Infallible>(
+ hyper::Response::builder()
+ .header("content-type", "application/json")
+ .body(http_body_util::Full::new(bytes::Bytes::from(body)))
+ .expect("build response"),
+ )
+ }
+ });
+
+ // A client that exits right after its response can reset the connection,
+ // so the served result only matters when no request came through.
+ let served = tokio::time::timeout(
+ TEST_SERVER_TIMEOUT,
+ hyper::server::conn::http2::Builder::new(hyper_util::rt::TokioExecutor::new())
+ .serve_connection(hyper_util::rt::TokioIo::new(stream), service),
+ )
+ .await;
+
+ let observed = observed.lock().expect("read request").take();
+ observed.unwrap_or_else(|| panic!("server received no HTTP/2 request: {served:?}"))
+}
+
impl TestEnv {
fn new() -> Self {
Self {
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 4593074f..7853e674 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -27,6 +27,7 @@ async-trait = { workspace = true }
bytes = { workspace = true }
futures-core = { workspace = true }
futures-util = { workspace = true }
+h2 = "0.4"
http = { workspace = true }
http-body = "1"
http-body-util = "0.1"
diff --git a/sdk/src/client.rs b/sdk/src/client.rs
index b146e975..110ca1a7 100644
--- a/sdk/src/client.rs
+++ b/sdk/src/client.rs
@@ -29,7 +29,7 @@ use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
pub use hyper_util::client::legacy::connect::Connect;
use hyper_util::{
client::legacy::{Client as HyperClient, connect::HttpConnector},
- rt::TokioExecutor,
+ rt::{TokioExecutor, TokioTimer},
};
use serde::{Serialize, de::DeserializeOwned};
use tokio::{
@@ -784,7 +784,12 @@ where
}
fn create_client(&self) -> PooledClient {
- let client = HyperClient::builder(TokioExecutor::new()).build(self.connector.clone());
+ let client = HyperClient::builder(TokioExecutor::new())
+ .timer(TokioTimer::new())
+ .http2_only(true)
+ .http2_keep_alive_interval(Duration::from_secs(20))
+ .http2_keep_alive_timeout(Duration::from_secs(10))
+ .build(self.connector.clone());
PooledClient::new(client)
}
diff --git a/sdk/src/error.rs b/sdk/src/error.rs
index a99cc1c8..fd42a79e 100644
--- a/sdk/src/error.rs
+++ b/sdk/src/error.rs
@@ -122,10 +122,18 @@ impl From for ClientError {
fn classify_hyper_source(err: &client::HttpError, err_msg: &str) -> Option {
let hyper_err = source_err::(err)?;
let err_msg = format!("{hyper_err} -> {err_msg}");
- if hyper_err.is_incomplete_message() {
+ if hyper_err.is_timeout() {
+ // The h2 keep-alive timing out fails requests sent on the dead connection.
+ Some(ClientError::Timeout)
+ } else if hyper_err.is_incomplete_message() {
Some(ClientError::ConnectionClosedEarly(err_msg))
} else if hyper_err.is_canceled() {
Some(ClientError::RequestCanceled(err_msg))
+ } else if source_err::(err).is_some_and(|e| e.is_io() || e.is_go_away()) {
+ // An I/O failure ends streaming bodies without tripping any hyper marker above.
+ // A remote GOAWAY ends streams dispatched onto a connection the server is
+ // gracefully shutting down.
+ Some(ClientError::ConnectionClosedEarly(err_msg))
} else {
None
}
@@ -136,6 +144,8 @@ fn classify_io_source(err: &client::HttpError, err_msg: &str) -> Option {err_msg}");
Some(match io_err.kind() {
std::io::ErrorKind::UnexpectedEof => ClientError::UnexpectedEof(err_msg),
+ // h2 surfaces a stream cut short by connection shutdown as a broken pipe.
+ std::io::ErrorKind::BrokenPipe => ClientError::ConnectionClosedEarly(err_msg),
std::io::ErrorKind::ConnectionReset => ClientError::ConnectionReset(err_msg),
std::io::ErrorKind::ConnectionAborted => ClientError::ConnectionAborted(err_msg),
std::io::ErrorKind::ConnectionRefused => ClientError::ConnectionRefused(err_msg),
diff --git a/sim/Cargo.lock b/sim/Cargo.lock
index 62fb0269..a2cf1f39 100644
--- a/sim/Cargo.lock
+++ b/sim/Cargo.lock
@@ -3699,7 +3699,7 @@ dependencies = [
[[package]]
name = "s2-lite"
-version = "0.42.6"
+version = "0.42.7"
dependencies = [
"async-stream",
"async-trait",
@@ -3761,6 +3761,7 @@ dependencies = [
"bytes",
"futures-core",
"futures-util",
+ "h2",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
From 7689acd48d7758424496ce0cdcff063e6b45b367 Mon Sep 17 00:00:00 2001
From: "detail-app[bot]" <180357370+detail-app[bot]@users.noreply.github.com>
Date: Mon, 31 Aug 2026 21:20:11 +0530
Subject: [PATCH 04/50] fix(sdk): classify h2 REFUSED_STREAM as retryable
(#716)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**Detail bug report:** [View on
Detail](https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_1ccfeb5d-95f9-46a0-badf-3561947108b4)
Closes #715
## Bug
HTTP/2 `RST_STREAM` frames with `REFUSED_STREAM` were classified as
non-retryable `ClientError::Other` instead of retryable
`ClientError::ConnectionClosedEarly`. The h2 classification in
`classify_hyper_source` only handled `is_io() || is_go_away()`, missing
`RST_STREAM` resets entirely. Since RFC 9113 §8.7 guarantees a
`REFUSED_STREAM` is sent before any processing occurred (so the request,
including non-idempotent methods, can be safely retried), the client was
failing instead of retrying.
## Fix
Extracted the nested `h2::Error` handling in `sdk/src/error.rs` into a
`classify_h2_error` helper and added a branch that classifies any h2
error with `reason() == Some(h2::Reason::REFUSED_STREAM)` as
`ConnectionClosedEarly` (retryable). I/O and GOAWAY h2 errors keep their
existing classification; all other reasons (and unknown/future codes)
still fall through to non-retryable `Other`.
The check is **reason-based, not `is_reset()`-based**. h2 surfaces a
received `RST_STREAM(REFUSED_STREAM)` in two shapes: a `Reset`-kind
error while reading the response body (`is_reset()==true`), and a
`Reason`-kind error while sending the request body — hyper wraps the
reason from `SendStream::poll_reset` via `h2::Error::from(reason)`,
which has `is_reset()==false`. An `is_reset()`-only check would miss the
request-body path; keying on `reason()` covers both. Other `RST_STREAM`
reasons (`INTERNAL_ERROR`, `CANCEL`, `FLOW_CONTROL_ERROR`,
`STREAM_CLOSED`, …) may indicate partial processing and are
intentionally left non-retryable.
## Testing
- **Unit tests** (`sdk/src/error.rs`): 4 new tests covering
`classify_h2_error` — REFUSED_STREAM classifies as retryable
`ConnectionClosedEarly`; it does so even when `!is_io() && !is_go_away()
&& !is_reset()` (the request-body path); all other named reasons and
unknown codes stay non-retryable; and the `ConnectionClosedEarly` vs
`Other` retryability contract holds. The full SDK unit suite passes
(110/110).
- **End-to-end test** (`sdk/tests/refused_stream_retry.rs`): a
self-contained integration test that spins up an h2 prior-knowledge
server which `RST_STREAM(REFUSED_STREAM)`s the first request and serves
a valid `list_basins` response on the retry. The SDK retries and
succeeds, and the server is observed to receive ≥2 requests. This
exercises the full `HttpError → classify_hyper_source →
classify_h2_error → ClientError → retry loop` path that can't be
unit-tested (hyper/h2 errors have no public constructors). Reverting the
fix makes this test fail with the exact bug symptom `Client(Other("send
error: client error (SendRequest)"))`, confirming it guards against
regression.
- **Live server integration**: ran `stream_ops` + `basin_ops` +
`account_ops` (non-token) against a local `s2 lite` server (HTTP/2
prior-knowledge over cleartext) — all pass, including the
producer/append-session happy-path tests. `metrics_ops` and the
`account_ops` access-token management tests return `501 not_implemented`
from `s2 lite` (a known limitation the CI `sdk-tests.yml` skips via
`--skip access_token --skip metrics`); these run against a full S2
server in CI and are unrelated to this change.
- **Could not verify**: the bug report's Evidence 3 claims oversized
request headers trigger `REFUSED_STREAM`. In h2 0.4.16, oversized
headers actually yield an HTTP `431` response
(`proto/streams/recv.rs:207-234`), not a per-stream `REFUSED_STREAM`
(the cited `recv.rs:1041` is the `max_concurrent_streams` `refused`
path). The fix is reason-based and trigger-agnostic, so any genuine
`REFUSED_STREAM` source is covered identically by the unit and
end-to-end tests.
- Routine checks (typecheck, clippy with `-D warnings` across all
targets, `cargo +nightly fmt --all --check`, and `cargo doc` with `-D
warnings`) all pass.
---
_Automatic Fixes PRs can be [configured
here](https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/settings/repos/repo_c4bd6a47-9b7d-4b62-9c18-8cf0ac18a8f9/bugs)._
---------
Co-authored-by: detail-app[bot] <180357370+detail-app[bot]@users.noreply.github.com>
Co-authored-by: Mehul Arora
---
sdk/src/error.rs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/sdk/src/error.rs b/sdk/src/error.rs
index fd42a79e..b8dd89a9 100644
--- a/sdk/src/error.rs
+++ b/sdk/src/error.rs
@@ -129,7 +129,9 @@ fn classify_hyper_source(err: &client::HttpError, err_msg: &str) -> Option(err).is_some_and(|e| e.is_io() || e.is_go_away()) {
+ } else if source_err::(err).is_some_and(|e| {
+ e.is_io() || e.is_go_away() || e.reason() == Some(h2::Reason::REFUSED_STREAM)
+ }) {
// An I/O failure ends streaming bodies without tripping any hyper marker above.
// A remote GOAWAY ends streams dispatched onto a connection the server is
// gracefully shutting down.
From 6bcc6a300d344d35d1e5ffbfdbadcc34a9eac838 Mon Sep 17 00:00:00 2001
From: Mehul Arora
Date: Wed, 2 Sep 2026 02:11:31 +0530
Subject: [PATCH 05/50] feat(sdk): act on s2s reconnect advice in append and
read sessions (#703)
---
sdk/src/api.rs | 45 +++++++++-
sdk/src/client.rs | 170 +++++++++++++++++++++++++++++---------
sdk/src/error.rs | 13 ++-
sdk/src/lib.rs | 1 +
sdk/src/reconnect.rs | 62 ++++++++++++++
sdk/src/session/append.rs | 130 +++++++++++++++++++++++++++--
sdk/src/session/read.rs | 109 ++++++++++++++++++++++--
7 files changed, 472 insertions(+), 58 deletions(-)
create mode 100644 sdk/src/reconnect.rs
diff --git a/sdk/src/api.rs b/sdk/src/api.rs
index 2ed7e37a..e518babc 100644
--- a/sdk/src/api.rs
+++ b/sdk/src/api.rs
@@ -42,6 +42,7 @@ use crate::{
client::{self, StreamingResponse, UnaryResponse},
error::{ClientError, server_error_has_no_side_effects, server_error_is_retryable},
frame_signal::FrameSignal,
+ reconnect::ReconnectAdvice,
retry::{RetryBackoff, RetryBackoffBuilder},
types::{
AccessToken, AccessTokenId, AccessTokenMode, AppendRetryPolicy, BasinAuthority, BasinName,
@@ -446,6 +447,7 @@ impl BasinClient {
inputs: I,
encryption: Option<&EncryptionKey>,
frame_signal: Option,
+ reconnect: ReconnectAdvice,
) -> Result, ApiError>
where
I: Stream- + Send + 'static,
@@ -483,13 +485,15 @@ impl BasinClient {
return Err(error);
}
};
- let mut bytes_stream = response.stream();
+ let (mut bytes_stream, poison_handle) = response.into_stream();
let auth_client = self.client.clone();
let mut buffer = BytesMut::new();
let mut decoder = FrameDecoder;
Ok(Box::pin(try_stream! {
+ let mut advice_seen = false;
+
while let Some(chunk) = bytes_stream.next().await {
let chunk = chunk?;
buffer.extend_from_slice(&chunk);
@@ -497,10 +501,18 @@ impl BasinClient {
loop {
match decoder.decode(&mut buffer) {
Ok(Some(SessionMessage::Regular(msg))) => {
+ if !advice_seen && msg.reconnect_advised() {
+ advice_seen = true;
+ poison_handle.poison();
+ reconnect.advise();
+ }
yield msg.try_into_proto()?;
}
Ok(Some(SessionMessage::Terminal(msg))) => {
let error: ApiError = msg.into();
+ if error.is_server_draining() {
+ poison_handle.poison();
+ }
auth_client.invalidate_access_token_if_rejected(
&error,
access_token.as_deref(),
@@ -526,6 +538,7 @@ impl BasinClient {
start: ReadStart,
end: ReadEnd,
encryption: Option<&EncryptionKey>,
+ reconnect: ReconnectAdvice,
) -> Result, ApiError> {
let url = self.uri(format!("v1/streams/{}/records", urlencoding::encode(name)));
@@ -549,13 +562,15 @@ impl BasinClient {
return Err(error);
}
};
- let mut bytes_stream = response.stream();
+ let (mut bytes_stream, poison_handle) = response.into_stream();
let auth_client = self.client.clone();
let mut buffer = BytesMut::new();
let mut decoder = FrameDecoder;
Ok(Box::pin(try_stream! {
+ let mut advice_seen = false;
+
while let Some(chunk) = bytes_stream.next().await {
let chunk = chunk?;
buffer.extend_from_slice(&chunk);
@@ -563,10 +578,18 @@ impl BasinClient {
loop {
match decoder.decode(&mut buffer) {
Ok(Some(SessionMessage::Regular(msg))) => {
+ if !advice_seen && msg.reconnect_advised() {
+ advice_seen = true;
+ poison_handle.poison();
+ reconnect.advise();
+ }
yield msg.try_into_proto()?;
}
Ok(Some(SessionMessage::Terminal(msg))) => {
let error: ApiError = msg.into();
+ if error.is_server_draining() {
+ poison_handle.poison();
+ }
auth_client.invalidate_access_token_if_rejected(
&error,
access_token.as_deref(),
@@ -650,6 +673,14 @@ impl ApiError {
}
}
+ pub(crate) fn is_server_draining(&self) -> bool {
+ matches!(
+ self,
+ Self::Server(StatusCode::SERVICE_UNAVAILABLE, response)
+ if response.code == "server_draining"
+ )
+ }
+
pub(crate) fn is_authentication_error(&self) -> bool {
matches!(
self,
@@ -1170,14 +1201,20 @@ impl StreamingResult for StreamingResponse {
}
let status = self.status();
- let bytes = self.into_bytes().await?;
+ let (bytes, poison_handle) = self.into_bytes_with_poison_handle().await?;
if status == StatusCode::RANGE_NOT_SATISFIABLE
&& let Ok(tail) = serde_json::from_slice::(&bytes)
{
return Err(ApiError::ReadUnwritten(tail));
}
match serde_json::from_slice::(&bytes) {
- Ok(response) => Err(ApiError::Server(status, response)),
+ Ok(response) => {
+ let error = ApiError::Server(status, response);
+ if error.is_server_draining() {
+ poison_handle.poison();
+ }
+ Err(error)
+ }
Err(error) => Err(ApiError::Client(ClientError::ResponseDecode(format!(
"could not decode server error {status}: {error}; body: {}",
String::from_utf8_lossy(&bytes),
diff --git a/sdk/src/client.rs b/sdk/src/client.rs
index 110ca1a7..b38d3f31 100644
--- a/sdk/src/client.rs
+++ b/sdk/src/client.rs
@@ -2,8 +2,8 @@ use std::{
collections::HashMap,
convert::Infallible,
sync::{
- Arc, Mutex,
- atomic::{AtomicUsize, Ordering},
+ Arc, Mutex, RwLock as StdRwLock,
+ atomic::{AtomicU64, AtomicUsize, Ordering},
},
time::{Duration, Instant},
};
@@ -369,20 +369,63 @@ impl UnaryResponse {
}
}
+/// Identifies a pooled connection within its host pool, so poisoning drops
+/// just the connection reconnect advice arrived on.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+struct ConnectionId(u64);
+
+impl ConnectionId {
+ fn next() -> Self {
+ static NEXT: AtomicU64 = AtomicU64::new(0);
+ Self(NEXT.fetch_add(1, Ordering::Relaxed))
+ }
+}
+
+/// Grants its holder the ability to poison the pooled connection a streaming
+/// response was served on, dropping it from the pool so no new request reuses
+/// it. Requests already in flight keep the connection.
+///
+/// Poisoning is idempotent: the connection is identified by its
+/// [`ConnectionId`], so poisoning it again — including from another session
+/// sharing the connection — is a no-op.
+pub struct PoisonHandle {
+ poison: Box,
+}
+
+impl PoisonHandle {
+ fn new(poison: impl Fn() + Send + Sync + 'static) -> Self {
+ Self {
+ poison: Box::new(poison),
+ }
+ }
+
+ pub(crate) fn poison(&self) {
+ (self.poison)();
+ }
+}
+
pub struct StreamingResponse {
status: StatusCode,
headers: HeaderMap,
body: Incoming,
permit: RequestPermit,
+ poison_handle: PoisonHandle,
}
impl StreamingResponse {
- fn new(status: StatusCode, headers: HeaderMap, body: Incoming, permit: RequestPermit) -> Self {
+ fn new(
+ status: StatusCode,
+ headers: HeaderMap,
+ body: Incoming,
+ permit: RequestPermit,
+ poison_handle: PoisonHandle,
+ ) -> Self {
Self {
status,
headers,
body,
permit,
+ poison_handle,
}
}
@@ -390,20 +433,37 @@ impl StreamingResponse {
self.status
}
- pub async fn into_bytes(self) -> Result {
- let bytes = self.body.collect().await?.to_bytes();
- decompress_body(&self.headers, bytes).await
+ pub(crate) async fn into_bytes_with_poison_handle(
+ self,
+ ) -> Result<(Bytes, PoisonHandle), HttpError> {
+ let Self {
+ headers,
+ body,
+ permit,
+ poison_handle,
+ ..
+ } = self;
+ let bytes = body.collect().await?.to_bytes();
+ let bytes = decompress_body(&headers, bytes).await?;
+ drop(permit);
+ Ok((bytes, poison_handle))
}
- pub fn stream(self) -> impl Stream
- > {
- let permit = self.permit;
- http_body_util::BodyStream::new(self.body).filter_map(move |result| {
+ pub fn into_stream(self) -> (impl Stream
- >, PoisonHandle) {
+ let Self {
+ body,
+ permit,
+ poison_handle,
+ ..
+ } = self;
+ let stream = http_body_util::BodyStream::new(body).filter_map(move |result| {
let _ = &permit;
std::future::ready(match result {
Ok(frame) => frame.into_data().ok().map(Ok),
Err(e) => Some(Err(HttpError::Receive(e))),
})
- })
+ });
+ (stream, poison_handle)
}
}
@@ -577,6 +637,7 @@ async fn init_streaming_with(
client: &HyperClient,
request: Request,
permit: RequestPermit,
+ poison_handle: PoisonHandle,
) -> Result
where
C: Connect + Clone + Send + Sync + 'static,
@@ -600,6 +661,7 @@ where
parts.headers,
body,
permit,
+ poison_handle,
))
};
@@ -729,6 +791,7 @@ impl Drop for RequestPermit {
}
struct PooledClient {
+ id: ConnectionId,
client: Arc>,
active_requests: Arc,
idle_since: Arc>>,
@@ -737,6 +800,7 @@ struct PooledClient {
impl PooledClient {
fn new(client: HyperClient) -> Self {
Self {
+ id: ConnectionId::next(),
client: Arc::new(client),
active_requests: Arc::new(AtomicUsize::new(0)),
idle_since: Arc::new(Mutex::new(Some(Instant::now()))),
@@ -768,7 +832,7 @@ impl PooledClient {
}
struct HostPool {
- clients: RwLock>>,
+ clients: StdRwLock>>,
connector: C,
}
@@ -778,7 +842,7 @@ where
{
fn new(connector: C) -> Self {
Self {
- clients: RwLock::new(Vec::new()),
+ clients: StdRwLock::new(Vec::new()),
connector,
}
}
@@ -793,19 +857,19 @@ where
PooledClient::new(client)
}
- async fn checkout(&self) -> (Arc>, RequestPermit) {
+ fn checkout(&self) -> (Arc>, RequestPermit, ConnectionId) {
{
- let clients = self.clients.read().await;
+ let clients = self.clients.read().unwrap();
for pooled in clients.iter() {
if let Some(permit) = pooled.request_permit() {
- return (pooled.client.clone(), permit);
+ return (pooled.client.clone(), permit, pooled.id);
}
}
}
- let mut clients = self.clients.write().await;
+ let mut clients = self.clients.write().unwrap();
for pooled in clients.iter() {
if let Some(permit) = pooled.request_permit() {
- return (pooled.client.clone(), permit);
+ return (pooled.client.clone(), permit, pooled.id);
}
}
let new_client = self.create_client();
@@ -813,14 +877,27 @@ where
.request_permit()
.expect("new client must have a permit");
let client = new_client.client.clone();
+ let id = new_client.id;
clients.push(new_client);
- (client, permit)
+ (client, permit, id)
}
- async fn reap_idle_clients(&self) {
+ /// Drop the pooled client identified by `id` so no new request reuses it.
+ /// Clients pinned to servers that are not going away stay pooled, requests
+ /// already in flight keep their connection, and poisoning the same
+ /// connection again is a no-op.
+ fn poison(&self, host: &str, id: ConnectionId) {
+ let mut clients = self.clients.write().unwrap();
+ let pooled = clients.len();
+ clients.retain(|pooled| pooled.id != id);
+ let removed = pooled - clients.len();
+ tracing::debug!(host, connection = ?id, removed, "poisoned pooled connections");
+ }
+
+ fn reap_idle_clients(&self) {
self.clients
.write()
- .await
+ .unwrap()
.retain(|pooled| !pooled.should_reap(IDLE_TIMEOUT));
}
@@ -877,8 +954,11 @@ where
.clone()
}
- async fn checkout(&self, host: &str) -> (Arc>, RequestPermit) {
- self.get_or_create_host_pool(host).await.checkout().await
+ async fn checkout(
+ &self,
+ host: &str,
+ ) -> (Arc>, RequestPermit, ConnectionId) {
+ self.get_or_create_host_pool(host).await.checkout()
}
}
@@ -891,7 +971,7 @@ async fn reap_idle_clients(
};
for pool in &pools {
- pool.reap_idle_clients().await;
+ pool.reap_idle_clients();
}
hosts.write().await.retain(|_, pool| !pool.is_empty());
@@ -903,13 +983,23 @@ where
C: Connect + Clone + Send + Sync + 'static,
{
async fn execute_unary(&self, request: Request) -> Result {
- let (client, _permit) = self.checkout(request.authority()).await;
+ let (client, _permit, _) = self.checkout(request.authority()).await;
execute_unary_with(&client, request).await
}
async fn init_streaming(&self, request: Request) -> Result {
- let (client, permit) = self.checkout(request.authority()).await;
- init_streaming_with(&client, request, permit).await
+ let host = request.authority().to_owned();
+ let pool = self.get_or_create_host_pool(&host).await;
+ let (client, permit, id) = pool.checkout();
+ // Weak: the handle can outlive the pool (a session holds it for the
+ // connection's lifetime) and must not keep a reaped pool alive.
+ let weak = Arc::downgrade(&pool);
+ let poison_handle = PoisonHandle::new(move || {
+ if let Some(pool) = weak.upgrade() {
+ pool.poison(&host, id);
+ }
+ });
+ init_streaming_with(&client, request, permit, poison_handle).await
}
}
@@ -960,7 +1050,7 @@ mod tests {
async fn host_client_count(pool: &Pool, host: &str) -> usize {
let hosts = pool.hosts.read().await;
match hosts.get(host) {
- Some(pool) => pool.clients.read().await.len(),
+ Some(pool) => pool.clients.read().unwrap().len(),
None => 0,
}
}
@@ -1003,7 +1093,7 @@ mod tests {
let pool = test_pool();
let mut permits = Vec::new();
for _ in 0..MAX_CONCURRENT_REQUESTS_PER_CLIENT {
- let (_client, permit) = pool.checkout(TEST_HOST).await;
+ let (_client, permit, _) = pool.checkout(TEST_HOST).await;
permits.push(permit);
}
assert_eq!(host_client_count(&pool, TEST_HOST).await, 1);
@@ -1014,12 +1104,12 @@ mod tests {
let pool = test_pool();
let mut permits = Vec::new();
for _ in 0..MAX_CONCURRENT_REQUESTS_PER_CLIENT {
- let (_client, permit) = pool.checkout(TEST_HOST).await;
+ let (_client, permit, _) = pool.checkout(TEST_HOST).await;
permits.push(permit);
}
assert_eq!(host_client_count(&pool, TEST_HOST).await, 1);
- let (_client, permit) = pool.checkout(TEST_HOST).await;
+ let (_client, permit, _) = pool.checkout(TEST_HOST).await;
permits.push(permit);
assert_eq!(host_client_count(&pool, TEST_HOST).await, 2);
}
@@ -1029,12 +1119,12 @@ mod tests {
let pool = test_pool();
let mut permits = Vec::new();
for _ in 0..MAX_CONCURRENT_REQUESTS_PER_CLIENT {
- let (_client, permit) = pool.checkout(TEST_HOST).await;
+ let (_client, permit, _) = pool.checkout(TEST_HOST).await;
permits.push(permit);
}
permits.pop();
- let (_client, permit) = pool.checkout(TEST_HOST).await;
+ let (_client, permit, _) = pool.checkout(TEST_HOST).await;
permits.push(permit);
assert_eq!(host_client_count(&pool, TEST_HOST).await, 1);
}
@@ -1044,10 +1134,10 @@ mod tests {
let pool = test_pool();
let mut permits = Vec::new();
for _ in 0..MAX_CONCURRENT_REQUESTS_PER_CLIENT {
- let (_client, permit) = pool.checkout(TEST_HOST).await;
+ let (_client, permit, _) = pool.checkout(TEST_HOST).await;
permits.push(permit);
}
- let (_client, permit) = pool.checkout(TEST_HOST).await;
+ let (_client, permit, _) = pool.checkout(TEST_HOST).await;
permits.push(permit);
assert_eq!(host_client_count(&pool, TEST_HOST).await, 2);
@@ -1055,7 +1145,7 @@ mod tests {
{
let hosts = pool.hosts.read().await;
let pool = hosts.get(TEST_HOST).unwrap();
- let clients = pool.clients.read().await;
+ let clients = pool.clients.read().unwrap();
for pooled in clients.iter() {
*pooled.idle_since.lock().unwrap() =
Some(Instant::now() - IDLE_TIMEOUT - Duration::from_secs(1));
@@ -1075,12 +1165,12 @@ mod tests {
let mut permits_a = Vec::new();
for _ in 0..MAX_CONCURRENT_REQUESTS_PER_CLIENT {
- let (_client, permit) = pool.checkout(host_a).await;
+ let (_client, permit, _) = pool.checkout(host_a).await;
permits_a.push(permit);
}
assert_eq!(host_client_count(&pool, host_a).await, 1);
- let (_client, permit_b) = pool.checkout(host_b).await;
+ let (_client, permit_b, _) = pool.checkout(host_b).await;
assert_eq!(host_client_count(&pool, host_b).await, 1);
assert_eq!(host_client_count(&pool, host_a).await, 1);
@@ -1092,15 +1182,15 @@ mod tests {
async fn reaper_removes_empty_host_entries() {
let pool = test_pool();
- let (_client, permit_a) = pool.checkout("host-a:443").await;
- let (_client, permit_b) = pool.checkout("host-b:443").await;
+ let (_client, permit_a, _) = pool.checkout("host-a:443").await;
+ let (_client, permit_b, _) = pool.checkout("host-b:443").await;
assert_eq!(pool.hosts.read().await.len(), 2);
drop(permit_a);
{
let hosts = pool.hosts.read().await;
let pool_a = hosts.get("host-a:443").unwrap();
- let clients = pool_a.clients.read().await;
+ let clients = pool_a.clients.read().unwrap();
for pooled in clients.iter() {
*pooled.idle_since.lock().unwrap() =
Some(Instant::now() - IDLE_TIMEOUT - Duration::from_secs(1));
diff --git a/sdk/src/error.rs b/sdk/src/error.rs
index b8dd89a9..7fd07726 100644
--- a/sdk/src/error.rs
+++ b/sdk/src/error.rs
@@ -125,7 +125,9 @@ fn classify_hyper_source(err: &client::HttpError, err_msg: &str) -> Option bool {
+ matches!(
+ self,
+ Self::Server(error)
+ if error.status == StatusCode::SERVICE_UNAVAILABLE
+ && error.code == "server_draining"
+ )
+ }
}
impl From for RequestError {
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
index ded5890b..5699fea9 100644
--- a/sdk/src/lib.rs
+++ b/sdk/src/lib.rs
@@ -92,6 +92,7 @@ issue.
mod api;
mod client;
mod frame_signal;
+mod reconnect;
mod session;
pub mod batching;
diff --git a/sdk/src/reconnect.rs b/sdk/src/reconnect.rs
new file mode 100644
index 00000000..9df29362
--- /dev/null
+++ b/sdk/src/reconnect.rs
@@ -0,0 +1,62 @@
+use std::{
+ sync::{
+ Arc,
+ atomic::{AtomicBool, Ordering},
+ },
+ time::Duration,
+};
+
+use tokio::time::Instant;
+
+/// Advised reconnects to attempt before staying on.
+pub(crate) const MAX_ADVISED_RECONNECTS: usize = 1;
+
+/// Gap after which the attempt count resets.
+pub(crate) const ADVISED_RECONNECT_IDLE: Duration = Duration::from_secs(60);
+
+/// Advised reconnects attempted lately.
+#[derive(Clone, Copy, Default)]
+pub(crate) struct AdvisedReconnects {
+ count: usize,
+ last: Option,
+}
+
+impl AdvisedReconnects {
+ pub(crate) fn record(&mut self) {
+ if !self.is_recent() {
+ self.count = 0;
+ }
+ self.last = Some(Instant::now());
+ self.count += 1;
+ }
+
+ /// Whether to act on advice, or stay until the server ends the connection.
+ pub(crate) fn should_reconnect(&self) -> bool {
+ !self.is_recent() || self.count < MAX_ADVISED_RECONNECTS
+ }
+
+ pub(crate) fn count(&self) -> usize {
+ self.count
+ }
+
+ fn is_recent(&self) -> bool {
+ self.last
+ .is_some_and(|at| at.elapsed() <= ADVISED_RECONNECT_IDLE)
+ }
+}
+
+/// An atomic flag tracking whether the server has advised reconnecting on this
+/// connection. Set by the response decoder when a frame carries the
+/// reconnect-advised bit, checked by the session loops.
+#[derive(Clone, Default)]
+pub(crate) struct ReconnectAdvice(Arc);
+
+impl ReconnectAdvice {
+ pub(crate) fn is_advised(&self) -> bool {
+ self.0.load(Ordering::Acquire)
+ }
+
+ pub(crate) fn advise(&self) {
+ self.0.store(true, Ordering::Release);
+ }
+}
diff --git a/sdk/src/session/append.rs b/sdk/src/session/append.rs
index 1a133a88..ea1f90fa 100644
--- a/sdk/src/session/append.rs
+++ b/sdk/src/session/append.rs
@@ -22,6 +22,7 @@ use crate::{
api::{ApiError, BasinClient, Streaming, retry_builder},
error::{AppendError, RequestError},
frame_signal::FrameSignal,
+ reconnect::{AdvisedReconnects, ReconnectAdvice},
retry::RetryBackoffBuilder,
types::{
AccessTokenMode, AppendAck, AppendInput, AppendRetryPolicy, EncryptionKey, MeteredBytes,
@@ -106,6 +107,13 @@ impl AppendSessionError {
Self::Append(AppendError::Request(error)) if error.is_authentication_error()
)
}
+
+ fn is_server_draining(&self) -> bool {
+ matches!(
+ self,
+ Self::Append(AppendError::Request(error)) if error.is_server_draining()
+ )
+ }
}
impl From for AppendSessionError {
@@ -200,6 +208,14 @@ struct SessionState {
stashed_submission: Option,
}
+impl SessionState {
+ fn is_close_complete(&self) -> bool {
+ self.close_tx.is_some()
+ && self.inflight_appends.is_empty()
+ && self.stashed_submission.is_none()
+ }
+}
+
/// A session for high-throughput appending with backpressure control. It can be created from
/// [`append_session`](crate::S2Stream::append_session).
///
@@ -481,6 +497,7 @@ async fn run_session_with_retry(
};
let mut prev_total_acked_records = 0;
let mut retry_backoff = retry_builder.build();
+ let mut advised_reconnects = AdvisedReconnects::default();
loop {
let result = run_session(
@@ -490,13 +507,33 @@ async fn run_session_with_retry(
&mut state,
buffer_size,
&frame_signal,
+ advised_reconnects,
)
.await;
match result {
- Ok(()) => {
+ Ok(SessionOutcome::Closed) => {
break;
}
+ Ok(SessionOutcome::ReconnectAdvised) => {
+ // The advised connection was already poisoned when the advice
+ // was first decoded, so reconnecting dials a fresh one.
+ advised_reconnects.record();
+ debug!(
+ inflight_appends_len = state.inflight_appends.len(),
+ advised_reconnects = advised_reconnects.count(),
+ "reconnecting append session on server advice"
+ );
+ }
+ Err(err) if err.is_server_draining() && state.is_close_complete() => break,
+ Err(err) if err.is_server_draining() => {
+ advised_reconnects.record();
+ debug!(
+ inflight_appends_len = state.inflight_appends.len(),
+ advised_reconnects = advised_reconnects.count(),
+ "reconnecting append session while server drains"
+ );
+ }
Err(err) => {
if prev_total_acked_records < state.total_acked_records {
prev_total_acked_records = state.total_acked_records;
@@ -556,6 +593,14 @@ async fn run_session_with_retry(
}
}
+/// How a connection attempt ended without failing.
+enum SessionOutcome {
+ /// Everything submitted was acknowledged and the caller closed the session.
+ Closed,
+ /// The server advised reconnecting and this connection drained cleanly.
+ ReconnectAdvised,
+}
+
async fn run_session(
client: &BasinClient,
stream: &StreamName,
@@ -563,17 +608,20 @@ async fn run_session(
state: &mut SessionState,
buffer_size: usize,
frame_signal: &Option,
-) -> Result<(), AppendSessionError> {
+ advised_reconnects: AdvisedReconnects,
+) -> Result {
if let Some(s) = frame_signal {
s.reset();
}
+ let reconnect = ReconnectAdvice::default();
let (input_tx, mut acks) = connect(
client,
stream,
encryption,
buffer_size,
frame_signal.clone(),
+ reconnect.clone(),
)
.await?;
let ack_timeout = client.config.request_timeout;
@@ -589,10 +637,24 @@ async fn run_session(
assert_eq!(state.inflight_bytes, 0);
}
+ if state.is_close_complete() {
+ return Ok(SessionOutcome::Closed);
+ }
+
let timer = MuxTimer::::default();
tokio::pin!(timer);
+ let mut declined_advice = false;
+
loop {
+ if reconnect.is_advised() && state.close_tx.is_none() && !declined_advice {
+ if advised_reconnects.should_reconnect() {
+ drain_for_reconnect(input_tx, acks, state, timer.as_mut(), ack_timeout).await?;
+ return Ok(SessionOutcome::ReconnectAdvised);
+ }
+ declined_advice = true;
+ }
+
tokio::select! {
(event_ord, _deadline) = &mut timer, if timer.is_armed() => {
match TimerEvent::from(event_ord) {
@@ -677,10 +739,7 @@ async fn run_session(
}
}
- if state.close_tx.is_some()
- && state.inflight_appends.is_empty()
- && state.stashed_submission.is_none()
- {
+ if state.is_close_complete() {
break;
}
}
@@ -689,7 +748,7 @@ async fn run_session(
assert_eq!(state.inflight_bytes, 0);
assert!(state.stashed_submission.is_none());
- Ok(())
+ Ok(SessionOutcome::Closed)
}
async fn resend(
@@ -771,12 +830,68 @@ async fn resend(
Ok(())
}
+/// Half-close so the server acknowledges everything it accepted and then ends
+/// the response cleanly. Every input reaches the server ahead of the request's
+/// end, so a clean end with appends still unacknowledged is a truncated
+/// response, and nothing is resent.
+async fn drain_for_reconnect(
+ input_tx: mpsc::Sender,
+ mut acks: Streaming,
+ state: &mut SessionState,
+ mut timer: Pin<&mut MuxTimer>,
+ ack_timeout: Duration,
+) -> Result<(), AppendSessionError> {
+ drop(input_tx);
+ loop {
+ // Bound the wait for the server's end of stream, which is otherwise
+ // unbounded once nothing is in flight.
+ if !timer.is_armed() {
+ timer.as_mut().fire_at(
+ TimerEvent::AckDeadline,
+ Instant::now() + ack_timeout,
+ CoalesceMode::Earliest,
+ );
+ }
+
+ tokio::select! {
+ (event_ord, _deadline) = &mut timer, if timer.is_armed() => {
+ match TimerEvent::from(event_ord) {
+ TimerEvent::AckDeadline => {
+ return Err(AppendSessionError::AckTimeout);
+ }
+ }
+ }
+
+ ack = acks.next() => {
+ match ack {
+ Some(Ok(ack)) => {
+ process_ack(ack, state, timer.as_mut())?;
+ }
+ Some(Err(err)) if err.is_server_draining() => {
+ return Ok(());
+ }
+ Some(Err(err)) => {
+ return Err(err.into());
+ }
+ None => {
+ if !state.inflight_appends.is_empty() {
+ return Err(AppendSessionError::StreamClosedEarly);
+ }
+ return Ok(());
+ }
+ }
+ }
+ }
+ }
+}
+
async fn connect(
client: &BasinClient,
stream: &StreamName,
encryption: Option<&EncryptionKey>,
buffer_size: usize,
frame_signal: Option,
+ reconnect: ReconnectAdvice,
) -> Result<(mpsc::Sender, Streaming), AppendSessionError> {
let (input_tx, input_rx) = mpsc::channel::(buffer_size);
let ack_stream = Box::pin(
@@ -786,6 +901,7 @@ async fn connect(
ReceiverStream::new(input_rx).map(|i| i.into()),
encryption,
frame_signal,
+ reconnect,
)
.await?
.map(|ack| match ack {
diff --git a/sdk/src/session/read.rs b/sdk/src/session/read.rs
index 87936cec..a3a07e99 100644
--- a/sdk/src/session/read.rs
+++ b/sdk/src/session/read.rs
@@ -20,6 +20,7 @@ use tracing::debug;
use crate::{
api::{ApiError, BasinClient, retry_builder},
error::{ReadError, RequestError},
+ reconnect::{AdvisedReconnects, ReconnectAdvice},
retry::RetryBackoff,
types::{
AccessTokenMode, EncryptionKey, MeteredBytes, ReadBatch, ReadInput, ReadSessionConfig,
@@ -46,6 +47,10 @@ impl ReadSessionFailure {
fn is_authentication_error(&self) -> bool {
matches!(self, Self::Api(error) if error.is_authentication_error())
}
+
+ fn is_server_draining(&self) -> bool {
+ matches!(self, Self::Api(error) if error.is_server_draining())
+ }
}
/// Errors returned by a read session.
@@ -87,9 +92,23 @@ impl From for ReadSessionError {
}
}
+/// The server heartbeats a tailing read session at a randomized gap of at most
+/// 15 seconds (), plus some buffer.
+const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(20);
+
type InternalStreaming =
Pin>>>;
+/// An item from a single read connection.
+enum ReadItem {
+ Batch(ReadBatch),
+ /// The server advised reconnecting and the response ended cleanly.
+ ///
+ /// Always the last item of a connection, emitted after the batch it rode
+ /// in on, so the resume position already accounts for that batch.
+ ReconnectAdvised,
+}
+
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
/// Error returned while waiting for a read session to catch up.
@@ -368,6 +387,7 @@ pub async fn read_session(
let access_token_mode = client.config.access_token.mode();
let baseline_wait = end.wait;
let mut last_tail_at: Option = None;
+ let mut advised_reconnects = AdvisedReconnects::default();
let initial_resume_seq_num = if start.clamp == Some(true) {
None
} else {
@@ -382,6 +402,8 @@ pub async fn read_session(
encryption.clone(),
start.clone(),
end.clone(),
+ ReconnectAdvice::default(),
+ advised_reconnects,
)
.await
{
@@ -390,6 +412,13 @@ pub async fn read_session(
break batches;
}
Err(err) => {
+ if take_server_draining_reconnect(&err, &mut advised_reconnects) {
+ debug!(
+ advised_reconnects = advised_reconnects.count(),
+ "reconnecting initial read session while server drains"
+ );
+ continue;
+ }
if let Some(backoff) =
retry_delay(&err, &mut retry_backoff, retry_policy, access_token_mode)
{
@@ -402,7 +431,7 @@ pub async fn read_session(
};
let updates = Box::pin(stream! {
- let mut batches: Option> = Some(batches);
+ let mut batches: Option> = Some(batches);
loop {
if batches.is_none() {
@@ -413,9 +442,19 @@ pub async fn read_session(
encryption.clone(),
start.clone(),
end.clone(),
+ ReconnectAdvice::default(),
+ advised_reconnects,
).await {
Ok(b) => batches = Some(b),
Err(err) => {
+ if take_server_draining_reconnect(&err, &mut advised_reconnects) {
+ debug!(
+ resume_seq_num = ?start.seq_num,
+ advised_reconnects = advised_reconnects.count(),
+ "reconnecting read session while server drains"
+ );
+ continue;
+ }
if let Some(backoff) =
retry_delay(
&err,
@@ -439,7 +478,25 @@ pub async fn read_session(
.next()
.await
{
- Some(Ok(batch)) => {
+ Some(Ok(ReadItem::ReconnectAdvised)) => {
+ batches = None;
+ // The advised connection was already poisoned when the
+ // advice was first decoded; reconnecting dials a fresh
+ // one. Avoid a useless reconnect for a read that was
+ // already satisfied when the advice arrived.
+ if read_limits_exhausted(&end) {
+ break;
+ }
+ advised_reconnects.record();
+ debug!(
+ resume_seq_num = ?start.seq_num,
+ advised_reconnects = advised_reconnects.count(),
+ "reconnecting read session on server advice"
+ );
+ yield Ok(ReadUpdate::behind());
+ continue;
+ }
+ Some(Ok(ReadItem::Batch(batch))) => {
if retry_backoff.used() > 0 {
retry_backoff.reset();
}
@@ -462,6 +519,18 @@ pub async fn read_session(
}
Some(Err(err)) => {
batches = None;
+ if err.is_server_draining() && read_limits_exhausted(&end) {
+ break;
+ }
+ if take_server_draining_reconnect(&err, &mut advised_reconnects) {
+ debug!(
+ resume_seq_num = ?start.seq_num,
+ advised_reconnects = advised_reconnects.count(),
+ "reconnecting read session while server drains"
+ );
+ yield Ok(ReadUpdate::behind());
+ continue;
+ }
if let Some(backoff) =
retry_delay(
&err,
@@ -513,15 +582,26 @@ async fn session_inner(
encryption: Option,
start: ReadStart,
end: ReadEnd,
-) -> Result, ReadSessionFailure> {
+ reconnect: ReconnectAdvice,
+ advised_reconnects: AdvisedReconnects,
+) -> Result, ReadSessionFailure> {
let mut batches = client
- .read_session(&name, start, end, encryption.as_ref())
+ .read_session(&name, start, end, encryption.as_ref(), reconnect.clone())
.await?;
+
+ let mut declined_advice = false;
Ok(Box::pin(try_stream! {
loop {
- match timeout(Duration::from_secs(20), batches.next()).await {
+ match timeout(HEARTBEAT_TIMEOUT, batches.next()).await {
Ok(Some(batch)) => {
- yield ReadBatch::from_api(batch?);
+ yield ReadItem::Batch(ReadBatch::from_api(batch?));
+ if reconnect.is_advised() && !declined_advice {
+ if advised_reconnects.should_reconnect() {
+ yield ReadItem::ReconnectAdvised;
+ break;
+ }
+ declined_advice = true;
+ }
}
Ok(None) => break,
Err(_) => Err(ReadSessionFailure::HeartbeatTimeout)?,
@@ -530,6 +610,11 @@ async fn session_inner(
}))
}
+/// Whether the read's `count` or `bytes` limit has been used up.
+fn read_limits_exhausted(end: &ReadEnd) -> bool {
+ end.count == Some(0) || end.bytes == Some(0)
+}
+
/// Compute the remaining wait budget for a retry.
///
/// During catchup (tail not yet observed), the full wait is sent.
@@ -585,6 +670,18 @@ fn retry_delay(
}
}
+fn take_server_draining_reconnect(
+ err: &ReadSessionFailure,
+ advised_reconnects: &mut AdvisedReconnects,
+) -> bool {
+ if err.is_server_draining() {
+ advised_reconnects.record();
+ true
+ } else {
+ false
+ }
+}
+
#[cfg(test)]
mod tests {
use bytes::Bytes;
From cabbd79ef98f6bcd0b31cb22bf0a65021043f02e Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
<262023388+release-pleaze[bot]@users.noreply.github.com>
Date: Thu, 3 Sep 2026 07:36:07 +0530
Subject: [PATCH 06/50] chore: release (#711)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 🤖 New release
* `s2-lite`: 0.42.7 -> 0.42.8 (✓ API compatible changes)
* `s2-sdk`: 0.34.3 -> 0.34.4 (✓ API compatible changes)
* `s2-cli`: 0.42.7 -> 0.42.8
* `s2-testcontainers`: 0.42.7 -> 0.42.8
Changelog
## `s2-lite`
## [0.42.8] - 2026-09-01
### Miscellaneous Tasks
- Update Cargo.lock dependencies
## `s2-sdk`
## [0.34.4] - 2026-09-01
### Features
- Act on s2s reconnect advice in append and read sessions
([#703](https://github.com/s2-streamstore/s2/issues/703))
### Bug Fixes
- Require http2
([#710](https://github.com/s2-streamstore/s2/issues/710))
- Classify h2 REFUSED_STREAM as retryable
([#716](https://github.com/s2-streamstore/s2/issues/716))
## `s2-cli`
## [0.42.8] - 2026-09-01
### Bug Fixes
- Require http2
([#710](https://github.com/s2-streamstore/s2/issues/710))
## `s2-testcontainers`
## [0.42.8] - 2026-09-01
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
Co-authored-by: release-pleaze[bot] <262023388+release-pleaze[bot]@users.noreply.github.com>
---
Cargo.lock | 8 ++++----
cli/CHANGELOG.md | 8 ++++++++
cli/Cargo.toml | 2 +-
lite/CHANGELOG.md | 8 ++++++++
lite/Cargo.toml | 2 +-
sdk/CHANGELOG.md | 13 +++++++++++++
sdk/Cargo.toml | 2 +-
testcontainers/CHANGELOG.md | 4 ++++
testcontainers/Cargo.toml | 2 +-
9 files changed, 41 insertions(+), 8 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 154d4b0c..5b9acf61 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4934,7 +4934,7 @@ dependencies = [
[[package]]
name = "s2-cli"
-version = "0.42.7"
+version = "0.42.8"
dependencies = [
"assert_cmd",
"async-stream",
@@ -5018,7 +5018,7 @@ dependencies = [
[[package]]
name = "s2-lite"
-version = "0.42.7"
+version = "0.42.8"
dependencies = [
"async-stream",
"async-trait",
@@ -5077,7 +5077,7 @@ dependencies = [
[[package]]
name = "s2-sdk"
-version = "0.34.3"
+version = "0.34.4"
dependencies = [
"assert_matches",
"async-compression",
@@ -5137,7 +5137,7 @@ dependencies = [
[[package]]
name = "s2-testcontainers"
-version = "0.42.7"
+version = "0.42.8"
dependencies = [
"reqwest",
"s2-sdk",
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 98eef49f..9dfeac91 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.42.8] - 2026-09-01
+
+### Bug Fixes
+
+- Require http2 ([#710](https://github.com/s2-streamstore/s2/issues/710))
+
+
+
## [0.42.7] - 2026-08-18
### Bug Fixes
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index a3d1cdbc..64dfddeb 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-cli"
-version = "0.42.7"
+version = "0.42.8"
description = "CLI for S2"
edition.workspace = true
license.workspace = true
diff --git a/lite/CHANGELOG.md b/lite/CHANGELOG.md
index 6480c3e8..3d7ff266 100644
--- a/lite/CHANGELOG.md
+++ b/lite/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.42.8] - 2026-09-01
+
+### Miscellaneous Tasks
+
+- Update Cargo.lock dependencies
+
+
+
## [0.42.7] - 2026-08-18
### Miscellaneous Tasks
diff --git a/lite/Cargo.toml b/lite/Cargo.toml
index 5003a940..bb5c7709 100644
--- a/lite/Cargo.toml
+++ b/lite/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-lite"
-version = "0.42.7"
+version = "0.42.8"
description = "Lightweight server implementation of S2, the durable streams API, backed by object storage"
edition.workspace = true
license.workspace = true
diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md
index b45dbbec..8d07c476 100644
--- a/sdk/CHANGELOG.md
+++ b/sdk/CHANGELOG.md
@@ -2,6 +2,19 @@
All notable changes to this project will be documented in this file.
+## [0.34.4] - 2026-09-01
+
+### Features
+
+- Act on s2s reconnect advice in append and read sessions ([#703](https://github.com/s2-streamstore/s2/issues/703))
+
+### Bug Fixes
+
+- Require http2 ([#710](https://github.com/s2-streamstore/s2/issues/710))
+- Classify h2 REFUSED_STREAM as retryable ([#716](https://github.com/s2-streamstore/s2/issues/716))
+
+
+
## [0.34.3] - 2026-08-13
### Features
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 7853e674..ecdd764f 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "s2-sdk"
description = "Rust SDK for S2"
-version = "0.34.3"
+version = "0.34.4"
edition.workspace = true
license.workspace = true
repository = "https://github.com/s2-streamstore/s2/tree/main/sdk"
diff --git a/testcontainers/CHANGELOG.md b/testcontainers/CHANGELOG.md
index 5ed24846..aee84165 100644
--- a/testcontainers/CHANGELOG.md
+++ b/testcontainers/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
+## [0.42.8] - 2026-09-01
+
+
+
## [0.42.7] - 2026-08-18
diff --git a/testcontainers/Cargo.toml b/testcontainers/Cargo.toml
index af38efd8..3dfb58e1 100644
--- a/testcontainers/Cargo.toml
+++ b/testcontainers/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-testcontainers"
-version = "0.42.7"
+version = "0.42.8"
description = "Testcontainers helpers for the S2 Docker image"
edition.workspace = true
license.workspace = true
From 3553cd9eea0a4711848e3126b14d9833139d83e9 Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
Date: Thu, 3 Sep 2026 02:43:11 +0000
Subject: [PATCH 07/50] Bump s2-lite-helm chart to appVersion 0.42.8
---
charts/s2-lite-helm/Chart.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/charts/s2-lite-helm/Chart.yaml b/charts/s2-lite-helm/Chart.yaml
index 54765424..35c7c06e 100644
--- a/charts/s2-lite-helm/Chart.yaml
+++ b/charts/s2-lite-helm/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
name: s2-lite-helm
description: Self-hostable S2 streaming datastore using SlateDB on object storage
type: application
-version: 0.1.63
-appVersion: "0.42.7"
+version: 0.1.64
+appVersion: "0.42.8"
keywords:
- s2
- streaming
From 10f011b625ebef18088f713e0ffdafb165994a19 Mon Sep 17 00:00:00 2001
From: chewbaucke
Date: Tue, 8 Sep 2026 03:32:37 +1000
Subject: [PATCH 08/50] fix(lite): close SlateDB on graceful shutdown
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Lite previously returned after draining HTTP requests without closing SlateDB, leaving memtables to be recovered from the WAL on the next startup.
Call `Backend::close()` after HTTP shutdown, delegating to SlateDB 0.15's `Db::close()` to flush memtables to L0 and shut down the database. Log the elapsed close time and propagate close errors. This reduces WAL replay on restart; Lite's existing `manifest_poll_interval` startup wait remains.
Shutdown awaits the database close without an application timeout. Deployments should allow enough termination grace for the final flush; an interrupted close may still leave WAL data to replay on the next startup.
Validation:
- `just fmt` and `just test` (744 tests passed).
- Local filesystem SIGTERM/reopen checks over HTTP and self-signed HTTPS preserved all 512 acknowledged records per scenario and accepted subsequent appends.
- The contributor reported restart-to-ready times of 2–6 seconds on Fly.io/Tigris, down from 106 seconds when quiet and 251–332 seconds after a burst, with a 1–2 second close and all 352 acknowledged records plus the seed record present after reopening. These remote-store timings were not independently reproduced during review.
---
lite/src/backend/core.rs | 8 ++++++++
lite/src/server.rs | 12 ++++++++++++
2 files changed, 20 insertions(+)
diff --git a/lite/src/backend/core.rs b/lite/src/backend/core.rs
index 04eace85..0b915877 100644
--- a/lite/src/backend/core.rs
+++ b/lite/src/backend/core.rs
@@ -71,6 +71,14 @@ impl Backend {
}
}
+ /// Flush memtables to L0 and close the database.
+ ///
+ /// Call after draining HTTP requests to reduce WAL replay on restart.
+ /// Dropping the backend does not close SlateDB.
+ pub async fn close(&self) -> Result<(), slatedb::Error> {
+ self.db.close().await
+ }
+
pub(super) fn bgtask_trigger(&self, trigger: BgtaskTrigger) {
let _ = self.bgtask_trigger_tx.send(trigger);
}
diff --git a/lite/src/server.rs b/lite/src/server.rs
index 7594684e..bb4ec354 100644
--- a/lite/src/server.rs
+++ b/lite/src/server.rs
@@ -199,6 +199,7 @@ pub async fn run(args: LiteArgs) -> eyre::Result<()> {
info!(%args.append_inflight_bytes, "starting backend");
let backend = Backend::new(db, args.append_inflight_bytes);
+ let shutdown_backend = backend.clone();
crate::backend::bgtasks::spawn(&backend);
if let Some(init_file) = &args.init_file {
@@ -278,6 +279,17 @@ pub async fn run(args: LiteArgs) -> eyre::Result<()> {
}
}
+ info!("http server stopped; closing SlateDB");
+ let close_started = Instant::now();
+ shutdown_backend
+ .close()
+ .await
+ .map_err(|error| eyre::eyre!("SlateDB close: {error}"))?;
+ info!(
+ elapsed_ms = close_started.elapsed().as_millis(),
+ "SlateDB closed"
+ );
+
Ok(())
}
From 741b0995fc189e8933424ed57e09f6b08635a68a Mon Sep 17 00:00:00 2001
From: "devin-ai-integration[bot]"
<158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 9 Sep 2026 17:13:37 -0700
Subject: [PATCH 09/50] fix(common): reject NUL bytes in stream names and
access token IDs (#728)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Black-box fuzzing of an S2 sandbox found that a NUL byte (0x00) in a
stream name — or in the list `prefix` / `start_after` query params —
surfaces as a generic `500 {"code":"other","message":"Internal Server
Error"}` instead of a structured 400:
```
POST /v1/streams {"stream":"a\u0000b"} -> 500 other
GET /v1/streams?prefix=a%00b -> 500 other
GET /v1/streams?start_after=a%00b -> 500 other
```
Every other control char (`\t`, `\n`, `\r`, `\x01`), arbitrary Unicode,
emoji, `/`, `?`, `#`, `%20`, etc. is accepted and round-trips fine; only
NUL breaks.
**Root cause:** `StreamNameStr::::validate_str` in
`common/src/stream.rs` only checks non-empty, not `.`/`..`, and `len <=
MAX_STREAM_NAME_LEN`. Nothing rejects an interior NUL, so it passes
validation and reaches the metastore, where Postgres/DSQL rejects NUL in
a `TEXT` column and the error isn't mapped to a client error. Because
`NameProps`, `PrefixProps`, and `StartAfterProps` all share
`validate_str`, the name, `prefix`, and `start_after` paths are all
affected.
**Fix** (minimal; no other character is newly rejected):
```rust
// StreamNameStr::::validate_str
if name.contains('\0') {
return Err(format!("stream {} must not contain NUL bytes", T::FIELD_NAME).into());
}
```
Applied identically to `AccessTokenIdStr::validate_str`
(`common/src/access.rs`), which mirrors the stream validator
rule-for-rule and had the same gap. `BasinNameStr` and `LocationName`
already enforce strict ASCII charsets that exclude NUL — no code change,
just added `nul` regression cases to lock it in.
Rejecting NUL narrows the documented contract ("between 1 and 512
bytes"), so the doc comments that feed the OpenAPI spec
(`CreateStreamRequest::stream`,
`ListStreamsRequest::{prefix,start_after}`,
`IssueAccessTokenRequest::id`,
`ListAccessTokensRequest::{prefix,start_after}`) and the SDK rustdoc on
the re-exported name/prefix/start-after types now say "must not contain
NUL bytes". The `api/specs` submodule is synced separately by the specs
workflow.
Since the fix is in the shared `s2-common` crate it covers both s2-cloud
and s2-lite. Verified against a locally built lite, which now returns
structured 400s:
```
create stream a\0b -> 400 bad_json "stream name must not contain NUL bytes ..."
?prefix=a%00b -> 400 bad_query "prefix: stream prefix must not contain NUL bytes"
?start_after=a%00b -> 400 bad_query "start_after: stream start-after must not contain NUL bytes"
create "a\tb/名前 😀?#" -> 201 (unchanged)
```
**s2-lite behaviour before this fix:** it did *not* 500. Running `main`
lite, all three requests succeeded (`201`/`200`) and the NUL name
round-tripped through list. SlateDB keys are raw bytes, and although
lite uses `\0` as the basin/stream separator in the `StreamMeta` key and
`StreamIdMapping` value, `deser_key` splits on the *first* `\0` and
basin names can never contain NUL, so the encoding stayed unambiguous.
The 500 is specific to s2-cloud's SQL-backed metastore; lite just
becomes consistent with cloud in rejecting NUL up-front.
**Tests:**
- Unit: `rstest` `nul` cases added to `validate_name_err` /
`validate_prefix_err` / `validate_start_after_err` (stream, basin,
access token) and `LocationName::validate_name_err`, plus
`control_chars` / `unicode` `validate_name_ok` cases guarding the
"nothing else newly rejected" intent.
- HTTP: new `handlers::v1::streams::test` module in lite (in-process
`Router::oneshot`, same pattern as the records handler tests) asserting
NUL in the create body → 400 `bad_json`, NUL in `prefix`/`start_after` →
400 `bad_query`, and control-char/Unicode names → 201. These fail if the
`validate_str` guard is removed.
`just clippy` and `just test` pass.
No version bumps; release is left to the normal release flow.
Link to Devin session:
https://app.devin.ai/sessions/503132b92ae34635b700d43f406c0ccd
Open in Devin Desktop:
https://app.devin.ai/desktop/session/503132b92ae34635b700d43f406c0ccd?variant=devin
Requested by: @sgbalogh
---------
Co-authored-by: Stephen Balogh
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
api/src/v1/access.rs | 5 +-
api/src/v1/stream/mod.rs | 4 +-
common/src/access.rs | 9 ++
common/src/basin.rs | 3 +
common/src/location.rs | 1 +
common/src/stream.rs | 14 +++
lite/src/handlers/v1/streams.rs | 154 ++++++++++++++++++++++++++++++++
sdk/src/types.rs | 14 +--
8 files changed, 196 insertions(+), 8 deletions(-)
diff --git a/api/src/v1/access.rs b/api/src/v1/access.rs
index 1dbc4173..a5dc1cc1 100644
--- a/api/src/v1/access.rs
+++ b/api/src/v1/access.rs
@@ -193,7 +193,8 @@ impl From for AccessTokenInfo {
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct IssueAccessTokenRequest {
/// Access token ID.
- /// It must be unique to the account and between 1 and 96 bytes in length.
+ /// It must be unique to the account and between 1 and 96 bytes in length, and must not
+ /// contain NUL bytes.
pub id: AccessTokenId,
/// Expiration time in RFC 3339 format.
/// If not set, the expiration will be set to that of the requestor's token.
@@ -407,9 +408,11 @@ impl From for ReadWritePermissions {
#[cfg_attr(feature = "utoipa", into_params(parameter_in = Query))]
pub struct ListAccessTokensRequest {
/// Filter to access tokens whose IDs begin with this prefix.
+ /// It must not contain NUL bytes.
#[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
pub prefix: Option,
/// Filter to access tokens whose IDs lexicographically start after this string.
+ /// It must not contain NUL bytes.
#[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
pub start_after: Option,
/// Number of results, up to a maximum of 1000.
diff --git a/api/src/v1/stream/mod.rs b/api/src/v1/stream/mod.rs
index 730820da..255879cd 100644
--- a/api/src/v1/stream/mod.rs
+++ b/api/src/v1/stream/mod.rs
@@ -54,9 +54,11 @@ impl From for StreamInfo {
#[cfg_attr(feature = "utoipa", into_params(parameter_in = Query))]
pub struct ListStreamsRequest {
/// Filter to streams whose names begin with this prefix.
+ /// It must not contain NUL bytes.
#[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
pub prefix: Option,
/// Filter to streams whose names lexicographically start after this string.
+ /// It must not contain NUL bytes.
#[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
pub start_after: Option,
/// Number of results, up to a maximum of 1000.
@@ -82,7 +84,7 @@ pub struct ListStreamsResponse {
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct CreateStreamRequest {
/// Stream name that is unique to the basin.
- /// It can be between 1 and 512 bytes in length.
+ /// It can be between 1 and 512 bytes in length, and must not contain NUL bytes.
pub stream: StreamName,
/// Stream configuration.
pub config: Option,
diff --git a/common/src/access.rs b/common/src/access.rs
index 01b3749f..5d67b948 100644
--- a/common/src/access.rs
+++ b/common/src/access.rs
@@ -30,6 +30,12 @@ impl AccessTokenIdStr {
);
}
+ if id.contains('\0') {
+ return Err(
+ format!("access token {} must not contain NUL bytes", T::FIELD_NAME).into(),
+ );
+ }
+
if id.len() > caps::MAX_ACCESS_TOKEN_ID_LEN {
return Err(format!(
"access token {} must not exceed {} bytes in length",
@@ -264,6 +270,7 @@ mod test {
#[case::dot(".".to_owned())]
#[case::dot_dot("..".to_owned())]
#[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
+ #[case::nul("a\0b".to_owned())]
fn validate_id_err(#[case] id: String) {
AccessTokenIdStr::::validate_str(&id).expect_err("expected validation error");
}
@@ -282,6 +289,7 @@ mod test {
#[rstest]
#[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
+ #[case::nul("a\0b".to_owned())]
fn validate_prefix_err(#[case] prefix: String) {
AccessTokenIdStr::::validate_str(&prefix)
.expect_err("expected validation error");
@@ -301,6 +309,7 @@ mod test {
#[rstest]
#[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
+ #[case::nul("a\0b".to_owned())]
fn validate_start_after_err(#[case] start_after: String) {
AccessTokenIdStr::::validate_str(&start_after)
.expect_err("expected validation error");
diff --git a/common/src/basin.rs b/common/src/basin.rs
index 255ce2f4..9538ce4f 100644
--- a/common/src/basin.rs
+++ b/common/src/basin.rs
@@ -231,6 +231,7 @@ mod test {
#[case::invalid_first_char("Abcdefgh".to_owned())]
#[case::invalid_last_char("abcdefg-".to_owned())]
#[case::invalid_characters("abcd_efg".to_owned())]
+ #[case::nul("abcd\0efg".to_owned())]
fn validate_name_err(#[case] name: String) {
BasinNameStr::::validate_str(&name).expect_err("expected validation error");
}
@@ -248,6 +249,7 @@ mod test {
#[case::too_long("a".repeat(crate::caps::MAX_BASIN_NAME_LEN + 1))]
#[case::invalid_first_char("-abc".to_owned())]
#[case::invalid_characters("ab_cd".to_owned())]
+ #[case::nul("ab\0cd".to_owned())]
fn validate_prefix_err(#[case] prefix: String) {
BasinNameStr::::validate_str(&prefix).expect_err("expected validation error");
}
@@ -267,6 +269,7 @@ mod test {
#[case::too_long("a".repeat(crate::caps::MAX_BASIN_NAME_LEN + 1))]
#[case::invalid_first_char("-abc".to_owned())]
#[case::invalid_characters("ab_cd".to_owned())]
+ #[case::nul("ab\0cd".to_owned())]
fn validate_start_after_err(#[case] start_after: String) {
BasinNameStr::::validate_str(&start_after)
.expect_err("expected validation error");
diff --git a/common/src/location.rs b/common/src/location.rs
index ca07367f..6ce67076 100644
--- a/common/src/location.rs
+++ b/common/src/location.rs
@@ -173,6 +173,7 @@ mod test {
#[case::slash("aws/us-east-1".to_owned())]
#[case::space("aws:us east-1".to_owned())]
#[case::multibyte("aws:é".to_owned())]
+ #[case::nul("aws:us\0east-1".to_owned())]
fn validate_name_err(#[case] location: String) {
location
.parse::()
diff --git a/common/src/stream.rs b/common/src/stream.rs
index 6ed7be49..8f6df6f8 100644
--- a/common/src/stream.rs
+++ b/common/src/stream.rs
@@ -34,6 +34,10 @@ impl StreamNameStr {
return Err(format!("stream {} must not be \".\" or \"..\"", T::FIELD_NAME).into());
}
+ if name.contains('\0') {
+ return Err(format!("stream {} must not contain NUL bytes", T::FIELD_NAME).into());
+ }
+
if name.len() > caps::MAX_STREAM_NAME_LEN {
return Err(format!(
"stream {} must not exceed {} bytes in length",
@@ -386,6 +390,8 @@ mod test {
#[rstest]
#[case::normal("my-stream".to_owned())]
+ #[case::control_chars("a\tb\nc\rd\x01e".to_owned())]
+ #[case::unicode("stream/名前 😀?#%20".to_owned())]
#[case::max_len("a".repeat(crate::caps::MAX_STREAM_NAME_LEN))]
fn validate_name_ok(#[case] name: String) {
assert_eq!(StreamNameStr::::validate_str(&name), Ok(()));
@@ -396,6 +402,10 @@ mod test {
#[case::dot(".".to_owned())]
#[case::dot_dot("..".to_owned())]
#[case::too_long("a".repeat(crate::caps::MAX_STREAM_NAME_LEN + 1))]
+ #[case::nul("a\0b".to_owned())]
+ #[case::leading_nul("\0a".to_owned())]
+ #[case::trailing_nul("a\0".to_owned())]
+ #[case::only_nul("\0".to_owned())]
fn validate_name_err(#[case] name: String) {
StreamNameStr::::validate_str(&name).expect_err("expected validation error");
}
@@ -411,6 +421,8 @@ mod test {
#[rstest]
#[case::too_long("a".repeat(crate::caps::MAX_STREAM_NAME_LEN + 1))]
+ #[case::nul("a\0b".to_owned())]
+ #[case::only_nul("\0".to_owned())]
fn validate_prefix_err(#[case] prefix: String) {
StreamNameStr::::validate_str(&prefix).expect_err("expected validation error");
}
@@ -429,6 +441,8 @@ mod test {
#[rstest]
#[case::too_long("a".repeat(crate::caps::MAX_STREAM_NAME_LEN + 1))]
+ #[case::nul("a\0b".to_owned())]
+ #[case::only_nul("\0".to_owned())]
fn validate_start_after_err(#[case] start_after: String) {
StreamNameStr::::validate_str(&start_after)
.expect_err("expected validation error");
diff --git a/lite/src/handlers/v1/streams.rs b/lite/src/handlers/v1/streams.rs
index 4f694284..1792da12 100644
--- a/lite/src/handlers/v1/streams.rs
+++ b/lite/src/handlers/v1/streams.rs
@@ -338,3 +338,157 @@ pub async fn reconfigure_stream(
.await?;
Ok(Json(config.into()))
}
+
+#[cfg(test)]
+mod test {
+ use std::{sync::Arc, time::Duration};
+
+ use axum::{
+ body::{self, Body},
+ http::{Request, StatusCode, header},
+ response::Response,
+ };
+ use bytesize::ByteSize;
+ use rstest::rstest;
+ use s2_common::{
+ basin::{BASIN_HEADER, BasinName},
+ config::BasinConfig,
+ resources::ProvisionMode,
+ };
+ use slatedb::{Db, config::Settings, object_store::memory::InMemory};
+ use tower::ServiceExt as _;
+ use uuid::Uuid;
+
+ use crate::{backend::Backend, handlers};
+
+ async fn setup_app(test_suffix: &str) -> (axum::Router, Backend, BasinName) {
+ let object_store = Arc::new(InMemory::new());
+ let db_path = format!("/tmp/streams-handler-test-{}", Uuid::new_v4());
+ let db = Db::builder(db_path, object_store)
+ .with_settings(Settings {
+ flush_interval: Some(Duration::from_millis(5)),
+ ..Default::default()
+ })
+ .build()
+ .await
+ .expect("create in-memory db");
+ let backend = Backend::new(db, ByteSize::mib(10));
+ let basin: BasinName = format!("test-basin-{test_suffix}").parse().unwrap();
+ backend
+ .provision_basin(
+ basin.clone(),
+ BasinConfig::default(),
+ ProvisionMode::CreateOnly {
+ request_token: None,
+ },
+ )
+ .await
+ .expect("create basin");
+ let app = handlers::router().with_state(backend.clone());
+ (app, backend, basin)
+ }
+
+ async fn send(app: &axum::Router, request: Request) -> Response {
+ app.clone()
+ .oneshot(request)
+ .await
+ .expect("request should complete")
+ }
+
+ async fn response_json(response: Response) -> serde_json::Value {
+ let body = body::to_bytes(response.into_body(), usize::MAX)
+ .await
+ .expect("response body");
+ serde_json::from_slice(&body).expect("json body")
+ }
+
+ async fn create_stream(app: &axum::Router, basin: &BasinName, name: &str) -> Response {
+ let payload = serde_json::json!({ "stream": name }).to_string();
+ send(
+ app,
+ Request::builder()
+ .method("POST")
+ .uri("/v1/streams")
+ .header(BASIN_HEADER.as_str(), basin.as_ref())
+ .header(header::CONTENT_TYPE, "application/json")
+ .body(Body::from(payload))
+ .unwrap(),
+ )
+ .await
+ }
+
+ async fn list_streams(app: &axum::Router, basin: &BasinName, query: &str) -> Response {
+ send(
+ app,
+ Request::builder()
+ .method("GET")
+ .uri(format!("/v1/streams?{query}"))
+ .header(BASIN_HEADER.as_str(), basin.as_ref())
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await
+ }
+
+ #[tokio::test]
+ async fn create_stream_with_nul_byte_is_bad_json() {
+ let (app, backend, basin) = setup_app("create-nul").await;
+
+ let response = create_stream(&app, &basin, "a\0b").await;
+
+ assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+ let error = response_json(response).await;
+ assert_eq!(error["code"], "bad_json");
+ assert!(
+ error["message"]
+ .as_str()
+ .unwrap()
+ .contains("stream name must not contain NUL bytes"),
+ "unexpected message: {error}"
+ );
+
+ backend.close().await.expect("close backend");
+ }
+
+ #[rstest]
+ #[case::control_chars("a\tb\nc\rd\x01e")]
+ #[case::unicode("stream/名前 😀?#%20")]
+ #[tokio::test]
+ async fn create_stream_with_other_chars_is_created(#[case] name: &str) {
+ let (app, backend, basin) = setup_app("create-ok").await;
+
+ let response = create_stream(&app, &basin, name).await;
+
+ assert_eq!(response.status(), StatusCode::CREATED);
+ let info = response_json(response).await;
+ assert_eq!(info["name"], name);
+
+ backend.close().await.expect("close backend");
+ }
+
+ #[rstest]
+ #[case::prefix("prefix=a%00b", "stream prefix must not contain NUL bytes")]
+ #[case::start_after("start_after=a%00b", "stream start-after must not contain NUL bytes")]
+ #[tokio::test]
+ async fn list_streams_with_nul_byte_is_bad_query(
+ #[case] query: &str,
+ #[case] expected_message: &str,
+ ) {
+ let (app, backend, basin) = setup_app("list-nul").await;
+
+ let response = list_streams(&app, &basin, query).await;
+
+ assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+ let error = response_json(response).await;
+ assert_eq!(error["code"], "bad_query");
+ assert!(
+ error["message"]
+ .as_str()
+ .unwrap()
+ .contains(expected_message),
+ "unexpected message: {error}"
+ );
+
+ backend.close().await.expect("close backend");
+ }
+}
diff --git a/sdk/src/types.rs b/sdk/src/types.rs
index 97b71416..6d324782 100644
--- a/sdk/src/types.rs
+++ b/sdk/src/types.rs
@@ -25,11 +25,12 @@ use s2_api::{v1 as api, v1::stream::s2s::CompressionAlgorithm};
pub use s2_common::ValidationError;
/// Access token ID.
///
-/// **Note:** It must be unique to the account and between 1 and 96 bytes in length.
+/// **Note:** It must be unique to the account and between 1 and 96 bytes in length, and must
+/// not contain NUL bytes.
pub use s2_common::access::AccessTokenId;
-/// See [`ListAccessTokensInput::prefix`].
+/// See [`ListAccessTokensInput::prefix`]. It must not contain NUL bytes.
pub use s2_common::access::AccessTokenIdPrefix;
-/// See [`ListAccessTokensInput::start_after`].
+/// See [`ListAccessTokensInput::start_after`]. It must not contain NUL bytes.
pub use s2_common::access::AccessTokenIdStartAfter;
/// Basin name.
///
@@ -47,11 +48,12 @@ pub use s2_common::basin::BasinNameStartAfter;
pub use s2_common::location::LocationName;
/// Stream name.
///
-/// **Note:** It must be unique to the basin and between 1 and 512 bytes in length.
+/// **Note:** It must be unique to the basin and between 1 and 512 bytes in length, and must
+/// not contain NUL bytes.
pub use s2_common::stream::StreamName;
-/// See [`ListStreamsInput::prefix`].
+/// See [`ListStreamsInput::prefix`]. It must not contain NUL bytes.
pub use s2_common::stream::StreamNamePrefix;
-/// See [`ListStreamsInput::start_after`].
+/// See [`ListStreamsInput::start_after`]. It must not contain NUL bytes.
pub use s2_common::stream::StreamNameStartAfter;
pub use s2_common::{
caps::RECORD_BATCH_MAX,
From b3784c31e79b071f05360a3181f60e87303c340c Mon Sep 17 00:00:00 2001
From: "devin-ai-integration[bot]"
<158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Wed, 9 Sep 2026 17:15:23 -0700
Subject: [PATCH 10/50] fix(api): switch JSON extractor from sonic-rs back to
serde_json (#729)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Reverts the `s2_api::data::Json` / `JsonOpt` axum extractor from
`sonic_rs::from_slice` (#383) back to `serde_json::from_slice`, and
drops the `sonic-rs` dependency.
**Why:** sonic-rs has no recursion bound on its *skip* path. When the
deserializer meets a value it doesn't want — an unknown field, or a
wrong-typed value such as `"body": [[[[…` where a `String` is expected —
it walks to the end of that value via `Parser::skip_one(true)` →
`skip_array`/`skip_object` → `skip_one` …, recursing once per nesting
level with no counter. (Its `MAX_ALLOWED_DEPTH = 255` guard from
cloudwego/sonic-rs#213 only covers the visitor path.) A request body
nested ~20k+ levels deep therefore overflows the tokio worker stack and
aborts the whole frontend process:
```
thread 'tokio-rt-worker' has overflowed its stack
fatal runtime error: stack overflow, aborting
```
Reported upstream as cloudwego/sonic-rs#232 (open, reproduces on
0.5.9/main).
serde_json is safe on every such path: type mismatches error out before
descending (`invalid type: sequence, expected a string` → 422),
`deserialize_ignored_any` skips iteratively with a heap stack, and
values that are actually deserialized hit the default 128-level
recursion limit (`recursion limit exceeded` → 400).
**Error classification** keeps the same 400/422/500 split:
```rust
serde_json::error::Category::Data => DataError (422)
serde_json::error::Category::Io => Other (500)
serde_json::error::Category::Syntax | Eof => SyntaxError (400)
```
**Performance:** measured on `AppendInput` in release builds (x86-64,
sonic-rs built with `-C target-cpu=native` so it gets AVX2), serde_json
was on par or faster than sonic-rs for realistic append bodies — 371 vs
459 µs for 1000×100 B records with headers, 1.66 vs 2.09 ms for 1000×1
KiB records with escapes, 132 vs 105 µs for a single 1 MiB string.
**Tests:** `deeply_nested_json_does_not_overflow_stack` parses 50k-deep
nesting in `body`, in an unknown field, and at top level on a 2
MiB-stack thread; the old `serde_json_sonic_rs_roundtrip` differential
test is kept as a plain `serde_json_roundtrip`.
Companion change for s2-cloud: s2-streamstore/s2-cloud#1777 (will be
reduced to bumping `s2-api` and switching the OTLP extractor once this
is released).
Link to Devin session:
https://app.devin.ai/sessions/7c3250684bc84290abeeb13826313c4b
Open in Devin Desktop:
https://app.devin.ai/desktop/session/7c3250684bc84290abeeb13826313c4b?variant=devin
Requested by: @sgbalogh
---------
Co-authored-by: Stephen Balogh
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
Cargo.lock | 65 -----------------------------------------
Cargo.toml | 1 -
api/Cargo.toml | 3 +-
api/src/data.rs | 78 ++++++++++++++++++++++++++++++++++---------------
4 files changed, 56 insertions(+), 91 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 5b9acf61..9fa1927f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -74,19 +74,6 @@ dependencies = [
"zeroize",
]
-[[package]]
-name = "ahash"
-version = "0.8.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
-dependencies = [
- "cfg-if",
- "getrandom 0.3.4",
- "once_cell",
- "version_check",
- "zerocopy",
-]
-
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -2114,18 +2101,6 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
-[[package]]
-name = "faststr"
-version = "0.2.34"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113"
-dependencies = [
- "bytes",
- "rkyv",
- "serde",
- "simdutf8",
-]
-
[[package]]
name = "ferroid"
version = "2.0.0"
@@ -4923,7 +4898,6 @@ dependencies = [
"s2-common",
"serde",
"serde_json",
- "sonic-rs",
"strum",
"thiserror 2.0.19",
"time",
@@ -5664,45 +5638,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e14297decde697ddf377c25752aead0927d5cfc89c2684d2af96901a4ceeea"
-[[package]]
-name = "sonic-number"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3775c3390edf958191f1ab1e8c5c188907feebd0f3ce1604cb621f72961dbf32"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "sonic-rs"
-version = "0.5.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d971cc77a245ccf1756dbd1a87c3e7f709c0191464096510d43eec056d0f2c4f"
-dependencies = [
- "ahash",
- "bumpalo",
- "bytes",
- "cfg-if",
- "faststr",
- "itoa",
- "ref-cast",
- "serde",
- "simdutf8",
- "sonic-number",
- "sonic-simd",
- "thiserror 2.0.19",
- "zmij",
-]
-
-[[package]]
-name = "sonic-simd"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f99e664ecd2d85a68c87e3c7a3cfe691f647ea9e835de984aba4d54a41f817d4"
-dependencies = [
- "cfg-if",
-]
-
[[package]]
name = "spin"
version = "0.10.1"
diff --git a/Cargo.toml b/Cargo.toml
index 037a28d8..a092a9b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -77,7 +77,6 @@ semver = "1.0"
serde = "1.0"
serde_json = "1.0"
slatedb = "0.15.0"
-sonic-rs = "0.5"
strum = "0.28"
tabled = "0.21"
tempfile = "3.27"
diff --git a/api/Cargo.toml b/api/Cargo.toml
index f27ce3ee..691ccc7a 100644
--- a/api/Cargo.toml
+++ b/api/Cargo.toml
@@ -17,7 +17,7 @@ include = [
]
[features]
-axum = ["dep:axum", "dep:sonic-rs", "s2-common/axum"]
+axum = ["dep:axum", "s2-common/axum"]
utoipa = ["dep:utoipa"]
codegen = ["dep:prost-build"]
@@ -36,7 +36,6 @@ prost = { workspace = true }
s2-common = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
-sonic-rs = { workspace = true, optional = true }
strum = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
time = { workspace = true, features = ["serde", "formatting", "parsing"] }
diff --git a/api/src/data.rs b/api/src/data.rs
index 07e8e918..f9fc4fc7 100644
--- a/api/src/data.rs
+++ b/api/src/data.rs
@@ -182,10 +182,10 @@ pub mod extract {
}
}
- fn classify_sonic_error(err: sonic_rs::Error) -> JsonExtractionRejection {
- use sonic_rs::error::Category;
+ fn classify_json_error(err: serde_json::Error) -> JsonExtractionRejection {
+ use serde_json::error::Category;
match err.classify() {
- Category::TypeUnmatched | Category::NotFound => JsonExtractionRejection::DataError {
+ Category::Data => JsonExtractionRejection::DataError {
status: http::StatusCode::UNPROCESSABLE_ENTITY,
message: err.to_string().into(),
},
@@ -193,7 +193,7 @@ pub mod extract {
status: http::StatusCode::INTERNAL_SERVER_ERROR,
message: err.to_string().into(),
},
- _ => JsonExtractionRejection::SyntaxError {
+ Category::Syntax | Category::Eof => JsonExtractionRejection::SyntaxError {
status: http::StatusCode::BAD_REQUEST,
message: err.to_string().into(),
},
@@ -223,9 +223,9 @@ pub mod extract {
message: e.body_text().into(),
}
})?;
- sonic_rs::from_slice(&bytes)
+ serde_json::from_slice(&bytes)
.map(Self)
- .map_err(classify_sonic_error)
+ .map_err(classify_json_error)
}
}
@@ -255,9 +255,9 @@ pub mod extract {
if bytes.is_empty() {
return Ok(None);
}
- sonic_rs::from_slice(&bytes)
+ serde_json::from_slice(&bytes)
.map(|v| Some(Self(v)))
- .map_err(classify_sonic_error)
+ .map_err(classify_json_error)
}
}
@@ -323,15 +323,12 @@ pub mod extract {
stream::{AppendInput, AppendRecord, Header},
};
- fn classify_json_error(
- json: &[u8],
- ) -> Result {
- sonic_rs::from_slice(json).map_err(classify_sonic_error)
+ fn parse_json(json: &[u8]) -> Result {
+ serde_json::from_slice(json).map_err(classify_json_error)
}
/// Verify that our rejection wrapper preserves axum's status code
- /// classification for a variety of invalid JSON payloads, now using
- /// sonic-rs as the deserializer.
+ /// classification for a variety of invalid JSON payloads.
#[test]
fn json_error_classification() {
let cases: &[(&[u8], http::StatusCode)] = &[
@@ -355,7 +352,7 @@ pub mod extract {
];
for (input, expected_status) in cases {
- let err = classify_json_error::(input).expect_err(&format!(
+ let err = parse_json::(input).expect_err(&format!(
"expected error for {:?}",
String::from_utf8_lossy(input)
));
@@ -373,24 +370,59 @@ pub mod extract {
#[test]
fn valid_json_parses_successfully() {
let input = br#"{"records": [], "match_seq_num": null}"#;
- let result = classify_json_error::(input);
+ let result = parse_json::(input);
assert!(result.is_ok());
}
- /// Differential test: serialize with serde_json, deserialize with
- /// both serde_json and sonic_rs, assert semantic equality.
+ /// A deeply nested value must never overflow the stack, wherever it
+ /// appears in the document: a wrong-typed value is rejected before it
+ /// is descended into, an unknown field is skipped iteratively, and
+ /// nesting that is actually deserialized hits the recursion limit.
#[test]
- fn serde_json_sonic_rs_roundtrip() {
+ fn deeply_nested_json_does_not_overflow_stack() {
+ const DEPTH: usize = 50_000;
+ let nested = format!("{}{}", "[".repeat(DEPTH), "]".repeat(DEPTH));
+ let cases = [
+ (
+ format!(r#"{{"records":[{{"body":{nested}}}]}}"#),
+ Some(http::StatusCode::UNPROCESSABLE_ENTITY),
+ ),
+ (format!(r#"{{"records":[],"unknown":{nested}}}"#), None),
+ (nested.clone(), Some(http::StatusCode::UNPROCESSABLE_ENTITY)),
+ ];
+ // Tokio's default worker stack size; unbounded recursion over
+ // 50k levels overflows it.
+ std::thread::Builder::new()
+ .stack_size(2 * 1024 * 1024)
+ .spawn(move || {
+ for (input, expected_status) in &cases {
+ let status = parse_json::(input.as_bytes())
+ .err()
+ .map(|e| e.status());
+ assert_eq!(status, *expected_status);
+ }
+ let err = parse_json::(nested.as_bytes()).unwrap_err();
+ assert_eq!(err.status(), http::StatusCode::BAD_REQUEST);
+ assert!(err.body_text().contains("recursion limit exceeded"));
+ })
+ .unwrap()
+ .join()
+ .unwrap();
+ }
+
+ /// Serialize with serde_json and deserialize again, asserting semantic
+ /// equality for shapes with custom (de)serialization.
+ #[test]
+ fn serde_json_roundtrip() {
fn assert_roundtrip(input: &T)
where
T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug,
{
let json = serde_json::to_vec(input).unwrap();
- let from_serde: T = serde_json::from_slice(&json).unwrap();
- let from_sonic: T = sonic_rs::from_slice(&json).unwrap();
+ let parsed: T = parse_json(&json).unwrap();
assert_eq!(
- format!("{from_serde:?}"),
- format!("{from_sonic:?}"),
+ format!("{input:?}"),
+ format!("{parsed:?}"),
"roundtrip mismatch for {}",
String::from_utf8_lossy(&json),
);
From 3f23989396fca921e9fbda9fb34cf79671e71f58 Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
<262023388+release-pleaze[bot]@users.noreply.github.com>
Date: Wed, 9 Sep 2026 17:36:34 -0700
Subject: [PATCH 11/50] chore: release (#725)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 🤖 New release
* `s2-common`: 0.41.1 -> 0.41.2 (✓ API compatible changes)
* `s2-api`: 0.31.1 -> 0.31.2 (✓ API compatible changes)
* `s2-lite`: 0.42.8 -> 0.42.9 (✓ API compatible changes)
* `s2-sdk`: 0.34.4 -> 0.34.5 (✓ API compatible changes)
* `s2-cli`: 0.42.8 -> 0.42.9
* `s2-testcontainers`: 0.42.8 -> 0.42.9
Changelog
## `s2-common`
## [0.41.2] - 2026-09-10
### Bug Fixes
- Reject NUL bytes in stream names and access token IDs
([#728](https://github.com/s2-streamstore/s2/issues/728))
## `s2-api`
## [0.31.2] - 2026-09-10
### Bug Fixes
- Reject NUL bytes in stream names and access token IDs
([#728](https://github.com/s2-streamstore/s2/issues/728))
- Switch JSON extractor from sonic-rs back to serde_json
([#729](https://github.com/s2-streamstore/s2/issues/729))
## `s2-lite`
## [0.42.9] - 2026-09-10
### Bug Fixes
- Close SlateDB on graceful shutdown
- Reject NUL bytes in stream names and access token IDs
([#728](https://github.com/s2-streamstore/s2/issues/728))
## `s2-sdk`
## [0.34.5] - 2026-09-10
### Bug Fixes
- Reject NUL bytes in stream names and access token IDs
([#728](https://github.com/s2-streamstore/s2/issues/728))
## `s2-cli`
## [0.42.9] - 2026-09-10
## `s2-testcontainers`
## [0.42.9] - 2026-09-10
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
Co-authored-by: release-pleaze[bot] <262023388+release-pleaze[bot]@users.noreply.github.com>
---
Cargo.lock | 12 ++++++------
api/CHANGELOG.md | 9 +++++++++
api/Cargo.toml | 2 +-
cli/CHANGELOG.md | 4 ++++
cli/Cargo.toml | 2 +-
common/CHANGELOG.md | 8 ++++++++
common/Cargo.toml | 2 +-
lite/CHANGELOG.md | 9 +++++++++
lite/Cargo.toml | 2 +-
sdk/CHANGELOG.md | 8 ++++++++
sdk/Cargo.toml | 2 +-
testcontainers/CHANGELOG.md | 4 ++++
testcontainers/Cargo.toml | 2 +-
13 files changed, 54 insertions(+), 12 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 9fa1927f..51f290d9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4878,7 +4878,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "s2-api"
-version = "0.31.1"
+version = "0.31.2"
dependencies = [
"axum",
"base64ct",
@@ -4908,7 +4908,7 @@ dependencies = [
[[package]]
name = "s2-cli"
-version = "0.42.8"
+version = "0.42.9"
dependencies = [
"assert_cmd",
"async-stream",
@@ -4968,7 +4968,7 @@ dependencies = [
[[package]]
name = "s2-common"
-version = "0.41.1"
+version = "0.41.2"
dependencies = [
"axum",
"base64ct",
@@ -4992,7 +4992,7 @@ dependencies = [
[[package]]
name = "s2-lite"
-version = "0.42.8"
+version = "0.42.9"
dependencies = [
"async-stream",
"async-trait",
@@ -5051,7 +5051,7 @@ dependencies = [
[[package]]
name = "s2-sdk"
-version = "0.34.4"
+version = "0.34.5"
dependencies = [
"assert_matches",
"async-compression",
@@ -5111,7 +5111,7 @@ dependencies = [
[[package]]
name = "s2-testcontainers"
-version = "0.42.8"
+version = "0.42.9"
dependencies = [
"reqwest",
"s2-sdk",
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index 4c989e96..91342822 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
+## [0.31.2] - 2026-09-10
+
+### Bug Fixes
+
+- Reject NUL bytes in stream names and access token IDs ([#728](https://github.com/s2-streamstore/s2/issues/728))
+- Switch JSON extractor from sonic-rs back to serde_json ([#729](https://github.com/s2-streamstore/s2/issues/729))
+
+
+
## [0.31.1] - 2026-08-13
### Features
diff --git a/api/Cargo.toml b/api/Cargo.toml
index 691ccc7a..f239cbbc 100644
--- a/api/Cargo.toml
+++ b/api/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-api"
-version = "0.31.1"
+version = "0.31.2"
description = "API types for S2, the durable streams API"
edition.workspace = true
license.workspace = true
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 9dfeac91..4c38a855 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
+## [0.42.9] - 2026-09-10
+
+
+
## [0.42.8] - 2026-09-01
### Bug Fixes
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 64dfddeb..18ed5321 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-cli"
-version = "0.42.8"
+version = "0.42.9"
description = "CLI for S2"
edition.workspace = true
license.workspace = true
diff --git a/common/CHANGELOG.md b/common/CHANGELOG.md
index 5d7db4c4..0dfc5ad3 100644
--- a/common/CHANGELOG.md
+++ b/common/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.41.2] - 2026-09-10
+
+### Bug Fixes
+
+- Reject NUL bytes in stream names and access token IDs ([#728](https://github.com/s2-streamstore/s2/issues/728))
+
+
+
## [0.41.1] - 2026-07-22
### Miscellaneous Tasks
diff --git a/common/Cargo.toml b/common/Cargo.toml
index 202963a7..d768f987 100644
--- a/common/Cargo.toml
+++ b/common/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-common"
-version = "0.41.1"
+version = "0.41.2"
description = "Common stuff for client and servers for S2, the durable streams API"
edition.workspace = true
license.workspace = true
diff --git a/lite/CHANGELOG.md b/lite/CHANGELOG.md
index 3d7ff266..1dd3d5de 100644
--- a/lite/CHANGELOG.md
+++ b/lite/CHANGELOG.md
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
+## [0.42.9] - 2026-09-10
+
+### Bug Fixes
+
+- Close SlateDB on graceful shutdown
+- Reject NUL bytes in stream names and access token IDs ([#728](https://github.com/s2-streamstore/s2/issues/728))
+
+
+
## [0.42.8] - 2026-09-01
### Miscellaneous Tasks
diff --git a/lite/Cargo.toml b/lite/Cargo.toml
index bb5c7709..5f32b3ac 100644
--- a/lite/Cargo.toml
+++ b/lite/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-lite"
-version = "0.42.8"
+version = "0.42.9"
description = "Lightweight server implementation of S2, the durable streams API, backed by object storage"
edition.workspace = true
license.workspace = true
diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md
index 8d07c476..db646914 100644
--- a/sdk/CHANGELOG.md
+++ b/sdk/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.34.5] - 2026-09-10
+
+### Bug Fixes
+
+- Reject NUL bytes in stream names and access token IDs ([#728](https://github.com/s2-streamstore/s2/issues/728))
+
+
+
## [0.34.4] - 2026-09-01
### Features
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index ecdd764f..564b6ff0 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "s2-sdk"
description = "Rust SDK for S2"
-version = "0.34.4"
+version = "0.34.5"
edition.workspace = true
license.workspace = true
repository = "https://github.com/s2-streamstore/s2/tree/main/sdk"
diff --git a/testcontainers/CHANGELOG.md b/testcontainers/CHANGELOG.md
index aee84165..a5f8c2b9 100644
--- a/testcontainers/CHANGELOG.md
+++ b/testcontainers/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
+## [0.42.9] - 2026-09-10
+
+
+
## [0.42.8] - 2026-09-01
diff --git a/testcontainers/Cargo.toml b/testcontainers/Cargo.toml
index 3dfb58e1..810e646e 100644
--- a/testcontainers/Cargo.toml
+++ b/testcontainers/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-testcontainers"
-version = "0.42.8"
+version = "0.42.9"
description = "Testcontainers helpers for the S2 Docker image"
edition.workspace = true
license.workspace = true
From 198640ea044f0a861dba173ec9233cbe8584e3de Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
Date: Thu, 10 Sep 2026 01:12:01 +0000
Subject: [PATCH 12/50] Bump s2-lite-helm chart to appVersion 0.42.9
---
charts/s2-lite-helm/Chart.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/charts/s2-lite-helm/Chart.yaml b/charts/s2-lite-helm/Chart.yaml
index 35c7c06e..cccc07a9 100644
--- a/charts/s2-lite-helm/Chart.yaml
+++ b/charts/s2-lite-helm/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
name: s2-lite-helm
description: Self-hostable S2 streaming datastore using SlateDB on object storage
type: application
-version: 0.1.64
-appVersion: "0.42.8"
+version: 0.1.65
+appVersion: "0.42.9"
keywords:
- s2
- streaming
From 7eb44972ed40d535008a004d697cad43ea347d00 Mon Sep 17 00:00:00 2001
From: Stephen Balogh
Date: Thu, 10 Sep 2026 16:58:19 -0700
Subject: [PATCH 13/50] feat(sdk): set default request headers (`_hidden` only)
(#731)
Allow a set of additional `default_headers` to be specified. This will
be present on all requests, unless replaced by the SDK.
Setting content-encoding headers is not supported, SDK needs full
control of that.
This is motivated by an internal (s2 cloud) usecase, hence the gate
under `_hidden`.
---
sdk/src/api.rs | 259 ++++++++++++++++++++++++++++++++++++++++++++--
sdk/src/client.rs | 42 ++++++--
sdk/src/types.rs | 91 ++++++++++++++++
3 files changed, 376 insertions(+), 16 deletions(-)
diff --git a/sdk/src/api.rs b/sdk/src/api.rs
index e518babc..f412eaec 100644
--- a/sdk/src/api.rs
+++ b/sdk/src/api.rs
@@ -794,7 +794,10 @@ impl BaseClient {
C: client::Connect + Clone + Send + Sync + 'static,
{
let access_token_mode = config.access_token.mode();
- let mut default_headers = HeaderMap::new();
+ let mut default_headers = config.default_headers.clone();
+ // Authorization belongs to the configured token, including when a
+ // refreshable provider supplies it immediately before each attempt.
+ default_headers.remove(AUTHORIZATION);
#[cfg(feature = "_hidden")]
let mut access_token_provider = None;
match &config.access_token {
@@ -1262,10 +1265,9 @@ fn provision_result_from_parts(
#[cfg(test)]
mod tests {
#[cfg(feature = "_hidden")]
- use std::sync::{
- Mutex,
- atomic::{AtomicBool, AtomicUsize, Ordering},
- };
+ use std::sync::Mutex;
+ #[cfg(feature = "_hidden")]
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
#[cfg(feature = "_hidden")]
use async_trait::async_trait;
@@ -1274,6 +1276,241 @@ mod tests {
use super::*;
+ #[cfg(feature = "_hidden")]
+ #[derive(Default)]
+ struct HeaderCapture {
+ unary: Mutex>,
+ streaming: Mutex>,
+ }
+
+ #[cfg(feature = "_hidden")]
+ #[async_trait]
+ impl client::RequestExecutor for HeaderCapture {
+ async fn execute_unary(
+ &self,
+ mut request: client::Request,
+ ) -> Result {
+ let mut headers = self.unary.lock().unwrap();
+ headers.push(request.headers_mut().clone());
+ // Exercise a real retry through RequestBuilder::send.
+ Ok(if headers.len() == 1 {
+ UnaryResponse::new_for_test(
+ StatusCode::SERVICE_UNAVAILABLE,
+ br#"{"code":"unavailable","message":"retry"}"#.to_vec(),
+ )
+ } else {
+ UnaryResponse::new_for_test(
+ StatusCode::OK,
+ br#"{"basins":[],"streams":[],"has_more":false}"#.to_vec(),
+ )
+ })
+ }
+
+ async fn init_streaming(
+ &self,
+ mut request: client::Request,
+ ) -> Result {
+ self.streaming
+ .lock()
+ .unwrap()
+ .push(request.headers_mut().clone());
+ // Capturing initiation is sufficient: do not construct a response body.
+ Err(client::HttpError::Timeout)
+ }
+ }
+
+ #[cfg(feature = "_hidden")]
+ #[tokio::test]
+ async fn default_headers_reach_account_basin_streaming_and_retry_requests() {
+ let mut session = HeaderValue::from_static("session-1");
+ session.set_sensitive(true);
+ let headers = HeaderMap::from_iter([
+ (
+ http::header::HeaderName::from_static("x-origin-session"),
+ session,
+ ),
+ (
+ AUTHORIZATION,
+ HeaderValue::from_static("Bearer wrong-token"),
+ ),
+ (
+ http::header::USER_AGENT,
+ HeaderValue::from_static("wrong-agent"),
+ ),
+ (
+ http::header::ACCEPT_ENCODING,
+ HeaderValue::from_static("wrong-encoding"),
+ ),
+ (
+ http::header::HeaderName::from_static(S2_BASIN),
+ HeaderValue::from_static("wrong-basin"),
+ ),
+ (CONTENT_TYPE, HeaderValue::from_static("wrong-content-type")),
+ ]);
+ let config = S2Config::new("actual-token")
+ .with_endpoints(S2Endpoints::for_endpoint("http://example.test").unwrap())
+ .with_compression(Compression::Gzip)
+ .with_default_headers(headers)
+ .unwrap();
+ let executor = Arc::new(HeaderCapture::default());
+ let mut base = BaseClient::init_with_connector(&config, HttpConnector::new()).unwrap();
+ base.client = executor.clone();
+ base.retry_builder = RetryBackoffBuilder::default()
+ .with_min_base_delay(Duration::ZERO)
+ .with_max_base_delay(Duration::ZERO)
+ .with_max_retries(1);
+ let account = AccountClient::init(config.clone(), base);
+ account
+ .list_basins(ListBasinsRequest {
+ prefix: None,
+ start_after: None,
+ limit: None,
+ })
+ .await
+ .unwrap();
+ let basin = account.basin_client("test-basin".parse().unwrap());
+ basin
+ .list_streams(ListStreamsRequest {
+ prefix: None,
+ start_after: None,
+ limit: None,
+ })
+ .await
+ .unwrap();
+
+ let stream = "test-stream".parse().unwrap();
+ assert!(
+ basin
+ .read_session(
+ &stream,
+ ReadStart {
+ seq_num: None,
+ timestamp: None,
+ tail_offset: None,
+ clamp: None
+ },
+ ReadEnd {
+ count: None,
+ bytes: None,
+ until: None,
+ wait: None
+ },
+ None,
+ ReconnectAdvice::default(),
+ )
+ .await
+ .is_err()
+ );
+ assert!(
+ basin
+ .append_session(
+ &stream,
+ futures_util::stream::empty(),
+ None,
+ None,
+ ReconnectAdvice::default(),
+ )
+ .await
+ .is_err()
+ );
+
+ let unary = executor.unary.lock().unwrap();
+ let streaming = executor.streaming.lock().unwrap();
+ assert_eq!(unary.len(), 3); // account, account retry, basin
+ assert_eq!(streaming.len(), 2); // read and append
+ for headers in unary.iter().chain(streaming.iter()) {
+ assert_eq!(headers["x-origin-session"], "session-1");
+ assert!(headers["x-origin-session"].is_sensitive());
+ assert_eq!(headers[AUTHORIZATION], "Bearer actual-token");
+ assert!(headers[AUTHORIZATION].is_sensitive());
+ assert_eq!(headers[http::header::USER_AGENT], config.user_agent);
+ assert_eq!(headers[http::header::ACCEPT_ENCODING], "gzip");
+ assert!(!headers.contains_key(http::header::CONTENT_ENCODING));
+ }
+ assert_eq!(unary[2][S2_BASIN], "test-basin");
+ for headers in streaming.iter() {
+ assert_eq!(headers[S2_BASIN], "test-basin");
+ assert_eq!(headers[CONTENT_TYPE], CONTENT_TYPE_S2S);
+ }
+ }
+
+ #[cfg(feature = "_hidden")]
+ #[rstest::rstest]
+ #[case::none(Compression::None, None, "gzip, zstd")]
+ #[case::gzip(Compression::Gzip, Some("gzip"), "gzip")]
+ #[case::zstd(Compression::Zstd, Some("zstd"), "zstd")]
+ #[tokio::test]
+ async fn default_accept_encoding_respects_configured_compression(
+ #[case] compression: Compression,
+ #[case] content_encoding: Option<&str>,
+ #[case] accept_encoding: &str,
+ ) {
+ let config = S2Config::new("token")
+ .with_compression(compression)
+ .with_default_headers(HeaderMap::from_iter([(
+ http::header::ACCEPT_ENCODING,
+ HeaderValue::from_static("gzip, zstd"),
+ )]))
+ .unwrap();
+ let client = BaseClient::init_with_connector(&config, HttpConnector::new()).unwrap();
+ let mut request = client
+ .post("http://example.test/v1/basins".parse().unwrap())
+ .json(&serde_json::json!({"name": "test-basin"}))
+ .build()
+ .unwrap()
+ .compress()
+ .await
+ .unwrap();
+ let headers = request.headers_mut();
+ assert_eq!(
+ headers
+ .get(http::header::CONTENT_ENCODING)
+ .map(|value| value.to_str().unwrap()),
+ content_encoding
+ );
+ assert_eq!(headers[http::header::ACCEPT_ENCODING], accept_encoding);
+ }
+
+ #[cfg(feature = "_hidden")]
+ #[tokio::test]
+ async fn default_headers_are_replaced_and_isolated_between_clients() {
+ let first_headers = HeaderMap::from_iter([
+ (
+ http::header::HeaderName::from_static("x-origin-session"),
+ HeaderValue::from_static("first"),
+ ),
+ (
+ http::header::HeaderName::from_static("x-first-only"),
+ HeaderValue::from_static("present"),
+ ),
+ ]);
+ let first = S2Config::new("token")
+ .with_default_headers(first_headers)
+ .unwrap();
+ let second = first
+ .clone()
+ .with_default_headers(HeaderMap::from_iter([(
+ http::header::HeaderName::from_static("x-origin-session"),
+ HeaderValue::from_static("second"),
+ )]))
+ .unwrap();
+ for (config, session) in [(&first, "first"), (&second, "second")] {
+ let client = BaseClient::init_with_connector(config, HttpConnector::new()).unwrap();
+ let mut request = client
+ .get("http://example.test".parse().unwrap())
+ .build()
+ .unwrap();
+ assert_eq!(request.headers_mut()["x-origin-session"], session);
+ assert_eq!(
+ request.headers_mut().contains_key("x-first-only"),
+ session == "first"
+ );
+ }
+ let cleared = second.with_default_headers(HeaderMap::new()).unwrap();
+ assert!(cleared.default_headers.is_empty());
+ assert!(S2Config::new("token").default_headers.is_empty());
+ }
+
#[cfg(feature = "_hidden")]
#[derive(Debug)]
struct RotatingTokenProvider {
@@ -1435,9 +1672,15 @@ mod tests {
#[cfg(feature = "_hidden")]
#[tokio::test]
async fn dynamic_access_token_is_loaded_for_each_attempt_and_marked_sensitive() {
- let config = S2Config::new("unused").with_access_token_provider(RotatingTokenProvider {
- generation: AtomicUsize::new(1),
- });
+ let config = S2Config::new("unused")
+ .with_default_headers(HeaderMap::from_iter([(
+ AUTHORIZATION,
+ HeaderValue::from_static("Bearer wrong-token"),
+ )]))
+ .unwrap()
+ .with_access_token_provider(RotatingTokenProvider {
+ generation: AtomicUsize::new(1),
+ });
let client = BaseClient::init_with_connector(&config, HttpConnector::new()).unwrap();
let uri = "http://example.test/v1/basins".parse().unwrap();
let mut request = client.get(uri).build().unwrap();
diff --git a/sdk/src/client.rs b/sdk/src/client.rs
index b38d3f31..abf927ab 100644
--- a/sdk/src/client.rs
+++ b/sdk/src/client.rs
@@ -304,9 +304,8 @@ impl RequestBuilder {
}
pub fn headers(mut self, headers: &HeaderMap) -> Self {
- for (key, value) in headers {
- self.headers.insert(key.clone(), value.clone());
- }
+ // An owned HeaderMap replaces each key's existing values while preserving duplicates.
+ self.headers.extend(headers.clone());
self
}
@@ -574,11 +573,7 @@ fn build_http_request(
let mut builder = http::Request::builder().method(method).uri(uri.clone());
if let Some(req_headers) = builder.headers_mut() {
- for (key, value) in headers {
- if let Some(key) = key {
- req_headers.insert(key, value);
- }
- }
+ *req_headers = headers;
if let Some(encoding) = content_encoding {
req_headers.insert(CONTENT_ENCODING, encoding);
}
@@ -1016,6 +1011,37 @@ mod tests {
Pool::new(HttpConnector::new())
}
+ #[test]
+ fn default_headers_preserve_multiple_values_and_sensitive_flags_on_wire_request() {
+ let mut headers = HeaderMap::new();
+ headers.append("x-tag", HeaderValue::from_static("first"));
+ let mut second = HeaderValue::from_static("second");
+ second.set_sensitive(true);
+ headers.append("x-tag", second);
+ headers.insert(CONTENT_ENCODING, HeaderValue::from_static("wrong-encoding"));
+
+ let request = RequestBuilder::get("http://example.test".parse().unwrap())
+ .header("x-tag", "replaced")
+ .headers(&headers)
+ .build()
+ .unwrap();
+ let cloned = request.try_clone().unwrap();
+ let http = build_http_request(
+ cloned.method,
+ &cloned.uri,
+ cloned.headers,
+ cloned.body.into_http_body(),
+ Some(HeaderValue::from_static("gzip")),
+ )
+ .unwrap();
+ let values = http.headers().get_all("x-tag").iter().collect::>();
+ assert_eq!(values.len(), 2);
+ assert_eq!(values[0], "first");
+ assert_eq!(values[1], "second");
+ assert!(values[1].is_sensitive());
+ assert_eq!(http.headers()[CONTENT_ENCODING], "gzip");
+ }
+
#[test]
fn uri_with_path_percent_encoded_segment() {
let base: Uri = "https://example.com".parse().unwrap();
diff --git a/sdk/src/types.rs b/sdk/src/types.rs
index 6d324782..9f2d6631 100644
--- a/sdk/src/types.rs
+++ b/sdk/src/types.rs
@@ -16,6 +16,7 @@ use std::{
use async_trait::async_trait;
use bytes::Bytes;
use http::{
+ HeaderMap,
header::HeaderValue,
uri::{Authority, Scheme},
};
@@ -511,6 +512,7 @@ pub struct S2Config {
pub(crate) retry: RetryConfig,
pub(crate) compression: Compression,
pub(crate) user_agent: HeaderValue,
+ pub(crate) default_headers: HeaderMap,
pub(crate) insecure_skip_cert_verification: bool,
pub(crate) rustls_crypto_provider: Option>,
}
@@ -528,6 +530,7 @@ impl S2Config {
user_agent: concat!("s2-sdk-rust/", env!("CARGO_PKG_VERSION"))
.parse()
.expect("valid user agent"),
+ default_headers: HeaderMap::new(),
insecure_skip_cert_verification: false,
rustls_crypto_provider: default_rustls_crypto_provider(),
}
@@ -547,6 +550,52 @@ impl S2Config {
Self { endpoints, ..self }
}
+ /// Set additional HTTP headers to send with every request.
+ ///
+ /// These headers apply to account, basin, and stream operations, including
+ /// retries and streaming requests. SDK-generated headers, such as
+ /// authorization and basin routing, take precedence over these defaults.
+ /// Calling this method again replaces the previous set of default headers.
+ ///
+ /// `Accept-Encoding` defaults are used only when [`Compression::None`] is
+ /// configured; otherwise the SDK sets the header to the configured
+ /// compression algorithm.
+ ///
+ /// Headers are sent to all configured S2 endpoints. Use
+ /// [`HeaderValue::set_sensitive`] for values that should be redacted in debug
+ /// output. Do not use these defaults for per-request identifiers, since the
+ /// same values are reused across requests.
+ ///
+ /// # Errors
+ ///
+ /// Returns an error if `default_headers` contains `Content-Encoding`,
+ /// `Content-Length`, or `Transfer-Encoding`. The SDK controls body framing.
+ /// Use [`Self::with_compression`] to configure request body encoding.
+ #[cfg(feature = "_hidden")]
+ #[doc(hidden)]
+ pub fn with_default_headers(self, default_headers: HeaderMap) -> Result {
+ if default_headers.contains_key(http::header::CONTENT_ENCODING) {
+ return Err(ValidationError(
+ "Content-Encoding cannot be set in default headers; use S2Config::with_compression instead"
+ .into(),
+ ));
+ }
+ for name in [
+ http::header::CONTENT_LENGTH,
+ http::header::TRANSFER_ENCODING,
+ ] {
+ if default_headers.contains_key(&name) {
+ return Err(ValidationError(format!(
+ "{name} cannot be set in default headers; the SDK controls request body framing"
+ )));
+ }
+ }
+ Ok(Self {
+ default_headers,
+ ..self
+ })
+ }
+
/// Set the timeout for establishing a connection to the server.
///
/// Defaults to `3s`.
@@ -3989,6 +4038,48 @@ mod tests {
assert!(!cfg.insecure_skip_cert_verification);
}
+ #[cfg(feature = "_hidden")]
+ #[rstest]
+ #[case::matching_compression("content-encoding", "gzip", Compression::Gzip)]
+ #[case::mixed_case("Content-Encoding", "identity", Compression::None)]
+ #[case::empty_value("content-encoding", "", Compression::None)]
+ fn default_headers_reject_content_encoding(
+ #[case] name: &str,
+ #[case] value: &str,
+ #[case] compression: Compression,
+ ) {
+ let headers = HeaderMap::from_iter([(
+ name.parse::().unwrap(),
+ HeaderValue::from_str(value).unwrap(),
+ )]);
+ let error = S2Config::new("token")
+ .with_compression(compression)
+ .with_default_headers(headers)
+ .unwrap_err();
+ assert!(error.0.contains("Content-Encoding"));
+ assert!(error.0.contains("with_compression"));
+ }
+
+ #[cfg(feature = "_hidden")]
+ #[rstest]
+ #[case::content_length("content-length", "123")]
+ #[case::content_length_mixed_case("Content-Length", "0")]
+ #[case::content_length_empty("content-length", "")]
+ #[case::transfer_encoding("transfer-encoding", "chunked")]
+ #[case::transfer_encoding_mixed_case("Transfer-Encoding", "chunked")]
+ #[case::transfer_encoding_empty("transfer-encoding", "")]
+ fn default_headers_reject_framing_headers(#[case] name: &str, #[case] value: &str) {
+ let headers = HeaderMap::from_iter([(
+ name.parse::().unwrap(),
+ HeaderValue::from_str(value).unwrap(),
+ )]);
+ let error = S2Config::new("token")
+ .with_default_headers(headers)
+ .unwrap_err();
+ assert!(error.0.contains(&name.to_ascii_lowercase()));
+ assert!(error.0.contains("framing"));
+ }
+
// -- StorageClass --
#[rstest]
From 24b3e7862022e1a1678c9b93ba4e6f61fe9f48ca Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
<262023388+release-pleaze[bot]@users.noreply.github.com>
Date: Thu, 10 Sep 2026 17:17:29 -0700
Subject: [PATCH 14/50] chore: release (#732)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 🤖 New release
* `s2-lite`: 0.42.9 -> 0.42.10 (✓ API compatible changes)
* `s2-sdk`: 0.34.5 -> 0.34.6 (✓ API compatible changes)
* `s2-cli`: 0.42.9 -> 0.42.10
* `s2-testcontainers`: 0.42.9 -> 0.42.10
Changelog
## `s2-lite`
## [0.42.10] - 2026-09-11
### Miscellaneous Tasks
- Update Cargo.lock dependencies
## `s2-sdk`
## [0.34.6] - 2026-09-11
### Features
- Set default request headers (`_hidden` only)
([#731](https://github.com/s2-streamstore/s2/issues/731))
## `s2-cli`
## [0.42.10] - 2026-09-11
## `s2-testcontainers`
## [0.42.10] - 2026-09-11
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
Co-authored-by: release-pleaze[bot] <262023388+release-pleaze[bot]@users.noreply.github.com>
---
Cargo.lock | 8 ++++----
cli/CHANGELOG.md | 4 ++++
cli/Cargo.toml | 2 +-
lite/CHANGELOG.md | 8 ++++++++
lite/Cargo.toml | 2 +-
sdk/CHANGELOG.md | 8 ++++++++
sdk/Cargo.toml | 2 +-
testcontainers/CHANGELOG.md | 4 ++++
testcontainers/Cargo.toml | 2 +-
9 files changed, 32 insertions(+), 8 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 51f290d9..6674ec5d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4908,7 +4908,7 @@ dependencies = [
[[package]]
name = "s2-cli"
-version = "0.42.9"
+version = "0.42.10"
dependencies = [
"assert_cmd",
"async-stream",
@@ -4992,7 +4992,7 @@ dependencies = [
[[package]]
name = "s2-lite"
-version = "0.42.9"
+version = "0.42.10"
dependencies = [
"async-stream",
"async-trait",
@@ -5051,7 +5051,7 @@ dependencies = [
[[package]]
name = "s2-sdk"
-version = "0.34.5"
+version = "0.34.6"
dependencies = [
"assert_matches",
"async-compression",
@@ -5111,7 +5111,7 @@ dependencies = [
[[package]]
name = "s2-testcontainers"
-version = "0.42.9"
+version = "0.42.10"
dependencies = [
"reqwest",
"s2-sdk",
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 4c38a855..e3577529 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
+## [0.42.10] - 2026-09-11
+
+
+
## [0.42.9] - 2026-09-10
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 18ed5321..e5922e1a 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-cli"
-version = "0.42.9"
+version = "0.42.10"
description = "CLI for S2"
edition.workspace = true
license.workspace = true
diff --git a/lite/CHANGELOG.md b/lite/CHANGELOG.md
index 1dd3d5de..214caca7 100644
--- a/lite/CHANGELOG.md
+++ b/lite/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.42.10] - 2026-09-11
+
+### Miscellaneous Tasks
+
+- Update Cargo.lock dependencies
+
+
+
## [0.42.9] - 2026-09-10
### Bug Fixes
diff --git a/lite/Cargo.toml b/lite/Cargo.toml
index 5f32b3ac..72013081 100644
--- a/lite/Cargo.toml
+++ b/lite/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-lite"
-version = "0.42.9"
+version = "0.42.10"
description = "Lightweight server implementation of S2, the durable streams API, backed by object storage"
edition.workspace = true
license.workspace = true
diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md
index db646914..d2d6b1ec 100644
--- a/sdk/CHANGELOG.md
+++ b/sdk/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.34.6] - 2026-09-11
+
+### Features
+
+- Set default request headers (`_hidden` only) ([#731](https://github.com/s2-streamstore/s2/issues/731))
+
+
+
## [0.34.5] - 2026-09-10
### Bug Fixes
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 564b6ff0..aa3665ae 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "s2-sdk"
description = "Rust SDK for S2"
-version = "0.34.5"
+version = "0.34.6"
edition.workspace = true
license.workspace = true
repository = "https://github.com/s2-streamstore/s2/tree/main/sdk"
diff --git a/testcontainers/CHANGELOG.md b/testcontainers/CHANGELOG.md
index a5f8c2b9..0fa9bdf8 100644
--- a/testcontainers/CHANGELOG.md
+++ b/testcontainers/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
+## [0.42.10] - 2026-09-11
+
+
+
## [0.42.9] - 2026-09-10
diff --git a/testcontainers/Cargo.toml b/testcontainers/Cargo.toml
index 810e646e..e955d07f 100644
--- a/testcontainers/Cargo.toml
+++ b/testcontainers/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-testcontainers"
-version = "0.42.9"
+version = "0.42.10"
description = "Testcontainers helpers for the S2 Docker image"
edition.workspace = true
license.workspace = true
From 4aebddaba02fb6541647a7f76d49ff094afbc98f Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
Date: Fri, 11 Sep 2026 00:56:03 +0000
Subject: [PATCH 15/50] Bump s2-lite-helm chart to appVersion 0.42.10
---
charts/s2-lite-helm/Chart.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/charts/s2-lite-helm/Chart.yaml b/charts/s2-lite-helm/Chart.yaml
index cccc07a9..25315647 100644
--- a/charts/s2-lite-helm/Chart.yaml
+++ b/charts/s2-lite-helm/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
name: s2-lite-helm
description: Self-hostable S2 streaming datastore using SlateDB on object storage
type: application
-version: 0.1.65
-appVersion: "0.42.9"
+version: 0.1.66
+appVersion: "0.42.10"
keywords:
- s2
- streaming
From a4f247aeed78fda270bfaa07ef4bc77d5f76d6d4 Mon Sep 17 00:00:00 2001
From: Mehul Arora
Date: Fri, 11 Sep 2026 21:01:21 +0530
Subject: [PATCH 16/50] feat: `s2-stream-config` header for auto-created
streams (#718)
## Summary
When a basin has `create_stream_on_append` enabled, an append can carry
an `s2-stream-config` header whose value is a compact JSON
`StreamConfig`. If that request is the one that creates the stream, the
config is layered over the basin's `default_stream_config`; unset fields
inherit the defaults. It is ignored once the stream exists, so clients
can attach it to every append without tracking whether the stream has
been created.
This gives per-stream config (e.g. retention, delete-on-empty) on
auto-created streams without a control-plane round trip.
Spec half: s2-streamstore/s2-specs#21 (this PR bumps the `api/specs`
submodule to that branch's commit; re-bump to the merge commit once it
lands).
## API
One header, same for JSON, proto and S2S (an append session is a single
request, so the header covers the whole session):
```sh
curl -X POST "https://$BASIN.b.s2.dev/v1/streams/tenant-42%2Fevents/records" \
-H "Authorization: Bearer $S2_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H 's2-stream-config: {"retention_policy":{"age":3600},"delete_on_empty":{"min_age_secs":300}}' \
-d '{"records": [{"body": "hello"}]}'
```
The value is validated exactly like a `CreateStream` config; an invalid
value is rejected with `400 bad_header` before any lookup and no stream
is created:
```json
{"code":"bad_header","message":"Invalid header `s2-stream-config`: age must be greater than 0 seconds"}
```
SDK: the option lives on the stream handle, like the encryption key, and
applies to unary appends, append sessions and producers:
```rust
let stream = basin
.stream(name)
.with_stream_config(
StreamConfig::new()
.with_retention_policy(RetentionPolicy::Age(3600))
.with_delete_on_empty(DeleteOnEmptyConfig::new().with_min_age(Duration::from_secs(300))),
);
stream.append(input).await?; // or
stream.producer(ProducerConfig::default()); // or stream.append_session(..)
```
CLI:
```sh
echo hello | s2 append s2://my-basin/tenant-42/events --format text \
--retention-policy 1h --delete-on-empty-min-age 5m
```
## Why a header
- No proto change: the same header carries the config for unary appends
and S2S sessions, so `AppendInput` (which flows into storage) stays
untouched.
- Known before the server responds. With a body/frame field, S2S
sessions needed the server to wait for the first frame before creating
the stream, while clients wait for response headers before sending it;
the header removes that ordering problem entirely.
- Reusable for read paths (`create_stream_on_read`) later, since `GET`
has no body.
## Changes
- **api**
- `v1::config::STREAM_CONFIG_HEADER` (`s2-stream-config`) and
`StreamConfigHeader`, a `ParseableHeader` that deserializes the JSON
`StreamConfig` and reuses `TryFrom for
OptionalStreamConfig` so validation lives in one place.
`to_header_value` for clients.
- `data::S2StreamConfigHeader` documents the header in OpenAPI (string
schema, with an example value; utoipa cannot express `content` on a
parameter).
- `AppendRequest::Unary` / `S2s` gain `stream_config:
OptionalStreamConfig`, parsed once in the extractor.
- **lite**
- `stream_handle_with_auto_create` takes an `AutoCreateOn` (`Append` /
`Read`) and the `OptionalStreamConfig` to layer over the basin defaults
when creating.
- `Backend::open_for_append(.., stream_config)` serves both unary
appends and sessions; the stream is created (or the request fails)
before the response, as before this feature.
- **sdk**: `S2Stream::with_stream_config`, mirroring
`with_encryption_key`. Internally, `AppendHeaders { encryption,
stream_config }` is threaded through sessions/producers and set on every
(re)connect.
- **cli**: `s2 append` accepts the same stream config flags as
`create-stream` (`--retention-policy`, `--storage-class`,
`--timestamping-*`, `--delete-on-empty-min-age`), listed under their own
help heading; set on the stream handle.
## Compatibility
- Old clients never send the header; old servers ignore unknown headers.
- `s2-api` public API change: `AppendRequest` variants gain a field.
## Testing
- `s2-api` unit: header parse/validate (valid, `{}`, invalid JSON, `age:
0`) and `to_header_value` roundtrip.
- Backend-level: applies + merges with basin defaults; existing stream
ignores config.
- HTTP-level: JSON unary with header; invalid header -> `400 bad_header`
with no stream created (both invalid config and non-JSON); S2S session
with header.
- SDK integration against `s2 lite`: unary + producer create with
config, existing stream unchanged.
- CLI integration against `s2 lite`: 47/47 pass.
- `clippy -D warnings` clean; workspace unit suites pass.
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Cursor
---
Cargo.lock | 4 +-
Cargo.toml | 2 +-
api/specs | 2 +-
api/src/data.rs | 19 ++
api/src/v1/config.rs | 98 +++++++-
api/src/v1/stream/extract.rs | 13 +-
api/src/v1/stream/mod.rs | 15 ++
cli/src/cli.rs | 18 ++
cli/src/main.rs | 10 +-
cli/src/ops.rs | 32 ++-
cli/tests/integration.rs | 89 +++++++
lite/src/backend/append.rs | 11 +-
lite/src/backend/core.rs | 99 ++++----
lite/src/backend/read.rs | 37 +--
lite/src/handlers/v1/records.rs | 230 ++++++++++++++++++-
lite/tests/backend/common/mod.rs | 3 +
lite/tests/backend/common/read.rs | 8 +-
lite/tests/backend/common/setup.rs | 9 +-
lite/tests/backend/data_plane/auto_create.rs | 139 ++++++++++-
sdk/src/api.rs | 23 +-
sdk/src/batching.rs | 1 +
sdk/src/ops.rs | 28 ++-
sdk/src/producer.rs | 29 ++-
sdk/src/session/append.rs | 49 ++--
sdk/src/session/mod.rs | 9 +
sdk/src/session/read.rs | 21 +-
sdk/src/types.rs | 32 +++
sdk/tests/stream_ops.rs | 117 +++++++++-
28 files changed, 1023 insertions(+), 124 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 6674ec5d..7ffc34a3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6615,7 +6615,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "utoipa"
version = "5.4.0"
-source = "git+https://github.com/infiniteregrets/utoipa?rev=9cd181d40ac0a50551ebe1995a4d73e8685890d6#9cd181d40ac0a50551ebe1995a4d73e8685890d6"
+source = "git+https://github.com/infiniteregrets/utoipa?rev=82bcb28a792ba9a0d29963827ec473823099fc94#82bcb28a792ba9a0d29963827ec473823099fc94"
dependencies = [
"indexmap 2.14.0",
"serde",
@@ -6626,7 +6626,7 @@ dependencies = [
[[package]]
name = "utoipa-gen"
version = "5.4.0"
-source = "git+https://github.com/infiniteregrets/utoipa?rev=9cd181d40ac0a50551ebe1995a4d73e8685890d6#9cd181d40ac0a50551ebe1995a4d73e8685890d6"
+source = "git+https://github.com/infiniteregrets/utoipa?rev=82bcb28a792ba9a0d29963827ec473823099fc94#82bcb28a792ba9a0d29963827ec473823099fc94"
dependencies = [
"proc-macro2",
"quote",
diff --git a/Cargo.toml b/Cargo.toml
index a092a9b7..d6801da5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -97,7 +97,7 @@ xxhash-rust = "0.8"
zstd = "0.13"
[patch.crates-io]
-utoipa = { git = "https://github.com/infiniteregrets/utoipa", rev = "9cd181d40ac0a50551ebe1995a4d73e8685890d6" }
+utoipa = { git = "https://github.com/infiniteregrets/utoipa", rev = "82bcb28a792ba9a0d29963827ec473823099fc94" }
[profile.dev]
panic = "abort"
diff --git a/api/specs b/api/specs
index 973e0d92..f29cbcaa 160000
--- a/api/specs
+++ b/api/specs
@@ -1 +1 @@
-Subproject commit 973e0d92166ee1386b71d4e7ade8bd0fc2aaf4b8
+Subproject commit f29cbcaafeaa3d4970918a9a31f9e00b55fdabb4
diff --git a/api/src/data.rs b/api/src/data.rs
index f9fc4fc7..0b7c41d2 100644
--- a/api/src/data.rs
+++ b/api/src/data.rs
@@ -96,6 +96,25 @@ pub struct S2FormatHeader {
pub s2_format: Format,
}
+#[rustfmt::skip]
+#[derive(Debug)]
+#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams))]
+#[cfg_attr(feature = "utoipa", into_params(parameter_in = Header))]
+pub struct S2StreamConfigHeader {
+ /// JSON-encoded `StreamConfig` to apply if the stream is created on append or read.
+ /// Unset fields inherit the basin's default stream configuration.
+ /// Ignored if the stream already exists.
+ /// Compact JSON is preferred.
+ #[cfg_attr(feature = "utoipa", param(
+ required = false,
+ rename = "s2-stream-config",
+ content_type = "application/json",
+ value_type = crate::v1::config::StreamConfig,
+ example = json!({"retention_policy":{"age":3600},"delete_on_empty":{"min_age_secs":300}}),
+ ))]
+ pub s2_stream_config: String,
+}
+
#[rustfmt::skip]
#[derive(Debug)]
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams))]
diff --git a/api/src/v1/config.rs b/api/src/v1/config.rs
index fc523439..d48c9a87 100644
--- a/api/src/v1/config.rs
+++ b/api/src/v1/config.rs
@@ -1,6 +1,7 @@
-use std::time::Duration;
+use std::{str::FromStr, time::Duration};
-use s2_common::maybe::Maybe;
+use http::{HeaderName, HeaderValue};
+use s2_common::{http::ParseableHeader, maybe::Maybe};
use serde::{Deserialize, Serialize};
#[rustfmt::skip]
@@ -331,6 +332,12 @@ impl StreamConfig {
Some(config)
}
}
+
+ /// Encode as compact JSON for the `s2-stream-config` header.
+ pub fn to_header_value(&self) -> HeaderValue {
+ let json = serde_json::to_string(self).expect("StreamConfig serializes to JSON");
+ HeaderValue::from_str(&json).expect("compact JSON of StreamConfig is a valid header value")
+ }
}
impl From for StreamConfig {
@@ -351,6 +358,30 @@ impl From for StreamConfig {
}
}
+pub static STREAM_CONFIG_HEADER: HeaderName = HeaderName::from_static("s2-stream-config");
+
+/// Value of the `s2-stream-config` header: a JSON-encoded [`StreamConfig`] to apply over the
+/// basin's default stream config if the stream is created on append or read. Ignored if the
+/// stream already exists.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct StreamConfigHeader(pub s2_common::config::OptionalStreamConfig);
+
+impl FromStr for StreamConfigHeader {
+ type Err = s2_common::ValidationError;
+
+ fn from_str(s: &str) -> Result {
+ let config: StreamConfig =
+ serde_json::from_str(s).map_err(|e| format!("invalid JSON: {e}"))?;
+ Ok(Self(config.try_into()?))
+ }
+}
+
+impl ParseableHeader for StreamConfigHeader {
+ fn name() -> &'static HeaderName {
+ &STREAM_CONFIG_HEADER
+ }
+}
+
impl TryFrom for s2_common::config::OptionalStreamConfig {
type Error = s2_common::ValidationError;
@@ -1057,4 +1088,67 @@ mod tests {
"delete_on_empty.min_age should be None"
);
}
+
+ #[test]
+ fn stream_config_header_parses_and_validates() {
+ let header: StreamConfigHeader =
+ r#"{"retention_policy":{"age":3600},"delete_on_empty":{"min_age_secs":300}}"#
+ .parse()
+ .unwrap();
+ assert_eq!(
+ header.0,
+ s2_common::config::OptionalStreamConfig {
+ retention_policy: Some(s2_common::config::RetentionPolicy::Age(
+ Duration::from_secs(3600)
+ )),
+ delete_on_empty: s2_common::config::OptionalDeleteOnEmptyConfig {
+ min_age: Some(Duration::from_secs(300)),
+ },
+ ..Default::default()
+ }
+ );
+
+ for spaced in [
+ r#"{ "retention_policy": { "age": 3600 }, "delete_on_empty": { "min_age_secs": 300 } }"#,
+ "{\t\"delete_on_empty\":\t{\"min_age_secs\":\t300},\t\"retention_policy\":\t{\"age\":\t3600}\t}",
+ " {\"retention_policy\":{\"age\":3600},\"delete_on_empty\":{\"min_age_secs\":300}} ",
+ ] {
+ let parsed: StreamConfigHeader = spaced.parse().unwrap();
+ assert_eq!(parsed, header, "{spaced:?}");
+ }
+
+ let empty: StreamConfigHeader = "{}".parse().unwrap();
+ assert_eq!(empty.0, Default::default());
+
+ let invalid_json = "not json".parse::().unwrap_err();
+ assert!(invalid_json.to_string().contains("invalid JSON"));
+
+ let invalid_age =
+ r#"{"retention_policy":{"age":0}}"#.parse::().unwrap_err();
+ assert!(
+ invalid_age
+ .to_string()
+ .contains("age must be greater than 0 seconds"),
+ "{invalid_age}"
+ );
+ }
+
+ #[test]
+ fn stream_config_header_value_roundtrips() {
+ let config = StreamConfig {
+ storage_class: Some(StorageClass::Express),
+ retention_policy: Some(RetentionPolicy::Infinite(InfiniteRetention {})),
+ timestamping: Some(TimestampingConfig {
+ mode: Some(TimestampingMode::ClientRequire),
+ uncapped: Some(true),
+ }),
+ delete_on_empty: Some(DeleteOnEmptyConfig { min_age_secs: 60 }),
+ };
+ let value = config.to_header_value();
+ let parsed: StreamConfigHeader = value.to_str().unwrap().parse().unwrap();
+ assert_eq!(
+ parsed.0,
+ s2_common::config::OptionalStreamConfig::try_from(config).unwrap()
+ );
+ }
}
diff --git a/api/src/v1/stream/extract.rs b/api/src/v1/stream/extract.rs
index 6d508d45..deae94fb 100644
--- a/api/src/v1/stream/extract.rs
+++ b/api/src/v1/stream/extract.rs
@@ -17,7 +17,7 @@ use crate::{
extract::{JsonExtractionRejection, ProtoRejection},
},
mime::JsonOrProto,
- v1::stream::sse::LastEventId,
+ v1::{config::StreamConfigHeader, stream::sse::LastEventId},
};
#[derive(Debug, thiserror::Error)]
@@ -54,6 +54,9 @@ where
async fn from_request(req: Request, state: &S) -> Result {
let content_type = crate::mime::content_type(req.headers());
let encryption_key = parse_header_opt::(req.headers())?;
+ let create_stream_config_patch = parse_header_opt::(req.headers())?
+ .map(|header| header.0)
+ .unwrap_or_default();
if content_type.as_ref().is_some_and(crate::mime::is_s2s_proto) {
let response_compression =
@@ -88,6 +91,7 @@ where
return Ok(Self::S2s {
encryption_key,
+ create_stream_config_patch,
inputs: Box::pin(inputs),
response_compression,
});
@@ -117,6 +121,7 @@ where
Ok(Self::Unary {
encryption_key,
+ create_stream_config_patch,
input,
response_mime,
})
@@ -132,12 +137,16 @@ where
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result {
let content_type = crate::mime::content_type(&parts.headers);
let encryption_key = parse_header_opt::(&parts.headers)?;
+ let create_stream_config_patch = parse_header_opt::(&parts.headers)?
+ .map(|header| header.0)
+ .unwrap_or_default();
if content_type.as_ref().is_some_and(crate::mime::is_s2s_proto) {
let response_compression =
s2s::CompressionAlgorithm::from_accept_encoding(&parts.headers);
return Ok(Self::S2s {
encryption_key,
+ create_stream_config_patch,
response_compression,
});
}
@@ -150,6 +159,7 @@ where
let last_event_id = parse_header_opt::(&parts.headers)?;
return Ok(Self::EventStream {
encryption_key,
+ create_stream_config_patch,
format,
last_event_id,
});
@@ -162,6 +172,7 @@ where
Ok(Self::Unary {
encryption_key,
+ create_stream_config_patch,
format,
response_mime,
})
diff --git a/api/src/v1/stream/mod.rs b/api/src/v1/stream/mod.rs
index 255879cd..2d8d66e8 100644
--- a/api/src/v1/stream/mod.rs
+++ b/api/src/v1/stream/mod.rs
@@ -11,6 +11,7 @@ use std::time::Duration;
use futures_core::stream::BoxStream;
use itertools::Itertools as _;
use s2_common::{
+ config::OptionalStreamConfig,
encryption::EncryptionKey,
record,
stream::{StreamName, StreamNamePrefix, StreamNameStartAfter},
@@ -210,18 +211,24 @@ pub enum ReadRequest {
/// Unary
Unary {
encryption_key: Option,
+ /// Parsed `s2-stream-config` header; empty if absent.
+ create_stream_config_patch: OptionalStreamConfig,
format: Format,
response_mime: JsonOrProto,
},
/// Server-Sent Events streaming response
EventStream {
encryption_key: Option,
+ /// Parsed `s2-stream-config` header; empty if absent.
+ create_stream_config_patch: OptionalStreamConfig,
format: Format,
last_event_id: Option,
},
/// S2S streaming response
S2s {
encryption_key: Option,
+ /// Parsed `s2-stream-config` header; empty if absent.
+ create_stream_config_patch: OptionalStreamConfig,
response_compression: s2s::CompressionAlgorithm,
},
}
@@ -230,12 +237,16 @@ pub enum AppendRequest {
/// Unary
Unary {
encryption_key: Option,
+ /// Parsed `s2-stream-config` header; empty if absent.
+ create_stream_config_patch: OptionalStreamConfig,
input: s2_common::stream::AppendInput,
response_mime: JsonOrProto,
},
/// S2S bi-directional streaming
S2s {
encryption_key: Option,
+ /// Parsed `s2-stream-config` header; empty if absent.
+ create_stream_config_patch: OptionalStreamConfig,
inputs: BoxStream<'static, Result>,
response_compression: s2s::CompressionAlgorithm,
},
@@ -246,21 +257,25 @@ impl std::fmt::Debug for AppendRequest {
match self {
AppendRequest::Unary {
encryption_key,
+ create_stream_config_patch,
input,
response_mime: response,
} => f
.debug_struct("AppendRequest::Unary")
.field("encryption_key", encryption_key)
+ .field("create_stream_config_patch", create_stream_config_patch)
.field("input", input)
.field("response", response)
.finish(),
AppendRequest::S2s {
encryption_key,
+ create_stream_config_patch,
response_compression,
..
} => f
.debug_struct("AppendRequest::S2s")
.field("encryption_key", encryption_key)
+ .field("create_stream_config_patch", create_stream_config_patch)
.field("response_compression", response_compression)
.finish(),
}
diff --git a/cli/src/cli.rs b/cli/src/cli.rs
index 74981c1a..fe5d148a 100644
--- a/cli/src/cli.rs
+++ b/cli/src/cli.rs
@@ -693,6 +693,15 @@ pub struct AppendArgs {
#[command(flatten)]
pub encryption_key: EncryptionKeyArgs,
+
+ /// Stream configuration to apply if the stream is created on append.
+ /// Unset fields inherit the basin's default stream configuration.
+ /// Ignored if the stream already exists.
+ #[command(
+ flatten,
+ next_help_heading = "Stream configuration (applied only if the stream is created on append)"
+ )]
+ pub stream_config: StreamConfig,
}
#[derive(Args, Debug, Clone, Default)]
@@ -771,6 +780,15 @@ pub struct ReadArgs {
#[command(flatten)]
pub encryption_key: EncryptionKeyArgs,
+
+ /// Stream configuration to apply if the stream is created on read.
+ /// Unset fields inherit the basin's default stream configuration.
+ /// Ignored if the stream already exists.
+ #[command(
+ flatten,
+ next_help_heading = "Stream configuration (applied only if the stream is created on read)"
+ )]
+ pub stream_config: StreamConfig,
}
#[derive(Args, Debug)]
diff --git a/cli/src/main.rs b/cli/src/main.rs
index cc809202..97d560f1 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -657,9 +657,13 @@ async fn run(cli: Cli) -> Result {
record_stream,
args.uri,
encryption_key.as_ref(),
- args.fencing_token,
- args.match_seq_num,
- *args.linger,
+ ops::AppendOptions {
+ fencing_token: args.fencing_token,
+ match_seq_num: args.match_seq_num,
+ linger: *args.linger,
+ stream_config: (!args.stream_config.is_empty())
+ .then(|| args.stream_config.into()),
+ },
);
let mut acks = std::pin::pin!(acks);
let mut last_printed_batch_end: Option = None;
diff --git a/cli/src/ops.rs b/cli/src/ops.rs
index 7ae3218d..468d6ab0 100644
--- a/cli/src/ops.rs
+++ b/cli/src/ops.rs
@@ -570,23 +570,32 @@ pub async fn read(
stop = stop.with_until(..until);
}
+ let mut input = ReadInput::new().with_start(start).with_stop(stop);
+ if !args.stream_config.is_empty() {
+ input = input.with_stream_config(args.stream_config.clone().into());
+ }
+
stream
- .read_session(
- ReadInput::new().with_start(start).with_stop(stop),
- ReadSessionConfig::default(),
- )
+ .read_session(input, ReadSessionConfig::default())
.await
.map_err(|e| CliError::op(OpKind::Read, e))
}
+/// Options controlling how records are appended.
+pub struct AppendOptions {
+ pub fencing_token: Option,
+ pub match_seq_num: Option,
+ pub linger: Duration,
+ /// Stream configuration to apply if the stream is created on append.
+ pub stream_config: Option,
+}
+
pub fn append<'a, S, E>(
s2: &'a S2,
records: S,
uri: S2BasinAndStreamUri,
encryption_key: Option<&'a EncryptionKey>,
- fencing_token: Option,
- match_seq_num: Option,
- linger: Duration,
+ options: AppendOptions,
) -> impl Stream- > + Send + 'a
where
S: Stream
- > + Send + Unpin + 'a,
@@ -594,12 +603,15 @@ where
{
let stream = stream_with_encryption(s2, uri, encryption_key);
- let batching_config = BatchingConfig::new().with_linger(linger);
+ let batching_config = BatchingConfig::new().with_linger(options.linger);
let mut producer_config = ProducerConfig::new().with_batching(batching_config);
- if let Some(ft) = fencing_token {
+ if let Some(config) = options.stream_config {
+ producer_config = producer_config.with_stream_config(config);
+ }
+ if let Some(ft) = options.fencing_token {
producer_config = producer_config.with_fencing_token(ft);
}
- if let Some(seq) = match_seq_num {
+ if let Some(seq) = options.match_seq_num {
producer_config = producer_config.with_match_seq_num(seq);
}
diff --git a/cli/tests/integration.rs b/cli/tests/integration.rs
index b193551f..7d0152f0 100644
--- a/cli/tests/integration.rs
+++ b/cli/tests/integration.rs
@@ -563,6 +563,95 @@ fn append_from_stdin() {
cleanup_stream(&basin, &stream);
}
+#[test]
+#[serial]
+fn append_with_stream_config() {
+ let basin = unique_name("test-cli-csoa-cfg");
+ s2().args([
+ "create-basin",
+ &basin,
+ "--retention-policy",
+ "7d",
+ "--create-stream-on-append",
+ ])
+ .assert()
+ .success();
+ wait_for_basin(&basin);
+
+ let stream = unique_name("test-csoa-new");
+ let uri = format!("s2://{basin}/{stream}");
+ s2().args([
+ "append",
+ &uri,
+ "--format",
+ "text",
+ "--input",
+ "-",
+ "--retention-policy",
+ "1h",
+ "--delete-on-empty-min-age",
+ "5m",
+ ])
+ .write_stdin("first record\n")
+ .assert()
+ .success();
+
+ s2().args(["get-stream-config", &uri])
+ .assert()
+ .success()
+ .stdout(
+ predicate::str::contains("1h")
+ .and(predicate::str::contains("5m"))
+ .and(predicate::str::contains("7days").not()),
+ );
+
+ s2().args([
+ "append",
+ &uri,
+ "--format",
+ "text",
+ "--input",
+ "-",
+ "--retention-policy",
+ "2h",
+ ])
+ .write_stdin("second record\n")
+ .assert()
+ .success();
+
+ s2().args(["get-stream-config", &uri])
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("1h").and(predicate::str::contains("2h").not()));
+
+ cleanup_stream(&basin, &stream);
+ cleanup_basin(&basin);
+}
+
+#[test]
+#[serial]
+fn read_with_stream_config() {
+ let basin = unique_name("test-cli-csor-cfg");
+ s2().args(["create-basin", &basin, "--create-stream-on-read"])
+ .assert()
+ .success();
+ wait_for_basin(&basin);
+
+ let stream = unique_name("test-csor-new");
+ let uri = format!("s2://{basin}/{stream}");
+ s2().args(["read", &uri, "--count", "1", "--retention-policy", "1h"])
+ .assert()
+ .failure();
+
+ s2().args(["get-stream-config", &uri])
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("1h"));
+
+ cleanup_stream(&basin, &stream);
+ cleanup_basin(&basin);
+}
+
#[test]
#[serial]
fn tail_stream() {
diff --git a/lite/src/backend/append.rs b/lite/src/backend/append.rs
index 82343edb..a8ebbe78 100644
--- a/lite/src/backend/append.rs
+++ b/lite/src/backend/append.rs
@@ -7,6 +7,7 @@ use std::{
use futures::{Stream, StreamExt as _, future::OptionFuture, stream::FuturesOrdered};
use s2_common::{
basin::BasinName,
+ config::OptionalStreamConfig,
encryption::{EncryptionKey, EncryptionSpec},
record::{SeqNum, StreamPosition},
stream::{AppendAck, AppendInput, StreamName},
@@ -14,20 +15,26 @@ use s2_common::{
use s2_storage::record::encrypt_append_input;
use tokio::sync::oneshot;
-use super::{Backend, StreamHandle};
+use super::{Backend, StreamHandle, core::AutoCreateOn};
use crate::backend::error::{AppendError, AppendErrorInternal, StorageError};
impl Backend {
+ /// Open a stream for an append or append session.
+ ///
+ /// `stream_config` is applied if the stream is created on append. Unset fields inherit the
+ /// basin's default stream configuration. Ignored if the stream already exists.
pub async fn open_for_append(
&self,
basin: &BasinName,
stream: &StreamName,
encryption_key: Option,
+ stream_config: OptionalStreamConfig,
) -> Result {
self.stream_handle_with_auto_create::(
basin,
stream,
- |config| config.create_stream_on_append,
+ AutoCreateOn::Append,
+ stream_config,
|cipher| Ok(EncryptionSpec::resolve(cipher, encryption_key)?),
)
.await
diff --git a/lite/src/backend/core.rs b/lite/src/backend/core.rs
index 0b915877..cf7558ad 100644
--- a/lite/src/backend/core.rs
+++ b/lite/src/backend/core.rs
@@ -331,11 +331,16 @@ impl Backend {
}
}
+ /// Resolve a handle for `stream`, creating it on demand if the basin config allows.
+ ///
+ /// `stream_config` is applied over the basin's default stream configuration only if the
+ /// stream is being created. It must already be validated.
pub(super) async fn stream_handle_with_auto_create(
&self,
basin: &BasinName,
stream: &StreamName,
- should_auto_create: impl FnOnce(&BasinConfig) -> bool,
+ auto_create_on: AutoCreateOn,
+ stream_config: OptionalStreamConfig,
resolve_encryption: impl FnOnce(Option) -> Result,
) -> Result
where
@@ -347,54 +352,66 @@ impl Backend {
+ From
+ From,
{
- match self.streamer_client_guarded(basin, stream).await {
- Ok(client) => Ok(StreamHandle {
- db: self.db.clone(),
- encryption: resolve_encryption(client.cipher())?,
- client,
- }),
+ let client = match self.streamer_client_guarded(basin, stream).await {
+ Ok(client) => client,
Err(StreamerError::StreamNotFound(e)) => {
let config = match self.get_basin_config(basin.clone()).await {
Ok(config) => config,
Err(GetBasinConfigError::Storage(e)) => Err(e)?,
Err(GetBasinConfigError::BasinNotFound(e)) => Err(e)?,
};
- if should_auto_create(&config) {
- if let Err(e) = self
- .provision_stream(
- basin.clone(),
- stream.clone(),
- OptionalStreamConfig::default(),
- ProvisionMode::CreateOnly {
- request_token: None,
- },
- )
- .await
- {
- match e {
- ProvisionStreamError::Storage(e) => Err(e)?,
- ProvisionStreamError::TransactionConflict(e) => Err(e)?,
- ProvisionStreamError::BasinDeletionPending(e) => Err(e)?,
- ProvisionStreamError::StreamDeletionPending(e) => Err(e)?,
- ProvisionStreamError::BasinNotFound(e) => Err(e)?,
- ProvisionStreamError::StreamAlreadyExists(_) => {}
- ProvisionStreamError::Validation(_) => {
- unreachable!("auto-create uses default config")
- }
+ if !auto_create_on.is_enabled(&config) {
+ return Err(e.into());
+ }
+ if let Err(e) = self
+ .provision_stream(
+ basin.clone(),
+ stream.clone(),
+ stream_config,
+ ProvisionMode::CreateOnly {
+ request_token: None,
+ },
+ )
+ .await
+ {
+ match e {
+ ProvisionStreamError::Storage(e) => Err(e)?,
+ ProvisionStreamError::TransactionConflict(e) => Err(e)?,
+ ProvisionStreamError::BasinDeletionPending(e) => Err(e)?,
+ ProvisionStreamError::StreamDeletionPending(e) => Err(e)?,
+ ProvisionStreamError::BasinNotFound(e) => Err(e)?,
+ ProvisionStreamError::StreamAlreadyExists(_) => {}
+ ProvisionStreamError::Validation(e) => {
+ unreachable!("auto-create config is validated at the API boundary: {e}")
}
}
- let client = self.streamer_client_guarded(basin, stream).await?;
- let encryption = resolve_encryption(client.cipher())?;
- Ok(StreamHandle {
- db: self.db.clone(),
- encryption,
- client,
- })
- } else {
- Err(e.into())
}
+ self.streamer_client_guarded(basin, stream).await?
}
- Err(e) => Err(e.into()),
+ Err(e) => return Err(e.into()),
+ };
+ Ok(StreamHandle {
+ db: self.db.clone(),
+ encryption: resolve_encryption(client.cipher())?,
+ client,
+ })
+ }
+}
+
+/// Which basin setting governs creating a missing stream on demand.
+#[derive(Debug, Clone, Copy)]
+pub(super) enum AutoCreateOn {
+ /// `create_stream_on_append`
+ Append,
+ /// `create_stream_on_read`
+ Read,
+}
+
+impl AutoCreateOn {
+ fn is_enabled(self, config: &BasinConfig) -> bool {
+ match self {
+ Self::Append => config.create_stream_on_append,
+ Self::Read => config.create_stream_on_read,
}
}
}
@@ -631,7 +648,9 @@ mod tests {
let basin = basin.clone();
let stream = stream.clone();
tokio::spawn(async move {
- let handle = backend.open_for_append(&basin, &stream, None).await?;
+ let handle = backend
+ .open_for_append(&basin, &stream, None, OptionalStreamConfig::default())
+ .await?;
handle.append(append_input(&format!("r{i}"))).await
})
})
diff --git a/lite/src/backend/read.rs b/lite/src/backend/read.rs
index 7043409f..c4e0df3d 100644
--- a/lite/src/backend/read.rs
+++ b/lite/src/backend/read.rs
@@ -4,6 +4,7 @@ use futures::{Stream, StreamExt as _};
use s2_common::{
basin::BasinName,
caps,
+ config::OptionalStreamConfig,
encryption::{EncryptionKey, EncryptionSpec},
read_extent::{EvaluatedReadLimit, ReadLimit, ReadUntil},
record::{Metered, MeteredSize as _, SeqNum, StreamPosition, Timestamp},
@@ -15,7 +16,7 @@ use s2_storage::record::{
use slatedb::config::{DurabilityLevel, ScanOptions};
use tokio::{sync::broadcast, time::Instant};
-use super::{Backend, StreamHandle};
+use super::{Backend, StreamHandle, core::AutoCreateOn};
use crate::{
backend::{
error::{
@@ -28,6 +29,7 @@ use crate::{
};
impl Backend {
+ /// Open a stream for a check tail.
pub async fn open_for_check_tail(
&self,
basin: &BasinName,
@@ -36,22 +38,29 @@ impl Backend {
self.stream_handle_with_auto_create::(
basin,
stream,
- |config| config.create_stream_on_read,
+ AutoCreateOn::Read,
+ OptionalStreamConfig::default(),
|_| Ok(EncryptionSpec::Plain),
)
.await
}
+ /// Open a stream for a read or read session.
+ ///
+ /// `stream_config` is applied if the stream is created on read. Unset fields inherit the
+ /// basin's default stream configuration. Ignored if the stream already exists.
pub async fn open_for_read(
&self,
basin: &BasinName,
stream: &StreamName,
encryption_key: Option,
+ stream_config: OptionalStreamConfig,
) -> Result {
self.stream_handle_with_auto_create::(
basin,
stream,
- |config| config.create_stream_on_read,
+ AutoCreateOn::Read,
+ stream_config,
|cipher| Ok(EncryptionSpec::resolve(cipher, encryption_key)?),
)
.await
@@ -562,7 +571,7 @@ mod tests {
let input = append_input(Record::try_from_parts(vec![], bytes::Bytes::from("x")).unwrap());
let ack = backend
- .open_for_append(&basin, &stream, None)
+ .open_for_append(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.append(input)
@@ -585,7 +594,7 @@ mod tests {
wait: None,
};
let session = backend
- .open_for_read(&basin, &stream, None)
+ .open_for_read(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.read(start, end)
@@ -642,7 +651,7 @@ mod tests {
};
let session = backend
- .open_for_read(&basin, &stream, None)
+ .open_for_read(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.read(start, end)
@@ -716,7 +725,7 @@ mod tests {
let initial_input =
append_input(Record::try_from_parts(vec![], bytes::Bytes::from("initial")).unwrap());
backend
- .open_for_append(&basin, &stream, None)
+ .open_for_append(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.append(initial_input)
@@ -736,7 +745,7 @@ mod tests {
};
let session = backend
- .open_for_read(&basin, &stream, None)
+ .open_for_read(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.read(start, end)
@@ -776,7 +785,7 @@ mod tests {
let follow_input =
append_input(Record::try_from_parts(vec![], bytes::Bytes::from("follow-1")).unwrap());
backend
- .open_for_append(&basin, &stream, None)
+ .open_for_append(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.append(follow_input)
@@ -881,7 +890,7 @@ mod tests {
wait: Some(wait),
};
let session = backend
- .open_for_read(&basin, &stream, None)
+ .open_for_read(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.read(start, end)
@@ -906,7 +915,7 @@ mod tests {
Record::try_from_parts(vec![], bytes::Bytes::from(format!("lagged-{i}"))).unwrap(),
);
let ack = backend
- .open_for_append(&basin, &stream, None)
+ .open_for_append(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.append(input)
@@ -960,7 +969,7 @@ mod tests {
let initial_input =
append_input(Record::try_from_parts(vec![], bytes::Bytes::from("initial")).unwrap());
backend
- .open_for_append(&basin, &stream, None)
+ .open_for_append(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.append(initial_input)
@@ -977,7 +986,7 @@ mod tests {
wait: None,
};
let session = backend
- .open_for_read(&basin, &stream, None)
+ .open_for_read(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.read(start, end)
@@ -1007,7 +1016,7 @@ mod tests {
let follow_input =
append_input(Record::try_from_parts(vec![], bytes::Bytes::from("follow-1")).unwrap());
backend
- .open_for_append(&basin, &stream, None)
+ .open_for_append(&basin, &stream, None, OptionalStreamConfig::default())
.await
.unwrap()
.append(follow_input)
diff --git a/lite/src/handlers/v1/records.rs b/lite/src/handlers/v1/records.rs
index 5532e20f..6221be80 100644
--- a/lite/src/handlers/v1/records.rs
+++ b/lite/src/handlers/v1/records.rs
@@ -161,6 +161,7 @@ pub struct ReadArgs {
v1t::StreamNamePathSegment,
s2_api::data::S2FormatHeader,
s2_api::data::S2EncryptionKeyHeader,
+ s2_api::data::S2StreamConfigHeader,
v1t::stream::ReadStart,
v1t::stream::ReadEnd,
),
@@ -186,12 +187,13 @@ pub async fn read(
match request {
v1t::stream::ReadRequest::Unary {
encryption_key,
+ create_stream_config_patch,
format,
response_mime,
} => {
let (start, end) = prepare_read(start, end, ReadMode::Unary)?;
let session = backend
- .open_for_read(&basin, &stream, encryption_key)
+ .open_for_read(&basin, &stream, encryption_key, create_stream_config_patch)
.await?
.read(start, end)
.await?;
@@ -209,13 +211,14 @@ pub async fn read(
}
v1t::stream::ReadRequest::EventStream {
encryption_key,
+ create_stream_config_patch,
format,
last_event_id,
} => {
let (start, end) = apply_last_event_id(start, end, last_event_id);
let (start, end) = prepare_read(start, end, ReadMode::Streaming)?;
let session = backend
- .open_for_read(&basin, &stream, encryption_key)
+ .open_for_read(&basin, &stream, encryption_key, create_stream_config_patch)
.await?
.read(start, end)
.await?;
@@ -265,11 +268,12 @@ pub async fn read(
}
v1t::stream::ReadRequest::S2s {
encryption_key,
+ create_stream_config_patch,
response_compression,
} => {
let (start, end) = prepare_read(start, end, ReadMode::Streaming)?;
let s2s_stream = backend
- .open_for_read(&basin, &stream, encryption_key)
+ .open_for_read(&basin, &stream, encryption_key, create_stream_config_patch)
.await?
.read(start, end)
.await?
@@ -364,6 +368,7 @@ pub struct AppendArgs {
v1t::StreamNamePathSegment,
s2_api::data::S2FormatHeader,
s2_api::data::S2EncryptionKeyHeader,
+ s2_api::data::S2StreamConfigHeader,
),
servers(
(url = super::paths::cloud_endpoints::BASIN, variables(
@@ -384,11 +389,12 @@ pub async fn append(
match request {
v1t::stream::AppendRequest::Unary {
encryption_key,
+ create_stream_config_patch,
input,
response_mime,
} => {
let handle = backend
- .open_for_append(&basin, &stream, encryption_key)
+ .open_for_append(&basin, &stream, encryption_key, create_stream_config_patch)
.await?;
let ack = handle.append(input).await?;
match response_mime {
@@ -404,11 +410,12 @@ pub async fn append(
}
v1t::stream::AppendRequest::S2s {
encryption_key,
+ create_stream_config_patch,
inputs,
response_compression,
} => {
let handle = backend
- .open_for_append(&basin, &stream, encryption_key)
+ .open_for_append(&basin, &stream, encryption_key, create_stream_config_patch)
.await?;
let (err_tx, err_rx) = tokio::sync::oneshot::channel();
@@ -469,13 +476,19 @@ mod tests {
use bytesize::ByteSize;
use futures::TryStreamExt as _;
use prost::Message as _;
- use s2_api::v1::stream::{
- proto,
- s2s::{FrameDecoder, SessionMessage},
+ use s2_api::v1::{
+ config::STREAM_CONFIG_HEADER,
+ stream::{
+ proto,
+ s2s::{self, FrameDecoder, SessionMessage},
+ },
};
use s2_common::{
basin::{BASIN_HEADER, BasinName},
- config::{BasinConfig, OptionalStreamConfig},
+ config::{
+ BasinConfig, DeleteOnEmptyConfig, OptionalStreamConfig, RetentionPolicy, StorageClass,
+ StreamConfig,
+ },
encryption::{EncryptionAlgorithm, EncryptionKey, S2_ENCRYPTION_KEY_HEADER},
read_extent::{ReadLimit, ReadUntil},
record::{EnvelopeRecord, Metered, Record},
@@ -597,7 +610,12 @@ mod tests {
encryption_key: EncryptionKey,
) {
backend
- .open_for_append(basin, stream, Some(encryption_key))
+ .open_for_append(
+ basin,
+ stream,
+ Some(encryption_key),
+ OptionalStreamConfig::default(),
+ )
.await
.expect("open append handle")
.append(append_input(body))
@@ -697,7 +715,12 @@ mod tests {
assert_eq!(ack.end.as_ref().map(|pos| pos.seq_num), Some(1));
let records = backend
- .open_for_read(&basin, &stream, Some(encryption_key.clone()))
+ .open_for_read(
+ &basin,
+ &stream,
+ Some(encryption_key.clone()),
+ OptionalStreamConfig::default(),
+ )
.await
.expect("open read handle")
.read(
@@ -731,6 +754,191 @@ mod tests {
assert_eq!(record.body().as_ref(), b"secret");
}
+ fn basin_config_with_create_stream_on_append() -> BasinConfig {
+ BasinConfig {
+ create_stream_on_append: true,
+ default_stream_config: OptionalStreamConfig {
+ storage_class: Some(StorageClass::Standard),
+ ..Default::default()
+ },
+ ..Default::default()
+ }
+ }
+
+ fn expected_auto_created_config() -> StreamConfig {
+ StreamConfig {
+ storage_class: StorageClass::Standard,
+ retention_policy: RetentionPolicy::Age(Duration::from_secs(3600)),
+ timestamping: Default::default(),
+ delete_on_empty: DeleteOnEmptyConfig {
+ min_age: Duration::from_secs(300),
+ },
+ }
+ }
+
+ const STREAM_CONFIG_HEADER_VALUE: &str =
+ r#"{"retention_policy":{"age":3600},"delete_on_empty":{"min_age_secs":300}}"#;
+
+ fn append_record_input(body: &'static [u8]) -> proto::AppendInput {
+ proto::AppendInput {
+ records: vec![proto::AppendRecord {
+ timestamp: None,
+ headers: vec![],
+ body: Bytes::from_static(body),
+ }],
+ match_seq_num: None,
+ fencing_token: None,
+ }
+ }
+
+ #[tokio::test]
+ async fn json_append_auto_creates_stream_with_stream_config_header() {
+ let (app, backend, basin, stream) = setup_app_without_stream(
+ "append-json-create-config",
+ basin_config_with_create_stream_on_append(),
+ )
+ .await;
+
+ let body = serde_json::json!({"records": [{"body": "hello"}]});
+ let response = send(
+ &app,
+ request_builder("POST", format!("/v1/streams/{stream}/records"), &basin)
+ .header(header::CONTENT_TYPE, "application/json")
+ .header(STREAM_CONFIG_HEADER.as_str(), STREAM_CONFIG_HEADER_VALUE)
+ .body(Body::from(body.to_string()))
+ .unwrap(),
+ )
+ .await;
+
+ assert_eq!(response.status(), StatusCode::OK);
+ let ack = response_json(response, "append ack body").await;
+ assert_eq!(ack["end"]["seq_num"], 1);
+ let config = backend
+ .get_stream_config(basin, stream)
+ .await
+ .expect("get stream config");
+ assert_eq!(config, expected_auto_created_config());
+ }
+
+ #[tokio::test]
+ async fn append_with_invalid_stream_config_header_is_rejected_without_creating() {
+ let (app, backend, basin, stream) = setup_app_without_stream(
+ "append-create-config-invalid",
+ basin_config_with_create_stream_on_append(),
+ )
+ .await;
+
+ for (value, expected_message) in [
+ (
+ r#"{"retention_policy":{"age":0}}"#,
+ "age must be greater than 0 seconds",
+ ),
+ ("not json", "invalid JSON"),
+ ] {
+ let body = serde_json::json!({"records": [{"body": "hello"}]});
+ let response = send(
+ &app,
+ request_builder("POST", format!("/v1/streams/{stream}/records"), &basin)
+ .header(header::CONTENT_TYPE, "application/json")
+ .header(STREAM_CONFIG_HEADER.as_str(), value)
+ .body(Body::from(body.to_string()))
+ .unwrap(),
+ )
+ .await;
+
+ assert_eq!(response.status(), StatusCode::BAD_REQUEST);
+ let info = response_json(response, "append error body").await;
+ assert_eq!(info["code"], "bad_header");
+ let message = info["message"].as_str().expect("error message string");
+ assert!(
+ message.contains("s2-stream-config") && message.contains(expected_message),
+ "{message}"
+ );
+ }
+ assert_no_streams(&backend, &basin).await;
+ }
+
+ #[tokio::test]
+ async fn s2s_append_session_auto_creates_stream_with_stream_config_header() {
+ let (app, backend, basin, stream) = setup_app_without_stream(
+ "append-s2s-create-config",
+ basin_config_with_create_stream_on_append(),
+ )
+ .await;
+
+ let frame = |body: &'static [u8]| {
+ SessionMessage::regular(s2s::CompressionAlgorithm::None, &append_record_input(body))
+ .expect("encode frame")
+ .encode()
+ };
+ let mut body = BytesMut::new();
+ body.extend_from_slice(&frame(b"first"));
+ body.extend_from_slice(&frame(b"second"));
+
+ let response = send(
+ &app,
+ request_builder("POST", format!("/v1/streams/{stream}/records"), &basin)
+ .header(header::CONTENT_TYPE, "s2s/proto")
+ .header(STREAM_CONFIG_HEADER.as_str(), STREAM_CONFIG_HEADER_VALUE)
+ .body(Body::from(body.freeze()))
+ .unwrap(),
+ )
+ .await;
+
+ assert_eq!(response.status(), StatusCode::OK);
+ let body = response_bytes(response, "s2s body").await;
+ let mut decoder = FrameDecoder;
+ let mut buf = BytesMut::from(body.as_ref());
+ let mut acks = Vec::new();
+ while let Some(frame) = decoder.decode(&mut buf).expect("frame decode") {
+ let SessionMessage::Regular(ack) = frame else {
+ panic!("expected regular frame");
+ };
+ acks.push(
+ ack.try_into_proto::()
+ .expect("decode append ack"),
+ );
+ }
+ assert_eq!(acks.len(), 2);
+ assert_eq!(acks[1].end.as_ref().map(|pos| pos.seq_num), Some(2));
+
+ let config = backend
+ .get_stream_config(basin, stream)
+ .await
+ .expect("get stream config");
+ assert_eq!(config, expected_auto_created_config());
+ }
+
+ #[tokio::test]
+ async fn read_auto_creates_stream_with_stream_config_header() {
+ let basin_config = BasinConfig {
+ create_stream_on_append: false,
+ create_stream_on_read: true,
+ ..basin_config_with_create_stream_on_append()
+ };
+ let (app, backend, basin, stream) =
+ setup_app_without_stream("read-create-config", basin_config).await;
+
+ let response = send(
+ &app,
+ request_builder(
+ "GET",
+ format!("/v1/streams/{stream}/records?seq_num=0"),
+ &basin,
+ )
+ .header(STREAM_CONFIG_HEADER.as_str(), STREAM_CONFIG_HEADER_VALUE)
+ .body(Body::empty())
+ .unwrap(),
+ )
+ .await;
+ assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
+ let config = backend
+ .get_stream_config(basin, stream)
+ .await
+ .expect("get stream config");
+ assert_eq!(config, expected_auto_created_config());
+ }
+
#[tokio::test]
async fn invalid_read_bounds_do_not_auto_create_stream() {
let basin_config = BasinConfig {
diff --git a/lite/tests/backend/common/mod.rs b/lite/tests/backend/common/mod.rs
index 019728e9..a4bea287 100644
--- a/lite/tests/backend/common/mod.rs
+++ b/lite/tests/backend/common/mod.rs
@@ -3,6 +3,7 @@ use std::pin::Pin;
use futures::Stream;
use s2_common::{
basin::BasinName,
+ config::OptionalStreamConfig,
encryption::EncryptionSpec,
record::StreamPosition,
stream::{AppendAck, AppendInput, StreamName},
@@ -30,6 +31,7 @@ pub async fn append(
&basin,
&stream,
encryption.and_then(encryption_key_for_spec),
+ OptionalStreamConfig::default(),
)
.await?
.append(input)
@@ -51,6 +53,7 @@ where
&basin,
&stream,
encryption.and_then(encryption_key_for_spec),
+ OptionalStreamConfig::default(),
)
.await?
.append_session(inputs);
diff --git a/lite/tests/backend/common/read.rs b/lite/tests/backend/common/read.rs
index 28cb6b33..6e3405ba 100644
--- a/lite/tests/backend/common/read.rs
+++ b/lite/tests/backend/common/read.rs
@@ -3,6 +3,7 @@ use std::{pin::Pin, task::Poll, time::Duration};
use futures::StreamExt;
use s2_common::{
basin::BasinName,
+ config::OptionalStreamConfig,
encryption::EncryptionSpec,
read_extent::{ReadLimit, ReadUntil},
record::{Record, SequencedRecord},
@@ -83,7 +84,12 @@ pub async fn try_open_read_session_with_encryption(
ReadError,
> {
let read_session = backend
- .open_for_read(basin, stream, encryption_key_for_spec(encryption))
+ .open_for_read(
+ basin,
+ stream,
+ encryption_key_for_spec(encryption),
+ OptionalStreamConfig::default(),
+ )
.await?
.read(start, end)
.await?;
diff --git a/lite/tests/backend/common/setup.rs b/lite/tests/backend/common/setup.rs
index 324ea3b6..d041c29b 100644
--- a/lite/tests/backend/common/setup.rs
+++ b/lite/tests/backend/common/setup.rs
@@ -237,7 +237,12 @@ pub async fn append_payloads_with_encryption(
fencing_token: None,
};
backend
- .open_for_append(basin, stream, encryption_key_for_spec(encryption))
+ .open_for_append(
+ basin,
+ stream,
+ encryption_key_for_spec(encryption),
+ OptionalStreamConfig::default(),
+ )
.await
.expect("Failed to open append handle")
.append(input)
@@ -257,7 +262,7 @@ pub async fn append_timestamped_payloads(
fencing_token: None,
};
backend
- .open_for_append(basin, stream, None)
+ .open_for_append(basin, stream, None, OptionalStreamConfig::default())
.await
.expect("Failed to open append handle")
.append(input)
diff --git a/lite/tests/backend/data_plane/auto_create.rs b/lite/tests/backend/data_plane/auto_create.rs
index 24bdfc00..42ae4d1f 100644
--- a/lite/tests/backend/data_plane/auto_create.rs
+++ b/lite/tests/backend/data_plane/auto_create.rs
@@ -3,7 +3,10 @@ use std::time::Duration;
use bytes::Bytes;
use s2_common::{
basin::BasinName,
- config::BasinConfig,
+ config::{
+ BasinConfig, DeleteOnEmptyConfig, OptionalDeleteOnEmptyConfig, OptionalStreamConfig,
+ RetentionPolicy, StorageClass, StreamConfig,
+ },
encryption::EncryptionAlgorithm,
read_extent::{ReadLimit, ReadUntil},
record::StreamPosition,
@@ -123,6 +126,140 @@ async fn test_backend_append_auto_creates_stream_with_basin_cipher() {
assert_eq!(envelope_bodies(&records), vec![b"secret".to_vec()]);
}
+fn basin_config_with_defaults() -> BasinConfig {
+ BasinConfig {
+ create_stream_on_append: true,
+ default_stream_config: OptionalStreamConfig {
+ storage_class: Some(StorageClass::Standard),
+ retention_policy: Some(RetentionPolicy::Age(Duration::from_secs(7 * 24 * 60 * 60))),
+ ..Default::default()
+ },
+ ..Default::default()
+ }
+}
+
+fn requested_stream_config() -> OptionalStreamConfig {
+ OptionalStreamConfig {
+ retention_policy: Some(RetentionPolicy::Age(Duration::from_secs(3600))),
+ delete_on_empty: OptionalDeleteOnEmptyConfig {
+ min_age: Some(Duration::from_secs(300)),
+ },
+ ..Default::default()
+ }
+}
+
+fn expected_merged_stream_config() -> StreamConfig {
+ StreamConfig {
+ storage_class: StorageClass::Standard,
+ retention_policy: RetentionPolicy::Age(Duration::from_secs(3600)),
+ timestamping: Default::default(),
+ delete_on_empty: DeleteOnEmptyConfig {
+ min_age: Duration::from_secs(300),
+ },
+ }
+}
+
+#[tokio::test]
+async fn test_backend_append_auto_create_applies_stream_config() {
+ let backend = create_backend().await;
+ let basin_name = create_test_basin(
+ &backend,
+ "backend-auto-create-config",
+ basin_config_with_defaults(),
+ )
+ .await;
+ let stream_name = test_stream_name("missing");
+
+ let input = AppendInput {
+ records: create_test_record_batch(vec![Bytes::from_static(b"hello")]),
+ match_seq_num: None,
+ fencing_token: None,
+ };
+ let ack = backend
+ .open_for_append(&basin_name, &stream_name, None, requested_stream_config())
+ .await
+ .expect("Failed to open append handle")
+ .append(input)
+ .await
+ .expect("Failed to append to auto-created stream");
+ assert_eq!(ack.end.seq_num, 1);
+
+ let config = backend
+ .get_stream_config(basin_name.clone(), stream_name.clone())
+ .await
+ .expect("Failed to get stream config");
+ assert_eq!(config, expected_merged_stream_config());
+}
+
+#[tokio::test]
+async fn test_backend_append_ignores_stream_config_for_existing_stream() {
+ let backend = create_backend().await;
+ let basin_name = create_test_basin(
+ &backend,
+ "backend-auto-create-config-existing",
+ basin_config_with_defaults(),
+ )
+ .await;
+ let stream_name = create_test_stream(
+ &backend,
+ &basin_name,
+ "existing",
+ OptionalStreamConfig::default(),
+ )
+ .await;
+ let before = backend
+ .get_stream_config(basin_name.clone(), stream_name.clone())
+ .await
+ .expect("Failed to get stream config");
+
+ let input = AppendInput {
+ records: create_test_record_batch(vec![Bytes::from_static(b"hello")]),
+ match_seq_num: None,
+ fencing_token: None,
+ };
+ backend
+ .open_for_append(&basin_name, &stream_name, None, requested_stream_config())
+ .await
+ .expect("Failed to open append handle")
+ .append(input)
+ .await
+ .expect("Failed to append to existing stream");
+
+ let after = backend
+ .get_stream_config(basin_name.clone(), stream_name.clone())
+ .await
+ .expect("Failed to get stream config");
+ assert_eq!(after, before);
+ assert_ne!(after, expected_merged_stream_config());
+}
+
+#[tokio::test]
+async fn test_backend_read_auto_create_applies_stream_config() {
+ let backend = create_backend().await;
+ let basin_name = create_test_basin(
+ &backend,
+ "backend-auto-create-read-config",
+ BasinConfig {
+ create_stream_on_append: false,
+ create_stream_on_read: true,
+ ..basin_config_with_defaults()
+ },
+ )
+ .await;
+ let stream_name = test_stream_name("missing");
+
+ backend
+ .open_for_read(&basin_name, &stream_name, None, requested_stream_config())
+ .await
+ .expect("Failed to open read handle on auto-created stream");
+
+ let config = backend
+ .get_stream_config(basin_name.clone(), stream_name.clone())
+ .await
+ .expect("Failed to get stream config");
+ assert_eq!(config, expected_merged_stream_config());
+}
+
#[tokio::test]
async fn test_backend_append_without_auto_create_returns_not_found() {
let backend = create_backend().await;
diff --git a/sdk/src/api.rs b/sdk/src/api.rs
index f412eaec..f67f4b99 100644
--- a/sdk/src/api.rs
+++ b/sdk/src/api.rs
@@ -18,7 +18,10 @@ use s2_api::v1::{
basin::{
BasinInfo, CreateBasinRequest, EnsureBasinRequest, ListBasinsRequest, ListBasinsResponse,
},
- config::{BasinConfig, BasinReconfiguration, StreamConfig, StreamReconfiguration},
+ config::{
+ BasinConfig, BasinReconfiguration, STREAM_CONFIG_HEADER, StreamConfig,
+ StreamReconfiguration,
+ },
location::LocationInfo,
metrics::{
AccountMetricSetRequest, BasinMetricSetRequest, MetricSetResponse, StreamMetricSetRequest,
@@ -384,6 +387,7 @@ impl BasinClient {
name: &StreamName,
input: AppendInput,
encryption: Option<&EncryptionKey>,
+ stream_config: Option<&StreamConfig>,
append_retry_policy: AppendRetryPolicy,
) -> Result {
let url = self.uri(format!("v1/streams/{}/records", urlencoding::encode(name)));
@@ -394,6 +398,7 @@ impl BasinClient {
.body(input.encode_to_vec())
.build()?;
set_encryption_header(&mut request, encryption);
+ set_stream_config_header(&mut request, stream_config);
let response = self
.request(request)
.with_append_retry_policy(append_retry_policy)
@@ -420,6 +425,7 @@ impl BasinClient {
start: ReadStart,
end: ReadEnd,
encryption: Option<&EncryptionKey>,
+ stream_config: Option<&StreamConfig>,
) -> Result {
let url = self.uri(format!("v1/streams/{}/records", urlencoding::encode(name)));
let mut builder = self
@@ -433,6 +439,7 @@ impl BasinClient {
}
let mut request = builder.build()?;
set_encryption_header(&mut request, encryption);
+ set_stream_config_header(&mut request, stream_config);
let response = self
.request(request)
.error_handler(read_response_error_handler)
@@ -446,6 +453,7 @@ impl BasinClient {
name: &StreamName,
inputs: I,
encryption: Option<&EncryptionKey>,
+ stream_config: Option<&StreamConfig>,
frame_signal: Option,
reconnect: ReconnectAdvice,
) -> Result, ApiError>
@@ -476,6 +484,7 @@ impl BasinClient {
add_basin_header_if_required(request_builder, &self.config.endpoints, &self.name);
let mut request = request_builder.build()?;
set_encryption_header(&mut request, encryption);
+ set_stream_config_header(&mut request, stream_config);
let (response, access_token) = self.client.init_streaming_authorized(request).await?;
let response = match response.into_result().await {
Ok(response) => response,
@@ -538,6 +547,7 @@ impl BasinClient {
start: ReadStart,
end: ReadEnd,
encryption: Option<&EncryptionKey>,
+ stream_config: Option<&StreamConfig>,
reconnect: ReconnectAdvice,
) -> Result, ApiError> {
let url = self.uri(format!("v1/streams/{}/records", urlencoding::encode(name)));
@@ -553,6 +563,7 @@ impl BasinClient {
add_basin_header_if_required(request_builder, &self.config.endpoints, &self.name);
let mut request = request_builder.build()?;
set_encryption_header(&mut request, encryption);
+ set_stream_config_header(&mut request, stream_config);
let (response, access_token) = self.client.init_streaming_authorized(request).await?;
let response = match response.into_result().await {
Ok(response) => response,
@@ -968,6 +979,14 @@ fn set_encryption_header(request: &mut client::Request, encryption: Option<&Encr
}
}
+fn set_stream_config_header(request: &mut client::Request, stream_config: Option<&StreamConfig>) {
+ if let Some(config) = stream_config {
+ request
+ .headers_mut()
+ .insert(STREAM_CONFIG_HEADER.clone(), config.to_header_value());
+ }
+}
+
pub fn retry_builder(config: &RetryConfig) -> RetryBackoffBuilder {
RetryBackoffBuilder::default()
.with_min_base_delay(config.min_base_delay)
@@ -1396,6 +1415,7 @@ mod tests {
wait: None
},
None,
+ None,
ReconnectAdvice::default(),
)
.await
@@ -1408,6 +1428,7 @@ mod tests {
futures_util::stream::empty(),
None,
None,
+ None,
ReconnectAdvice::default(),
)
.await
diff --git a/sdk/src/batching.rs b/sdk/src/batching.rs
index 00086f1f..eac2c5ab 100644
--- a/sdk/src/batching.rs
+++ b/sdk/src/batching.rs
@@ -174,6 +174,7 @@ impl Stream for AppendInputs {
records: batch,
match_seq_num,
fencing_token: self.fencing_token.clone(),
+ stream_config: None,
})))
}
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
diff --git a/sdk/src/ops.rs b/sdk/src/ops.rs
index be287bec..db339709 100644
--- a/sdk/src/ops.rs
+++ b/sdk/src/ops.rs
@@ -4,7 +4,9 @@ use crate::{
api::{AccountClient, BaseClient, BasinClient},
error::{AppendError, ReadError, RequestError},
producer::{Producer, ProducerConfig},
- session::{self, AppendSession, AppendSessionConfig, ReadSession, ReadSessionError},
+ session::{
+ self, AppendSession, AppendSessionConfig, ReadSession, ReadSessionError, StreamHeaders,
+ },
types::{
AccessTokenId, AccessTokenInfo, AppendAck, AppendInput, BasinConfig, BasinInfo, BasinName,
CreateBasinInput, CreateStreamInput, DeleteBasinInput, DeleteStreamInput, EncryptionKey,
@@ -437,6 +439,13 @@ impl S2Stream {
}
}
+ fn headers(&self, stream_config: Option<&StreamConfig>) -> StreamHeaders {
+ StreamHeaders {
+ encryption: self.encryption.clone(),
+ stream_config: stream_config.cloned().map(Into::into),
+ }
+ }
+
/// Check tail position.
pub async fn check_tail(&self) -> Result {
let response = self.client.check_tail(&self.name).await?;
@@ -444,13 +453,18 @@ impl S2Stream {
}
/// Append records.
- pub async fn append(&self, input: AppendInput) -> Result {
+ pub async fn append(&self, mut input: AppendInput) -> Result {
+ let stream_config = input
+ .stream_config
+ .take()
+ .map(s2_api::v1::config::StreamConfig::from);
let ack = self
.client
.append(
&self.name,
input.into(),
self.encryption.as_ref(),
+ stream_config.as_ref(),
self.client.config.retry.append_retry_policy,
)
.await?;
@@ -459,6 +473,9 @@ impl S2Stream {
/// Read records.
pub async fn read(&self, input: ReadInput) -> Result {
+ let stream_config = input
+ .stream_config
+ .map(s2_api::v1::config::StreamConfig::from);
let batch = self
.client
.read(
@@ -466,6 +483,7 @@ impl S2Stream {
input.start.into(),
input.stop.into(),
self.encryption.as_ref(),
+ stream_config.as_ref(),
)
.await?;
let mut batch = ReadBatch::from_api(batch);
@@ -480,7 +498,7 @@ impl S2Stream {
AppendSession::new(
self.client.clone(),
self.name.clone(),
- self.encryption.clone(),
+ self.headers(config.stream_config()),
config,
)
}
@@ -490,7 +508,7 @@ impl S2Stream {
Producer::new(
self.client.clone(),
self.name.clone(),
- self.encryption.clone(),
+ self.headers(config.stream_config()),
config,
)
}
@@ -504,7 +522,7 @@ impl S2Stream {
session::read_session(
self.client.clone(),
self.name.clone(),
- self.encryption.clone(),
+ self.headers(input.stream_config.as_ref()),
input,
config,
)
diff --git a/sdk/src/producer.rs b/sdk/src/producer.rs
index 632c31b5..293f774a 100644
--- a/sdk/src/producer.rs
+++ b/sdk/src/producer.rs
@@ -20,9 +20,11 @@ use crate::{
api::BasinClient,
batching::{AppendInputs, AppendRecordBatches, BatchingConfig},
error::ProducerError,
- session::{AppendPermit, AppendPermits, AppendSessionInternal, BatchSubmitTicket},
+ session::{
+ AppendPermit, AppendPermits, AppendSessionInternal, BatchSubmitTicket, StreamHeaders,
+ },
types::{
- AppendAck, AppendRecord, EncryptionKey, FencingToken, MeteredBytes, ONE_MIB, StreamName,
+ AppendAck, AppendRecord, FencingToken, MeteredBytes, ONE_MIB, StreamConfig, StreamName,
ValidationError,
},
};
@@ -75,6 +77,7 @@ pub struct ProducerConfig {
batching: BatchingConfig,
fencing_token: Option,
match_seq_num: Option,
+ stream_config: Option,
}
impl Default for ProducerConfig {
@@ -84,6 +87,7 @@ impl Default for ProducerConfig {
batching: BatchingConfig::default(),
fencing_token: None,
match_seq_num: None,
+ stream_config: None,
}
}
}
@@ -137,6 +141,23 @@ impl ProducerConfig {
..self
}
}
+
+ /// Set the stream configuration to apply if the stream is created on append.
+ ///
+ /// Unset fields inherit the basin's default stream configuration. Ignored if the stream
+ /// already exists.
+ ///
+ /// Defaults to `None`.
+ pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
+ Self {
+ stream_config: Some(stream_config),
+ ..self
+ }
+ }
+
+ pub(crate) fn stream_config(&self) -> Option<&StreamConfig> {
+ self.stream_config.as_ref()
+ }
}
/// High-level interface for submitting individual [`AppendRecord`]s.
@@ -154,12 +175,12 @@ impl Producer {
pub(crate) fn new(
client: BasinClient,
stream: StreamName,
- encryption: Option,
+ headers: StreamHeaders,
config: ProducerConfig,
) -> Self {
let (cmd_tx, cmd_rx) = mpsc::channel::(RECORD_BATCH_MAX.count);
let permits = AppendPermits::new(None, config.max_unacked_bytes);
- let session = AppendSessionInternal::new(client, stream, encryption);
+ let session = AppendSessionInternal::new(client, stream, headers);
let terminal_err = Arc::new(OnceLock::new());
let _handle = AbortOnDropHandle::new(tokio::spawn(Self::run(
session,
diff --git a/sdk/src/session/append.rs b/sdk/src/session/append.rs
index ea1f90fa..74f53641 100644
--- a/sdk/src/session/append.rs
+++ b/sdk/src/session/append.rs
@@ -24,9 +24,10 @@ use crate::{
frame_signal::FrameSignal,
reconnect::{AdvisedReconnects, ReconnectAdvice},
retry::RetryBackoffBuilder,
+ session::StreamHeaders,
types::{
- AccessTokenMode, AppendAck, AppendInput, AppendRetryPolicy, EncryptionKey, MeteredBytes,
- ONE_MIB, StreamName, StreamPosition, ValidationError,
+ AccessTokenMode, AppendAck, AppendInput, AppendRetryPolicy, MeteredBytes, ONE_MIB,
+ StreamConfig, StreamName, StreamPosition, ValidationError,
},
};
@@ -154,6 +155,7 @@ impl Future for BatchSubmitTicket {
pub struct AppendSessionConfig {
max_unacked_bytes: u32,
max_unacked_batches: Option,
+ stream_config: Option,
}
impl Default for AppendSessionConfig {
@@ -161,6 +163,7 @@ impl Default for AppendSessionConfig {
Self {
max_unacked_bytes: 5 * ONE_MIB,
max_unacked_batches: None,
+ stream_config: None,
}
}
}
@@ -195,6 +198,23 @@ impl AppendSessionConfig {
..self
}
}
+
+ /// Set the stream configuration to apply if the stream is created on append.
+ ///
+ /// Unset fields inherit the basin's default stream configuration. Ignored if the stream
+ /// already exists.
+ ///
+ /// Defaults to `None`.
+ pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
+ Self {
+ stream_config: Some(stream_config),
+ ..self
+ }
+ }
+
+ pub(crate) fn stream_config(&self) -> Option<&StreamConfig> {
+ self.stream_config.as_ref()
+ }
}
struct SessionState {
@@ -231,7 +251,7 @@ impl AppendSession {
pub(crate) fn new(
client: BasinClient,
stream: StreamName,
- encryption: Option,
+ headers: StreamHeaders,
config: AppendSessionConfig,
) -> Self {
let buffer_size = config
@@ -245,7 +265,7 @@ impl AppendSession {
let handle = AbortOnDropHandle::new(tokio::spawn(run_session_with_retry(
client,
stream,
- encryption,
+ headers,
cmd_rx,
retry_builder,
buffer_size,
@@ -356,11 +376,7 @@ pub(crate) struct AppendSessionInternal {
}
impl AppendSessionInternal {
- pub(crate) fn new(
- client: BasinClient,
- stream: StreamName,
- encryption: Option,
- ) -> Self {
+ pub(crate) fn new(client: BasinClient, stream: StreamName, headers: StreamHeaders) -> Self {
let buffer_size = DEFAULT_CHANNEL_BUFFER_SIZE;
let (cmd_tx, cmd_rx) = mpsc::channel(buffer_size);
let retry_builder = retry_builder(&client.config.retry);
@@ -368,7 +384,7 @@ impl AppendSessionInternal {
let handle = AbortOnDropHandle::new(tokio::spawn(run_session_with_retry(
client,
stream,
- encryption,
+ headers,
cmd_rx,
retry_builder,
buffer_size,
@@ -473,7 +489,7 @@ impl AppendPermits {
async fn run_session_with_retry(
client: BasinClient,
stream: StreamName,
- encryption: Option,
+ headers: StreamHeaders,
cmd_rx: mpsc::Receiver,
retry_builder: RetryBackoffBuilder,
buffer_size: usize,
@@ -503,7 +519,7 @@ async fn run_session_with_retry(
let result = run_session(
&client,
&stream,
- encryption.as_ref(),
+ &headers,
&mut state,
buffer_size,
&frame_signal,
@@ -604,7 +620,7 @@ enum SessionOutcome {
async fn run_session(
client: &BasinClient,
stream: &StreamName,
- encryption: Option<&EncryptionKey>,
+ headers: &StreamHeaders,
state: &mut SessionState,
buffer_size: usize,
frame_signal: &Option,
@@ -618,7 +634,7 @@ async fn run_session(
let (input_tx, mut acks) = connect(
client,
stream,
- encryption,
+ headers,
buffer_size,
frame_signal.clone(),
reconnect.clone(),
@@ -888,7 +904,7 @@ async fn drain_for_reconnect(
async fn connect(
client: &BasinClient,
stream: &StreamName,
- encryption: Option<&EncryptionKey>,
+ headers: &StreamHeaders,
buffer_size: usize,
frame_signal: Option,
reconnect: ReconnectAdvice,
@@ -899,7 +915,8 @@ async fn connect(
.append_session(
stream,
ReceiverStream::new(input_rx).map(|i| i.into()),
- encryption,
+ headers.encryption.as_ref(),
+ headers.stream_config.as_ref(),
frame_signal,
reconnect,
)
diff --git a/sdk/src/session/mod.rs b/sdk/src/session/mod.rs
index 107df6e4..7868ed29 100644
--- a/sdk/src/session/mod.rs
+++ b/sdk/src/session/mod.rs
@@ -5,3 +5,12 @@ pub(crate) use append::{AppendPermit, AppendPermits, AppendSessionInternal, Batc
pub use append::{AppendSession, AppendSessionConfig};
pub(crate) use read::read_session;
pub use read::{ReadSession, ReadSessionError};
+
+/// Per-stream options sent as request headers on every (re)connect of a session.
+#[derive(Debug, Clone, Default)]
+pub(crate) struct StreamHeaders {
+ /// `s2-encryption-key`
+ pub encryption: Option,
+ /// `s2-stream-config`
+ pub stream_config: Option,
+}
diff --git a/sdk/src/session/read.rs b/sdk/src/session/read.rs
index a3a07e99..83f848f4 100644
--- a/sdk/src/session/read.rs
+++ b/sdk/src/session/read.rs
@@ -22,8 +22,9 @@ use crate::{
error::{ReadError, RequestError},
reconnect::{AdvisedReconnects, ReconnectAdvice},
retry::RetryBackoff,
+ session::StreamHeaders,
types::{
- AccessTokenMode, EncryptionKey, MeteredBytes, ReadBatch, ReadInput, ReadSessionConfig,
+ AccessTokenMode, MeteredBytes, ReadBatch, ReadInput, ReadSessionConfig,
ReadSessionRetryPolicy, StreamName, StreamPosition,
},
};
@@ -371,7 +372,7 @@ impl Drop for ReadSession {
pub async fn read_session(
client: BasinClient,
name: StreamName,
- encryption: Option,
+ headers: StreamHeaders,
input: ReadInput,
config: ReadSessionConfig,
) -> Result {
@@ -379,6 +380,7 @@ pub async fn read_session(
start,
stop,
ignore_command_records,
+ stream_config: _,
} = input;
let mut start: ReadStart = start.into();
let mut end: ReadEnd = stop.into();
@@ -399,7 +401,7 @@ pub async fn read_session(
match session_inner(
client.clone(),
name.clone(),
- encryption.clone(),
+ headers.clone(),
start.clone(),
end.clone(),
ReconnectAdvice::default(),
@@ -439,7 +441,7 @@ pub async fn read_session(
match session_inner(
client.clone(),
name.clone(),
- encryption.clone(),
+ headers.clone(),
start.clone(),
end.clone(),
ReconnectAdvice::default(),
@@ -579,14 +581,21 @@ fn update_resume_start(start: &mut ReadStart, batch: &ReadBatch) {
async fn session_inner(
client: BasinClient,
name: StreamName,
- encryption: Option,
+ headers: StreamHeaders,
start: ReadStart,
end: ReadEnd,
reconnect: ReconnectAdvice,
advised_reconnects: AdvisedReconnects,
) -> Result, ReadSessionFailure> {
let mut batches = client
- .read_session(&name, start, end, encryption.as_ref(), reconnect.clone())
+ .read_session(
+ &name,
+ start,
+ end,
+ headers.encryption.as_ref(),
+ headers.stream_config.as_ref(),
+ reconnect.clone(),
+ )
.await?;
let mut declined_advice = false;
diff --git a/sdk/src/types.rs b/sdk/src/types.rs
index 9f2d6631..a66f8c44 100644
--- a/sdk/src/types.rs
+++ b/sdk/src/types.rs
@@ -3411,6 +3411,16 @@ pub struct AppendInput {
/// If unspecified, no matching is performed. If specified and mismatched,
/// the append fails. A stream defaults to `""` as its fencing token.
pub fencing_token: Option,
+ /// Stream configuration to apply if the stream is created on append.
+ ///
+ /// Unset fields inherit the basin's default stream configuration. Ignored if the stream
+ /// already exists.
+ ///
+ /// Only used by [`append`](crate::S2Stream::append). Append sessions send the header once
+ /// at connect; see
+ /// [`AppendSessionConfig::with_stream_config`](crate::append_session::AppendSessionConfig::with_stream_config)
+ /// and [`ProducerConfig::with_stream_config`](crate::producer::ProducerConfig::with_stream_config).
+ pub stream_config: Option,
}
impl AppendInput {
@@ -3420,6 +3430,15 @@ impl AppendInput {
records,
match_seq_num: None,
fencing_token: None,
+ stream_config: None,
+ }
+ }
+
+ /// Set the stream configuration to apply if the stream is created on append.
+ pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
+ Self {
+ stream_config: Some(stream_config),
+ ..self
}
}
@@ -3676,6 +3695,11 @@ pub struct ReadInput {
///
/// Defaults to `false`.
pub ignore_command_records: bool,
+ /// Stream configuration to apply if the stream is created on read.
+ ///
+ /// Unset fields inherit the basin's default stream configuration. Ignored if the stream
+ /// already exists.
+ pub stream_config: Option,
}
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
@@ -3743,6 +3767,14 @@ impl ReadInput {
..self
}
}
+
+ /// Set the stream configuration to apply if the stream is created on read.
+ pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
+ Self {
+ stream_config: Some(stream_config),
+ ..self
+ }
+ }
}
#[derive(Debug, Clone)]
diff --git a/sdk/tests/stream_ops.rs b/sdk/tests/stream_ops.rs
index 54d81180..3a953f4f 100644
--- a/sdk/tests/stream_ops.rs
+++ b/sdk/tests/stream_ops.rs
@@ -3,7 +3,7 @@ mod common;
use std::time::Duration;
use assert_matches::assert_matches;
-use common::{S2Stream, SharedS2Basin, s2_config, unique_basin_name, unique_stream_name};
+use common::{S2Stream, SharedS2Basin, s2, s2_config, unique_basin_name, unique_stream_name};
use futures_util::{StreamExt, poll};
use rstest::rstest;
use s2_sdk::{
@@ -2211,3 +2211,118 @@ async fn producer_for_non_existent_stream_errors(
Ok(())
}
+
+#[tokio::test]
+async fn stream_config_applies_only_when_append_creates_stream()
+-> Result<(), Box> {
+ let s2 = s2();
+ let basin_name = unique_basin_name();
+ s2.create_basin(
+ CreateBasinInput::new(basin_name.clone()).with_config(
+ BasinConfig::new()
+ .with_create_stream_on_append(true)
+ .with_default_stream_config(
+ StreamConfig::new().with_storage_class(StorageClass::Standard),
+ ),
+ ),
+ )
+ .await?;
+ let basin = s2.basin(basin_name.clone());
+
+ let stream_config = StreamConfig::new()
+ .with_retention_policy(RetentionPolicy::Age(3600))
+ .with_delete_on_empty(DeleteOnEmptyConfig::new().with_min_age(Duration::from_secs(300)));
+
+ let unary_stream = unique_stream_name();
+ basin
+ .stream(unary_stream.clone())
+ .append(
+ AppendInput::new(AppendRecordBatch::try_from_iter([AppendRecord::new(
+ "hello",
+ )?])?)
+ .with_stream_config(stream_config.clone()),
+ )
+ .await?;
+ let config = basin.get_stream_config(unary_stream).await?;
+ assert_matches!(
+ config,
+ StreamConfig {
+ storage_class: Some(StorageClass::Standard),
+ retention_policy: Some(RetentionPolicy::Age(3600)),
+ delete_on_empty: Some(DeleteOnEmptyConfig {
+ min_age_secs: 300,
+ ..
+ }),
+ ..
+ }
+ );
+
+ let session_stream = unique_stream_name();
+ let producer = basin
+ .stream(session_stream.clone())
+ .producer(ProducerConfig::new().with_stream_config(stream_config.clone()));
+ producer.submit(AppendRecord::new("hello")?).await?.await?;
+ producer.close().await?;
+ let config = basin.get_stream_config(session_stream).await?;
+ assert_matches!(
+ config,
+ StreamConfig {
+ retention_policy: Some(RetentionPolicy::Age(3600)),
+ delete_on_empty: Some(DeleteOnEmptyConfig {
+ min_age_secs: 300,
+ ..
+ }),
+ ..
+ }
+ );
+
+ let existing_stream = unique_stream_name();
+ basin
+ .create_stream(CreateStreamInput::new(existing_stream.clone()))
+ .await?;
+ let before = basin.get_stream_config(existing_stream.clone()).await?;
+ basin
+ .stream(existing_stream.clone())
+ .append(
+ AppendInput::new(AppendRecordBatch::try_from_iter([AppendRecord::new(
+ "hello",
+ )?])?)
+ .with_stream_config(stream_config),
+ )
+ .await?;
+ let after = basin.get_stream_config(existing_stream).await?;
+ assert_eq!(after, before);
+ assert_ne!(after.retention_policy, Some(RetentionPolicy::Age(3600)));
+
+ s2.delete_basin(DeleteBasinInput::new(basin_name)).await?;
+ Ok(())
+}
+
+#[tokio::test]
+async fn stream_config_applies_when_read_creates_stream() -> Result<(), Box>
+{
+ let s2 = s2();
+ let basin_name = unique_basin_name();
+ s2.create_basin(
+ CreateBasinInput::new(basin_name.clone())
+ .with_config(BasinConfig::new().with_create_stream_on_read(true)),
+ )
+ .await?;
+ let basin = s2.basin(basin_name.clone());
+
+ let stream_config = StreamConfig::new().with_retention_policy(RetentionPolicy::Age(3600));
+
+ let session_stream = unique_stream_name();
+ let _session = basin
+ .stream(session_stream.clone())
+ .read_session(
+ ReadInput::new().with_stream_config(stream_config),
+ ReadSessionConfig::default(),
+ )
+ .await?;
+ let config = basin.get_stream_config(session_stream).await?;
+ assert_eq!(config.retention_policy, Some(RetentionPolicy::Age(3600)));
+
+ s2.delete_basin(DeleteBasinInput::new(basin_name)).await?;
+ Ok(())
+}
From 1a0312afa097743f6d9ba686f8ee1c064880ab73 Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
<262023388+release-pleaze[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 21:12:01 +0530
Subject: [PATCH 17/50] chore: release (#733)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 🤖 New release
* `s2-api`: 0.31.2 -> 0.31.3 (✓ API compatible changes)
* `s2-lite`: 0.42.10 -> 0.42.11 (✓ API compatible changes)
* `s2-sdk`: 0.34.6 -> 0.34.7 (✓ API compatible changes)
* `s2-cli`: 0.42.10 -> 0.42.11
* `s2-testcontainers`: 0.42.10 -> 0.42.11
Changelog
## `s2-api`
## [0.31.3] - 2026-09-11
### Features
- `s2-stream-config` header for auto-created streams
([#718](https://github.com/s2-streamstore/s2/issues/718))
## `s2-lite`
## [0.42.11] - 2026-09-11
### Features
- `s2-stream-config` header for auto-created streams
([#718](https://github.com/s2-streamstore/s2/issues/718))
## `s2-sdk`
## [0.34.7] - 2026-09-11
### Features
- `s2-stream-config` header for auto-created streams
([#718](https://github.com/s2-streamstore/s2/issues/718))
## `s2-cli`
## [0.42.11] - 2026-09-11
### Features
- `s2-stream-config` header for auto-created streams
([#718](https://github.com/s2-streamstore/s2/issues/718))
## `s2-testcontainers`
## [0.42.11] - 2026-09-11
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
Co-authored-by: release-pleaze[bot] <262023388+release-pleaze[bot]@users.noreply.github.com>
---
Cargo.lock | 10 +++++-----
api/CHANGELOG.md | 8 ++++++++
api/Cargo.toml | 2 +-
cli/CHANGELOG.md | 8 ++++++++
cli/Cargo.toml | 2 +-
lite/CHANGELOG.md | 8 ++++++++
lite/Cargo.toml | 2 +-
sdk/CHANGELOG.md | 8 ++++++++
sdk/Cargo.toml | 2 +-
testcontainers/CHANGELOG.md | 4 ++++
testcontainers/Cargo.toml | 2 +-
11 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 7ffc34a3..9a5b2d9f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4878,7 +4878,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "s2-api"
-version = "0.31.2"
+version = "0.31.3"
dependencies = [
"axum",
"base64ct",
@@ -4908,7 +4908,7 @@ dependencies = [
[[package]]
name = "s2-cli"
-version = "0.42.10"
+version = "0.42.11"
dependencies = [
"assert_cmd",
"async-stream",
@@ -4992,7 +4992,7 @@ dependencies = [
[[package]]
name = "s2-lite"
-version = "0.42.10"
+version = "0.42.11"
dependencies = [
"async-stream",
"async-trait",
@@ -5051,7 +5051,7 @@ dependencies = [
[[package]]
name = "s2-sdk"
-version = "0.34.6"
+version = "0.34.7"
dependencies = [
"assert_matches",
"async-compression",
@@ -5111,7 +5111,7 @@ dependencies = [
[[package]]
name = "s2-testcontainers"
-version = "0.42.10"
+version = "0.42.11"
dependencies = [
"reqwest",
"s2-sdk",
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index 91342822..70473d74 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.31.3] - 2026-09-11
+
+### Features
+
+- `s2-stream-config` header for auto-created streams ([#718](https://github.com/s2-streamstore/s2/issues/718))
+
+
+
## [0.31.2] - 2026-09-10
### Bug Fixes
diff --git a/api/Cargo.toml b/api/Cargo.toml
index f239cbbc..3615870a 100644
--- a/api/Cargo.toml
+++ b/api/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-api"
-version = "0.31.2"
+version = "0.31.3"
description = "API types for S2, the durable streams API"
edition.workspace = true
license.workspace = true
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index e3577529..9f50daa7 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.42.11] - 2026-09-11
+
+### Features
+
+- `s2-stream-config` header for auto-created streams ([#718](https://github.com/s2-streamstore/s2/issues/718))
+
+
+
## [0.42.10] - 2026-09-11
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index e5922e1a..7904e85d 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-cli"
-version = "0.42.10"
+version = "0.42.11"
description = "CLI for S2"
edition.workspace = true
license.workspace = true
diff --git a/lite/CHANGELOG.md b/lite/CHANGELOG.md
index 214caca7..9a55a37e 100644
--- a/lite/CHANGELOG.md
+++ b/lite/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.42.11] - 2026-09-11
+
+### Features
+
+- `s2-stream-config` header for auto-created streams ([#718](https://github.com/s2-streamstore/s2/issues/718))
+
+
+
## [0.42.10] - 2026-09-11
### Miscellaneous Tasks
diff --git a/lite/Cargo.toml b/lite/Cargo.toml
index 72013081..9f6790cb 100644
--- a/lite/Cargo.toml
+++ b/lite/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-lite"
-version = "0.42.10"
+version = "0.42.11"
description = "Lightweight server implementation of S2, the durable streams API, backed by object storage"
edition.workspace = true
license.workspace = true
diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md
index d2d6b1ec..bf935a06 100644
--- a/sdk/CHANGELOG.md
+++ b/sdk/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.34.7] - 2026-09-11
+
+### Features
+
+- `s2-stream-config` header for auto-created streams ([#718](https://github.com/s2-streamstore/s2/issues/718))
+
+
+
## [0.34.6] - 2026-09-11
### Features
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index aa3665ae..94dfb1e6 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "s2-sdk"
description = "Rust SDK for S2"
-version = "0.34.6"
+version = "0.34.7"
edition.workspace = true
license.workspace = true
repository = "https://github.com/s2-streamstore/s2/tree/main/sdk"
diff --git a/testcontainers/CHANGELOG.md b/testcontainers/CHANGELOG.md
index 0fa9bdf8..2fa76cdc 100644
--- a/testcontainers/CHANGELOG.md
+++ b/testcontainers/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
+## [0.42.11] - 2026-09-11
+
+
+
## [0.42.10] - 2026-09-11
diff --git a/testcontainers/Cargo.toml b/testcontainers/Cargo.toml
index e955d07f..68a46e6b 100644
--- a/testcontainers/Cargo.toml
+++ b/testcontainers/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-testcontainers"
-version = "0.42.10"
+version = "0.42.11"
description = "Testcontainers helpers for the S2 Docker image"
edition.workspace = true
license.workspace = true
From c364f506eeb9feffc1ea24cb2add6d9f19d87fcc Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
Date: Fri, 11 Sep 2026 16:19:31 +0000
Subject: [PATCH 18/50] Bump s2-lite-helm chart to appVersion 0.42.11
---
charts/s2-lite-helm/Chart.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/charts/s2-lite-helm/Chart.yaml b/charts/s2-lite-helm/Chart.yaml
index 25315647..8481fb0f 100644
--- a/charts/s2-lite-helm/Chart.yaml
+++ b/charts/s2-lite-helm/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
name: s2-lite-helm
description: Self-hostable S2 streaming datastore using SlateDB on object storage
type: application
-version: 0.1.66
-appVersion: "0.42.10"
+version: 0.1.67
+appVersion: "0.42.11"
keywords:
- s2
- streaming
From 09b57bf77f154d391431e7c0bc05e72730ae1e86 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 12 Sep 2026 02:24:09 +0530
Subject: [PATCH 19/50] chore: sync specs submodule (#739)
This PR updates the following submodules:
| **Remote Repository** | **Submodule Path** | **Change** |
| --- | --- | --- |
|
[s2-streamstore/s2-specs](https://github.com/s2-streamstore/s2-specs.git)
| api/specs |
[f29cbca...edaa1fb](https://github.com/s2-streamstore/s2-specs/compare/f29cbcaafeaa3d4970918a9a31f9e00b55fdabb4...edaa1fbcb2507362d6c680c66cdc88660e73e036)
|
---
This PR description was generated by
[sgoudham/update-git-submodules](https://github.com/sgoudham/update-git-submodules).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
---
api/specs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/api/specs b/api/specs
index f29cbcaa..edaa1fbc 160000
--- a/api/specs
+++ b/api/specs
@@ -1 +1 @@
-Subproject commit f29cbcaafeaa3d4970918a9a31f9e00b55fdabb4
+Subproject commit edaa1fbcb2507362d6c680c66cdc88660e73e036
From a271f7d9c4a185e4d0d4e73101f81980d7273737 Mon Sep 17 00:00:00 2001
From: "devin-ai-integration[bot]"
<158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Fri, 11 Sep 2026 14:11:47 -0700
Subject: [PATCH 20/50] fix(sdk): reject Content-Type in default headers (#740)
## Summary
Reject `Content-Type` in `S2Config::with_default_headers`, alongside the
existing encoding/framing restrictions. The SDK chooses the protocol for
each operation: a default `Content-Type: s2s/proto` can make a unary
read receive streaming frames, causing a decode error or timeout.
This addresses #737 at configuration validation, as an alternative to
the per-read override in #738. All `Content-Type` values are rejected
because request format belongs to the SDK.
Extend the existing rejection tests to cover S2S, protobuf, JSON,
mixed-case header names, and empty values. Remove the now-invalid
Content-Type default from the header propagation fixture while
preserving its assertions.
Validation: the five new cases failed before the fix. `just test` passes
all 796 workspace tests; SDK clippy with all features/targets and `just
fmt` also pass.
Closes #737
Link to Devin session:
https://app.devin.ai/sessions/c3dea6e292ce4071a41baf4943f1557c
Open in Devin Desktop:
https://app.devin.ai/desktop/session/c3dea6e292ce4071a41baf4943f1557c?variant=devin
Requested by: @sgbalogh
Co-authored-by: Stephen Balogh
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
sdk/src/api.rs | 1 -
sdk/src/types.rs | 13 ++++++++++---
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/sdk/src/api.rs b/sdk/src/api.rs
index f67f4b99..3a72e173 100644
--- a/sdk/src/api.rs
+++ b/sdk/src/api.rs
@@ -1364,7 +1364,6 @@ mod tests {
http::header::HeaderName::from_static(S2_BASIN),
HeaderValue::from_static("wrong-basin"),
),
- (CONTENT_TYPE, HeaderValue::from_static("wrong-content-type")),
]);
let config = S2Config::new("actual-token")
.with_endpoints(S2Endpoints::for_endpoint("http://example.test").unwrap())
diff --git a/sdk/src/types.rs b/sdk/src/types.rs
index a66f8c44..3e312852 100644
--- a/sdk/src/types.rs
+++ b/sdk/src/types.rs
@@ -568,8 +568,9 @@ impl S2Config {
///
/// # Errors
///
- /// Returns an error if `default_headers` contains `Content-Encoding`,
- /// `Content-Length`, or `Transfer-Encoding`. The SDK controls body framing.
+ /// Returns an error if `default_headers` contains `Content-Type`,
+ /// `Content-Encoding`, `Content-Length`, or `Transfer-Encoding`.
+ /// The SDK controls request format and body framing.
/// Use [`Self::with_compression`] to configure request body encoding.
#[cfg(feature = "_hidden")]
#[doc(hidden)]
@@ -581,12 +582,13 @@ impl S2Config {
));
}
for name in [
+ http::header::CONTENT_TYPE,
http::header::CONTENT_LENGTH,
http::header::TRANSFER_ENCODING,
] {
if default_headers.contains_key(&name) {
return Err(ValidationError(format!(
- "{name} cannot be set in default headers; the SDK controls request body framing"
+ "{name} cannot be set in default headers; the SDK controls request format and body framing"
)));
}
}
@@ -4094,6 +4096,11 @@ mod tests {
#[cfg(feature = "_hidden")]
#[rstest]
+ #[case::content_type_s2s("content-type", "s2s/proto")]
+ #[case::content_type_protobuf("content-type", "application/protobuf")]
+ #[case::content_type_json("content-type", "application/json")]
+ #[case::content_type_mixed_case("Content-Type", "s2s/proto")]
+ #[case::content_type_empty("content-type", "")]
#[case::content_length("content-length", "123")]
#[case::content_length_mixed_case("Content-Length", "0")]
#[case::content_length_empty("content-length", "")]
From 1841dafda0a085235279c1889bb8bc473cddc6c3 Mon Sep 17 00:00:00 2001
From: Shikhar Bhushan
Date: Tue, 15 Sep 2026 23:03:11 -0700
Subject: [PATCH 21/50] ci: allow exact-version security exceptions to
dependency cooldown (#744)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The publication cooldown can reject the first release fixing a security
advisory while `cargo deny` rejects the older vulnerable release. This
adds exceptions for reviewed security updates, matched to exact
crate/version pairs, and updates S2 to the approved Rustls release.
`security-exceptions.toml` records the crate, exact version, advisory,
and reason. The gate validates entries, checks that publication metadata
exists, and prints the justification when it waives publication age.
Other versions retain the normal cooldown. The initial entry approves
`rustls 0.23.45` for
[RUSTSEC-2026-0285](https://rustsec.org/advisories/RUSTSEC-2026-0285.html).
Exceptions ship with the shared action; consumers pick them up by
updating their pinned action or reusable-workflow commit. The README
also documents the command-scoped Cargo resolver override needed to
select an approved fresh release. Existing first-party exemptions and
other dependency checks continue to apply.
### Dependency changes
The targeted nightly Cargo update changes four transitive package
versions, with no manifest changes:
| Crate | Before → after | Upstream changes and risk |
| --- | --- | --- |
| rustls | 0.23.43 → 0.23.45 | Fixes accepting TLS 1.3 handshake
messages at the wrong encryption level; raises AWS-LC and webpki
dependency floors. [Release
notes](https://github.com/rustls/rustls/releases/tag/v/0.23.45). |
| aws-lc-rs | 1.17.3 → 1.18.1 | Stabilizes ML-DSA APIs and tightens
validation of invalid crypto inputs.
[1.18.0](https://github.com/aws/aws-lc-rs/releases/tag/v1.18.0),
[1.18.1](https://github.com/aws/aws-lc-rs/releases/tag/v1.18.1). |
| aws-lc-sys | 0.43.0 → 0.45.0 | Updates bundled AWS-LC from 5.2 to 5.7,
including native crypto/build changes and padded-decryption output
handling. Largest runtime/build change in this update. [Wrapper
notes](https://github.com/aws/aws-lc-rs/releases/tag/v1.18.1), [AWS-LC
5.7](https://github.com/aws/aws-lc/releases/tag/v5.7.0). |
| rustls-webpki | 0.103.13 → 0.103.15 | Uses stabilized ML-DSA APIs; the
final patch fixes documentation builds.
[.14](https://github.com/rustls/webpki/releases/tag/v/0.103.14),
[.15](https://github.com/rustls/webpki/releases/tag/v/0.103.15). |
Rustls requires the newer AWS-LC/webpki dependency lines. The selected
transitive versions have already completed the cooldown; only Rustls
needs the exception. Cargo also re-resolves some Windows and tempfile
dependency edges to versions already present in the lockfile.
### Validation
- 15 deterministic Python tests pass, covering exact matching, other
fresh dependencies, malformed and duplicate entries, publication
metadata, and existing cooldown behavior. A dedicated workflow runs
them.
- The gate passes against S2's four new package versions and against
cachey PR #147; in both cases only `rustls 0.23.45` uses the exception,
with the advisory and reason printed.
- After the dependency update: `just fmt`, `just test` (796 passed),
locked workspace Clippy with all features/targets and warnings denied,
`cargo deny check`, and `git diff --check` pass.
Related: https://github.com/s2-streamstore/cachey/pull/147
---
.../rust-dependency-cooldown/.gitignore | 1 +
.../rust-dependency-cooldown/README.md | 51 +++
.../actions/rust-dependency-cooldown/check.py | 40 ++-
.../security-exceptions.toml | 5 +
.../rust-dependency-cooldown/test_check.py | 290 ++++++++++++++++++
.../rust-dependency-cooldown-tests.yml | 22 ++
Cargo.lock | 32 +-
7 files changed, 424 insertions(+), 17 deletions(-)
create mode 100644 .github/actions/rust-dependency-cooldown/.gitignore
create mode 100644 .github/actions/rust-dependency-cooldown/README.md
create mode 100644 .github/actions/rust-dependency-cooldown/security-exceptions.toml
create mode 100644 .github/actions/rust-dependency-cooldown/test_check.py
create mode 100644 .github/workflows/rust-dependency-cooldown-tests.yml
diff --git a/.github/actions/rust-dependency-cooldown/.gitignore b/.github/actions/rust-dependency-cooldown/.gitignore
new file mode 100644
index 00000000..c18dd8d8
--- /dev/null
+++ b/.github/actions/rust-dependency-cooldown/.gitignore
@@ -0,0 +1 @@
+__pycache__/
diff --git a/.github/actions/rust-dependency-cooldown/README.md b/.github/actions/rust-dependency-cooldown/README.md
new file mode 100644
index 00000000..c36831f3
--- /dev/null
+++ b/.github/actions/rust-dependency-cooldown/README.md
@@ -0,0 +1,51 @@
+# Rust dependency cooldown
+
+This action checks newly locked crates.io versions against the caller's
+`registry.global-min-publish-age` setting. S2-owned crates listed in
+`first-party-crates.txt` are exempt from the publication-age check.
+
+## Security exceptions
+
+`security-exceptions.toml` records reviewed exceptions for individual security
+releases. Each entry requires an exact crate name and version, an advisory
+identifier or URL, and a reason:
+
+```toml
+[[exception]]
+crate = "rustls"
+version = "0.23.45"
+advisory = "RUSTSEC-2026-0285"
+reason = "Fixes TLS 1.3 handshake messages accepted at the wrong encryption level"
+```
+
+Review the advisory and confirm that the exact release fixes it before adding an
+entry. Ranges, wildcards, duplicate entries, and malformed configuration are
+rejected. The action prints the crate, version, advisory, and reason whenever it
+uses an exception. The exception waives only publication age; other dependency
+checks, including `cargo deny`, continue to apply.
+
+The exception file is distributed with this action. After merging an exception,
+update consuming repositories' pinned action or reusable-workflow commit to pick
+it up. Future releases of the same crate still have to satisfy the cooldown.
+
+Entries need no expiry field: an approved release follows the normal age check
+once it is old enough. Old entries may be removed in a later cleanup. An empty
+file or `exception = []` means there are no security exceptions.
+
+Cargo also enforces publication age during dependency resolution. To select an
+approved release, override that resolver check for the targeted update command:
+
+```sh
+CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow cargo +nightly update -p rustls --precise 0.23.45
+```
+
+Run the cooldown action on the resulting lockfile to check every newly selected
+version against the exception list and normal age requirement.
+
+## Tests
+
+Run from the repository root:
+
+```sh
+python3 -m unittest discover -s .github/actions/rust-dependency-cooldown -p 'test_*.py' -v
+```
diff --git a/.github/actions/rust-dependency-cooldown/check.py b/.github/actions/rust-dependency-cooldown/check.py
index 535e2884..53f7d0b0 100644
--- a/.github/actions/rust-dependency-cooldown/check.py
+++ b/.github/actions/rust-dependency-cooldown/check.py
@@ -20,6 +20,7 @@ ACTION_ROOT = Path(__file__).resolve().parent
REPO_ROOT = Path.cwd()
CONFIG = REPO_ROOT / ".cargo" / "config.toml"
ALLOWLIST = ACTION_ROOT / "first-party-crates.txt"
+SECURITY_EXCEPTIONS = ACTION_ROOT / "security-exceptions.toml"
CRATES_IO_SOURCE = "registry+https://github.com/rust-lang/crates.io-index"
INDEX_BASE = "https://index.crates.io"
USER_AGENT = "s2 minimum-publish-age check (github.com/s2-streamstore/s2)"
@@ -69,6 +70,36 @@ def allowed_crates() -> set[str]:
}
+def security_exceptions() -> dict[tuple[str, str], dict[str, str]]:
+ with SECURITY_EXCEPTIONS.open("rb") as exceptions_file:
+ document = tomllib.load(exceptions_file)
+ if document.keys() - {"exception"}:
+ raise ValueError(f"unexpected fields in {SECURITY_EXCEPTIONS}")
+ entries = document.get("exception", [])
+ if not isinstance(entries, list):
+ raise ValueError(f"expected [[exception]] entries in {SECURITY_EXCEPTIONS}")
+ required_fields = {"crate", "version", "advisory", "reason"}
+ exceptions: dict[tuple[str, str], dict[str, str]] = {}
+ for index, entry in enumerate(entries, start=1):
+ if not isinstance(entry, dict) or entry.keys() != required_fields:
+ raise ValueError(f"security exception {index} must contain {sorted(required_fields)}")
+ for field, value in entry.items():
+ if not isinstance(value, str) or not value or value != value.strip():
+ raise ValueError(f"security exception {index} has invalid {field}")
+ if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]*", entry["crate"]) is None:
+ raise ValueError(f"security exception {index} requires an exact crate name")
+ if re.fullmatch(
+ r"[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?",
+ entry["version"],
+ ) is None:
+ raise ValueError(f"security exception {index} requires an exact version")
+ key = (entry["crate"], entry["version"])
+ if key in exceptions:
+ raise ValueError(f"duplicate security exception for {key[0]} {key[1]}")
+ exceptions[key] = entry
+ return exceptions
+
+
def crates_io_versions(lock_text: str) -> set[tuple[str, str]]:
packages = tomllib.loads(lock_text).get("package", [])
return {
@@ -196,6 +227,7 @@ def main() -> int:
try:
if args.base_ref and run_git("rev-parse", "--verify", "--quiet", args.base_ref).returncode:
raise ValueError(f"base ref {args.base_ref!r} is not available")
+ approved_exceptions = security_exceptions()
if args.base_ref and not has_relevant_changes(args.base_ref):
print("No Rust dependency cooldown files changed; check skipped.")
return 0
@@ -231,6 +263,12 @@ def main() -> int:
checked += 1
crate_age = now - pubtime
if crate_age < age_limit:
+ if exception := approved_exceptions.get((name, version)):
+ print(
+ f"Publication cooldown waived for {name} {version} ({lockfile}): "
+ f"{exception['advisory']} — {exception['reason']}"
+ )
+ continue
violations.append(
f"{name} {version} ({lockfile}): published "
f"{crate_age.total_seconds() / 86400:.1f} days ago; "
@@ -246,7 +284,7 @@ def main() -> int:
print(f" - {violation}", file=sys.stderr)
return 1
- print(f"Checked {checked} new crates.io version(s); all are at least {age_text} old.")
+ print(f"Checked {checked} new crates.io version(s); publication cooldown policy satisfied.")
return 0
diff --git a/.github/actions/rust-dependency-cooldown/security-exceptions.toml b/.github/actions/rust-dependency-cooldown/security-exceptions.toml
new file mode 100644
index 00000000..c0e3a044
--- /dev/null
+++ b/.github/actions/rust-dependency-cooldown/security-exceptions.toml
@@ -0,0 +1,5 @@
+[[exception]]
+crate = "rustls"
+version = "0.23.45"
+advisory = "RUSTSEC-2026-0285"
+reason = "Fixes TLS 1.3 handshake messages accepted at the wrong encryption level"
diff --git a/.github/actions/rust-dependency-cooldown/test_check.py b/.github/actions/rust-dependency-cooldown/test_check.py
new file mode 100644
index 00000000..313e59a5
--- /dev/null
+++ b/.github/actions/rust-dependency-cooldown/test_check.py
@@ -0,0 +1,290 @@
+from __future__ import annotations
+
+import importlib.util
+import io
+import json
+import subprocess
+import sys
+import tempfile
+import unittest
+from contextlib import ExitStack, redirect_stderr, redirect_stdout
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+from unittest.mock import patch
+
+
+SPEC = importlib.util.spec_from_file_location(
+ "cooldown_check", Path(__file__).with_name("check.py")
+)
+assert SPEC is not None and SPEC.loader is not None
+check = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(check)
+
+NOW = datetime(2026, 9, 16, tzinfo=timezone.utc)
+APPROVED_EXCEPTION = {
+ "crate": "rustls",
+ "version": "0.23.45",
+ "advisory": "https://rustsec.org/advisories/RUSTSEC-2026-0285.html",
+ "reason": "First release fixing TLS 1.3 handshake encryption-level validation.",
+}
+
+
+def exception_document(*entries: dict[str, object]) -> str:
+ return "\n".join(
+ "[[exception]]\n"
+ + "\n".join(f"{name} = {json.dumps(value)}" for name, value in entry.items())
+ + "\n"
+ for entry in entries
+ )
+
+
+class CooldownSecurityExceptionTests(unittest.TestCase):
+ def setUp(self) -> None:
+ temporary_directory = tempfile.TemporaryDirectory()
+ self.addCleanup(temporary_directory.cleanup)
+ temporary_root = Path(temporary_directory.name)
+ self.repo = temporary_root / "consumer"
+ self.repo.mkdir()
+ self.action = temporary_root / "trusted-action"
+ self.action.mkdir()
+ self.exceptions = self.action / "security-exceptions.toml"
+ self.exceptions.write_text(exception_document(APPROVED_EXCEPTION))
+ (self.action / "first-party-crates.txt").write_text("s2-api\n")
+ (self.repo / ".cargo").mkdir()
+ (self.repo / ".cargo" / "config.toml").write_text(
+ '[registry]\nglobal-min-publish-age = "7 days"\n'
+ )
+ self.write_lock()
+ self.git("init", "--quiet")
+ self.commit()
+
+ def git(self, *arguments: str) -> None:
+ subprocess.run(
+ [
+ "git",
+ "-c",
+ "user.name=Cooldown Tests",
+ "-c",
+ "user.email=cooldown-tests@example.invalid",
+ "-c",
+ "commit.gpgsign=false",
+ "-c",
+ "core.hooksPath=/dev/null",
+ *arguments,
+ ],
+ cwd=self.repo,
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+
+ def commit(self) -> None:
+ self.git("add", ".")
+ self.git("commit", "--quiet", "-m", "test: record baseline")
+
+ def write_lock(self, *packages: tuple[str, str, str]) -> None:
+ contents = "version = 3\n"
+ for name, version, source in packages:
+ contents += (
+ "\n[[package]]\n"
+ f"name = {json.dumps(name)}\n"
+ f"version = {json.dumps(version)}\n"
+ f"source = {json.dumps(source)}\n"
+ )
+ (self.repo / "Cargo.lock").write_text(contents)
+
+ def run_gate(
+ self,
+ publication_times: dict[str, dict[str, datetime]] | None = None,
+ ) -> tuple[int, str, str, list[str]]:
+ published = publication_times or {
+ "rustls": {"0.23.45": NOW - timedelta(days=1)},
+ }
+ output, errors = io.StringIO(), io.StringIO()
+ with ExitStack() as stack:
+ for name, value in {
+ "ACTION_ROOT": self.action,
+ "REPO_ROOT": self.repo,
+ "CONFIG": self.repo / ".cargo" / "config.toml",
+ "ALLOWLIST": self.action / "first-party-crates.txt",
+ "SECURITY_EXCEPTIONS": self.exceptions,
+ }.items():
+ stack.enter_context(patch.object(check, name, value))
+ stack.enter_context(
+ patch.object(sys, "argv", ["check.py", "--repo-root", str(self.repo), "HEAD"])
+ )
+ clock = stack.enter_context(patch.object(check, "datetime", wraps=datetime))
+ clock.now.return_value = NOW
+ lookup = stack.enter_context(
+ patch.object(check, "publication_times", side_effect=published.__getitem__)
+ )
+ stack.enter_context(redirect_stdout(output))
+ stack.enter_context(redirect_stderr(errors))
+ status = check.main()
+ lookups = [call.args[0] for call in lookup.call_args_list]
+ return status, output.getvalue(), errors.getvalue(), lookups
+
+ def test_exact_security_exception_passes_and_reports_justification(self) -> None:
+ self.write_lock(("rustls", "0.23.45", check.CRATES_IO_SOURCE))
+ status, output, errors, _ = self.run_gate()
+ self.assertEqual(status, 0, errors)
+ for field in APPROVED_EXCEPTION.values():
+ self.assertIn(field, output)
+
+ def test_exception_does_not_cover_another_version_or_crate(self) -> None:
+ for name, version in [("rustls", "0.23.46"), ("another-crate", "0.23.45")]:
+ with self.subTest(crate=name, version=version):
+ self.write_lock((name, version, check.CRATES_IO_SOURCE))
+ status, _, errors, _ = self.run_gate(
+ {name: {version: NOW - timedelta(days=1)}}
+ )
+ self.assertEqual(status, 1)
+ self.assertIn(f"{name} {version}", errors)
+
+ def test_exception_does_not_hide_an_unrelated_young_dependency(self) -> None:
+ self.write_lock(
+ ("rustls", "0.23.45", check.CRATES_IO_SOURCE),
+ ("another-crate", "1.0.0", check.CRATES_IO_SOURCE),
+ )
+ status, output, errors, _ = self.run_gate(
+ {
+ "rustls": {"0.23.45": NOW - timedelta(days=1)},
+ "another-crate": {"1.0.0": NOW - timedelta(days=1)},
+ }
+ )
+ self.assertEqual(status, 1)
+ self.assertIn(APPROVED_EXCEPTION["advisory"], output)
+ self.assertIn("another-crate 1.0.0", errors)
+ self.assertNotIn("rustls 0.23.45", errors)
+
+ def test_consumer_repository_cannot_supply_its_own_exception(self) -> None:
+ unapproved = {**APPROVED_EXCEPTION, "version": "0.23.46"}
+ consumer_action = self.repo / ".github" / "actions" / "rust-dependency-cooldown"
+ consumer_action.mkdir(parents=True)
+ for directory in [self.repo, self.repo / ".cargo", consumer_action]:
+ (directory / "security-exceptions.toml").write_text(exception_document(unapproved))
+ self.write_lock(("rustls", "0.23.46", check.CRATES_IO_SOURCE))
+ status, _, errors, _ = self.run_gate(
+ {"rustls": {"0.23.46": NOW - timedelta(days=1)}}
+ )
+ self.assertEqual(status, 1)
+ self.assertIn("rustls 0.23.46", errors)
+
+ def test_malformed_security_exceptions_fail_closed(self) -> None:
+ documents = {
+ "invalid TOML": "[[exception]",
+ "not an array": "exception = 1\n",
+ "not a table": "exception = [1]\n",
+ "unknown top-level field": "exceptions = []\n",
+ }
+ for field in APPROVED_EXCEPTION:
+ missing = {name: value for name, value in APPROVED_EXCEPTION.items() if name != field}
+ documents[f"missing {field}"] = exception_document(missing)
+ for value in ["", " ", 123, f" {APPROVED_EXCEPTION[field]}"]:
+ documents[f"invalid {field}: {value!r}"] = exception_document(
+ {**APPROVED_EXCEPTION, field: value}
+ )
+ documents["unknown field"] = exception_document({**APPROVED_EXCEPTION, "extra": "value"})
+ documents["duplicate pair"] = exception_document(APPROVED_EXCEPTION, APPROVED_EXCEPTION)
+ self.write_lock(("rustls", "0.23.45", check.CRATES_IO_SOURCE))
+ for description, document in documents.items():
+ with self.subTest(description=description):
+ self.exceptions.write_text(document)
+ status, _, errors, _ = self.run_gate()
+ self.assertEqual(status, 2)
+ self.assertIn("minimum-publish-age error", errors)
+
+ def test_missing_action_exception_file_fails_closed(self) -> None:
+ self.exceptions.unlink()
+ self.write_lock(("rustls", "0.23.45", check.CRATES_IO_SOURCE))
+ status, _, errors, _ = self.run_gate()
+ self.assertEqual(status, 2)
+ self.assertIn("minimum-publish-age error", errors)
+
+ def test_malformed_exceptions_fail_even_when_no_lockfile_changed(self) -> None:
+ self.exceptions.write_text("[[exception]")
+ status, _, errors, _ = self.run_gate()
+ self.assertEqual(status, 2)
+ self.assertIn("minimum-publish-age error", errors)
+
+ def test_empty_exception_lists_do_not_exempt_young_dependencies(self) -> None:
+ self.write_lock(("rustls", "0.23.45", check.CRATES_IO_SOURCE))
+ for document in ["", "exception = []\n"]:
+ with self.subTest(document=document):
+ self.exceptions.write_text(document)
+ status, _, errors, _ = self.run_gate()
+ self.assertEqual(status, 1)
+ self.assertIn("rustls 0.23.45", errors)
+
+ def test_exception_ranges_and_wildcards_are_rejected(self) -> None:
+ self.write_lock(("rustls", "0.23.45", check.CRATES_IO_SOURCE))
+ for field, value in [
+ ("crate", "rustls*"),
+ ("version", "*"),
+ ("version", "0.23.*"),
+ ("version", ">=0.23.45"),
+ ]:
+ with self.subTest(field=field, value=value):
+ self.exceptions.write_text(
+ exception_document({**APPROVED_EXCEPTION, field: value})
+ )
+ status, _, errors, _ = self.run_gate()
+ self.assertEqual(status, 2)
+ self.assertIn("minimum-publish-age error", errors)
+
+ def test_approved_version_still_requires_a_publication_time(self) -> None:
+ self.write_lock(("rustls", "0.23.45", check.CRATES_IO_SOURCE))
+ status, _, errors, _ = self.run_gate({"rustls": {}})
+ self.assertEqual(status, 2)
+ self.assertIn("no publication time for rustls 0.23.45", errors)
+
+ def test_mature_approved_version_uses_normal_age_check(self) -> None:
+ self.write_lock(("rustls", "0.23.45", check.CRATES_IO_SOURCE))
+ status, output, errors, lookups = self.run_gate(
+ {"rustls": {"0.23.45": NOW - timedelta(days=7)}}
+ )
+ self.assertEqual(status, 0, errors)
+ self.assertNotIn(APPROVED_EXCEPTION["advisory"], output)
+ self.assertEqual(lookups, ["rustls"])
+
+ def test_age_boundary_for_ordinary_dependencies_is_unchanged(self) -> None:
+ self.write_lock(("another-crate", "1.0.0", check.CRATES_IO_SOURCE))
+ for age, expected_status in [(timedelta(days=7), 0), (timedelta(days=7, seconds=-1), 1)]:
+ with self.subTest(age=age):
+ status, _, errors, _ = self.run_gate({"another-crate": {"1.0.0": NOW - age}})
+ self.assertEqual(status, expected_status, errors)
+
+ def test_first_party_allowlist_still_exempts_new_versions(self) -> None:
+ self.write_lock(("s2-api", "99.0.0", check.CRATES_IO_SOURCE))
+ status, _, errors, lookups = self.run_gate()
+ self.assertEqual(status, 0, errors)
+ self.assertEqual(lookups, [])
+
+ def test_other_sources_are_not_processed_as_security_exceptions(self) -> None:
+ for source in [
+ "registry+https://example.invalid/index",
+ "git+https://example.invalid/rustls",
+ ]:
+ with self.subTest(source=source):
+ self.write_lock(("rustls", "0.23.45", source))
+ status, output, errors, lookups = self.run_gate()
+ self.assertEqual(status, 0, errors)
+ self.assertNotIn(APPROVED_EXCEPTION["advisory"], output)
+ self.assertEqual(lookups, [])
+
+ def test_existing_locked_versions_are_not_rechecked(self) -> None:
+ self.write_lock(("already-locked", "1.0.0", check.CRATES_IO_SOURCE))
+ self.commit()
+ self.write_lock(
+ ("already-locked", "1.0.0", check.CRATES_IO_SOURCE),
+ ("another-crate", "1.0.0", check.CRATES_IO_SOURCE),
+ )
+ status, _, errors, lookups = self.run_gate(
+ {"another-crate": {"1.0.0": NOW - timedelta(days=8)}}
+ )
+ self.assertEqual(status, 0, errors)
+ self.assertEqual(lookups, ["another-crate"])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/.github/workflows/rust-dependency-cooldown-tests.yml b/.github/workflows/rust-dependency-cooldown-tests.yml
new file mode 100644
index 00000000..76f56ecc
--- /dev/null
+++ b/.github/workflows/rust-dependency-cooldown-tests.yml
@@ -0,0 +1,22 @@
+name: Rust dependency cooldown tests
+
+on:
+ pull_request:
+ paths:
+ - .github/actions/rust-dependency-cooldown/**
+ - .github/workflows/rust-dependency-cooldown-tests.yml
+ push:
+ branches: [main]
+ paths:
+ - .github/actions/rust-dependency-cooldown/**
+ - .github/workflows/rust-dependency-cooldown-tests.yml
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - run: python3 -m unittest discover -s .github/actions/rust-dependency-cooldown -p 'test_*.py' -v
diff --git a/Cargo.lock b/Cargo.lock
index 9a5b2d9f..ee239f35 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -505,9 +505,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
-version = "1.17.3"
+version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1"
+checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e"
dependencies = [
"aws-lc-sys",
"untrusted 0.7.1",
@@ -516,9 +516,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.43.0"
+version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c"
+checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27"
dependencies = [
"cc",
"cmake",
@@ -1445,7 +1445,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -2030,7 +2030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -4340,7 +4340,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -4772,14 +4772,14 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.12.1",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "rustls"
-version = "0.23.43"
+version = "0.23.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
+checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
dependencies = [
"aws-lc-rs",
"log",
@@ -4831,7 +4831,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -4842,9 +4842,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
-version = "0.103.13"
+version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"aws-lc-rs",
"ring",
@@ -5820,10 +5820,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
- "getrandom 0.4.3",
+ "getrandom 0.3.4",
"once_cell",
"rustix 1.1.4",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
@@ -6826,7 +6826,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
From 52b6e2e8fb7b8b7b13a34a8b7b149429ea9e726a Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
<262023388+release-pleaze[bot]@users.noreply.github.com>
Date: Thu, 17 Sep 2026 20:55:25 -0700
Subject: [PATCH 22/50] chore: release (#742)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 🤖 New release
* `s2-api`: 0.31.3 -> 0.31.4 (✓ API compatible changes)
* `s2-lite`: 0.42.11 -> 0.42.12 (✓ API compatible changes)
* `s2-sdk`: 0.34.7 -> 0.34.8 (✓ API compatible changes)
* `s2-cli`: 0.42.11 -> 0.42.12
* `s2-testcontainers`: 0.42.11 -> 0.42.12
Changelog
## `s2-api`
## [0.31.4] - 2026-09-16
### Miscellaneous Tasks
- Sync specs submodule
([#739](https://github.com/s2-streamstore/s2/issues/739))
## `s2-lite`
## [0.42.12] - 2026-09-16
### Miscellaneous Tasks
- Update Cargo.lock dependencies
## `s2-sdk`
## [0.34.8] - 2026-09-16
### Bug Fixes
- Reject Content-Type in default headers
([#740](https://github.com/s2-streamstore/s2/issues/740))
## `s2-cli`
## [0.42.12] - 2026-09-16
### Miscellaneous Tasks
- Update Cargo.lock dependencies
## `s2-testcontainers`
## [0.42.12] - 2026-09-16
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
Co-authored-by: release-pleaze[bot] <262023388+release-pleaze[bot]@users.noreply.github.com>
---
Cargo.lock | 10 +++++-----
api/CHANGELOG.md | 8 ++++++++
api/Cargo.toml | 2 +-
cli/CHANGELOG.md | 8 ++++++++
cli/Cargo.toml | 2 +-
lite/CHANGELOG.md | 8 ++++++++
lite/Cargo.toml | 2 +-
sdk/CHANGELOG.md | 8 ++++++++
sdk/Cargo.toml | 2 +-
testcontainers/CHANGELOG.md | 4 ++++
testcontainers/Cargo.toml | 2 +-
11 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index ee239f35..c3f99fa9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4878,7 +4878,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "s2-api"
-version = "0.31.3"
+version = "0.31.4"
dependencies = [
"axum",
"base64ct",
@@ -4908,7 +4908,7 @@ dependencies = [
[[package]]
name = "s2-cli"
-version = "0.42.11"
+version = "0.42.12"
dependencies = [
"assert_cmd",
"async-stream",
@@ -4992,7 +4992,7 @@ dependencies = [
[[package]]
name = "s2-lite"
-version = "0.42.11"
+version = "0.42.12"
dependencies = [
"async-stream",
"async-trait",
@@ -5051,7 +5051,7 @@ dependencies = [
[[package]]
name = "s2-sdk"
-version = "0.34.7"
+version = "0.34.8"
dependencies = [
"assert_matches",
"async-compression",
@@ -5111,7 +5111,7 @@ dependencies = [
[[package]]
name = "s2-testcontainers"
-version = "0.42.11"
+version = "0.42.12"
dependencies = [
"reqwest",
"s2-sdk",
diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md
index 70473d74..cf39588b 100644
--- a/api/CHANGELOG.md
+++ b/api/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.31.4] - 2026-09-16
+
+### Miscellaneous Tasks
+
+- Sync specs submodule ([#739](https://github.com/s2-streamstore/s2/issues/739))
+
+
+
## [0.31.3] - 2026-09-11
### Features
diff --git a/api/Cargo.toml b/api/Cargo.toml
index 3615870a..ed5cb33c 100644
--- a/api/Cargo.toml
+++ b/api/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-api"
-version = "0.31.3"
+version = "0.31.4"
description = "API types for S2, the durable streams API"
edition.workspace = true
license.workspace = true
diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 9f50daa7..6697e40d 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.42.12] - 2026-09-16
+
+### Miscellaneous Tasks
+
+- Update Cargo.lock dependencies
+
+
+
## [0.42.11] - 2026-09-11
### Features
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 7904e85d..b4abf635 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-cli"
-version = "0.42.11"
+version = "0.42.12"
description = "CLI for S2"
edition.workspace = true
license.workspace = true
diff --git a/lite/CHANGELOG.md b/lite/CHANGELOG.md
index 9a55a37e..082bea58 100644
--- a/lite/CHANGELOG.md
+++ b/lite/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.42.12] - 2026-09-16
+
+### Miscellaneous Tasks
+
+- Update Cargo.lock dependencies
+
+
+
## [0.42.11] - 2026-09-11
### Features
diff --git a/lite/Cargo.toml b/lite/Cargo.toml
index 9f6790cb..fae14721 100644
--- a/lite/Cargo.toml
+++ b/lite/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-lite"
-version = "0.42.11"
+version = "0.42.12"
description = "Lightweight server implementation of S2, the durable streams API, backed by object storage"
edition.workspace = true
license.workspace = true
diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md
index bf935a06..2c9d748f 100644
--- a/sdk/CHANGELOG.md
+++ b/sdk/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to this project will be documented in this file.
+## [0.34.8] - 2026-09-16
+
+### Bug Fixes
+
+- Reject Content-Type in default headers ([#740](https://github.com/s2-streamstore/s2/issues/740))
+
+
+
## [0.34.7] - 2026-09-11
### Features
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
index 94dfb1e6..ffab8fe2 100644
--- a/sdk/Cargo.toml
+++ b/sdk/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "s2-sdk"
description = "Rust SDK for S2"
-version = "0.34.7"
+version = "0.34.8"
edition.workspace = true
license.workspace = true
repository = "https://github.com/s2-streamstore/s2/tree/main/sdk"
diff --git a/testcontainers/CHANGELOG.md b/testcontainers/CHANGELOG.md
index 2fa76cdc..8585945b 100644
--- a/testcontainers/CHANGELOG.md
+++ b/testcontainers/CHANGELOG.md
@@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.
+## [0.42.12] - 2026-09-16
+
+
+
## [0.42.11] - 2026-09-11
diff --git a/testcontainers/Cargo.toml b/testcontainers/Cargo.toml
index 68a46e6b..f22a7ff7 100644
--- a/testcontainers/Cargo.toml
+++ b/testcontainers/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "s2-testcontainers"
-version = "0.42.11"
+version = "0.42.12"
description = "Testcontainers helpers for the S2 Docker image"
edition.workspace = true
license.workspace = true
From 17156f6e10d3707c6672a9e6ab44ad0be6f374ab Mon Sep 17 00:00:00 2001
From: "release-pleaze[bot]"
Date: Fri, 18 Sep 2026 04:30:10 +0000
Subject: [PATCH 23/50] Bump s2-lite-helm chart to appVersion 0.42.12
---
charts/s2-lite-helm/Chart.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/charts/s2-lite-helm/Chart.yaml b/charts/s2-lite-helm/Chart.yaml
index 8481fb0f..74743646 100644
--- a/charts/s2-lite-helm/Chart.yaml
+++ b/charts/s2-lite-helm/Chart.yaml
@@ -2,8 +2,8 @@ apiVersion: v2
name: s2-lite-helm
description: Self-hostable S2 streaming datastore using SlateDB on object storage
type: application
-version: 0.1.67
-appVersion: "0.42.11"
+version: 0.1.68
+appVersion: "0.42.12"
keywords:
- s2
- streaming
From 69919e9d99a952ad815490e2995b565c93add04c Mon Sep 17 00:00:00 2001
From: Shikhar Bhushan
Date: Thu, 17 Sep 2026 22:09:54 -0700
Subject: [PATCH 24/50] chore(deps): upgrade SlateDB to 0.16 and refresh
dependencies (#755)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Upgrade SlateDB from 0.15 to 0.16 and refresh both Rust workspaces to
the latest releases allowed by the seven-day publication cooldown,
including incompatible direct dependency upgrades.
SlateDB writes now return before object-storage durability. Explicitly
await durability for metadata transactions, background deletion/trim
work, and test fixtures; retain the append pipeline's existing
sequence-based durability notifier. Rename the TTL option to
`ExpireAfterMillis` without changing retention units. Basin provisioning
also waits for the existing metadata row's sequence before returning an
unchanged Ensure result, an idempotent create result, or an
already-exists error, matching stream provisioning. Control-plane
regression tests share a fixture with automatic flushing disabled and
verify that basin/stream creation and concurrent basin retries wait for
durability. They synchronize through public SlateDB snapshot sequences
and check visibility through the backend APIs, without depending on
private metadata keys. A shared test-only `assert_durable()` helper
removes the repeated unwrap-and-wait chains from 53 fixture writes.
Remove the `proc-macro-error2` advisory exception after the tabled
upgrade removes that dependency.
## Changelog notes
| Dependency | Review and adaptation |
| --- | --- |
| `slatedb`, `slatedb-common`, `slatedb-txn-obj` **0.15.0 → 0.16.0** |
Adapt to [explicit write
durability](https://github.com/slatedb/slatedb/pull/1985) and
[millisecond TTL names](https://github.com/slatedb/slatedb/pull/1989).
The release also fixes compaction resurrection and sequence-tracker
deserialization, writes ManifestV2 universally while retaining V1 reads,
and increases the default GC interval to ten minutes. Existing
byte-based scan options remain valid.
[Release](https://github.com/slatedb/slatedb/releases/tag/v0.16.0). |
| `zstd` **0.13.3 → 0.14.0** | Prepared dictionaries must outlive their
streams, fixing a safe-code dangling-pointer issue; `Decoder::finish`
now consumes the remaining frame. Moves to `zstd-safe` 8 and
BSD-3-Clause licensing. Our streaming calls compile unchanged. SlateDB
still brings a separate 0.13.3 copy.
[Release](https://github.com/gyscos/zstd-rs/releases/tag/v0.14.0). |
| `dirs` **6.0.0 → 7.0.0** | Windows `preference_dir` moves from
LocalAppData to RoamingAppData. The CLI uses `config_dir`, `home_dir`,
and `cache_dir`, so its paths do not require migration.
[Changelog](https://docs.rs/crate/dirs/7.0.0#changelog). |
| `tabled` **0.21.0 → 0.22.0**, `tabled_derive` **0.11.0 → 0.12.0**,
`json_to_table` **0.13.0 → 0.14.0** | Derive diagnostics move from the
unmaintained `proc-macro-error2` to `syn::Error`; adds compact-table row
skipping and text attributes. `json_to_table` follows the tabled
version; no separate release notes were found for that wrapper.
[Changelog](https://github.com/zhiburt/tabled/blob/master/CHANGELOG.md),
[wrapper
comparison](https://github.com/zhiburt/tabled/compare/1b537fecdcd498b5d495c442646754013f771d35...7c1141044a395e7390222d4345837b9b62acc032).
|
| `rstest` / `rstest_macros` **0.26.1 → 0.27.0** | Raises MSRV to 1.85;
fixes traced mutable arguments and generated imports, and disables
unused futures-util defaults. Existing test attributes compile
unchanged.
[Release](https://github.com/la10736/rstest/releases/tag/v0.27.0). |
| `testcontainers` **0.27.3 → 0.28.0** | Updates the public Bollard
dependency to 0.21 and parse-display to 0.11. Helper code compiles
unchanged; see Docker validation below.
[Release](https://github.com/testcontainers/testcontainers-rs/releases/tag/0.28.0).
|
| `bytesize` **2.6.0 → 2.7.0** | Restores `display()` availability by
removing the problematic no-alloc support. Align the simulator
requirement with 2.7.
[Release](https://github.com/bytesize-rs/bytesize/releases/tag/bytesize-v2.7.0).
|
| `keyring` **4.1.6 → 4.2.0** | Refreshes platform credential-store
dependencies, including restored Android CLI support and 64-bit
restrictions for the DB keystore. Existing platform integration compiles
unchanged.
[Release](https://github.com/open-source-cooperative/keyring-rs/releases/tag/v4.2.0).
|
| `uuid` **1.24.0 → 1.26.1** | Fixes V7 counter placement and
overflowing Timestamp-to-SystemTime conversion; adds V7 precision
configuration. Our V4 generation calls are unchanged.
[1.26.1](https://github.com/uuid-rs/uuid/releases/tag/v1.26.1),
[1.26.0](https://github.com/uuid-rs/uuid/releases/tag/v1.26.0). |
| Simulator `s3s` **0.14.1 → 0.15.0** | Constant-time signature
comparison, tighter SigV4 region/expiry validation, and
streaming/checksum fixes; MSRV is 1.96. The mock S3 implementation needs
no API edits. 0.16 is still inside the cooldown.
[Changelog](https://github.com/s3s-project/s3s/blob/v0.15.0/CHANGELOG.md).
|
| Simulator `bytes` **1.12.0 → 1.12.1**, requirement **1.11 → 1.12** |
Fixes handling of a panicking `Box::new`; the main workspace already
used 1.12.1.
[Release](https://github.com/tokio-rs/bytes/releases/tag/v1.12.1). |
| Simulator `http` **1.4.2 → 1.5.0**, requirement **1.4 → 1.5** | Adds
QUERY and fixes URI builder/length validation.
[Release](https://github.com/hyperium/http/releases/tag/v1.5.0). |
| `hyper` **1.11.0 → 1.11.1**; simulator **1.10.1 → 1.11.1**,
requirement **1.9 → 1.11** | Fixes HTTP/1 trailer recognition, pooled
`Connection: close` handling, and flushing before yielding.
[Release](https://github.com/hyperium/hyper/releases/tag/v1.11.1). |
| SDK `tokio` requirement **1.6 → 1.53**, simulator **1.52 → 1.53** |
Align declared minimums with the already-locked 1.53.1 runtime; the
resolved Tokio version does not change.
[Release](https://github.com/tokio-rs/tokio/releases/tag/tokio-1.53.1).
|
Runtime-sensitive and incompatible transitive updates
- **Storage/cache:** `foyer` and its common/memory/storage/tokio crates
**0.22.3 → 0.22.6** fix stale cache entries after rejected admission,
in-flight entry membership checks, and musl ioctl types. Its
runtime/sketch refresh introduces `asyncband`, `datasketches`, and Jiff.
[0.22.4](https://github.com/foyer-rs/foyer/releases/tag/v0.22.4),
[0.22.6](https://github.com/foyer-rs/foyer/releases/tag/v0.22.6).
- **AWS:** `aws-config` **1.10.1 → 1.12.0** (simulator **1.8.15 →
1.12.0**), `aws-runtime` **1.9.1 → 1.9.2**, `aws-types` **1.5.0 →
1.6.0**, and the SSO/SSOOIDC/STS clients advance to **1.109.0 / 1.111.0
/ 1.114.0**. Clock-skew correction is now enabled by default; Smithy
adds pool controls and opt-in telemetry capture. The selected AWS
runtime requires Rust 1.94.1. [AWS
release](https://github.com/awslabs/aws-sdk-rust/releases/tag/release-2026-09-04),
[Smithy pool
controls](https://github.com/smithy-lang/smithy-rs/releases/tag/release-2026-08-19).
- **Smithy:** main `aws-smithy-http-client` **1.2.0 → 1.4.0**, runtime
**1.12.1 → 1.14.0**, runtime-api **1.14.0 → 1.16.0**, and types **1.6.1
→ 1.6.3**. Refreshing the older simulator lock also advances HTTP
**0.63.6 → 0.64.0**, JSON **0.62.7 → 0.63.0**, observability **0.2.6 →
0.3.0**, query/XML **0.60.15 → 0.62.0**, and schema **0.1.0 → 0.2.0**,
with the corresponding credential/SigV4/async crates. These align it
with the main workspace's runtime and protocol families; S3 smoke and
deterministic packet-loss scenarios pass. [Release
history](https://github.com/smithy-lang/smithy-rs/releases), [selected
package
versions](https://github.com/awslabs/aws-sdk-rust/releases/tag/release-2026-09-10).
- **HTTP/TLS:** `reqwest` **0.13.4 → 0.13.5** fixes proxy credential
selection and wrapped timeout recognition; `h2` **0.4.16 → 0.4.19**
fixes DATA-frame accounting and caps the encoder table; `tokio-rustls`
**0.26.4 → 0.26.5** can return more bytes per read. The graph no longer
requires `ureq` or `ureq-proto`.
[Reqwest](https://github.com/seanmonstar/reqwest/releases/tag/v0.13.5),
[h2](https://github.com/hyperium/h2/releases/tag/v0.4.19),
[tokio-rustls](https://github.com/rustls/tokio-rustls/releases/tag/v/0.26.5).
- **Certificate parsing/encoding:** `rcgen` **0.14.8 → 0.14.10** fixes
DER/spec compliance and uses stable AWS-LC ML-DSA. `pem` **3.0.6 →
4.0.0** adopts `base64` 0.23 and raises MSRV to 1.71; no formal PEM 4
release notes were found. `base64` **0.22.1 → 0.23.1** adds default SIMD
engines and changes `InvalidLastSymbol` information; 0.22 remains for
other consumers.
[rcgen](https://github.com/rustls/rcgen/releases/tag/v0.14.10), [PEM
comparison](https://github.com/jcreekmore/pem-rs/compare/7d6157704805dcedf703229926938a71b1ddd0b1...99c15c08f1389153ed037c33750f8605bbf8bd55),
[base64
notes](https://docs.rs/crate/base64/0.23.1/source/RELEASE-NOTES.md).
- **Crypto:** `blake3` **1.8.5 → 1.8.7** removes `arrayref` following an
upstream account compromise. `aes` **0.9.2 → 0.9.3** enables VAES
backends by default and raises MSRV to 1.89; `aes-gcm` **0.11.0 →
0.11.1** replaces subtle with ctutils. The Linux secret-service stack
moves `cbc` **0.1.2 → 0.2.1**, `hkdf` **0.12.4 → 0.13.0**, and
`block-padding` **0.3.3 → 0.4.2** to the Rust 2024/cipher 0.5/hmac 0.13
APIs, eliminating the older AES/cipher/HMAC/SHA2 copies.
[BLAKE3](https://github.com/BLAKE3-team/BLAKE3/releases/tag/1.8.7),
[AES](https://docs.rs/crate/aes/0.9.3/source/CHANGELOG.md),
[AES-GCM](https://docs.rs/crate/aes-gcm/0.11.1/source/CHANGELOG.md),
[CBC](https://docs.rs/crate/cbc/0.2.1/source/CHANGELOG.md),
[HKDF](https://docs.rs/crate/hkdf/0.13.0/source/CHANGELOG.md),
[padding](https://docs.rs/crate/block-padding/0.4.2/source/CHANGELOG.md).
- **Simulator crypto:** `aws-lc-rs` **1.17.0 → 1.18.1**, `aws-lc-sys`
**0.41.0 → 0.45.0**, and `rustls` **0.23.40 → 0.23.45** align with the
main workspace. AWS-LC tightens buffer/IV/key API contracts; Rustls
0.23.45 is the repository-approved security exception for TLS handshake
encryption-level validation. The getrandom fork remains pinned. [AWS-LC
release](https://github.com/aws/aws-lc-rs/releases/tag/v1.18.1), [Rustls
release](https://github.com/rustls/rustls/releases/tag/v/0.23.45).
- **Compression:** `async-compression` **0.4.43 → 0.4.46** (simulator
**0.4.42 → 0.4.46**), codecs **0.4.38 → 0.4.41**, and core **0.4.32 →
0.4.33** adopt zstd 0.14 and reject stalled deflate64 input. `flate2`
**1.1.9 → 1.1.10** fixes gzip write loops and rejects truncated deflate
streams. `miniz_oxide` **0.8.9 → 0.9.1** adds partial flushing, fixes
incomplete Huffman-tree acceptance, and makes several status/config
enums non-exhaustive. `zlib-rs` **0.6.6 → 0.6.7** fixes a use-after-free
in `set_level`. `zstd-safe` adds **8.0.0** alongside **7.3.0** and
`zstd-sys` becomes **2.1.0+zstd.1.5.7**. [Async
compression](https://docs.rs/crate/async-compression/0.4.46/source/CHANGELOG.md),
[flate2](https://github.com/rust-lang/flate2-rs/releases/tag/1.1.10),
[miniz
comparison](https://github.com/Frommi/miniz_oxide/compare/44e43c7786e379b2b1a7fde4aa0e63be719e583d...4e582392df3a739d2b0dfd2c537dc33e8942be38),
[zlib-rs](https://github.com/trifectatechfoundation/zlib-rs/releases/tag/v0.6.7),
[zstd-safe](https://github.com/gyscos/zstd-rs/releases/tag/zstd-safe-8.0.0).
- **Async/OS:** the futures family **0.3.33 → 0.3.34** (simulator
**0.3.32 → 0.3.34**) preserves cloned waker identity and updates its
macro parser. `mio` **1.2.2 → 1.2.3** fixes Unix-domain listener
readiness and Wine support. Related crossbeam, io-uring, libc, and
simulator Tokio utility updates require no owned API migration; the
simulator's trace-level determinism tests cover clock/RNG scheduling
behavior.
[Futures](https://github.com/rust-lang/futures-rs/releases/tag/0.3.34),
[Mio](https://docs.rs/crate/mio/1.2.3/source/CHANGELOG.md).
- **Derive/parser APIs:** `darling`/core/macro **0.23.0 → 0.24.1**,
`zvariant_utils` **3.5.0 → 4.2.0**, simulator `serde_derive_internals`
**0.29.1 → 0.30.0**, and simulator `syn` **2.0.118 → 2.0.119 + 3.0.5**
move transitive macro APIs to syn 3 (the main workspace already used syn
3). Darling fixes custom parsing and skipped-variant diagnostics;
zvariant_utils moves derive generation internally and deprecates
GVariant support. No application macro migration is needed.
[Darling](https://docs.rs/crate/darling/0.24.1/source/CHANGELOG.md),
[zvariant_utils](https://docs.rs/crate/zvariant_utils/4.2.0/source/CHANGELOG.md),
[Serde change](https://github.com/serde-rs/serde/pull/3085).
- **Docker helpers:** `bollard` **0.20.2 → 0.21.1** and buildkit-proto
**0.7.0 → 0.8.1** refresh Docker API models, add Podman support, and fix
logs without trailing newlines. `parse-display`/derive **0.9.1 →
0.11.0** change combined display/regex handling, add optional-field
parsing, and use Rust 2024. No separate formal release notes were found
for parse-display. [Bollard
comparison](https://github.com/fussybeaver/bollard/compare/ddd21715ac76ccaf83db1b5a346c014e1fa83b64...f9ec79e7546edfb674b1066c44a68815cb52251c),
[parse-display
comparison](https://github.com/frozenlib/parse-display/compare/2fd6c6ed8e737f3dfefae0442dafb06e6d3ff1d1...3a91021cd3e79c915fb72002bdc09f4d2966a9c5).
- **QUIC:** `quinn-proto` **0.11.16 → 0.11.17** fixes three remotely
triggered memory-exhaustion bugs and a CUBIC congestion-window overflow.
[Release](https://github.com/quinn-rs/quinn/releases/tag/quinn-proto-0.11.17).
Version 0.11.18 contains further panic fixes but is still inside the
publication cooldown and has no repository exception.
- **Serialization:** `serde_with`/macros **3.21.0 → 3.23.0** cap
attacker-controlled allocation hints for duplicate-key collection
adapters, add optional Jiff adapters, and update syn/base64.
`zvariant`/derive **5.13.1 → 5.15.0** correct fixed-size
struct/dictionary padding and deprecate GVariant support. [Serde-with
changelog](https://docs.rs/crate/serde_with/3.23.0/source/CHANGELOG.md),
[Zvariant
changelog](https://docs.rs/crate/zvariant/5.15.0/source/CHANGELOG.md).
- **Credential transport:** `zbus`/macros **5.18.0 → 5.19.0** make Tokio
and async-io features additive and surface connection failures as
`Error::Connection`; the secret-service dependency refresh uses the
updated crypto family above.
[Changelog](https://docs.rs/crate/zbus/5.19.0/source/CHANGELOG.md).
- **Unicode:** ICU collections/locale/normalization/property crates
**2.2.0 → 2.3.0**, provider **2.2.0 → 2.3.1**, and new segmenter
dependencies bring Unicode 17 property and line-segmentation updates.
[Release](https://github.com/unicode-org/icu4x/releases/tag/icu%402.3.0).
- **Concurrency and telemetry:** `crossbeam-epoch` **0.9.20 → 0.9.21**
and `crossbeam-utils` **0.8.22 → 0.8.23** improve ThreadSanitizer
compatibility and fix a leaked `ShardedLockWriteGuard` aliasing
violation. `portable-atomic` **1.14.0 → 1.15.0** fixes missing memory
barriers on older ARM targets. `log` **0.4.33 → 0.4.34** adds boxed
loggers with alloc support.
[Epoch](https://docs.rs/crate/crossbeam-epoch/0.9.21/source/CHANGELOG.md),
[Utils](https://docs.rs/crate/crossbeam-utils/0.8.23/source/CHANGELOG.md),
[Portable
atomic](https://docs.rs/crate/portable-atomic/1.15.0/source/CHANGELOG.md),
[Log](https://docs.rs/crate/log/0.4.34/source/CHANGELOG.md).
The complete lockfile delta below includes maintenance updates to the
serialization, Unicode, credential-store, QUIC, and platform-support
families. The main workspace and simulator build without further owned
API changes; Linux and other target-specific behavior is covered by CI
rather than the local macOS runs.
## Risk notes
- SlateDB changes persistence timing and writes ManifestV2. The
migration preserves durable acknowledgements and the append pipeline's
batching; upgrades also adopt the upstream ten-minute garbage-collection
default.
- Public APIs exposing SlateDB or testcontainers types now use their new
incompatible versions. The SDK Tokio requirement is explicitly raised to
1.53; the simulator now requires Rust 1.96 through s3s.
- Preserve the utoipa exact-version/git patch, simulator getrandom fork,
s2-verification revision, and existing Rustls 0.23.45 security
exception. Newer releases inside the seven-day cooldown remain deferred.
## Validation
- `just fmt`, `cargo sort --workspace --check`, and `git diff --check`:
pass.
- `cargo metadata --locked --format-version 1` for both workspaces:
pass.
- `just clippy`: pass; simulator Clippy with `--locked`, all targets,
and `RUSTFLAGS="--cfg tokio_unstable"`: pass.
- `just test`: **802 passed**. All four basin retry regression cases
were also verified to fail when the retry durability waits were
temporarily removed, then pass with the waits restored.
- `cargo nextest run --locked -p s2-testcontainers`: **4 passed**
against published image 0.42.11 before rebasing onto the release commit.
After rebase, **3 passed / 1 blocked** because main now selects image
0.42.12, which returns `manifest unknown` from GHCR. CI builds the
matching image from the PR source before running these tests.
- Simulator smoke seed 1 and trace-level determinism checks for smoke
seed 1, linearizable seed 1, and linearizable seed 2 with `--fail-rate
0.005`: pass. The linearizable scenarios each produce 300 matching
history records across repeated runs; the separate Go Porcupine checker
was not run locally.
- Publication cooldown check: **372 new crate versions pass**, retaining
the exact Rustls security exception; `just deny`: pass with existing
unrelated warnings.
- Live cloud SDK/CLI integration tests were not run locally.
- [PR CI](https://github.com/s2-streamstore/s2/pull/755/checks): all
required checks passed for the basin durability fix; rerunning for the
control-plane test reorganization.
Complete registry lockfile version changes
### `Cargo.lock`
| Package | Before | After |
| --- | --- | --- |
| `aes` | 0.8.4, 0.9.2 | 0.9.3 |
| `aes-gcm` | 0.11.0 | 0.11.1 |
| `aho-corasick` | 1.1.4 | 1.1.5 |
| `android_system_properties` | 0.1.5 | 0.1.6 |
| `apple-native-keyring-store` | 1.0.1 | 1.0.2 |
| `arrayref` | 0.3.9 | — |
| `async-compression` | 0.4.43 | 0.4.46 |
| `async-trait` | 0.1.91 | 0.1.92 |
| `asyncband` | — | 0.7.2 |
| `aws-config` | 1.10.1 | 1.12.0 |
| `aws-runtime` | 1.9.1 | 1.9.2 |
| `aws-sdk-sso` | 1.105.0 | 1.109.0 |
| `aws-sdk-ssooidc` | 1.107.0 | 1.111.0 |
| `aws-sdk-sts` | 1.110.0 | 1.114.0 |
| `aws-smithy-http-client` | 1.2.0 | 1.4.0 |
| `aws-smithy-runtime` | 1.12.1 | 1.14.0 |
| `aws-smithy-runtime-api` | 1.14.0 | 1.16.0 |
| `aws-smithy-types` | 1.6.1 | 1.6.3 |
| `aws-types` | 1.5.0 | 1.6.0 |
| `base64` | 0.22.1 | 0.22.1, 0.23.1 |
| `bitflags` | 2.13.1 | 1.3.2, 2.13.2 |
| `blake3` | 1.8.5 | 1.8.7 |
| `block-padding` | 0.3.3 | 0.4.2 |
| `blocking` | 1.6.2 | 1.7.0 |
| `bollard` | 0.20.2 | 0.21.1 |
| `bollard-buildkit-proto` | 0.7.0 | 0.8.1 |
| `bollard-stubs` | 1.52.1-rc.29.1.3 | 1.53.1-rc.29.3.1 |
| `bstr` | 1.13.0 | 1.13.1 |
| `bytecheck` | 0.8.2 | 0.8.3 |
| `bytecheck_derive` | 0.8.2 | 0.8.3 |
| `bytesize` | 2.6.0 | 2.7.0 |
| `cbc` | 0.1.2 | 0.2.1 |
| `cc` | 1.4.0 | 1.4.5 |
| `chacha20` | 0.10.1 | 0.10.2 |
| `cipher` | 0.4.4, 0.5.2 | 0.5.2 |
| `clap` | 4.6.5 | 4.6.6 |
| `clap_builder` | 4.6.5 | 4.6.6 |
| `cmsketch` | 0.2.4 | — |
| `combine` | 4.6.7 | 4.6.8 |
| `compression-codecs` | 0.4.38 | 0.4.41 |
| `compression-core` | 0.4.32 | 0.4.33 |
| `console` | 0.16.4 | 0.16.6 |
| `core_detect` | — | 1.0.0 |
| `cpufeatures` | 0.2.17, 0.3.0 | 0.2.17, 0.3.1 |
| `crc32fast` | 1.5.0 | 1.5.1 |
| `crossbeam-epoch` | 0.9.20 | 0.9.21 |
| `crossbeam-utils` | 0.8.22 | 0.8.23 |
| `darling` | 0.21.3, 0.23.0 | 0.21.3, 0.24.1 |
| `darling_core` | 0.21.3, 0.23.0 | 0.21.3, 0.24.1 |
| `darling_macro` | 0.21.3, 0.23.0 | 0.21.3, 0.24.1 |
| `data-encoding` | 2.11.0 | 2.11.1 |
| `datasketches` | — | 0.3.0 |
| `defmt` | — | 1.1.1 |
| `defmt-macros` | — | 1.1.1 |
| `defmt-parser` | — | 1.0.0 |
| `dirs` | 6.0.0 | 7.0.0 |
| `either` | 1.17.0 | 1.18.0 |
| `encoding_rs` | 0.8.35 | 0.8.41 |
| `eyre` | 0.6.12 | 0.6.14 |
| `find-msvc-tools` | 0.1.9 | 0.1.12 |
| `flate2` | 1.1.9 | 1.1.10 |
| `foyer` | 0.22.3 | 0.22.6 |
| `foyer-common` | 0.22.3 | 0.22.6 |
| `foyer-memory` | 0.22.3 | 0.22.6 |
| `foyer-storage` | 0.22.3 | 0.22.6 |
| `foyer-tokio` | 0.22.3 | 0.22.6 |
| `futures` | 0.3.33 | 0.3.34 |
| `futures-channel` | 0.3.33 | 0.3.34 |
| `futures-core` | 0.3.33 | 0.3.34 |
| `futures-executor` | 0.3.33 | 0.3.34 |
| `futures-io` | 0.3.33 | 0.3.34 |
| `futures-macro` | 0.3.33 | 0.3.34 |
| `futures-sink` | 0.3.33 | 0.3.34 |
| `futures-task` | 0.3.33 | 0.3.34 |
| `futures-util` | 0.3.33 | 0.3.34 |
| `h2` | 0.4.16 | 0.4.19 |
| `hermit-abi` | 0.5.2 | 0.5.3 |
| `hkdf` | 0.12.4 | 0.13.0 |
| `hmac` | 0.12.1, 0.13.0 | 0.13.0 |
| `http-body-util` | 0.1.4 | 0.1.5 |
| `hybrid-array` | 0.4.14 | 0.4.15 |
| `hyper` | 1.11.0 | 1.11.1 |
| `icu_collections` | 2.2.0 | 2.3.0 |
| `icu_locale_core` | 2.2.0 | 2.3.0 |
| `icu_locale_fallback` | — | 2.3.0 |
| `icu_locale_fallback_data` | — | 2.3.0 |
| `icu_normalizer` | 2.2.0 | 2.3.0 |
| `icu_normalizer_data` | 2.2.0 | 2.3.0 |
| `icu_properties` | 2.2.0 | 2.3.0 |
| `icu_properties_data` | 2.2.0 | 2.3.0 |
| `icu_provider` | 2.2.0 | 2.3.1 |
| `icu_segmenter` | — | 2.3.0 |
| `icu_segmenter_data` | — | 2.3.0 |
| `indexmap` | 1.9.3, 2.14.0 | 1.9.3, 2.14.2 |
| `inout` | 0.1.4, 0.2.2 | 0.2.2 |
| `io-uring` | 0.7.13 | 0.7.15 |
| `ipnet` | 2.12.0 | 2.12.2 |
| `jiff` | — | 0.2.35 |
| `jiff-core` | — | 0.1.0 |
| `jiff-static` | — | 0.2.35 |
| `jiff-tzdb` | — | 0.1.8 |
| `jiff-tzdb-platform` | — | 0.1.3 |
| `js-sys` | 0.3.103 | 0.3.105 |
| `json_to_table` | 0.13.0 | 0.14.0 |
| `keyring` | 4.1.6 | 4.2.0 |
| `libredox` | 0.1.18 | 0.1.23 |
| `litemap` | 0.8.2 | 0.8.3 |
| `log` | 0.4.33 | 0.4.34 |
| `lru` | 0.18.2 | 0.18.4 |
| `mea` | 0.6.5 | — |
| `miniz_oxide` | 0.8.9 | 0.8.9, 0.9.1 |
| `mio` | 1.2.2 | 1.2.3 |
| `multiversion` | — | 0.9.0 |
| `multiversion-macros` | — | 0.9.0 |
| `multiversion_no_op` | — | 1.0.0 |
| `num-integer` | 0.1.46 | 0.1.47 |
| `owo-colors` | 4.3.0 | 4.4.0 |
| `parse-display` | 0.9.1 | 0.11.0 |
| `parse-display-derive` | 0.9.1 | 0.11.0 |
| `pem` | 3.0.6 | 4.0.0 |
| `pest` | 2.8.8 | 2.9.1 |
| `pest_derive` | 2.8.8 | 2.9.1 |
| `pest_generator` | 2.8.8 | 2.9.1 |
| `pest_meta` | 2.8.8 | 2.9.1 |
| `pkg-config` | 0.3.33 | 0.3.34 |
| `portable-atomic` | 1.14.0 | 1.15.0 |
| `portable-atomic-util` | — | 0.2.8 |
| `potential_utf` | 0.1.5 | 0.1.6 |
| `proc-macro-error-attr2` | 2.0.0 | — |
| `proc-macro-error2` | 2.0.1 | — |
| `ptr_meta` | 0.3.1 | 0.3.2 |
| `ptr_meta_derive` | 0.3.1 | 0.3.2 |
| `quinn-proto` | 0.11.16 | 0.11.17 |
| `rancor` | 0.1.2 | 0.1.3 |
| `rcgen` | 0.14.8 | 0.14.10 |
| `ref-cast` | 1.0.26 | 1.0.27 |
| `ref-cast-impl` | 1.0.26 | 1.0.27 |
| `regex-automata` | 0.4.16 | 0.4.18 |
| `reqwest` | 0.13.4 | 0.13.5 |
| `rkyv` | 0.8.17 | 0.8.18 |
| `rkyv_derive` | 0.8.17 | 0.8.18 |
| `rstest` | 0.26.1 | 0.27.0 |
| `rstest_macros` | 0.26.1 | 0.27.0 |
| `rtoolbox` | 0.0.5 | 0.0.6 |
| `rust_decimal` | 1.42.1 | 1.43.0 |
| `secret-service` | 5.1.0 | 5.2.0 |
| `serde_with` | 3.21.0 | 3.23.0 |
| `serde_with_macros` | 3.21.0 | 3.23.0 |
| `sha2` | 0.10.9, 0.11.0 | 0.11.0 |
| `slatedb` | 0.15.0 | 0.16.0 |
| `slatedb-common` | 0.15.0 | 0.16.0 |
| `slatedb-txn-obj` | 0.15.0 | 0.16.0 |
| `smallvec` | 1.15.2 | 1.16.0 |
| `syn` | 2.0.119, 3.0.3 | 2.0.119, 3.0.5 |
| `tabled` | 0.21.0 | 0.22.0 |
| `tabled_derive` | 0.11.0 | 0.12.0 |
| `testcontainers` | 0.27.3 | 0.28.0 |
| `textwrap` | 0.16.2 | 0.16.3 |
| `thiserror` | 1.0.69, 2.0.19 | 1.0.69, 2.0.20 |
| `thiserror-impl` | 1.0.69, 2.0.19 | 1.0.69, 2.0.20 |
| `time` | 0.3.54 | 0.3.55 |
| `tinystr` | 0.8.3 | 0.8.4 |
| `tinyvec` | 1.12.0 | 1.13.2 |
| `tokio-rustls` | 0.26.4 | 0.26.5 |
| `toml` | 0.8.23, 1.1.4+spec-1.1.0 | 0.8.23, 1.1.6+spec-1.1.0 |
| `toml_edit` | 0.22.27, 0.25.13+spec-1.1.0 | 0.22.27,
0.25.15+spec-1.1.0 |
| `tower-http` | 0.6.11, 0.7.0 | 0.6.11, 0.7.1 |
| `twox-hash` | 2.1.3 | 2.1.4 |
| `unicode-linebreak` | 0.1.5 | — |
| `ureq` | 3.3.0 | — |
| `ureq-proto` | 0.6.0 | — |
| `utf8-zero` | 0.8.1 | — |
| `uuid` | 1.24.0 | 1.26.1 |
| `wasm-bindgen` | 0.2.126 | 0.2.128 |
| `wasm-bindgen-futures` | 0.4.76 | 0.4.78 |
| `wasm-bindgen-macro` | 0.2.126 | 0.2.128 |
| `wasm-bindgen-macro-support` | 0.2.126 | 0.2.128 |
| `wasm-bindgen-shared` | 0.2.126 | 0.2.128 |
| `web-sys` | 0.3.103 | 0.3.105 |
| `writeable` | 0.6.3 | 0.6.4 |
| `yaml-rust2` | 0.11.0 | 0.11.1 |
| `zbus` | 5.18.0 | 5.19.0 |
| `zbus-secret-service-keyring-store` | 1.0.0 | 1.0.1 |
| `zbus_macros` | 5.18.0 | 5.19.0 |
| `zcheapstr` | — | 1.1.0 |
| `zerocopy` | 0.8.55 | 0.8.57 |
| `zerocopy-derive` | 0.8.55 | 0.8.57 |
| `zerotrie` | 0.2.4 | 0.2.5 |
| `zerovec` | 0.11.6 | 0.11.8 |
| `zerovec-derive` | 0.11.3 | 0.11.6 |
| `zlib-rs` | 0.6.6 | 0.6.7 |
| `zstd` | 0.13.3 | 0.13.3, 0.14.0 |
| `zstd-safe` | 7.2.4 | 7.3.0, 8.0.0 |
| `zstd-sys` | 2.0.16+zstd.1.5.7 | 2.1.0+zstd.1.5.7 |
| `zvariant` | 5.13.1 | 5.15.0 |
| `zvariant_derive` | 5.13.1 | 5.15.0 |
| `zvariant_utils` | 3.5.0 | 4.2.0 |
### `sim/Cargo.lock`
| Package | Before | After |
| --- | --- | --- |
| `aegis` | 0.9.12 | 0.9.15 |
| `aes` | 0.9.1 | 0.9.3 |
| `aes-gcm` | 0.11.0 | 0.11.1 |
| `ahash` | 0.8.12 | — |
| `aho-corasick` | 1.1.4 | 1.1.5 |
| `android_system_properties` | 0.1.5 | 0.1.6 |
| `anyhow` | 1.0.102 | 1.0.104 |
| `arc-swap` | 1.9.1 | 1.9.2 |
| `arrayref` | 0.3.9 | — |
| `arrayvec` | 0.7.6 | 0.7.8 |
| `async-compression` | 0.4.42 | 0.4.46 |
| `async-trait` | 0.1.89 | 0.1.92 |
| `asyncband` | — | 0.7.2 |
| `aws-config` | 1.8.15 | 1.12.0 |
| `aws-credential-types` | 1.2.14 | 1.3.0 |
| `aws-lc-rs` | 1.17.0 | 1.18.1 |
| `aws-lc-sys` | 0.41.0 | 0.45.0 |
| `aws-runtime` | 1.7.2 | 1.9.2 |
| `aws-sdk-sso` | 1.97.0 | 1.109.0 |
| `aws-sdk-ssooidc` | 1.99.0 | 1.111.0 |
| `aws-sdk-sts` | 1.102.0 | 1.114.0 |
| `aws-sigv4` | 1.4.2 | 1.5.1 |
| `aws-smithy-async` | 1.2.14 | 1.3.0 |
| `aws-smithy-http` | 0.63.6 | 0.64.0 |
| `aws-smithy-http-client` | 1.1.13 | 1.4.0 |
| `aws-smithy-json` | 0.62.7 | 0.63.0 |
| `aws-smithy-observability` | 0.2.6 | 0.3.0 |
| `aws-smithy-query` | 0.60.15 | 0.62.0 |
| `aws-smithy-runtime` | 1.11.3 | 1.14.0 |
| `aws-smithy-runtime-api` | 1.12.3 | 1.16.0 |
| `aws-smithy-runtime-api-macros` | 1.0.0 | 1.1.0 |
| `aws-smithy-schema` | 0.1.0 | 0.2.0 |
| `aws-smithy-types` | 1.5.0 | 1.6.3 |
| `aws-smithy-xml` | 0.60.15 | 0.62.0 |
| `aws-types` | 1.3.16 | 1.6.0 |
| `base64` | 0.22.1 | 0.22.1, 0.23.1 |
| `bitflags` | 2.13.0 | 2.13.2 |
| `blake3` | 1.8.5 | 1.8.7 |
| `bytemuck` | 1.25.0 | 1.25.2 |
| `bytes` | 1.12.0 | 1.12.1 |
| `bytesize` | 2.6.0 | 2.7.0 |
| `cc` | 1.2.64 | 1.4.5 |
| `cfg_aliases` | 0.2.1 | 0.2.2 |
| `chacha20` | 0.10.0 | 0.10.2 |
| `clap` | 4.6.1 | 4.6.6 |
| `clap_builder` | 4.6.0 | 4.6.6 |
| `clap_derive` | 4.6.1 | 4.6.4 |
| `cmsketch` | 0.2.4 | — |
| `combine` | 4.6.7 | 4.6.8 |
| `compression-codecs` | 0.4.38 | 0.4.41 |
| `compression-core` | 0.4.32 | 0.4.33 |
| `cpufeatures` | 0.2.17, 0.3.0 | 0.2.17, 0.3.1 |
| `crc32fast` | 1.5.0 | 1.5.1 |
| `crossbeam-epoch` | 0.9.18 | 0.9.21 |
| `crossbeam-utils` | 0.8.21 | 0.8.23 |
| `data-encoding` | 2.11.0 | 2.11.1 |
| `datasketches` | — | 0.3.0 |
| `displaydoc` | 0.2.6 | 0.2.7 |
| `either` | 1.16.0 | 1.18.0 |
| `enumset` | 1.1.13 | 1.1.14 |
| `event-listener` | 5.4.1 | 5.4.2 |
| `eyre` | 0.6.12 | 0.6.14 |
| `fastrand` | 2.4.1 | 2.5.0 |
| `faststr` | 0.2.34 | — |
| `find-msvc-tools` | 0.1.9 | 0.1.12 |
| `flate2` | 1.1.9 | 1.1.10 |
| `foyer` | 0.22.3 | 0.22.6 |
| `foyer-common` | 0.22.3 | 0.22.6 |
| `foyer-memory` | 0.22.3 | 0.22.6 |
| `foyer-storage` | 0.22.3 | 0.22.6 |
| `foyer-tokio` | 0.22.3 | 0.22.6 |
| `fs-err` | 3.3.0 | 3.3.1 |
| `futures` | 0.3.32 | 0.3.34 |
| `futures-channel` | 0.3.32 | 0.3.34 |
| `futures-core` | 0.3.32 | 0.3.34 |
| `futures-executor` | 0.3.32 | 0.3.34 |
| `futures-io` | 0.3.32 | 0.3.34 |
| `futures-macro` | 0.3.32 | 0.3.34 |
| `futures-sink` | 0.3.32 | 0.3.34 |
| `futures-task` | 0.3.32 | 0.3.34 |
| `futures-util` | 0.3.32 | 0.3.34 |
| `h2` | 0.4.16 | 0.4.19 |
| `hashbrown` | 0.14.5, 0.15.5, 0.16.1, 0.17.1 | 0.14.5, 0.15.5, 0.17.1
|
| `hermit-abi` | 0.5.2 | 0.5.3 |
| `hmac` | 0.12.1, 0.13.0 | 0.13.0 |
| `http` | 0.2.12, 1.4.2 | 0.2.12, 1.5.0 |
| `http-body` | 0.4.6, 1.0.1 | 0.4.6, 1.1.0 |
| `http-body-util` | 0.1.3 | 0.1.5 |
| `hybrid-array` | 0.4.12 | 0.4.15 |
| `hyper` | 1.10.1 | 1.11.1 |
| `icu_collections` | 2.2.0 | 2.3.0 |
| `icu_locale_core` | 2.2.0 | 2.3.0 |
| `icu_normalizer` | 2.2.0 | 2.3.0 |
| `icu_normalizer_data` | 2.2.0 | 2.3.0 |
| `icu_properties` | 2.2.0 | 2.3.0 |
| `icu_properties_data` | 2.2.0 | 2.3.0 |
| `icu_provider` | 2.2.0 | 2.3.1 |
| `indexmap` | 2.14.0 | 2.14.2 |
| `io-uring` | 0.7.12 | 0.7.15 |
| `ipnet` | 2.12.0 | 2.12.2 |
| `jobserver` | 0.1.34 | 0.1.35 |
| `js-sys` | 0.3.102 | 0.3.105 |
| `libc` | 0.2.186 | 0.2.189 |
| `linkme` | 0.3.36 | 0.3.37 |
| `linkme-impl` | 0.3.36 | 0.3.37 |
| `litemap` | 0.8.2 | 0.8.3 |
| `log` | 0.4.32 | 0.4.34 |
| `lru` | 0.18.2 | 0.18.4 |
| `mea` | 0.6.4 | — |
| `memchr` | 2.8.2 | 2.8.3 |
| `miniz_oxide` | 0.8.9 | 0.9.1 |
| `mio` | 1.2.1 | 1.2.3 |
| `munge` | 0.4.7 | — |
| `munge_macro` | 0.4.7 | — |
| `num-bigint` | 0.4.6 | 0.4.8 |
| `num-integer` | 0.1.46 | 0.1.47 |
| `object_store` | 0.14.0 | 0.14.1 |
| `pem` | 3.0.6 | 4.0.0 |
| `pkg-config` | 0.3.33 | 0.3.34 |
| `polyval` | 0.7.1 | 0.7.3 |
| `potential_utf` | 0.1.5 | 0.1.6 |
| `proc-macro2` | 1.0.106 | 1.0.107 |
| `ptr_meta` | 0.3.1 | — |
| `ptr_meta_derive` | 0.3.1 | — |
| `quick-xml` | 0.40.1, 0.41.0 | 0.41.0 |
| `quinn` | 0.11.9 | 0.11.11 |
| `quinn-proto` | 0.11.14 | 0.11.17 |
| `quinn-udp` | 0.5.14 | 0.5.15 |
| `quote` | 1.0.45 | 1.0.47 |
| `rancor` | 0.1.1 | — |
| `rand` | 0.10.1, 0.8.6, 0.9.4 | 0.10.2, 0.8.8, 0.9.5 |
| `rand_pcg` | — | 0.10.2 |
| `rcgen` | 0.14.8 | 0.14.10 |
| `ref-cast` | 1.0.25 | 1.0.27 |
| `ref-cast-impl` | 1.0.25 | 1.0.27 |
| `regex` | — | 1.13.1 |
| `regex-automata` | 0.4.14 | 0.4.18 |
| `rend` | 0.5.3 | — |
| `reqwest` | 0.13.4 | 0.13.5 |
| `rkyv` | 0.8.16 | — |
| `rkyv_derive` | 0.8.16 | — |
| `rust_decimal` | 1.42.1 | 1.43.0 |
| `rustc-hash` | 2.1.2 | 2.1.3 |
| `rustls` | 0.23.40 | 0.23.45 |
| `rustls-pki-types` | 1.14.1 | 1.15.1 |
| `rustls-webpki` | 0.103.13 | 0.103.15 |
| `rustversion` | 1.0.22 | 1.0.23 |
| `s3s` | 0.14.1 | 0.15.0 |
| `schemars` | 1.2.1 | 1.2.2 |
| `schemars_derive` | 1.2.1 | 1.2.2 |
| `serde` | 1.0.228 | 1.0.229 |
| `serde_core` | 1.0.228 | 1.0.229 |
| `serde_derive` | 1.0.228 | 1.0.229 |
| `serde_derive_internals` | 0.29.1 | 0.30.0 |
| `serde_json` | 1.0.150 | 1.0.151 |
| `sha1` | 0.10.6, 0.11.0 | 0.10.7, 0.11.0 |
| `sha2` | 0.10.9, 0.11.0 | 0.11.0 |
| `simd-adler32` | 0.3.9 | 0.3.10 |
| `simd_cesu8` | 1.1.1 | 1.2.0 |
| `slatedb` | 0.15.0 | 0.16.0 |
| `slatedb-common` | 0.15.0 | 0.16.0 |
| `slatedb-txn-obj` | 0.15.0 | 0.16.0 |
| `smallvec` | 1.15.2 | 1.16.0 |
| `socket2` | 0.6.4 | 0.6.5 |
| `sonic-number` | 0.1.2 | — |
| `sonic-rs` | 0.5.8 | — |
| `sonic-simd` | 0.1.4 | — |
| `spin` | 0.10.0 | 0.10.1 |
| `syn` | 2.0.118 | 2.0.119, 3.0.5 |
| `thiserror` | 1.0.69, 2.0.18 | 1.0.69, 2.0.20 |
| `thiserror-impl` | 1.0.69, 2.0.18 | 1.0.69, 2.0.20 |
| `thread_local` | 1.1.9 | 1.1.10 |
| `time` | 0.3.49 | 0.3.55 |
| `time-macros` | 0.2.29 | 0.2.32 |
| `tinystr` | 0.8.3 | 0.8.4 |
| `tinyvec` | 1.11.0 | 1.13.2 |
| `tokio-macros` | 2.7.0 | 2.7.2 |
| `tokio-rustls` | 0.26.4 | 0.26.5 |
| `tokio-stream` | 0.1.18 | 0.1.19 |
| `tokio-util` | 0.7.18 | 0.7.19 |
| `tower-http` | 0.6.11, 0.7.0 | 0.6.11, 0.7.1 |
| `twox-hash` | 2.1.2 | 2.1.4 |
| `uuid` | 1.23.3 | 1.26.1 |
| `wasm-bindgen` | 0.2.125 | 0.2.128 |
| `wasm-bindgen-futures` | 0.4.75 | 0.4.78 |
| `wasm-bindgen-macro` | 0.2.125 | 0.2.128 |
| `wasm-bindgen-macro-support` | 0.2.125 | 0.2.128 |
| `wasm-bindgen-shared` | 0.2.125 | 0.2.128 |
| `web-sys` | 0.3.102 | 0.3.105 |
| `webpki-root-certs` | 1.0.8 | 1.0.9 |
| `windows-sys` | 0.52.0, 0.59.0, 0.60.2, 0.61.2 | 0.52.0, 0.59.0,
0.61.2 |
| `windows-targets` | 0.52.6, 0.53.5 | 0.52.6 |
| `windows_aarch64_gnullvm` | 0.52.6, 0.53.1 | 0.52.6 |
| `windows_aarch64_msvc` | 0.52.6, 0.53.1 | 0.52.6 |
| `windows_i686_gnu` | 0.52.6, 0.53.1 | 0.52.6 |
| `windows_i686_gnullvm` | 0.52.6, 0.53.1 | 0.52.6 |
| `windows_i686_msvc` | 0.52.6, 0.53.1 | 0.52.6 |
| `windows_x86_64_gnu` | 0.52.6, 0.53.1 | 0.52.6 |
| `windows_x86_64_gnullvm` | 0.52.6, 0.53.1 | 0.52.6 |
| `windows_x86_64_msvc` | 0.52.6, 0.53.1 | 0.52.6 |
| `writeable` | 0.6.3 | 0.6.4 |
| `xxhash-rust` | 0.8.15 | 0.8.18 |
| `zerocopy` | 0.8.52 | 0.8.57 |
| `zerocopy-derive` | 0.8.52 | 0.8.57 |
| `zerotrie` | 0.2.4 | 0.2.5 |
| `zerovec` | 0.11.6 | 0.11.8 |
| `zerovec-derive` | 0.11.3 | 0.11.6 |
| `zlib-rs` | — | 0.6.7 |
| `zmij` | 1.0.21 | 1.0.23 |
| `zstd` | 0.13.3 | 0.13.3, 0.14.0 |
| `zstd-safe` | 7.2.4 | 7.3.0, 8.0.0 |
| `zstd-sys` | 2.0.16+zstd.1.5.7 | 2.1.0+zstd.1.5.7 |
---
Cargo.lock | 1322 ++++++++++--------
Cargo.toml | 20 +-
deny.toml | 2 -
lite/src/backend/basins.rs | 35 +-
lite/src/backend/bgtasks/basin_deletion.rs | 48 +-
lite/src/backend/bgtasks/stream_doe.rs | 66 +-
lite/src/backend/bgtasks/stream_trim.rs | 102 +-
lite/src/backend/core.rs | 3 +-
lite/src/backend/mod.rs | 3 +
lite/src/backend/read.rs | 16 +-
lite/src/backend/store.rs | 8 +
lite/src/backend/streamer.rs | 11 +-
lite/src/backend/streams.rs | 8 +-
lite/src/backend/test_util.rs | 18 +
lite/tests/backend/common/setup.rs | 52 +-
lite/tests/backend/control_plane/basin.rs | 74 +
lite/tests/backend/control_plane/stream.rs | 28 +
sdk/Cargo.toml | 2 +-
sim/Cargo.lock | 1445 +++++++++-----------
sim/Cargo.toml | 14 +-
20 files changed, 1700 insertions(+), 1577 deletions(-)
create mode 100644 lite/src/backend/test_util.rs
diff --git a/Cargo.lock b/Cargo.lock
index c3f99fa9..10792de7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99"
dependencies = [
"crypto-common 0.2.2",
- "inout 0.2.2",
+ "inout",
]
[[package]]
@@ -39,46 +39,36 @@ dependencies = [
[[package]]
name = "aes"
-version = "0.8.4"
+version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
dependencies = [
- "cfg-if",
- "cipher 0.4.4",
- "cpufeatures 0.2.17",
-]
-
-[[package]]
-name = "aes"
-version = "0.9.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58"
-dependencies = [
- "cipher 0.5.2",
+ "cipher",
"cpubits",
- "cpufeatures 0.3.0",
+ "cpufeatures 0.3.1",
+ "zeroize",
]
[[package]]
name = "aes-gcm"
-version = "0.11.0"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028"
+checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f"
dependencies = [
"aead",
- "aes 0.9.2",
- "cipher 0.5.2",
+ "aes",
+ "cipher",
"ctr",
+ "ctutils",
"ghash",
- "subtle",
"zeroize",
]
[[package]]
name = "aho-corasick"
-version = "1.1.4"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
dependencies = [
"memchr",
]
@@ -97,9 +87,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
dependencies = [
"libc",
]
@@ -162,9 +152,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "apple-native-keyring-store"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "797f94b6a53d7d10b56dc18290e0d40a2158352f108bb4ff32350825081a9f29"
+checksum = "2b350bfd03649e07aa05c0a81b3e15934374e585c98204a57e20b9d49f49bb9a"
dependencies = [
"keyring-core",
"log",
@@ -186,12 +176,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
-[[package]]
-name = "arrayref"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
-
[[package]]
name = "arrayvec"
version = "0.7.8"
@@ -210,7 +194,7 @@ dependencies = [
"nom",
"num-traits",
"rusticata-macros",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"time",
]
@@ -300,9 +284,9 @@ dependencies = [
[[package]]
name = "async-compression"
-version = "0.4.43"
+version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3976abdc8fe7d1133d43d304afd42abdf5bc3e1319d263d223bde07b5efc4be8"
+checksum = "4f10dafd0c8d2e51ae9a748805777613ed0bbe17bf586b76c8311f45c020a32f"
dependencies = [
"compression-codecs",
"compression-core",
@@ -430,15 +414,21 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
-version = "0.1.91"
+version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
+[[package]]
+name = "asyncband"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2d85fd3d291fabcc40c7232c92c280ec1754fd7b5d7ea769f143222143e179a"
+
[[package]]
name = "atomic"
version = "0.6.1"
@@ -462,9 +452,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-config"
-version = "1.10.1"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b180a3c8b55960db3426d8964b8745e652466a1a49fe1a2eda828046d30b5e4"
+checksum = "b8d7b388a9fc3a6db15a5ec778c38b354eff1364882c94d08e0252f7a47dcaa4"
dependencies = [
"aws-credential-types",
"aws-runtime",
@@ -529,9 +519,9 @@ dependencies = [
[[package]]
name = "aws-runtime"
-version = "1.9.1"
+version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb"
+checksum = "ef47857a1d4488b528f4a5d5715fa7c3300820897824152234d3fa22b1426657"
dependencies = [
"aws-credential-types",
"aws-sigv4",
@@ -554,9 +544,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
-version = "1.105.0"
+version = "1.109.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ffd0fbe7873cb548a7aa60f9573c268fff94155397fd4f14dc9f1ecaaab8516"
+checksum = "c3cfe74df5d9ad2fedd691973ad3521ebf4f27a3c68c792556686aedb5519bab"
dependencies = [
"arc-swap",
"aws-credential-types",
@@ -580,9 +570,9 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
-version = "1.107.0"
+version = "1.111.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "175763eb222a46377df7aa257a3bca980ab3e96703fefc8f4d0b8da6ad2e254c"
+checksum = "81b0ec31ed6191bd11350aae4b2004198f2db21350cb0a20c57e0a92e55dd161"
dependencies = [
"arc-swap",
"aws-credential-types",
@@ -606,9 +596,9 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
-version = "1.110.0"
+version = "1.114.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b14781dfbff48984017d57167b6ea0b6471c6920ec52b44a2677c7feb3c13"
+checksum = "ef45745026107ec30c4ef86bd8ae4b002e7e5f6a86e4225240bdf6b06a0b944a"
dependencies = [
"arc-swap",
"aws-credential-types",
@@ -644,11 +634,11 @@ dependencies = [
"bytes",
"form_urlencoded",
"hex",
- "hmac 0.13.0",
+ "hmac",
"http 0.2.12",
"http 1.5.0",
"percent-encoding",
- "sha2 0.11.0",
+ "sha2",
"time",
"tracing",
]
@@ -687,9 +677,9 @@ dependencies = [
[[package]]
name = "aws-smithy-http-client"
-version = "1.2.0"
+version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "635d23afda0a6ab48d666c4d447c4873e8d1e83518a2be2093122397e50b838e"
+checksum = "ebfd138fac0337cee7516c352757ea73b9f2266e57d0bcb5bc70e9547e45aef1"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
@@ -744,9 +734,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime"
-version = "1.12.1"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1"
+checksum = "b82e438d30e02a825d363bd639a9efaed68a8089d86101054b0081e7e0d3e606"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
@@ -770,9 +760,9 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
-version = "1.14.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb"
+checksum = "9c054752dd9e4dc73d0b75748c99ac2d0feafbf2f25c7b0516f03a3534161223"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api-macros",
@@ -810,9 +800,9 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
-version = "1.6.1"
+version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8"
+checksum = "8f94d16e797ec62cd999fc9d5942b48fa7050c3093ddadff48e4d7528d16fcb9"
dependencies = [
"base64-simd",
"bytes",
@@ -845,9 +835,9 @@ dependencies = [
[[package]]
name = "aws-types"
-version = "1.5.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94"
+checksum = "209f3a6d82a6e9e5f94abbed94c7a26e1c052341002bf57a5fb5481f625896fc"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
@@ -964,7 +954,7 @@ dependencies = [
"addr2line",
"cfg-if",
"libc",
- "miniz_oxide",
+ "miniz_oxide 0.8.9",
"object",
"rustc-demangle",
"windows-link 0.2.1",
@@ -985,6 +975,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
[[package]]
name = "base64-simd"
version = "0.8.0"
@@ -1036,25 +1032,30 @@ dependencies = [
[[package]]
name = "bitflags"
-version = "2.13.1"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
dependencies = [
"serde_core",
]
[[package]]
name = "blake3"
-version = "1.8.5"
+version = "1.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
+checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
dependencies = [
- "arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
- "cpufeatures 0.3.0",
+ "cpufeatures 0.3.1",
]
[[package]]
@@ -1077,18 +1078,18 @@ dependencies = [
[[package]]
name = "block-padding"
-version = "0.3.3"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
dependencies = [
- "generic-array",
+ "hybrid-array",
]
[[package]]
name = "blocking"
-version = "1.6.2"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
dependencies = [
"async-channel",
"async-task",
@@ -1099,13 +1100,13 @@ dependencies = [
[[package]]
name = "bollard"
-version = "0.20.2"
+version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4"
+checksum = "dbe8358268799ebb3e4df23cb9d47f4c72bbc4f5247e2fa6a1bf7b6c0baea220"
dependencies = [
"async-stream",
- "base64",
- "bitflags",
+ "base64 0.22.1",
+ "bitflags 2.13.2",
"bollard-buildkit-proto",
"bollard-stubs",
"bytes",
@@ -1123,7 +1124,7 @@ dependencies = [
"log",
"num",
"pin-project-lite",
- "rand 0.9.5",
+ "rand 0.10.2",
"rustls",
"rustls-native-certs",
"rustls-pki-types",
@@ -1131,7 +1132,7 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_urlencoded",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"time",
"tokio",
"tokio-stream",
@@ -1144,24 +1145,23 @@ dependencies = [
[[package]]
name = "bollard-buildkit-proto"
-version = "0.7.0"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad"
+checksum = "b5c97450e79c7c565302dd92e86b08823b47550fcb4fc5ce910194d1b087a1a3"
dependencies = [
"prost",
"prost-types",
"tonic",
"tonic-prost",
- "ureq",
]
[[package]]
name = "bollard-stubs"
-version = "1.52.1-rc.29.1.3"
+version = "1.53.1-rc.29.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066"
+checksum = "ce412eb6f7096743011dc3cb5c674caeb24ced61d8c498fe07cf7998a4fea889"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bollard-buildkit-proto",
"bytes",
"prost",
@@ -1182,9 +1182,9 @@ dependencies = [
[[package]]
name = "bstr"
-version = "1.13.0"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530"
+checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
dependencies = [
"memchr",
"regex-automata",
@@ -1199,9 +1199,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytecheck"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0caa33a2c0edca0419d15ac723dff03f1956f7978329b1e3b5fdaaaed9d3ca8b"
+checksum = "26333eeac754f0ad8a6bcd0eb0ac012156302e4e16b852b72ee399aea4f12c29"
dependencies = [
"bytecheck_derive",
"ptr_meta",
@@ -1211,13 +1211,13 @@ dependencies = [
[[package]]
name = "bytecheck_derive"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "89385e82b5d1821d2219e0b095efa2cc1f246cbf99080f3be46a1a85c0d392d9"
+checksum = "46d07918caa9eeaaf06b7873925c53a61daac173539b4f7715090745e44e4e69"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
@@ -1259,9 +1259,9 @@ dependencies = [
[[package]]
name = "bytesize"
-version = "2.6.0"
+version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "351a3e803ee3c6eaeee6b00076b767514b37c32a73d326c3ec7abddb7d6c3493"
+checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b"
[[package]]
name = "castaway"
@@ -1274,18 +1274,18 @@ dependencies = [
[[package]]
name = "cbc"
-version = "0.1.2"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
dependencies = [
- "cipher 0.4.4",
+ "cipher",
]
[[package]]
name = "cc"
-version = "1.4.0"
+version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
+checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1307,12 +1307,12 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
- "cpufeatures 0.3.0",
+ "cpufeatures 0.3.1",
"rand_core 0.10.1",
]
@@ -1330,16 +1330,6 @@ dependencies = [
"windows-link 0.2.1",
]
-[[package]]
-name = "cipher"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
-dependencies = [
- "crypto-common 0.1.7",
- "inout 0.1.4",
-]
-
[[package]]
name = "cipher"
version = "0.5.2"
@@ -1348,14 +1338,14 @@ checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
dependencies = [
"block-buffer 0.12.1",
"crypto-common 0.2.2",
- "inout 0.2.2",
+ "inout",
]
[[package]]
name = "clap"
-version = "4.6.5"
+version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf"
+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1363,9 +1353,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.6.5"
+version = "4.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078"
+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
dependencies = [
"anstream",
"anstyle",
@@ -1382,7 +1372,7 @@ dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -1406,12 +1396,6 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
-[[package]]
-name = "cmsketch"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7ee2cfacbd29706479902b06d75ad8f1362900836aa32799eabc7e004bfd854"
-
[[package]]
name = "color-print"
version = "0.3.7"
@@ -1445,14 +1429,14 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
name = "combine"
-version = "4.6.7"
+version = "4.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
dependencies = [
"bytes",
"memchr",
@@ -1475,22 +1459,22 @@ dependencies = [
[[package]]
name = "compression-codecs"
-version = "0.4.38"
+version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf"
+checksum = "58a6d0db8759036a783bc7c3f7a07f8cef3bf9470eb1db3bc86e8bcd1c5d0fe8"
dependencies = [
"compression-core",
"flate2",
"memchr",
- "zstd",
- "zstd-safe",
+ "zstd 0.14.0",
+ "zstd-safe 8.0.0",
]
[[package]]
name = "compression-core"
-version = "0.4.32"
+version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
+checksum = "6e8ccc4ea9f6acc32d102c0f6d471d11d913ad15f20c04de743374861fa1d414"
[[package]]
name = "concurrent-queue"
@@ -1516,16 +1500,16 @@ dependencies = [
"serde-untagged",
"serde_core",
"serde_json",
- "toml 1.1.4+spec-1.1.0",
+ "toml 1.1.6+spec-1.1.0",
"winnow 1.0.4",
"yaml-rust2",
]
[[package]]
name = "console"
-version = "0.16.4"
+version = "0.16.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c"
+checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3"
dependencies = [
"encode_unicode",
"libc",
@@ -1601,6 +1585,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "core_detect"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
+
[[package]]
name = "cpubits"
version = "0.1.1"
@@ -1618,9 +1608,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
@@ -1637,18 +1627,18 @@ dependencies = [
[[package]]
name = "crc32fast"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-epoch"
-version = "0.9.20"
+version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
+checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
@@ -1665,9 +1655,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
-version = "0.8.22"
+version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crunchy"
@@ -1702,7 +1692,7 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21"
dependencies = [
- "cipher 0.5.2",
+ "cipher",
]
[[package]]
@@ -1726,12 +1716,12 @@ dependencies = [
[[package]]
name = "darling"
-version = "0.23.0"
+version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
+checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
dependencies = [
- "darling_core 0.23.0",
- "darling_macro 0.23.0",
+ "darling_core 0.24.1",
+ "darling_macro 0.24.1",
]
[[package]]
@@ -1749,15 +1739,15 @@ dependencies = [
[[package]]
name = "darling_core"
-version = "0.23.0"
+version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
+checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff"
dependencies = [
"ident_case",
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
@@ -1773,13 +1763,13 @@ dependencies = [
[[package]]
name = "darling_macro"
-version = "0.23.0"
+version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
+checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
dependencies = [
- "darling_core 0.23.0",
+ "darling_core 0.24.1",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
@@ -1798,9 +1788,46 @@ dependencies = [
[[package]]
name = "data-encoding"
-version = "2.11.0"
+version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+
+[[package]]
+name = "datasketches"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c4cf71a36b46dcfc00e5014c0c20ccad2b1b6a008304d7d57d2749b2d41b3d"
+
+[[package]]
+name = "defmt"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
+dependencies = [
+ "defmt-parser",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.20",
+]
[[package]]
name = "der-parser"
@@ -1839,7 +1866,6 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common 0.1.7",
- "subtle",
]
[[package]]
@@ -1856,9 +1882,9 @@ dependencies = [
[[package]]
name = "dirs"
-version = "6.0.0"
+version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
dependencies = [
"dirs-sys",
]
@@ -1872,7 +1898,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -1883,7 +1909,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -1901,7 +1927,7 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d"
dependencies = [
- "base64",
+ "base64 0.22.1",
"serde",
"serde_json",
]
@@ -1926,7 +1952,7 @@ checksum = "f88959de2d447fd3eddcf1909d1f19fe084e27a056a6904203dc5d8b9e771c1e"
dependencies = [
"rust_decimal",
"serde",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"time",
"winnow 0.6.26",
]
@@ -1939,9 +1965,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
-version = "1.17.0"
+version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
+checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
[[package]]
name = "encode_unicode"
@@ -1951,11 +1977,17 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "encoding_rs"
-version = "0.8.35"
+version = "0.8.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a"
dependencies = [
"cfg-if",
+ "core_detect",
+ "multiversion",
+ "multiversion_no_op",
+ "rustversion",
+ "scopeguard",
+ "simdutf8",
]
[[package]]
@@ -2030,7 +2062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -2065,10 +2097,11 @@ dependencies = [
[[package]]
name = "eyre"
-version = "0.6.12"
+version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
+checksum = "c08309dbcc659c5549a24ddb9b27027640641b282ef5768267c7e675558986a3"
dependencies = [
+ "autocfg",
"indenter",
"once_cell",
]
@@ -2130,9 +2163,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
-version = "0.1.9"
+version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"
[[package]]
name = "fixedbitset"
@@ -2146,18 +2179,18 @@ version = "25.12.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"rustc_version",
]
[[package]]
name = "flate2"
-version = "1.1.9"
+version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
- "miniz_oxide",
+ "miniz_oxide 0.9.1",
"zlib-rs",
]
@@ -2199,18 +2232,18 @@ dependencies = [
[[package]]
name = "foyer"
-version = "0.22.3"
+version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0abc0b87814989efa711f9becd9f26969820e2d3905db27d10969c4bd45890"
+checksum = "6a59f42276891c0a4ce683fcda1aa1b4fd1065d68116c264e4bf49b9d318a0ed"
dependencies = [
"anyhow",
+ "asyncband",
"equivalent",
"foyer-common",
"foyer-memory",
"foyer-storage",
"foyer-tokio",
"futures-util",
- "mea",
"mixtrics",
"pin-project",
"serde",
@@ -2219,9 +2252,9 @@ dependencies = [
[[package]]
name = "foyer-common"
-version = "0.22.3"
+version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3db80d5dece93adb7ad709c84578794724a9cba342a7e566c3551c7ec626789"
+checksum = "643b47d510032a01e5af70dca288b0c5ec531646c1a8392e793fb5b0c13bef30"
dependencies = [
"anyhow",
"bincode",
@@ -2246,21 +2279,21 @@ dependencies = [
[[package]]
name = "foyer-memory"
-version = "0.22.3"
+version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db907f40a527ca2aa2f40a5f68b32ea58aa70f050cd233518e9ffd402cfba6ce"
+checksum = "ae51f5089f3d9025ae77f17ea66d2489ac0f82fbb1d91462807bea174d486d82"
dependencies = [
"anyhow",
- "bitflags",
- "cmsketch",
+ "asyncband",
+ "bitflags 2.13.2",
+ "datasketches",
"equivalent",
"foyer-common",
"foyer-intrusive-collections",
"foyer-tokio",
"futures-util",
- "hashbrown 0.16.1",
- "itertools 0.14.0",
- "mea",
+ "hashbrown 0.17.1",
+ "itertools 0.15.0",
"mixtrics",
"parking_lot",
"paste",
@@ -2271,12 +2304,13 @@ dependencies = [
[[package]]
name = "foyer-storage"
-version = "0.22.3"
+version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1983f1db3d0710e9c9d5fc116d9202dccd41a2d1e032572224f1aff5520aa958"
+checksum = "118192f532ba013f0cdc607efc22324b9ed855baed185608af0daa986e835c6b"
dependencies = [
"allocator-api2",
"anyhow",
+ "asyncband",
"bytes",
"core_affinity",
"equivalent",
@@ -2287,26 +2321,25 @@ dependencies = [
"fs4",
"futures-core",
"futures-util",
- "hashbrown 0.16.1",
+ "hashbrown 0.17.1",
"io-uring",
- "itertools 0.14.0",
+ "itertools 0.15.0",
"libc",
"lz4",
- "mea",
"parking_lot",
"pin-project",
- "rand 0.9.5",
+ "rand 0.10.2",
"serde",
"tracing",
"twox-hash",
- "zstd",
+ "zstd 0.13.3",
]
[[package]]
name = "foyer-tokio"
-version = "0.22.3"
+version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6577b05a7ffad0db555aedf00bfe52af818220fc4c1c3a7a12520896fc38627"
+checksum = "6741d1133dfaab64d3f8a9290bbfed3685084add28f8b6ee7a6264aa4dc26918"
dependencies = [
"tokio",
]
@@ -2349,9 +2382,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futures"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218"
+checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
dependencies = [
"futures-channel",
"futures-core",
@@ -2364,9 +2397,9 @@ dependencies = [
[[package]]
name = "futures-channel"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
+checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4"
dependencies = [
"futures-core",
"futures-sink",
@@ -2374,15 +2407,15 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-executor"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
+checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
dependencies = [
"futures-core",
"futures-task",
@@ -2391,9 +2424,9 @@ dependencies = [
[[package]]
name = "futures-io"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
+checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
[[package]]
name = "futures-lite"
@@ -2410,26 +2443,26 @@ dependencies = [
[[package]]
name = "futures-macro"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b"
+checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
name = "futures-sink"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
+checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d"
[[package]]
name = "futures-task"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-timer"
@@ -2439,9 +2472,9 @@ checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-channel",
"futures-core",
@@ -2510,6 +2543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5"
dependencies = [
"polyval",
+ "zeroize",
]
[[package]]
@@ -2538,9 +2572,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.4.16"
+version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
+checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16"
dependencies = [
"atomic-waker",
"bytes",
@@ -2548,7 +2582,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.5.0",
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"slab",
"tokio",
"tokio-util",
@@ -2582,8 +2616,6 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
- "allocator-api2",
- "equivalent",
"foldhash 0.2.0",
]
@@ -2621,9 +2653,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
[[package]]
name = "hex"
@@ -2633,20 +2665,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
-version = "0.12.4"
+version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018"
dependencies = [
- "hmac 0.12.1",
-]
-
-[[package]]
-name = "hmac"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
-dependencies = [
- "digest 0.10.7",
+ "hmac",
]
[[package]]
@@ -2711,9 +2734,9 @@ dependencies = [
[[package]]
name = "http-body-util"
-version = "0.1.4"
+version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
+checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
dependencies = [
"bytes",
"futures-core",
@@ -2742,18 +2765,18 @@ checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
[[package]]
name = "hybrid-array"
-version = "0.4.14"
+version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b"
+checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"typenum",
]
[[package]]
name = "hyper"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
+checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43"
dependencies = [
"atomic-waker",
"bytes",
@@ -2820,7 +2843,7 @@ version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
- "base64",
+ "base64 0.22.1",
"bytes",
"futures-channel",
"futures-util",
@@ -2878,9 +2901,9 @@ dependencies = [
[[package]]
name = "icu_collections"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
@@ -2892,22 +2915,43 @@ dependencies = [
[[package]]
name = "icu_locale_core"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
+ "serde",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
-name = "icu_normalizer"
-version = "2.2.0"
+name = "icu_locale_fallback"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9"
+dependencies = [
+ "icu_locale_core",
+ "icu_locale_fallback_data",
+ "icu_provider",
+ "potential_utf",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_fallback_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8"
+
+[[package]]
+name = "icu_normalizer"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -2919,16 +2963,17 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
+ "displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
@@ -2939,18 +2984,20 @@ dependencies = [
[[package]]
name = "icu_properties_data"
-version = "2.2.0"
+version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
-version = "2.2.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
+ "serde",
+ "stable_deref_trait",
"writeable",
"yoke",
"zerofrom",
@@ -2958,6 +3005,28 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "icu_segmenter"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82d07aafccd67af15d02512a6adf5896fbc5ed00f2e99b471d2efa14016db3db"
+dependencies = [
+ "icu_collections",
+ "icu_locale_fallback",
+ "icu_provider",
+ "icu_segmenter_data",
+ "potential_utf",
+ "smallvec",
+ "utf8_iter",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_segmenter_data"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
+
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -3004,9 +3073,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.14.0"
+version = "2.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
@@ -3033,41 +3102,32 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
-[[package]]
-name = "inout"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
-dependencies = [
- "block-padding",
- "generic-array",
-]
-
[[package]]
name = "inout"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
dependencies = [
+ "block-padding",
"hybrid-array",
]
[[package]]
name = "io-uring"
-version = "0.7.13"
+version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9080b15e63775b9a2ac7dca720f7050a8b955e092ea0f6020a4a80f69998cdc0"
+checksum = "ed3bd0ecfbb87805f538bb7b32e5239ca0763890c623e349860ecba69469f2bb"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"cfg-if",
"libc",
]
[[package]]
name = "ipnet"
-version = "2.12.0"
+version = "2.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0"
[[package]]
name = "is_ci"
@@ -3105,6 +3165,59 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+[[package]]
+name = "jiff"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc"
+dependencies = [
+ "defmt",
+ "jiff-core",
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+ "windows-link 0.2.1",
+]
+
+[[package]]
+name = "jiff-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09"
+dependencies = [
+ "defmt",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204"
+dependencies = [
+ "jiff-core",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
[[package]]
name = "jni"
version = "0.22.4"
@@ -3117,7 +3230,7 @@ dependencies = [
"jni-sys",
"log",
"simd_cesu8",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"walkdir",
"windows-link 0.2.1",
]
@@ -3166,9 +3279,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.103"
+version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
dependencies = [
"cfg-if",
"futures-util",
@@ -3188,9 +3301,9 @@ dependencies = [
[[package]]
name = "json_to_table"
-version = "0.13.0"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f91246cf42b255a705436e360ca5894e376a50fdf696e4e60cb1bb6dd648e21"
+checksum = "0ec64c9908ffa97b6ef6044d96c9f56de1c81643d3b9fbc2823e8847ab11b467"
dependencies = [
"serde_json",
"tabled",
@@ -3198,9 +3311,9 @@ dependencies = [
[[package]]
name = "keyring"
-version = "4.1.6"
+version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "72585bb6cc9bc370d1d545b7e23fcce71dfd4461c5e15275e3cf51bdfd9a980a"
+checksum = "2270074a3d26bcac93c1dc5d2845eb4c089e8d761ccf6e0ea266a16004640627"
dependencies = [
"apple-native-keyring-store",
"keyring-core",
@@ -3231,9 +3344,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "libredox"
-version = "0.1.18"
+version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652"
+checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed"
dependencies = [
"libc",
]
@@ -3252,9 +3365,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
-version = "0.8.2"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "lock_api"
@@ -3267,15 +3380,15 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.33"
+version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lru"
-version = "0.18.2"
+version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a"
+checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25"
dependencies = [
"hashbrown 0.17.1",
]
@@ -3339,15 +3452,6 @@ dependencies = [
"digest 0.11.3",
]
-[[package]]
-name = "mea"
-version = "0.6.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31fc7d159de0085ab6dd7ff145a9819442cfd3d098f783263120503c3f3e58b0"
-dependencies = [
- "slab",
-]
-
[[package]]
name = "memchr"
version = "2.8.3"
@@ -3410,6 +3514,15 @@ name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
dependencies = [
"adler2",
"simd-adler32",
@@ -3417,9 +3530,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.2.2"
+version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
+checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8"
dependencies = [
"libc",
"wasi",
@@ -3442,6 +3555,33 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
+[[package]]
+name = "multiversion"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c"
+dependencies = [
+ "multiversion-macros",
+]
+
+[[package]]
+name = "multiversion-macros"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustversion",
+ "syn 3.0.5",
+]
+
+[[package]]
+name = "multiversion_no_op"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
+
[[package]]
name = "munge"
version = "0.4.7"
@@ -3468,7 +3608,7 @@ version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"cfg-if",
"cfg_aliases",
"libc",
@@ -3505,7 +3645,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -3549,9 +3689,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-integer"
-version = "0.1.46"
+version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
@@ -3602,7 +3742,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
]
[[package]]
@@ -3632,7 +3772,7 @@ checksum = "d354792e39fa5f0009e47623cf8b15b099bf9a652fa55c6f817fe28ac84fea50"
dependencies = [
"async-trait",
"aws-lc-rs",
- "base64",
+ "base64 0.22.1",
"bytes",
"chrono",
"crc-fast",
@@ -3656,7 +3796,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"tokio",
"tracing",
"url",
@@ -3751,9 +3891,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
[[package]]
name = "owo-colors"
-version = "4.3.0"
+version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
+checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
[[package]]
name = "papergrid"
@@ -3797,9 +3937,9 @@ dependencies = [
[[package]]
name = "parse-display"
-version = "0.9.1"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a"
+checksum = "e78deb158fb1d73b29efb4b7e9b9860b78059c670de06bd28df8d0b458ded0eb"
dependencies = [
"parse-display-derive",
"regex",
@@ -3808,9 +3948,9 @@ dependencies = [
[[package]]
name = "parse-display-derive"
-version = "0.9.1"
+version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281"
+checksum = "8e95a50d1084dab562913062c4c34bb204b68fc6ec38a1395909ff5aaaf4f10a"
dependencies = [
"proc-macro2",
"quote",
@@ -3857,11 +3997,11 @@ dependencies = [
[[package]]
name = "pem"
-version = "3.0.6"
+version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
dependencies = [
- "base64",
+ "base64 0.23.1",
"serde_core",
]
@@ -3873,9 +4013,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
-version = "2.8.8"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2"
+checksum = "6d45aeb61b4bf818e12d4205f2466f8c4748f85f4fce0146d1c03d69d753f0ad"
dependencies = [
"memchr",
"ucd-trie",
@@ -3883,9 +4023,9 @@ dependencies = [
[[package]]
name = "pest_derive"
-version = "2.8.8"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd"
+checksum = "89cc5a242e25ed4e7704d0be240f2cfbe20a8c27e7e252d94835be93d92dc39f"
dependencies = [
"pest",
"pest_generator",
@@ -3893,9 +4033,9 @@ dependencies = [
[[package]]
name = "pest_generator"
-version = "2.8.8"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6"
+checksum = "7abf21475cc3820fe4b2ca2dc2142902f67a02189f3b5b3a229f4febc01a43e5"
dependencies = [
"pest",
"pest_meta",
@@ -3906,9 +4046,9 @@ dependencies = [
[[package]]
name = "pest_meta"
-version = "2.8.8"
+version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c"
+checksum = "adba4db388f687393c18c51348d44a41d870ca9df71a2c98172ea3035dc6936e"
dependencies = [
"pest",
]
@@ -3921,7 +4061,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
]
[[package]]
@@ -3969,9 +4109,9 @@ dependencies = [
[[package]]
name = "pkg-config"
-version = "0.3.33"
+version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "polling"
@@ -3994,22 +4134,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd"
dependencies = [
"cpubits",
- "cpufeatures 0.3.0",
+ "cpufeatures 0.3.1",
"universal-hash",
+ "zeroize",
]
[[package]]
name = "portable-atomic"
-version = "1.14.0"
+version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
+checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
+dependencies = [
+ "portable-atomic",
+]
[[package]]
name = "potential_utf"
-version = "0.1.5"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
dependencies = [
+ "serde_core",
+ "writeable",
"zerovec",
]
@@ -4074,29 +4226,7 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
- "toml_edit 0.25.13+spec-1.1.0",
-]
-
-[[package]]
-name = "proc-macro-error-attr2"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
-dependencies = [
- "proc-macro2",
- "quote",
-]
-
-[[package]]
-name = "proc-macro-error2"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
-dependencies = [
- "proc-macro-error-attr2",
- "proc-macro2",
- "quote",
- "syn 2.0.119",
+ "toml_edit 0.25.15+spec-1.1.0",
]
[[package]]
@@ -4127,7 +4257,7 @@ version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"hex",
"procfs-core",
"rustix 0.38.44",
@@ -4139,7 +4269,7 @@ version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"hex",
]
@@ -4157,7 +4287,7 @@ dependencies = [
"parking_lot",
"procfs",
"protobuf",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
]
[[package]]
@@ -4168,7 +4298,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [
"bit-set",
"bit-vec 0.8.0",
- "bitflags",
+ "bitflags 2.13.2",
"num-traits",
"rand 0.9.5",
"rand_chacha",
@@ -4252,22 +4382,22 @@ dependencies = [
[[package]]
name = "ptr_meta"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79"
+checksum = "743da816b98c921cdbe8628ef7381b76f25ecf4da599fc80aca90eae7ef70cc0"
dependencies = [
"ptr_meta_derive",
]
[[package]]
name = "ptr_meta_derive"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1"
+checksum = "1c8d9ca532f185d5d4db7a7c9d51420b452168ea1c2b913953281bd6fe1fcbd0"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
@@ -4300,7 +4430,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"tokio",
"tracing",
"web-time",
@@ -4308,9 +4438,9 @@ dependencies = [
[[package]]
name = "quinn-proto"
-version = "0.11.16"
+version = "0.11.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83"
dependencies = [
"aws-lc-rs",
"bytes",
@@ -4323,7 +4453,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
@@ -4340,7 +4470,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4366,9 +4496,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rancor"
-version = "0.1.2"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daff8b7b3ccf5f7ba270b3e7a0a4d4c701c5797e38dec27c7e2c3dbb830fed1c"
+checksum = "9b534442d0fcdb55d66f373d9cac6d33b6293a2335bc2136dbd06ce0e87d2572"
dependencies = [
"ptr_meta",
]
@@ -4448,9 +4578,9 @@ dependencies = [
[[package]]
name = "rcgen"
-version = "0.14.8"
+version = "0.14.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055"
+checksum = "8774e05a7d0de114588e6a28fe7e71694b82614ed569d86d8b389dfbc98b8ad8"
dependencies = [
"aws-lc-rs",
"pem",
@@ -4466,7 +4596,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
]
[[package]]
@@ -4477,27 +4607,27 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.17",
"libredox",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
]
[[package]]
name = "ref-cast"
-version = "1.0.26"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d"
+checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
-version = "1.0.26"
+version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
+checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -4514,9 +4644,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.16"
+version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
@@ -4552,11 +4682,11 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.13.4"
+version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029"
dependencies = [
- "base64",
+ "base64 0.23.1",
"bytes",
"futures-core",
"futures-util",
@@ -4607,14 +4737,14 @@ dependencies = [
[[package]]
name = "rkyv"
-version = "0.8.17"
+version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "815cc8a37159a463064825246cadb07961e25cd9885908606f6d08a98d8f8874"
+checksum = "d9776093b7ca170454ab1406954f7b7d97a57c51dc6c0642957fb2ef25c2d399"
dependencies = [
"bytecheck",
"bytes",
"hashbrown 0.17.1",
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"munge",
"ptr_meta",
"rancor",
@@ -4626,13 +4756,13 @@ dependencies = [
[[package]]
name = "rkyv_derive"
-version = "0.8.17"
+version = "0.8.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0ed1a78a1b19d184b0daa629dd9a024573173ec7d485b287cb369fb3607cc1c"
+checksum = "1c25ef604ac7dd839d44d64648952ea23c97866f124ff671b0ed2cf3ad9bb06e"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
@@ -4641,7 +4771,7 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"once_cell",
"serde",
"serde_derive",
@@ -4662,9 +4792,9 @@ dependencies = [
[[package]]
name = "rstest"
-version = "0.26.1"
+version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49"
+checksum = "948203e6d13b83e51a90d7cf236dd58a0065f23f4b902f00f306e7eb2bd09b9c"
dependencies = [
"futures-timer",
"futures-util",
@@ -4673,9 +4803,9 @@ dependencies = [
[[package]]
name = "rstest_macros"
-version = "0.26.1"
+version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0"
+checksum = "a8d92eaf7b6e51e12471d71ddc72608e7151573de44099aacfbb1128177c0da4"
dependencies = [
"cfg-if",
"glob",
@@ -4691,12 +4821,12 @@ dependencies = [
[[package]]
name = "rtoolbox"
-version = "0.0.5"
+version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844"
+checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4711,9 +4841,9 @@ dependencies = [
[[package]]
name = "rust_decimal"
-version = "1.42.1"
+version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a"
+checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a"
dependencies = [
"arrayvec",
"num-traits",
@@ -4755,7 +4885,7 @@ version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"errno",
"libc",
"linux-raw-sys 0.4.15",
@@ -4768,11 +4898,11 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"errno",
"libc",
"linux-raw-sys 0.12.1",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4831,7 +4961,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4899,11 +5029,11 @@ dependencies = [
"serde",
"serde_json",
"strum",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"time",
"tokio-util",
"utoipa",
- "zstd",
+ "zstd 0.14.0",
]
[[package]]
@@ -4949,16 +5079,16 @@ dependencies = [
"serde",
"serde_json",
"serial_test",
- "sha2 0.11.0",
+ "sha2",
"strum",
"tabled",
"tempfile",
"terminal_size",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"tikv-jemallocator",
"tokio",
"tokio-stream",
- "toml 1.1.4+spec-1.1.0",
+ "toml 1.1.6+spec-1.1.0",
"tracing",
"tracing-subscriber",
"uuid",
@@ -4985,7 +5115,7 @@ dependencies = [
"serde",
"serde_json",
"strum",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"time",
"utoipa",
]
@@ -5007,7 +5137,7 @@ dependencies = [
"eyre",
"futures",
"http 1.5.0",
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"itertools 0.15.0",
"parking_lot",
"prometheus",
@@ -5025,13 +5155,13 @@ dependencies = [
"serde_json",
"slatedb",
"strum",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"tikv-jemallocator",
"time",
"tokio",
"tokio-util",
"tower",
- "tower-http 0.7.0",
+ "tower-http 0.7.1",
"tracing",
"tracing-subscriber",
"utoipa",
@@ -5080,7 +5210,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"test-context",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"time",
"tokio",
"tokio-muxt",
@@ -5106,7 +5236,7 @@ dependencies = [
"s2-common",
"secrecy",
"serde",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
]
[[package]]
@@ -5116,7 +5246,7 @@ dependencies = [
"reqwest",
"s2-sdk",
"testcontainers",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"tokio",
]
@@ -5172,7 +5302,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -5192,20 +5322,20 @@ dependencies = [
[[package]]
name = "secret-service"
-version = "5.1.0"
+version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a62d7f86047af0077255a29494136b9aaaf697c76ff70b8e49cded4e2623c14"
+checksum = "5107b24b91445dd2aa449a258a1807b63240942157292354dc5bfdbeb8bc6db8"
dependencies = [
- "aes 0.8.4",
+ "aes",
"cbc",
"futures-util",
- "generic-array",
- "getrandom 0.2.17",
+ "getrandom 0.4.3",
"hkdf",
+ "hybrid-array",
"num",
"once_cell",
"serde",
- "sha2 0.10.9",
+ "sha2",
"zbus",
]
@@ -5215,7 +5345,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -5288,7 +5418,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -5299,7 +5429,7 @@ checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -5308,7 +5438,7 @@ version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"itoa",
"memchr",
"serde",
@@ -5335,7 +5465,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -5370,16 +5500,17 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.21.0"
+version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
+checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4"
dependencies = [
- "base64",
+ "base64 0.23.1",
"bs58",
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
+ "jiff",
"schemars 0.9.0",
"schemars 1.2.2",
"serde_core",
@@ -5390,14 +5521,14 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.21.0"
+version = "3.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
+checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc"
dependencies = [
- "darling 0.23.0",
+ "darling 0.24.1",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
@@ -5406,7 +5537,7 @@ version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"itoa",
"ryu",
"serde",
@@ -5435,7 +5566,7 @@ checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -5449,17 +5580,6 @@ dependencies = [
"digest 0.10.7",
]
-[[package]]
-name = "sha2"
-version = "0.10.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
-dependencies = [
- "cfg-if",
- "cpufeatures 0.2.17",
- "digest 0.10.7",
-]
-
[[package]]
name = "sha2"
version = "0.11.0"
@@ -5467,7 +5587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
- "cpufeatures 0.3.0",
+ "cpufeatures 0.3.1",
"digest 0.11.3",
]
@@ -5532,15 +5652,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "slatedb"
-version = "0.15.0"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35ca56b01922b15aa69fe3abb62cadc985d86032c9647e4606e211c4da751a76"
+checksum = "fb40332f41e231926df3a0ef742c05316b43368ba4b520433d2a1eba1ab00f0f"
dependencies = [
"async-channel",
"async-trait",
"atomic",
"backon",
- "bitflags",
+ "bitflags 2.13.2",
"bytes",
"chrono",
"crc32fast",
@@ -5574,14 +5694,14 @@ dependencies = [
"url",
"uuid",
"walkdir",
- "zstd",
+ "zstd 0.13.3",
]
[[package]]
name = "slatedb-common"
-version = "0.15.0"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0aa8de522ff46a0f9b5a66f45e650d125421d4d91990933591092f9d010c40d1"
+checksum = "6b77f238f348e95e1653a5235f880ac703011b82582bd556f2aba405abf8180e"
dependencies = [
"chrono",
"log",
@@ -5595,9 +5715,9 @@ dependencies = [
[[package]]
name = "slatedb-txn-obj"
-version = "0.15.0"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d85fd9c0c86dd4954524fa8238d0548bd80f4a9a66983cbde3728402d339993e"
+checksum = "1ad63b6c250219e26d71f497820f970274c301f896366daa56915f2a05df0e0a"
dependencies = [
"async-trait",
"bytes",
@@ -5618,9 +5738,9 @@ checksum = "88414a5ca1f85d82cc34471e975f0f74f6aa54c40f062efa42c0080e7f763f81"
[[package]]
name = "smallvec"
-version = "1.15.2"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
[[package]]
name = "socket2"
@@ -5746,9 +5866,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "3.0.3"
+version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
dependencies = [
"proc-macro2",
"quote",
@@ -5791,9 +5911,9 @@ dependencies = [
[[package]]
name = "tabled"
-version = "0.21.0"
+version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5dc662e6da844ad6e428ad16b57967c9d33c82e16bb1c258326c0c078605dff"
+checksum = "2d2596a104db1900b943f97d793a6c99addca918c4525c999f751a0f17d472eb"
dependencies = [
"papergrid",
"tabled_derive",
@@ -5802,12 +5922,11 @@ dependencies = [
[[package]]
name = "tabled_derive"
-version = "0.11.0"
+version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ea5d1b13ca6cff1f9231ffd62f15eefd72543dab5e468735f1a456728a02846"
+checksum = "7dca1937322a1e892b1a65f6a6736183bb0a29d3b4234d1d53bc436dff9beb76"
dependencies = [
"heck 0.5.0",
- "proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.119",
@@ -5823,7 +5942,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix 1.1.4",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5833,7 +5952,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
dependencies = [
"rustix 1.1.4",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5865,9 +5984,9 @@ dependencies = [
[[package]]
name = "testcontainers"
-version = "0.27.3"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e"
+checksum = "6e2bbe381afaaa58ea610c5fc3ffb2184063a32b3e358a179f0b4865dd59934a"
dependencies = [
"astral-tokio-tar",
"async-trait",
@@ -5887,7 +6006,7 @@ dependencies = [
"serde",
"serde_json",
"serde_with",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"tokio",
"tokio-stream",
"tokio-util",
@@ -5905,11 +6024,11 @@ dependencies = [
[[package]]
name = "textwrap"
-version = "0.16.2"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+checksum = "b81c0cb5fce14f53e49c1d4da0c508334ff12040221bb8ab01b2dabd91d04b6e"
dependencies = [
- "unicode-linebreak",
+ "icu_segmenter",
"unicode-width 0.2.2",
]
@@ -5924,11 +6043,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.19"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
+checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
- "thiserror-impl 2.0.19",
+ "thiserror-impl 2.0.20",
]
[[package]]
@@ -5944,13 +6063,13 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.19"
+version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
+checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -5984,9 +6103,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.54"
+version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
@@ -6023,19 +6142,20 @@ dependencies = [
[[package]]
name = "tinystr"
-version = "0.8.3"
+version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
+ "serde_core",
"zerovec",
]
[[package]]
name = "tinyvec"
-version = "1.12.0"
+version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"
dependencies = [
"tinyvec_macros",
]
@@ -6071,7 +6191,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
dependencies = [
"proc-macro2",
"quote",
- "syn 3.0.3",
+ "syn 3.0.5",
]
[[package]]
@@ -6086,9 +6206,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
-version = "0.26.4"
+version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67"
dependencies = [
"rustls",
"tokio",
@@ -6157,11 +6277,11 @@ dependencies = [
[[package]]
name = "toml"
-version = "1.1.4+spec-1.1.0"
+version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5"
+checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"serde_core",
"serde_spanned 1.1.1",
"toml_datetime 1.1.1+spec-1.1.0",
@@ -6194,7 +6314,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
@@ -6204,11 +6324,11 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.25.13+spec-1.1.0"
+version = "0.25.15+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
+checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow 1.0.4",
@@ -6243,7 +6363,7 @@ checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
dependencies = [
"async-trait",
"axum",
- "base64",
+ "base64 0.22.1",
"bytes",
"h2",
"http 1.5.0",
@@ -6283,7 +6403,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"pin-project-lite",
"slab",
"sync_wrapper",
@@ -6300,7 +6420,7 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
- "bitflags",
+ "bitflags 2.13.2",
"bytes",
"futures-util",
"http 1.5.0",
@@ -6314,12 +6434,12 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.7.0"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233"
+checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c"
dependencies = [
"async-compression",
- "bitflags",
+ "bitflags 2.13.2",
"bytes",
"futures-core",
"http 1.5.0",
@@ -6416,12 +6536,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "twox-hash"
-version = "2.1.3"
+version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8464ec13c3691491391d9fce00f6416c9a48e46972f72d7865688be2080192c9"
-dependencies = [
- "rand 0.10.2",
-]
+checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a"
[[package]]
name = "typed-path"
@@ -6490,12 +6607,6 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
-[[package]]
-name = "unicode-linebreak"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
-
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
@@ -6548,33 +6659,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
-[[package]]
-name = "ureq"
-version = "3.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
-dependencies = [
- "base64",
- "log",
- "percent-encoding",
- "rustls",
- "rustls-pki-types",
- "ureq-proto",
- "utf8-zero",
-]
-
-[[package]]
-name = "ureq-proto"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
-dependencies = [
- "base64",
- "http 1.5.0",
- "httparse",
- "log",
-]
-
[[package]]
name = "url"
version = "2.5.8"
@@ -6594,12 +6678,6 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
-[[package]]
-name = "utf8-zero"
-version = "0.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
-
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -6617,7 +6695,7 @@ name = "utoipa"
version = "5.4.0"
source = "git+https://github.com/infiniteregrets/utoipa?rev=82bcb28a792ba9a0d29963827ec473823099fc94#82bcb28a792ba9a0d29963827ec473823099fc94"
dependencies = [
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"serde",
"serde_json",
"utoipa-gen",
@@ -6635,9 +6713,9 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.24.0"
+version = "1.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
+checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce"
dependencies = [
"getrandom 0.4.3",
"js-sys",
@@ -6709,9 +6787,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
-version = "0.2.126"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
dependencies = [
"cfg-if",
"once_cell",
@@ -6722,9 +6800,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.76"
+version = "0.4.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
+checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -6732,9 +6810,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.126"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -6742,22 +6820,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.126"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.126"
+version = "0.2.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
dependencies = [
"unicode-ident",
]
@@ -6777,9 +6855,9 @@ dependencies = [
[[package]]
name = "web-sys"
-version = "0.3.103"
+version = "0.3.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -6826,7 +6904,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -7122,9 +7200,9 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "writeable"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
[[package]]
name = "x509-parser"
@@ -7140,7 +7218,7 @@ dependencies = [
"nom",
"oid-registry",
"rusticata-macros",
- "thiserror 2.0.19",
+ "thiserror 2.0.20",
"time",
]
@@ -7168,9 +7246,9 @@ checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6"
[[package]]
name = "yaml-rust2"
-version = "0.11.0"
+version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e"
+checksum = "b36710ce3a279cfce8465dbab826f161675a262950b922cb2c3663852dfe9eb0"
dependencies = [
"arraydeque",
"encoding_rs",
@@ -7218,9 +7296,9 @@ dependencies = [
[[package]]
name = "zbus"
-version = "5.18.0"
+version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
dependencies = [
"async-broadcast",
"async-executor",
@@ -7253,9 +7331,9 @@ dependencies = [
[[package]]
name = "zbus-secret-service-keyring-store"
-version = "1.0.0"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ccede190ba363386a24e8021c7f3848393976609ec9f5d1f8c6c09ef37075b4"
+checksum = "74801d001b9e7729adb4f1825b67b398185fed424749aa3d8bacf70417137d9a"
dependencies = [
"keyring-core",
"secret-service",
@@ -7264,14 +7342,14 @@ dependencies = [
[[package]]
name = "zbus_macros"
-version = "5.18.0"
+version = "5.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -7289,19 +7367,28 @@ dependencies = [
]
[[package]]
-name = "zerocopy"
-version = "0.8.55"
+name = "zcheapstr"
+version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.55"
+version = "0.8.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb"
+checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
dependencies = [
"proc-macro2",
"quote",
@@ -7337,21 +7424,23 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]]
name = "zerotrie"
-version = "0.2.4"
+version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
+ "zerovec",
]
[[package]]
name = "zerovec"
-version = "0.11.6"
+version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
+ "serde",
"yoke",
"zerofrom",
"zerovec-derive",
@@ -7359,13 +7448,13 @@ dependencies = [
[[package]]
name = "zerovec-derive"
-version = "0.11.3"
+version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
]
[[package]]
@@ -7376,7 +7465,7 @@ checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
dependencies = [
"crc32fast",
"flate2",
- "indexmap 2.14.0",
+ "indexmap 2.14.2",
"memchr",
"typed-path",
"zopfli",
@@ -7384,9 +7473,9 @@ dependencies = [
[[package]]
name = "zlib-rs"
-version = "0.6.6"
+version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5"
+checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
[[package]]
name = "zmij"
@@ -7412,23 +7501,41 @@ version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
- "zstd-safe",
+ "zstd-safe 7.3.0",
+]
+
+[[package]]
+name = "zstd"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf06bd8162af0734b344780deb55b42a2429ae430870d13fcc12f238e880fe6e"
+dependencies = [
+ "zstd-safe 8.0.0",
]
[[package]]
name = "zstd-safe"
-version = "7.2.4"
+version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae42c0555055784c70058d19ba8e275528e8a99a706684868ace5da4e716a4ab"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
-version = "2.0.16+zstd.1.5.7"
+version = "2.1.0+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0"
dependencies = [
"cc",
"pkg-config",
@@ -7436,40 +7543,41 @@ dependencies = [
[[package]]
name = "zvariant"
-version = "5.13.1"
+version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911"
+checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 1.0.4",
+ "zcheapstr",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
-version = "5.13.1"
+version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12"
+checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
- "syn 2.0.119",
+ "syn 3.0.5",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
-version = "3.5.0"
+version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
dependencies = [
"proc-macro2",
"quote",
"serde",
- "syn 2.0.119",
+ "syn 3.0.5",
"winnow 1.0.4",
]
diff --git a/Cargo.toml b/Cargo.toml
index d6801da5..31b11445 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,14 +29,14 @@ axum-server = "0.8"
base64ct = "1.8"
blake3 = "1.8"
bytes = "1"
-bytesize = "2.6"
+bytesize = "2.7"
clap = "4.6"
color-print = "0.3"
colored = "3.1"
compact_str = "0.10"
config = "0.15"
dashmap = "6.2"
-dirs = "6.0"
+dirs = "7.0"
enumset = "1.1"
eyre = "0.6"
flate2 = "1.1"
@@ -49,8 +49,8 @@ humantime = "2.4"
indexmap = "2.14"
indicatif = "0.18"
itertools = "0.15"
-json_to_table = "0.13"
-keyring = "4.1"
+json_to_table = "0.14"
+keyring = "4.2"
miette = "7.6"
mime = "0.3"
parking_lot = "0.12"
@@ -63,7 +63,7 @@ rcgen = { version = "0.14", default-features = false, features = ["crypto", "pem
reqwest = { version = "0.13", default-features = false, features = ["rustls"] }
rkyv = "0.8"
rpassword = "7.5"
-rstest = "0.26"
+rstest = "0.27"
rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12"] }
s2-api = { path = "api", version = "0.31" }
s2-common = { path = "common", version = "0.41" }
@@ -76,11 +76,11 @@ secrecy = "0.10.3"
semver = "1.0"
serde = "1.0"
serde_json = "1.0"
-slatedb = "0.15.0"
+slatedb = "0.16.0"
strum = "0.28"
-tabled = "0.21"
+tabled = "0.22"
tempfile = "3.27"
-testcontainers = "0.27"
+testcontainers = "0.28"
thiserror = "2.0"
tikv-jemallocator = { version = "0.7", features = ["unprefixed_malloc_on_supported_platforms"] }
time = "0.3"
@@ -92,9 +92,9 @@ tower-http = "0.7"
tracing = "0.1"
tracing-subscriber = "0.3"
utoipa = "=5.4"
-uuid = "1.24"
+uuid = "1.26"
xxhash-rust = "0.8"
-zstd = "0.13"
+zstd = "0.14"
[patch.crates-io]
utoipa = { git = "https://github.com/infiniteregrets/utoipa", rev = "82bcb28a792ba9a0d29963827ec473823099fc94" }
diff --git a/deny.toml b/deny.toml
index 48b28369..24aac6b4 100644
--- a/deny.toml
+++ b/deny.toml
@@ -6,8 +6,6 @@ ignore = [
# Transitive deps via slatedb/foyer — not actionable.
"RUSTSEC-2024-0436", # paste unmaintained
"RUSTSEC-2025-0141", # bincode unmaintained
- # Transitive dep via tabled_derive — no fixed release available.
- "RUSTSEC-2026-0173", # proc-macro-error2 unmaintained
# Transitive dep via object_store (slatedb) — fix requires object_store >=0.13.
"RUSTSEC-2026-0194", # quick-xml quadratic attribute check
"RUSTSEC-2026-0195", # quick-xml unbounded namespace allocation
diff --git a/lite/src/backend/basins.rs b/lite/src/backend/basins.rs
index c37511af..211c056e 100644
--- a/lite/src/backend/basins.rs
+++ b/lite/src/backend/basins.rs
@@ -11,11 +11,16 @@ use slatedb::{
};
use time::OffsetDateTime;
-use super::{Backend, bgtasks::BgtaskTrigger, store::db_txn_get};
+use super::{
+ Backend,
+ bgtasks::BgtaskTrigger,
+ store::{db_txn_commit_durable, db_txn_get},
+};
use crate::backend::{
error::{
BasinAlreadyExistsError, BasinDeletionPendingError, BasinNotFoundError, DeleteBasinError,
GetBasinConfigError, ListBasinsError, ProvisionBasinError, ReconfigureBasinError,
+ StorageError,
},
kv,
};
@@ -63,6 +68,8 @@ impl Backend {
Ok(Page::new(basins, has_more))
}
+ /// Any outcome asserting the basin exists — `Created`, `Updated`, `Noop`,
+ /// or `BasinAlreadyExists` — is readable at `DurabilityLevel::Remote`.
pub async fn provision_basin(
&self,
basin: BasinName,
@@ -73,7 +80,13 @@ impl Backend {
let txn = self.db.begin(IsolationLevel::SerializableSnapshot).await?;
- let existing_meta = db_txn_get(&txn, &meta_key, kv::basin_meta::deser_value).await?;
+ // A transaction can see metadata that has not been flushed yet.
+ let existing_entry = txn.get_key_value(&meta_key).await?;
+ let existing_seq = existing_entry.as_ref().map(|kv| kv.seq);
+ let existing_meta = existing_entry
+ .map(|kv| kv::basin_meta::deser_value(kv.value))
+ .transpose()
+ .map_err(StorageError::from)?;
if let Some(existing_meta) = &existing_meta
&& existing_meta.deleted_at.is_some()
{
@@ -85,7 +98,7 @@ impl Backend {
let new_creation_idempotency_key = request_token
.as_ref()
.map(|req_token| creation_idempotency_key(req_token, &config));
- return if new_creation_idempotency_key.is_some()
+ let result = if new_creation_idempotency_key.is_some()
&& existing.creation_idempotency_key == new_creation_idempotency_key
{
Ok(ProvisionResult::Noop(BasinInfo {
@@ -97,6 +110,10 @@ impl Backend {
} else {
Err(BasinAlreadyExistsError { basin }.into())
};
+ drop(txn);
+ self.await_durable_seq(existing_seq.expect("existing meta was read"))
+ .await?;
+ return result;
}
(Some(existing), ProvisionMode::Ensure) => {
let meta = kv::basin_meta::BasinMeta {
@@ -130,11 +147,15 @@ impl Backend {
}),
};
- if !matches!(&outcome, ProvisionResult::Noop(_)) {
+ if matches!(&outcome, ProvisionResult::Noop(_)) {
+ drop(txn);
+ self.await_durable_seq(existing_seq.expect("noop implies existing meta"))
+ .await?;
+ } else {
let meta = outcome.inner();
txn.put(&meta_key, kv::basin_meta::ser_value(meta))?;
- txn.commit().await?;
+ db_txn_commit_durable(txn).await?;
}
Ok(outcome.map(|meta| BasinInfo {
@@ -179,7 +200,7 @@ impl Backend {
txn.put(&meta_key, kv::basin_meta::ser_value(&meta))?;
- txn.commit().await?;
+ db_txn_commit_durable(txn).await?;
Ok(meta.config)
}
@@ -197,7 +218,7 @@ impl Backend {
kv::basin_deletion_pending::ser_key(&basin),
kv::basin_deletion_pending::ser_value(&StreamNameStartAfter::default()),
)?;
- txn.commit().await?;
+ db_txn_commit_durable(txn).await?;
self.bgtask_trigger(BgtaskTrigger::BasinDeletion);
}
Ok(())
diff --git a/lite/src/backend/bgtasks/basin_deletion.rs b/lite/src/backend/bgtasks/basin_deletion.rs
index e11563be..8fdda524 100644
--- a/lite/src/backend/bgtasks/basin_deletion.rs
+++ b/lite/src/backend/bgtasks/basin_deletion.rs
@@ -122,7 +122,7 @@ impl Backend {
kv::basin_deletion_pending::ser_key(basin),
kv::basin_deletion_pending::ser_value(cursor),
);
- self.db.write(batch).await?;
+ self.db.write(batch).await?.await_durable().await?;
Ok(())
}
@@ -131,7 +131,7 @@ impl Backend {
let mut batch = WriteBatch::new();
batch.delete(kv::basin_meta::ser_key(basin));
batch.delete(kv::basin_deletion_pending::ser_key(basin));
- self.db.write(batch).await?;
+ self.db.write(batch).await?.await_durable().await?;
Ok(())
}
}
@@ -149,7 +149,7 @@ mod tests {
use time::OffsetDateTime;
use super::super::tests::test_backend;
- use crate::backend::{Backend, kv};
+ use crate::backend::{Backend, kv, test_util::DbWriteTestExt as _};
fn basin_meta(deleted_at: Option) -> kv::basin_meta::BasinMeta {
kv::basin_meta::BasinMeta {
@@ -185,16 +185,16 @@ mod tests {
kv::basin_meta::ser_key(basin),
kv::basin_meta::ser_value(&basin_meta(Some(OffsetDateTime::now_utc()))),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::basin_deletion_pending::ser_key(basin),
kv::basin_deletion_pending::ser_value(&StreamNameStartAfter::default()),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
}
async fn seed_tombstoned_streams(backend: &Backend, basin: &BasinName, count: usize) {
@@ -207,7 +207,7 @@ mod tests {
kv::stream_meta::ser_value(&stream_meta(Some(deleted_at))),
);
}
- backend.db.write(batch).await.unwrap();
+ backend.db.write(batch).assert_durable().await;
}
#[tokio::test]
@@ -221,16 +221,16 @@ mod tests {
kv::basin_meta::ser_key(&basin),
kv::basin_meta::ser_value(&basin_meta(Some(OffsetDateTime::now_utc()))),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::basin_deletion_pending::ser_key(&basin),
kv::basin_deletion_pending::ser_value(&StreamNameStartAfter::default()),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let has_more = backend.clone().tick_basin_deletion().await.unwrap();
assert!(!has_more);
@@ -266,8 +266,8 @@ mod tests {
kv::stream_meta::ser_key(&basin, &stream),
kv::stream_meta::ser_value(&stream_meta(None)),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let has_more = backend.clone().tick_basin_deletion().await.unwrap();
assert!(!has_more);
@@ -384,16 +384,16 @@ mod tests {
kv::basin_meta::ser_key(&basin),
kv::basin_meta::ser_value(&basin_meta(Some(OffsetDateTime::now_utc()))),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::basin_deletion_pending::ser_key(&basin),
kv::basin_deletion_pending::ser_value(&cursor),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
// First tick resets cursor from past-end back to the beginning.
let has_more = backend.clone().tick_basin_deletion().await.unwrap();
@@ -435,8 +435,8 @@ mod tests {
kv::stream_meta::ser_key(&basin, &stream),
kv::stream_meta::ser_value(&stream_meta(Some(deleted_at))),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let has_more = backend.clone().tick_basin_deletion().await.unwrap();
assert!(!has_more);
@@ -474,8 +474,8 @@ mod tests {
kv::stream_meta::ser_key(&basin, &stream),
kv::stream_meta::ser_value(&stream_meta(Some(deleted_at))),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
// First tick: blocked by tombstoned stream.
let has_more = backend.clone().tick_basin_deletion().await.unwrap();
@@ -493,8 +493,8 @@ mod tests {
backend
.db
.delete(kv::stream_meta::ser_key(&basin, &stream))
- .await
- .unwrap();
+ .assert_durable()
+ .await;
// Second tick: no streams remain, completes.
let has_more = backend.clone().tick_basin_deletion().await.unwrap();
diff --git a/lite/src/backend/bgtasks/stream_doe.rs b/lite/src/backend/bgtasks/stream_doe.rs
index d09b108d..91d3a0ab 100644
--- a/lite/src/backend/bgtasks/stream_doe.rs
+++ b/lite/src/backend/bgtasks/stream_doe.rs
@@ -138,7 +138,7 @@ impl Backend {
for entry in pending {
batch.delete(kv::stream_doe_deadline::ser_key(entry.deadline, stream_id));
}
- self.db.write(batch).await?;
+ self.db.write(batch).await?.await_durable().await?;
Ok(())
}
@@ -170,6 +170,8 @@ impl Backend {
kv::stream_doe_deadline::ser_key(deadline, stream_id),
kv::stream_doe_deadline::ser_value(min_age),
)
+ .await?
+ .await_durable()
.await?;
Ok(())
}
@@ -194,7 +196,7 @@ mod tests {
use super::{super::tests::test_backend, PendingDoeBatch, TimestampSecs};
use crate::{
- backend::{Backend, kv},
+ backend::{Backend, kv, test_util::DbWriteTestExt as _},
stream_id::StreamId,
};
@@ -237,24 +239,24 @@ mod tests {
creation_idempotency_key: None,
}),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_meta::ser_key(basin, stream),
kv::stream_meta::ser_value(&meta),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_id_mapping::ser_key(stream_id),
kv::stream_id_mapping::ser_value(basin, stream),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
stream_id
}
@@ -289,8 +291,8 @@ mod tests {
backend
.db
.put(key.clone(), kv::stream_tail_position::ser_value(position))
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let kv = backend
.db
.get_key_value(key)
@@ -353,8 +355,8 @@ mod tests {
kv::stream_doe_deadline::ser_key(deadline, stream_id),
kv::stream_doe_deadline::ser_value(MIN_AGE),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
process_pending_stream_doe_at(&backend, stream_id, deadline).await;
@@ -394,8 +396,8 @@ mod tests {
kv::stream_doe_deadline::ser_key(deadline, stream_id),
kv::stream_doe_deadline::ser_value(min_age),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
process_pending_stream_doe_at(&backend, stream_id, deadline).await;
@@ -445,8 +447,8 @@ mod tests {
kv::stream_doe_deadline::ser_key(deadline, stream_id),
kv::stream_doe_deadline::ser_value(MIN_AGE),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
process_pending_stream_doe_at(&backend, stream_id, deadline).await;
@@ -491,16 +493,16 @@ mod tests {
kv::stream_record_timestamp::ser_key(stream_id, pos),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_doe_deadline::ser_key(deadline, stream_id),
kv::stream_doe_deadline::ser_value(MIN_AGE),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let has_more = backend.clone().tick_stream_doe().await.unwrap();
assert!(!has_more);
@@ -549,8 +551,8 @@ mod tests {
kv::stream_doe_deadline::ser_key(deadline, stream_id),
kv::stream_doe_deadline::ser_value(MIN_AGE),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let has_more = backend.clone().tick_stream_doe().await.unwrap();
assert!(!has_more);
@@ -594,16 +596,16 @@ mod tests {
kv::stream_doe_deadline::ser_key(deadline_a, stream_id),
kv::stream_doe_deadline::ser_value(MIN_AGE),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_doe_deadline::ser_key(deadline_b, stream_id),
kv::stream_doe_deadline::ser_value(MIN_AGE),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let page = backend.list_pending_stream_doe(far_future).await.unwrap();
assert!(!page.has_more);
@@ -711,16 +713,16 @@ mod tests {
kv::stream_tail_position::ser_key(stream_id),
kv::stream_tail_position::ser_value(pos),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_record_timestamp::ser_key(stream_id, pos),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let min_age = Duration::from_secs(30);
let expected_delay =
@@ -776,8 +778,8 @@ mod tests {
kv::stream_doe_deadline::ser_key(existing_deadline, stream_id),
kv::stream_doe_deadline::ser_value(initial_min_age),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.reconfigure_stream(
diff --git a/lite/src/backend/bgtasks/stream_trim.rs b/lite/src/backend/bgtasks/stream_trim.rs
index 2a1f332d..8cee7a21 100644
--- a/lite/src/backend/bgtasks/stream_trim.rs
+++ b/lite/src/backend/bgtasks/stream_trim.rs
@@ -9,7 +9,12 @@ use slatedb::{
use tracing::instrument;
use crate::{
- backend::{Backend, error::StorageError, kv, store::db_txn_get},
+ backend::{
+ Backend,
+ error::StorageError,
+ kv,
+ store::{db_txn_commit_durable, db_txn_get},
+ },
stream_id::StreamId,
};
@@ -100,13 +105,13 @@ impl Backend {
batch.delete(kv::stream_record_data::ser_key(stream_id, pos));
batch_size += 1;
if batch_size >= DELETE_BATCH_SIZE {
- self.db.write(batch).await?;
+ self.db.write(batch).await?.await_durable().await?;
batch = WriteBatch::new();
batch_size = 0;
}
}
if batch_size > 0 {
- self.db.write(batch).await?;
+ self.db.write(batch).await?.await_durable().await?;
}
Ok(has_remaining_records)
}
@@ -147,7 +152,7 @@ impl Backend {
txn.delete(kv::stream_tail_position::ser_key(stream_id))?;
txn.delete(kv::stream_fencing_token::ser_key(stream_id))?;
}
- txn.commit().await?;
+ db_txn_commit_durable(txn).await?;
Ok(())
}
}
@@ -170,7 +175,10 @@ mod tests {
use time::OffsetDateTime;
use super::super::tests::test_backend;
- use crate::{backend::kv, stream_id::StreamId};
+ use crate::{
+ backend::{kv, test_util::DbWriteTestExt as _},
+ stream_id::StreamId,
+ };
fn test_record() -> Metered {
let record = Record::try_from_parts(vec![], Bytes::from_static(b"trim-test")).unwrap();
@@ -198,16 +206,16 @@ mod tests {
kv::stream_record_data::ser_key(stream_id, pos),
kv::stream_record_data::ser_value(metered.as_ref()),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_record_timestamp::ser_key(stream_id, pos),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
}
backend
@@ -216,8 +224,8 @@ mod tests {
kv::stream_trim_point::ser_key(stream_id),
kv::stream_trim_point::ser_value(trim_point(3)),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend.clone().tick_stream_trim().await.unwrap();
@@ -275,16 +283,16 @@ mod tests {
kv::stream_meta::ser_key(&basin, &stream),
kv::stream_meta::ser_value(&meta),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_id_mapping::ser_key(stream_id),
kv::stream_id_mapping::ser_value(&basin, &stream),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
@@ -294,8 +302,8 @@ mod tests {
timestamp: 1234,
}),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let token = FencingToken::from_str("token-1").unwrap();
backend
.db
@@ -303,8 +311,8 @@ mod tests {
kv::stream_fencing_token::ser_key(stream_id),
kv::stream_fencing_token::ser_value(&token),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
for seq in 0..3 {
let pos = StreamPosition {
@@ -317,16 +325,16 @@ mod tests {
kv::stream_record_data::ser_key(stream_id, pos),
kv::stream_record_data::ser_value(metered.as_ref()),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_record_timestamp::ser_key(stream_id, pos),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
}
backend
@@ -335,8 +343,8 @@ mod tests {
kv::stream_trim_point::ser_key(stream_id),
kv::stream_trim_point::ser_value(trim_point(SeqNum::MAX)),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend.clone().tick_stream_trim().await.unwrap();
@@ -402,8 +410,8 @@ mod tests {
kv::stream_trim_point::ser_key(stream_id),
kv::stream_trim_point::ser_value(trim_point(10)),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.finalize_trim(stream_id, trim_point(5))
@@ -435,7 +443,7 @@ mod tests {
kv::stream_trim_point::ser_value(trim_point(1)),
);
}
- backend.db.write(batch).await.unwrap();
+ backend.db.write(batch).assert_durable().await;
let has_more = backend.clone().tick_stream_trim().await.unwrap();
assert!(has_more);
@@ -472,24 +480,24 @@ mod tests {
kv::stream_record_data::ser_key(stream_id, pos),
kv::stream_record_data::ser_value(metered.as_ref()),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_record_timestamp::ser_key(stream_id, pos),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_trim_point::ser_key(stream_id),
kv::stream_trim_point::ser_value(trim_point(1)),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend.clone().tick_stream_trim().await.unwrap();
@@ -532,16 +540,16 @@ mod tests {
kv::stream_record_data::ser_key(stream_id_a, pos_a),
kv::stream_record_data::ser_value(metered.as_ref()),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_record_timestamp::ser_key(stream_id_a, pos_a),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
let pos_b = StreamPosition {
seq_num: seq,
@@ -553,16 +561,16 @@ mod tests {
kv::stream_record_data::ser_key(stream_id_b, pos_b),
kv::stream_record_data::ser_value(metered.as_ref()),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
kv::stream_record_timestamp::ser_key(stream_id_b, pos_b),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
}
backend
@@ -571,8 +579,8 @@ mod tests {
kv::stream_trim_point::ser_key(stream_id_a),
kv::stream_trim_point::ser_value(trim_point(2)),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend.clone().tick_stream_trim().await.unwrap();
@@ -645,7 +653,7 @@ mod tests {
kv::stream_trim_point::ser_key(stream_id),
kv::stream_trim_point::ser_value(trim_point(total)),
);
- backend.db.write(batch).await.unwrap();
+ backend.db.write(batch).assert_durable().await;
backend.clone().tick_stream_trim().await.unwrap();
@@ -706,8 +714,8 @@ mod tests {
kv::stream_trim_point::ser_key(stream_id),
kv::stream_trim_point::ser_value(trim_point(5)),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.finalize_trim(stream_id, trim_point(5))
diff --git a/lite/src/backend/core.rs b/lite/src/backend/core.rs
index cf7558ad..e291833c 100644
--- a/lite/src/backend/core.rs
+++ b/lite/src/backend/core.rs
@@ -431,6 +431,7 @@ mod tests {
use time::OffsetDateTime;
use super::*;
+ use crate::backend::test_util::DbWriteTestExt as _;
async fn new_test_backend() -> Backend {
let object_store: Arc =
@@ -484,7 +485,7 @@ mod tests {
kv::stream_record_data::ser_key(stream_id, record_pos),
kv::stream_record_data::ser_value(metered_record.as_ref()),
);
- backend.db.write(wb).await.unwrap();
+ backend.db.write(wb).assert_durable().await;
backend
.start_streamer(StreamerGenerationId::next(), basin.clone(), stream.clone())
diff --git a/lite/src/backend/mod.rs b/lite/src/backend/mod.rs
index be8db24f..f3dd8acb 100644
--- a/lite/src/backend/mod.rs
+++ b/lite/src/backend/mod.rs
@@ -11,6 +11,9 @@ mod store;
mod streamer;
mod streams;
+#[cfg(test)]
+mod test_util;
+
mod append;
mod kv;
diff --git a/lite/src/backend/read.rs b/lite/src/backend/read.rs
index c4e0df3d..b0c8ece9 100644
--- a/lite/src/backend/read.rs
+++ b/lite/src/backend/read.rs
@@ -430,7 +430,9 @@ mod tests {
use super::*;
use crate::{
- backend::{FOLLOWER_MAX_LAG, kv, streamer::DORMANT_TIMEOUT},
+ backend::{
+ FOLLOWER_MAX_LAG, kv, streamer::DORMANT_TIMEOUT, test_util::DbWriteTestExt as _,
+ },
stream_id::StreamId,
};
@@ -505,8 +507,8 @@ mod tests {
),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
backend
.db
.put(
@@ -519,8 +521,8 @@ mod tests {
),
kv::stream_record_timestamp::ser_value(),
)
- .await
- .unwrap();
+ .assert_durable()
+ .await;
// Should find record in stream_a
let result = resolve_timestamp(&backend.db, stream_a, 500).await.unwrap();
@@ -582,7 +584,7 @@ mod tests {
let stream_id = StreamId::new(&basin, &stream);
let mut batch = WriteBatch::new();
batch.delete(kv::stream_record_data::ser_key(stream_id, ack.start));
- backend.db.write(batch).await.unwrap();
+ backend.db.write(batch).assert_durable().await;
let start = ReadStart {
from: ReadFrom::SeqNum(0),
@@ -924,7 +926,7 @@ mod tests {
delete_batch.delete(kv::stream_record_data::ser_key(stream_id, ack.start));
}
- backend.db.write(delete_batch).await.unwrap();
+ backend.db.write(delete_batch).assert_durable().await;
tokio::time::advance(wait + Duration::from_secs(1)).await;
tokio::task::yield_now().await;
diff --git a/lite/src/backend/store.rs b/lite/src/backend/store.rs
index 791bc561..30212f62 100644
--- a/lite/src/backend/store.rs
+++ b/lite/src/backend/store.rs
@@ -42,3 +42,11 @@ pub(super) async fn db_txn_get + Send, V>(
let value = txn.get(key).await?.map(deser).transpose()?;
Ok(value)
}
+
+/// Commit metadata changes and wait until remote reads can observe them.
+pub(super) async fn db_txn_commit_durable(txn: DbTransaction) -> Result<(), slatedb::Error> {
+ if let Some(handle) = txn.commit().await? {
+ handle.await_durable().await?;
+ }
+ Ok(())
+}
diff --git a/lite/src/backend/streamer.rs b/lite/src/backend/streamer.rs
index d8acb6a3..553705b1 100644
--- a/lite/src/backend/streamer.rs
+++ b/lite/src/backend/streamer.rs
@@ -28,7 +28,7 @@ use s2_storage::record::{
};
use slatedb::{
IterationOrder, WriteBatch,
- config::{PutOptions, ScanOptions, Ttl, WriteOptions},
+ config::{PutOptions, ScanOptions, Ttl},
};
use tokio::{
sync::{Semaphore, SemaphorePermit, broadcast, mpsc, oneshot},
@@ -1019,7 +1019,7 @@ async fn db_submit_append(
}: DbSubmitAppendOptions,
) -> Result {
let ttl = match retention {
- RetentionPolicy::Age(age) => Ttl::ExpireAfter(age.as_millis() as u64),
+ RetentionPolicy::Age(age) => Ttl::ExpireAfterMillis(age.as_millis() as u64),
RetentionPolicy::Infinite() => Ttl::NoExpiry,
};
let ttl_put_opts = PutOptions { ttl };
@@ -1058,11 +1058,8 @@ async fn db_submit_append(
kv::stream_tail_position::ser_key(stream_id),
kv::stream_tail_position::ser_value(next_pos(&records)),
);
- let write_opts = WriteOptions {
- await_durable: false,
- ..Default::default()
- };
- let write_handle = db.write_with_options(wb, &write_opts).await?;
+ // The durability notifier tracks this sequence and acknowledges the append after flush.
+ let write_handle = db.write(wb).await?;
Ok(InFlightAppend {
db_seq: write_handle.seqnum(),
records,
diff --git a/lite/src/backend/streams.rs b/lite/src/backend/streams.rs
index b59d8032..a9542c9e 100644
--- a/lite/src/backend/streams.rs
+++ b/lite/src/backend/streams.rs
@@ -15,7 +15,7 @@ use tracing::instrument;
use super::{
Backend,
- store::db_txn_get,
+ store::{db_txn_commit_durable, db_txn_get},
streamer::{TerminalTrimCondition, TerminalTrimOutcome, doe_arm_delay},
};
use crate::{
@@ -226,7 +226,7 @@ impl Backend {
)?;
}
- txn.commit().await?;
+ db_txn_commit_durable(txn).await?;
}
if let ProvisionResult::Updated(meta) = &outcome
@@ -333,7 +333,7 @@ impl Backend {
)?;
}
- txn.commit().await?;
+ db_txn_commit_durable(txn).await?;
if let Some(client) = self.streamer_client_if_active(&basin, &stream) {
client.advise_reconfig(meta.config.clone());
@@ -390,7 +390,7 @@ impl Backend {
if meta.deleted_at.is_none() {
meta.deleted_at = Some(OffsetDateTime::now_utc());
txn.put(&meta_key, kv::stream_meta::ser_value(&meta))?;
- txn.commit().await?;
+ db_txn_commit_durable(txn).await?;
}
Ok(())
}
diff --git a/lite/src/backend/test_util.rs b/lite/src/backend/test_util.rs
new file mode 100644
index 00000000..13994a23
--- /dev/null
+++ b/lite/src/backend/test_util.rs
@@ -0,0 +1,18 @@
+use std::future::Future;
+
+use slatedb::{Error, WriteHandle};
+
+/// Finish a fixture write and assert that remote reads can observe it.
+pub(super) trait DbWriteTestExt:
+ Future