Skip to content

Sessions: large databases' checkpoints in an object store (RESTATE_SQL_OBJECT_STORE) - #309

Merged
slinkydeveloper merged 2 commits into
mainfrom
object-storage
Nov 10, 2026
Merged

slinkydeveloper merged 2 commits into
mainfrom
object-storage

Conversation

@slinkydeveloper

@slinkydeveloper slinkydeveloper commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Session objects can now keep a large database's checkpoints in an object store (S3, GCS, Azure, or a directory) instead of in Restate. This upgrades shared-core to 8.5.0, which adds state::ObjectStoreL1 (restatedev/sdk-shared-core#115). It is the same change sdk-sessions makes for Node.

RESTATE_SQL_OBJECT_STORE=s3://my-bucket/sessions   # unset: every pack stays in Restate, as before
AWS_REGION=eu-central-1                            # plus keys, or IRSA on EKS
# MinIO, R2, ...: AWS_ENDPOINT=http://minio:9000 AWS_ALLOW_HTTP=true

Once a database's packs in Restate pass 4 MiB (compressed), a checkpoint uploads them to the store as one pack. Restate keeps the head, the tail, the manifests and the last few MiB, so a database under 4 MiB never touches the bucket. Each database gets its own directory under the URL's path (Ledger/a is _4cedger_2fa). Credentials come from the provider's own variables (AWS_*, GOOGLE_*, AZURE_*).

Two rules:

  • Same store everywhere: once a database has promoted, every process that serves it needs the same RESTATE_SQL_OBJECT_STORE, or its opens fail.
  • One cluster per path: give each Restate cluster its own path in the store.

A value that names no store this build can serve fails every open with its message.

Changes

  • Cargo.toml: shared-core 8.5.0 with object_store_aws, object_store_gcp and object_store_azure. The crypto feature is now aws_lc_rs instead of rust_crypto. The cloud backends need aws-lc, and with this switch the tunnel's TLS and request identity use aws-lc too, so ring is no longer built and we don't ship two crypto libraries.
  • src/sessions.rs:
    • object_store() builds the store once per process from the environment, and sql_open gives each database its own directory in it.
    • A session's calls are polled on the asyncio loop's thread, but the store's HTTP client (reqwest) needs a tokio runtime. So the object store's futures alone are polled inside pyo3-async-runtimes' runtime, the one the tunnel already uses (InRuntime). Nothing else changes how it runs, the page cache included.
  • tests/sessions_object_store.py: the variable is read once per process, so each scenario runs in a child process.
    • A database past 4 MiB promotes to a directory store, reads back cold, works with a reader beside it, and has its old packs deleted after a rewrite.
    • A store the build can't serve fails the open with a message that names the variable.

Testing

  • just test-fast: 399 passed. 397 run without a store configured, which is the default path.
  • just lint, just lint-rust, and pyright and mypy on the new test are clean.
  • tests/connect.py: request identity verification now runs on aws-lc and passes. Its fake Cloud peer speaks plain HTTP, so no test covers a real TLS handshake.
  • S3 (manual, against an S3 mock): promotion, a cold read of 12 MiB and a rewrite all work from the asyncio thread, and the rewrite deletes the old packs. Without InRuntime, the first request panics with there is no reactor running.

Before release

  • aws-lc is a C library, and this PR builds it only on Linux x86_64. aws-lc-sys 0.47 ships pre-generated bindings for every release target (Linux x86_64 and aarch64, glibc and musl, and macOS on both architectures), so those builds need only a C compiler, as ring did, not cmake. The release workflow hasn't run with it yet; a dry run would confirm.
  • docker.yml builds test-services/Dockerfile for linux/arm64 under QEMU on every push to main, so after merge that build compiles aws-lc too. Its build stage already installs build-essential, so it should build, but under emulation it will take longer. Dispatching docker.yml on this branch would check it first, but it also pushes a test-services-python:object-storage image.
  • The extension grows by about 7 MiB, almost all of it S3 support (aws-lc, object_store, the HTTP client). For sdk-sessions' addon that was +46%.

🤖 Generated with Claude Code

…L_OBJECT_STORE)

Upgrade restate-sdk-shared-core to 8.5.0, with its object store backends.
Set RESTATE_SQL_OBJECT_STORE to `s3://bucket/path`, `gs://bucket/path`,
`az://container/path` or `file:///a/directory`, and a database whose packs
in Restate pass 4 MiB moves them to the object store in one upload.
Credentials come from the provider's variables (`AWS_*`, `GOOGLE_*`,
`AZURE_*`); MinIO or R2 is `s3://` with `AWS_ENDPOINT`. Unset, every pack
stays in Restate, as before.

The store is shared-core's `state::ObjectStoreL1`, made once per process;
each namespace gets its own directory. Its requests are reqwest's, which
need a tokio runtime, and a session's calls are polled on the asyncio
loop's thread: the store's futures alone are polled inside
pyo3-async-runtimes' runtime (`InRuntime`). Without it, the first request
panics with "there is no reactor running".

Crypto: `aws_lc_rs` instead of `rust_crypto`. The object stores' backends
are aws-lc; with it, the tunnel's TLS and request identity use aws-lc too,
and ring is no longer built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ℹ️ No critical issues — one stale doc and one post-merge build to watch.

Reviewed changes

I reviewed the full PR: the shared-core 8.5.0 bump and crypto switch, the object store wiring in src/sessions.rs, and the new subprocess tests. I rebuilt the extension from this branch. tests/sessions_object_store.py passes (2/2), and so do tests/connect.py and tests/sessions_database.py (43/43).

  • shared-core 8.5.0: compared with 8.4.0, it only adds state/l1.rs (ObjectStoreL1, segment) and changes some docs in blobs.rs. format.rs is unchanged, so the head magic and state format are the same, and the bump can be rolled back.
  • rust_crypto → aws_lc_rs: cargo tree --locked --target all -i ring prints nothing. ring is still listed in Cargo.lock, but no target builds it.
  • object_store(): the store is built once per process behind a std::sync::Mutex. It does not cache an error, so a bad value fails every open, the same way page_cache() handles a bad size.
  • InRuntime / in_runtime: every blob-store poll enters pyo3-async-runtimes' multi-thread runtime. Wake-ups from tokio worker threads reach the asyncio loop through foreign_wakes, so a worker never waits for the GIL.
  • Tests: each scenario runs in a child process because the variable is read once per process. They cover promotion to a file:// store, a cold read from both tiers, a reader alongside a writer, garbage collection after a rewrite, and the error for a store the build can't serve.

ℹ️ docker.yml will build aws-lc for arm64 under QEMU right after merge

The PR's "Before release" section lists the release targets, but .github/workflows/docker.yml also runs on every push to main. It builds test-services/Dockerfile for linux/arm64,linux/amd64 through QEMU, and that uv build --wheel will now compile aws-lc as well. The Dockerfile already notes that the arm64 build "takes the better part of an hour" under QEMU. The first push to main will show whether aws-lc builds there and how much time it adds, unless docker.yml is dispatched manually on this branch before merging.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread src/sessions.rs Resolved
@github-actions

github-actions Bot commented Nov 10, 2026 •

Copy link
Copy Markdown

Test Results

  8 files  ±0    8 suites  ±0   3m 39s ⏱️ -26s
 71 tests ±0   71 ✅ ±0  0 💤 ±0  0 ❌ ±0 
322 runs  ±0  322 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 722c841. ± Comparison against base commit ef98082.

♻️ This comment has been updated with latest results.

The module docs and `TaskWaker`'s said the page cache's disk tier is the
only other thread that wakes a call; with an object store, the tokio
runtime driving its requests does too, through the same `foreign_wakes`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found. The stale-doc point from the last review is fixed.

Reviewed changes

I reviewed 722c841, which only changes doc comments in src/sessions.rs.

  • Module docs ("Where a call runs"): the section now says two kinds of thread wake a call — the page cache's disk tier and, when an object store is configured, the tokio runtime behind InRuntime. Both reach the asyncio loop through foreign_wakes.
  • TaskWaker doc: it now names the object store's tokio runtime as a second thread that can wake a task.

From the last review, the docker.yml arm64-under-QEMU build still needs watching after merge. It isn't a problem in the code.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@slinkydeveloper
slinkydeveloper merged commit b307b99 into main Nov 10, 2026
12 checks passed
@slinkydeveloper
slinkydeveloper deleted the object-storage branch November 10, 2026 16:50
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant