Allow setting the minimum epoch duration in kopia (#4798)

This PR lays the groundwork for changing persistent config info in
kopia. Specifically, it allows setting the minimum epoch duration (but
does so in a generic manner that can be expanded on later)

This PR only adds the code to actually do the configuration, it doesn't
add wiring to make this new function available to other callers

---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No

#### Type of change

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #4782

#### Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-12-04 16:08:35 -08:00 committed by GitHub
parent a2e80073be
commit 7ef5a33edf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 273 additions and 0 deletions

View File

@ -24,6 +24,7 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/kopia/retention"
"github.com/alcionai/corso/src/pkg/control/repository"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/storage"
)
@ -38,6 +39,24 @@ const (
var (
ErrSettingDefaultConfig = clues.New("setting default repo config values")
ErrorRepoAlreadyExists = clues.New("repo already exists")
// minEpochDurationLowerBound is the minimum corso will allow the kopia epoch
// duration to be set to. This number can still be tuned further, right now
// it's just to make sure it's not set to something totally wild.
//
// Note that there are still other parameters kopia checks when deciding if
// the epoch should be changed. This is just the min amount of time since the
// last epoch change that's required.
minEpochDurationLowerBound = 2 * time.Hour
// minEpochDurationUpperBound is the maximum corso will allow the kopia epoch
// duration to be set to. This number can still be tuned further, right now
// it's just to make sure it's not set to something totally wild.
//
// Note that there are still other parameters kopia checks when deciding if
// the epoch should be changed. This is just the min amount of time since the
// last epoch change that's required.
minEpochDurationUpperBound = 7 * 24 * time.Hour
)
// Having all fields set to 0 causes it to keep max-int versions of snapshots.
@ -611,3 +630,96 @@ func (w *conn) UpdatePassword(
return nil
}
// getPersistentConfig returns the current mutable parameters and blob storage
// config for the repo. It doesn't return the required parameters because it's
// unable to name the type they represent since it's in the internal kopia
// package.
func (w *conn) getPersistentConfig(
ctx context.Context,
) (format.MutableParameters, format.BlobStorageConfiguration, error) {
directRepo, ok := w.Repository.(repo.DirectRepository)
if !ok {
return format.MutableParameters{},
format.BlobStorageConfiguration{},
clues.NewWC(ctx, "getting repo handle")
}
formatManager := directRepo.FormatManager()
mutableParams, err := formatManager.GetMutableParameters()
if err != nil {
return format.MutableParameters{},
format.BlobStorageConfiguration{},
clues.WrapWC(ctx, err, "getting mutable parameters")
}
blobCfg, err := formatManager.BlobCfgBlob()
if err != nil {
return format.MutableParameters{},
format.BlobStorageConfiguration{},
clues.WrapWC(ctx, err, "getting blob config")
}
return mutableParams, blobCfg, nil
}
func (w *conn) updatePersistentConfig(
ctx context.Context,
config repository.PersistentConfig,
) error {
directRepo, ok := w.Repository.(repo.DirectRepository)
if !ok {
return clues.NewWC(ctx, "getting repo handle")
}
formatManager := directRepo.FormatManager()
// Get current config structs.
mutableParams, blobCfg, err := w.getPersistentConfig(ctx)
if err != nil {
return clues.Stack(err)
}
reqFeatures, err := formatManager.RequiredFeatures()
if err != nil {
return clues.WrapWC(ctx, err, "getting required features")
}
// Apply requested updates.
var changed bool
if d, ok := ptr.ValOK(config.MinEpochDuration); ok {
// Don't let the user set this value too small or too large.
if d < minEpochDurationLowerBound || d > minEpochDurationUpperBound {
return clues.NewWC(ctx, fmt.Sprintf(
"min epoch duration outside allowed limits of [%v, %v]",
minEpochDurationLowerBound,
minEpochDurationUpperBound))
}
if d != mutableParams.EpochParameters.MinEpochDuration {
ctx = clues.Add(
ctx,
"old_min_epoch_duration", mutableParams.EpochParameters.MinEpochDuration,
"new_min_epoch_duration", d)
changed = true
mutableParams.EpochParameters.MinEpochDuration = d
}
}
// Exit or update config structs if there were changes.
if !changed {
logger.Ctx(ctx).Info("no config parameter changes")
return nil
}
logger.Ctx(ctx).Info("persisting config parameter changes")
return clues.WrapWC(
ctx,
formatManager.SetParameters(ctx, mutableParams, blobCfg, reqFeatures),
"persisting updated config").
OrNil()
}

View File

@ -9,6 +9,7 @@ import (
"github.com/alcionai/clues"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/format"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/snapshot/policy"
"github.com/stretchr/testify/assert"
@ -475,6 +476,153 @@ func (suite *WrapperIntegrationSuite) TestSetUserAndHost() {
assert.NoError(t, err, clues.ToCore(err))
}
func (suite *WrapperIntegrationSuite) TestUpdatePersistentConfig() {
table := []struct {
name string
opts func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) repository.PersistentConfig
expectErr assert.ErrorAssertionFunc
expectConfig func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) (format.MutableParameters, format.BlobStorageConfiguration)
}{
{
name: "NoOptionsSet NoChange",
opts: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) repository.PersistentConfig {
return repository.PersistentConfig{}
},
expectErr: assert.NoError,
expectConfig: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) (format.MutableParameters, format.BlobStorageConfiguration) {
return startParams, startBlobConfig
},
},
{
name: "NoValueChange NoChange",
opts: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) repository.PersistentConfig {
return repository.PersistentConfig{
MinEpochDuration: ptr.To(startParams.EpochParameters.MinEpochDuration),
}
},
expectErr: assert.NoError,
expectConfig: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) (format.MutableParameters, format.BlobStorageConfiguration) {
return startParams, startBlobConfig
},
},
{
name: "MinEpochLessThanLowerBound Errors",
opts: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) repository.PersistentConfig {
return repository.PersistentConfig{
MinEpochDuration: ptr.To(minEpochDurationLowerBound - time.Second),
}
},
expectErr: assert.Error,
expectConfig: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) (format.MutableParameters, format.BlobStorageConfiguration) {
return startParams, startBlobConfig
},
},
{
name: "MinEpochGreaterThanUpperBound Errors",
opts: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) repository.PersistentConfig {
return repository.PersistentConfig{
MinEpochDuration: ptr.To(minEpochDurationUpperBound + time.Second),
}
},
expectErr: assert.Error,
expectConfig: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) (format.MutableParameters, format.BlobStorageConfiguration) {
return startParams, startBlobConfig
},
},
{
name: "UpdateMinEpoch Succeeds",
opts: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) repository.PersistentConfig {
return repository.PersistentConfig{
MinEpochDuration: ptr.To(minEpochDurationLowerBound),
}
},
expectErr: assert.NoError,
expectConfig: func(
startParams format.MutableParameters,
startBlobConfig format.BlobStorageConfiguration,
) (format.MutableParameters, format.BlobStorageConfiguration) {
startParams.EpochParameters.MinEpochDuration = minEpochDurationLowerBound
return startParams, startBlobConfig
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
repoNameHash := strTD.NewHashForRepoConfigName()
ctx, flush := tester.NewContext(t)
defer flush()
st1 := storeTD.NewPrefixedS3Storage(t)
connection := NewConn(st1)
err := connection.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
require.NoError(t, err, "initializing repo: %v", clues.ToCore(err))
startParams, startBlobConfig, err := connection.getPersistentConfig(ctx)
require.NoError(t, err, clues.ToCore(err))
opts := test.opts(startParams, startBlobConfig)
err = connection.updatePersistentConfig(ctx, opts)
test.expectErr(t, err, clues.ToCore(err))
// Need to close and reopen the repo since the format manager will cache
// the old value for some amount of time.
err = connection.Close(ctx)
require.NoError(t, err, clues.ToCore(err))
// Open another connection to the repo to verify params are as expected
// across repo connect calls.
err = connection.Connect(ctx, repository.Options{}, repoNameHash)
require.NoError(t, err, clues.ToCore(err))
gotParams, gotBlobConfig, err := connection.getPersistentConfig(ctx)
require.NoError(t, err, clues.ToCore(err))
expectParams, expectBlobConfig := test.expectConfig(startParams, startBlobConfig)
assert.Equal(t, expectParams, gotParams)
assert.Equal(t, expectBlobConfig, gotBlobConfig)
})
}
}
// ---------------
// integration tests that require object locking to be enabled on the bucket.
// ---------------

View File

@ -0,0 +1,13 @@
package repository
import (
"time"
)
// PersistentConfig represents configuration info that is persisted in the corso
// repo and can be updated with repository.UpdatePersistentConfig. Leaving a
// field as nil will result in no updates being made to it (i.e. PATCH
// semantics).
type PersistentConfig struct {
MinEpochDuration *time.Duration
}