Skip to content

feat(docker): add an s3 stack profile for a real bucket as the cold tier - #38

Merged
nhobin219 merged 3 commits into
mainfrom
nhobin219/s3-cold-tier-profile
Jul 31, 2026
Merged

feat(docker): add an s3 stack profile for a real bucket as the cold tier#38
nhobin219 merged 3 commits into
mainfrom
nhobin219/s3-cold-tier-profile

Conversation

@nhobin219

@nhobin219 nhobin219 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Adds a third stack profile, s3, that points the cold tier at a real S3 bucket while Postgres stays local.

export PENCA_S3_BUCKET=my-penca-bucket
export PENCA_S3_REGION=us-west-1
just penca-up --profile s3 --db ~/.penca/data

Why

The OBJECT_STORAGE_* keys were literals in compose.yml, duplicated across query/write/lifecycle, so there was no env knob — pointing at a real bucket meant hand-writing a compose override file. That mechanism was documented only in a comment at the top of compose.yml, where nobody finds it.

What changed

  • One x-object-storage anchor merged into the three servicers, interpolated with the existing SeaweedFS values as defaults. dev.env / test.env say nothing about storage and resolve byte-identically to before. Three hand-synced copies of a block whose disagreement surfaces as an empty table rather than an error is the part worth deleting.
  • SeaweedFS split out of infra into its own profile. Postgres and the object store are separately external; "real bucket, local Postgres" was not expressible while both lived in infra. The s3 profile starts no gateway.
  • docker/s3.env requires PENCA_S3_BUCKET with no default — a silent fall-through to the dev bucket name would point a deployment at the wrong store, and reads against the wrong bucket look like an empty table, not a failure.
  • docs/development.md gains a "Backing the cold tier with a real S3 bucket" section and an s3 row in the profile table.

Compose semantics — measured, not assumed

Four behaviours this depends on were verified against docker compose directly. One would have shipped broken on reasoning alone:

Behaviour Result
${VAR:-default} blanked from an env file Impossible — substitutes on empty as well as unset
${VAR-default} (no colon) blanked from an env file Works — substitutes only when unset
${VAR:?msg} inside an env-file value Aborts the run with the custom message
YAML merge key inside environment: Supported
required: false with the dependency present Still waits (measured 9s vs 1s); no failure when the profile is off

Endpoint and both credential keys therefore use the no-colon form: a real-S3 deployment has to blank all three. An empty endpoint makes ObjectStorageConfig skip with_endpoint so the endpoint derives from the region; empty keys fall through to the standard AWS credential chain under an instance role. With ${VAR:-default} every S3 deployment would have stayed silently pinned to http://seaweedfs:8333.

Two fixes that only surfaced by running it

  • penca-down --profile s3 hit the required-var check, so you could bring a stack up in one shell and be unable to tear it down from another. penca-down and penca-logs now pass a placeholder — neither reads the bucket.
  • just echoes recipe body lines including comments, so the penca-logs note moved above the recipe.

Testing

  • just check — green.
  • End-to-end against a real bucket in us-west-1: CREATE TABLE + inserts over Flight SQL, forced PersistBranch/SnapshotBranch, re-read through the cold path, UPDATE/DELETE, flushed again. Objects landed under persist/, snapshot/, tx_log/; the SeaweedFS bucket stayed size:0 chunk:0, confirming nothing fell back to it.
  • dev and test profile service lists and resolved storage env compared before/after — identical.
  • Full integration suite on a fresh stack: 726 passed, 16 skipped, 0 failed — see the comment below.

Also in this PR

One README change, folded in at the author's request rather than split out: the intro said
catalog and branch metadata "is served from Postgres and stays that way," which reads as
Postgres being a permanent home for metadata. The intended end state is the opposite —
object storage is the only durable tier for anything Penca stores, and the metadata Postgres
serves today gets checkpointed out and reconstituted into Postgres tables at startup, so a
Postgres page is always rebuildable and never a system of record. Updated in both places the
claim appeared (intro + roadmap entry).

🤖 Generated with Claude Code

Nico Bautista Hobin and others added 2 commits July 31, 2026 02:27
Pointing Penca's cold tier at a real S3 bucket required hand-writing a
compose override file: the OBJECT_STORAGE_* keys were literals in
compose.yml, duplicated across query/write/lifecycle, so there was no
env knob to turn. The mechanism was documented only in a comment at the
top of compose.yml.

Collapse the three copies into one x-object-storage anchor merged into
each servicer, interpolated with the existing SeaweedFS values as
defaults, so dev.env and test.env stay silent about storage and resolve
byte-identically to before. Three hand-synced copies of a block whose
disagreement surfaces as an empty table rather than an error is the part
worth deleting.

Endpoint and both credential keys use the no-colon ${VAR-default} form.
A real-S3 deployment has to blank all three -- an empty endpoint is what
makes ObjectStorageConfig skip with_endpoint so the endpoint derives
from the region, and empty keys are what fall through to the standard
AWS credential chain under an instance role. ${VAR:-default} substitutes
on empty as well as unset, so it cannot express that; verified against
docker compose rather than assumed.

Split SeaweedFS and its bucket-init job out of the infra profile into
their own. Postgres and the object store are separately external, and
"real bucket, local Postgres" was not expressible while both lived in
infra. The servicers' dependency on seaweedfs-init becomes
required: false, which still waits when the job is present (measured:
9s vs 1s) and does not fail when the profile is off.

docker/s3.env requires PENCA_S3_BUCKET with no default. A silent
fall-through to the dev bucket name would point a deployment at the
wrong store, and reads against the wrong bucket surface as an empty
table rather than an error. penca-down and penca-logs pass a
placeholder, so a missing variable can never strand a running stack --
neither reads the bucket.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The intro said catalog and branch metadata "is served from Postgres and
stays that way," which reads as Postgres being a permanent home for
metadata. The intended end state is the opposite: object storage is the
only durable tier for anything Penca stores, and the metadata Postgres
serves today gets checkpointed out and reconstituted into Postgres
tables at startup, so a Postgres page is always rebuildable and never a
system of record.

Say that in both places the claim appears -- the intro paragraph and the
roadmap entry, which already described checkpoint-and-reload but stopped
at "recoverable from the object store alone" without naming the
consequence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nhobin219

Copy link
Copy Markdown
Contributor Author

Integration gate: passed

Full suite on a fresh test-profile stack (just penca-down + just integration-test, clean volumes).

Phase Result
Serial 66 passed, 676 deselected — 2m23s
Parallel (4 xdist workers) 660 passed, 16 skipped — 6m40s
Total 726 passed, 16 skipped, 0 failed, 0 errors

Zero FAILED / ERROR / Traceback lines in the run. This is the signal that matters here — branch CI skips the integration job via its changed-paths gate, and this PR reshapes the compose graph all 726 tests run against.

Checked for the two false-signal modes before trusting the green: no second just integration-test racing this one over the same COMPOSE_PROJECT_NAME (launched under setsid for that reason), and the stack was genuinely fresh — volumes were deleted beforehand, not reused.

"the roadmap checkpoints it to the object store" reads as the roadmap
performing the work. Checkpointing is what sits ON the roadmap. Recast
so the work is the subject and the roadmap is where it lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nhobin219
nhobin219 enabled auto-merge July 31, 2026 17:41
@nhobin219
nhobin219 added this pull request to the merge queue Jul 31, 2026
Merged via the queue into main with commit 877597e Jul 31, 2026
12 checks passed
@nhobin219
nhobin219 deleted the nhobin219/s3-cold-tier-profile branch July 31, 2026 18:02
@nhobin219

Copy link
Copy Markdown
Contributor Author

Post-merge review on 7040348 surfaced two Medium findings, both confirmed and fixed in #40: required: false also downgrades a failed dependency (removing CHA-542's fail-fast for dev/test), and seaweedfs-init still hardcoded the bucket name while the servicers had moved to ${OBJECT_STORAGE_BUCKET}.

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