Skip to content

Commit 89e4ffc

Browse files
rybartlomiejubplotnicknathanwhitmagurotuna
committed
v0.3.0
- v0.3.0 adds a replicated write-behind log. Fleets of two or more nodes acknowledge writes after peer fsync while bucket upload finishes later. In testing, this delivers 10× lower write latency and over 100× fewer Class A S3 operations. A v0.2.1 fleet can roll forward one node at a time. - Added support for Azure Blob Storage (`az://<BUCKET_NAME>`). - Added support for D1 databases, including the `celld d1` CLI for migrations and SQL execution. - Added support for cron triggers, with durable fleet-wide scheduling, retries for failed handlers, and recovery of one missed run after downtime. - Streaming and rewrapping request bodies are up to 70× faster, and text decoding is up to 44× faster. - WebSocket upgrades now work correctly for Durable Object subrequests and cross-script service bindings. - `setInterval()` now works correctly. - `celld deploy` now accepts Node built-in specifiers (E.G. `node:util`) - Each isolate now has a configurable V8 heap limit. celld refuses new hibernatable WebSockets and oversized SQL materialization near that limit, then resumes service after memory use falls. - Correctness fixes prevent overlapping alarm handlers, events from resuming in freed isolates, lost transactions at replication segment boundaries, and database corruption after eviction and reactivation. - Closed issues: [#139](#139), [#144](#144), [#147](#147), [#149](#149), [#150](#150), [#152](#152), [#154](#154), [#155](#155), [#156](#156), [#157](#157), and [#158](#158). Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com> Co-authored-by: Ben Plotnick <bplotnick@users.noreply.github.com> Co-authored-by: Nathan Whitaker <17734409+nathanwhit@users.noreply.github.com> Co-authored-by: Yusuke Tanaka <yusuktan@maguro.dev> Co-authored-by: magurotuna <23649474+magurotuna@users.noreply.github.com>
1 parent ae8fac0 commit 89e4ffc

97 files changed

Lines changed: 18642 additions & 5262 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ encoding_rs = "0.8"
4646
# unstable-split gives the read and write halves as separate objects. The
4747
# tunnel needs them: a WebSocket read is not cancel-safe, so a live read
4848
# future must survive while the same socket is written, and that is only
49-
# possible when the halves are not one borrow. See tests/p0/runtime/tunnel_cancel.rs.
49+
# possible when the halves are not one borrow.
5050
#
5151
# The feature is unstable by name, so a 0.8.x release can rename or drop it
5252
# without upstream calling that a break, and a caret range would take it. Deno
@@ -64,6 +64,7 @@ hyper-util = { version = "0.1", features = ["tokio"] }
6464
libc = "0.2"
6565
lz4_flex = { version = "0.11", features = ["frame"] }
6666
md-5 = "0.10"
67+
nix = { version = "0.31", features = ["feature", "fs"] }
6768
object_store = { version = "0.11", default-features = false }
6869
# The native column-writer API only: no `arrow` feature, so the arrow
6970
# crate stack stays out of the binary. Revisit only if the telemetry

README.md

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,24 @@ Self-hosted, distributed **Durable Objects**.
55
celld is an open-source daemon that runs Cloudflare Workers and Durable
66
Objects on your own machines. Each object is its own SQLite database.
77
celld addresses an object by name and replicates it to a bucket that you
8-
own. The bucket can be S3-compatible or Google Cloud Storage. The nodes
9-
coordinate through that bucket alone, with no control plane and no
10-
consensus. Because every object is its own small database,
11-
applications shard by construction — the contention and blast-radius failures
12-
of one shared database are designed out, not managed. A cell that no node
13-
holds is inactive, and an inactive cell costs nearly nothing. Learn more at
8+
own. The bucket can be S3-compatible, Google Cloud Storage, or Azure
9+
Blob Storage. The nodes coordinate through that bucket alone, with no
10+
control plane and no consensus. Because every object is its own small
11+
database, applications shard by construction — the contention and
12+
blast-radius failures of one shared database are designed out, not
13+
managed. A cell that no node holds is inactive, and an inactive cell
14+
costs nearly nothing. Learn more at
1415
[celld.dev](https://celld.dev) or read the
1516
[documentation](https://celld.dev/docs).
1617

1718
## How it works
1819

1920
Every `celld` node embeds V8 and executes Wrangler bundles. The fleet shares
2021
one bucket, which contains deployments, cell state, and small ownership
21-
records. The bucket can be S3-compatible or Google Cloud Storage. Object-storage compare-and-swap ensures that exactly one node owns a
22-
cell at a time, without a membership protocol, failure detector, or consensus
23-
service.
22+
records. The bucket can be S3-compatible, Google Cloud Storage, or Azure
23+
Blob Storage. Object-storage compare-and-swap ensures that exactly one
24+
node owns a cell at a time, without a membership protocol, failure
25+
detector, or consensus service.
2426

2527
celld continuously replicates each cell's SQLite database to the bucket.
2628
When a cell moves, or when an inactive cell activates, its new owner restores
@@ -109,6 +111,27 @@ celld --bucket gs://my-cells-bucket --listen 0.0.0.0:8080 \
109111
--internal-listen 10.0.0.12:8081 --advertise 10.0.0.12:8081
110112
```
111113

114+
An `az://` bucket selects Azure Blob Storage, where the NAME is the
115+
container. The storage account comes from `AZURE_STORAGE_ACCOUNT_NAME`.
116+
celld requires exactly one storage account key, managed identity, or workload
117+
identity. An AKS workload identity uses `AZURE_AUTHORITY_HOST`,
118+
`AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_FEDERATED_TOKEN_FILE`.
119+
The authority host must identify the public Azure cloud. A Microsoft Entra
120+
identity needs data-plane permission to read, write, list, and delete blobs.
121+
The `Storage Blob Data Contributor` role supplies these permissions. celld
122+
rejects an S3 `--endpoint` for an `az://` bucket, and it ignores the storage
123+
region. celld qualifies an `az://` bucket: the conditional-write contract and
124+
the multipart upload path were tested against a live Azure account on
125+
2026-08-18, under each of the three credential families named above. See
126+
[ownership and fencing](docs/fencing.md):
127+
128+
```sh
129+
export AZURE_STORAGE_ACCOUNT_NAME=myaccount
130+
celld deploy . --bucket az://my-cells-container
131+
celld --bucket az://my-cells-container --listen 0.0.0.0:8080 \
132+
--internal-listen 10.0.0.12:8081 --advertise 10.0.0.12:8081
133+
```
134+
112135
A fleet runs one application, and every node loads its
113136
latest successfully committed deployment from `deploy/current.json`. Run
114137
`celld --help` for the complete command line.
@@ -145,6 +168,14 @@ and incompatible protocols. It also prints each node's coarse resident-cell,
145168
WebSocket, RSS, CPU, file-descriptor, pressure, and shedding sample. Pass one
146169
or more `--peer NODE_ID` options to restrict the check.
147170

171+
`celld d1` runs SQL and migrations against a deployed D1 database. It finds a
172+
node through the same node leases, and that node sends the work to the node
173+
that owns the database:
174+
175+
```sh
176+
celld d1 migrations apply ledger --bucket s3://my-cells-bucket
177+
```
178+
148179
Set a hard resident-cell limit on each loaded node:
149180

150181
```sh
@@ -175,6 +206,25 @@ set size, and celld reports this at startup. `CELLD_MAX_RSS_MB=0`
175206
disables the threshold and the cap together. When celld cannot read the size of
176207
the available memory, it applies a cap of 125% of an explicit threshold.
177208

209+
Each isolate also has a V8 heap limit, and this limit is separate from the
210+
memory of the node. The default is 128 MB, and it matches the limit of a
211+
Durable Object on Cloudflare. Set `CELLD_V8_HEAP_LIMIT_MB` to change it. The
212+
limit decides how much state one isolate can hold, so it decides how many
213+
hibernatable WebSocket clients a cell can carry. Each client holds state in the
214+
heap. A cell holds approximately 50,000 clients with the default limit, and it
215+
needs approximately 512 MB to hold 100,000 clients.
216+
217+
An isolate that uses more than 90% of this limit refuses a new hibernatable
218+
WebSocket, and the error names the heap. The refusal is not permanent, and the
219+
isolate accepts a WebSocket again when the use of the heap falls under 90%.
220+
221+
An isolate that reaches the limit stops more than an accept. It also stops the
222+
materialization of a SQL result set, and that error names the heap too. celld
223+
measures the heap before each event, and the isolate serves again when the use
224+
of the heap falls under 75% of the limit. An idle isolate holds a dead heap
225+
until something allocates again, so celld forces a collection when a
226+
measurement is above that share. A restart of the process is not necessary.
227+
178228
Under pressure, celld durably replicates and fences the least-recently used idle
179229
cells. It then publishes the cells as unowned without resetting their epochs.
180230
Those cells become inactive, and celld refuses to reacquire new unowned cells.

clippy.toml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# The execution boundary must use celld::asyncrt. Modules which never execute
2+
# in the World carry an explicit lint allow and are listed in the S0 audit.
3+
disallowed-methods = [
4+
{ path = "tokio::spawn", reason = "use celld::asyncrt::spawn inside the World boundary" },
5+
{ path = "tokio::runtime::Handle::spawn", reason = "use celld::asyncrt::spawn inside the World boundary" },
6+
{ path = "tokio::task::spawn_blocking", reason = "use celld::asyncrt::blocking inside the World boundary" },
7+
{ path = "tokio::runtime::Handle::spawn_blocking", reason = "use celld::asyncrt::blocking inside the World boundary" },
8+
{ path = "tokio::time::sleep", reason = "use celld::asyncrt::sleep inside the World boundary" },
9+
{ path = "tokio::time::sleep_until", reason = "use celld::asyncrt::sleep_until inside the World boundary" },
10+
{ path = "tokio::time::timeout", reason = "use celld::asyncrt::timeout inside the World boundary" },
11+
{ path = "tokio::time::timeout_at", reason = "use celld::asyncrt::timeout_at inside the World boundary" },
12+
{ path = "tokio::time::interval", reason = "use celld::asyncrt::interval inside the World boundary" },
13+
{ path = "tokio::time::interval_at", reason = "use celld::asyncrt::interval_at inside the World boundary" },
14+
{ path = "std::time::SystemTime::now", reason = "use the execution-domain wall clock inside the World boundary" },
15+
{ path = "std::time::Instant::now", reason = "use the execution-domain monotonic clock inside the World boundary" },
16+
{ path = "rand::random", reason = "use a labeled execution-domain RNG inside the World boundary" },
17+
{ path = "std::process::id", reason = "use the execution-domain process tag inside the World boundary" },
18+
{ path = "std::fs::read", reason = "use the injected whole-node filesystem for celld node storage" },
19+
{ path = "std::fs::read_to_string", reason = "use the injected whole-node filesystem for celld node storage" },
20+
{ path = "std::fs::read_dir", reason = "use the injected whole-node filesystem for celld node storage" },
21+
{ path = "std::fs::metadata", reason = "use the injected whole-node filesystem for celld node storage" },
22+
{ path = "std::fs::symlink_metadata", reason = "use the injected whole-node filesystem for celld node storage" },
23+
{ path = "std::fs::canonicalize", reason = "use the injected whole-node filesystem for celld node storage" },
24+
{ path = "std::fs::copy", reason = "use the injected whole-node filesystem for celld node storage" },
25+
{ path = "std::fs::exists", reason = "use injected filesystem metadata for celld node storage" },
26+
{ path = "std::fs::hard_link", reason = "use the injected whole-node filesystem for celld node storage" },
27+
{ path = "std::fs::read_link", reason = "use the injected whole-node filesystem for celld node storage" },
28+
{ path = "std::fs::File::create", reason = "use the injected whole-node filesystem for celld node storage" },
29+
{ path = "std::fs::File::open", reason = "use the injected whole-node filesystem for celld node storage" },
30+
{ path = "std::fs::File::options", reason = "use the injected whole-node filesystem for celld node storage" },
31+
{ path = "std::fs::File::sync_all", reason = "use the injected whole-node filesystem for celld node storage" },
32+
{ path = "std::fs::File::sync_data", reason = "use the injected whole-node filesystem for celld node storage" },
33+
{ path = "std::fs::OpenOptions::new", reason = "use the injected whole-node filesystem for celld node storage" },
34+
{ path = "std::fs::OpenOptions::open", reason = "use the injected whole-node filesystem for celld node storage" },
35+
{ path = "std::fs::write", reason = "use the injected whole-node filesystem for celld node storage" },
36+
{ path = "std::fs::rename", reason = "use the injected whole-node filesystem for celld node storage" },
37+
{ path = "std::fs::remove_file", reason = "use the injected whole-node filesystem for celld node storage" },
38+
{ path = "std::fs::remove_dir", reason = "use the injected whole-node filesystem for celld node storage" },
39+
{ path = "std::fs::remove_dir_all", reason = "use the injected whole-node filesystem for celld node storage" },
40+
{ path = "std::fs::create_dir", reason = "use the injected whole-node filesystem for celld node storage" },
41+
{ path = "std::fs::create_dir_all", reason = "use the injected whole-node filesystem for celld node storage" },
42+
{ path = "std::fs::set_permissions", reason = "use the injected whole-node filesystem for celld node storage" },
43+
{ path = "std::path::Path::exists", reason = "use injected filesystem metadata for celld node storage" },
44+
{ path = "std::path::Path::try_exists", reason = "use injected filesystem metadata for celld node storage" },
45+
{ path = "std::path::Path::is_file", reason = "use injected filesystem metadata for celld node storage" },
46+
{ path = "std::path::Path::is_dir", reason = "use injected filesystem metadata for celld node storage" },
47+
{ path = "std::path::Path::is_symlink", reason = "use injected filesystem metadata for celld node storage" },
48+
{ path = "std::path::Path::metadata", reason = "use injected filesystem metadata for celld node storage" },
49+
{ path = "std::path::Path::symlink_metadata", reason = "use injected filesystem metadata for celld node storage" },
50+
{ path = "std::path::Path::canonicalize", reason = "use the injected whole-node filesystem for celld node storage" },
51+
{ path = "std::path::Path::read_link", reason = "use the injected whole-node filesystem for celld node storage" },
52+
{ path = "std::path::Path::read_dir", reason = "use the injected whole-node filesystem for celld node storage" },
53+
{ path = "tokio::fs::read", reason = "use the injected whole-node filesystem for celld node storage" },
54+
{ path = "tokio::fs::read_dir", reason = "use the injected whole-node filesystem for celld node storage" },
55+
{ path = "tokio::fs::read_to_string", reason = "use the injected whole-node filesystem for celld node storage" },
56+
{ path = "tokio::fs::metadata", reason = "use the injected whole-node filesystem for celld node storage" },
57+
{ path = "tokio::fs::symlink_metadata", reason = "use the injected whole-node filesystem for celld node storage" },
58+
{ path = "tokio::fs::canonicalize", reason = "use the injected whole-node filesystem for celld node storage" },
59+
{ path = "tokio::fs::copy", reason = "use the injected whole-node filesystem for celld node storage" },
60+
{ path = "tokio::fs::try_exists", reason = "use injected filesystem metadata for celld node storage" },
61+
{ path = "tokio::fs::hard_link", reason = "use the injected whole-node filesystem for celld node storage" },
62+
{ path = "tokio::fs::read_link", reason = "use the injected whole-node filesystem for celld node storage" },
63+
{ path = "tokio::fs::File::create", reason = "use the injected whole-node filesystem for celld node storage" },
64+
{ path = "tokio::fs::File::open", reason = "use the injected whole-node filesystem for celld node storage" },
65+
{ path = "tokio::fs::File::sync_all", reason = "use the injected whole-node filesystem for celld node storage" },
66+
{ path = "tokio::fs::File::sync_data", reason = "use the injected whole-node filesystem for celld node storage" },
67+
{ path = "tokio::fs::OpenOptions::new", reason = "use the injected whole-node filesystem for celld node storage" },
68+
{ path = "tokio::fs::OpenOptions::open", reason = "use the injected whole-node filesystem for celld node storage" },
69+
{ path = "tokio::fs::write", reason = "use the injected whole-node filesystem for celld node storage" },
70+
{ path = "tokio::fs::rename", reason = "use the injected whole-node filesystem for celld node storage" },
71+
{ path = "tokio::fs::remove_file", reason = "use the injected whole-node filesystem for celld node storage" },
72+
{ path = "tokio::fs::remove_dir", reason = "use the injected whole-node filesystem for celld node storage" },
73+
{ path = "tokio::fs::remove_dir_all", reason = "use the injected whole-node filesystem for celld node storage" },
74+
{ path = "tokio::fs::create_dir", reason = "use the injected whole-node filesystem for celld node storage" },
75+
{ path = "tokio::fs::create_dir_all", reason = "use the injected whole-node filesystem for celld node storage" },
76+
{ path = "tokio::fs::set_permissions", reason = "use the injected whole-node filesystem for celld node storage" },
77+
]
78+
79+
disallowed-macros = [
80+
{ path = "tokio::select", reason = "use celld::asyncrt::select with a declared priority inside the World boundary" },
81+
]
82+
83+
disallowed-types = [
84+
{ path = "rand::rngs::OsRng", reason = "use a labeled execution-domain RNG inside the World boundary" },
85+
]

crates/celld/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "celld"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
edition = "2021"
55
rust-version = "1.94.1"
66
license = "Apache-2.0"
@@ -39,7 +39,7 @@ http-body-util.workspace = true
3939
hyper.workspace = true
4040
hyper-util.workspace = true
4141
md-5.workspace = true
42-
object_store = { workspace = true, features = ["aws", "gcp"] }
42+
object_store = { workspace = true, features = ["aws", "azure", "gcp"] }
4343
parquet.workspace = true
4444
percent-encoding.workspace = true
4545
p256.workspace = true
@@ -78,3 +78,8 @@ webpki-roots.workspace = true
7878

7979
[target.'cfg(unix)'.dependencies]
8080
libc.workspace = true
81+
nix.workspace = true
82+
83+
[dev-dependencies]
84+
async-trait.workspace = true
85+
lz4_flex.workspace = true

0 commit comments

Comments
 (0)