From b26157231d32a4e659e17bee63bb48d013e14056 Mon Sep 17 00:00:00 2001 From: Valera Shaitorov <shaitorov@perx.ru> Date: Mon, 17 Apr 2023 15:55:25 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20Collections=20middlewares?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../middleware/caching_middleware.go | 136 ++++++ .../middleware/caching_middleware_test.go | 458 ++++++++++++++++++ .../middleware/error_logging_middleware.go | 101 ++++ .../middleware/logging_middleware.go | 296 +++++++++++ pkg/collections/middleware/middleware.go | 28 ++ .../middleware/recovering_middleware.go | 116 +++++ 6 files changed, 1135 insertions(+) create mode 100644 pkg/collections/middleware/caching_middleware.go create mode 100644 pkg/collections/middleware/caching_middleware_test.go create mode 100644 pkg/collections/middleware/error_logging_middleware.go create mode 100644 pkg/collections/middleware/logging_middleware.go create mode 100644 pkg/collections/middleware/middleware.go create mode 100644 pkg/collections/middleware/recovering_middleware.go diff --git a/pkg/collections/middleware/caching_middleware.go b/pkg/collections/middleware/caching_middleware.go new file mode 100644 index 00000000..0bb41c9e --- /dev/null +++ b/pkg/collections/middleware/caching_middleware.go @@ -0,0 +1,136 @@ +package service + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/collections" + envService "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/schema" +) + +func makeKey(spaceId, envId, collectionId string, disableSchemaIncludes bool) string { + s := spaceId + "-" + envId + "-" + collectionId + "-" + if disableSchemaIncludes { + s += "1" + } else { + s += "0" + } + return s +} + +func CachingMiddleware(cache *cache.Cache, envs envService.Environments) Middleware { + return func(next service.Collections) service.Collections { + return &cachingMiddleware{ + cache: cache, + next: next, + envs: envs, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Collections + envs envService.Environments +} + +func (m cachingMiddleware) Create(ctx context.Context, collection *service.Collection) (coll *service.Collection, err error) { + return m.next.Create(ctx, collection) +} + +func (m cachingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*service.GetOptions) (coll *service.Collection, err error) { + + opts := service.MergeGetOptions(options...) + value, e := m.cache.Get(makeKey(spaceId, envId, collectionId, opts.DisableSchemaIncludes)) + if e == nil { + return value.(*service.Collection), err + } + coll, err = m.next.Get(ctx, spaceId, envId, collectionId, options...) + if err == nil { + env, err := m.envs.Get(ctx, coll.SpaceID, coll.EnvID) + if err != nil { + return nil, err + } + m.cache.Set(makeKey(coll.SpaceID, env.ID, coll.ID, opts.DisableSchemaIncludes), coll) + for _, al := range env.Aliases { + m.cache.Set(makeKey(coll.SpaceID, al, coll.ID, opts.DisableSchemaIncludes), coll) + } + + } + return coll, err +} + +func (m cachingMiddleware) List(ctx context.Context, spaceId, envId string, filter *service.Filter) (collections []*service.Collection, err error) { + return m.next.List(ctx, spaceId, envId, filter) +} + +func (m cachingMiddleware) Update(ctx context.Context, coll *service.Collection) (err error) { + + err = m.next.Update(ctx, coll) + if err == nil { + env, err := m.envs.Get(ctx, coll.SpaceID, coll.EnvID) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, coll.ID, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, coll.ID, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, coll.ID, true)) + m.cache.Remove(makeKey(env.SpaceID, al, coll.ID, false)) + } + } + return err +} + +func (m cachingMiddleware) SetSchema(ctx context.Context, spaceId, envId, collectionId string, schema *schema.Schema) (err error) { + err = m.next.SetSchema(ctx, spaceId, envId, collectionId, schema) + if err == nil { + env, err := m.envs.Get(ctx, spaceId, envId) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false)) + } + } + return err +} + +func (m cachingMiddleware) SetState(ctx context.Context, spaceId, envId, collectionId string, state *service.StateInfo) (err error) { + err = m.next.SetState(ctx, spaceId, envId, collectionId, state) + if err == nil { + env, err := m.envs.Get(ctx, spaceId, envId) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false)) + } + } + return err +} + +func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) { + + err = m.next.Delete(ctx, spaceId, envId, collectionId) + if err == nil { + env, err := m.envs.Get(ctx, spaceId, envId) + if err != nil { + return err + } + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false)) + for _, al := range env.Aliases { + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true)) + m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false)) + } + } + return err +} diff --git a/pkg/collections/middleware/caching_middleware_test.go b/pkg/collections/middleware/caching_middleware_test.go new file mode 100644 index 00000000..3a5f88e6 --- /dev/null +++ b/pkg/collections/middleware/caching_middleware_test.go @@ -0,0 +1,458 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/collections" + colsmocks "git.perx.ru/perxis/perxis-go/pkg/collections/mocks" + "git.perx.ru/perxis/perxis-go/pkg/environments" + envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestCollections_Cache(t *testing.T) { + + const ( + colID = "colID" + spaceID = "spaceID" + envID = "envId" + envAlias = "envAlias" + size = 5 + ttl = 20 * time.Millisecond + ) + + ErrNotFound := errors.NotFound(errors.New("not found")) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кеша при повторном запросе по ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по alias окружения.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("Get from cache(by Alias)", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кеша при повторном запросе по Alias окружения.") + + v3, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по ID окружения.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("Get from cache with options", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID, mock.Anything).Run(func(args mock.Arguments) { + require.Len(t, args, 5) + opt := args.Get(4).(*collections.GetOptions) + assert.True(t, opt.DisableSchemaIncludes) + }).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + _, err := svc.Get(ctx, spaceID, envID, colID, []*collections.GetOptions{{DisableSchemaIncludes: true}}...) + require.NoError(t, err) + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + //t.Run("List from cache", func(t *testing.T) { + // col := &colsmocks.Collections{} + // env := &envmocks.Environments{} + // + // svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + // + // col.On("List", mock.Anything, spaceID, envID).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + // env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + // + // vl1, err := svc.List(ctx, spaceID, envID, nil) + // require.NoError(t, err) + // + // vl2, err := svc.List(ctx, spaceID, envID, nil) + // require.NoError(t, err) + // assert.Len(t, vl2, 1) + // assert.Same(t, vl1[0], vl2[0], "При повторном запросе по ID окружения, ожидается получение списка объектов из кеша.") + // + // vl3, err := svc.List(ctx, spaceID, envAlias, nil) + // require.NoError(t, err) + // assert.Len(t, vl3, 1) + // assert.Same(t, vl3[0], vl2[0], "При повторном запросе по Alias окружения, ожидается получение списка объектов из кеша.") + // + // env.AssertExpectations(t) + // col.AssertExpectations(t) + //}) + + t.Run("List", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + _, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + + _, err = svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Update", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по Alias окружения.") + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + err = svc.Update(ctx, &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления был удален из кэша и будет запрошен заново из сервиса.") + + v5, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта из кеша по Alias окружения.") + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.NotSame(t, vl1[0], vl2[0], "Ожидает что после обновления элементы будут запрошены заново из сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Update(by Alias)", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + //env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по Alias окружения.") + + v3, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по ID окружения.") + + vl1, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Update", mock.Anything, mock.Anything).Return(nil).Once() + err = svc.Update(ctx, &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}) + require.NoError(t, err) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + //env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления был удален из кэша и будет запрошен заново из сервиса.") + + v5, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта из кеша по Alias окружения.") + + vl4, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.NotSame(t, vl1[0], vl4[0], "Ожидает что после обновления элементы будут запрошены заново из сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Set Schema", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по Alias окружения.") + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кеша по ID окружения.") + + vl3, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов из кеша по Alias окружения.") + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("SetSchema", mock.Anything, spaceID, envID, colID, mock.Anything).Return(nil).Once() + err = svc.SetSchema(ctx, spaceID, envID, colID, &schema.Schema{}) + require.NoError(t, err) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once() + + v4, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления схемы был удален из кэша и будет запрошен заново из сервиса.") + + v5, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v4, v5, "Ожидается получение объекта из кеша по Alias окружения.") + + vl4, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.NotSame(t, vl4[0], vl3[0], "Ожидает что после обновления схемы элементы будут запрошены заново из сервиса.") + + vl5, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Equal(t, vl4[0], vl5[0], "Ожидается получение объектов из кеша по Alias окружения..") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Delete", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по ID окружения.") + + v3, err := svc.Get(ctx, spaceID, envAlias, colID) + require.NoError(t, err) + assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по Alias окружения.") + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кеша по ID окружения.") + + vl3, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов из кеша по Alias окружения.") + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Delete", mock.Anything, spaceID, envID, colID).Return(nil).Once() + err = svc.Delete(ctx, spaceID, envID, colID) + require.NoError(t, err) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Get", mock.Anything, spaceID, envID, colID).Return(nil, ErrNotFound).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{}, nil).Once() + + _, err = svc.Get(ctx, spaceID, envID, colID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидает что элемент был удален из кэша и получена ошибка от сервиса.") + + vl4, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl4, 0, "Ожидает что элементы были удалены из кэша.") + + col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(nil, ErrNotFound).Once() + + _, err = svc.Get(ctx, spaceID, envAlias, colID) + require.Error(t, err) + assert.EqualError(t, err, "not found", "Ожидает что элемент был удален из кэша и получена ошибка от сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After Create", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once() + + vl1, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + + vl2, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кеша по ID окружения.") + + vl3, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl2, 1) + assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов из кеша по Alias окружения.") + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("Create", mock.Anything, mock.Anything).Return(&collections.Collection{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}, nil).Once() + _, err = svc.Create(ctx, &collections.Collection{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}) + require.NoError(t, err) + + //env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once() + col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{ + {ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, + {ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}, + }, nil).Once() + col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{ + {ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, + {ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}, + }, nil).Once() + + vl4, err := svc.List(ctx, spaceID, envID, nil) + require.NoError(t, err) + assert.Len(t, vl4, 2, "Ожидает что элементы были удалены из кэша и получены заново из сервиса.") + + vl5, err := svc.List(ctx, spaceID, envAlias, nil) + require.NoError(t, err) + assert.Len(t, vl5, 2) + assert.Equal(t, vl4[0], vl5[0], "Ожидается получение объектов из кеша по Alias окружения..") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + col := &colsmocks.Collections{} + env := &envmocks.Environments{} + + svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col) + + env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil) + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + v1, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + + v2, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.") + + time.Sleep(2 * ttl) + + col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once() + + v3, err := svc.Get(ctx, spaceID, envID, colID) + require.NoError(t, err) + assert.NotSame(t, v3, v2, "Ожидает что элемент был удален из кэша и будет запрошен заново из сервиса.") + + env.AssertExpectations(t) + col.AssertExpectations(t) + }) + }) + +} diff --git a/pkg/collections/middleware/error_logging_middleware.go b/pkg/collections/middleware/error_logging_middleware.go new file mode 100644 index 00000000..cdee51d6 --- /dev/null +++ b/pkg/collections/middleware/error_logging_middleware.go @@ -0,0 +1,101 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/error_log +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements collections.Collections that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next collections.Collections +} + +// ErrorLoggingMiddleware instruments an implementation of the collections.Collections with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next collections.Collections) collections.Collections { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Create(ctx, collection) +} + +func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Delete(ctx, spaceId, envId, collectionId) +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, spaceId, envId, collectionId, options...) +} + +func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.List(ctx, spaceId, envId, filter) +} + +func (m *errorLoggingMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.SetSchema(ctx, spaceId, envId, collectionId, schema) +} + +func (m *errorLoggingMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.SetState(ctx, spaceId, envId, collectionId, state) +} + +func (m *errorLoggingMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Update(ctx, coll) +} diff --git a/pkg/collections/middleware/logging_middleware.go b/pkg/collections/middleware/logging_middleware.go new file mode 100644 index 00000000..53fcc846 --- /dev/null +++ b/pkg/collections/middleware/logging_middleware.go @@ -0,0 +1,296 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/access_log +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l "" + +import ( + "context" + "fmt" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/auth" + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements collections.Collections that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next collections.Collections +} + +// LoggingMiddleware instruments an implementation of the collections.Collections with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next collections.Collections) collections.Collections { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "collection": collection} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Create.Request", fields...) + + created, err = m.next.Create(ctx, collection) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "created": created, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Create.Response", fields...) + + return created, err +} + +func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Delete.Request", fields...) + + err = m.next.Delete(ctx, spaceId, envId, collectionId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Delete.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "options": options} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Request", fields...) + + collection, err = m.next.Get(ctx, spaceId, envId, collectionId, options...) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "collection": collection, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return collection, err +} + +func (m *loggingMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "filter": filter} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Request", fields...) + + collections, err = m.next.List(ctx, spaceId, envId, filter) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "collections": collections, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("List.Response", fields...) + + return collections, err +} + +func (m *loggingMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "schema": schema} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("SetSchema.Request", fields...) + + err = m.next.SetSchema(ctx, spaceId, envId, collectionId, schema) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("SetSchema.Response", fields...) + + return err +} + +func (m *loggingMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "spaceId": spaceId, + "envId": envId, + "collectionId": collectionId, + "state": state} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("SetState.Request", fields...) + + err = m.next.SetState(ctx, spaceId, envId, collectionId, state) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("SetState.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "coll": coll} { + if k == "ctx" { + fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx)))) + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Update.Request", fields...) + + err = m.next.Update(ctx, coll) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Update.Response", fields...) + + return err +} diff --git a/pkg/collections/middleware/middleware.go b/pkg/collections/middleware/middleware.go new file mode 100644 index 00000000..911368f0 --- /dev/null +++ b/pkg/collections/middleware/middleware.go @@ -0,0 +1,28 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/middleware +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/collections" + "go.uber.org/zap" +) + +type Middleware func(collections.Collections) collections.Collections + +func WithLog(s collections.Collections, logger *zap.Logger, log_access bool) collections.Collections { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Collections") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/collections/middleware/recovering_middleware.go b/pkg/collections/middleware/recovering_middleware.go new file mode 100644 index 00000000..fb61326f --- /dev/null +++ b/pkg/collections/middleware/recovering_middleware.go @@ -0,0 +1,116 @@ +// Code generated by gowrap. DO NOT EDIT. +// template: ../../../assets/templates/middleware/recovery +// gowrap: http://github.com/hexdigest/gowrap + +package service + +//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/collections" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "go.uber.org/zap" +) + +// recoveringMiddleware implements collections.Collections that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next collections.Collections +} + +// RecoveringMiddleware instruments an implementation of the collections.Collections with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next collections.Collections) collections.Collections { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Create(ctx, collection) +} + +func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Delete(ctx, spaceId, envId, collectionId) +} + +func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Get(ctx, spaceId, envId, collectionId, options...) +} + +func (m *recoveringMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.List(ctx, spaceId, envId, filter) +} + +func (m *recoveringMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.SetSchema(ctx, spaceId, envId, collectionId, schema) +} + +func (m *recoveringMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.SetState(ctx, spaceId, envId, collectionId, state) +} + +func (m *recoveringMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) { + logger := m.logger + defer func() { + if r := recover(); r != nil { + logger.Error("panic", zap.Error(fmt.Errorf("%v", r))) + err = fmt.Errorf("%v", r) + } + }() + + return m.next.Update(ctx, coll) +} -- GitLab