Files
kopia/internal/server/server_maintenance_test.go
Julio López 3fbe0bd424 fix(server): prevent maintenance task on read-only repos (#4408)
Avoid starting a maintenance task on the server when
the repository connection (configuration) is read-only.

Also:
* Check for read-only repo before running maintenance.
* Move direct repo check to startMaintenanceManager.
* Rename function as maybeStartMaintenanceManager.
  The name reflects that the function may not start a
  maintenance task manager.
* Add connect options to repotesting.
* Add test for maintenance on read-only repo.

- Fixes: #4373
2025-02-13 20:13:22 -08:00

141 lines
3.5 KiB
Go

package server
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/internal/clock"
"github.com/kopia/kopia/internal/repotesting"
"github.com/kopia/kopia/notification/notifytemplate"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/maintenance"
)
type testServer struct {
mu sync.Mutex
runCounter atomic.Int32
refreshSchedulerCount atomic.Int32
log func(msg string, args ...any)
// +checklocks:mu
err error
}
func (s *testServer) runMaintenanceTask(ctx context.Context, dr repo.DirectRepository) error {
s.runCounter.Add(1)
if s.log != nil {
s.log("runMaintenanceTask")
}
s.mu.Lock()
ne := s.err
s.err = nil
s.mu.Unlock()
return ne
}
func (s *testServer) refreshScheduler(reason string) {
s.refreshSchedulerCount.Add(1)
}
func (s *testServer) enableErrorNotifications() bool {
return false
}
func (s *testServer) notificationTemplateOptions() notifytemplate.Options {
return notifytemplate.DefaultOptions
}
func TestServerMaintenance(t *testing.T) {
ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
require.NoError(t, repo.DirectWriteSession(ctx, env.RepositoryWriter, repo.WriteSessionOptions{}, func(ctx context.Context, dw repo.DirectRepositoryWriter) error {
return maintenance.SetParams(ctx, dw, &maintenance.Params{
Owner: env.Repository.ClientOptions().UsernameAtHost(),
QuickCycle: maintenance.CycleParams{
Enabled: true,
Interval: 5 * time.Second,
},
FullCycle: maintenance.CycleParams{
Enabled: true,
Interval: 10 * time.Second,
},
})
}))
ts := &testServer{log: t.Logf}
mm := maybeStartMaintenanceManager(ctx, env.RepositoryWriter, ts, time.Minute)
require.Equal(t, time.Time{}, mm.nextMaintenanceNoEarlierThan)
defer mm.stop(ctx)
// trigger and make sure it runs
mm.trigger()
require.Eventually(t, func() bool {
return ts.runCounter.Load() == 1 && ts.refreshSchedulerCount.Load() == 1
}, 3*time.Second, 10*time.Millisecond)
ts.mu.Lock()
ts.err = errors.New("some error")
ts.mu.Unlock()
mm.trigger()
require.Eventually(t, func() bool {
mm.mu.Lock()
defer mm.mu.Unlock()
return ts.runCounter.Load() == 2 && !mm.nextMaintenanceNoEarlierThan.IsZero()
}, 3*time.Second, 10*time.Millisecond)
// after a failure next maintenance time should be deferred by a minute.
require.Greater(t, mm.nextMaintenanceTime().Sub(clock.Now()), 50*time.Second)
}
func TestServerMaintenanceReadOnlyRepoConnection(t *testing.T) {
ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
require.NoError(t, repo.DirectWriteSession(ctx, env.RepositoryWriter, repo.WriteSessionOptions{}, func(ctx context.Context, dw repo.DirectRepositoryWriter) error {
return maintenance.SetParams(ctx, dw, &maintenance.Params{
Owner: env.Repository.ClientOptions().UsernameAtHost(),
QuickCycle: maintenance.CycleParams{
Enabled: true,
Interval: 5 * time.Second,
},
FullCycle: maintenance.CycleParams{
Enabled: true,
Interval: 10 * time.Second,
},
})
}))
dr, ok := env.Repository.(repo.DirectRepository)
require.True(t, ok, "not a direct repository connection")
dr.Refresh(ctx)
// make repo read-only
co := env.Repository.ClientOptions()
co.ReadOnly = true
repo.SetClientOptions(ctx, env.ConfigFile(), co)
env.MustReopen(t)
ts := &testServer{log: t.Logf}
mm := maybeStartMaintenanceManager(ctx, env.RepositoryWriter, ts, time.Minute)
require.Nil(t, mm, "maintenance task should not be created on read-only repo")
}