diff --git a/pkg/members/middleware/caching_middleware.go b/pkg/members/middleware/caching_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..2faa5ce433281d8d396bb5c912e4e91cfaff727f --- /dev/null +++ b/pkg/members/middleware/caching_middleware.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + service "git.perx.ru/perxis/perxis-go/pkg/members" +) + +func makeKey(ss ...string) string { + return strings.Join(ss, "-") +} + +func CachingMiddleware(cache *cache.Cache) Middleware { + return func(next service.Members) service.Members { + return &cachingMiddleware{ + cache: cache, + next: next, + } + } +} + +type cachingMiddleware struct { + cache *cache.Cache + next service.Members +} + +func (m cachingMiddleware) Set(ctx context.Context, orgId string, userId string, role service.Role) (err error) { + + err = m.next.Set(ctx, orgId, userId, role) + if err == nil { + m.cache.Remove(makeKey(orgId, userId)) + m.cache.Remove(makeKey(orgId)) + m.cache.Remove(makeKey(userId)) + } + return err +} + +func (m cachingMiddleware) Get(ctx context.Context, orgId string, userId string) (role service.Role, err error) { + + key := makeKey(orgId, userId) + value, e := m.cache.Get(key) + if e == nil { + return value.(service.Role), err + } + role, err = m.next.Get(ctx, orgId, userId) + if err == nil { + m.cache.Set(key, role) + } + return role, err +} + +func (m cachingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) { + + err = m.next.Remove(ctx, orgId, userId) + if err == nil { + m.cache.Remove(makeKey(orgId, userId)) + m.cache.Remove(makeKey(orgId)) + m.cache.Remove(makeKey(userId)) + } + return err +} + +func (m cachingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) { + + err = m.next.RemoveAll(ctx, orgId) + if err == nil { + members, _ := m.ListMembers(ctx, orgId) + for _, member := range members { + m.cache.Remove(member.UserId) + m.cache.Remove(makeKey(orgId, member.UserId)) + } + } + return err +} + +func (m cachingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*service.Member, err error) { + + value, e := m.cache.Get(makeKey(orgId)) + if e == nil { + return value.([]*service.Member), err + } + members, err = m.next.ListMembers(ctx, orgId) + if err == nil { + m.cache.Set(makeKey(orgId), members) + } + return members, err +} + +func (m cachingMiddleware) ListOrganizations(ctx context.Context, userId string) (members []*service.Member, err error) { + + value, e := m.cache.Get(makeKey(userId)) + if e == nil { + return value.([]*service.Member), err + } + members, err = m.next.ListOrganizations(ctx, userId) + if err == nil { + m.cache.Set(makeKey(userId), members) + } + return members, err +} diff --git a/pkg/members/middleware/caching_middleware_test.go b/pkg/members/middleware/caching_middleware_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1844dc58ebb97b228800a95f9c9cb8b142407fb4 --- /dev/null +++ b/pkg/members/middleware/caching_middleware_test.go @@ -0,0 +1,147 @@ +package service + +import ( + "context" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/cache" + "git.perx.ru/perxis/perxis-go/pkg/members" + mocksmembers "git.perx.ru/perxis/perxis-go/pkg/members/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestMembersCache(t *testing.T) { + + const ( + orgId = "orgId" + userId = "userId" + size = 5 + ttl = 20 * time.Millisecond + ) + + ctx := context.Background() + + t.Run("Get from cache", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша, после повторного запроса get.") + + mbrs.AssertExpectations(t) + }) + + t.Run("Invalidate cache", func(t *testing.T) { + t.Run("After Set", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.") + + mbrs.On("Set", mock.Anything, orgId, userId, members.RoleMember).Return(nil).Once() + + err = svc.Set(ctx, orgId, userId, members.RoleMember) + require.NoError(t, err) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша и получение заново из сервиса.") + mbrs.AssertExpectations(t) + }) + + t.Run("After Remove", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.") + + mbrs.On("Remove", mock.Anything, orgId, userId).Return(nil).Once() + + err = svc.Remove(ctx, orgId, userId) + require.NoError(t, err) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.NotMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша после удаления из хранилища и получение заново из сервиса.") + + mbrs.AssertExpectations(t) + }) + + t.Run("After RemoveAll", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + mbrs.On("ListMembers", mock.Anything, orgId).Return([]*members.Member{{OrgId: orgId, UserId: userId, Role: members.RoleOwner}}, nil) + + v1, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.") + + mbrs.On("RemoveAll", mock.Anything, orgId).Return(nil).Once() + + err = svc.RemoveAll(ctx, orgId) + require.NoError(t, err) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.NotMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша после удаления из хранилища и получение заново из сервиса.") + + mbrs.AssertExpectations(t) + }) + + t.Run("After TTL expired", func(t *testing.T) { + mbrs := &mocksmembers.Members{} + svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once() + + v1, err := svc.Get(ctx, orgId, userId) + + v2, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.") + + time.Sleep(2 * ttl) + + mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleMember, nil).Once() + + v3, err := svc.Get(ctx, orgId, userId) + require.NoError(t, err) + assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша после истечения ttl и получение заново из сервиса.") + mbrs.AssertExpectations(t) + }) + }) +} diff --git a/pkg/members/middleware/error_logging_middleware.go b/pkg/members/middleware/error_logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..5eb4e3b3ff6ba6417fdb96310916cca0345a055f --- /dev/null +++ b/pkg/members/middleware/error_logging_middleware.go @@ -0,0 +1,90 @@ +// 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/members -i Members -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l "" + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/members" + "go.uber.org/zap" +) + +// errorLoggingMiddleware implements members.Members that is instrumented with logging +type errorLoggingMiddleware struct { + logger *zap.Logger + next members.Members +} + +// ErrorLoggingMiddleware instruments an implementation of the members.Members with simple logging +func ErrorLoggingMiddleware(logger *zap.Logger) Middleware { + return func(next members.Members) members.Members { + return &errorLoggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *errorLoggingMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Get(ctx, orgId, userId) +} + +func (m *errorLoggingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.ListMembers(ctx, orgId) +} + +func (m *errorLoggingMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.ListOrganizations(ctx, userId) +} + +func (m *errorLoggingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Remove(ctx, orgId, userId) +} + +func (m *errorLoggingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.RemoveAll(ctx, orgId) +} + +func (m *errorLoggingMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) { + logger := m.logger + defer func() { + if err != nil { + logger.Warn("response error", zap.Error(err)) + } + }() + return m.next.Set(ctx, orgId, userId, role) +} diff --git a/pkg/members/middleware/logging_middleware.go b/pkg/members/middleware/logging_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..3b1687039af00f3747cf75b52bd0e18bc4512f1e --- /dev/null +++ b/pkg/members/middleware/logging_middleware.go @@ -0,0 +1,251 @@ +// 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/members -i Members -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/members" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// loggingMiddleware implements members.Members that is instrumented with logging +type loggingMiddleware struct { + logger *zap.Logger + next members.Members +} + +// LoggingMiddleware instruments an implementation of the members.Members with simple logging +func LoggingMiddleware(logger *zap.Logger) Middleware { + return func(next members.Members) members.Members { + return &loggingMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *loggingMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId, + "userId": userId} { + 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...) + + role, err = m.next.Get(ctx, orgId, userId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "role": role, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("Get.Response", fields...) + + return role, err +} + +func (m *loggingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId} { + 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("ListMembers.Request", fields...) + + members, err = m.next.ListMembers(ctx, orgId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "members": members, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("ListMembers.Response", fields...) + + return members, err +} + +func (m *loggingMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "userId": userId} { + 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("ListOrganizations.Request", fields...) + + organizations, err = m.next.ListOrganizations(ctx, userId) + + fields = []zapcore.Field{ + zap.Duration("time", time.Since(begin)), + zap.Error(err), + } + + for k, v := range map[string]interface{}{ + "organizations": organizations, + "err": err} { + if k == "err" { + continue + } + fields = append(fields, zap.Reflect(k, v)) + } + + m.logger.Debug("ListOrganizations.Response", fields...) + + return organizations, err +} + +func (m *loggingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId, + "userId": userId} { + 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("Remove.Request", fields...) + + err = m.next.Remove(ctx, orgId, userId) + + 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("Remove.Response", fields...) + + return err +} + +func (m *loggingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId} { + 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("RemoveAll.Request", fields...) + + err = m.next.RemoveAll(ctx, orgId) + + 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("RemoveAll.Response", fields...) + + return err +} + +func (m *loggingMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) { + begin := time.Now() + var fields []zapcore.Field + for k, v := range map[string]interface{}{ + "ctx": ctx, + "orgId": orgId, + "userId": userId, + "role": role} { + 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("Set.Request", fields...) + + err = m.next.Set(ctx, orgId, userId, role) + + 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("Set.Response", fields...) + + return err +} diff --git a/pkg/members/middleware/middleware.go b/pkg/members/middleware/middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..1aa0cfbe798f5688587ab8e4f5d4e166383d8d92 --- /dev/null +++ b/pkg/members/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/members -i Members -t ../../../assets/templates/middleware/middleware -o middleware.go -l "" + +import ( + "git.perx.ru/perxis/perxis-go/pkg/members" + "go.uber.org/zap" +) + +type Middleware func(members.Members) members.Members + +func WithLog(s members.Members, logger *zap.Logger, log_access bool) members.Members { + if logger == nil { + logger = zap.NewNop() + } + + logger = logger.Named("Members") + s = ErrorLoggingMiddleware(logger)(s) + if log_access { + s = LoggingMiddleware(logger)(s) + } + s = RecoveringMiddleware(logger)(s) + return s +} diff --git a/pkg/members/middleware/recovering_middleware.go b/pkg/members/middleware/recovering_middleware.go new file mode 100644 index 0000000000000000000000000000000000000000..ec6db9f480d35215c47bff6fceef8c0e01a77447 --- /dev/null +++ b/pkg/members/middleware/recovering_middleware.go @@ -0,0 +1,103 @@ +// 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/members -i Members -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l "" + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/members" + "go.uber.org/zap" +) + +// recoveringMiddleware implements members.Members that is instrumented with logging +type recoveringMiddleware struct { + logger *zap.Logger + next members.Members +} + +// RecoveringMiddleware instruments an implementation of the members.Members with simple logging +func RecoveringMiddleware(logger *zap.Logger) Middleware { + return func(next members.Members) members.Members { + return &recoveringMiddleware{ + next: next, + logger: logger, + } + } +} + +func (m *recoveringMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, 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, orgId, userId) +} + +func (m *recoveringMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, 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.ListMembers(ctx, orgId) +} + +func (m *recoveringMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, 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.ListOrganizations(ctx, userId) +} + +func (m *recoveringMiddleware) Remove(ctx context.Context, orgId string, userId 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.Remove(ctx, orgId, userId) +} + +func (m *recoveringMiddleware) RemoveAll(ctx context.Context, orgId 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.RemoveAll(ctx, orgId) +} + +func (m *recoveringMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (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.Set(ctx, orgId, userId, role) +}