fix(snapshot): restore incarnation-aware repository tests
- commit
- f1d18226b34a50e2d013336e57edb07b932b1330
- author
- brice · · 2026-07-25 22:51 UTC
- parent
- c8509d8
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +79 -52
M internal/adapters/snapshot/legacy_load_test.go +5 -1
| @@ -47,7 +47,11 @@ func (r *Repository) Load(ctx context.Context, name string) (ports.SnapshotGener | |||
| 47 | 47 | if err := ctx.Err(); err != nil { | |
| 48 | 48 | return ports.SnapshotGeneration{}, err | |
| 49 | 49 | } | |
| 50 | - | key := sessionKey(name) | |
| 50 | + | id := legacyIncarnationID(name) | |
| 51 | + | key, err := incarnationKey(id) | |
| 52 | + | if err != nil { | |
| 53 | + | return ports.SnapshotGeneration{}, err | |
| 54 | + | } | |
| 51 | 55 | lock := r.lockSession(key) | |
| 52 | 56 | defer r.unlockSession(lock) | |
| 53 | 57 | if err := ctx.Err(); err != nil { | |
| @@ -47,7 +47,11 @@ func (r *Repository) Load(ctx context.Context, name string) (ports.SnapshotGener | |||||
| 47 | if err := ctx.Err(); err != nil { | 47 | if err := ctx.Err(); err != nil { | ||
| 48 | return ports.SnapshotGeneration{}, err | 48 | return ports.SnapshotGeneration{}, err | ||
| 49 | } | 49 | } | ||
| 50 | - | key := sessionKey(name) | 50 | + | id := legacyIncarnationID(name) |
| 51 | + | key, err := incarnationKey(id) | |||
| 52 | + | if err != nil { | |||
| 53 | + | return ports.SnapshotGeneration{}, err | |||
| 54 | + | } | |||
| 51 | lock := r.lockSession(key) | 55 | lock := r.lockSession(key) | ||
| 52 | defer r.unlockSession(lock) | 56 | defer r.unlockSession(lock) | ||
| 53 | if err := ctx.Err(); err != nil { | 57 | if err := ctx.Err(); err != nil { | ||
M internal/adapters/snapshot/load_bounds_test.go +5 -5
| @@ -29,7 +29,7 @@ func TestRepositoryListStopsAtTraversalBudget(t *testing.T) { | |||
| 29 | 29 | ||
| 30 | 30 | func TestRepositoryLoadStopsAtGenerationTraversalBudget(t *testing.T) { | |
| 31 | 31 | repo := NewRepository(privateDir(t)) | |
| 32 | - | key := sessionKey("named") | |
| 32 | + | key := legacyIncarnationID("named").String() | |
| 33 | 33 | generations := filepath.Join(repo.legacySessionPath(key), repositoryGenerations) | |
| 34 | 34 | if err := os.MkdirAll(generations, 0o700); err != nil { | |
| 35 | 35 | t.Fatal(err) | |
| @@ -53,7 +53,7 @@ func TestRepositoryLoadFallsBackNewestToOldestWithoutGenerationEnumeration(t *te | |||
| 53 | 53 | t.Fatal(err) | |
| 54 | 54 | } | |
| 55 | 55 | } | |
| 56 | - | key := sessionKey("named") | |
| 56 | + | key := legacyIncarnationID("named").String() | |
| 57 | 57 | publication := repositoryPublication(t, "named", 3, []byte("state-3")) | |
| 58 | 58 | if err := os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)); err != nil { | |
| 59 | 59 | t.Fatal(err) | |
| @@ -79,17 +79,17 @@ func TestRepositoryMaintainDoesNotCollectBeyondRetainedMetadataBudget(t *testing | |||
| 79 | 79 | if err := repo.Maintain(context.Background()); err != nil { | |
| 80 | 80 | t.Fatal(err) | |
| 81 | 81 | } | |
| 82 | - | if repo.maintenanceSessions[sessionKey("named")] == nil { | |
| 82 | + | if repo.maintenanceSessions[legacyIncarnationID("named").String()] == nil { | |
| 83 | 83 | break | |
| 84 | 84 | } | |
| 85 | 85 | if pass == maxMaintenanceMarkedGenerations*3-1 { | |
| 86 | 86 | t.Fatal("maintenance did not complete after bounded object-shard traversal") | |
| 87 | 87 | } | |
| 88 | 88 | } | |
| 89 | - | if _, err := os.Lstat(repo.legacyManifestPath(sessionKey("named"), 1)); err != nil { | |
| 89 | + | if _, err := os.Lstat(repo.legacyManifestPath(legacyIncarnationID("named").String(), 1)); err != nil { | |
| 90 | 90 | t.Fatalf("old manifest collected after mark budget overflow: %v", err) | |
| 91 | 91 | } | |
| 92 | - | if state := repo.maintenanceSessions[sessionKey("named")]; state != nil { | |
| 92 | + | if state := repo.maintenanceSessions[legacyIncarnationID("named").String()]; state != nil { | |
| 93 | 93 | t.Fatalf("overflow maintenance state retained = %#v, want reset", state) | |
| 94 | 94 | } | |
| 95 | 95 | } | |
| @@ -29,7 +29,7 @@ func TestRepositoryListStopsAtTraversalBudget(t *testing.T) { | |||||
| 29 | 29 | ||||
| 30 | func TestRepositoryLoadStopsAtGenerationTraversalBudget(t *testing.T) { | 30 | func TestRepositoryLoadStopsAtGenerationTraversalBudget(t *testing.T) { | ||
| 31 | repo := NewRepository(privateDir(t)) | 31 | repo := NewRepository(privateDir(t)) | ||
| 32 | - | key := sessionKey("named") | 32 | + | key := legacyIncarnationID("named").String() |
| 33 | generations := filepath.Join(repo.legacySessionPath(key), repositoryGenerations) | 33 | generations := filepath.Join(repo.legacySessionPath(key), repositoryGenerations) | ||
| 34 | if err := os.MkdirAll(generations, 0o700); err != nil { | 34 | if err := os.MkdirAll(generations, 0o700); err != nil { | ||
| 35 | t.Fatal(err) | 35 | t.Fatal(err) | ||
| @@ -53,7 +53,7 @@ func TestRepositoryLoadFallsBackNewestToOldestWithoutGenerationEnumeration(t *te | |||||
| 53 | t.Fatal(err) | 53 | t.Fatal(err) | ||
| 54 | } | 54 | } | ||
| 55 | } | 55 | } | ||
| 56 | - | key := sessionKey("named") | 56 | + | key := legacyIncarnationID("named").String() |
| 57 | publication := repositoryPublication(t, "named", 3, []byte("state-3")) | 57 | publication := repositoryPublication(t, "named", 3, []byte("state-3")) | ||
| 58 | if err := os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)); err != nil { | 58 | if err := os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)); err != nil { | ||
| 59 | t.Fatal(err) | 59 | t.Fatal(err) | ||
| @@ -79,17 +79,17 @@ func TestRepositoryMaintainDoesNotCollectBeyondRetainedMetadataBudget(t *testing | |||||
| 79 | if err := repo.Maintain(context.Background()); err != nil { | 79 | if err := repo.Maintain(context.Background()); err != nil { | ||
| 80 | t.Fatal(err) | 80 | t.Fatal(err) | ||
| 81 | } | 81 | } | ||
| 82 | - | if repo.maintenanceSessions[sessionKey("named")] == nil { | 82 | + | if repo.maintenanceSessions[legacyIncarnationID("named").String()] == nil { |
| 83 | break | 83 | break | ||
| 84 | } | 84 | } | ||
| 85 | if pass == maxMaintenanceMarkedGenerations*3-1 { | 85 | if pass == maxMaintenanceMarkedGenerations*3-1 { | ||
| 86 | t.Fatal("maintenance did not complete after bounded object-shard traversal") | 86 | t.Fatal("maintenance did not complete after bounded object-shard traversal") | ||
| 87 | } | 87 | } | ||
| 88 | } | 88 | } | ||
| 89 | - | if _, err := os.Lstat(repo.legacyManifestPath(sessionKey("named"), 1)); err != nil { | 89 | + | if _, err := os.Lstat(repo.legacyManifestPath(legacyIncarnationID("named").String(), 1)); err != nil { |
| 90 | t.Fatalf("old manifest collected after mark budget overflow: %v", err) | 90 | t.Fatalf("old manifest collected after mark budget overflow: %v", err) | ||
| 91 | } | 91 | } | ||
| 92 | - | if state := repo.maintenanceSessions[sessionKey("named")]; state != nil { | 92 | + | if state := repo.maintenanceSessions[legacyIncarnationID("named").String()]; state != nil { |
| 93 | t.Fatalf("overflow maintenance state retained = %#v, want reset", state) | 93 | t.Fatalf("overflow maintenance state retained = %#v, want reset", state) | ||
| 94 | } | 94 | } | ||
| 95 | } | 95 | } | ||
M internal/adapters/snapshot/load_major_test.go +3 -3
| @@ -25,14 +25,14 @@ func TestRepositoryCurrentGenerationPropagatesOperationalHeadErrors(t *testing.T | |||
| 25 | 25 | ||
| 26 | 26 | repo.hooks.beforeHeadRead = func(string) error { return syscall.EIO } | |
| 27 | 27 | ||
| 28 | - | _, _, err := repo.currentGeneration(context.Background(), "named", sessionKey("named")) | |
| 28 | + | _, _, err := repo.currentGeneration(context.Background(), "named", legacyIncarnationID("named").String()) | |
| 29 | 29 | require.ErrorIs(t, err, syscall.EIO) | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | 32 | func TestRepositoryLoadFallsBackFromInvalidHead(t *testing.T) { | |
| 33 | 33 | repo := NewRepository(privateDir(t)) | |
| 34 | 34 | require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state")))) | |
| 35 | - | require.NoError(t, os.WriteFile(repo.legacyHeadPath(sessionKey("named")), []byte("invalid"), 0o600)) | |
| 35 | + | require.NoError(t, os.WriteFile(repo.legacyHeadPath(legacyIncarnationID("named").String()), []byte("invalid"), 0o600)) | |
| 36 | 36 | ||
| 37 | 37 | got, err := repo.Load(context.Background(), "named") | |
| 38 | 38 | require.NoError(t, err) | |
| @@ -44,7 +44,7 @@ func TestRepositoryLoadFallsBackAcrossMultipleCorruptCandidates(t *testing.T) { | |||
| 44 | 44 | for generation := uint64(1); generation <= 4; generation++ { | |
| 45 | 45 | require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", generation, []byte{byte(generation)}))) | |
| 46 | 46 | } | |
| 47 | - | key := sessionKey("named") | |
| 47 | + | key := legacyIncarnationID("named").String() | |
| 48 | 48 | for _, generation := range []uint64{4, 3} { | |
| 49 | 49 | publication := repositoryPublication(t, "named", generation, []byte{byte(generation)}) | |
| 50 | 50 | require.NoError(t, os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest))) | |
| @@ -25,14 +25,14 @@ func TestRepositoryCurrentGenerationPropagatesOperationalHeadErrors(t *testing.T | |||||
| 25 | 25 | ||||
| 26 | repo.hooks.beforeHeadRead = func(string) error { return syscall.EIO } | 26 | repo.hooks.beforeHeadRead = func(string) error { return syscall.EIO } | ||
| 27 | 27 | ||||
| 28 | - | _, _, err := repo.currentGeneration(context.Background(), "named", sessionKey("named")) | 28 | + | _, _, err := repo.currentGeneration(context.Background(), "named", legacyIncarnationID("named").String()) |
| 29 | require.ErrorIs(t, err, syscall.EIO) | 29 | require.ErrorIs(t, err, syscall.EIO) | ||
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | func TestRepositoryLoadFallsBackFromInvalidHead(t *testing.T) { | 32 | func TestRepositoryLoadFallsBackFromInvalidHead(t *testing.T) { | ||
| 33 | repo := NewRepository(privateDir(t)) | 33 | repo := NewRepository(privateDir(t)) | ||
| 34 | require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state")))) | 34 | require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state")))) | ||
| 35 | - | require.NoError(t, os.WriteFile(repo.legacyHeadPath(sessionKey("named")), []byte("invalid"), 0o600)) | 35 | + | require.NoError(t, os.WriteFile(repo.legacyHeadPath(legacyIncarnationID("named").String()), []byte("invalid"), 0o600)) |
| 36 | 36 | ||||
| 37 | got, err := repo.Load(context.Background(), "named") | 37 | got, err := repo.Load(context.Background(), "named") | ||
| 38 | require.NoError(t, err) | 38 | require.NoError(t, err) | ||
| @@ -44,7 +44,7 @@ func TestRepositoryLoadFallsBackAcrossMultipleCorruptCandidates(t *testing.T) { | |||||
| 44 | for generation := uint64(1); generation <= 4; generation++ { | 44 | for generation := uint64(1); generation <= 4; generation++ { | ||
| 45 | require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", generation, []byte{byte(generation)}))) | 45 | require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", generation, []byte{byte(generation)}))) | ||
| 46 | } | 46 | } | ||
| 47 | - | key := sessionKey("named") | 47 | + | key := legacyIncarnationID("named").String() |
| 48 | for _, generation := range []uint64{4, 3} { | 48 | for _, generation := range []uint64{4, 3} { | ||
| 49 | publication := repositoryPublication(t, "named", generation, []byte{byte(generation)}) | 49 | publication := repositoryPublication(t, "named", generation, []byte{byte(generation)}) | ||
| 50 | require.NoError(t, os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest))) | 50 | require.NoError(t, os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest))) | ||
M internal/adapters/snapshot/maintenance_delete.go +12 -8
| @@ -328,37 +328,41 @@ func (r *Repository) Delete(ctx context.Context, name string) error { | |||
| 328 | 328 | if err := ctx.Err(); err != nil { | |
| 329 | 329 | return err | |
| 330 | 330 | } | |
| 331 | - | key := sessionKey(name) | |
| 331 | + | id := legacyIncarnationID(name) | |
| 332 | + | key, err := incarnationKey(id) | |
| 333 | + | if err != nil { | |
| 334 | + | return err | |
| 335 | + | } | |
| 332 | 336 | lock := r.lockSession(key) | |
| 333 | 337 | defer r.unlockSession(lock) | |
| 334 | 338 | if err := ctx.Err(); err != nil { | |
| 335 | 339 | return err | |
| 336 | 340 | } | |
| 337 | 341 | ||
| 338 | - | canonical := r.legacySessionPath(key) | |
| 342 | + | canonical := r.sessionPath(id) | |
| 339 | 343 | sessions := filepath.Dir(canonical) | |
| 340 | 344 | // A prior rename may have succeeded while its parent sync failed. Complete | |
| 341 | 345 | // that durability boundary before considering a canonical directory: this | |
| 342 | 346 | // also leaves a newly recreated session untouched. | |
| 343 | 347 | pending, err := r.pendingQuarantine(sessions, key) | |
| 344 | 348 | if err != nil { | |
| 345 | - | return fmt.Errorf("stat deleting snapshot session %q: %w", key, safeFilesystemError(err)) | |
| 349 | + | return fmt.Errorf("stat deleting snapshot session %q: %w", name, safeFilesystemError(err)) | |
| 346 | 350 | } | |
| 347 | 351 | if pending { | |
| 348 | 352 | if err := ctx.Err(); err != nil { | |
| 349 | 353 | return err | |
| 350 | 354 | } | |
| 351 | 355 | if err := r.syncDirectory(sessions); err != nil { | |
| 352 | - | return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err)) | |
| 356 | + | return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err)) | |
| 353 | 357 | } | |
| 354 | 358 | return nil | |
| 355 | 359 | } | |
| 356 | 360 | if dir, err := r.openDirectory(canonical); errors.Is(err, os.ErrNotExist) { | |
| 357 | 361 | return nil | |
| 358 | 362 | } else if err != nil { | |
| 359 | - | return fmt.Errorf("stat snapshot session %q: %w", key, safeFilesystemError(err)) | |
| 363 | + | return fmt.Errorf("stat snapshot session %q: %w", name, safeFilesystemError(err)) | |
| 360 | 364 | } else if err := dir.Close(); err != nil { | |
| 361 | - | return fmt.Errorf("close snapshot session %q: %w", key, safeFilesystemError(err)) | |
| 365 | + | return fmt.Errorf("close snapshot session %q: %w", name, safeFilesystemError(err)) | |
| 362 | 366 | } | |
| 363 | 367 | quarantine := filepath.Join(sessions, deletingSessionName(key)) | |
| 364 | 368 | if err := ctx.Err(); err != nil { | |
| @@ -368,13 +372,13 @@ func (r *Repository) Delete(ctx context.Context, name string) error { | |||
| 368 | 372 | // mark made for the old namespace before that replacement becomes possible. | |
| 369 | 373 | r.invalidateStorageEpoch(key) | |
| 370 | 374 | if err := r.rename(canonical, quarantine); err != nil { | |
| 371 | - | return fmt.Errorf("quarantine snapshot session %q: %w", key, safeFilesystemError(err)) | |
| 375 | + | return fmt.Errorf("quarantine snapshot session %q: %w", name, safeFilesystemError(err)) | |
| 372 | 376 | } | |
| 373 | 377 | if err := ctx.Err(); err != nil { | |
| 374 | 378 | return err | |
| 375 | 379 | } | |
| 376 | 380 | if err := r.syncDirectory(sessions); err != nil { | |
| 377 | - | return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err)) | |
| 381 | + | return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err)) | |
| 378 | 382 | } | |
| 379 | 383 | return nil | |
| 380 | 384 | } | |
| @@ -328,37 +328,41 @@ func (r *Repository) Delete(ctx context.Context, name string) error { | |||||
| 328 | if err := ctx.Err(); err != nil { | 328 | if err := ctx.Err(); err != nil { | ||
| 329 | return err | 329 | return err | ||
| 330 | } | 330 | } | ||
| 331 | - | key := sessionKey(name) | 331 | + | id := legacyIncarnationID(name) |
| 332 | + | key, err := incarnationKey(id) | |||
| 333 | + | if err != nil { | |||
| 334 | + | return err | |||
| 335 | + | } | |||
| 332 | lock := r.lockSession(key) | 336 | lock := r.lockSession(key) | ||
| 333 | defer r.unlockSession(lock) | 337 | defer r.unlockSession(lock) | ||
| 334 | if err := ctx.Err(); err != nil { | 338 | if err := ctx.Err(); err != nil { | ||
| 335 | return err | 339 | return err | ||
| 336 | } | 340 | } | ||
| 337 | 341 | ||||
| 338 | - | canonical := r.legacySessionPath(key) | 342 | + | canonical := r.sessionPath(id) |
| 339 | sessions := filepath.Dir(canonical) | 343 | sessions := filepath.Dir(canonical) | ||
| 340 | // A prior rename may have succeeded while its parent sync failed. Complete | 344 | // A prior rename may have succeeded while its parent sync failed. Complete | ||
| 341 | // that durability boundary before considering a canonical directory: this | 345 | // that durability boundary before considering a canonical directory: this | ||
| 342 | // also leaves a newly recreated session untouched. | 346 | // also leaves a newly recreated session untouched. | ||
| 343 | pending, err := r.pendingQuarantine(sessions, key) | 347 | pending, err := r.pendingQuarantine(sessions, key) | ||
| 344 | if err != nil { | 348 | if err != nil { | ||
| 345 | - | return fmt.Errorf("stat deleting snapshot session %q: %w", key, safeFilesystemError(err)) | 349 | + | return fmt.Errorf("stat deleting snapshot session %q: %w", name, safeFilesystemError(err)) |
| 346 | } | 350 | } | ||
| 347 | if pending { | 351 | if pending { | ||
| 348 | if err := ctx.Err(); err != nil { | 352 | if err := ctx.Err(); err != nil { | ||
| 349 | return err | 353 | return err | ||
| 350 | } | 354 | } | ||
| 351 | if err := r.syncDirectory(sessions); err != nil { | 355 | if err := r.syncDirectory(sessions); err != nil { | ||
| 352 | - | return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err)) | 356 | + | return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err)) |
| 353 | } | 357 | } | ||
| 354 | return nil | 358 | return nil | ||
| 355 | } | 359 | } | ||
| 356 | if dir, err := r.openDirectory(canonical); errors.Is(err, os.ErrNotExist) { | 360 | if dir, err := r.openDirectory(canonical); errors.Is(err, os.ErrNotExist) { | ||
| 357 | return nil | 361 | return nil | ||
| 358 | } else if err != nil { | 362 | } else if err != nil { | ||
| 359 | - | return fmt.Errorf("stat snapshot session %q: %w", key, safeFilesystemError(err)) | 363 | + | return fmt.Errorf("stat snapshot session %q: %w", name, safeFilesystemError(err)) |
| 360 | } else if err := dir.Close(); err != nil { | 364 | } else if err := dir.Close(); err != nil { | ||
| 361 | - | return fmt.Errorf("close snapshot session %q: %w", key, safeFilesystemError(err)) | 365 | + | return fmt.Errorf("close snapshot session %q: %w", name, safeFilesystemError(err)) |
| 362 | } | 366 | } | ||
| 363 | quarantine := filepath.Join(sessions, deletingSessionName(key)) | 367 | quarantine := filepath.Join(sessions, deletingSessionName(key)) | ||
| 364 | if err := ctx.Err(); err != nil { | 368 | if err := ctx.Err(); err != nil { | ||
| @@ -368,13 +372,13 @@ func (r *Repository) Delete(ctx context.Context, name string) error { | |||||
| 368 | // mark made for the old namespace before that replacement becomes possible. | 372 | // mark made for the old namespace before that replacement becomes possible. | ||
| 369 | r.invalidateStorageEpoch(key) | 373 | r.invalidateStorageEpoch(key) | ||
| 370 | if err := r.rename(canonical, quarantine); err != nil { | 374 | if err := r.rename(canonical, quarantine); err != nil { | ||
| 371 | - | return fmt.Errorf("quarantine snapshot session %q: %w", key, safeFilesystemError(err)) | 375 | + | return fmt.Errorf("quarantine snapshot session %q: %w", name, safeFilesystemError(err)) |
| 372 | } | 376 | } | ||
| 373 | if err := ctx.Err(); err != nil { | 377 | if err := ctx.Err(); err != nil { | ||
| 374 | return err | 378 | return err | ||
| 375 | } | 379 | } | ||
| 376 | if err := r.syncDirectory(sessions); err != nil { | 380 | if err := r.syncDirectory(sessions); err != nil { | ||
| 377 | - | return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err)) | 381 | + | return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err)) |
| 378 | } | 382 | } | ||
| 379 | return nil | 383 | return nil | ||
| 380 | } | 384 | } | ||
M internal/adapters/snapshot/maintenance_gc.go +1 -1
| @@ -174,7 +174,7 @@ func (r *Repository) markSession(ctx context.Context, key string, state *session | |||
| 174 | 174 | continue | |
| 175 | 175 | } | |
| 176 | 176 | manifest, err := codec.UnmarshalManifest(data) | |
| 177 | - | if err != nil || manifest.Generation != generation || sessionKey(manifest.Name) != key { | |
| 177 | + | if err != nil || manifest.Generation != generation || manifest.IncarnationID.String() != key { | |
| 178 | 178 | state.uncertain = true | |
| 179 | 179 | continue | |
| 180 | 180 | } | |
| @@ -174,7 +174,7 @@ func (r *Repository) markSession(ctx context.Context, key string, state *session | |||||
| 174 | continue | 174 | continue | ||
| 175 | } | 175 | } | ||
| 176 | manifest, err := codec.UnmarshalManifest(data) | 176 | manifest, err := codec.UnmarshalManifest(data) | ||
| 177 | - | if err != nil || manifest.Generation != generation || sessionKey(manifest.Name) != key { | 177 | + | if err != nil || manifest.Generation != generation || manifest.IncarnationID.String() != key { |
| 178 | state.uncertain = true | 178 | state.uncertain = true | ||
| 179 | continue | 179 | continue | ||
| 180 | } | 180 | } | ||
M internal/adapters/snapshot/maintenance_hardening_test.go +2 -2
| @@ -11,7 +11,7 @@ import ( | |||
| 11 | 11 | ||
| 12 | 12 | func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) { | |
| 13 | 13 | repo := NewRepository(privateDir(t)) | |
| 14 | - | key := sessionKey("named") | |
| 14 | + | key := legacyIncarnationID("named").String() | |
| 15 | 15 | objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | |
| 16 | 16 | const shards = maintenanceBatch * 2 | |
| 17 | 17 | for i := 0; i < shards; i++ { | |
| @@ -48,7 +48,7 @@ func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) | |||
| 48 | 48 | ||
| 49 | 49 | func TestRepositoryMaintainBoundsEmptyObjectTempShards(t *testing.T) { | |
| 50 | 50 | repo := NewRepository(privateDir(t)) | |
| 51 | - | key := sessionKey("named") | |
| 51 | + | key := legacyIncarnationID("named").String() | |
| 52 | 52 | objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | |
| 53 | 53 | const shards = maintenanceBatch * 2 | |
| 54 | 54 | for i := 0; i < shards; i++ { | |
| @@ -11,7 +11,7 @@ import ( | |||||
| 11 | 11 | ||||
| 12 | func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) { | 12 | func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) { | ||
| 13 | repo := NewRepository(privateDir(t)) | 13 | repo := NewRepository(privateDir(t)) | ||
| 14 | - | key := sessionKey("named") | 14 | + | key := legacyIncarnationID("named").String() |
| 15 | objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | 15 | objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | ||
| 16 | const shards = maintenanceBatch * 2 | 16 | const shards = maintenanceBatch * 2 | ||
| 17 | for i := 0; i < shards; i++ { | 17 | for i := 0; i < shards; i++ { | ||
| @@ -48,7 +48,7 @@ func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) | |||||
| 48 | 48 | ||||
| 49 | func TestRepositoryMaintainBoundsEmptyObjectTempShards(t *testing.T) { | 49 | func TestRepositoryMaintainBoundsEmptyObjectTempShards(t *testing.T) { | ||
| 50 | repo := NewRepository(privateDir(t)) | 50 | repo := NewRepository(privateDir(t)) | ||
| 51 | - | key := sessionKey("named") | 51 | + | key := legacyIncarnationID("named").String() |
| 52 | objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | 52 | objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | ||
| 53 | const shards = maintenanceBatch * 2 | 53 | const shards = maintenanceBatch * 2 | ||
| 54 | for i := 0; i < shards; i++ { | 54 | for i := 0; i < shards; i++ { | ||
M internal/adapters/snapshot/maintenance_test.go +12 -12
| @@ -296,7 +296,7 @@ func TestRepositoryDeleteQuarantinesCanonicalSessionBeforeCleanup(t *testing.T) | |||
| 296 | 296 | t.Fatal(err) | |
| 297 | 297 | } | |
| 298 | 298 | ||
| 299 | - | canonical := repo.legacySessionPath(sessionKey("named")) | |
| 299 | + | canonical := repo.legacySessionPath(legacyIncarnationID("named").String()) | |
| 300 | 300 | if _, err := os.Lstat(canonical); !errors.Is(err, os.ErrNotExist) { | |
| 301 | 301 | t.Fatalf("canonical session after Delete = %v, want not exist", err) | |
| 302 | 302 | } | |
| @@ -329,7 +329,7 @@ func TestRepositoryDeleteReturnsRootSyncFailureAfterQuarantine(t *testing.T) { | |||
| 329 | 329 | if err := repo.Delete(context.Background(), "named"); !errors.Is(err, injected) { | |
| 330 | 330 | t.Fatalf("Delete error = %v, want root sync failure", err) | |
| 331 | 331 | } | |
| 332 | - | if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) { | |
| 332 | + | if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) { | |
| 333 | 333 | t.Fatalf("canonical session after failed sync = %v, want not exist", err) | |
| 334 | 334 | } | |
| 335 | 335 | } | |
| @@ -366,7 +366,7 @@ func TestRepositoryDeleteWaitsForPublication(t *testing.T) { | |||
| 366 | 366 | if err := <-deleteDone; err != nil { | |
| 367 | 367 | t.Fatal(err) | |
| 368 | 368 | } | |
| 369 | - | if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) { | |
| 369 | + | if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) { | |
| 370 | 370 | t.Fatalf("canonical session after concurrent Delete = %v, want not exist", err) | |
| 371 | 371 | } | |
| 372 | 372 | } | |
| @@ -378,7 +378,7 @@ func TestRepositoryMaintainRetainsNewestTwoCompleteGenerations(t *testing.T) { | |||
| 378 | 378 | t.Fatal(err) | |
| 379 | 379 | } | |
| 380 | 380 | } | |
| 381 | - | key := sessionKey("named") | |
| 381 | + | key := legacyIncarnationID("named").String() | |
| 382 | 382 | for pass := 0; pass < maintenanceBatch; pass++ { | |
| 383 | 383 | if err := repo.Maintain(context.Background()); err != nil { | |
| 384 | 384 | t.Fatal(err) | |
| @@ -416,7 +416,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) { | |||
| 416 | 416 | t.Fatal(err) | |
| 417 | 417 | } | |
| 418 | 418 | } | |
| 419 | - | key := sessionKey("named") | |
| 419 | + | key := legacyIncarnationID("named").String() | |
| 420 | 420 | manifest := repositoryPublication(t, "named", 3, []byte{3}) | |
| 421 | 421 | if err := os.Remove(repo.legacyObjectPath(key, manifest.Objects[1].Digest)); err != nil { | |
| 422 | 422 | t.Fatal(err) | |
| @@ -435,7 +435,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) { | |||
| 435 | 435 | func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.T) { | |
| 436 | 436 | t.Run("publish", func(t *testing.T) { | |
| 437 | 437 | repo := NewRepository(privateDir(t)) | |
| 438 | - | key := sessionKey("named") | |
| 438 | + | key := legacyIncarnationID("named").String() | |
| 439 | 439 | lock := repo.lockSession(key) | |
| 440 | 440 | ctx, cancel := context.WithCancel(context.Background()) | |
| 441 | 441 | reached := make(chan struct{}) | |
| @@ -459,7 +459,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing. | |||
| 459 | 459 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | |
| 460 | 460 | t.Fatal(err) | |
| 461 | 461 | } | |
| 462 | - | key := sessionKey("named") | |
| 462 | + | key := legacyIncarnationID("named").String() | |
| 463 | 463 | for _, operation := range []struct { | |
| 464 | 464 | name string | |
| 465 | 465 | run func(context.Context) error | |
| @@ -494,7 +494,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing. | |||
| 494 | 494 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | |
| 495 | 495 | t.Fatal(err) | |
| 496 | 496 | } | |
| 497 | - | lock := repo.lockSession(sessionKey("named")) | |
| 497 | + | lock := repo.lockSession(legacyIncarnationID("named").String()) | |
| 498 | 498 | ctx, cancel := context.WithCancel(context.Background()) | |
| 499 | 499 | reached := make(chan struct{}) | |
| 500 | 500 | release := make(chan struct{}) | |
| @@ -600,7 +600,7 @@ func TestRepositoryMaintainClosesContinuationDirectories(t *testing.T) { | |||
| 600 | 600 | ||
| 601 | 601 | func TestRepositoryMaintainBoundsQueuedShardNames(t *testing.T) { | |
| 602 | 602 | repo := NewRepository(privateDir(t)) | |
| 603 | - | key := sessionKey("named") | |
| 603 | + | key := legacyIncarnationID("named").String() | |
| 604 | 604 | root := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | |
| 605 | 605 | for i := 0; i < maintenanceBatch*2; i++ { | |
| 606 | 606 | if err := os.MkdirAll(filepath.Join(root, fmt.Sprintf("%02x", i)), 0o700); err != nil { | |
| @@ -696,7 +696,7 @@ func TestRepositoryMaintainMarksLargeGenerationSetBeforeSweeping(t *testing.T) { | |||
| 696 | 696 | } | |
| 697 | 697 | newest = publication | |
| 698 | 698 | } | |
| 699 | - | key := sessionKey("named") | |
| 699 | + | key := legacyIncarnationID("named").String() | |
| 700 | 700 | stale := sha256.Sum256([]byte("stale object")) | |
| 701 | 701 | stalePath := repo.legacyObjectPath(key, stale) | |
| 702 | 702 | if err := os.MkdirAll(filepath.Dir(stalePath), 0o700); err != nil { | |
| @@ -869,7 +869,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin | |||
| 869 | 869 | } { | |
| 870 | 870 | t.Run(head.name, func(t *testing.T) { | |
| 871 | 871 | repo := NewRepository(privateDir(t)) | |
| 872 | - | key := sessionKey("named") | |
| 872 | + | key := legacyIncarnationID("named").String() | |
| 873 | 873 | publications := make([]ports.SnapshotPublication, 0, maintenanceBatch+2) | |
| 874 | 874 | for generation := uint64(1); generation <= maintenanceBatch+2; generation++ { | |
| 875 | 875 | publication := repositoryPublication(t, "named", generation, []byte(fmt.Sprintf("state-%d", generation))) | |
| @@ -921,7 +921,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin | |||
| 921 | 921 | ||
| 922 | 922 | func TestRepositoryMaintainRestartsMarkAfterFailedPublication(t *testing.T) { | |
| 923 | 923 | repo := NewRepository(privateDir(t)) | |
| 924 | - | key := sessionKey("named") | |
| 924 | + | key := legacyIncarnationID("named").String() | |
| 925 | 925 | first := publicationWithTailShard(t, "named", 1, "ff") | |
| 926 | 926 | stalePaths := make([]string, 0, maintenanceBatch+1) | |
| 927 | 927 | for i := 0; i < maintenanceBatch+1; i++ { | |
| @@ -296,7 +296,7 @@ func TestRepositoryDeleteQuarantinesCanonicalSessionBeforeCleanup(t *testing.T) | |||||
| 296 | t.Fatal(err) | 296 | t.Fatal(err) | ||
| 297 | } | 297 | } | ||
| 298 | 298 | ||||
| 299 | - | canonical := repo.legacySessionPath(sessionKey("named")) | 299 | + | canonical := repo.legacySessionPath(legacyIncarnationID("named").String()) |
| 300 | if _, err := os.Lstat(canonical); !errors.Is(err, os.ErrNotExist) { | 300 | if _, err := os.Lstat(canonical); !errors.Is(err, os.ErrNotExist) { | ||
| 301 | t.Fatalf("canonical session after Delete = %v, want not exist", err) | 301 | t.Fatalf("canonical session after Delete = %v, want not exist", err) | ||
| 302 | } | 302 | } | ||
| @@ -329,7 +329,7 @@ func TestRepositoryDeleteReturnsRootSyncFailureAfterQuarantine(t *testing.T) { | |||||
| 329 | if err := repo.Delete(context.Background(), "named"); !errors.Is(err, injected) { | 329 | if err := repo.Delete(context.Background(), "named"); !errors.Is(err, injected) { | ||
| 330 | t.Fatalf("Delete error = %v, want root sync failure", err) | 330 | t.Fatalf("Delete error = %v, want root sync failure", err) | ||
| 331 | } | 331 | } | ||
| 332 | - | if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) { | 332 | + | if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) { |
| 333 | t.Fatalf("canonical session after failed sync = %v, want not exist", err) | 333 | t.Fatalf("canonical session after failed sync = %v, want not exist", err) | ||
| 334 | } | 334 | } | ||
| 335 | } | 335 | } | ||
| @@ -366,7 +366,7 @@ func TestRepositoryDeleteWaitsForPublication(t *testing.T) { | |||||
| 366 | if err := <-deleteDone; err != nil { | 366 | if err := <-deleteDone; err != nil { | ||
| 367 | t.Fatal(err) | 367 | t.Fatal(err) | ||
| 368 | } | 368 | } | ||
| 369 | - | if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) { | 369 | + | if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) { |
| 370 | t.Fatalf("canonical session after concurrent Delete = %v, want not exist", err) | 370 | t.Fatalf("canonical session after concurrent Delete = %v, want not exist", err) | ||
| 371 | } | 371 | } | ||
| 372 | } | 372 | } | ||
| @@ -378,7 +378,7 @@ func TestRepositoryMaintainRetainsNewestTwoCompleteGenerations(t *testing.T) { | |||||
| 378 | t.Fatal(err) | 378 | t.Fatal(err) | ||
| 379 | } | 379 | } | ||
| 380 | } | 380 | } | ||
| 381 | - | key := sessionKey("named") | 381 | + | key := legacyIncarnationID("named").String() |
| 382 | for pass := 0; pass < maintenanceBatch; pass++ { | 382 | for pass := 0; pass < maintenanceBatch; pass++ { | ||
| 383 | if err := repo.Maintain(context.Background()); err != nil { | 383 | if err := repo.Maintain(context.Background()); err != nil { | ||
| 384 | t.Fatal(err) | 384 | t.Fatal(err) | ||
| @@ -416,7 +416,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) { | |||||
| 416 | t.Fatal(err) | 416 | t.Fatal(err) | ||
| 417 | } | 417 | } | ||
| 418 | } | 418 | } | ||
| 419 | - | key := sessionKey("named") | 419 | + | key := legacyIncarnationID("named").String() |
| 420 | manifest := repositoryPublication(t, "named", 3, []byte{3}) | 420 | manifest := repositoryPublication(t, "named", 3, []byte{3}) | ||
| 421 | if err := os.Remove(repo.legacyObjectPath(key, manifest.Objects[1].Digest)); err != nil { | 421 | if err := os.Remove(repo.legacyObjectPath(key, manifest.Objects[1].Digest)); err != nil { | ||
| 422 | t.Fatal(err) | 422 | t.Fatal(err) | ||
| @@ -435,7 +435,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) { | |||||
| 435 | func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.T) { | 435 | func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.T) { | ||
| 436 | t.Run("publish", func(t *testing.T) { | 436 | t.Run("publish", func(t *testing.T) { | ||
| 437 | repo := NewRepository(privateDir(t)) | 437 | repo := NewRepository(privateDir(t)) | ||
| 438 | - | key := sessionKey("named") | 438 | + | key := legacyIncarnationID("named").String() |
| 439 | lock := repo.lockSession(key) | 439 | lock := repo.lockSession(key) | ||
| 440 | ctx, cancel := context.WithCancel(context.Background()) | 440 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 441 | reached := make(chan struct{}) | 441 | reached := make(chan struct{}) | ||
| @@ -459,7 +459,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing. | |||||
| 459 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | 459 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | ||
| 460 | t.Fatal(err) | 460 | t.Fatal(err) | ||
| 461 | } | 461 | } | ||
| 462 | - | key := sessionKey("named") | 462 | + | key := legacyIncarnationID("named").String() |
| 463 | for _, operation := range []struct { | 463 | for _, operation := range []struct { | ||
| 464 | name string | 464 | name string | ||
| 465 | run func(context.Context) error | 465 | run func(context.Context) error | ||
| @@ -494,7 +494,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing. | |||||
| 494 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | 494 | if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil { | ||
| 495 | t.Fatal(err) | 495 | t.Fatal(err) | ||
| 496 | } | 496 | } | ||
| 497 | - | lock := repo.lockSession(sessionKey("named")) | 497 | + | lock := repo.lockSession(legacyIncarnationID("named").String()) |
| 498 | ctx, cancel := context.WithCancel(context.Background()) | 498 | ctx, cancel := context.WithCancel(context.Background()) | ||
| 499 | reached := make(chan struct{}) | 499 | reached := make(chan struct{}) | ||
| 500 | release := make(chan struct{}) | 500 | release := make(chan struct{}) | ||
| @@ -600,7 +600,7 @@ func TestRepositoryMaintainClosesContinuationDirectories(t *testing.T) { | |||||
| 600 | 600 | ||||
| 601 | func TestRepositoryMaintainBoundsQueuedShardNames(t *testing.T) { | 601 | func TestRepositoryMaintainBoundsQueuedShardNames(t *testing.T) { | ||
| 602 | repo := NewRepository(privateDir(t)) | 602 | repo := NewRepository(privateDir(t)) | ||
| 603 | - | key := sessionKey("named") | 603 | + | key := legacyIncarnationID("named").String() |
| 604 | root := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | 604 | root := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir) | ||
| 605 | for i := 0; i < maintenanceBatch*2; i++ { | 605 | for i := 0; i < maintenanceBatch*2; i++ { | ||
| 606 | if err := os.MkdirAll(filepath.Join(root, fmt.Sprintf("%02x", i)), 0o700); err != nil { | 606 | if err := os.MkdirAll(filepath.Join(root, fmt.Sprintf("%02x", i)), 0o700); err != nil { | ||
| @@ -696,7 +696,7 @@ func TestRepositoryMaintainMarksLargeGenerationSetBeforeSweeping(t *testing.T) { | |||||
| 696 | } | 696 | } | ||
| 697 | newest = publication | 697 | newest = publication | ||
| 698 | } | 698 | } | ||
| 699 | - | key := sessionKey("named") | 699 | + | key := legacyIncarnationID("named").String() |
| 700 | stale := sha256.Sum256([]byte("stale object")) | 700 | stale := sha256.Sum256([]byte("stale object")) | ||
| 701 | stalePath := repo.legacyObjectPath(key, stale) | 701 | stalePath := repo.legacyObjectPath(key, stale) | ||
| 702 | if err := os.MkdirAll(filepath.Dir(stalePath), 0o700); err != nil { | 702 | if err := os.MkdirAll(filepath.Dir(stalePath), 0o700); err != nil { | ||
| @@ -869,7 +869,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin | |||||
| 869 | } { | 869 | } { | ||
| 870 | t.Run(head.name, func(t *testing.T) { | 870 | t.Run(head.name, func(t *testing.T) { | ||
| 871 | repo := NewRepository(privateDir(t)) | 871 | repo := NewRepository(privateDir(t)) | ||
| 872 | - | key := sessionKey("named") | 872 | + | key := legacyIncarnationID("named").String() |
| 873 | publications := make([]ports.SnapshotPublication, 0, maintenanceBatch+2) | 873 | publications := make([]ports.SnapshotPublication, 0, maintenanceBatch+2) | ||
| 874 | for generation := uint64(1); generation <= maintenanceBatch+2; generation++ { | 874 | for generation := uint64(1); generation <= maintenanceBatch+2; generation++ { | ||
| 875 | publication := repositoryPublication(t, "named", generation, []byte(fmt.Sprintf("state-%d", generation))) | 875 | publication := repositoryPublication(t, "named", generation, []byte(fmt.Sprintf("state-%d", generation))) | ||
| @@ -921,7 +921,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin | |||||
| 921 | 921 | ||||
| 922 | func TestRepositoryMaintainRestartsMarkAfterFailedPublication(t *testing.T) { | 922 | func TestRepositoryMaintainRestartsMarkAfterFailedPublication(t *testing.T) { | ||
| 923 | repo := NewRepository(privateDir(t)) | 923 | repo := NewRepository(privateDir(t)) | ||
| 924 | - | key := sessionKey("named") | 924 | + | key := legacyIncarnationID("named").String() |
| 925 | first := publicationWithTailShard(t, "named", 1, "ff") | 925 | first := publicationWithTailShard(t, "named", 1, "ff") | ||
| 926 | stalePaths := make([]string, 0, maintenanceBatch+1) | 926 | stalePaths := make([]string, 0, maintenanceBatch+1) | ||
| 927 | for i := 0; i < maintenanceBatch+1; i++ { | 927 | for i := 0; i < maintenanceBatch+1; i++ { | ||
M internal/adapters/snapshot/path.go +9 -0
| @@ -27,6 +27,15 @@ func filenameForName(name string) string { | |||
| 27 | 27 | return "@" + hex.EncodeToString(sum[:])[:40] + ".snap" | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | + | // legacyIncarnationID is used only by retired name-keyed compatibility | |
| 31 | + | // operations. New callers must supply the durable catalogue incarnation ID. | |
| 32 | + | func legacyIncarnationID(name string) domain.IncarnationID { | |
| 33 | + | id := domain.IncarnationID{} | |
| 34 | + | digest := sha256.Sum256([]byte(name)) | |
| 35 | + | copy(id[:], digest[:]) | |
| 36 | + | return id | |
| 37 | + | } | |
| 38 | + | ||
| 30 | 39 | func incarnationKey(id domain.IncarnationID) (string, error) { | |
| 31 | 40 | if id == (domain.IncarnationID{}) { | |
| 32 | 41 | return "", fmt.Errorf("snapshot: zero incarnation ID") | |
| @@ -27,6 +27,15 @@ func filenameForName(name string) string { | |||||
| 27 | return "@" + hex.EncodeToString(sum[:])[:40] + ".snap" | 27 | return "@" + hex.EncodeToString(sum[:])[:40] + ".snap" | ||
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | + | // legacyIncarnationID is used only by retired name-keyed compatibility | |||
| 31 | + | // operations. New callers must supply the durable catalogue incarnation ID. | |||
| 32 | + | func legacyIncarnationID(name string) domain.IncarnationID { | |||
| 33 | + | id := domain.IncarnationID{} | |||
| 34 | + | digest := sha256.Sum256([]byte(name)) | |||
| 35 | + | copy(id[:], digest[:]) | |||
| 36 | + | return id | |||
| 37 | + | } | |||
| 38 | + | ||||
| 30 | func incarnationKey(id domain.IncarnationID) (string, error) { | 39 | func incarnationKey(id domain.IncarnationID) (string, error) { | ||
| 31 | if id == (domain.IncarnationID{}) { | 40 | if id == (domain.IncarnationID{}) { | ||
| 32 | return "", fmt.Errorf("snapshot: zero incarnation ID") | 41 | return "", fmt.Errorf("snapshot: zero incarnation ID") | ||
M internal/adapters/snapshot/path_security_test.go +1 -1
| @@ -153,7 +153,7 @@ func TestRepositoryRejectsSymlinkedGenerationAndObjectShards(t *testing.T) { | |||
| 153 | 153 | if err := repo.Publish(context.Background(), publication); err != nil { | |
| 154 | 154 | t.Fatal(err) | |
| 155 | 155 | } | |
| 156 | - | key := sessionKey(publication.Name) | |
| 156 | + | key := legacyIncarnationID(publication.Name).String() | |
| 157 | 157 | inside := filepath.Join(repo.legacySessionPath(key), target) | |
| 158 | 158 | outside := t.TempDir() | |
| 159 | 159 | guard := filepath.Join(outside, "must-not-change") | |
| @@ -153,7 +153,7 @@ func TestRepositoryRejectsSymlinkedGenerationAndObjectShards(t *testing.T) { | |||||
| 153 | if err := repo.Publish(context.Background(), publication); err != nil { | 153 | if err := repo.Publish(context.Background(), publication); err != nil { | ||
| 154 | t.Fatal(err) | 154 | t.Fatal(err) | ||
| 155 | } | 155 | } | ||
| 156 | - | key := sessionKey(publication.Name) | 156 | + | key := legacyIncarnationID(publication.Name).String() |
| 157 | inside := filepath.Join(repo.legacySessionPath(key), target) | 157 | inside := filepath.Join(repo.legacySessionPath(key), target) | ||
| 158 | outside := t.TempDir() | 158 | outside := t.TempDir() | ||
| 159 | guard := filepath.Join(outside, "must-not-change") | 159 | guard := filepath.Join(outside, "must-not-change") | ||
M internal/adapters/snapshot/publish.go +7 -3
| @@ -194,9 +194,13 @@ func (r *Repository) currentIncarnationPublication(ctx context.Context, publicat | |||
| 194 | 194 | if manifest.IncarnationID != publication.IncarnationID || manifest.Generation != generation { | |
| 195 | 195 | return 0, nil, nil, fmt.Errorf("current manifest identity mismatch") | |
| 196 | 196 | } | |
| 197 | - | wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest} | |
| 198 | - | if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) { | |
| 199 | - | return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint") | |
| 197 | + | // Parent checkpoints are optional in manifest v2. When a publisher supplies | |
| 198 | + | // one, it must bind the next generation to the authoritative current HEAD. | |
| 199 | + | if publication.ParentCheckpoint != nil { | |
| 200 | + | wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest} | |
| 201 | + | if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) { | |
| 202 | + | return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint") | |
| 203 | + | } | |
| 200 | 204 | } | |
| 201 | 205 | refs := manifestRefs(manifest) | |
| 202 | 206 | if refs == nil || !withinGenerationBudget(len(data), refs) { | |
| @@ -194,9 +194,13 @@ func (r *Repository) currentIncarnationPublication(ctx context.Context, publicat | |||||
| 194 | if manifest.IncarnationID != publication.IncarnationID || manifest.Generation != generation { | 194 | if manifest.IncarnationID != publication.IncarnationID || manifest.Generation != generation { | ||
| 195 | return 0, nil, nil, fmt.Errorf("current manifest identity mismatch") | 195 | return 0, nil, nil, fmt.Errorf("current manifest identity mismatch") | ||
| 196 | } | 196 | } | ||
| 197 | - | wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest} | 197 | + | // Parent checkpoints are optional in manifest v2. When a publisher supplies |
| 198 | - | if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) { | 198 | + | // one, it must bind the next generation to the authoritative current HEAD. |
| 199 | - | return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint") | 199 | + | if publication.ParentCheckpoint != nil { |
| 200 | + | wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest} | |||
| 201 | + | if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) { | |||
| 202 | + | return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint") | |||
| 203 | + | } | |||
| 200 | } | 204 | } | ||
| 201 | refs := manifestRefs(manifest) | 205 | refs := manifestRefs(manifest) | ||
| 202 | if refs == nil || !withinGenerationBudget(len(data), refs) { | 206 | if refs == nil || !withinGenerationBudget(len(data), refs) { | ||
M internal/adapters/snapshot/publish_efficiency_test.go +5 -4
| @@ -119,7 +119,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) { | |||
| 119 | 119 | } | |
| 120 | 120 | second := repositoryPublication(t, "named", 2, []byte("second")) | |
| 121 | 121 | newTail := second.Objects[0] | |
| 122 | - | key := sessionKey(second.Name) | |
| 122 | + | key := legacyIncarnationID(second.Name).String() | |
| 123 | 123 | if err := os.MkdirAll(filepath.Dir(repo.legacyObjectPath(key, newTail.Digest)), 0o700); err != nil { | |
| 124 | 124 | t.Fatal(err) | |
| 125 | 125 | } | |
| @@ -143,6 +143,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) { | |||
| 143 | 143 | ||
| 144 | 144 | func largeIncrementalPublications(t testing.TB, name string, count int) (ports.SnapshotPublication, ports.SnapshotPublication) { | |
| 145 | 145 | t.Helper() | |
| 146 | + | id := legacyIncarnationID(name) | |
| 146 | 147 | sealed := make([]codec.ObjectRef, 0, count) | |
| 147 | 148 | objects := make([]ports.SnapshotObject, 0, count+2) | |
| 148 | 149 | for i := range count { | |
| @@ -163,18 +164,18 @@ func largeIncrementalPublications(t testing.TB, name string, count int) (ports.S | |||
| 163 | 164 | if err != nil { | |
| 164 | 165 | t.Fatal(err) | |
| 165 | 166 | } | |
| 166 | - | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) | |
| 167 | + | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) | |
| 167 | 168 | if err != nil { | |
| 168 | 169 | t.Fatal(err) | |
| 169 | 170 | } | |
| 170 | - | return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)} | |
| 171 | + | return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)} | |
| 171 | 172 | } | |
| 172 | 173 | return makePublication(1, "tail-1", "visible-1"), makePublication(2, "tail-2", "visible-2") | |
| 173 | 174 | } | |
| 174 | 175 | ||
| 175 | 176 | func seedCompletePublication(t testing.TB, repo *Repository, publication ports.SnapshotPublication) { | |
| 176 | 177 | t.Helper() | |
| 177 | - | key := sessionKey(publication.Name) | |
| 178 | + | key := legacyIncarnationID(publication.Name).String() | |
| 178 | 179 | if err := repo.ensureSession(publication.IncarnationID); err != nil { | |
| 179 | 180 | t.Fatal(err) | |
| 180 | 181 | } | |
| @@ -119,7 +119,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) { | |||||
| 119 | } | 119 | } | ||
| 120 | second := repositoryPublication(t, "named", 2, []byte("second")) | 120 | second := repositoryPublication(t, "named", 2, []byte("second")) | ||
| 121 | newTail := second.Objects[0] | 121 | newTail := second.Objects[0] | ||
| 122 | - | key := sessionKey(second.Name) | 122 | + | key := legacyIncarnationID(second.Name).String() |
| 123 | if err := os.MkdirAll(filepath.Dir(repo.legacyObjectPath(key, newTail.Digest)), 0o700); err != nil { | 123 | if err := os.MkdirAll(filepath.Dir(repo.legacyObjectPath(key, newTail.Digest)), 0o700); err != nil { | ||
| 124 | t.Fatal(err) | 124 | t.Fatal(err) | ||
| 125 | } | 125 | } | ||
| @@ -143,6 +143,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) { | |||||
| 143 | 143 | ||||
| 144 | func largeIncrementalPublications(t testing.TB, name string, count int) (ports.SnapshotPublication, ports.SnapshotPublication) { | 144 | func largeIncrementalPublications(t testing.TB, name string, count int) (ports.SnapshotPublication, ports.SnapshotPublication) { | ||
| 145 | t.Helper() | 145 | t.Helper() | ||
| 146 | + | id := legacyIncarnationID(name) | |||
| 146 | sealed := make([]codec.ObjectRef, 0, count) | 147 | sealed := make([]codec.ObjectRef, 0, count) | ||
| 147 | objects := make([]ports.SnapshotObject, 0, count+2) | 148 | objects := make([]ports.SnapshotObject, 0, count+2) | ||
| 148 | for i := range count { | 149 | for i := range count { | ||
| @@ -163,18 +164,18 @@ func largeIncrementalPublications(t testing.TB, name string, count int) (ports.S | |||||
| 163 | if err != nil { | 164 | if err != nil { | ||
| 164 | t.Fatal(err) | 165 | t.Fatal(err) | ||
| 165 | } | 166 | } | ||
| 166 | - | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) | 167 | + | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) |
| 167 | if err != nil { | 168 | if err != nil { | ||
| 168 | t.Fatal(err) | 169 | t.Fatal(err) | ||
| 169 | } | 170 | } | ||
| 170 | - | return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)} | 171 | + | return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)} |
| 171 | } | 172 | } | ||
| 172 | return makePublication(1, "tail-1", "visible-1"), makePublication(2, "tail-2", "visible-2") | 173 | return makePublication(1, "tail-1", "visible-1"), makePublication(2, "tail-2", "visible-2") | ||
| 173 | } | 174 | } | ||
| 174 | 175 | ||||
| 175 | func seedCompletePublication(t testing.TB, repo *Repository, publication ports.SnapshotPublication) { | 176 | func seedCompletePublication(t testing.TB, repo *Repository, publication ports.SnapshotPublication) { | ||
| 176 | t.Helper() | 177 | t.Helper() | ||
| 177 | - | key := sessionKey(publication.Name) | 178 | + | key := legacyIncarnationID(publication.Name).String() |
| 178 | if err := repo.ensureSession(publication.IncarnationID); err != nil { | 179 | if err := repo.ensureSession(publication.IncarnationID); err != nil { | ||
| 179 | t.Fatal(err) | 180 | t.Fatal(err) | ||
| 180 | } | 181 | } | ||
M internal/adapters/snapshot/repository_durability_test.go +4 -4
| @@ -16,7 +16,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) { | |||
| 16 | 16 | repo := NewRepository(privateDir(t)) | |
| 17 | 17 | pub := repositoryPublication(t, "named", 1, []byte("state")) | |
| 18 | 18 | object := pub.Objects[0] | |
| 19 | - | target := repo.legacyObjectPath(sessionKey(pub.Name), object.Digest) | |
| 19 | + | target := repo.legacyObjectPath(legacyIncarnationID(pub.Name).String(), object.Digest) | |
| 20 | 20 | repo.hooks.installImmutable = func(path string) error { | |
| 21 | 21 | if path == target { | |
| 22 | 22 | return os.WriteFile(path, []byte("attacker bytes"), 0o600) | |
| @@ -37,7 +37,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) { | |||
| 37 | 37 | ||
| 38 | 38 | func prepareHeadStage(t *testing.T, repo *Repository, pub ports.SnapshotPublication) { | |
| 39 | 39 | t.Helper() | |
| 40 | - | key := sessionKey(pub.Name) | |
| 40 | + | key := legacyIncarnationID(pub.Name).String() | |
| 41 | 41 | prepareObjects(t, repo, pub) | |
| 42 | 42 | if err := repo.writeImmutable(repo.legacyManifestPath(key, pub.Generation), pub.Manifest, func(data []byte) error { | |
| 43 | 43 | if !bytes.Equal(data, pub.Manifest) { | |
| @@ -112,7 +112,7 @@ func preparePublicationLocation(t *testing.T, repo *Repository, pub ports.Snapsh | |||
| 112 | 112 | } | |
| 113 | 113 | ||
| 114 | 114 | func injectBoundary(repo *Repository, location, operation string, pub ports.SnapshotPublication) { | |
| 115 | - | key := sessionKey(pub.Name) | |
| 115 | + | key := legacyIncarnationID(pub.Name).String() | |
| 116 | 116 | dir := filepath.Dir(repo.legacyObjectPath(key, pub.Objects[0].Digest)) | |
| 117 | 117 | if location == "manifest" { | |
| 118 | 118 | dir = filepath.Dir(repo.legacyManifestPath(key, pub.Generation)) | |
| @@ -167,7 +167,7 @@ func injectBoundary(repo *Repository, location, operation string, pub ports.Snap | |||
| 167 | 167 | ||
| 168 | 168 | func prepareObjects(t *testing.T, repo *Repository, pub ports.SnapshotPublication) { | |
| 169 | 169 | t.Helper() | |
| 170 | - | key := sessionKey(pub.Name) | |
| 170 | + | key := legacyIncarnationID(pub.Name).String() | |
| 171 | 171 | for _, object := range pub.Objects { | |
| 172 | 172 | ref := manifestReference(t, pub.Manifest, object.Digest) | |
| 173 | 173 | if err := repo.writeImmutable(repo.legacyObjectPath(key, object.Digest), object.Data, func(data []byte) error { | |
| @@ -16,7 +16,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) { | |||||
| 16 | repo := NewRepository(privateDir(t)) | 16 | repo := NewRepository(privateDir(t)) | ||
| 17 | pub := repositoryPublication(t, "named", 1, []byte("state")) | 17 | pub := repositoryPublication(t, "named", 1, []byte("state")) | ||
| 18 | object := pub.Objects[0] | 18 | object := pub.Objects[0] | ||
| 19 | - | target := repo.legacyObjectPath(sessionKey(pub.Name), object.Digest) | 19 | + | target := repo.legacyObjectPath(legacyIncarnationID(pub.Name).String(), object.Digest) |
| 20 | repo.hooks.installImmutable = func(path string) error { | 20 | repo.hooks.installImmutable = func(path string) error { | ||
| 21 | if path == target { | 21 | if path == target { | ||
| 22 | return os.WriteFile(path, []byte("attacker bytes"), 0o600) | 22 | return os.WriteFile(path, []byte("attacker bytes"), 0o600) | ||
| @@ -37,7 +37,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) { | |||||
| 37 | 37 | ||||
| 38 | func prepareHeadStage(t *testing.T, repo *Repository, pub ports.SnapshotPublication) { | 38 | func prepareHeadStage(t *testing.T, repo *Repository, pub ports.SnapshotPublication) { | ||
| 39 | t.Helper() | 39 | t.Helper() | ||
| 40 | - | key := sessionKey(pub.Name) | 40 | + | key := legacyIncarnationID(pub.Name).String() |
| 41 | prepareObjects(t, repo, pub) | 41 | prepareObjects(t, repo, pub) | ||
| 42 | if err := repo.writeImmutable(repo.legacyManifestPath(key, pub.Generation), pub.Manifest, func(data []byte) error { | 42 | if err := repo.writeImmutable(repo.legacyManifestPath(key, pub.Generation), pub.Manifest, func(data []byte) error { | ||
| 43 | if !bytes.Equal(data, pub.Manifest) { | 43 | if !bytes.Equal(data, pub.Manifest) { | ||
| @@ -112,7 +112,7 @@ func preparePublicationLocation(t *testing.T, repo *Repository, pub ports.Snapsh | |||||
| 112 | } | 112 | } | ||
| 113 | 113 | ||||
| 114 | func injectBoundary(repo *Repository, location, operation string, pub ports.SnapshotPublication) { | 114 | func injectBoundary(repo *Repository, location, operation string, pub ports.SnapshotPublication) { | ||
| 115 | - | key := sessionKey(pub.Name) | 115 | + | key := legacyIncarnationID(pub.Name).String() |
| 116 | dir := filepath.Dir(repo.legacyObjectPath(key, pub.Objects[0].Digest)) | 116 | dir := filepath.Dir(repo.legacyObjectPath(key, pub.Objects[0].Digest)) | ||
| 117 | if location == "manifest" { | 117 | if location == "manifest" { | ||
| 118 | dir = filepath.Dir(repo.legacyManifestPath(key, pub.Generation)) | 118 | dir = filepath.Dir(repo.legacyManifestPath(key, pub.Generation)) | ||
| @@ -167,7 +167,7 @@ func injectBoundary(repo *Repository, location, operation string, pub ports.Snap | |||||
| 167 | 167 | ||||
| 168 | func prepareObjects(t *testing.T, repo *Repository, pub ports.SnapshotPublication) { | 168 | func prepareObjects(t *testing.T, repo *Repository, pub ports.SnapshotPublication) { | ||
| 169 | t.Helper() | 169 | t.Helper() | ||
| 170 | - | key := sessionKey(pub.Name) | 170 | + | key := legacyIncarnationID(pub.Name).String() |
| 171 | for _, object := range pub.Objects { | 171 | for _, object := range pub.Objects { | ||
| 172 | ref := manifestReference(t, pub.Manifest, object.Digest) | 172 | ref := manifestReference(t, pub.Manifest, object.Digest) | ||
| 173 | if err := repo.writeImmutable(repo.legacyObjectPath(key, object.Digest), object.Data, func(data []byte) error { | 173 | if err := repo.writeImmutable(repo.legacyObjectPath(key, object.Digest), object.Data, func(data []byte) error { | ||
M internal/adapters/snapshot/repository_fault_test.go +9 -5
| @@ -56,7 +56,7 @@ func TestRepositoryCleanupFaultsSurfaceForImmutableAndMutablePublication(t *test | |||
| 56 | 56 | t.Fatal(err) | |
| 57 | 57 | } | |
| 58 | 58 | second := repositoryPublication(t, "named", 2, []byte("two")) | |
| 59 | - | key := sessionKey(second.Name) | |
| 59 | + | key := legacyIncarnationID(second.Name).String() | |
| 60 | 60 | if tc.location == "manifest" { | |
| 61 | 61 | prepareObjects(t, repo, second) | |
| 62 | 62 | } else { | |
| @@ -119,10 +119,14 @@ func TestRepositoryNewDirectorySyncFaultsAreIndependent(t *testing.T) { | |||
| 119 | 119 | {name: "repository", phase: "repository", occurrence: 1, directory: func(_ *Repository, root string) string { return filepath.Dir(root) }}, | |
| 120 | 120 | {name: "sessions", phase: "sessions", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.dir }}, | |
| 121 | 121 | {name: "session", phase: "session", occurrence: 1, directory: func(repo *Repository, _ string) string { return filepath.Join(repo.dir, repositorySessionsDir) }}, | |
| 122 | - | {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }}, | |
| 123 | - | {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }}, | |
| 122 | + | {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string { | |
| 123 | + | return repo.legacySessionPath(legacyIncarnationID("named").String()) | |
| 124 | + | }}, | |
| 125 | + | {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string { | |
| 126 | + | return repo.legacySessionPath(legacyIncarnationID("named").String()) | |
| 127 | + | }}, | |
| 124 | 128 | {name: "object shard", phase: "object shard", occurrence: 1, directory: func(repo *Repository, _ string) string { | |
| 125 | - | return filepath.Join(repo.legacySessionPath(sessionKey("named")), repositoryObjectsDir) | |
| 129 | + | return filepath.Join(repo.legacySessionPath(legacyIncarnationID("named").String()), repositoryObjectsDir) | |
| 126 | 130 | }}, | |
| 127 | 131 | } | |
| 128 | 132 | for _, tc := range cases { | |
| @@ -191,7 +195,7 @@ func TestRepositoryLoadFallsBackFromIncompleteNewestGeneration(t *testing.T) { | |||
| 191 | 195 | if err != nil { | |
| 192 | 196 | t.Fatal(err) | |
| 193 | 197 | } | |
| 194 | - | if err := os.Remove(repo.legacyObjectPath(sessionKey("named"), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil { | |
| 198 | + | if err := os.Remove(repo.legacyObjectPath(legacyIncarnationID("named").String(), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil { | |
| 195 | 199 | t.Fatal(err) | |
| 196 | 200 | } | |
| 197 | 201 | got, err := repo.Load(context.Background(), "named") | |
| @@ -56,7 +56,7 @@ func TestRepositoryCleanupFaultsSurfaceForImmutableAndMutablePublication(t *test | |||||
| 56 | t.Fatal(err) | 56 | t.Fatal(err) | ||
| 57 | } | 57 | } | ||
| 58 | second := repositoryPublication(t, "named", 2, []byte("two")) | 58 | second := repositoryPublication(t, "named", 2, []byte("two")) | ||
| 59 | - | key := sessionKey(second.Name) | 59 | + | key := legacyIncarnationID(second.Name).String() |
| 60 | if tc.location == "manifest" { | 60 | if tc.location == "manifest" { | ||
| 61 | prepareObjects(t, repo, second) | 61 | prepareObjects(t, repo, second) | ||
| 62 | } else { | 62 | } else { | ||
| @@ -119,10 +119,14 @@ func TestRepositoryNewDirectorySyncFaultsAreIndependent(t *testing.T) { | |||||
| 119 | {name: "repository", phase: "repository", occurrence: 1, directory: func(_ *Repository, root string) string { return filepath.Dir(root) }}, | 119 | {name: "repository", phase: "repository", occurrence: 1, directory: func(_ *Repository, root string) string { return filepath.Dir(root) }}, | ||
| 120 | {name: "sessions", phase: "sessions", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.dir }}, | 120 | {name: "sessions", phase: "sessions", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.dir }}, | ||
| 121 | {name: "session", phase: "session", occurrence: 1, directory: func(repo *Repository, _ string) string { return filepath.Join(repo.dir, repositorySessionsDir) }}, | 121 | {name: "session", phase: "session", occurrence: 1, directory: func(repo *Repository, _ string) string { return filepath.Join(repo.dir, repositorySessionsDir) }}, | ||
| 122 | - | {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }}, | 122 | + | {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string { |
| 123 | - | {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }}, | 123 | + | return repo.legacySessionPath(legacyIncarnationID("named").String()) |
| 124 | + | }}, | |||
| 125 | + | {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string { | |||
| 126 | + | return repo.legacySessionPath(legacyIncarnationID("named").String()) | |||
| 127 | + | }}, | |||
| 124 | {name: "object shard", phase: "object shard", occurrence: 1, directory: func(repo *Repository, _ string) string { | 128 | {name: "object shard", phase: "object shard", occurrence: 1, directory: func(repo *Repository, _ string) string { | ||
| 125 | - | return filepath.Join(repo.legacySessionPath(sessionKey("named")), repositoryObjectsDir) | 129 | + | return filepath.Join(repo.legacySessionPath(legacyIncarnationID("named").String()), repositoryObjectsDir) |
| 126 | }}, | 130 | }}, | ||
| 127 | } | 131 | } | ||
| 128 | for _, tc := range cases { | 132 | for _, tc := range cases { | ||
| @@ -191,7 +195,7 @@ func TestRepositoryLoadFallsBackFromIncompleteNewestGeneration(t *testing.T) { | |||||
| 191 | if err != nil { | 195 | if err != nil { | ||
| 192 | t.Fatal(err) | 196 | t.Fatal(err) | ||
| 193 | } | 197 | } | ||
| 194 | - | if err := os.Remove(repo.legacyObjectPath(sessionKey("named"), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil { | 198 | + | if err := os.Remove(repo.legacyObjectPath(legacyIncarnationID("named").String(), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil { |
| 195 | t.Fatal(err) | 199 | t.Fatal(err) | ||
| 196 | } | 200 | } | ||
| 197 | got, err := repo.Load(context.Background(), "named") | 201 | got, err := repo.Load(context.Background(), "named") | ||
M internal/adapters/snapshot/repository_phase6_test.go +1 -1
| @@ -101,7 +101,7 @@ func TestRepositoryDeleteUsesDeterministicPendingQuarantine(t *testing.T) { | |||
| 101 | 101 | if got := checks.Load(); got != 1 { | |
| 102 | 102 | t.Fatalf("pending quarantine checks = %d, want one deterministic lookup", got) | |
| 103 | 103 | } | |
| 104 | - | if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(sessionKey("named")))); err != nil { | |
| 104 | + | if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(legacyIncarnationID("named").String()))); err != nil { | |
| 105 | 105 | t.Fatalf("deterministic quarantine missing: %v", err) | |
| 106 | 106 | } | |
| 107 | 107 | } | |
| @@ -101,7 +101,7 @@ func TestRepositoryDeleteUsesDeterministicPendingQuarantine(t *testing.T) { | |||||
| 101 | if got := checks.Load(); got != 1 { | 101 | if got := checks.Load(); got != 1 { | ||
| 102 | t.Fatalf("pending quarantine checks = %d, want one deterministic lookup", got) | 102 | t.Fatalf("pending quarantine checks = %d, want one deterministic lookup", got) | ||
| 103 | } | 103 | } | ||
| 104 | - | if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(sessionKey("named")))); err != nil { | 104 | + | if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(legacyIncarnationID("named").String()))); err != nil { |
| 105 | t.Fatalf("deterministic quarantine missing: %v", err) | 105 | t.Fatalf("deterministic quarantine missing: %v", err) | ||
| 106 | } | 106 | } | ||
| 107 | } | 107 | } | ||
M internal/adapters/snapshot/repository_test.go +3 -2
| @@ -55,6 +55,7 @@ func TestRepositoryDoesNotRewriteVerifiedImmutableBlob(t *testing.T) { | |||
| 55 | 55 | ||
| 56 | 56 | func repositoryPublication(t *testing.T, name string, generation uint64, payload []byte) ports.SnapshotPublication { | |
| 57 | 57 | t.Helper() | |
| 58 | + | id := legacyIncarnationID(name) | |
| 58 | 59 | tail, err := codec.MarshalObject(codec.HistoryTail, payload) | |
| 59 | 60 | if err != nil { | |
| 60 | 61 | t.Fatal(err) | |
| @@ -63,9 +64,9 @@ func repositoryPublication(t *testing.T, name string, generation uint64, payload | |||
| 63 | 64 | if err != nil { | |
| 64 | 65 | t.Fatal(err) | |
| 65 | 66 | } | |
| 66 | - | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) | |
| 67 | + | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) | |
| 67 | 68 | if err != nil { | |
| 68 | 69 | t.Fatal(err) | |
| 69 | 70 | } | |
| 70 | - | return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}} | |
| 71 | + | return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}} | |
| 71 | 72 | } | |
| @@ -55,6 +55,7 @@ func TestRepositoryDoesNotRewriteVerifiedImmutableBlob(t *testing.T) { | |||||
| 55 | 55 | ||||
| 56 | func repositoryPublication(t *testing.T, name string, generation uint64, payload []byte) ports.SnapshotPublication { | 56 | func repositoryPublication(t *testing.T, name string, generation uint64, payload []byte) ports.SnapshotPublication { | ||
| 57 | t.Helper() | 57 | t.Helper() | ||
| 58 | + | id := legacyIncarnationID(name) | |||
| 58 | tail, err := codec.MarshalObject(codec.HistoryTail, payload) | 59 | tail, err := codec.MarshalObject(codec.HistoryTail, payload) | ||
| 59 | if err != nil { | 60 | if err != nil { | ||
| 60 | t.Fatal(err) | 61 | t.Fatal(err) | ||
| @@ -63,9 +64,9 @@ func repositoryPublication(t *testing.T, name string, generation uint64, payload | |||||
| 63 | if err != nil { | 64 | if err != nil { | ||
| 64 | t.Fatal(err) | 65 | t.Fatal(err) | ||
| 65 | } | 66 | } | ||
| 66 | - | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) | 67 | + | manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}}) |
| 67 | if err != nil { | 68 | if err != nil { | ||
| 68 | t.Fatal(err) | 69 | t.Fatal(err) | ||
| 69 | } | 70 | } | ||
| 70 | - | return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}} | 71 | + | return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}} |
| 71 | } | 72 | } | ||