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:
parent
78632f56e7
commit
b9e33901f4
@ -2,6 +2,7 @@ package kopia
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/kopia/kopia/repo"
|
"github.com/kopia/kopia/repo"
|
||||||
@ -11,7 +12,11 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/model"
|
"github.com/alcionai/corso/src/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
const stableIDKey = "stableID"
|
const (
|
||||||
|
stableIDKey = "stableID"
|
||||||
|
modelVersionKey = "storeVersion"
|
||||||
|
globalModelVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound = errors.New("not found")
|
ErrNotFound = errors.New("not found")
|
||||||
@ -27,12 +32,14 @@ func NewModelStore(c *conn) (*ModelStore, error) {
|
|||||||
return nil, errors.Wrap(err, "creating ModelStore")
|
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.
|
// ModelStore must not be accessed after the given KopiaWrapper is closed.
|
||||||
type ModelStore struct {
|
type ModelStore struct {
|
||||||
c *conn
|
c *conn
|
||||||
|
// Stash a reference here so testing can easily change it.
|
||||||
|
modelVersion int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *ModelStore) Close(ctx context.Context) error {
|
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(
|
func tagsForModelWithID(
|
||||||
s model.Schema,
|
s model.Schema,
|
||||||
id model.StableID,
|
id model.StableID,
|
||||||
|
version int,
|
||||||
tags map[string]string,
|
tags map[string]string,
|
||||||
) (map[string]string, error) {
|
) (map[string]string, error) {
|
||||||
if !s.Valid() {
|
if !s.Valid() {
|
||||||
@ -91,6 +99,12 @@ func tagsForModelWithID(
|
|||||||
|
|
||||||
res[stableIDKey] = string(id)
|
res[stableIDKey] = string(id)
|
||||||
|
|
||||||
|
if _, ok := res[modelVersionKey]; ok {
|
||||||
|
return nil, errors.WithStack(errBadTagKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
res[modelVersionKey] = strconv.Itoa(version)
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +126,7 @@ func putInner(
|
|||||||
base.ID = model.StableID(uuid.NewString())
|
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 {
|
if err != nil {
|
||||||
// Will be wrapped at a higher layer.
|
// Will be wrapped at a higher layer.
|
||||||
return err
|
return err
|
||||||
@ -140,6 +154,8 @@ func (ms *ModelStore) Put(
|
|||||||
return errors.WithStack(errUnrecognizedSchema)
|
return errors.WithStack(errUnrecognizedSchema)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.Base().Version = ms.modelVersion
|
||||||
|
|
||||||
err := repo.WriteSession(
|
err := repo.WriteSession(
|
||||||
ctx,
|
ctx,
|
||||||
ms.c,
|
ms.c,
|
||||||
@ -159,22 +175,45 @@ func (ms *ModelStore) Put(
|
|||||||
|
|
||||||
func stripHiddenTags(tags map[string]string) {
|
func stripHiddenTags(tags map[string]string) {
|
||||||
delete(tags, stableIDKey)
|
delete(tags, stableIDKey)
|
||||||
|
delete(tags, modelVersionKey)
|
||||||
delete(tags, manifest.TypeLabelKey)
|
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]
|
id, ok := m.Labels[stableIDKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.WithStack(errNoStableID)
|
return errors.WithStack(errNoStableID)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &model.BaseModel{
|
v, err := strconv.Atoi(m.Labels[modelVersionKey])
|
||||||
ModelStoreID: m.ID,
|
if err != nil {
|
||||||
ID: model.StableID(id),
|
return errors.Wrap(err, "parsing model version")
|
||||||
Tags: m.Labels,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return res, nil
|
||||||
}
|
}
|
||||||
@ -208,7 +247,7 @@ func (ms *ModelStore) GetIDsForType(
|
|||||||
res := make([]*model.BaseModel, 0, len(metadata))
|
res := make([]*model.BaseModel, 0, len(metadata))
|
||||||
|
|
||||||
for _, m := range metadata {
|
for _, m := range metadata {
|
||||||
bm, err := baseModelFromMetadata(m)
|
bm, err := ms.baseModelFromMetadata(m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "parsing model metadata")
|
return nil, errors.Wrap(err, "parsing model metadata")
|
||||||
}
|
}
|
||||||
@ -304,12 +343,10 @@ func (ms *ModelStore) GetWithModelStoreID(
|
|||||||
return errors.WithStack(errModelTypeMismatch)
|
return errors.WithStack(errModelTypeMismatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
base := data.Base()
|
return errors.Wrap(
|
||||||
base.Tags = metadata.Labels
|
ms.populateBaseModelFromMetadata(data.Base(), metadata),
|
||||||
stripHiddenTags(base.Tags)
|
"getting model by ID",
|
||||||
base.ModelStoreID = id
|
)
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkPrevModelVersion compares the ModelType and ModelStoreID in this model
|
// checkPrevModelVersion compares the ModelType and ModelStoreID in this model
|
||||||
@ -368,6 +405,8 @@ func (ms *ModelStore) Update(
|
|||||||
return errors.WithStack(errNoModelStoreID)
|
return errors.WithStack(errNoModelStoreID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base.Version = ms.modelVersion
|
||||||
|
|
||||||
// TODO(ashmrtnz): Can remove if bottleneck.
|
// TODO(ashmrtnz): Can remove if bottleneck.
|
||||||
if err := ms.checkPrevModelVersion(ctx, s, base); err != nil {
|
if err := ms.checkPrevModelVersion(ctx, s, base); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -29,7 +29,7 @@ func getModelStore(t *testing.T, ctx context.Context) *ModelStore {
|
|||||||
c, err := openKopiaRepo(t, ctx)
|
c, err := openKopiaRepo(t, ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return &ModelStore{c}
|
return &ModelStore{c: c, modelVersion: globalModelVersion}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------
|
// ---------------
|
||||||
@ -101,6 +101,12 @@ func (suite *ModelStoreIntegrationSuite) TestBadTagsErrors() {
|
|||||||
manifest.TypeLabelKey: "foo",
|
manifest.TypeLabelKey: "foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "storeVersion",
|
||||||
|
tags: map[string]string{
|
||||||
|
manifest.TypeLabelKey: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range table {
|
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() {
|
func (suite *ModelStoreIntegrationSuite) TestPutGet() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
s model.Schema
|
s model.Schema
|
||||||
@ -247,6 +270,7 @@ func (suite *ModelStoreIntegrationSuite) TestPutGet() {
|
|||||||
|
|
||||||
require.NotEmpty(t, foo.ModelStoreID)
|
require.NotEmpty(t, foo.ModelStoreID)
|
||||||
require.NotEmpty(t, foo.ID)
|
require.NotEmpty(t, foo.ID)
|
||||||
|
require.Equal(t, globalModelVersion, foo.Version)
|
||||||
|
|
||||||
returned := &fooModel{}
|
returned := &fooModel{}
|
||||||
err = suite.m.Get(suite.ctx, test.s, foo.ID, returned)
|
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)
|
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() {
|
func (suite *ModelStoreIntegrationSuite) TestPutGetOfType() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
s model.Schema
|
s model.Schema
|
||||||
@ -527,12 +567,14 @@ func (suite *ModelStoreIntegrationSuite) TestPutUpdate() {
|
|||||||
name: "NoTags",
|
name: "NoTags",
|
||||||
mutator: func(m *fooModel) {
|
mutator: func(m *fooModel) {
|
||||||
m.Bar = "baz"
|
m.Bar = "baz"
|
||||||
|
m.Version = 42
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "WithTags",
|
name: "WithTags",
|
||||||
mutator: func(m *fooModel) {
|
mutator: func(m *fooModel) {
|
||||||
m.Bar = "baz"
|
m.Bar = "baz"
|
||||||
|
m.Version = 42
|
||||||
m.Tags = map[string]string{
|
m.Tags = map[string]string{
|
||||||
"a": "42",
|
"a": "42",
|
||||||
}
|
}
|
||||||
@ -558,11 +600,15 @@ func (suite *ModelStoreIntegrationSuite) TestPutUpdate() {
|
|||||||
|
|
||||||
oldModelID := foo.ModelStoreID
|
oldModelID := foo.ModelStoreID
|
||||||
oldStableID := foo.ID
|
oldStableID := foo.ID
|
||||||
|
oldVersion := foo.Version
|
||||||
|
|
||||||
test.mutator(foo)
|
test.mutator(foo)
|
||||||
|
|
||||||
require.NoError(t, m.Update(ctx, theModelType, foo))
|
require.NoError(t, m.Update(ctx, theModelType, foo))
|
||||||
assert.Equal(t, oldStableID, foo.ID)
|
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{}
|
returned := &fooModel{}
|
||||||
require.NoError(
|
require.NoError(
|
||||||
@ -571,7 +617,8 @@ func (suite *ModelStoreIntegrationSuite) TestPutUpdate() {
|
|||||||
|
|
||||||
ids, err := m.GetIDsForType(ctx, theModelType, nil)
|
ids, err := m.GetIDsForType(ctx, theModelType, nil)
|
||||||
require.NoError(t, err)
|
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 {
|
if oldModelID == foo.ModelStoreID {
|
||||||
// Unlikely, but we don't control ModelStoreID generation and can't
|
// Unlikely, but we don't control ModelStoreID generation and can't
|
||||||
|
|||||||
@ -44,6 +44,9 @@ type BaseModel struct {
|
|||||||
// to refer to this one. This field may change if the model is updated. This
|
// to refer to this one. This field may change if the model is updated. This
|
||||||
// field should be treated as read-only by users.
|
// field should be treated as read-only by users.
|
||||||
ModelStoreID manifest.ID `json:"-"`
|
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
|
// 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
|
// the struct are not serialized directly into the stored model, but are part
|
||||||
// of the metadata for the model.
|
// of the metadata for the model.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user