diff --git a/Dockerfile b/Dockerfile index afbb3da..81a496e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ COPY --from=build /src/dist/container/bin/ /usr/local/bin/ COPY --from=build /src/dist/worker-app/ /app/dist/worker-app/ COPY --from=build /src/dist/cloudflare/assets/ /app/dist/cloudflare/assets/ COPY --from=build /src/dist/container/wrangler.jsonc /app/wrangler.jsonc -ENV CELLD_ESBUILD=/usr/local/bin/esbuild CELLD_IDLE_EVICT_S=60 +ENV CELLD_ESBUILD=/usr/local/bin/esbuild CELLD_LTX_RETENTION_SECS=3600 WORKDIR /app USER 65532:65532 VOLUME ["/app/.celld"] diff --git a/cloudflare/library.ts b/cloudflare/library.ts index 5c705c5..530d7a4 100644 --- a/cloudflare/library.ts +++ b/cloudflare/library.ts @@ -150,6 +150,9 @@ export class ArtifactLibrary extends DurableObject { } } this.sql.exec("create index if not exists serve_events_version on serve_events(version_id, served_at)"); + // History lists read only these columns. Covering them keeps listings from + // reading each revision's source, which precedes them in the row. + this.sql.exec("create index if not exists versions_history on versions(artifact_id, revision, id, created_at, source_hash, runtime, reason, restored_from)"); }); } diff --git a/cloudflare/scripts.ts b/cloudflare/scripts.ts index c0774a7..8e07762 100644 --- a/cloudflare/scripts.ts +++ b/cloudflare/scripts.ts @@ -42,6 +42,9 @@ export class ScriptLibrary extends DurableObject { this.sql.exec(`alter table ${table} add column project text not null default '{"files":{},"dependencies":{},"lock":{}}'`); } } + // History lists read only these columns, newest first. Covering them keeps + // listings from reading each revision's source, which precedes them in the row. + this.sql.exec("create index if not exists script_versions_history on script_versions(created_at, id, workspace, name, revision, source_hash, reason, restored_from)"); } private row(input: {workspace: string; name: string}): Row { const {workspace,name} = key(input); diff --git a/docs/celld-deployment.md b/docs/celld-deployment.md index d301219..6f18db6 100644 --- a/docs/celld-deployment.md +++ b/docs/celld-deployment.md @@ -51,11 +51,18 @@ For production or multiple machines, use celld's bucket-backed node mode: 4. Terminate TLS and authenticate users in the application or a chosen reverse proxy. Keep the operator/peer listener on a trusted private network, with an encrypted overlay when that network does not provide confidentiality. -5. Use `/.well-known/celld/health` for node readiness. `/health` is an application +5. Set `CELLD_LTX_RETENTION_SECS` (for example, `3600`) on every node once all + nodes run 0.6.1. Without it, celld never deletes superseded snapshot epochs, + so bucket usage grows with each activation, including every wake after idle + eviction. Epoch deletion requires list-after-write consistency; see the + [storage guarantees](https://github.com/denoland/celld/blob/v0.6.1/docs/guarantees.md) + before enabling it on a multi-region bucket. Nodes using `bucket` durability + delete nothing. Unset it on every node before rolling back to celld 0.6.0. +6. Use `/.well-known/celld/health` for node readiness. `/health` is an application route and does not establish node readiness. Allow graceful SIGTERM shutdown: the supervisor's stop grace must exceed celld's configured shutdown bound (40 seconds by default), with enough time for the expected drain and handoff. -6. Update app code through `celld deploy`. Running nodes adopt deployments in +7. Update app code through `celld deploy`. Running nodes adopt deployments in place; a failed build leaves the previous deployment serving. For runtime upgrades, follow the release-specific shutdown requirements. The [0.6.0 release](https://github.com/denoland/celld/releases/tag/v0.6.0) requires diff --git a/docs/daemon.md b/docs/daemon.md index 9163423..608da11 100644 --- a/docs/daemon.md +++ b/docs/daemon.md @@ -132,6 +132,16 @@ credentials or other environment variables. The native server binds only to loopback. Its state is local application data and is not uploaded or backed up automatically. +celld stores full copies of an object's database each time the object is unloaded +and started again. The host keeps idle objects in memory, so this happens when the +server restarts or celld unloads objects under memory pressure, not after each idle +period. Setting `CELLD_IDLE_EVICT_S` unloads objects after that many idle seconds, +and every later wake stores more copies. The host deletes copies that are older than +one hour and no longer needed for recovery (`CELLD_LTX_RETENTION_SECS`, default +`3600`). Deletion can take about ten minutes after the next write to that object. +Set `CELLD_LTX_RETENTION_SECS=0` to keep every copy; storage then grows with each +restart. + For bucket-backed production nodes, TLS/authentication boundaries, health checks, and graceful rollout guidance, see [celld deployment](celld-deployment.md). diff --git a/docs/docker.md b/docs/docker.md index bf3574e..4a78294 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -15,7 +15,9 @@ docker run -d --name artifacts --restart unless-stopped \ Open [localhost:4786](http://127.0.0.1:4786); the MCP endpoint is `http://127.0.0.1:4786/mcp`. The named volume preserves apps, databases, files, and schedules across container replacements. Stop the container before backing -up the volume. Allow 60 seconds for graceful shutdown. +up the volume. Allow 60 seconds for graceful shutdown. The image deletes +superseded storage snapshots after one hour, as described in the +[host guide](daemon.md#data-and-configuration). ## Configure network access diff --git a/src/local/server.ts b/src/local/server.ts index 49f23f5..00d5db4 100644 --- a/src/local/server.ts +++ b/src/local/server.ts @@ -61,7 +61,11 @@ export async function runCelldServer(options: { port?: number; stateDir?: string const esbuild = esbuildRequire.resolve(`@esbuild/${process.platform}-${process.arch}/bin/esbuild`); child = Bun.spawn([executable, "dev", project, "--host", "127.0.0.1", "--port", String(port), "--no-watch", "--logs"], { cwd: project, - env: { ...process.env, CELLD_ESBUILD: esbuild, CELLD_IDLE_EVICT_S: process.env.CELLD_IDLE_EVICT_S ?? "60", RUST_LOG: process.env.RUST_LOG ?? "warn" }, + // Each unload and restart of an object stores full copies of its database, so + // idle objects stay resident (celld still unloads them under memory pressure). + // Retention lets celld delete superseded copies. + env: { ...process.env, CELLD_ESBUILD: esbuild, + CELLD_LTX_RETENTION_SECS: process.env.CELLD_LTX_RETENTION_SECS ?? "3600", RUST_LOG: process.env.RUST_LOG ?? "warn" }, stdin: "ignore", stdout: "pipe", stderr: "pipe", }); const forward = async (stream: ReadableStream | number | undefined) => {