denoland/celldPublic

self-hosted, distributed Durable Objects

Alarms get created when others are still running

LE

lemeb opened ·

Hi!

If an alarm takes longer than 15 seconds (or whatever `CELLD_OPERATION_DEADLINE_MS` is), celld assumes it failed and starts it again, but the first one is still running. And so you end up with several copies of the same alarm running at the same time.

Repro (v0.6.1, `celld dev`). Put `wrangler.jsonc` and `index.js` in `./proj`, then run `CELLD=/path/to/celld ./repro.sh`.

<details>

`wrangler.jsonc`:

```jsonc { "name": "alarm-overlap", "main": "index.js", "compatibility_date": "2026-01-01", "durable_objects": { "bindings": [{ "name": "CELL", "class_name": "Cell" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Cell"] }] } ```

`index.js`:

```js import { DurableObject } from 'cloudflare:workers';

const log = []; let running = 0; let n = 0;

export class Cell extends DurableObject { async alarm() { const id = ++n; running++; log.push({ id, event: 'start', at: Date.now(), concurrent: running }); try { await new Promise((resolve) => setTimeout(resolve, 20000)); } finally { running--; log.push({ e.now() }); } } async fetch(request) { if (new URL(r'/arm') { await this.ctx.storage.setAlarm(Date.now() + 10); return new Response('armed\n'); } return Response.json(log); } }

export default { fetch(request, return env.CELL.get(env.CELL.idFromName('a')).fetch(request); }, }; ```

`repro.sh`:

```bash #!/bin/bash set -u cd "$(dirname "$0")" CELLD_OPERATION_DEADLINE_MS=5000 "${CELLD:-celld}" dev proj --port 8788 --no-watch --logs > dev.log 2>&1 & DEV=$! trap 'kill $DEV; wait $DEV' EXIT until grep -q "ready http" dev.log; do kill -0 $DEV 2>/dev/null || exit 1; sleep 1; done curl -s http://127.0.0.1:8788/arm sleep 30 curl -s http://127.0.0.1:8788/log; echo ``` Observed on v0.6.1 (one alarm that sleeps 20 s, deadline set to 5 s):

| copy | starts at | copies running | |---|---|---| | 1 | 0 s | 1 | | 2 | 5.5 s | 2 | | 3 | 11.0 s | 3 | | 4 | 16.5 s | 4 | </details>