From a42fc2917e22dd46c4a5425533eee446ca0b0b80 Mon Sep 17 00:00:00 2001 From: Nico Bautista Hobin Date: Fri, 31 Jul 2026 15:00:20 -0400 Subject: [PATCH] fix(docker): restore seaweedfs-init fail-fast, interpolate its bucket Two review findings on #38. `required: false` is not scoped to "the dependency is absent". It equally downgrades a dependency that IS present but exited non-zero: compose logs a warning, starts the dependents anyway and exits 0. Measured against a dependency that exits 1: required: false -> warning, dependents start, compose exit 0 no required: false -> error, dependents do not start, compose exit 1 Putting it in compose.yml therefore deleted the fail-fast that seaweedfs-init's `s3.bucket.list` assertion exists to provide (CHA-542) for the dev and test profiles -- a failed bucket create would let the servicers boot against a nonexistent bucket and surface later as opaque NoSuchBucket errors. #38 verified the flag on the success path (waits) and the absent path (starts clean), but never the failure path. It cannot simply be dropped: with the seaweedfs profile off, the service is not in the project and compose rejects the whole thing with `depends on undefined service "seaweedfs-init": invalid compose project`. So move the relaxation into docker/compose.s3.yml, added by penca-up only for the s3 profile. The safe behavior becomes the default and the relaxation is opt-in, scoped to the one profile that cannot have the dependency; dev, test and any raw `docker compose -f docker/compose.yml` keep the hard gate. depends_on merges per key, so the fragment relaxes seaweedfs-init while postgres, bootstrap-init and write's lifecycle dependency are untouched. Second, seaweedfs-init still hardcoded `penca` for both the create and the awk assertion while the servicers moved to ${OBJECT_STORAGE_BUCKET:-penca}. Setting that variable with the seaweedfs profile on -- or leaking it as a shell export, which outranks --env-file for interpolation -- had the gateway create one bucket while the servicers opened another, which reads as an empty table rather than an error. Interpolate the same variable in the init job so the two cannot diverge. Co-Authored-By: Claude Opus 5 (1M context) --- Justfile | 10 ++++++++-- docker/compose.s3.yml | 40 ++++++++++++++++++++++++++++++++++++++++ docker/compose.yml | 28 +++++++++++----------------- 3 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 docker/compose.s3.yml diff --git a/Justfile b/Justfile index 575a877..1929e78 100644 --- a/Justfile +++ b/Justfile @@ -627,9 +627,15 @@ penca-up profile="dev" db="": vm-gc # `seaweedfs` (the in-stack S3 gateway + its bucket-init job) is dropped # for stack profiles whose cold tier is a real bucket — it would idle with # nothing to serve, and its bucket-init job would assert a bucket the - # deployment never reads. Add future real-bucket profiles to this case. + # deployment never reads. Those profiles also need compose.s3.yml, which + # relaxes the servicers' dependency on the now-absent seaweedfs-init; see + # that file for why the relaxation is opt-in rather than the default. + # Add future real-bucket profiles to this case. case "{{profile}}" in - s3) profiles="--profile infra --profile penca-backend" ;; + s3) + compose_files="$compose_files -f docker/compose.s3.yml" + profiles="--profile infra --profile penca-backend" + ;; *) profiles="--profile infra --profile seaweedfs --profile penca-backend" ;; esac diff --git a/docker/compose.s3.yml b/docker/compose.s3.yml new file mode 100644 index 0000000..fc7bacd --- /dev/null +++ b/docker/compose.s3.yml @@ -0,0 +1,40 @@ +# Override fragment for the `s3` stack profile, layered on docker/compose.yml: +# +# docker compose -f docker/compose.yml -f docker/compose.s3.yml \ +# --env-file docker/s3.env --profile infra --profile penca-backend up -d +# +# `just penca-up --profile s3` adds it automatically. +# +# Sole purpose: relax the servicers' dependency on `seaweedfs-init`. The `s3` +# profile points the cold tier at a real bucket, so the `seaweedfs` compose +# profile is off and that service is not part of the project at all — without +# `required: false` compose refuses the whole project with `service "query" +# depends on undefined service "seaweedfs-init": invalid compose project`. +# +# This lives here rather than in compose.yml because `required: false` is NOT +# scoped to "the dependency is absent". It equally downgrades a dependency that +# IS present but exited non-zero: compose logs a warning and starts the +# dependents anyway, exiting 0. In the base file it would therefore delete the +# fail-fast that seaweedfs-init's `s3.bucket.list` assertion exists to provide +# (CHA-542) for the dev and test profiles, letting the servicers boot against a +# nonexistent bucket and fail later as opaque NoSuchBucket errors. +# +# So the safe behavior is the default and the relaxation is opt-in, scoped to +# the one profile that cannot have the dependency. dev/test, and any raw +# `docker compose -f docker/compose.yml` invocation, keep the hard gate. +services: + query: + depends_on: + seaweedfs-init: + condition: service_completed_successfully + required: false + write: + depends_on: + seaweedfs-init: + condition: service_completed_successfully + required: false + lifecycle: + depends_on: + seaweedfs-init: + condition: service_completed_successfully + required: false diff --git a/docker/compose.yml b/docker/compose.yml index f5646c0..e1a62b5 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -127,6 +127,15 @@ services: # errors. Asserting the end state restores the fail-fast `mc mb` gave us. # `awk` on an exact field rather than `grep -w`, which treats `-` as a word # boundary and would accept a stray `penca-foo` as proof of `penca`. CHA-542. + # + # The bucket name comes from the same OBJECT_STORAGE_BUCKET the servicers + # read (see the x-object-storage anchor). Hardcoding `penca` here while they + # interpolate would let the gateway create one bucket and the servicers open + # another — and since the shell environment outranks `--env-file` for + # interpolation, a stray export was enough to cause it. That divergence + # surfaces as an empty table rather than an error, which is the failure mode + # the anchor exists to prevent; this is the fourth participant it has to + # cover. seaweedfs-init: image: chrislusf/seaweedfs:4.33 profiles: [seaweedfs] @@ -137,9 +146,9 @@ services: - /bin/sh - -c - | - echo 's3.bucket.create -name penca' | timeout 60 weed shell -master=seaweedfs:9333 + echo 's3.bucket.create -name ${OBJECT_STORAGE_BUCKET:-penca}' | timeout 60 weed shell -master=seaweedfs:9333 echo 's3.bucket.list' | timeout 60 weed shell -master=seaweedfs:9333 \ - | awk '$1 == "penca" { found = 1 } END { exit !found }' + | awk -v want='${OBJECT_STORAGE_BUCKET:-penca}' '$1 == want { found = 1 } END { exit !found }' # One-shot init job: seeds global Penca tables + the default catalog # row that SQL_SERVER_DEFAULT_CATALOG resolves against. All runtime @@ -180,11 +189,6 @@ services: condition: service_healthy seaweedfs-init: condition: service_completed_successfully - # `required: false` so the servicers still start when the `seaweedfs` - # profile is off (the `s3` profile points them at a real bucket, where - # the in-stack gateway has nothing to serve). When the profile IS on, - # this is still a hard wait on the bucket-create job. - required: false bootstrap-init: condition: service_completed_successfully environment: @@ -231,11 +235,6 @@ services: condition: service_healthy seaweedfs-init: condition: service_completed_successfully - # `required: false` so the servicers still start when the `seaweedfs` - # profile is off (the `s3` profile points them at a real bucket, where - # the in-stack gateway has nothing to serve). When the profile IS on, - # this is still a hard wait on the bucket-create job. - required: false bootstrap-init: condition: service_completed_successfully # CHA-273 rework: the write path connects to the lifecycle service at @@ -276,11 +275,6 @@ services: condition: service_healthy seaweedfs-init: condition: service_completed_successfully - # `required: false` so the servicers still start when the `seaweedfs` - # profile is off (the `s3` profile points them at a real bucket, where - # the in-stack gateway has nothing to serve). When the profile IS on, - # this is still a hard wait on the bucket-create job. - required: false bootstrap-init: condition: service_completed_successfully environment: