test(daemon): cover the restored generation payload budget
- commit
- c002b8840d9c27bd8c31c126ef66de30fd86c656
- author
- brice · · 2026-10-08 08:54 UTC
- committer
- brice · · 2026-10-08 09:31 UTC
- parent
- 351738d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +44 -3
M internal/usecase/daemon/snapshot_restore_incremental.go +8 -3
| @@ -387,6 +387,14 @@ func (d *Daemon) logRejectedRestoreCandidate(record domain.CatalogueRecord, cand | |||
| 387 | 387 | } | |
| 388 | 388 | ||
| 389 | 389 | func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Session, error) { | |
| 390 | + | // Objects are compressed, so the on-disk read budget no longer bounds the | |
| 391 | + | // inflated generation; budget its payloads as one uncompressed checkpoint. | |
| 392 | + | return sessionFromGenerationBudget(generation, snapcodec.MaxGenerationPayloadBytes) | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | // sessionFromGenerationBudget decodes a generation whose inflated object | |
| 396 | + | // payloads total at most budget bytes. | |
| 397 | + | func sessionFromGenerationBudget(generation ports.SnapshotGeneration, budget int) (snapcodec.Session, error) { | |
| 390 | 398 | manifest, err := snapcodec.UnmarshalManifest(generation.Manifest) | |
| 391 | 399 | if err != nil { | |
| 392 | 400 | return snapcodec.Session{}, err | |
| @@ -395,9 +403,6 @@ func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Sessi | |||
| 395 | 403 | return snapcodec.Session{}, fmt.Errorf("snapshot: generation identity mismatch") | |
| 396 | 404 | } | |
| 397 | 405 | result := snapcodec.Session{Name: manifest.Name, CreatedAt: manifest.CreatedAt, Active: manifest.Active, Tabs: make([]snapcodec.Tab, 0, len(manifest.Tabs))} | |
| 398 | - | // Objects are compressed, so the on-disk read budget no longer bounds the | |
| 399 | - | // inflated generation; budget its payloads as one uncompressed checkpoint. | |
| 400 | - | budget := snapcodec.MaxGenerationPayloadBytes | |
| 401 | 406 | object := func(ref snapcodec.ObjectRef, kind snapcodec.ObjectKind) ([]byte, error) { | |
| 402 | 407 | data, err := generationObject(generation, ref, kind) | |
| 403 | 408 | if err != nil { | |
| @@ -387,6 +387,14 @@ func (d *Daemon) logRejectedRestoreCandidate(record domain.CatalogueRecord, cand | |||||
| 387 | } | 387 | } | ||
| 388 | 388 | ||||
| 389 | func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Session, error) { | 389 | func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Session, error) { | ||
| 390 | + | // Objects are compressed, so the on-disk read budget no longer bounds the | |||
| 391 | + | // inflated generation; budget its payloads as one uncompressed checkpoint. | |||
| 392 | + | return sessionFromGenerationBudget(generation, snapcodec.MaxGenerationPayloadBytes) | |||
| 393 | + | } | |||
| 394 | + | ||||
| 395 | + | // sessionFromGenerationBudget decodes a generation whose inflated object | |||
| 396 | + | // payloads total at most budget bytes. | |||
| 397 | + | func sessionFromGenerationBudget(generation ports.SnapshotGeneration, budget int) (snapcodec.Session, error) { | |||
| 390 | manifest, err := snapcodec.UnmarshalManifest(generation.Manifest) | 398 | manifest, err := snapcodec.UnmarshalManifest(generation.Manifest) | ||
| 391 | if err != nil { | 399 | if err != nil { | ||
| 392 | return snapcodec.Session{}, err | 400 | return snapcodec.Session{}, err | ||
| @@ -395,9 +403,6 @@ func sessionFromGeneration(generation ports.SnapshotGeneration) (snapcodec.Sessi | |||||
| 395 | return snapcodec.Session{}, fmt.Errorf("snapshot: generation identity mismatch") | 403 | return snapcodec.Session{}, fmt.Errorf("snapshot: generation identity mismatch") | ||
| 396 | } | 404 | } | ||
| 397 | result := snapcodec.Session{Name: manifest.Name, CreatedAt: manifest.CreatedAt, Active: manifest.Active, Tabs: make([]snapcodec.Tab, 0, len(manifest.Tabs))} | 405 | result := snapcodec.Session{Name: manifest.Name, CreatedAt: manifest.CreatedAt, Active: manifest.Active, Tabs: make([]snapcodec.Tab, 0, len(manifest.Tabs))} | ||
| 398 | - | // Objects are compressed, so the on-disk read budget no longer bounds the | |||
| 399 | - | // inflated generation; budget its payloads as one uncompressed checkpoint. | |||
| 400 | - | budget := snapcodec.MaxGenerationPayloadBytes | |||
| 401 | object := func(ref snapcodec.ObjectRef, kind snapcodec.ObjectKind) ([]byte, error) { | 406 | object := func(ref snapcodec.ObjectRef, kind snapcodec.ObjectKind) ([]byte, error) { | ||
| 402 | data, err := generationObject(generation, ref, kind) | 407 | data, err := generationObject(generation, ref, kind) | ||
| 403 | if err != nil { | 408 | if err != nil { | ||
M internal/usecase/daemon/snapshot_restore_test.go +36 -0
| @@ -623,3 +623,39 @@ func TestRestoreIncrementalFallbackAndInvalidObjectMappings(t *testing.T) { | |||
| 623 | 623 | }) | |
| 624 | 624 | } | |
| 625 | 625 | } | |
| 626 | + | ||
| 627 | + | // TestRestoreIncrementalPayloadBudget bounds the inflated payloads of one | |
| 628 | + | // generation: compressed objects must not restore past the budget. | |
| 629 | + | func TestRestoreIncrementalPayloadBudget(t *testing.T) { | |
| 630 | + | generation := acceptanceGeneration(t, restoreAcceptanceSession(t, "budget"), 3) | |
| 631 | + | decoded, err := sessionFromGeneration(generation) | |
| 632 | + | require.NoError(t, err) | |
| 633 | + | total := 0 | |
| 634 | + | for _, tab := range decoded.Tabs { | |
| 635 | + | for _, pane := range tab.Panes { | |
| 636 | + | for _, chunk := range pane.SealedChunks { | |
| 637 | + | total += len(chunk) | |
| 638 | + | } | |
| 639 | + | total += len(pane.Tail) + len(pane.Transcript) | |
| 640 | + | } | |
| 641 | + | } | |
| 642 | + | require.Positive(t, total) | |
| 643 | + | ||
| 644 | + | for _, tt := range []struct { | |
| 645 | + | name string | |
| 646 | + | budget int | |
| 647 | + | wantErr bool | |
| 648 | + | }{ | |
| 649 | + | {name: "exact budget", budget: total}, | |
| 650 | + | {name: "one byte short", budget: total - 1, wantErr: true}, | |
| 651 | + | } { | |
| 652 | + | t.Run(tt.name, func(t *testing.T) { | |
| 653 | + | _, err := sessionFromGenerationBudget(cloneAcceptanceGeneration(generation), tt.budget) | |
| 654 | + | if tt.wantErr { | |
| 655 | + | require.ErrorContains(t, err, "generation payload too large") | |
| 656 | + | return | |
| 657 | + | } | |
| 658 | + | require.NoError(t, err) | |
| 659 | + | }) | |
| 660 | + | } | |
| 661 | + | } | |
| @@ -623,3 +623,39 @@ func TestRestoreIncrementalFallbackAndInvalidObjectMappings(t *testing.T) { | |||||
| 623 | }) | 623 | }) | ||
| 624 | } | 624 | } | ||
| 625 | } | 625 | } | ||
| 626 | + | ||||
| 627 | + | // TestRestoreIncrementalPayloadBudget bounds the inflated payloads of one | |||
| 628 | + | // generation: compressed objects must not restore past the budget. | |||
| 629 | + | func TestRestoreIncrementalPayloadBudget(t *testing.T) { | |||
| 630 | + | generation := acceptanceGeneration(t, restoreAcceptanceSession(t, "budget"), 3) | |||
| 631 | + | decoded, err := sessionFromGeneration(generation) | |||
| 632 | + | require.NoError(t, err) | |||
| 633 | + | total := 0 | |||
| 634 | + | for _, tab := range decoded.Tabs { | |||
| 635 | + | for _, pane := range tab.Panes { | |||
| 636 | + | for _, chunk := range pane.SealedChunks { | |||
| 637 | + | total += len(chunk) | |||
| 638 | + | } | |||
| 639 | + | total += len(pane.Tail) + len(pane.Transcript) | |||
| 640 | + | } | |||
| 641 | + | } | |||
| 642 | + | require.Positive(t, total) | |||
| 643 | + | ||||
| 644 | + | for _, tt := range []struct { | |||
| 645 | + | name string | |||
| 646 | + | budget int | |||
| 647 | + | wantErr bool | |||
| 648 | + | }{ | |||
| 649 | + | {name: "exact budget", budget: total}, | |||
| 650 | + | {name: "one byte short", budget: total - 1, wantErr: true}, | |||
| 651 | + | } { | |||
| 652 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 653 | + | _, err := sessionFromGenerationBudget(cloneAcceptanceGeneration(generation), tt.budget) | |||
| 654 | + | if tt.wantErr { | |||
| 655 | + | require.ErrorContains(t, err, "generation payload too large") | |||
| 656 | + | return | |||
| 657 | + | } | |||
| 658 | + | require.NoError(t, err) | |||
| 659 | + | }) | |||
| 660 | + | } | |||
| 661 | + | } | |||