feat: key provider
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -12,50 +14,29 @@ type memoryStoreItem struct {
|
||||
}
|
||||
|
||||
// MemoryStore is an in-memory key-value store that is safe for concurrent use.
|
||||
// It now supports simple K/V, HASH, and LIST data types.
|
||||
type MemoryStore struct {
|
||||
mu sync.RWMutex
|
||||
data map[string]memoryStoreItem
|
||||
stopCh chan struct{} // Channel to stop the cleanup goroutine
|
||||
mu sync.RWMutex
|
||||
// Using 'any' to store different data structures (memoryStoreItem, map[string]string, []string)
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
// NewMemoryStore creates and returns a new MemoryStore instance.
|
||||
// It also starts a background goroutine to periodically clean up expired keys.
|
||||
func NewMemoryStore() *MemoryStore {
|
||||
s := &MemoryStore{
|
||||
data: make(map[string]memoryStoreItem),
|
||||
stopCh: make(chan struct{}),
|
||||
data: make(map[string]any),
|
||||
}
|
||||
go s.cleanupLoop(1 * time.Minute)
|
||||
// The cleanup loop was removed as it's not compatible with multiple data types
|
||||
// without a unified expiration mechanism, and the KeyPool feature does not rely on TTLs.
|
||||
return s
|
||||
}
|
||||
|
||||
// Close stops the background cleanup goroutine.
|
||||
// Close cleans up resources.
|
||||
func (s *MemoryStore) Close() error {
|
||||
close(s.stopCh)
|
||||
// Nothing to close for now.
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupLoop periodically iterates through the store and removes expired keys.
|
||||
func (s *MemoryStore) cleanupLoop(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.mu.Lock()
|
||||
now := time.Now().UnixNano()
|
||||
for key, item := range s.data {
|
||||
if item.expiresAt > 0 && now > item.expiresAt {
|
||||
delete(s.data, key)
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
case <-s.stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores a key-value pair.
|
||||
func (s *MemoryStore) Set(key string, value []byte, ttl time.Duration) error {
|
||||
@@ -77,13 +58,18 @@ func (s *MemoryStore) Set(key string, value []byte, ttl time.Duration) error {
|
||||
// Get retrieves a value by its key.
|
||||
func (s *MemoryStore) Get(key string) ([]byte, error) {
|
||||
s.mu.RLock()
|
||||
item, exists := s.data[key]
|
||||
rawItem, exists := s.data[key]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
item, ok := rawItem.(memoryStoreItem)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
|
||||
}
|
||||
|
||||
// Check for expiration
|
||||
if item.expiresAt > 0 && time.Now().UnixNano() > item.expiresAt {
|
||||
// Lazy deletion
|
||||
@@ -107,20 +93,213 @@ func (s *MemoryStore) Delete(key string) error {
|
||||
// Exists checks if a key exists.
|
||||
func (s *MemoryStore) Exists(key string) (bool, error) {
|
||||
s.mu.RLock()
|
||||
item, exists := s.data[key]
|
||||
rawItem, exists := s.data[key]
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if item.expiresAt > 0 && time.Now().UnixNano() > item.expiresAt {
|
||||
// Lazy deletion
|
||||
s.mu.Lock()
|
||||
delete(s.data, key)
|
||||
s.mu.Unlock()
|
||||
return false, nil
|
||||
// Check for expiration only if it's a simple K/V item
|
||||
if item, ok := rawItem.(memoryStoreItem); ok {
|
||||
if item.expiresAt > 0 && time.Now().UnixNano() > item.expiresAt {
|
||||
// Lazy deletion
|
||||
s.mu.Lock()
|
||||
delete(s.data, key)
|
||||
s.mu.Unlock()
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SetNX sets a key-value pair if the key does not already exist.
|
||||
func (s *MemoryStore) SetNX(key string, value []byte, ttl time.Duration) (bool, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// In memory store, we need to manually check for existence and expiration
|
||||
rawItem, exists := s.data[key]
|
||||
if exists {
|
||||
if item, ok := rawItem.(memoryStoreItem); ok {
|
||||
if item.expiresAt == 0 || time.Now().UnixNano() < item.expiresAt {
|
||||
// Key exists and is not expired
|
||||
return false, nil
|
||||
}
|
||||
} else {
|
||||
// Key exists but is not a simple K/V item, treat as existing
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Key does not exist or is expired, so we can set it.
|
||||
var expiresAt int64
|
||||
if ttl > 0 {
|
||||
expiresAt = time.Now().UnixNano() + ttl.Nanoseconds()
|
||||
}
|
||||
s.data[key] = memoryStoreItem{
|
||||
value: value,
|
||||
expiresAt: expiresAt,
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// --- HASH operations ---
|
||||
|
||||
func (s *MemoryStore) HSet(key, field string, value any) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var hash map[string]string
|
||||
rawHash, exists := s.data[key]
|
||||
if !exists {
|
||||
hash = make(map[string]string)
|
||||
s.data[key] = hash
|
||||
} else {
|
||||
var ok bool
|
||||
hash, ok = rawHash.(map[string]string)
|
||||
if !ok {
|
||||
return fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
|
||||
}
|
||||
}
|
||||
|
||||
hash[field] = fmt.Sprint(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) HGetAll(key string) (map[string]string, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
rawHash, exists := s.data[key]
|
||||
if !exists {
|
||||
// Per Redis convention, HGETALL on a non-existent key returns an empty map, not an error.
|
||||
return make(map[string]string), nil
|
||||
}
|
||||
|
||||
hash, ok := rawHash.(map[string]string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
|
||||
}
|
||||
|
||||
// Return a copy to prevent race conditions on the returned map
|
||||
result := make(map[string]string, len(hash))
|
||||
for k, v := range hash {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) HIncrBy(key, field string, incr int64) (int64, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var hash map[string]string
|
||||
rawHash, exists := s.data[key]
|
||||
if !exists {
|
||||
hash = make(map[string]string)
|
||||
s.data[key] = hash
|
||||
} else {
|
||||
var ok bool
|
||||
hash, ok = rawHash.(map[string]string)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
|
||||
}
|
||||
}
|
||||
|
||||
currentVal, _ := strconv.ParseInt(hash[field], 10, 64)
|
||||
newVal := currentVal + incr
|
||||
hash[field] = strconv.FormatInt(newVal, 10)
|
||||
|
||||
return newVal, nil
|
||||
}
|
||||
|
||||
// --- LIST operations ---
|
||||
|
||||
func (s *MemoryStore) LPush(key string, values ...any) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var list []string
|
||||
rawList, exists := s.data[key]
|
||||
if !exists {
|
||||
list = make([]string, 0)
|
||||
} else {
|
||||
var ok bool
|
||||
list, ok = rawList.([]string)
|
||||
if !ok {
|
||||
return fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
|
||||
}
|
||||
}
|
||||
|
||||
strValues := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
strValues[i] = fmt.Sprint(v)
|
||||
}
|
||||
|
||||
s.data[key] = append(strValues, list...) // Prepend
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) LRem(key string, count int64, value any) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
rawList, exists := s.data[key]
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
list, ok := rawList.([]string)
|
||||
if !ok {
|
||||
return fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
|
||||
}
|
||||
|
||||
strValue := fmt.Sprint(value)
|
||||
newList := make([]string, 0, len(list))
|
||||
|
||||
// LREM with count = 0: Remove all elements equal to value.
|
||||
if count != 0 {
|
||||
// For now, only implement count = 0 behavior as it's what we need.
|
||||
return fmt.Errorf("LRem with non-zero count is not implemented in MemoryStore")
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
if item != strValue {
|
||||
newList = append(newList, item)
|
||||
}
|
||||
}
|
||||
s.data[key] = newList
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MemoryStore) Rotate(key string) (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
rawList, exists := s.data[key]
|
||||
if !exists {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
list, ok := rawList.([]string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("type mismatch: key '%s' holds a different data type", key)
|
||||
}
|
||||
|
||||
if len(list) == 0 {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
// "RPOP"
|
||||
lastIndex := len(list) - 1
|
||||
item := list[lastIndex]
|
||||
|
||||
// "LPUSH"
|
||||
newList := append([]string{item}, list[:lastIndex]...)
|
||||
s.data[key] = newList
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
@@ -49,7 +49,71 @@ func (s *RedisStore) Exists(key string) (bool, error) {
|
||||
return val > 0, nil
|
||||
}
|
||||
|
||||
// SetNX sets a key-value pair in Redis if the key does not already exist.
|
||||
func (s *RedisStore) SetNX(key string, value []byte, ttl time.Duration) (bool, error) {
|
||||
return s.client.SetNX(context.Background(), key, value, ttl).Result()
|
||||
}
|
||||
|
||||
// Close closes the Redis client connection.
|
||||
func (s *RedisStore) Close() error {
|
||||
return s.client.Close()
|
||||
}
|
||||
|
||||
// --- HASH operations ---
|
||||
|
||||
func (s *RedisStore) HSet(key, field string, value any) error {
|
||||
return s.client.HSet(context.Background(), key, field, value).Err()
|
||||
}
|
||||
|
||||
func (s *RedisStore) HGetAll(key string) (map[string]string, error) {
|
||||
return s.client.HGetAll(context.Background(), key).Result()
|
||||
}
|
||||
|
||||
func (s *RedisStore) HIncrBy(key, field string, incr int64) (int64, error) {
|
||||
return s.client.HIncrBy(context.Background(), key, field, incr).Result()
|
||||
}
|
||||
|
||||
// --- LIST operations ---
|
||||
|
||||
func (s *RedisStore) LPush(key string, values ...any) error {
|
||||
return s.client.LPush(context.Background(), key, values...).Err()
|
||||
}
|
||||
|
||||
func (s *RedisStore) LRem(key string, count int64, value any) error {
|
||||
return s.client.LRem(context.Background(), key, count, value).Err()
|
||||
}
|
||||
|
||||
func (s *RedisStore) Rotate(key string) (string, error) {
|
||||
val, err := s.client.RPopLPush(context.Background(), key, key).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return "", ErrNotFound
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// --- Pipeliner implementation ---
|
||||
|
||||
type redisPipeliner struct {
|
||||
pipe redis.Pipeliner
|
||||
}
|
||||
|
||||
// HSet adds an HSET command to the pipeline.
|
||||
func (p *redisPipeliner) HSet(key string, values map[string]any) {
|
||||
p.pipe.HSet(context.Background(), key, values)
|
||||
}
|
||||
|
||||
// Exec executes all commands in the pipeline.
|
||||
func (p *redisPipeliner) Exec() error {
|
||||
_, err := p.pipe.Exec(context.Background())
|
||||
return err
|
||||
}
|
||||
|
||||
// Pipeline creates a new pipeline.
|
||||
func (s *RedisStore) Pipeline() Pipeliner {
|
||||
return &redisPipeliner{
|
||||
pipe: s.client.Pipeline(),
|
||||
}
|
||||
}
|
||||
|
@@ -28,6 +28,31 @@ type Store interface {
|
||||
// Exists checks if a key exists in the store.
|
||||
Exists(key string) (bool, error)
|
||||
|
||||
// SetNX sets a key-value pair if the key does not already exist.
|
||||
// It returns true if the key was set, false otherwise.
|
||||
SetNX(key string, value []byte, ttl time.Duration) (bool, error)
|
||||
|
||||
// HASH operations
|
||||
HSet(key, field string, value any) error
|
||||
HGetAll(key string) (map[string]string, error)
|
||||
HIncrBy(key, field string, incr int64) (int64, error)
|
||||
|
||||
// LIST operations
|
||||
LPush(key string, values ...any) error
|
||||
LRem(key string, count int64, value any) error
|
||||
Rotate(key string) (string, error)
|
||||
|
||||
// Close closes the store and releases any underlying resources.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Pipeliner defines an interface for executing a batch of commands.
|
||||
type Pipeliner interface {
|
||||
HSet(key string, values map[string]any)
|
||||
Exec() error
|
||||
}
|
||||
|
||||
// RedisPipeliner is an optional interface that a Store can implement to provide pipelining.
|
||||
type RedisPipeliner interface {
|
||||
Pipeline() Pipeliner
|
||||
}
|
||||
|
Reference in New Issue
Block a user