Deploy computation changes
A change to a derive or aggregate does nothing until you build and deploy the bundle. You have two options:
- Direct deploy recomputes stored derived values in one step, then switches code and results together. Fine for small apps.
- Staged deploy rebuilds in pages while the current version keeps serving, then switches when you activate it. Use it for large rebuilds.
- Stage.
admin.stageDeployment(bundle, { requestId })records the new bundle and any new indexes. - Advance. Each
advancecall backfills new indexes, then rebuilds a page of derived values in the background. Requests keep using the current code. - Ready. Once the rebuild is done, ordinary writes keep the new graph up to date until you activate or cancel.
- Activate.
activateswitches bundle, aliases, policy, keys, indexes and derived state in one step. - Collect. Call
collectuntil the phase iscollectedto free the old graph. Finish this before the next deployment.
- Use a unique request ID for each deployment and save it with the bundle. Progress survives leader changes, restarts and partition moves.
- Before resuming, check that both the request ID and bundle hash match, rather than staging again.
- Nothing runs on its own: your code calls
advanceandactivate. The progress counters show work done, not a percentage.
import { FlowerAdmin } from "@flower-js/sdk";
import { buildBundle } from "@flower-js/sdk/bundle";
const admin = new FlowerAdmin("http://127.0.0.1:7101", { adminToken: process.env.FLOWER_ADMIN_TOKEN });
const bundle = await buildBundle("app.ts");
const requestId = "application-upgrade-v2"; // Save and reuse for this bundle.
const maxBytes = 256 * 1024;
let build = (await admin.stagedDeploymentStatus()).value;
if (build === null || build.phase === "collected") {
build = (await admin.stageDeployment(bundle, { requestId })).value;
}
if (build.requestId !== requestId || build.bundleHash !== bundle.hash) {
throw new Error("Existing deployment ID/bundle does not match this intent");
}
while (build.phase === "backfill" || build.phase === "rebuilding") {
build = (await admin.controlStagedDeployment({
operation: "advance", requestId, maxBytes,
})).value;
}
if (build.phase === "failed") {
throw new Error(build.error ?? "Cancel and collect the failed target");
}
if (build.phase === "ready") {
build = (await admin.controlStagedDeployment({
operation: "activate", requestId,
})).value;
}
while (build.phase === "active" || build.phase === "canceled") {
build = (await admin.controlStagedDeployment({
operation: "collect", requestId, maxBytes,
})).value;
}- Uncertain response: read the status and continue with the same request ID. Aborting a request does not undo committed progress.
- Page or activation error: earlier progress is kept and the active app doesn’t change. Adjust the budget and retry, or cancel and collect.
- Phase
failed: a write hit a fatal error while maintaining the new graph. The job can’t activate; cancel and collect it. - Canceled: a canceled deployment can’t be activated. After collecting, stage corrected code with a new request ID.
- No rollback: after activation, fix forward with another deployment.
- While a cross-group transaction is prepared, deployment controls return
TRANSACTION_PREPARED. Status still works.
Tune rebuild pages
Each page blocks writes while it prepares and commits; reads continue. Smaller pages mean shorter write stalls and a slower rebuild.
FLOWER_DEPLOYMENT_PAGE_MSsets the target time per page. Default 200 ms, capped byFLOWER_EVALUATION_TIMEOUT_MS. Try 20 ms if write latency matters, and measure.maxByteson each request limits page size. It defaults to the transaction byte budget.
export FLOWER_DEPLOYMENT_PAGE_MS=20
export FLOWER_WRITER_BATCH_MS=50
# Start or restart each Flower node with these settings.- These are startup settings. Restart nodes to apply them. Ordinary write batching is unaffected.
- The page target is not a hard deadline. A page can take the target plus a full evaluation timeout.
- One derived value and the new dependencies it pulls in must fit in one evaluation. The whole graph’s metadata must fit in memory.
- Until you activate, every write also maintains the new graph, and the extra graph uses storage until collected.
The staged deployment benchmark shows the throughput and latency tradeoff. Those runs used an older shared setting, so treat them as rough.
Migrate stored records
Deploying rebuilds derived values. It does not change stored records. During a staged deployment, old and new code share the same records, so both must read every record shape.
- Deploy compatible code. Read old and new shapes, write only the new shape, and add a task that converts one bounded batch per run.
- Save progress with the data. Read a cursor from a migration record, convert a page, and save the next cursor with the changed rows.
duereturnsctx.now()until the cursor says done, so maintenance keeps going page by page. A mutation called by hand works too: check each row’s version so a retried batch doesn’t convert twice, and reuse the request ID for an uncertain batch. - Page by key. Use
ctx.scan(rows, { gt: afterKey, limit: 100 }), leaving outgton the first page. An empty page means you’re done. Typed-key collections reject key bounds, so page them, or an indexed subset, withctx.rangeon fields the migration doesn’t change. Avoid offsets and unbounded scans. - Verify, then clean up. Check that no old rows remain. Then deploy code without the compatibility path and remove old fields with bounded mutations.
- Names are identities. Renaming a collection or derived value, or changing a TypeScript type, doesn’t touch existing data. Copy data explicitly.
- A task needs no public method. If you expose a migration mutation, protect it with access rules: an operator token doesn’t make a public mutation admin-only.
- During a staged deployment, each migration batch also updates the new graph. Size batches for that.
See the range and cursor rules, mutation context and staged deployment API. Moving a partition between Raft groups keeps data and code as they are; it is not a migration.