diff --git a/pkg/database/cacheDB/cache.go b/pkg/database/cacheDB/cache.go index b3697af..42a1e8e 100644 --- a/pkg/database/cacheDB/cache.go +++ b/pkg/database/cacheDB/cache.go @@ -5,32 +5,49 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "github.com/colibri-project-io/colibri-sdk-go/pkg/base/config" ) +const ( + errRedisNil string = "redis: nil" + errRedisMoved string = "MOVED" +) + // Cache struct type Cache[T any] struct { name string ttl time.Duration } -// NewCache create a new pointer to Cache struct. +// NewCache creates a new pointer to Cache struct. +// +// Parameters: +// - name: a string representing the name of the cache. +// - ttl: a time.Duration representing the time to live for the cache items. +// Returns a pointer to Cache[T]. func NewCache[T any](name string, ttl time.Duration) *Cache[T] { return &Cache[T]{name, ttl} } -// Many returns a slice of T value +// Many retrieves multiple items of type T from the cache. +// +// ctx: The context for the cache operation. +// Returns a slice of retrieved items of type T and an error. func (c *Cache[T]) Many(ctx context.Context) ([]T, error) { if err := c.validate(); err != nil { return nil, err } - result, err := instance.Get(ctx, c.getNamePrefixed()).Bytes() + result, err := c.get(ctx) if err != nil { return nil, err } + if result == nil { + return nil, nil + } list := make([]T, 0) if err = json.Unmarshal(result, &list); err != nil { @@ -40,16 +57,22 @@ func (c *Cache[T]) Many(ctx context.Context) ([]T, error) { return list, nil } -// One return a pointer of T value +// One retrieves a single item of type T from the cache. +// +// ctx: The context for the cache operation. +// Returns a pointer to the retrieved item of type T and an error. func (c *Cache[T]) One(ctx context.Context) (*T, error) { if err := c.validate(); err != nil { return nil, err } - result, err := instance.Get(ctx, c.getNamePrefixed()).Bytes() + result, err := c.get(ctx) if err != nil { return nil, err } + if result == nil { + return nil, nil + } model := new(T) if err = json.Unmarshal(result, &model); err != nil { @@ -59,7 +82,11 @@ func (c *Cache[T]) One(ctx context.Context) (*T, error) { return model, nil } -// Set save data in cacheDB +// Set save data in cacheDB. +// +// ctx: The context for the cache operation. +// data: The data to be saved in the cache. +// Returns an error. func (c *Cache[T]) Set(ctx context.Context, data any) error { if err := c.validate(); err != nil { return err @@ -70,18 +97,25 @@ func (c *Cache[T]) Set(ctx context.Context, data any) error { return err } - return instance.Set(ctx, c.getNamePrefixed(), jsonData, c.ttl).Err() + return c.set(ctx, jsonData) } -// Del delete data in cachedDB +// Del delete data in cachedDB. +// +// ctx: The context for the cache operation. +// Returns an error. func (c *Cache[T]) Del(ctx context.Context) error { if err := c.validate(); err != nil { return err } - return instance.Del(ctx, c.getNamePrefixed()).Err() + return c.del(ctx) } +// validate checks if the cache is initialized and has a name. +// +// No parameters. +// Returns an error. func (c *Cache[T]) validate() error { if instance == nil { return errors.New("Cache not initialized") @@ -94,6 +128,88 @@ func (c *Cache[T]) validate() error { return nil } +// getNamePrefixed returns a string with the prefixed name using the application name and cache name. +// +// No parameters. +// Returns a string. func (c *Cache[T]) getNamePrefixed() string { return fmt.Sprintf("%s::%s", config.APP_NAME, c.name) } + +// isErrRedisMoved checks if the error contains the string "MOVED". +// +// Parameter: +// - err: The error to check. +// Return type: bool +func (c *Cache[T]) isErrRedisMoved(err error) bool { + return strings.Contains(err.Error(), errRedisMoved) +} + +// reconectInstanceAfterError updates the address of the instance based on the last element of the error message. +// +// Parameter: +// - err: The error that triggered the reconnection. +func (c *Cache[T]) reconectInstanceAfterError(err error) { + movedSetInfo := strings.Split(err.Error(), " ") + instance.Options().Addr = movedSetInfo[len(movedSetInfo)-1] +} + +// get retrieves data from the cache and handles errors including redis MOVED error. +// +// ctx: The context for the cache operation. +// Returns a byte slice and an error. +func (c *Cache[T]) get(ctx context.Context) ([]byte, error) { + for { + result, err := instance.Get(ctx, c.getNamePrefixed()).Bytes() + if err != nil { + if err.Error() == errRedisNil { + return nil, nil + } else if c.isErrRedisMoved(err) { + c.reconectInstanceAfterError(err) + continue + } else { + return nil, err + } + } + return result, nil + } +} + +// set saves data in the cacheDB. +// +// ctx: The context for the cache operation. +// data: The data to be saved in the cache. +// Returns an error. +func (c *Cache[T]) set(ctx context.Context, data []byte) error { + for { + err := instance.Set(ctx, c.getNamePrefixed(), data, c.ttl).Err() + if err != nil { + if c.isErrRedisMoved(err) { + c.reconectInstanceAfterError(err) + continue + } else { + return err + } + } + return nil + } +} + +// del deletes data in cachedDB. +// +// ctx: The context for the cache operation. +// Returns an error. +func (c *Cache[T]) del(ctx context.Context) error { + for { + err := instance.Del(ctx, c.getNamePrefixed()).Err() + if err != nil { + if c.isErrRedisMoved(err) { + c.reconectInstanceAfterError(err) + continue + } else { + return err + } + } + return nil + } +} diff --git a/pkg/database/cacheDB/cache_db.go b/pkg/database/cacheDB/cache_db.go index da08014..85a2d5b 100644 --- a/pkg/database/cacheDB/cache_db.go +++ b/pkg/database/cacheDB/cache_db.go @@ -12,20 +12,12 @@ import ( type cacheDBObserver struct{} -func (o cacheDBObserver) Close() { - logging.Info("waiting to safely close the cache connection") - if observer.WaitRunningTimeout() { - logging.Warn("WaitGroup timed out, forcing close the cache connection") - } - - logging.Info("closing cache connection") - if err := instance.Close(); err != nil { - logging.Error("error when closing cache connection: %v", err) - } -} - var instance *redis.Client +// Initialize initializes the cache database connection. +// +// No parameters. +// No return values. func Initialize() { opts := &redis.Options{Addr: config.CACHE_URI, Password: config.CACHE_PASSWORD} @@ -36,7 +28,22 @@ func Initialize() { } instance = redisClient - logging.Info("Cache database connected") observer.Attach(cacheDBObserver{}) logging.Info("Cache database connected") } + +// Close closes the cache connection safely. +// +// No parameters. +// No return values. +func (o cacheDBObserver) Close() { + logging.Info("waiting to safely close the cache connection") + if observer.WaitRunningTimeout() { + logging.Warn("WaitGroup timed out, forcing close the cache connection") + } + + logging.Info("closing cache connection") + if err := instance.Close(); err != nil { + logging.Error("error when closing cache connection: %v", err) + } +} diff --git a/pkg/database/cacheDB/cache_test.go b/pkg/database/cacheDB/cache_test.go index 0dc0e6b..ad262f9 100644 --- a/pkg/database/cacheDB/cache_test.go +++ b/pkg/database/cacheDB/cache_test.go @@ -25,6 +25,7 @@ func TestCacheNotInitializedValidation(t *testing.T) { } func TestCache(t *testing.T) { + ctx := context.Background() expected := []userCached{ {Id: 1, Name: "User 1"}, {Id: 2, Name: "User 2"}, @@ -40,31 +41,31 @@ func TestCache(t *testing.T) { assert.NotNil(t, result.ttl) }) - t.Run("Should return error when cache name is empty value", func(t *testing.T) { + t.Run("Should return error when cache name is empty value on call many", func(t *testing.T) { cache := Cache[userCached]{} - _, err := cache.Many(context.Background()) + _, err := cache.Many(ctx) assert.NotNil(t, err) }) t.Run("Should return error when cache name is empty value on call one", func(t *testing.T) { cache := Cache[userCached]{} - _, err := cache.One(context.Background()) + _, err := cache.One(ctx) assert.NotNil(t, err) }) t.Run("Should return error when cache name is empty value on call set", func(t *testing.T) { cache := Cache[userCached]{} - err := cache.Set(context.Background(), cache) + err := cache.Set(ctx, cache) assert.NotNil(t, err) }) t.Run("Should return error when cache name is empty value on call del", func(t *testing.T) { cache := Cache[userCached]{} - err := cache.Del(context.Background()) + err := cache.Del(ctx) assert.NotNil(t, err) }) @@ -74,17 +75,17 @@ func TestCache(t *testing.T) { "invalid": make(chan int), } - err := cache.Set(context.Background(), invalid) + err := cache.Set(ctx, invalid) assert.NotNil(t, err) }) t.Run("Should set many data in cache", func(t *testing.T) { cache := NewCache[userCached]("cache-test", time.Hour) - err := cache.Set(context.Background(), expected) + err := cache.Set(ctx, expected) assert.Nil(t, err) - result, err := cache.Many(context.Background()) + result, err := cache.Many(ctx) assert.Nil(t, err) assert.NotNil(t, result) assert.Len(t, result, 3) @@ -94,10 +95,10 @@ func TestCache(t *testing.T) { t.Run("Should set one data in cache", func(t *testing.T) { cache := NewCache[userCached]("cache-test", time.Hour) - err := cache.Set(context.Background(), expected[0]) + err := cache.Set(ctx, expected[0]) assert.Nil(t, err) - result, err := cache.One(context.Background()) + result, err := cache.One(ctx) assert.Nil(t, err) assert.NotNil(t, result) assert.Equal(t, expected[0], *result) @@ -106,20 +107,20 @@ func TestCache(t *testing.T) { t.Run("Should del data in cache", func(t *testing.T) { cache := NewCache[userCached]("cache-test", time.Hour) - err := cache.Set(context.Background(), expected) + err := cache.Set(ctx, expected) assert.Nil(t, err) - result, err := cache.Many(context.Background()) + result, err := cache.Many(ctx) assert.Nil(t, err) assert.NotNil(t, result) assert.Len(t, result, 3) assert.Equal(t, expected, result) - err = cache.Del(context.Background()) + err = cache.Del(ctx) assert.Nil(t, err) - result, err = cache.Many(context.Background()) - assert.NotNil(t, err) + result, err = cache.Many(ctx) + assert.Nil(t, err) assert.Nil(t, result) }) }