diff --git a/cmd/admin-handlers-pools.go b/cmd/admin-handlers-pools.go index cb53806b7..46f259567 100644 --- a/cmd/admin-handlers-pools.go +++ b/cmd/admin-handlers-pools.go @@ -373,7 +373,9 @@ func (a adminAPIHandlers) RebalanceStop(w http.ResponseWriter, r *http.Request) // Cancel any ongoing rebalance operation globalNotificationSys.StopRebalance(r.Context()) writeSuccessResponseHeadersOnly(w) - adminLogIf(ctx, pools.saveRebalanceStats(GlobalContext, 0, rebalSaveStoppedAt)) + if err := pools.saveRebalanceStats(GlobalContext, 0, rebalSaveStoppedAt); !errors.Is(err, errRebalanceNotStarted) { + adminLogIf(ctx, err) + } globalNotificationSys.LoadRebalanceMeta(ctx, false) } diff --git a/cmd/erasure-server-pool-rebalance.go b/cmd/erasure-server-pool-rebalance.go index a23a9eb3d..8626dd965 100644 --- a/cmd/erasure-server-pool-rebalance.go +++ b/cmd/erasure-server-pool-rebalance.go @@ -826,6 +826,10 @@ func (z *erasureServerPools) saveRebalanceStats(ctx context.Context, poolIdx int // if not found, we store the memory metadata back // when rebalance status changed, will notify all nodes update status to memory, we can treat the memory metadata is the latest status if errors.Is(err, errConfigNotFound) { + if z.rebalMeta == nil { + // No rebalance has run, e.g. a rebalance stop without one. + return errRebalanceNotStarted + } r = z.rebalMeta } diff --git a/cmd/erasure-server-pool-rebalance_test.go b/cmd/erasure-server-pool-rebalance_test.go new file mode 100644 index 000000000..8d8047d1a --- /dev/null +++ b/cmd/erasure-server-pool-rebalance_test.go @@ -0,0 +1,42 @@ +// Copyright (c) 2026 Denis Babchuk +// +// This file is part of Silo Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "errors" + "testing" +) + +// Without rebalance.bin and without in-memory metadata no rebalance has run, +// so there is nothing to update: saving must report that instead of +// dereferencing the missing metadata, and must not create rebalance.bin. +func TestSaveRebalanceStatsWithoutMeta(t *testing.T) { + z, _ := consistencyPools(t) + for _, opts := range []rebalSaveOpts{rebalSaveStoppedAt, rebalSaveStats} { + if err := z.saveRebalanceStats(t.Context(), 0, opts); !errors.Is(err, errRebalanceNotStarted) { + t.Fatalf("opts %d: got %v, want %v", opts, err, errRebalanceNotStarted) + } + if z.rebalMeta != nil { + t.Fatalf("opts %d: in-memory rebalance metadata was set", opts) + } + r := &rebalanceMeta{} + if err := r.load(t.Context(), z.serverPools[0]); !errors.Is(err, errConfigNotFound) { + t.Fatalf("opts %d: rebalance.bin after save: %v, want %v", opts, err, errConfigNotFound) + } + } +}