Repository navigation
fix(studio-server): save an edit Windows briefly holds without freezing the app - #7744
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
There was a problem hiding this comment.
Verdict: REQUEST CHANGES · 1 blocker · 0 important · 0 nits · head 10aa4348
The 'fails without the fix:' line is present. The blocker: a route that catches everything now swallows the hold, so a cut fails at once where main rode the hold out. ↓ inline
Scope
Read in full: renameWithRetry.ts, the new middleware, backupJournal.ts, blobStore.ts, freezeFrame.ts, and the in-scope write and catch sites (split-batch, applyFileMutations rollback, PUT files/*). I ran files.heldFile.test.ts with an extra split-batch probe, at this head and on main's code. CI was still running when I posted this; Studio server files on Windows is green.
— Genesis
What
On Windows, an edit to a project file that another program holds for a moment (a virus scan or a sync client opening the file just written) is now saved once the hold clears, up to about 5 s later, and the server keeps answering other requests while it waits.
Before, the edit gave up after about 1.3 s and the person's edit was lost (423 busy). During those 1.3 s the whole server process slept, so every other request, the preview included, stalled with it.
Why
renameWithRetryrides out a Windows hold by sleeping the thread between attempts (Atomics.wait, 10 ms doubling to 640 ms). Every edit route writes through it synchronously, so a hold froze the process for up to 1.3 s, and a hold longer than that lost the edit.Related work
Follows #7731, which made the edit routes answer a held file 423 and a read-only one 403 instead of a bare 500.
How
core/renameWithRetry.ts:catchHeldRename(work)runsworkso that a Windows hold on a sync rename inside it is thrown at once instead of slept on, and hands back that hold. The sync write stays one attempt, so the order of a write and its receipt is unchanged. The hold is thrown as a non-Errorvalue, so a host'sonErrordoes not hear about attempts that are going to be retried. Only the first hold in an edit is handed over this way: it is also recorded, so a route that catches every error and answers a failed write itself (a cut) is still sent again, and every rename after it (a rollback of files already written) rides holds out with the sync retry, as before.studio-server/routes/files.ts: a middleware on the edit routes (POST and PUT tofiles/*,file-mutations/*and the GSAP mutation routes; multipart uploads excepted) keeps a copy of the request, and when its write is held, waits (awaited, 10 ms doubling to 2,560 ms, about 5 s in all) and sends the same edit again. The first answer that is not held is the answer. A route that caught the hold and still answered success is not sent again. Once the waits are spent it throws the hold, so the host'sonErrorreports a refused edit once; the existing middleware then replaces whateveronErroranswered with 423{"error":"file in use","why":"busy"}, the words the client shows.studio-server/history/blobStore.ts: undo and restore write back withrenameWithRetryAsync, checking that the project is still writable right before each attempt..hyperframes/backup(10 kept per file):snapshotBeforeWritenow keeps no second copy when the newest backup of that file already holds the same bytes, so a replayed edit no longer pushes out the file's older backups;docs/contributing/testing-local-changes.mdx: ahyperframes previewstarted throughnpxkeeps its server running whennpxis stopped; stop it by the process listening on its port.Test plan
files.heldFile.test.ts, Windows is simulated by a rename that answersEBUSYuntil a release time:files.heldReplay.test.ts: a held then saved element edit, with a real project history, applies once, leaves one backup and one history entry;renameWithRetry.test.ts: a hold a caller swallowed is still handed back, and the next rename in the same edit rides its hold out;freezeFrame.test.ts: a freeze whose write throws leaves no still behind.main's code, "still saves the edit when the hold clears after 3000 ms" (423, edit lost) and "keeps the app answering while it waits" (423, the process slept). Earlier versions of this change failed the rest: retrying without the non-Errorhold failed the three "reports" checks (the host's error handler heard the edit 10 times); without the queue, the later edit answered first and the replay overwrote it; without the backup check, one held edit left 4 identical backups; without the freeze-frame cleanup, the still stayed; before the first hold was recorded, the cut answered 500 and a second rename in the same edit was refused at once.files.heldFile.test.ts,files.heldReplay.test.ts,freezeFrame.test.tsandblobStore.test.tspass 3 runs in a row on Linux; the fullstudio-serversuite passes (106 files, 1,886 tests);core'srenameWithRetryandatomicFiletests pass;tsc --noEmit,oxfmtandoxlintare clean on the changed files.files.heldFile.test.tsonwindows-latest, real held files included.main's code was not run on Windows with these tests.