Skip to content

Reserve remix storage before taking the project save lock - #75

Merged
Mistium merged 2 commits into
mainfrom
remix-hold-outside-lock
Nov 1, 2026
Merged

Mistium merged 2 commits into
mainfrom
remix-hold-outside-lock

Conversation

@Mistium

@Mistium Mistium commented Nov 1, 2026

Copy link
Copy Markdown
Member

Remixing reserved the remixer's storage with the Rotur storage ledger, a network call, while holding the parent project's save lock. While Rotur was slow or down, uploads, history writes and the project.json flush on reads of the parent all waited behind it. This was the last Rotur call left under a save lock after #73.

Remixes now use the same record-and-replay approach as project updates:

  • Unlocked pass: load the parent, run the view and remix checks, work out the fork's storage (the original's project.json and assets plus the new history), and reserve the hold.
  • Locked pass: under projectSaveLock(id), reload the parent, redo every check and the fork preparation, and replay the hold. If the parent changed so the computed storage differs, the attempt releases its hold and retries. After three tries it returns 409 project_changed, like updates.
  • Holds: each attempt releases its hold in a defer, so a throw can't leave it reserved. A successful remix keeps and settles its hold as before.
  • Fork id: generated once per request and reused, because the hold depends on it. No fork state is written on an attempt that retries.
  • Classroom copies: forkProjectFor keeps its signature, and now also releases its hold if a call throws.

Verification

  • New tests: project-remix.test.osl runs the real handler with stubs. It covers:
    • an unchanged parent, and a parent that changed before the lock, or kept changing (409);
    • full storage (413), a failed save, a throw (hold and save lock released), and accounts outside Rotur storage;
    • a parent made private (404) or closed to remixes (403) between the passes;
    • classroom copies.
  • Smoke: a new step delays the fake ledger during a remix. On main, an upload to the parent waits for it; on this branch it doesn't. That is the only smoke difference: 328 steps, 0 server errors on both.
  • Refusals: the reviewer also checked refused remixes (private, not remixable, missing, bad branch, full storage, anonymous) against both binaries, and they match.
  • Checks: osl test passes 60, lsp check and conventions are clean, and the route dump is identical.
  • Review: an independent review found no must-fix issues. Its should-fix item, a test for permissions revoked between the passes, is added here.

Known and accepted:

  • Concurrent remixes: two remixes of the same parent sent at once by a user at their storage limit can overshoot by one fork's history, because both holds are reserved against the same starting point. The ledger settles correctly afterwards, and main already has this gap for remixes of different parents.
  • Unused snapshot: if the parent's project.json changes between the two passes, the snapshot staged by the first pass is left unused.

There is no visible change.

Remixing asked the Rotur storage ledger for a hold while holding the parent
project's save lock, so a slow or unreachable Rotur stalled uploads, history
writes and project.json flushes on the parent. The remix now reserves the
hold against an unlocked copy of the parent, then reloads the parent under
the lock and replays the hold only if the fork's storage still matches. A
parent that changed in between releases the hold and tries again, with a 409
after three tries like project updates. Holds are released in a deferred
block, and a successful remix keeps and settles its hold as before.
@Mistium
Mistium merged commit 6ab167c into main Nov 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant