diff --git a/.gitignore b/.gitignore index 8c33cbb..87c5d9d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ # Dependency directories (remove the comment below to include it) vendor/ -.idea \ No newline at end of file +.idea +_backup \ No newline at end of file diff --git a/cache/manager.go b/cache/manager.go deleted file mode 100644 index b784b4f..0000000 --- a/cache/manager.go +++ /dev/null @@ -1,56 +0,0 @@ -package cache - -import ( - "sync" - - "github.com/go-packagist/go-kratos-components/contract/cache" -) - -type Config struct { - Default string - - Stores map[string]cache.Store -} - -type Manager struct { - config *Config - - stores map[string]cache.Repository - rw sync.RWMutex -} - -func NewManager(config *Config) *Manager { - return &Manager{ - config: config, - stores: make(map[string]cache.Repository), - } -} - -func (m *Manager) Connect(names ...string) cache.Repository { - if len(names) == 0 { - names = []string{m.config.Default} - } - - name := names[0] - - m.rw.RLock() - if store, ok := m.stores[name]; ok { - m.rw.RUnlock() - return store - } - m.rw.RUnlock() - - m.rw.Lock() - defer m.rw.Unlock() - m.stores[name] = m.resolve(name) - - return m.stores[name] -} - -func (m *Manager) resolve(name string) cache.Repository { - if store, ok := m.config.Stores[name]; ok { - return NewRepository(store) - } - - panic("cache: unknown store name: " + name) -} diff --git a/cache/manager_test.go b/cache/manager_test.go deleted file mode 100644 index 66c521d..0000000 --- a/cache/manager_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package cache - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/go-packagist/go-kratos-components/contract/cache" - "github.com/stretchr/testify/assert" -) - -type mockStore struct { - items map[string]interface{} - rw sync.RWMutex -} - -func newMockStore() cache.Store { - return &mockStore{ - items: make(map[string]interface{}), - } -} - -func (m *mockStore) Has(ctx context.Context, key string) bool { - m.rw.RLock() - defer m.rw.RUnlock() - - _, ok := m.items[key] - - return ok -} - -func (m *mockStore) Get(ctx context.Context, key string, dest interface{}) error { - m.rw.RLock() - defer m.rw.RUnlock() - - if item, ok := m.items[key]; ok { - return valueOf(item, dest) - } - - return cache.ErrKeyNotFound -} - -func (m *mockStore) Put(ctx context.Context, key string, value interface{}, ttl time.Duration) error { - m.rw.Lock() - defer m.rw.Unlock() - - m.items[key] = value - - return nil -} - -func (m *mockStore) Increment(ctx context.Context, key string, value int) (int, error) { - panic("implement me") -} - -func (m *mockStore) Decrement(ctx context.Context, key string, value int) (int, error) { - panic("implement me") -} - -func (m *mockStore) Forever(ctx context.Context, key string, value interface{}) error { - panic("implement me") -} - -func (m *mockStore) Forget(ctx context.Context, key string) error { - panic("implement me") -} - -func (m *mockStore) Flush(ctx context.Context) error { - panic("implement me") -} - -func (m *mockStore) GetPrefix() string { - panic("implement me") -} - -var _ cache.Store = (*mockStore)(nil) - -func TestManager(t *testing.T) { - m := NewManager(&Config{ - Default: "test1", - Stores: map[string]cache.Store{ - "test1": newMockStore(), - "test2": newMockStore(), - }, - }) - - var test1, test2, test3, test4 string - - // use default - assert.NoError(t, m.Connect().Put(ctx, "test", "test", time.Second*10)) - assert.NoError(t, m.Connect().Get(ctx, "test", &test1)) - assert.Equal(t, "test", test1) - - // use test1 - assert.NoError(t, m.Connect("test1").Get(ctx, "test", &test2)) - assert.Equal(t, "test", test2) - - // use test2 - assert.Error(t, m.Connect("test2").Get(ctx, "test", &test3)) - assert.NotEqual(t, "test", test3) - - assert.NoError(t, m.Connect("test2").Put(ctx, "test", "test", time.Second*10)) - assert.NoError(t, m.Connect("test2").Get(ctx, "test", &test4)) - assert.Equal(t, "test", test4) - - // unknown - assert.Panics(t, func() { - m.Connect("unknown").Get(ctx, "test", &test3) - }) -} diff --git a/cache/redis/store.go b/cache/redis/store.go deleted file mode 100644 index e8b252c..0000000 --- a/cache/redis/store.go +++ /dev/null @@ -1,140 +0,0 @@ -package redis - -import ( - "context" - "time" - - "github.com/go-packagist/go-kratos-components/contract/cache" - "github.com/go-packagist/go-kratos-components/serializer" - "github.com/go-packagist/go-kratos-components/serializer/json" - "github.com/redis/go-redis/v9" -) - -type options struct { - prefix string - redis redis.Cmdable - serializer serializer.Serializable -} - -func (o *options) setup() { - if o.redis == nil { - panic("redis is nil") - } - - if o.serializer == nil { - o.serializer = json.Serializer - } -} - -type Option func(*options) - -func Prefix(prefix string) Option { - return func(o *options) { - if prefix != "" { - o.prefix = prefix + ":" - } - } -} - -func Redis(redis redis.Cmdable) Option { - return func(o *options) { - o.redis = redis - } -} - -func Serializer(serializer serializer.Serializable) Option { - return func(o *options) { - o.serializer = serializer - } -} - -type Store struct { - opt *options -} - -var _ cache.Store = (*Store)(nil) -var _ cache.Addable = (*Store)(nil) - -func New(opts ...Option) cache.Store { - o := &options{} - for _, opt := range opts { - opt(o) - } - - o.setup() - - return &Store{ - opt: o, - } -} - -func (s *Store) Has(ctx context.Context, key string) bool { - if result := s.opt.redis.Exists(ctx, s.opt.prefix+key); result.Err() == nil && result.Val() == 1 { - return true - } - - return false -} - -func (s *Store) Get(ctx context.Context, key string, dest interface{}) error { - result := s.opt.redis.Get(ctx, s.opt.prefix+key) - - if result.Err() != nil { - return result.Err() - } - - return s.opt.serializer.Unserialize([]byte(result.Val()), dest) -} - -func (s *Store) Put(ctx context.Context, key string, value interface{}, ttl time.Duration) error { - if data, err := s.opt.serializer.Serialize(value); err != nil { - return err - } else { - return s.opt.redis.SetEx(ctx, s.opt.prefix+key, data, ttl).Err() - } -} - -func (s *Store) Increment(ctx context.Context, key string, value int) (int, error) { - if result := s.opt.redis.IncrBy(ctx, s.opt.prefix+key, int64(value)); result.Err() != nil { - return 0, result.Err() - } else { - return int(result.Val()), nil - } -} - -func (s *Store) Decrement(ctx context.Context, key string, value int) (int, error) { - if result := s.opt.redis.DecrBy(ctx, s.opt.prefix+key, int64(value)); result.Err() != nil { - return 0, result.Err() - } else { - return int(result.Val()), nil - } -} - -func (s *Store) Forever(ctx context.Context, key string, value interface{}) error { - if data, err := s.opt.serializer.Serialize(value); err != nil { - return err - } else { - return s.opt.redis.Set(ctx, s.opt.prefix+key, data, redis.KeepTTL).Err() - } -} - -func (s *Store) Forget(ctx context.Context, key string) error { - return s.opt.redis.Del(ctx, s.opt.prefix+key).Err() -} - -func (s *Store) Flush(ctx context.Context) error { - return s.opt.redis.FlushAll(ctx).Err() -} - -func (s *Store) GetPrefix() string { - return s.opt.prefix -} - -func (s *Store) Add(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) { - if data, err := s.opt.serializer.Serialize(value); err != nil { - return false, err - } else { - result := s.opt.redis.SetNX(ctx, s.opt.prefix+key, data, ttl) - return result.Val(), result.Err() - } -} diff --git a/cache/redis/store_test.go b/cache/redis/store_test.go deleted file mode 100644 index a3ac67c..0000000 --- a/cache/redis/store_test.go +++ /dev/null @@ -1,159 +0,0 @@ -package redis - -import ( - "context" - "testing" - "time" - - "github.com/go-packagist/go-kratos-components/contract/cache" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/assert" -) - -var ( - ctx = context.Background() - addr = "127.0.0.1:6379" -) - -type person struct { - Name string - Age int - Sex string -} - -func createStore() cache.Store { - return New( - Prefix("redis-test"), - Redis( - redis.NewClient(&redis.Options{ - Addr: addr, - }), - ), - ) -} - -func TestRedis_Put(t *testing.T) { - c := createStore() - defer c.Flush(ctx) - c.Flush(ctx) - - // put - assert.NoError(t, c.Put(ctx, "int", 1, time.Second*10)) - assert.NoError(t, c.Put(ctx, "int32", int32(1), time.Second*10)) - assert.NoError(t, c.Put(ctx, "string", "test", time.Second*10)) - assert.NoError(t, c.Put(ctx, "bool", true, time.Second*10)) - var now = time.Now() - assert.NoError(t, c.Put(ctx, "time", time.Now(), time.Second*10)) - assert.NoError(t, c.Put(ctx, "person", &person{ - Name: "test", - Age: 18, - Sex: "male", - }, time.Second*10)) - assert.NoError(t, c.Put(ctx, "person", person{ - Name: "test", - Age: 18, - Sex: "male", - }, time.Second*10)) - assert.NoError(t, c.Put(ctx, "nil", nil, time.Second*10)) - - // check - var ( - intVal int - int32Val int32 - stringVal string - boolVal bool - timeVal time.Time - personVal *person - personVal2 person - nilVal interface{} - ) - assert.NoError(t, c.Get(ctx, "int", &intVal)) - assert.NoError(t, c.Get(ctx, "int32", &int32Val)) - assert.NoError(t, c.Get(ctx, "string", &stringVal)) - assert.NoError(t, c.Get(ctx, "bool", &boolVal)) - assert.NoError(t, c.Get(ctx, "time", &timeVal)) - assert.NoError(t, c.Get(ctx, "person", &personVal)) - assert.NoError(t, c.Get(ctx, "person", &personVal2)) - assert.NoError(t, c.Get(ctx, "nil", &nilVal)) - - assert.Equal(t, 1, intVal) - assert.Equal(t, int32(1), int32Val) - assert.Equal(t, "test", stringVal) - assert.Equal(t, true, boolVal) - assert.Equal(t, now.Format("2006-01-02 15:04:05"), timeVal.Format("2006-01-02 15:04:05")) - assert.Equal(t, &person{ - Name: "test", - Age: 18, - Sex: "male", - }, personVal) - assert.Equal(t, person{ - Name: "test", - Age: 18, - Sex: "male", - }, personVal2) - assert.Nil(t, nilVal) -} - -func TestRedis_Get(t *testing.T) { - c := createStore() - defer c.Flush(ctx) - c.Flush(ctx) - - assert.NoError(t, c.Put(ctx, "int", 1, time.Second*10)) - var intVal int - assert.NoError(t, c.Get(ctx, "int", &intVal)) - assert.Equal(t, 1, intVal) - - // err - var notExistVal int - assert.Error(t, c.Get(ctx, "not_exist", ¬ExistVal)) -} - -func TestRedis_GetPrefix(t *testing.T) { - c := createStore() - - assert.Equal(t, "redis-test:", c.GetPrefix()) -} - -func TestRedis_Forget(t *testing.T) { - c := createStore() - defer c.Flush(ctx) - c.Flush(ctx) - - assert.NoError(t, c.Put(ctx, "int", 1, time.Second*10)) - var intVal int - assert.NoError(t, c.Get(ctx, "int", &intVal)) - assert.Equal(t, 1, intVal) - - assert.NoError(t, c.Forget(ctx, "int")) - - var notExistVal int - assert.Error(t, c.Get(ctx, "int", ¬ExistVal)) -} - -func TestRedis_Forever(t *testing.T) { - c := createStore() - defer c.Flush(ctx) - c.Flush(ctx) - - assert.NoError(t, c.Forever(ctx, "int", 1)) - -} - -func TestRedis_Add(t *testing.T) { - c := createStore() - defer c.Flush(ctx) - c.Flush(ctx) - - if addable, ok := c.(cache.Addable); ok { - r1, err1 := addable.Add(ctx, "int", 1, time.Second*10) - assert.NoError(t, err1) - assert.True(t, r1) - - r2, err2 := addable.Add(ctx, "int", 1, time.Second*10) - assert.NoError(t, err2) - assert.False(t, r2) - } else { - t.Errorf("Addable interface is not implemented") - } -} diff --git a/cache/repository.go b/cache/repository.go deleted file mode 100644 index 1637c58..0000000 --- a/cache/repository.go +++ /dev/null @@ -1,60 +0,0 @@ -package cache - -import ( - "context" - "time" - - "github.com/go-packagist/go-kratos-components/contract/cache" -) - -type Repository struct { - cache.Store -} - -func NewRepository(store cache.Store) cache.Repository { - return &Repository{ - Store: store, - } -} - -func (r *Repository) Missing(ctx context.Context, key string) bool { - return !r.Store.Has(ctx, key) -} - -func (r *Repository) Delete(ctx context.Context, key string) error { - return r.Store.Forget(ctx, key) -} - -func (r *Repository) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { - return r.Store.Put(ctx, key, value, ttl) -} - -func (r *Repository) Add(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) { - if addable, ok := r.Store.(cache.Addable); ok { - return addable.Add(ctx, key, value, ttl) - } - - if r.Missing(ctx, key) { - if err := r.Set(ctx, key, value, ttl); err != nil { - return false, err - } else { - return true, nil - } - } - - return false, nil -} - -func (r *Repository) Remember(ctx context.Context, key string, dest interface{}, value func() interface{}, ttl time.Duration) error { - if r.Missing(ctx, key) { - v := value() - - if err := r.Set(ctx, key, v, ttl); err != nil { - return err - } - - return valueOf(v, dest) - } - - return r.Get(ctx, key, dest) -} diff --git a/cache/respository_test.go b/cache/respository_test.go deleted file mode 100644 index 63aba9e..0000000 --- a/cache/respository_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package cache - -import ( - "context" - "testing" - "time" - - redisCache "github.com/go-packagist/go-kratos-components/cache/redis" - "github.com/go-packagist/go-kratos-components/contract/cache" - "github.com/redis/go-redis/v9" - "github.com/stretchr/testify/assert" -) - -var ctx = context.Background() - -func createRedisRepository() cache.Repository { - return NewRepository( - redisCache.New( - redisCache.Prefix("repository"), - redisCache.Redis( - redis.NewClient(&redis.Options{ - Addr: "127.0.0.1:6379", - }), - ), - ), - ) -} - -func TestRepository_Add(t *testing.T) { - r := createRedisRepository() - - added, err := r.Add(ctx, "test", 1, time.Second*30) - assert.NoError(t, err) - assert.True(t, added) - - added2, err2 := r.Add(ctx, "test", 1, time.Second*30) - assert.NoError(t, err2) - assert.False(t, added2) -} - -func TestRepository_Remember(t *testing.T) { - r := createRedisRepository() - - var value string - err1 := r.Remember(ctx, "remember", &value, func() interface{} { - return "test" - }, time.Second*10) - - assert.NoError(t, err1) - assert.Equal(t, "test", value) - - err2 := r.Remember(ctx, "remember", &value, func() interface{} { - return "test2" - }, time.Second*10) - assert.NoError(t, err2) - assert.Equal(t, "test", value) -} diff --git a/cache/util.go b/cache/util.go deleted file mode 100644 index 8082d5d..0000000 --- a/cache/util.go +++ /dev/null @@ -1,27 +0,0 @@ -package cache - -import ( - "fmt" - "reflect" -) - -var ( - ErrDestMustBePointer = fmt.Errorf("cache: dest must be a pointer") - ErrDestMustNotBeNil = fmt.Errorf("cache: dest must not be nil") -) - -func valueOf(src interface{}, dest interface{}) error { - rv := reflect.ValueOf(dest) - - if rv.Kind() != reflect.Ptr { - return ErrDestMustBePointer - } - - if rv.IsNil() { - return ErrDestMustNotBeNil - } - - rv.Elem().Set(reflect.ValueOf(src)) - - return nil -} diff --git a/contract/cache/repository.go b/contract/cache/repository.go deleted file mode 100644 index 7a9c6df..0000000 --- a/contract/cache/repository.go +++ /dev/null @@ -1,22 +0,0 @@ -package cache - -import ( - "context" - "errors" - "time" -) - -var ( - ErrKeyAlreadyExists = errors.New("cache: key already exists") - ErrKeyNotFound = errors.New("cache: key not found") -) - -type Repository interface { - Store - Addable - - Missing(ctx context.Context, key string) bool - Delete(ctx context.Context, key string) error - Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error - Remember(ctx context.Context, key string, dest interface{}, value func() interface{}, ttl time.Duration) error -} diff --git a/x/README.md b/x/README.md new file mode 100644 index 0000000..6532d48 --- /dev/null +++ b/x/README.md @@ -0,0 +1,3 @@ +# X + +The packages in this directory are not stable versions, please use with caution! \ No newline at end of file diff --git a/x/cache/redis/store.go b/x/cache/redis/store.go new file mode 100644 index 0000000..2fa4e7d --- /dev/null +++ b/x/cache/redis/store.go @@ -0,0 +1,130 @@ +package redis + +import ( + "context" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/go-packagist/go-kratos-components/serializer" + "github.com/go-packagist/go-kratos-components/serializer/json" + "github.com/go-packagist/go-kratos-components/x/cache" +) + +type Store struct { + redis redis.Cmdable + + opts *options +} + +type options struct { + prefix string + serializer serializer.Serializable +} + +type Option func(*options) + +func Prefix(prefix string) Option { + return func(o *options) { + if prefix != "" { + o.prefix = prefix + ":" + } + } +} + +func Serializer(serializer serializer.Serializable) Option { + return func(o *options) { + o.serializer = serializer + } +} + +var ( + _ cache.Store = (*Store)(nil) +) + +func New(redis redis.Cmdable, opts ...Option) *Store { + opt := &options{ + serializer: json.Serializer, + } + + for _, o := range opts { + o(opt) + } + + return &Store{ + redis: redis, + opts: opt, + } +} + +func (s *Store) Has(ctx context.Context, key string) (bool, error) { + if r := s.redis.Exists(ctx, s.opts.prefix+key); r.Err() != nil { + return false, r.Err() + } else { + return r.Val() > 0, nil + } +} + +func (s *Store) Get(ctx context.Context, key string, dest interface{}) error { + if r := s.redis.Get(ctx, s.opts.prefix+key); r.Err() != nil { + return r.Err() + } else { + return s.opts.serializer.Unserialize([]byte(r.Val()), dest) + } +} + +func (s *Store) Put(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) { + if valued, err := s.opts.serializer.Serialize(value); err != nil { + return false, err + } else if r := s.redis.Set(ctx, s.opts.prefix+key, valued, ttl); r.Err() != nil { + return false, r.Err() + } else { + return r.Val() == "OK", nil + } +} + +func (s *Store) Increment(ctx context.Context, key string, value int) (int, error) { + if r := s.redis.IncrBy(ctx, s.opts.prefix+key, int64(value)); r.Err() != nil { + return 0, r.Err() + } else { + return int(r.Val()), nil + } +} + +func (s *Store) Decrement(ctx context.Context, key string, value int) (int, error) { + if r := s.redis.DecrBy(ctx, s.opts.prefix+key, int64(value)); r.Err() != nil { + return 0, r.Err() + } else { + return int(r.Val()), nil + } +} + +func (s *Store) Forever(ctx context.Context, key string, value interface{}) (bool, error) { + if valued, err := s.opts.serializer.Serialize(value); err != nil { + return false, err + } else if r := s.redis.Set(ctx, s.opts.prefix+key, valued, redis.KeepTTL); r.Err() != nil { + return false, r.Err() + } else { + return r.Val() == "OK", nil + } +} + +func (s *Store) Forget(ctx context.Context, key string) (bool, error) { + if r := s.redis.Del(ctx, s.opts.prefix+key); r.Err() != nil { + return false, r.Err() + } else { + return r.Val() > 0, nil + } +} + +func (s *Store) Flush(ctx context.Context) (bool, error) { + if r := s.redis.FlushAll(ctx); r.Err() != nil { + return false, r.Err() + } else { + return r.Val() == "OK", nil + } +} + +func (s *Store) GetPrefix() string { + return s.opts.prefix +} diff --git a/x/cache/redis/store_test.go b/x/cache/redis/store_test.go new file mode 100644 index 0000000..65a229e --- /dev/null +++ b/x/cache/redis/store_test.go @@ -0,0 +1 @@ +package redis diff --git a/x/cache/repository.go b/x/cache/repository.go new file mode 100644 index 0000000..f409e8c --- /dev/null +++ b/x/cache/repository.go @@ -0,0 +1,79 @@ +package cache + +import ( + "context" + "time" + + "github.com/go-packagist/go-kratos-components/helper" +) + +type Repository interface { + Store + Addable + + Missing(ctx context.Context, key string) (bool, error) + Delete(ctx context.Context, key string) (bool, error) + Set(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) + Remember(ctx context.Context, key string, dest interface{}, value func() interface{}, ttl time.Duration) error +} + +type repository struct { + Store +} + +func NewRepository(store Store) Repository { + return &repository{ + Store: store, + } +} + +func (r *repository) Missing(ctx context.Context, key string) (bool, error) { + if had, err := r.Store.Has(ctx, key); err != nil { + return false, err + } else { + return !had, nil + } +} +func (r *repository) Add(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) { + // if the store is addable, use it + if store, ok := r.Store.(Addable); ok { + return store.Add(ctx, key, value, ttl) + } + + // otherwise, use the default implementation + if missing, err := r.Missing(ctx, key); err != nil { + return false, err + } else if missing { + if status, err := r.Set(ctx, key, value, ttl); err != nil { + return false, err + } else { + return status, nil + } + } else { + return false, nil + } +} + +func (r *repository) Delete(ctx context.Context, key string) (bool, error) { + return r.Forget(ctx, key) +} + +func (r *repository) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) { + return r.Put(ctx, key, value, ttl) +} + +func (r *repository) Remember(ctx context.Context, key string, dest interface{}, value func() interface{}, ttl time.Duration) error { + if missing, err := r.Missing(ctx, key); err != nil { + return err + } else if missing { + v := value() + + if _, err := r.Set(ctx, key, v, ttl); err != nil { + return err + } + + return helper.ValueOf(v, dest) + } else { + return r.Get(ctx, key, dest) + } +} diff --git a/contract/cache/store.go b/x/cache/store.go similarity index 65% rename from contract/cache/store.go rename to x/cache/store.go index 3858740..6bc5463 100644 --- a/contract/cache/store.go +++ b/x/cache/store.go @@ -6,21 +6,21 @@ import ( ) type Store interface { - Has(ctx context.Context, key string) bool + Has(ctx context.Context, key string) (bool, error) Get(ctx context.Context, key string, dest interface{}) error - Put(ctx context.Context, key string, value interface{}, ttl time.Duration) error + Put(ctx context.Context, key string, value interface{}, ttl time.Duration) (bool, error) Increment(ctx context.Context, key string, value int) (int, error) Decrement(ctx context.Context, key string, value int) (int, error) - Forever(ctx context.Context, key string, value interface{}) error + Forever(ctx context.Context, key string, value interface{}) (bool, error) - Forget(ctx context.Context, key string) error + Forget(ctx context.Context, key string) (bool, error) - Flush(ctx context.Context) error + Flush(ctx context.Context) (bool, error) GetPrefix() string }