Skip to content

feat: stack snapshots for fast dev iteration - #18

Merged
frrist merged 3 commits into
mainfrom
frrist/snapshots
Apr 22, 2026
Merged

frrist merged 3 commits into
mainfrom
frrist/snapshots

Conversation

@frrist

@frrist frrist commented Apr 21, 2026

Copy link
Copy Markdown
Member

Requires storacha/filecoin-services#16

Adds smelt snapshot save/load/list/rm plus make up SNAPSHOT=<name-or-path> so developers can capture a running stack (chain state, volumes, keys, proofs, manifest) and restore it later to skip the ~45s cold-boot cost of contract deploy + piri registration. Restored boots reach the same state in ~10s.

Core mechanics:

  • On save, the stack is stopped gracefully. The blockchain container's SIGTERM trap dumps anvil state to generated/snapshot-scratch/; smelt then archives every named docker volume via a busybox tar helper, copies keys/proofs/smelt.yml, and writes a manifest.json descriptor.
  • On load, the snapshot's smelt.yml becomes a session manifest at generated/snapshot-scratch/smelt.yml. While present, smelt generate and smelt snapshot save read from it; project-root smelt.yml is never modified. make clean / make nuke end the session.
  • Chain state bind-mount now points at generated/snapshot-scratch/ so make down's SIGTERM dump is what make up reads next — no more revert-to-baseline on restart.

Portability / shareability:

  • compose.yml pins name: smelt so docker volume names are stable across checkouts regardless of directory basename.
  • Snapshot descriptor captures per-service image tag AND content digest via docker image inspect, so load reports tag drift (teammate's .env differs) and digest drift at same tag (rolling tag re-pulled) as separate warnings.
  • Makefile's new check-docker target fails early on engine <25 (start_interval and top-level name: require 25+).

Required adjacent changes:

  • systems/common/compose.yml: dynamodb-local gets -dbPath + a volume so delegator's allow-list and upload's provider registry survive restarts (were previously in-memory only).
  • systems/upload/post_start.sh: idempotent — tolerates "already registered" from sprue when booting a snapshot that captured upload's provider registry.
  • Healthchecks across all services gain start_interval: 1s so service_healthy dependencies resolve ~1s after readiness instead of waiting for the next 5-10s interval tick.
  • systems/blockchain/compose.yml: healthcheck switched to test -f /tmp/ready sentinel (set by the entrypoint after contracts deploy and mockrpc is up) — more accurate than a port probe, and properly distinguishes init-mode-deploy-in-progress from ready.

Docs:

  • New docs/SNAPSHOTS.md covers everything above end-to-end.
  • docs/GETTING_STARTED.md updated: docker engine 25+ prerequisite, Linux/macOS only, correct upload command flow (space generate + source add + upload), piri-N naming, make clean/nuke semantics.
  • docs/TROUBLESHOOTING.md: new Prerequisites Failing section, piri → piri-0 in examples, cross-ref SNAPSHOTS.md.
  • docs/ARCHITECTURE.md: describes filecoin-services MODE=init|load and how smelt wires the SIGTERM-dump resume loop.
  • docs/EXTENDING.md: healthcheck template gains start_interval.
  • README.md + CLAUDE.md updated with snapshot commands and semantics.

Depends on filecoin-services load-mode SIGTERM dump changes (separate PR to storacha/filecoin-services).

@Peeja Peeja mentioned this pull request Apr 21, 2026
@frrist
frrist requested review from Peeja April 22, 2026 02:20

@alanshaw alanshaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in general I'm ok with this. As discussed, I'd ideally like the snapshots to live outside this repo and to be able to pull them in as requested but I understand that is a bunch more work and I can live with the current setup for the time being.

Comment thread pkg/stack/options.go
// Incompatible with WithSnapshot, WithPiriCount, WithPiriNodes. The
// SMELT_TEST_NO_SNAPSHOT env-var skip pattern applies the same as with
// WithSnapshot.
func WithEmbeddedSnapshot(name string) Option {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌

frrist added 3 commits April 22, 2026 13:37
Adds `smelt snapshot save/load/list/rm` plus `make up SNAPSHOT=<name-or-path>`
so developers can capture a running stack (chain state, volumes, keys,
proofs, manifest) and restore it later to skip the ~45s cold-boot cost
of contract deploy + piri registration. Restored boots reach the same
state in ~10s.

Core mechanics:

- On save, the stack is stopped gracefully. The blockchain container's
  SIGTERM trap dumps anvil state to generated/snapshot-scratch/; smelt
  then archives every named docker volume via a busybox tar helper,
  copies keys/proofs/smelt.yml, and writes a manifest.json descriptor.
- On load, the snapshot's smelt.yml becomes a *session manifest* at
  generated/snapshot-scratch/smelt.yml. While present, smelt generate
  and smelt snapshot save read from it; project-root smelt.yml is
  never modified. make clean / make nuke end the session.
- Chain state bind-mount now points at generated/snapshot-scratch/ so
  make down's SIGTERM dump is what make up reads next — no more
  revert-to-baseline on restart.

Portability / shareability:

- compose.yml pins `name: smelt` so docker volume names are stable
  across checkouts regardless of directory basename.
- Snapshot descriptor captures per-service image tag AND content digest
  via `docker image inspect`, so load reports tag drift (teammate's
  .env differs) and digest drift at same tag (rolling tag re-pulled)
  as separate warnings.
- Makefile's new check-docker target fails early on engine <25
  (start_interval and top-level `name:` require 25+).

Required adjacent changes:

- systems/common/compose.yml: dynamodb-local gets -dbPath + a volume
  so delegator's allow-list and upload's provider registry survive
  restarts (were previously in-memory only).
- systems/upload/post_start.sh: idempotent — tolerates "already
  registered" from sprue when booting a snapshot that captured
  upload's provider registry.
- Healthchecks across all services gain start_interval: 1s so
  service_healthy dependencies resolve ~1s after readiness instead of
  waiting for the next 5-10s interval tick.
- systems/blockchain/compose.yml: healthcheck switched to `test -f
  /tmp/ready` sentinel (set by the entrypoint after contracts deploy
  and mockrpc is up) — more accurate than a port probe, and properly
  distinguishes init-mode-deploy-in-progress from ready.

Docs:

- New docs/SNAPSHOTS.md covers everything above end-to-end.
- docs/GETTING_STARTED.md updated: docker engine 25+ prerequisite,
  Linux/macOS only, correct upload command flow (space generate +
  source add + upload), piri-N naming, make clean/nuke semantics.
- docs/TROUBLESHOOTING.md: new Prerequisites Failing section,
  piri → piri-0 in examples, cross-ref SNAPSHOTS.md.
- docs/ARCHITECTURE.md: describes filecoin-services MODE=init|load
  and how smelt wires the SIGTERM-dump resume loop.
- docs/EXTENDING.md: healthcheck template gains start_interval.
- README.md + CLAUDE.md updated with snapshot commands and semantics.
Previously every service published on a port identical to the default of
some common dev tool (3000, 5432, 6379, 8000, 8080, 8545, 9000, ...),
causing collisions on any machine that also runs those tools. Moves
every host-side port binding into a dedicated 15XXX block, sized and
positioned to avoid both common dev-tool defaults and the Linux
ephemeral port range. Container-internal ports and in-network DNS URLs
(http://indexer:80, ws://blockchain:8545) are unchanged.

Ports: blockchain 15000, dynamodb 15010, redis 15020, signing-service
15030, delegator 15040, indexer 15050, upload/sprue 15060, minio
15070/15071, piri-minio 15072/15073, smtp/smtp4dev 15080/15081, ipni
15090/15091/15092, piri-N 15100+N.

Does not address parallel SDK test collisions (issue #5) — two
concurrent stacks still collide on the same 15XXX ports. That needs
dynamic/ephemeral host ports or an SDK-only ports override and is a
separate change.
* feat: ephemeral host ports for SDK stack tests (#5)

Two `pkg/stack.NewStack` calls in parallel previously collided on fixed
host ports (8545, 8080, 4000+N, ...). This makes every SDK-provisioned
stack use ephemeral host ports so tests can run concurrently.

Changes in pkg/stack, applied to the extracted compose tree per test:
- Rewrite every `- "HOST:CONTAINER"` ports entry to `- "CONTAINER"` so
  Docker assigns a random host port. `PiriEndpointN`/`EmailEndpoint`
  already use `MappedPort` and pick up the ephemeral port automatically.
- Drop `external: true` from the root compose's storacha-network
  declaration so each stack gets a project-scoped network (compose
  namespaces it by project name). Parallel stacks otherwise share the
  one bridge and collide on service DNS names.
- Point sprue's `public_url` at `http://upload:80` so validation emails
  embed an in-network URL instead of a now-random host:port.

With the sprue URL in-network, the host can't click validation links
directly. Adds `ExecDoer` in pkg/clients/guppy that issues HTTP requests
via curl inside a stack container. `SMTP4DevLoginValidator` gains an
optional `Clicker` for the validation POST (defaults to
`http.DefaultClient`); `ContainerClient` wires in `ExecDoer{guppy}` so
the default in-container flow keeps working.

Closes #5.

* Addresses @alanshaw's review on #21 (with the direction @Peeja suggested
in the follow-up xref to #20).

Every host-side port mapping, the root network's `external:` flag, and
sprue's `public_url` move from hand-maintained fixed values (previously
rewritten at test time by pkg/stack/ports.go) to `${VAR:-default}`
interpolation in the compose files. `make up` leaves the vars unset and
gets the fixed 15XXX ports + external storacha-network. pkg/stack's SDK
test path passes an env map to composeStack.WithEnv, flipping every
port to ephemeral (Docker picks the host port, MappedPort resolves it),
SMELT_NETWORK_EXTERNAL=false (each test gets a project-scoped network),
and SPRUE_SERVER_PUBLIC_URL=http://upload:80 (validation emails embed
an in-network URL the ExecDoer-based clicker can reach).

- pkg/stack/ports.go + ports_test.go: deleted
- pkg/stack/testmode_env.go: new, defines the SMELT_* env map
- pkg/generate/compose.go: piri-N, piri-postgres, piri-minio all emit
  the interpolated form
- systems/*/compose.yml, compose.yml: every port + the network external
  flag wrapped; upload.compose.yml adds \`- SPRUE_SERVER_PUBLIC_URL\`
  pass-through
@frrist
frrist force-pushed the frrist/snapshots branch from d50e838 to 833d200 Compare April 22, 2026 20:42
@frrist
frrist merged commit 589acdb into main Apr 22, 2026
5 checks 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.

Checkpoint + restore a post-init stack to eliminate startup wait during dev

2 participants