You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a1f00e6
Browse filesBrowse the repository at this point in the historyBrowse files
fix(platform): stop large Worker uploads from timing out (#601)
## Problem
Large apps cannot deploy to Void Cloud. The `napi-image` deploy
([run](https://github.com/Brooooooklyn/Image/actions/runs/40348054155/job/121667923309))
failed with `The Worker upload did not finish within 120 seconds. Deploy
again.`
```
queue job: PUT script to Cloudflare, 120 s timer per attempt
body = hdr ─ GET module 1 from R2 ─ bytes ─ hdr ─ GET module 2 ─ ... ─ GET module 744
(one GET at a time, inside the timer)
attempt 1: 120 s ✗ → attempt 2: 120 s ✗ → attempt 3 ✗ → attempt 4 ✗ (≈ 490 s, then fail)
```
- The app had 744 modules (413 JS + 331 source maps). 120 s / 744 leaves
about 161 ms for each R2 GET, its hash, and its send.
- Each retry reads all modules again, so a retry cannot succeed.
- The 120 s limit was only measured with local Miniflare R2.
- Before #439, the modules were read before the upload, with no timer.
That is why this app deployed on 10-07.
`@void/md` has a separate problem: `markdown-it-attrs@5` loads
`markdown-it` at runtime, but `@void/md` does not provide it. Apps fail
with `Cannot find module 'markdown-it'` unless they add it themselves.
## What this PR changes
```
body: hdr ─ module i (already open) ─ bytes ─ open the next modules ahead ...
up to 4 modules / 8 MiB ahead, in order
timer: no bytes for 60 s → stop (a stalled upload)
no answer 120 s after body → stop
8 min for all attempts → stop (shared budget, not 120 s × 4)
```
```
queued run (15 min Cloudflare limit)
├─ 0 ─────────────────────── 10 min ─────── 15 min
│ app upload + DO replays │ compensation reserve
│ all end at this deadline ──┘
└─ a DO replay does not start if undoing the hosts already moved would not fit
```
- `attempt_failed` and the new `attempt_done` logs record bytes sent,
time waiting on R2, and time waiting on Cloudflare. A later timeout
shows which side was slow.
- The custom Durable Object host replay sends the same modules. It uses
the same read-ahead and limits.
- `@void/md` now depends on `markdown-it`.
## Decisions
| Question | Choice | Why |
|---|---|---|
| Read-ahead bound | 4 modules, 8 MiB declared size | One API isolate
has 128 MiB for all requests. With the PUT, 4 GETs stay under the
6-connection limit. |
| A module over 8 MiB | Opens only at its turn | Opens stay in order.
Memory stays bounded. |
| Timer model | Idle 60 s + response 120 s + shared 8 min budget | A
slow upload that still sends bytes finishes. A stalled one still stops
quickly. |
| DO replay limit | min(8 min, run deadline), with time kept to undo |
The old fixed 60 s would bring the large-bundle timeout back. |
| Retry rules | Not changed | 4xx is still final. The saved-script check
is still used. |
## Rollout
```
merge → deploy hosted production (platform) → Image redeploys
(Brooooooklyn/Image also stops emitting source maps)
```
No CLI change is needed. The fix is all on the platform side.
## Known limits
- The real time split on production R2 is not measured yet. The new logs
will show it after the first large deploy.
- Recovery after an expired lease (`settleExpiredDeployment`) and HTTP
rollback still have no run deadline. Each replay there keeps the
8-minute cap.
- A Durable Object replay runs inside the cutover transaction, which
locks the project row. A slow but progressing replay can now hold that
lock for longer than the old 60 s.
- Compensation still has no deadline of its own. If restoring the
previous release onto a moved Durable Object host takes much longer than
the forward move, the queued run can still pass 15 minutes. Lease
recovery (`settleExpiredDeployment`) then restores the host, so mixed
versions can last until the 18-minute lease expires. This design existed
before this PR. A full fix needs a restore budget before any host
changes, plus recovery that resumes across runs. It is left for a
separate change.
## Testing
| Check | Result |
|---|---|
| `test/unit/worker-upload.test.ts` (read-ahead order, byte bound,
release, watchdog, run deadline) | pass |
| `test/integration/api-deploy.test.ts` (watchdog cuts stalled attempts,
budget stops retries) | pass |
| `test/integration/managed-durable-objects.test.ts` (queued run ends DO
replay at the deadline; undo time kept) | pass |
| `test/integration/deployment-execution.test.ts` (queued upload capped
by the run deadline) | pass |
| Mutation checks: read-ahead 0, no byte bound, no idle re-arm, no
budget, no deadline wiring | each fails a test |
| `@void/md` packed into a fresh Yarn PnP project | `markdown-it-attrs`
loads; it failed before |
| `vp run check` | 33/33 |
| `vp run test --base origin/main` | 14911 passed, 28 skipped |
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
0 commit comments