From 344c0b27b9b662454f2194a3b528771a27a4c60d Mon Sep 17 00:00:00 2001 From: Wan Muhafidz Faldi Date: Fri, 15 Nov 2024 11:06:52 +0700 Subject: [PATCH 1/2] Add function IsLockedTTLWithLimit checks if the key has been incremented more than the specified limit --- candiutils/locker.go | 30 +++++++++++++++++++++++++++ codebase/interfaces/locker.go | 1 + mocks/candiutils/LockerOption.go | 32 +++++++++++++++++++++++++++++ mocks/codebase/interfaces/Locker.go | 20 +++++++++++++++++- 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 mocks/candiutils/LockerOption.go diff --git a/candiutils/locker.go b/candiutils/locker.go index 63a6138..8ca05cd 100644 --- a/candiutils/locker.go +++ b/candiutils/locker.go @@ -104,6 +104,33 @@ func (r *RedisLocker) IsLockedTTL(key string, TTL time.Duration) bool { return incr > 1 } +// IsLockedTTLWithLimit checks if the key has been incremented more than the specified limit +// within the given TTL. If the key is being created for the first time, it sets the TTL. +// Example usage: check if a key has been incremented more than 10 times within 1 minute. +func (r *RedisLocker) IsLockedTTLWithLimit(key string, limit int, TTL time.Duration) bool { + conn := r.pool.Get() + defer conn.Close() + + lockKey := fmt.Sprintf("%s:%s", r.lockeroptions.Prefix, key) + incr, err := redis.Int64(conn.Do("INCR", lockKey)) + if err != nil { + return false + } + + var expireTime time.Duration + if TTL > 0 { + expireTime = TTL + } else { + expireTime = r.lockeroptions.TTL + } + + if expireTime > 0 && incr == 1 { + conn.Do("EXPIRE", lockKey, int(expireTime.Seconds())) + } + + return incr > int64(limit) +} + func (r *RedisLocker) HasBeenLocked(key string) bool { conn := r.pool.Get() defer conn.Close() @@ -234,3 +261,6 @@ func (NoopLocker) GetPrefixLocker() string { return "" } // GetTTLLocker method func (NoopLocker) GetTTLLocker() time.Duration { return 0 } + +// IsLockedTTLWithLimit method +func (NoopLocker) IsLockedTTLWithLimit(string, int, time.Duration) bool { return false } diff --git a/codebase/interfaces/locker.go b/codebase/interfaces/locker.go index afc484a..3ec642f 100644 --- a/codebase/interfaces/locker.go +++ b/codebase/interfaces/locker.go @@ -13,6 +13,7 @@ type ( Lock(key string, timeout time.Duration) (unlockFunc func(), err error) GetPrefixLocker() string GetTTLLocker() time.Duration + IsLockedTTLWithLimit(key string, limit int, TTL time.Duration) bool Closer } ) diff --git a/mocks/candiutils/LockerOption.go b/mocks/candiutils/LockerOption.go new file mode 100644 index 0000000..715c2e1 --- /dev/null +++ b/mocks/candiutils/LockerOption.go @@ -0,0 +1,32 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mocks + +import ( + candiutils "github.com/golangid/candi/candiutils" + 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 *candiutils.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/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) From b0b5e0b787db0ed0db736dbb55550a85bd71506a Mon Sep 17 00:00:00 2001 From: 9iksans Date: Thu, 16 Jan 2025 14:26:24 +0700 Subject: [PATCH 2/2] feat - add candiutils redis locker with options --- candiutils/locker.go | 64 +++++++++---------- codebase/interfaces/locker.go | 8 ++- mocks/candiutils/HTTPRequest.go | 2 +- mocks/candiutils/WorkerPool.go | 6 +- mocks/candiutils/cronparser/Schedule.go | 2 +- mocks/{candiutils => options}/LockerOption.go | 6 +- options/locker.go | 15 +++++ 7 files changed, 58 insertions(+), 45 deletions(-) rename mocks/{candiutils => options}/LockerOption.go (80%) create mode 100644 options/locker.go diff --git a/candiutils/locker.go b/candiutils/locker.go index 8ca05cd..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,31 +103,26 @@ func (r *RedisLocker) IsLockedTTL(key string, TTL time.Duration) bool { return incr > 1 } -// IsLockedTTLWithLimit checks if the key has been incremented more than the specified limit -// within the given TTL. If the key is being created for the first time, it sets the TTL. -// Example usage: check if a key has been incremented more than 10 times within 1 minute. -func (r *RedisLocker) IsLockedTTLWithLimit(key string, limit int, TTL time.Duration) bool { +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 } - var expireTime time.Duration - if TTL > 0 { - expireTime = TTL - } else { - expireTime = r.lockeroptions.TTL - } - - if expireTime > 0 && incr == 1 { - conn.Do("EXPIRE", lockKey, int(expireTime.Seconds())) + withLimit := lockOpt.Limit > 1 + if lockOpt.TTL > 0 && !(withLimit && incr == 1) { + conn.Do("EXPIRE", lockKey, int(lockOpt.TTL.Seconds())) } - - return incr > int64(limit) + return incr > int64(lockOpt.Limit) } func (r *RedisLocker) HasBeenLocked(key string) bool { @@ -242,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 } @@ -254,13 +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 } -// IsLockedTTLWithLimit method -func (NoopLocker) IsLockedTTLWithLimit(string, int, time.Duration) bool { return false } +func (NoopLocker) Disconnect(context.Context) error { return nil } diff --git a/codebase/interfaces/locker.go b/codebase/interfaces/locker.go index 3ec642f..515d822 100644 --- a/codebase/interfaces/locker.go +++ b/codebase/interfaces/locker.go @@ -1,19 +1,23 @@ 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) Lock(key string, timeout time.Duration) (unlockFunc func(), err error) GetPrefixLocker() string GetTTLLocker() time.Duration - IsLockedTTLWithLimit(key string, limit int, TTL time.Duration) bool Closer } ) 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/candiutils/LockerOption.go b/mocks/options/LockerOption.go similarity index 80% rename from mocks/candiutils/LockerOption.go rename to mocks/options/LockerOption.go index 715c2e1..34ccf5d 100644 --- a/mocks/candiutils/LockerOption.go +++ b/mocks/options/LockerOption.go @@ -1,9 +1,9 @@ -// Code generated by mockery v2.46.3. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks import ( - candiutils "github.com/golangid/candi/candiutils" + options "github.com/golangid/candi/options" mock "github.com/stretchr/testify/mock" ) @@ -13,7 +13,7 @@ type LockerOption struct { } // Execute provides a mock function with given fields: _a0 -func (_m *LockerOption) Execute(_a0 *candiutils.LockerOptions) { +func (_m *LockerOption) Execute(_a0 *options.LockerOptions) { _m.Called(_a0) } 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) +)