diff --git a/candiutils/locker.go b/candiutils/locker.go index 63a6138..65ed57e 100644 --- a/candiutils/locker.go +++ b/candiutils/locker.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/golangid/candi/options" "github.com/gomodule/redigo/redis" ) @@ -15,39 +16,37 @@ type ( // RedisLocker lock using redis RedisLocker struct { pool *redis.Pool - lockeroptions LockerOptions + lockeroptions options.LockerOptions } // NoopLocker empty locker NoopLocker struct{} - - // Options for RedisLocker - LockerOptions struct { - Prefix string - TTL time.Duration - } - - // Option function type for setting options - LockerOption func(*LockerOptions) ) // WithPrefix sets the prefix for keys -func WithPrefixLocker(prefix string) LockerOption { - return func(o *LockerOptions) { +func WithPrefixLocker(prefix string) options.LockerOption { + return func(o *options.LockerOptions) { o.Prefix = prefix } } // WithTTL sets the default TTL for keys -func WithTTLLocker(ttl time.Duration) LockerOption { - return func(o *LockerOptions) { +func WithTTLLocker(ttl time.Duration) options.LockerOption { + return func(o *options.LockerOptions) { o.TTL = ttl } } +// WithLimit sets the limit for keys +func WithLimitLocker(limit int) options.LockerOption { + return func(o *options.LockerOptions) { + o.Limit = limit + } +} + // NewRedisLocker constructor -func NewRedisLocker(pool *redis.Pool, opts ...LockerOption) *RedisLocker { - lockeroptions := LockerOptions{ +func NewRedisLocker(pool *redis.Pool, opts ...options.LockerOption) *RedisLocker { + lockeroptions := options.LockerOptions{ Prefix: "LOCKFOR", TTL: 0, } @@ -104,6 +103,28 @@ func (r *RedisLocker) IsLockedTTL(key string, TTL time.Duration) bool { return incr > 1 } +func (r *RedisLocker) IsLockedWithOpts(key string, opts ...options.LockerOption) bool { + conn := r.pool.Get() + defer conn.Close() + + lockOpt := r.lockeroptions + for _, opt := range opts { + opt(&lockOpt) + } + + lockKey := fmt.Sprintf("%s:%s", r.lockeroptions.Prefix, key) + incr, err := redis.Int64(conn.Do("INCR", lockKey)) + if err != nil { + return false + } + + withLimit := lockOpt.Limit > 1 + if lockOpt.TTL > 0 && !(withLimit && incr == 1) { + conn.Do("EXPIRE", lockKey, int(lockOpt.TTL.Seconds())) + } + return incr > int64(lockOpt.Limit) +} + func (r *RedisLocker) HasBeenLocked(key string) bool { conn := r.pool.Get() defer conn.Close() @@ -215,6 +236,9 @@ func (NoopLocker) IsLocked(string) bool { return false } // IsLockedTTL method func (NoopLocker) IsLockedTTL(string, time.Duration) bool { return false } +// IsLockedWithOpts method +func (NoopLocker) IsLockedWithOpts(string, ...options.LockerOption) bool { return false } + // HasBeenLocked method func (NoopLocker) HasBeenLocked(string) bool { return false } @@ -227,10 +251,10 @@ func (NoopLocker) Reset(string) {} // Lock method func (NoopLocker) Lock(string, time.Duration) (func(), error) { return func() {}, nil } -func (NoopLocker) Disconnect(context.Context) error { return nil } - // GetPrefix method func (NoopLocker) GetPrefixLocker() string { return "" } // GetTTLLocker method func (NoopLocker) GetTTLLocker() time.Duration { return 0 } + +func (NoopLocker) Disconnect(context.Context) error { return nil } diff --git a/codebase/interfaces/locker.go b/codebase/interfaces/locker.go index afc484a..515d822 100644 --- a/codebase/interfaces/locker.go +++ b/codebase/interfaces/locker.go @@ -1,12 +1,17 @@ package interfaces -import "time" +import ( + "time" + + "github.com/golangid/candi/options" +) type ( // Locker abstraction, lock concurrent process Locker interface { IsLocked(key string) bool IsLockedTTL(key string, ttl time.Duration) bool + IsLockedWithOpts(key string, opts ...options.LockerOption) bool HasBeenLocked(key string) bool Unlock(key string) Reset(key string) diff --git a/mocks/candiutils/HTTPRequest.go b/mocks/candiutils/HTTPRequest.go index b2a01e2..5a05654 100644 --- a/mocks/candiutils/HTTPRequest.go +++ b/mocks/candiutils/HTTPRequest.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks diff --git a/mocks/candiutils/WorkerPool.go b/mocks/candiutils/WorkerPool.go index 8bb492c..0a87032 100644 --- a/mocks/candiutils/WorkerPool.go +++ b/mocks/candiutils/WorkerPool.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks @@ -9,7 +9,7 @@ import ( ) // WorkerPool is an autogenerated mock type for the WorkerPool type -type WorkerPool[T any] struct { +type WorkerPool[T interface{}] struct { mock.Mock } @@ -30,7 +30,7 @@ func (_m *WorkerPool[T]) Finish() { // NewWorkerPool creates a new instance of WorkerPool. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewWorkerPool[T any](t interface { +func NewWorkerPool[T interface{}](t interface { mock.TestingT Cleanup(func()) }) *WorkerPool[T] { diff --git a/mocks/candiutils/cronparser/Schedule.go b/mocks/candiutils/cronparser/Schedule.go index 5b4250d..73ab9ee 100644 --- a/mocks/candiutils/cronparser/Schedule.go +++ b/mocks/candiutils/cronparser/Schedule.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks diff --git a/mocks/codebase/interfaces/Locker.go b/mocks/codebase/interfaces/Locker.go index 531c21a..80c053b 100644 --- a/mocks/codebase/interfaces/Locker.go +++ b/mocks/codebase/interfaces/Locker.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.46.3. DO NOT EDIT. package mocks @@ -123,6 +123,24 @@ func (_m *Locker) IsLockedTTL(key string, ttl time.Duration) bool { return r0 } +// IsLockedTTLWithLimit provides a mock function with given fields: key, limit, TTL +func (_m *Locker) IsLockedTTLWithLimit(key string, limit int, TTL time.Duration) bool { + ret := _m.Called(key, limit, TTL) + + if len(ret) == 0 { + panic("no return value specified for IsLockedTTLWithLimit") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(string, int, time.Duration) bool); ok { + r0 = rf(key, limit, TTL) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // Lock provides a mock function with given fields: key, timeout func (_m *Locker) Lock(key string, timeout time.Duration) (func(), error) { ret := _m.Called(key, timeout) diff --git a/mocks/options/LockerOption.go b/mocks/options/LockerOption.go new file mode 100644 index 0000000..34ccf5d --- /dev/null +++ b/mocks/options/LockerOption.go @@ -0,0 +1,32 @@ +// Code generated by mockery v2.46.0. DO NOT EDIT. + +package mocks + +import ( + options "github.com/golangid/candi/options" + mock "github.com/stretchr/testify/mock" +) + +// LockerOption is an autogenerated mock type for the LockerOption type +type LockerOption struct { + mock.Mock +} + +// Execute provides a mock function with given fields: _a0 +func (_m *LockerOption) Execute(_a0 *options.LockerOptions) { + _m.Called(_a0) +} + +// NewLockerOption creates a new instance of LockerOption. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewLockerOption(t interface { + mock.TestingT + Cleanup(func()) +}) *LockerOption { + mock := &LockerOption{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/options/locker.go b/options/locker.go new file mode 100644 index 0000000..aafe8bc --- /dev/null +++ b/options/locker.go @@ -0,0 +1,15 @@ +package options + +import "time" + +type ( + // Options for RedisLocker + LockerOptions struct { + Prefix string + TTL time.Duration + Limit int + } + + // Option function type for setting options + LockerOption func(*LockerOptions) +)