Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b0dc17d
fix(rest): roll a clone or restore back when its parent group is deleted
kvaps Sep 8, 2026
629cd66
fix(rest): drop the definition only if its replicas went with it
kvaps Sep 8, 2026
05e8bb9
fix(rest): never undo a completed restore because its safety net failed
kvaps Sep 8, 2026
de598bc
fix(rest): drop the parent only over replicas that were stamped for d…
kvaps Sep 10, 2026
eeff4eb
fix(rest): stop reporting a broken leftover as a finished clone
kvaps Sep 10, 2026
f8594fa
fix(rest): tell the caller when the safety net did not run
kvaps Sep 10, 2026
3e229c9
fix(rest): decide a rollback on a read that has seen its own writes
kvaps Sep 14, 2026
d569cac
fix(rest): refuse and reap in a rollback the way rd d does
kvaps Sep 14, 2026
268a304
fix(rest): answer a clone replay only over live replicas
kvaps Sep 14, 2026
4e44fb3
fix(rest): roll back the partial clone a failed materialise leaves
kvaps Sep 14, 2026
3f463e6
fix(rest): keep the rollbacks alive past the request, off adopted work
kvaps Sep 15, 2026
c7de70a
fix(rest): refuse an unreadable parent group as unreadable
kvaps Sep 15, 2026
0118023
fix(rest): warn when the volume-less clone could not check its group
kvaps Sep 15, 2026
331a860
fix(rest): roll the volume-less clone back the way the others do
kvaps Sep 16, 2026
4cccaa9
fix(rest): let the process outlive a rollback it started
kvaps Sep 16, 2026
4b7cdd9
fix(rest): make a materialisation state whose definition it wrote
kvaps Sep 16, 2026
d52e03e
fix(rest): re-read the parent group on the context the rollback uses
kvaps Sep 21, 2026
82ae4cf
fix(rest): word a failed rollback for the step and for both readings
kvaps Sep 21, 2026
290a933
test(rest): hold the snapshot sweep a rollback runs after its delete
kvaps Sep 21, 2026
0cc1a2a
fix(rest): refuse a replay over a clone whose rollback gave up
kvaps Sep 21, 2026
3e7e8e9
docs(rest): say the RG-deleted guard does not cover spawn yet
kvaps Sep 21, 2026
6d779d9
style(rest): keep the restore door under the length limit
kvaps Sep 21, 2026
cbedcfc
Merge branch main into fix/clone-restore-rg-deleted-race
kvaps Sep 30, 2026
e628ea8
fix(rest): keep the abandoned-rollback mark on the definition it marks
kvaps Sep 30, 2026
ec0a283
fix(rest): mark a rollback before it touches anything, on one budget
kvaps Sep 30, 2026
bcf04cd
fix(store): patch a definition the cache has not caught up with
kvaps Oct 1, 2026
7491221
fix(store): keep the restore marker off the props that travel
kvaps Oct 1, 2026
091d9b5
fix(rest): read the abandoned-rollback mark past the cache
kvaps Oct 1, 2026
93efd59
fix(rest): tell a failed snapshot read apart from a snapshot
kvaps Oct 1, 2026
7ef88a3
fix(rest): give each abandoned-rollback mark a budget of its own
kvaps Oct 1, 2026
7a58e53
fix(rest): give the abandoned-rollback mark a second, not a fifth
kvaps Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,7 @@ spec:
volumeMounts: []
volumes: []
serviceAccountName: controller-manager
terminationGracePeriodSeconds: 10
# The REST server's graceful-shutdown window is derived from the
# detached rollback budget (pkg/rest/rg_deleted_race.go); this has to
# outlive it, or a SIGTERM kills a compensation mid-cascade.
terminationGracePeriodSeconds: 20
100 changes: 100 additions & 0 deletions internal/cli/local_props_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: Apache-2.0

package cli_test

import (
"context"
"testing"

apiv1 "github.com/cozystack/blockstor/pkg/api/v1"
"github.com/cozystack/blockstor/pkg/store"
)

func markPVCX(ctx context.Context, backend store.Store) {
def, err := backend.ResourceDefinitions().Get(ctx, "pvc-x")
if err != nil {
return
}

if def.Props == nil {
def.Props = map[string]string{}
}

def.Props[store.RollbackAbandonedProp] = "snapshots"
_ = backend.ResourceDefinitions().Update(ctx, &def)
}

// The CLI copies a definition's props onward on the same two paths the REST
// door does, and the abandoned-rollback mark is about the definition it sits
// on, never about a copy of it.
func TestCLIDoesNotCarryTheAbandonedRollbackMarkOnward(t *testing.T) {
t.Parallel()

t.Run("snapshot-create", func(t *testing.T) {
t.Parallel()

app, _, errBuf := newApp(t, func(ctx context.Context, backend store.Store) {
seedSnapshotSource(ctx, backend)
markPVCX(ctx, backend)
})

if got := app.Run(t.Context(), []string{"s", "c", "pvc-x", "snap-m"}); got != 0 {
t.Fatalf("create exit = %d (stderr: %s)", got, errBuf.String())
}

snap, err := appStore(t, app).Snapshots().Get(t.Context(), "pvc-x", "snap-m")
if err != nil {
t.Fatalf("get snapshot: %v", err)
}

if step, ok := snap.Props[store.RollbackAbandonedProp]; ok {
t.Errorf("the snapshot carries the source's mark %q", step)
}
})

for _, tc := range []struct {
name string
seed func(context.Context, store.Store)
}{
{name: "restore-from-a-snapshot-carrying-it", seed: func(ctx context.Context, backend store.Store) {
seedSnapshotSource(ctx, backend)
_ = backend.Snapshots().Create(ctx, &apiv1.Snapshot{
Name: "snap-m", ResourceName: "pvc-x", Nodes: []string{"node-1", "node-2"},
Props: map[string]string{store.RollbackAbandonedProp: "snapshots"},
VolumeDefinitions: []apiv1.SnapshotVolumeDef{{VolumeNumber: 0, SizeKib: 1 << 20}},
})
}},
{name: "restore-falling-back-to-the-source-props", seed: func(ctx context.Context, backend store.Store) {
seedSnapshotSource(ctx, backend)
markPVCX(ctx, backend)
_ = backend.Snapshots().Create(ctx, &apiv1.Snapshot{
Name: "snap-m", ResourceName: "pvc-x", Nodes: []string{"node-1", "node-2"},
VolumeDefinitions: []apiv1.SnapshotVolumeDef{{VolumeNumber: 0, SizeKib: 1 << 20}},
})
}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

app, _, errBuf := newApp(t, tc.seed)

argv := []string{
"s", "resource", "restore",
"--from-resource", "pvc-x", "--from-snapshot", "snap-m", "--to-resource", "pvc-m",
}

if got := app.Run(t.Context(), argv); got != 0 {
t.Fatalf("restore exit = %d (stderr: %s)", got, errBuf.String())
}

def, err := appStore(t, app).ResourceDefinitions().Get(t.Context(), "pvc-m")
if err != nil {
t.Fatalf("get restored definition: %v", err)
}

if step, ok := def.Props[store.RollbackAbandonedProp]; ok {
t.Errorf("the restored definition carries the mark %q", step)
}
})
}
}
7 changes: 3 additions & 4 deletions internal/cli/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
"strings"

Expand Down Expand Up @@ -235,7 +234,7 @@ func hydrateSnapshot(ctx context.Context, run *runContext, snap *apiv1.Snapshot)
}

if snap.Props == nil {
snap.Props = def.Props
snap.Props = store.TravellingProps(def.Props)
}

if snap.SnapshotDefinitionProps == nil {
Expand Down Expand Up @@ -327,11 +326,11 @@ func snapshotRestoreResource(ctx context.Context, run *runContext) error {
// group breaks the restore-then-list workflow.
ResourceGroupName: src.ResourceGroupName,
LayerStack: src.LayerStack,
Props: maps.Clone(snap.Props),
Props: store.TravellingProps(snap.Props),
}

if def.Props == nil {
def.Props = maps.Clone(src.Props)
def.Props = store.TravellingProps(src.Props)
}

if def.Props == nil {
Expand Down
4 changes: 4 additions & 0 deletions pkg/rest/cache_invalidation_bug_124_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ func (l *laggingRDs) Get(ctx context.Context, name string) (apiv1.ResourceDefini
return l.inner.Get(ctx, name) //nolint:wrapcheck // test helper
}

func (l *laggingRDs) GetUncached(ctx context.Context, name string) (apiv1.ResourceDefinition, error) {
return l.inner.GetUncached(ctx, name) //nolint:wrapcheck // test helper
}

func (l *laggingRDs) Create(ctx context.Context, rd *apiv1.ResourceDefinition) error {
return l.inner.Create(ctx, rd) //nolint:wrapcheck // test helper
}
Expand Down
Loading
Loading