region ha: relocation names a generation's files by main's one rule, <location>/files/g-<generation>/ (moves.GenerationFilesPrefix, the twin of GenerationPrefix), in its pinner and allowed-reference rule, build, prepared verifier, install, release flush and probe evidence; main's C8 provider (movesFilesPrefix) and the park view's files live set delegate to it, so a failover finds the provider's manifests, the provider consumes relocation's prepared manifest, and the park view classifies and pins relocation's files (merge round 6, finding 1).
Under the old files/<generation>/ layout the park view's classifier placed no relocation files object in a generation, so every park of the holder's location in the authoritative region refused (ErrUnsafe) and the files live set never read relocation's manifests: a stalled park, not a deletion. Relocation's tests now name the rule; the build fixture keeps files manifests in the versioned data bucket, as production's one bucket does. internal/sim models no files keys and is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
20 files changed,
+208
−65
internal/host/movesfiles.go+5 −4internal/host/movessources.go+1 −1internal/host/relocationbuild.go+2 −2internal/host/relocationbuild_test.go+9 −7internal/host/relocationfilesprefix_test.go+108 −0internal/host/relocationflush.go+2 −1internal/host/relocationflush_adoption_test.go+3 −3internal/host/relocationflush_saferead_linux_test.go+1 −1internal/host/relocationflush_wake_linux_test.go+1 −1internal/host/relocationinstall.go+3 −2internal/host/relocationinstall_linux_test.go+2 −1internal/host/relocationinstall_test.go+2 −2internal/host/relocationowners_test.go+2 −2internal/host/relocationwake.go+1 −1internal/host/relocationwake_test.go+1 −1internal/hostrelocate/pin.go+10 −6internal/hostrelocate/pin_envkey_test.go+2 −2internal/hostrelocate/pin_test.go+29 −28internal/moves/build.go+17 −0internal/moves/build_test.go+7 −0
diff --git a/internal/host/movesfiles.go b/internal/host/movesfiles.go| index d694da7b3..1b272f1e0 100644 |
| --- a/internal/host/movesfiles.go |
| +++ b/internal/host/movesfiles.go |
| @@ -127,9 +127,10 @@ func decodePreparedManifest(raw, key, op string, stamp uint64) (preparedManifest |
| // movesFilesPrefix is a generation's files prefix in its location, |
| // <location>/files/g-<generation>/, the rule pinFilesLiveSet and the park |
| -// view's files live set share (spec §7.6). |
| +// view's files live set share (spec §7.6), and region HA's relocation |
| +// with them: moves.GenerationFilesPrefix, the one rule. |
| func movesFilesPrefix(location, generation string) string { |
| - return strings.TrimSuffix(location, "/") + "/files/g-" + generation + "/" |
| + return moves.GenerationFilesPrefix(location, generation) |
| } |
| // movesFilesGeneration accepts a generation name as the claim does |
| @@ -547,7 +548,7 @@ func (f *movesFiles) movesFilesAllowed(ctx context.Context, o hostclaim.Observed |
| if err != nil { |
| return nil, err |
| } |
| - base := strings.TrimSuffix(location, "/") + "/files/g-" |
| + base := moves.FilesGenerationBase(location) |
| for _, origin := range named { |
| if set[origin] { |
| continue |
| @@ -585,7 +586,7 @@ func (f *movesFiles) movesFilesAllowed(ctx context.Context, o hostclaim.Observed |
| // set's (pinFilesLiveSet), so every object a live manifest names stays |
| // pinned under it. |
| func movesFilesOrigins(location, prefix string, m hostfiles.Manifest) ([]string, error) { |
| - base := strings.TrimSuffix(location, "/") + "/files/g-" |
| + base := moves.FilesGenerationBase(location) |
| seen := map[string]bool{} |
| var out []string |
| for _, entries := range []map[string]hostfiles.Entry{m.Entries, m.Platform} { |
diff --git a/internal/host/movessources.go b/internal/host/movessources.go| index bac3531ab..3b8a8ed1c 100644 |
| --- a/internal/host/movessources.go |
| +++ b/internal/host/movessources.go |
| @@ -297,7 +297,7 @@ func pinFilesLiveSet(ctx context.Context, data genselect.Reader, bucket, locatio |
| continue |
| } |
| seen[gen] = true |
| - prefix := location + "/files/g-" + gen + "/" |
| + prefix := movesFilesPrefix(location, gen) |
| key := hostfiles.ManifestKey(prefix) |
| var current *lease.ObjectVersion |
| for i := range listing { |
diff --git a/internal/host/relocationbuild.go b/internal/host/relocationbuild.go| index ec7dd5db0..897af6c1b 100644 |
| --- a/internal/host/relocationbuild.go |
| +++ b/internal/host/relocationbuild.go |
| @@ -103,7 +103,7 @@ func (a ClaimAssembly) relocationBuildP(cfg RelocationAssemblyConfig) func(conte |
| if err := check(); err != nil { |
| return err |
| } |
| - if _, err := hostrelocate.CopyPinnedManifest(alive, a.Claims, cfg.Data, pin.Files, c.Holder.Location+"/files/"+p+"/"); err != nil { |
| + if _, err := hostrelocate.CopyPinnedManifest(alive, a.Claims, cfg.Data, pin.Files, moves.GenerationFilesPrefix(c.Holder.Location, p)); err != nil { |
| return err |
| } |
| if pin.DB == nil { |
| @@ -237,7 +237,7 @@ func (v PreparedFromInstallManifest) Verify(ctx context.Context, o hostclaim.Obs |
| return hostclaim.PreparedAuthority{}, false, err |
| } |
| p := c.Op.Journal["prepared"] |
| - raw, _, err := v.Files.Get(ctx, hostfiles.ManifestKey(c.Holder.Location+"/files/"+p+"/")) |
| + raw, _, err := v.Files.Get(ctx, hostfiles.ManifestKey(moves.GenerationFilesPrefix(c.Holder.Location, p))) |
| if errors.Is(err, os.ErrNotExist) { |
| return hostclaim.PreparedAuthority{}, false, nil // not built yet |
| } |
diff --git a/internal/host/relocationbuild_test.go b/internal/host/relocationbuild_test.go| index 98914ad44..77d3c2b1e 100644 |
| --- a/internal/host/relocationbuild_test.go |
| +++ b/internal/host/relocationbuild_test.go |
| @@ -110,7 +110,9 @@ func newBuildFixtureWith(t *testing.T, o buildOptions) *buildFixture { |
| withDB := o.DB |
| f := &buildFixture{t: t, claims: release.NewDirStore(t.TempDir()), bucket: leasetest.NewVersionedBucket()} |
| f.bucket.AddBucket("data") |
| - f.files = &hookedStore{Store: release.NewDirStore(t.TempDir())} |
| + // P's files manifests and the pinned objects live in the versioned data |
| + // bucket, as production's files store and versioned client are one bucket. |
| + f.files = &hookedStore{Store: bucketStore{b: f.bucket, bucket: "data"}} |
| f.versions = &hookedVersions{VersionedStore: f.bucket} |
| handler, _ := leasetest.NewS3Server(f.bucket, "data") |
| srv := httptest.NewServer(handler) // 127.0.0.1 only |
| @@ -149,7 +151,7 @@ func newBuildFixtureWith(t *testing.T, o buildOptions) *buildFixture { |
| } else { |
| pin.Empty = "session-0" |
| } |
| - srcPrefix := location + "/files/g1/" |
| + srcPrefix := location + "/files/g-g1/" |
| pinned := hostfiles.Manifest{V: hostfiles.ManifestVersion, Seq: 5, Platform: map[string]hostfiles.Entry{}, Entries: map[string]hostfiles.Entry{}} |
| object := func(body string, mode uint32) hostfiles.Entry { |
| sum := sha256.Sum256([]byte(body)) |
| @@ -402,7 +404,7 @@ func TestPreparedRefusesAPartialBuild(t *testing.T) { |
| if err := f.build(); err == nil { |
| t.Fatal("the stopped build succeeded") |
| } |
| - if _, _, err := f.files.Get(t.Context(), hostfiles.ManifestKey(f.obs.Claim.Holder.Location+"/files/p1/")); err != nil { |
| + if _, _, err := f.files.Get(t.Context(), hostfiles.ManifestKey(f.obs.Claim.Holder.Location+"/files/g-p1/")); err != nil { |
| t.Fatalf("P's files manifest was not written first: %v", err) |
| } |
| if _, done, err := f.verifier.Verify(t.Context(), f.obs); done || err != nil { |
| @@ -424,7 +426,7 @@ func TestPreparedRefusesAPartialBuild(t *testing.T) { |
| f := newBuildFixture(t, true) |
| bad := hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}} |
| raw, _ := json.Marshal(bad) |
| - if _, err := f.files.Store.Put(t.Context(), hostfiles.ManifestKey(f.obs.Claim.Holder.Location+"/files/p1/"), raw, ""); err != nil { |
| + if _, err := f.files.Store.Put(t.Context(), hostfiles.ManifestKey(f.obs.Claim.Holder.Location+"/files/g-p1/"), raw, ""); err != nil { |
| t.Fatal(err) |
| } |
| if _, done, err := f.verifier.Verify(t.Context(), f.obs); done || !errors.Is(err, hostrelocate.ErrRefused) { |
| @@ -506,7 +508,7 @@ func TestRelocationBuildReplaysToTheSameBytes(t *testing.T) { |
| } |
| key := moves.InstallManifestKey(f.prefix()) |
| first := f.bucket.LatestID("data", key) |
| - filesKey := hostfiles.ManifestKey(f.obs.Claim.Holder.Location + "/files/p1/") |
| + filesKey := hostfiles.ManifestKey(f.obs.Claim.Holder.Location + "/files/g-p1/") |
| before, _, err := f.files.Get(t.Context(), filesKey) |
| if err != nil { |
| t.Fatal(err) |
| @@ -761,7 +763,7 @@ func TestRelocationBuildPChecksItsAuthorityBeforeEachStep(t *testing.T) { |
| if tc.at == 1 && f.bucket.Snapshot() != before { |
| t.Fatal("the replica lease was taken without authority") |
| } |
| - _, _, err := f.files.Get(t.Context(), hostfiles.ManifestKey(f.obs.Claim.Holder.Location+"/files/p1/")) |
| + _, _, err := f.files.Get(t.Context(), hostfiles.ManifestKey(f.obs.Claim.Holder.Location+"/files/g-p1/")) |
| if wrote := err == nil; wrote != (tc.at > 2) { |
| t.Fatalf("P's files manifest written=%v (%v)", wrote, err) |
| } |
| @@ -856,7 +858,7 @@ func TestPreparedRefusesAnythingButThisPinsBuild(t *testing.T) { |
| t.Fatalf("the untouched build: done=%v %v", done, err) |
| } |
| if tc.files != nil { |
| - key := hostfiles.ManifestKey(f.obs.Claim.Holder.Location + "/files/p1/") |
| + key := hostfiles.ManifestKey(f.obs.Claim.Holder.Location + "/files/g-p1/") |
| raw, etag, err := f.files.Get(ctx, key) |
| if err != nil { |
| t.Fatal(err) |
diff --git a/internal/host/relocationfilesprefix_test.go b/internal/host/relocationfilesprefix_test.go| new file mode 100644 |
| index 000000000..3ba62e1c0 |
| --- /dev/null |
| +++ b/internal/host/relocationfilesprefix_test.go |
| @@ -0,0 +1,108 @@ |
| +package host |
| + |
| +import ( |
| + "context" |
| + "os" |
| + "strings" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostfiles" |
| + "github.com/carlosframework/platform/internal/lease" |
| + "github.com/carlosframework/platform/internal/lease/leasetest" |
| + "github.com/carlosframework/platform/internal/park" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +// bucketStore is a release.Store over one bucket of a versioned bucket, |
| +// as production's S3 store is over the data bucket its versioned client |
| +// lists: current reads with the current version as the token, create-only |
| +// and compare-and-swap writes, deletes as delete markers. |
| +type bucketStore struct { |
| + b *leasetest.VersionedBucket |
| + bucket string |
| +} |
| + |
| +func (s bucketStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| + return casBucketStore(s).Get(ctx, key) |
| +} |
| + |
| +func (s bucketStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) { |
| + return casBucketStore(s).Put(ctx, key, body, ifMatch) |
| +} |
| + |
| +func (s bucketStore) List(ctx context.Context, prefix string) ([]string, error) { |
| + return casBucketStore(s).List(ctx, prefix) |
| +} |
| + |
| +func (s bucketStore) ListDirs(ctx context.Context, prefix string) ([]string, error) { |
| + keys, err := s.List(ctx, prefix) |
| + if err != nil { |
| + return nil, err |
| + } |
| + seen := map[string]bool{} |
| + var out []string |
| + for _, k := range keys { |
| + dir, _, ok := strings.Cut(strings.TrimPrefix(k, prefix), "/") |
| + if ok && !seen[dir] { |
| + seen[dir] = true |
| + out = append(out, dir) |
| + } |
| + } |
| + return out, nil |
| +} |
| + |
| +func (s bucketStore) Delete(_ context.Context, key string) error { |
| + if _, ok := s.b.Current(s.bucket, key); !ok { |
| + return os.ErrNotExist |
| + } |
| + s.b.BareDelete(s.bucket, key) |
| + return nil |
| +} |
| + |
| +var ( |
| + _ release.Store = bucketStore{} |
| + _ lease.VersionedStore = (*leasetest.VersionedBucket)(nil) |
| +) |
| + |
| +// Merge round 6, finding 1: relocation names a generation's files by the |
| +// one files prefix rule (moves.GenerationFilesPrefix), so what its build |
| +// writes is what main's provider and the park view read. P's prepared |
| +// manifest is at movesFilesPrefix(location, P); every files object under |
| +// the location belongs to a generation by the park view's classifier |
| +// (landingGenerationOf), where the old files/<generation>/ layout |
| +// belonged to none and refused every park of the location; and the files |
| +// live set pins every object P's manifest names, which it never read |
| +// under the old layout. |
| +func TestRelocationFilesUseTheParkViewsPrefix(t *testing.T) { |
| + f := newBuildFixtureWith(t, buildOptions{Files: map[string]string{"public/a.txt": "a", "uploads/b.bin": "b"}}) |
| + if err := f.build(); err != nil { |
| + t.Fatal(err) |
| + } |
| + ctx := t.Context() |
| + location := f.obs.Claim.Holder.Location |
| + m, found, err := hostfiles.Load(ctx, f.files, movesFilesPrefix(location, "p1")) |
| + if err != nil || !found || m.Seq != 0 || len(m.Entries) != 2 { |
| + t.Fatalf("P's prepared manifest at the files prefix rule: found=%v %+v %v", found, m, err) |
| + } |
| + listing, err := f.bucket.ListVersions(ctx, "data", location+"/files/") |
| + if err != nil || len(listing) == 0 { |
| + t.Fatalf("listing: %v", err) |
| + } |
| + for _, x := range listing { |
| + if g, ok := landingGenerationOf(location, x.Key); !ok || (g != "g1" && g != "p1") { |
| + t.Fatalf("%s belongs to %q (%v), not a generation", x.Key, g, ok) |
| + } |
| + } |
| + if _, ok := landingGenerationOf(location, location+"/files/p1/manifest.json"); ok { |
| + t.Fatal("the old files/<generation>/ layout is classified") |
| + } |
| + v := park.View{GenerationOf: func(r park.Ref) (string, bool) { return landingGenerationOf(location, r.Key) }} |
| + if err := pinFilesLiveSet(ctx, f.bucket, "data", location, listing, &v, "p1"); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, e := range m.Entries { |
| + if !v.Pinned[park.Ref{Key: e.Object, VersionID: f.bucket.LatestID("data", e.Object)}] { |
| + t.Fatalf("%s, which P's manifest names, is not pinned: %v", e.Object, v.Pinned) |
| + } |
| + } |
| +} |
diff --git a/internal/host/relocationflush.go b/internal/host/relocationflush.go| index 461cf9c69..4ee812d1a 100644 |
| --- a/internal/host/relocationflush.go |
| +++ b/internal/host/relocationflush.go |
| @@ -14,6 +14,7 @@ import ( |
| "github.com/carlosframework/platform/internal/hostfiles" |
| "github.com/carlosframework/platform/internal/instancefs" |
| "github.com/carlosframework/platform/internal/instancekey" |
| + "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| @@ -58,7 +59,7 @@ func (f ReleaseFlushFiles) Flush(ctx context.Context, obs hostclaim.Observed) (i |
| if envPath == "" || filesDir == "" { |
| return 0, fmt.Errorf("%w: no files directory or env file for %s", ErrClaimStopUnavailable, obs.Host) |
| } |
| - p, err := hostfiles.Open(ctx, f.Data, h.Location+"/files/"+h.Generation+"/") |
| + p, err := hostfiles.Open(ctx, f.Data, moves.GenerationFilesPrefix(h.Location, h.Generation)) |
| if err != nil { |
| return 0, err |
| } |
diff --git a/internal/host/relocationflush_adoption_test.go b/internal/host/relocationflush_adoption_test.go| index 5b57dd8dc..47dabf686 100644 |
| --- a/internal/host/relocationflush_adoption_test.go |
| +++ b/internal/host/relocationflush_adoption_test.go |
| @@ -62,7 +62,7 @@ func TestReleaseFlushPublishesOnlyTheReleasingWritersAdoption(t *testing.T) { |
| if !errors.Is(err, ErrClaimStopUnavailable) { |
| t.Fatalf("a malformed record was flushed: %v", err) |
| } |
| - if _, ok, err := hostfiles.Load(ctx, data, "instances/"+host+"/inc/files/g6/"); ok || err != nil { |
| + if _, ok, err := hostfiles.Load(ctx, data, "instances/"+host+"/inc/files/g-g6/"); ok || err != nil { |
| t.Fatalf("a refused flush committed a manifest: %v %v", ok, err) |
| } |
| return |
| @@ -70,7 +70,7 @@ func TestReleaseFlushPublishesOnlyTheReleasingWritersAdoption(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - prefix := "instances/" + host + "/inc/files/g6/" |
| + prefix := "instances/" + host + "/inc/files/g-g6/" |
| m, ok, err := hostfiles.Load(ctx, data, prefix) |
| if err != nil || !ok || m.Seq != seq { |
| t.Fatalf("manifest: %v %v", ok, err) |
| @@ -107,7 +107,7 @@ func TestReleaseFlushRefusesAHostWithNoPaths(t *testing.T) { |
| if _, err := f.Flush(ctx, obs); !errors.Is(err, ErrClaimStopUnavailable) { |
| t.Fatalf("flushed with no paths: %v", err) |
| } |
| - if _, ok, err := hostfiles.Load(ctx, data, "instances/"+host+"/inc/files/g6/"); ok || err != nil { |
| + if _, ok, err := hostfiles.Load(ctx, data, "instances/"+host+"/inc/files/g-g6/"); ok || err != nil { |
| t.Fatalf("a refused flush committed a manifest: %v %v", ok, err) |
| } |
| } |
diff --git a/internal/host/relocationflush_saferead_linux_test.go b/internal/host/relocationflush_saferead_linux_test.go| index 78d3beab6..d5bf76cc2 100644 |
| --- a/internal/host/relocationflush_saferead_linux_test.go |
| +++ b/internal/host/relocationflush_saferead_linux_test.go |
| @@ -187,7 +187,7 @@ func newFlushFixture(t *testing.T) *flushFixture { |
| env: []byte("CARLOS_INSTANCE_KEY=k1\n"), |
| record: instancekey.AdoptionRecord(writer, instancekey.SourcePlatform, instancekey.Digest("k1")), |
| envPath: filepath.Join(inst, host+".env"), recPath: filepath.Join(inst, host+instancekey.AdoptionSuffix), |
| - prefix: "instances/" + host + "/inc/files/g6/", |
| + prefix: "instances/" + host + "/inc/files/g-g6/", |
| } |
| if err := os.WriteFile(x.envPath, x.env, 0o600); err != nil { |
| t.Fatal(err) |
diff --git a/internal/host/relocationflush_wake_linux_test.go b/internal/host/relocationflush_wake_linux_test.go| index b24905683..2d698c31b 100644 |
| --- a/internal/host/relocationflush_wake_linux_test.go |
| +++ b/internal/host/relocationflush_wake_linux_test.go |
| @@ -57,7 +57,7 @@ func TestReleaseFlushFilesCarriesTheLastWakeStateAndEnv(t *testing.T) { |
| if err != nil || seq <= first { |
| t.Fatalf("second flush: %d %v", seq, err) |
| } |
| - prefix := "instances/" + host + "/inc/files/g6/" |
| + prefix := "instances/" + host + "/inc/files/g-g6/" |
| m, ok, err := hostfiles.Load(ctx, data, prefix) |
| if err != nil || !ok || m.Seq != seq { |
| t.Fatalf("manifest: %+v %v %v", m, ok, err) |
diff --git a/internal/host/relocationinstall.go b/internal/host/relocationinstall.go| index 3a493210a..8e3affdee 100644 |
| --- a/internal/host/relocationinstall.go |
| +++ b/internal/host/relocationinstall.go |
| @@ -22,6 +22,7 @@ import ( |
| "github.com/carlosframework/platform/internal/hostwrite" |
| "github.com/carlosframework/platform/internal/instancefs" |
| "github.com/carlosframework/platform/internal/instancekey" |
| + "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/registry" |
| "github.com/carlosframework/platform/internal/release" |
| @@ -401,7 +402,7 @@ func (a ClaimAssembly) relocationInstall(cfg RelocationAssemblyConfig) func(cont |
| return hostclaim.LocalInstall{}, err |
| } |
| p := c.Op.Journal["prepared"] |
| - prefix := c.Holder.Location + "/files/" + p + "/" |
| + prefix := moves.GenerationFilesPrefix(c.Holder.Location, p) |
| origins := append(append([]string(nil), pin.Files.Origins...), prefix) |
| // What P's manifest says about the env file and the wake state is |
| // judged before anything is written: a manifest missing either, |
| @@ -701,7 +702,7 @@ func (a ClaimAssembly) relocationVerifyLocal(cfg RelocationAssemblyConfig) func( |
| return false, nil |
| } |
| p := o.Claim.Op.Journal["prepared"] |
| - prefix := o.Claim.Holder.Location + "/files/" + p + "/" |
| + prefix := moves.GenerationFilesPrefix(o.Claim.Holder.Location, p) |
| m, found, err := hostfiles.Load(ctx, cfg.Data, prefix) |
| if err != nil { |
| return false, err |
diff --git a/internal/host/relocationinstall_linux_test.go b/internal/host/relocationinstall_linux_test.go| index 4396bc67a..c9c18fc12 100644 |
| --- a/internal/host/relocationinstall_linux_test.go |
| +++ b/internal/host/relocationinstall_linux_test.go |
| @@ -26,6 +26,7 @@ import ( |
| "github.com/carlosframework/platform/internal/hostrelocate" |
| "github.com/carlosframework/platform/internal/hostwrite" |
| "github.com/carlosframework/platform/internal/instancekey" |
| + "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/park" |
| "github.com/carlosframework/platform/internal/provision" |
| "github.com/carlosframework/platform/internal/registry" |
| @@ -821,7 +822,7 @@ func TestRelocationVerifyLocalReadsTheInstallBack(t *testing.T) { |
| // (hostfiles.Load); one that is gone is a refusal, not an install to |
| // repair (the install refuses without it too). |
| o := f.read() |
| - if err := f.cfg.Data.Delete(ctx, hostfiles.ManifestKey(o.Claim.Holder.Location+"/files/"+o.Claim.Op.Journal["prepared"]+"/")); err != nil { |
| + if err := f.cfg.Data.Delete(ctx, hostfiles.ManifestKey(moves.GenerationFilesPrefix(o.Claim.Holder.Location, o.Claim.Op.Journal["prepared"]))); err != nil { |
| t.Fatal(err) |
| } |
| if ok, err := verify(ctx, o, l); ok || !errors.Is(err, hostrelocate.ErrRefused) { |
diff --git a/internal/host/relocationinstall_test.go b/internal/host/relocationinstall_test.go| index c74178789..9782e8941 100644 |
| --- a/internal/host/relocationinstall_test.go |
| +++ b/internal/host/relocationinstall_test.go |
| @@ -69,8 +69,8 @@ func TestRelocationInstallPolicyIsTyped(t *testing.T) { |
| // missing one under a pin of published files, and a tombstone (which no |
| // publisher writes) under either pin, refuse. |
| func TestRelocationPlatformItemNeedsEvidenceForNone(t *testing.T) { |
| - published := hostrelocate.PinObject{Files: hostrelocate.FilesPin{Prefix: "loc/files/g1/", SHA256: "ab", Retained: "retained/ab"}} |
| - never := hostrelocate.PinObject{Files: hostrelocate.FilesPin{Prefix: "loc/files/g1/"}} |
| + published := hostrelocate.PinObject{Files: hostrelocate.FilesPin{Prefix: "loc/files/g-g1/", SHA256: "ab", Retained: "retained/ab"}} |
| + never := hostrelocate.PinObject{Files: hostrelocate.FilesPin{Prefix: "loc/files/g-g1/"}} |
| live := hostfiles.Manifest{Platform: map[string]hostfiles.Entry{hostfiles.EnvItem: {Object: "o", SHA256: "cd", Size: 1, Seq: 1}}} |
| missing := hostfiles.Manifest{Platform: map[string]hostfiles.Entry{}} |
| tomb := hostfiles.Manifest{Platform: map[string]hostfiles.Entry{hostfiles.EnvItem: {Deleted: true, Seq: 2}}} |
diff --git a/internal/host/relocationowners_test.go b/internal/host/relocationowners_test.go| index 941e9abc9..7398186b2 100644 |
| --- a/internal/host/relocationowners_test.go |
| +++ b/internal/host/relocationowners_test.go |
| @@ -125,7 +125,7 @@ func newROFixture(t *testing.T, o roOptions) *roFixture { |
| auth.InstallManifest, auth.SHA256, auth.SnapshotTXID = moves.InstallManifestKey(prefix), hex.EncodeToString(sum[:]), 42 |
| } |
| pm, _ := json.Marshal(hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}) |
| - if _, err := f.files.Put(ctx, hostfiles.ManifestKey(h.Location+"/files/"+roP+"/"), pm, ""); err != nil { |
| + if _, err := f.files.Put(ctx, hostfiles.ManifestKey(moves.GenerationFilesPrefix(h.Location, roP)), pm, ""); err != nil { |
| t.Fatal(err) |
| } |
| pmSum := sha256.Sum256(pm) |
| @@ -368,7 +368,7 @@ func TestRelocationAdmissionEvidenceBindsTheBaselineAndThePreparedManifest(t *te |
| } |
| // P's files manifest replaced after the build: the digest no longer |
| // matches the authority. |
| - key := hostfiles.ManifestKey(o.Claim.Holder.Location + "/files/" + roP + "/") |
| + key := hostfiles.ManifestKey(moves.GenerationFilesPrefix(o.Claim.Holder.Location, roP)) |
| _, etag, err := f.files.Get(ctx, key) |
| if err != nil { |
| t.Fatal(err) |
diff --git a/internal/host/relocationwake.go b/internal/host/relocationwake.go| index 540242350..484562fbc 100644 |
| --- a/internal/host/relocationwake.go |
| +++ b/internal/host/relocationwake.go |
| @@ -420,7 +420,7 @@ func (r *RelocationWakeInputs) AdmissionEvidence(ctx context.Context, o hostclai |
| if !in.KeepStamp || hostclaim.PreparedOpened(c) || in.PredecessorCertified != certified { |
| return hostbox.ErrChanged |
| } |
| - raw, _, err := r.FilesStore.Get(ctx, hostfiles.ManifestKey(h.Location+"/files/"+prepared+"/")) |
| + raw, _, err := r.FilesStore.Get(ctx, hostfiles.ManifestKey(moves.GenerationFilesPrefix(h.Location, prepared))) |
| if err != nil { |
| return err |
| } |
diff --git a/internal/host/relocationwake_test.go b/internal/host/relocationwake_test.go| index b8312514a..ee1248053 100644 |
| --- a/internal/host/relocationwake_test.go |
| +++ b/internal/host/relocationwake_test.go |
| @@ -64,7 +64,7 @@ func newRelocationProbeRig(t *testing.T, launch ClaimServingLaunch) *relocationP |
| imRaw := mustJSON(t, im) |
| bucket.Put("data", moves.InstallManifestKey(prefix), imRaw) |
| pm := mustJSON(t, hostfiles.Manifest{V: hostfiles.ManifestVersion, Entries: map[string]hostfiles.Entry{}, Platform: map[string]hostfiles.Entry{}}) |
| - if _, err := files.Put(ctx, hostfiles.ManifestKey(h.Location+"/files/"+roP+"/"), pm, ""); err != nil { |
| + if _, err := files.Put(ctx, hostfiles.ManifestKey(moves.GenerationFilesPrefix(h.Location, roP)), pm, ""); err != nil { |
| t.Fatal(err) |
| } |
| imSum, pmSum := sha256.Sum256(imRaw), sha256.Sum256(pm) |
diff --git a/internal/hostrelocate/pin.go b/internal/hostrelocate/pin.go| index 01c7cacb2..105375dd8 100644 |
| --- a/internal/hostrelocate/pin.go |
| +++ b/internal/hostrelocate/pin.go |
| @@ -16,6 +16,7 @@ import ( |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostfiles" |
| "github.com/carlosframework/platform/internal/instancekey" |
| + "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/park" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| @@ -243,7 +244,9 @@ var ErrFilesUnevidenced = errors.New("hostrelocate: the files evidence does not |
| // eight. An unchanged file published at g1 and carried by every later copy |
| // stays restorable at g10. Every referenced object must lie under this |
| // holder's location (its incarnation) in this region's data bucket, at |
| -// files/<generation>/objects/; any other reference refuses the pin. |
| +// files/g-<generation>/objects/ (moves.GenerationFilesPrefix, the files |
| +// prefix rule the C8 provider and the park view's files live set share); |
| +// any other reference refuses the pin. |
| func (p StorePinner) filesPin(ctx context.Context, writer func() (instancekey.Binding, error), h *hostclaim.Holder, releasedEmpty, needKey bool) (FilesPin, *KeyPin, []byte, error) { |
| chain := []string{h.Generation} |
| for i := len(h.Admissions) - 1; i >= 0; i-- { |
| @@ -256,7 +259,7 @@ func (p StorePinner) filesPin(ctx context.Context, writer func() (instancekey.Bi |
| continue |
| } |
| seen[gen] = true |
| - origins = append(origins, h.Location+"/files/"+gen+"/") |
| + origins = append(origins, moves.GenerationFilesPrefix(h.Location, gen)) |
| } |
| for _, prefix := range origins { |
| raw, _, err := p.Data.Get(ctx, hostfiles.ManifestKey(prefix)) |
| @@ -324,15 +327,16 @@ func (p StorePinner) filesPin(ctx context.Context, writer func() (instancekey.Bi |
| if gen == "" { |
| gen = hostclaim.RootGeneration |
| } |
| - prefix := h.Location + "/files/" + gen + "/" |
| + prefix := moves.GenerationFilesPrefix(h.Location, gen) |
| return FilesPin{Prefix: prefix, Origins: []string{prefix}}, nil, nil, nil |
| } |
| // referencedOrigins adds to origins the files prefix of every generation |
| // whose objects m references, each of which must be an object this |
| -// incarnation's location published: <location>/files/<generation>/objects/. |
| +// incarnation's location published: <location>/files/g-<generation>/objects/ |
| +// (moves.GenerationFilesPrefix). |
| func referencedOrigins(location string, origins []string, m hostfiles.Manifest) ([]string, error) { |
| - base := location + "/files/" |
| + base := moves.FilesGenerationBase(location) |
| have := make(map[string]bool, len(origins)) |
| for _, o := range origins { |
| have[o] = true |
| @@ -346,7 +350,7 @@ func referencedOrigins(location string, origins []string, m hostfiles.Manifest) |
| if !strings.HasPrefix(e.Object, base) || !ok || gen == "" || gen == "." || gen == ".." || !strings.HasPrefix(rest, "objects/") { |
| return fmt.Errorf("%w: %s references %s, outside %s: %w", ErrRefused, name, e.Object, base, hostfiles.ErrOrigin) |
| } |
| - if o := base + gen + "/"; !have[o] { |
| + if o := moves.GenerationFilesPrefix(location, gen); !have[o] { |
| have[o] = true |
| extra = append(extra, o) |
| } |
diff --git a/internal/hostrelocate/pin_envkey_test.go b/internal/hostrelocate/pin_envkey_test.go| index 3c0932dee..d41cbc87a 100644 |
| --- a/internal/hostrelocate/pin_envkey_test.go |
| +++ b/internal/hostrelocate/pin_envkey_test.go |
| @@ -78,7 +78,7 @@ func TestStorePinnerKeyEvidence(t *testing.T) { |
| item := func(name, body string, put bool) { |
| sum := sha256.Sum256([]byte(body)) |
| sha := hex.EncodeToString(sum[:]) |
| - obj := pinLocation + "/files/" + gen + "/objects/" + fmt.Sprintf("%016d-%s", 4, sha) |
| + obj := pinLocation + "/files/g-" + gen + "/objects/" + fmt.Sprintf("%016d-%s", 4, sha) |
| if put { |
| if _, err := data.Put(ctx, obj, []byte(body), ""); err != nil { |
| t.Fatal(err) |
| @@ -153,7 +153,7 @@ func TestStorePinnerConfigRecordNeedsTheEnvItem(t *testing.T) { |
| item := func(name, body string) { |
| sum := sha256.Sum256([]byte(body)) |
| sha := hex.EncodeToString(sum[:]) |
| - obj := pinLocation + "/files/g3/objects/" + fmt.Sprintf("%016d-%s", 4, sha) |
| + obj := pinLocation + "/files/g-g3/objects/" + fmt.Sprintf("%016d-%s", 4, sha) |
| if _, err := data.Put(ctx, obj, []byte(body), ""); err != nil { |
| t.Fatal(err) |
| } |
diff --git a/internal/hostrelocate/pin_test.go b/internal/hostrelocate/pin_test.go| index 2c491498a..95a8ddfa8 100644 |
| --- a/internal/hostrelocate/pin_test.go |
| +++ b/internal/hostrelocate/pin_test.go |
| @@ -15,6 +15,7 @@ import ( |
| "github.com/carlosframework/platform/internal/genselect" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| "github.com/carlosframework/platform/internal/hostfiles" |
| + "github.com/carlosframework/platform/internal/moves" |
| "github.com/carlosframework/platform/internal/park" |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| @@ -32,7 +33,7 @@ func pinClaim() hostclaim.Observed { |
| // every pinned manifest carries (withPlatformItems). |
| func keyed(m hostfiles.Manifest) hostfiles.Manifest { |
| sha := strings.Repeat("b", 64) |
| - m.Entries["instance.key"] = hostfiles.Entry{Object: pinLocation + "/files/g1/objects/0000000000000001-" + sha, SHA256: sha, Size: 32, Mode: 0o600, Seq: 1} |
| + m.Entries["instance.key"] = hostfiles.Entry{Object: pinLocation + "/files/g-g1/objects/0000000000000001-" + sha, SHA256: sha, Size: 32, Mode: 0o600, Seq: 1} |
| return withPlatformItems(m) |
| } |
| @@ -47,7 +48,7 @@ func withPlatformItems(m hostfiles.Manifest) hostfiles.Manifest { |
| func withItem(m hostfiles.Manifest, name, digit string) hostfiles.Manifest { |
| if _, ok := m.Platform[name]; !ok { |
| sha := strings.Repeat(digit, 64) |
| - m.Platform[name] = hostfiles.Entry{Object: pinLocation + "/files/g1/objects/0000000000000001-" + sha, SHA256: sha, Size: 10, Mode: 0o600, Seq: 1} |
| + m.Platform[name] = hostfiles.Entry{Object: pinLocation + "/files/g-g1/objects/0000000000000001-" + sha, SHA256: sha, Size: 10, Mode: 0o600, Seq: 1} |
| } |
| return m |
| } |
| @@ -67,7 +68,7 @@ func stagePlatformItems(t *testing.T, p *hostfiles.Publisher) { |
| func putManifest(t *testing.T, data release.Store, gen string, m hostfiles.Manifest) (string, []byte) { |
| t.Helper() |
| raw, _ := json.Marshal(m) |
| - key := hostfiles.ManifestKey(pinLocation + "/files/" + gen + "/") |
| + key := hostfiles.ManifestKey(moves.GenerationFilesPrefix(pinLocation, gen)) |
| if _, err := data.Put(context.Background(), key, raw, ""); err != nil { |
| t.Fatal(err) |
| } |
| @@ -98,14 +99,14 @@ func TestStorePinnerPinsTheNewestFilesManifestInTheChain(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if pin.FilesManifest != hostfiles.ManifestKey(pinLocation+"/files/g2/") || pin.FilesSeq != 5 || pin.MaxTXID != 41 || pin.Key != PinKey(host, "rel-1", 12) || pin.Empty { |
| + if pin.FilesManifest != hostfiles.ManifestKey(pinLocation+"/files/g-g2/") || pin.FilesSeq != 5 || pin.MaxTXID != 41 || pin.Key != PinKey(host, "rel-1", 12) || pin.Empty { |
| t.Fatalf("pin: %+v", pin) |
| } |
| obj, err := ReadPin(ctx, claims, pin) |
| if err != nil { |
| t.Fatal(err) |
| } |
| - if want := []string{pinLocation + "/files/g3/", pinLocation + "/files/g2/", pinLocation + "/files/g1/"}; !equalStrings(obj.Files.Origins, want) || obj.ClaimGen != 12 { |
| + if want := []string{pinLocation + "/files/g-g3/", pinLocation + "/files/g-g2/", pinLocation + "/files/g-g1/"}; !equalStrings(obj.Files.Origins, want) || obj.ClaimGen != 12 { |
| t.Fatalf("origins %v gen %d", obj.Files.Origins, obj.ClaimGen) |
| } |
| // The exact bytes are retained in the control bucket before the pin. |
| @@ -239,7 +240,7 @@ func TestStorePinnerPinsEmptyOnlyWithEvidence(t *testing.T) { |
| func TestStorePinnerRefusesAnUnreadableManifest(t *testing.T) { |
| ctx := context.Background() |
| claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| - if _, err := data.Put(ctx, hostfiles.ManifestKey(pinLocation+"/files/g3/"), []byte(`{"v":1}`), ""); err != nil { |
| + if _, err := data.Put(ctx, hostfiles.ManifestKey(pinLocation+"/files/g-g3/"), []byte(`{"v":1}`), ""); err != nil { |
| t.Fatal(err) |
| } |
| p := StorePinner{Claims: claims, Data: data, Release: routeRelease, Bucket: "data", Region: "ie", SelectDB: selectAt(41)} |
| @@ -266,7 +267,7 @@ func pinnedManifest(t *testing.T, claims, data release.Store, gen, origin string |
| if _, err := claims.Put(context.Background(), retained, raw, ""); err != nil { |
| t.Fatal(err) |
| } |
| - prefix := pinLocation + "/files/" + gen + "/" |
| + prefix := moves.GenerationFilesPrefix(pinLocation, gen) |
| return FilesPin{Prefix: prefix, Seq: seq, SHA256: manifestSHA, Origins: []string{prefix, origin}, Retained: retained}, m |
| } |
| @@ -274,19 +275,19 @@ func TestCopyPinnedManifestEnforcesThePin(t *testing.T) { |
| ctx := context.Background() |
| t.Run("a second relocation keeps objects of an earlier generation", func(t *testing.T) { |
| claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| - pin, want := pinnedManifest(t, claims, data, "P1", pinLocation+"/files/g1/", 3) |
| - got, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/P2/") |
| + pin, want := pinnedManifest(t, claims, data, "P1", pinLocation+"/files/g-g1/", 3) |
| + got, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/g-P2/") |
| if err != nil || got.Seq != 0 || want.Seq != 3 || got.Entries["a.txt"] != want.Entries["a.txt"] { |
| t.Fatalf("copy: %+v %v", got, err) |
| } |
| // A replay is a success; the copy's entries still name g1's object. |
| - if _, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/P2/"); err != nil { |
| + if _, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/g-P2/"); err != nil { |
| t.Fatalf("replay: %v", err) |
| } |
| // Without the earlier generation among the origins the copy refuses. |
| narrow := pin |
| narrow.Origins = []string{pin.Prefix} |
| - if _, err := CopyPinnedManifest(ctx, claims, data, narrow, pinLocation+"/files/P3/"); !errors.Is(err, hostfiles.ErrOrigin) { |
| + if _, err := CopyPinnedManifest(ctx, claims, data, narrow, pinLocation+"/files/g-P3/"); !errors.Is(err, hostfiles.ErrOrigin) { |
| t.Fatalf("an inherited object outside the origins: %v", err) |
| } |
| }) |
| @@ -294,7 +295,7 @@ func TestCopyPinnedManifestEnforcesThePin(t *testing.T) { |
| // A late PUT to the source changes nothing: P is written from the |
| // retained bytes, so an admitted pin stays usable (B2-4). |
| claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| - pin, m := pinnedManifest(t, claims, data, "P1", pinLocation+"/files/g1/", 3) |
| + pin, m := pinnedManifest(t, claims, data, "P1", pinLocation+"/files/g-g1/", 3) |
| m.Seq = 4 |
| m.Entries["late.txt"] = m.Entries["a.txt"] |
| raw, _ := json.Marshal(m) |
| @@ -302,14 +303,14 @@ func TestCopyPinnedManifestEnforcesThePin(t *testing.T) { |
| if _, err := data.Put(ctx, hostfiles.ManifestKey(pin.Prefix), raw, etag); err != nil { |
| t.Fatal(err) |
| } |
| - got, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/P2/") |
| + got, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/g-P2/") |
| if _, late := got.Entries["late.txt"]; err != nil || got.Seq != 0 || late || len(got.Entries) != 1 { |
| t.Fatalf("the pinned revision: %+v %v", got, err) |
| } |
| }) |
| t.Run("a retained copy that is not the pinned bytes", func(t *testing.T) { |
| claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| - pin, m := pinnedManifest(t, claims, data, "P1", pinLocation+"/files/g1/", 3) |
| + pin, m := pinnedManifest(t, claims, data, "P1", pinLocation+"/files/g-g1/", 3) |
| e := m.Entries["a.txt"] |
| e.Size = 6 |
| m.Entries["a.txt"] = e |
| @@ -318,21 +319,21 @@ func TestCopyPinnedManifestEnforcesThePin(t *testing.T) { |
| if _, err := tampered.Put(ctx, pin.Retained, raw, ""); err != nil { |
| t.Fatal(err) |
| } |
| - if _, err := CopyPinnedManifest(ctx, tampered, data, pin, pinLocation+"/files/P2/"); !errors.Is(err, ErrRefused) { |
| + if _, err := CopyPinnedManifest(ctx, tampered, data, pin, pinLocation+"/files/g-P2/"); !errors.Is(err, ErrRefused) { |
| t.Fatalf("bytes the pin did not vouch for were copied: %v", err) |
| } |
| - if _, _, err := data.Get(ctx, hostfiles.ManifestKey(pinLocation+"/files/P2/")); err == nil { |
| + if _, _, err := data.Get(ctx, hostfiles.ManifestKey(pinLocation+"/files/g-P2/")); err == nil { |
| t.Fatal("P's manifest was written from bytes the pin did not admit") |
| } |
| }) |
| t.Run("a host that never published files", func(t *testing.T) { |
| claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| - pin := FilesPin{Prefix: pinLocation + "/files/g3/", Origins: []string{pinLocation + "/files/g3/"}} |
| - m, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/P1/") |
| + pin := FilesPin{Prefix: pinLocation + "/files/g-g3/", Origins: []string{pinLocation + "/files/g-g3/"}} |
| + m, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/g-P1/") |
| if err != nil || m.Seq != 0 || len(m.Entries) != 0 { |
| t.Fatalf("empty manifest: %+v %v", m, err) |
| } |
| - if _, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/P1/"); err != nil { |
| + if _, err := CopyPinnedManifest(ctx, claims, data, pin, pinLocation+"/files/g-P1/"); err != nil { |
| t.Fatalf("replay: %v", err) |
| } |
| }) |
| @@ -373,7 +374,7 @@ func TestPinKeepsAFileOriginTheAdmissionListTrimmed(t *testing.T) { |
| gen := fmt.Sprintf("g%d", i) |
| o = step(o, hostclaim.AdmitWriter(hostclaim.Admission{Region: "ie", PredecessorRegion: "ie", ObservedSession: o.Claim.Holder.Session, ObservedGeneration: o.Claim.Holder.Generation, |
| Box: "src", Mode: "cold", NewGeneration: gen, PredecessorCertified: true})) |
| - prefix := loc + "/files/" + gen + "/" |
| + prefix := moves.GenerationFilesPrefix(loc, gen) |
| gens = append(gens, prefix) |
| if prev != "" { |
| if _, err := hostfiles.CopyManifest(ctx, data, prev, prefix, prevSeq, gens); err != nil { |
| @@ -411,8 +412,8 @@ func TestPinKeepsAFileOriginTheAdmissionListTrimmed(t *testing.T) { |
| t.Fatalf("g1 is still in the admission list: %+v", h.Admissions) |
| } |
| } |
| - m, _, err := hostfiles.Load(ctx, data, loc+"/files/g10/") |
| - if err != nil || !strings.HasPrefix(m.Entries["kept.txt"].Object, loc+"/files/g1/objects/") { |
| + m, _, err := hostfiles.Load(ctx, data, loc+"/files/g-g10/") |
| + if err != nil || !strings.HasPrefix(m.Entries["kept.txt"].Object, loc+"/files/g-g1/objects/") { |
| t.Fatalf("the unchanged file is not g1's object: %+v %v", m.Entries["kept.txt"], err) |
| } |
| obs := o |
| @@ -426,16 +427,16 @@ func TestPinKeepsAFileOriginTheAdmissionListTrimmed(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - got, err := CopyPinnedManifest(ctx, claims, data, obj.Files, loc+"/files/P1/") |
| + got, err := CopyPinnedManifest(ctx, claims, data, obj.Files, loc+"/files/g-P1/") |
| if err != nil || got.Entries["kept.txt"] != m.Entries["kept.txt"] { |
| t.Fatalf("copy with g1 trimmed from the admissions: %+v %v", got.Entries, err) |
| } |
| // A reference outside this incarnation's files is never admitted. |
| foreign := m |
| - foreign.Entries = map[string]hostfiles.Entry{"instance.key": m.Entries["instance.key"], "x.txt": {Object: "instances/" + host + "/other-inc/files/g1/objects/" + strings.Repeat("a", 64), Seq: 1, SHA256: strings.Repeat("a", 64)}} |
| + foreign.Entries = map[string]hostfiles.Entry{"instance.key": m.Entries["instance.key"], "x.txt": {Object: "instances/" + host + "/other-inc/files/g-g1/objects/" + strings.Repeat("a", 64), Seq: 1, SHA256: strings.Repeat("a", 64)}} |
| raw, _ := json.Marshal(foreign) |
| - _, etag, _ := data.Get(ctx, hostfiles.ManifestKey(loc+"/files/g10/")) |
| - if _, err := data.Put(ctx, hostfiles.ManifestKey(loc+"/files/g10/"), raw, etag); err != nil { |
| + _, etag, _ := data.Get(ctx, hostfiles.ManifestKey(loc+"/files/g-g10/")) |
| + if _, err := data.Put(ctx, hostfiles.ManifestKey(loc+"/files/g-g10/"), raw, etag); err != nil { |
| t.Fatal(err) |
| } |
| obs.Claim.Gen++ |
| @@ -452,7 +453,7 @@ func TestPinKeepsAFileOriginTheAdmissionListTrimmed(t *testing.T) { |
| func TestPreparedManifestIsSequenceZeroAndTheSessionPublishesOne(t *testing.T) { |
| ctx := context.Background() |
| claims, data := release.NewDirStore(t.TempDir()), release.NewDirStore(t.TempDir()) |
| - src := pinLocation + "/files/g3/" |
| + src := pinLocation + "/files/g-g3/" |
| p, err := hostfiles.Open(ctx, data, src) |
| if err != nil { |
| t.Fatal(err) |
| @@ -482,7 +483,7 @@ func TestPreparedManifestIsSequenceZeroAndTheSessionPublishesOne(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - prepared := pinLocation + "/files/P1/" |
| + prepared := pinLocation + "/files/g-P1/" |
| m, err := CopyPinnedManifest(ctx, claims, data, obj.Files, prepared) |
| if err != nil || m.Seq != 0 { |
| t.Fatalf("prepared manifest: %+v %v", m, err) |
diff --git a/internal/moves/build.go b/internal/moves/build.go| index f50022234..465cdb5f5 100644 |
| --- a/internal/moves/build.go |
| +++ b/internal/moves/build.go |
| @@ -76,6 +76,23 @@ func GenerationPrefix(location, op string) string { |
| return prefix |
| } |
| +// FilesGenerationBase is `<location>/files/g-`, the start of every |
| +// generation's files prefix in a location (GenerationFilesPrefix): what |
| +// follows it up to the next "/" is the generation. |
| +func FilesGenerationBase(location string) string { |
| + return strings.TrimSuffix(location, "/") + "/files/g-" |
| +} |
| + |
| +// GenerationFilesPrefix is `<location>/files/g-<generation>/` (spec §7.6): |
| +// a generation's files manifest and the content objects it published. It |
| +// is the one files prefix rule, GenerationPrefix's twin: the C8 files |
| +// provider (host.movesFilesPrefix), the park view's files live set and |
| +// classifier, and region HA's relocation (its pinner, build, install, |
| +// release flush and probe) all name a generation's files through it. |
| +func GenerationFilesPrefix(location, generation string) string { |
| + return FilesGenerationBase(location) + generation + "/" |
| +} |
| + |
| // SnapshotKey is the generation's level-9 snapshot [1, n]. |
| func SnapshotKey(prefix string, n uint64) string { |
| return prefix + fmt.Sprintf("%04x/", litestream.SnapshotLevel) + ltx.FormatFilename(1, ltx.TXID(n)) |
diff --git a/internal/moves/build_test.go b/internal/moves/build_test.go| index fa0096f87..d2b93ee5f 100644 |
| --- a/internal/moves/build_test.go |
| +++ b/internal/moves/build_test.go |
| @@ -347,6 +347,13 @@ func TestGenerationPrefixAndKeys(t *testing.T) { |
| if got := GenerationPrefix("instances/h/inc2", "op1"); got != "instances/h/inc2/g-op1/" { |
| t.Fatalf("GenerationPrefix with an incarnation = %q", got) |
| } |
| + // The files twin (spec §7.6, merge round 6): every reader and writer of |
| + // a generation's files names them by this rule. |
| + for _, loc := range []string{"instances/h/inc2", "instances/h/inc2/"} { |
| + if got := GenerationFilesPrefix(loc, "op1"); got != "instances/h/inc2/files/g-op1/" || !strings.HasPrefix(got, FilesGenerationBase(loc)) { |
| + t.Fatalf("GenerationFilesPrefix(%q) = %q", loc, got) |
| + } |
| + } |
| p := "instances/h/g-op1/" |
| if got := SnapshotKey(p, 7); got != p+"0009/0000000000000001-0000000000000007.ltx" { |
| t.Fatalf("SnapshotKey = %q", got) |