Skip to content

Upgrade an a5 database to a8 in place instead of asking to delete it - #180

Open
srbaker wants to merge 7 commits into
mainfrom
feat/engine-upgrade-in-place
Open

srbaker wants to merge 7 commits into
mainfrom
feat/engine-upgrade-in-place

Conversation

@srbaker

@srbaker srbaker commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #178.

The engine pin moves to 4.0.0.a8. A database GemStone 4.0.0.a5 wrote is now upgraded in place with Grail's scripts/upgrade_engine.sh --in-place (GemTalk/Grail#1867), so users no longer have to delete it.

How it behaves

ensureRunning checks the extent with copydbf -i after the shared-memory step and before the processes start. It checks once per window and extent, not on every cell.

  • Upgradable (a pair in ENGINE_UPGRADES, currently a5 → a8), when someone asked for the database (Start, a notebook cell, the Shell, Run or Debug File):
    • A modal asks first. It names the backup (db/data/extent0.dbf.pre-4.0.0.a5) and says that a failure restores it.
    • GemDB stops the stone and listener with the previous engine, under the stone lock. After an extension update the old stone is usually still running, and a8's stopstone cannot log in to an a5 stone (error 4044, measured). If a stone is still listed, GemDB does not run the script.
    • It runs the bundled script with GEMSTONE set to the new engine and GemDB's GEMSTONE_GLOBAL_DIR, under a temporary stone named gemdbupgrade. The latest upgrade_engine: line shows as progress, and the full output goes to the GemDB log.
    • On success, it checks that copydbf reads a8. It then removes the Grail and MCP stamps, so the start continues and reinstalls both without asking: the upgrade removed their compiled methods. A notification says where the backup is.
    • On failure, the script has already restored the extent. GemDB re-reads its version and says whether the database is back as it was, or else points at the backup. A modal error with Show Log follows, and the database is not started.
    • Declined: nothing is touched and nothing starts.
  • Upgradable, but nobody asked (auto-start, an MCP agent): GemDB does not upgrade. It shows a warning with an Upgrade Database… button that runs Start, which asks. The GemDB Shell's cold start meets the pre-start guard, which says how to get the upgrade.
  • Refused as before: an older engine with no measured pair, or an unreadable version, still gets the "delete it" message. It now also names what GemDB can upgrade from. A database written by a newer engine is refused without the advice to delete it.
  • Setup (prepareFiles) lets an upgradable database through, because setup touches no database.
  • New databaseStarted outcomes: upgradeNeeded, upgradeDeclined, upgradeFailed, documented in docs/telemetry.md.

The long comment on assertDatabaseMatchesEngine, the CLAUDE.md section on engine upgrades, docs/automation-boundary.md (the upgrade is now under "Asked") and the CHANGELOG are updated. bundle-grail.sh and check-vsix.sh now require scripts/upgrade_engine.sh.

Testing

  • npm test: 1316 passed. New coverage: version ordering and judging; the guard's messages; the upgrade steps against a fake world (declined, no disk room, stop order under the lock, never running the script while the stone is up, the restored and unconfirmed failure messages, a zero exit that the extent does not confirm); running the script itself (the arguments of the --in-place interface, and the reason taken from its last line); and how ensureRunning handles each trigger.
  • lint, format:check, typecheck, typecheck:strict and lint:toolchain are clean.
  • New local-only integration test, src/__integration__/engineUpgrade.test.ts. It needs GEMDB_TEST_UPGRADE_EXTENT, an a5 extent, and skips without it. It runs the real script from the bundled grail/ on a copy, in the fixture's own global dir. 4 of 4 passed with the a8 engine and Grail at 5d0333f, on a GemDB-shaped database (createDatabase's config, the a5 key, extent pregrown to 10240 MB): a decline leaves the database alone; a failure (a wrong from-version) restores it byte-for-byte and leaves nothing running; the upgrade itself took 45s, after which the database reads a8, the backup equals the original, nothing is left running, and the stamps are gone.
  • By hand, end to end, with a separate global dir and stone names of its own:
    1. An a5 stone was run on a copy of an a5 Grail database and stopped, leaving a5 tranlogs.
    2. The bundled script upgraded the copy (50s).
    3. An a8 stone started on it normally, without -N.
    4. resources/install-grail.sh installed Grail 0c5e215 (exit 0), with "Recompiling 5826 Grail methods an engine upgrade removed".
    5. Python ran: a class, sum, a dict holding a tuple and None, and Unicode upper().
  • Not run: the full npm run test:integration. It needs the pinned engine under ~/GemDB, which I did not touch.

Remaining

🤖 Generated with Claude Code

srbaker and others added 2 commits October 27, 2026 10:56
Closes #178.

The engine pin moves to 4.0.0.a8. Until now every pin move orphaned every
database, because no 4.0 alpha ships upgradeImage. Grail now carries the
upgrade (GemTalk/Grail#1866, `scripts/upgrade_engine.sh --in-place`), so a
start that finds an a5 database upgrades it:

- `ensureRunning` reads the extent's version with copydbf (once per window
  and extent), after shared memory and before the processes start. For a
  pair in `ENGINE_UPGRADES` (a5 -> a8), it asks, modally, naming the backup.
- It stops the stone and listener with the PREVIOUS engine, under the stone
  lock: after an extension update the old stone is usually still up, and
  a8's stopstone cannot log in to an a5 stone (4044, measured). It refuses
  to run the script while a stone is still listed.
- It runs the bundled script with GEMSTONE = the new engine and GemDB's
  GEMSTONE_GLOBAL_DIR, under a temporary stone `gemdbupgrade`, and shows the
  script's latest `upgrade_engine: ` line as progress.
- On success it forgets the Grail and MCP stamps, so the start goes on to
  install both again, and says where the backup is. On failure the script
  has restored the extent; GemDB checks the version copydbf reads, says
  whether it is back, points at the backup and the log, and does not start.
- Auto-start and agents are not asked: they get a warning with an
  "Upgrade Database..." button that runs Start. The GemDB Shell's cold
  start meets the pre-start guard, which now says how to get the upgrade.

The guard (`assertDatabaseMatchesEngine`) now sorts the database into
matches / upgrade / older / newer / unknown. Older without a measured pair
and unknown are refused as before; newer is refused without the advice to
delete it. Setup lets an upgradable database through, since it touches
none. New telemetry outcomes: upgradeNeeded, upgradeDeclined, upgradeFailed.

bundle-grail.sh and check-vsix.sh now require scripts/upgrade_engine.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Temporarily the head of GemTalk/Grail PR #1867 (0c5e215), which carries
scripts/upgrade_engine.sh --in-place and the install's repair of a Grail an
engine upgrade has stripped. Move it to the merge commit once #1867 lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@srbaker srbaker self-assigned this Oct 27, 2026
srbaker and others added 5 commits October 27, 2026 12:29
…od fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The brain-freeze gate failed on #180 at 84850be: the upgrade's temporary
stone ran with the product's key and defaults, took the 8192 MB default
maximum, and refused GemDB's extent, which is pregrown to the key's
10240 MB. Grail's upgrade_engine.sh is gaining UPGRADE_ENGINE_STONE_CONFIG,
the caller's own stone configuration.

GemDB's stone reads two files, conf/system.conf (the cap, pregrow, the
threshold) and then conf/gemdb.conf (KEYFILE, the page cache), and neither
alone carries both the key and the cap. So the upgrade writes them, in that
reading order, to db/log/upgrade-stone.conf (after ensureSpaceLimits, as a
start would) and passes that.

The integration test passed because it copied a bare 335 MB extent into an
empty directory, so the script's stone never met a GemDB configuration. It
now builds the database the way an earlier GemDB did: createDatabase's
config, the previous engine's key, and the extent pregrown to 10240 MB by a
stone of the previous engine on GemDB's config (GEMDB_TEST_UPGRADE_ENGINE).
Against Grail 0c5e215, which ignores the variable, it now fails exactly as
the gate did ("FAILED while starting stone gemdbupgrade"), with the extent
restored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…config

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… ab9de40

The brain-freeze gate now upgrades a5 -> a8 with its data identical, but the
web app's `import email.message` was refused ("EmailMessage changed its
bases (Message -> MIMEPart)"): Grail replaced its own email package with
CPython's after c8cbc92. Grail ab9de40 repairs such a base change on a class
with no stored instances in scripts/redeployModules.gs, which Grail's
install.sh runs and GemDB's install-grail.sh did not.

install-grail.sh now runs it last, after gemdb and unicodedata, as Grail's
install.sh does (same topaz settings), so those two are already current and
left alone. A failure is a warning, as in Grail: the modules it could not
rebuild stay stale and rebuild on their next import. Every
`redeployModules:` line goes to the log, so a failed module is named.
bundle-grail.sh and check-vsix.sh now require scripts/redeployModules.gs,
which inputs nothing else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Upgrade an a5 database to a8 in place instead of asking users to delete it

1 participant