Add versions to models (#1064)

## Description

Currently only the ModelStore populates and checks versions.

Some high-level points:
* fail any sort of get (just metadata or full model) if there's a version mismatch
* update operations automatically set things to the current version
* versions are stored as tags so there's some int->string (and vice versa) munging
* versions stored as tags so they can be repopulated even if only the metadata (BaseModel) for a model is pulled. This is done mostly for consistency

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🐹 Trivial/Minor

## Issue(s)

* closes #284 

## Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2022-10-05 17:46:49 -07:00 committed by GitHub
parent 78632f56e7
commit b9e33901f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 108 additions and 19 deletions

View File

@ -2,6 +2,7 @@ package kopia
import (
"context"
"strconv"
"github.com/google/uuid"
"github.com/kopia/kopia/repo"
@ -11,7 +12,11 @@ import (
"github.com/alcionai/corso/src/internal/model"
)
const stableIDKey = "stableID"
const (
stableIDKey = "stableID"
modelVersionKey = "storeVersion"
globalModelVersion = 1
)
var (
ErrNotFound = errors.New("not found")
@ -27,12 +32,14 @@ func NewModelStore(c *conn) (*ModelStore, error) {
return nil, errors.Wrap(err, "creating ModelStore")
}
return &ModelStore{c: c}, nil
return &ModelStore{c: c, modelVersion: globalModelVersion}, nil
}
// ModelStore must not be accessed after the given KopiaWrapper is closed.
type ModelStore struct {
c *conn
// Stash a reference here so testing can easily change it.
modelVersion int
}
func (ms *ModelStore) Close(ctx context.Context) error {
@ -70,6 +77,7 @@ func tagsForModel(s model.Schema, tags map[string]string) (map[string]string, er
func tagsForModelWithID(
s model.Schema,
id model.StableID,
version int,
tags map[string]string,
) (map[string]string, error) {
if !s.Valid() {
@ -91,6 +99,12 @@ func tagsForModelWithID(
res[stableIDKey] = string(id)
if _, ok := res[modelVersionKey]; ok {
return nil, errors.WithStack(errBadTagKey)
}
res[modelVersionKey] = strconv.Itoa(version)
return res, nil
}
@ -112,7 +126,7 @@ func putInner(
base.ID = model.StableID(uuid.NewString())
}
tmpTags, err := tagsForModelWithID(s, base.ID, base.Tags)
tmpTags, err := tagsForModelWithID(s, base.ID, base.Version, base.Tags)
if err != nil {
// Will be wrapped at a higher layer.
return err
@ -140,6 +154,8 @@ func (ms *ModelStore) Put(
return errors.WithStack(errUnrecognizedSchema)
}
m.Base().Version = ms.modelVersion
err := repo.WriteSession(
ctx,
ms.c,
@ -159,22 +175,45 @@ func (ms *ModelStore) Put(
func stripHiddenTags(tags map[string]string) {
delete(tags, stableIDKey)
delete(tags, modelVersionKey)
delete(tags, manifest.TypeLabelKey)
}
func baseModelFromMetadata(m *manifest.EntryMetadata) (*model.BaseModel, error) {
func (ms ModelStore) populateBaseModelFromMetadata(
base *model.BaseModel,
m *manifest.EntryMetadata,
) error {
id, ok := m.Labels[stableIDKey]
if !ok {
return nil, errors.WithStack(errNoStableID)
return errors.WithStack(errNoStableID)
}
res := &model.BaseModel{
ModelStoreID: m.ID,
ID: model.StableID(id),
Tags: m.Labels,
v, err := strconv.Atoi(m.Labels[modelVersionKey])
if err != nil {
return errors.Wrap(err, "parsing model version")
}
stripHiddenTags(res.Tags)
if v != ms.modelVersion {
return errors.Errorf("bad model version %s", m.Labels[modelVersionKey])
}
base.ModelStoreID = m.ID
base.ID = model.StableID(id)
base.Version = v
base.Tags = m.Labels
stripHiddenTags(base.Tags)
return nil
}
func (ms ModelStore) baseModelFromMetadata(
m *manifest.EntryMetadata,
) (*model.BaseModel, error) {
res := &model.BaseModel{}
if err := ms.populateBaseModelFromMetadata(res, m); err != nil {
return nil, err
}
return res, nil
}
@ -208,7 +247,7 @@ func (ms *ModelStore) GetIDsForType(
res := make([]*model.BaseModel, 0, len(metadata))
for _, m := range metadata {
bm, err := baseModelFromMetadata(m)
bm, err := ms.baseModelFromMetadata(m)
if err != nil {
return nil, errors.Wrap(err, "parsing model metadata")
}
@ -304,12 +343,10 @@ func (ms *ModelStore) GetWithModelStoreID(
return errors.WithStack(errModelTypeMismatch)
}
base := data.Base()
base.Tags = metadata.Labels
stripHiddenTags(base.Tags)
base.ModelStoreID = id
return nil
return errors.Wrap(
ms.populateBaseModelFromMetadata(data.Base(), metadata),
"getting model by ID",
)
}
// checkPrevModelVersion compares the ModelType and ModelStoreID in this model
@ -368,6 +405,8 @@ func (ms *ModelStore) Update(
return errors.WithStack(errNoModelStoreID)
}
base.Version = ms.modelVersion
// TODO(ashmrtnz): Can remove if bottleneck.
if err := ms.checkPrevModelVersion(ctx, s, base); err != nil {
return err

View File

@ -29,7 +29,7 @@ func getModelStore(t *testing.T, ctx context.Context) *ModelStore {
c, err := openKopiaRepo(t, ctx)
require.NoError(t, err)
return &ModelStore{c}
return &ModelStore{c: c, modelVersion: globalModelVersion}
}
// ---------------
@ -101,6 +101,12 @@ func (suite *ModelStoreIntegrationSuite) TestBadTagsErrors() {
manifest.TypeLabelKey: "foo",
},
},
{
name: "storeVersion",
tags: map[string]string{
manifest.TypeLabelKey: "foo",
},
},
}
for _, test := range table {
@ -204,6 +210,23 @@ func (suite *ModelStoreIntegrationSuite) TestBadTypeErrors() {
)
}
func (suite *ModelStoreIntegrationSuite) TestPutGetBadVersion() {
t := suite.T()
schema := model.BackupOpSchema
foo := &fooModel{Bar: uuid.NewString()}
// Avoid some silly test errors from comparing nil to empty map.
foo.Tags = map[string]string{}
err := suite.m.Put(suite.ctx, schema, foo)
require.NoError(t, err)
suite.m.modelVersion = 42
returned := &fooModel{}
err = suite.m.Get(suite.ctx, schema, foo.ID, returned)
assert.Error(t, err)
}
func (suite *ModelStoreIntegrationSuite) TestPutGet() {
table := []struct {
s model.Schema
@ -247,6 +270,7 @@ func (suite *ModelStoreIntegrationSuite) TestPutGet() {
require.NotEmpty(t, foo.ModelStoreID)
require.NotEmpty(t, foo.ID)
require.Equal(t, globalModelVersion, foo.Version)
returned := &fooModel{}
err = suite.m.Get(suite.ctx, test.s, foo.ID, returned)
@ -340,6 +364,22 @@ func (suite *ModelStoreIntegrationSuite) TestGet_NotFoundErrors() {
t, suite.m.GetWithModelStoreID(suite.ctx, model.BackupOpSchema, "baz", nil), ErrNotFound)
}
func (suite *ModelStoreIntegrationSuite) TestPutGetOfTypeBadVersion() {
t := suite.T()
schema := model.BackupOpSchema
foo := &fooModel{Bar: uuid.NewString()}
err := suite.m.Put(suite.ctx, schema, foo)
require.NoError(t, err)
suite.m.modelVersion = 42
ids, err := suite.m.GetIDsForType(suite.ctx, schema, nil)
assert.Error(t, err)
assert.Empty(t, ids)
}
func (suite *ModelStoreIntegrationSuite) TestPutGetOfType() {
table := []struct {
s model.Schema
@ -527,12 +567,14 @@ func (suite *ModelStoreIntegrationSuite) TestPutUpdate() {
name: "NoTags",
mutator: func(m *fooModel) {
m.Bar = "baz"
m.Version = 42
},
},
{
name: "WithTags",
mutator: func(m *fooModel) {
m.Bar = "baz"
m.Version = 42
m.Tags = map[string]string{
"a": "42",
}
@ -558,11 +600,15 @@ func (suite *ModelStoreIntegrationSuite) TestPutUpdate() {
oldModelID := foo.ModelStoreID
oldStableID := foo.ID
oldVersion := foo.Version
test.mutator(foo)
require.NoError(t, m.Update(ctx, theModelType, foo))
assert.Equal(t, oldStableID, foo.ID)
// The version in the model store has not changed so we get the old
// version back.
assert.Equal(t, oldVersion, foo.Version)
returned := &fooModel{}
require.NoError(
@ -571,7 +617,8 @@ func (suite *ModelStoreIntegrationSuite) TestPutUpdate() {
ids, err := m.GetIDsForType(ctx, theModelType, nil)
require.NoError(t, err)
assert.Len(t, ids, 1)
require.Len(t, ids, 1)
assert.Equal(t, globalModelVersion, ids[0].Version)
if oldModelID == foo.ModelStoreID {
// Unlikely, but we don't control ModelStoreID generation and can't

View File

@ -44,6 +44,9 @@ type BaseModel struct {
// to refer to this one. This field may change if the model is updated. This
// field should be treated as read-only by users.
ModelStoreID manifest.ID `json:"-"`
// Version is a version number that can help track changes across models.
// TODO(ashmrtn): Reference version control documentation.
Version int `json:"-"`
// Tags associated with this model in the store to facilitate lookup. Tags in
// the struct are not serialized directly into the stored model, but are part
// of the metadata for the model.