first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,432 @@
package kvstore
import (
"errors"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/bytedance/sonic"
)
var (
ErrClosed = errors.New("kvstore is closed")
ErrEmptyKey = errors.New("key cannot be empty")
ErrNotFound = errors.New("key not found")
ErrInvalidTTL = errors.New("ttl cannot be negative")
)
const (
defaultCleanupInterval = 30 * time.Second
noExpirationUnixNanos = int64(0)
)
// Config controls in-memory KV store behavior.
type Config struct {
// CleanupInterval controls how often expired entries are removed.
// If <= 0, defaults to 30s.
CleanupInterval time.Duration
// DefaultTTL applies when Set is used.
// A zero value means entries do not expire by default.
DefaultTTL time.Duration
}
type entry struct {
value any
writtenAt int64 // unix nanos, 0 means not written yet
expiresAt int64 // unix nanos, 0 means no expiration
}
// Store is an in-memory KV store with optional TTL support.
type Store struct {
mu sync.RWMutex
data map[string]entry
defaultTTL time.Duration
cleanupInterval time.Duration
closed atomic.Bool
stopCh chan struct{}
stopOnce sync.Once
cleanupWg sync.WaitGroup
delegate SyncDelegate
decoders map[string]TypeDecoder
decoderMu sync.RWMutex
}
// SyncDelegate is notified of all mutations, enabling cross-node replication.
// All calls happen synchronously after the local mutation has succeeded.
// writtenAt / deletedAt are absolute Unix nanosecond timestamps used by remote
// nodes for last-write-wins conflict resolution.
// expiresAt is an absolute Unix nanosecond timestamp; 0 means no expiration.
type SyncDelegate interface {
OnSet(key string, valueJSON []byte, writtenAt int64, expiresAt int64)
OnDelete(key string, deletedAt int64)
}
// TypeDecoder reconstructs a concrete value from its JSON representation.
// Register decoders by key prefix via RegisterDecoder.
type TypeDecoder func(data []byte) (any, error)
// SetDelegate plugs in the cluster sync implementation.
func (s *Store) SetDelegate(d SyncDelegate) {
s.delegate = d
}
// RegisterDecoder registers a decoder for keys matching the given prefix.
// Used by the receiving side to reconstruct concrete types from gossip payloads.
func (s *Store) RegisterDecoder(keyPrefix string, decoder TypeDecoder) {
s.decoderMu.Lock()
s.decoders[keyPrefix] = decoder
s.decoderMu.Unlock()
}
// New creates a new in-memory KV store.
func New(cfg Config) (*Store, error) {
if cfg.DefaultTTL < 0 {
return nil, ErrInvalidTTL
}
cleanupInterval := cfg.CleanupInterval
if cleanupInterval <= 0 {
cleanupInterval = defaultCleanupInterval
}
s := &Store{
data: make(map[string]entry),
defaultTTL: cfg.DefaultTTL,
cleanupInterval: cleanupInterval,
stopCh: make(chan struct{}),
decoders: make(map[string]TypeDecoder),
}
s.cleanupWg.Add(1)
go s.cleanupLoop()
return s, nil
}
// Set stores a value using the store's default TTL.
func (s *Store) Set(key string, value any) error {
return s.SetWithTTL(key, value, s.defaultTTL)
}
// SetWithTTL stores a value with an explicit TTL.
// ttl=0 means no expiration.
func (s *Store) SetWithTTL(key string, value any, ttl time.Duration) error {
if err := s.validateMutable(key, ttl); err != nil {
return err
}
now := time.Now().UnixNano()
var expiresAt int64
if ttl > 0 {
expiresAt = now + int64(ttl)
}
var valueJSON []byte
var err error
if s.delegate != nil {
valueJSON, err = sonic.Marshal(value)
if err != nil {
return err
}
}
s.mu.Lock()
s.data[key] = entry{
value: value,
writtenAt: now,
expiresAt: expiresAt,
}
s.mu.Unlock()
if s.delegate != nil {
s.delegate.OnSet(key, valueJSON, now, expiresAt)
}
return nil
}
// SetNXWithTTL atomically sets a value with TTL only if the key does not exist.
// Returns true if the key was set, false if the key already existed.
// ttl=0 means no expiration.
func (s *Store) SetNXWithTTL(key string, value any, ttl time.Duration) (bool, error) {
if err := s.validateMutable(key, ttl); err != nil {
return false, err
}
now := time.Now().UnixNano()
var expiresAt int64
if ttl > 0 {
expiresAt = now + int64(ttl)
}
var valueJSON []byte
var err error
if s.delegate != nil {
valueJSON, err = sonic.Marshal(value)
if err != nil {
return false, err
}
}
s.mu.Lock()
// Check if key exists and is not expired
if existing, ok := s.data[key]; ok {
if !isExpired(existing, now) {
s.mu.Unlock()
return false, nil // Key already exists
}
// Key exists but is expired, allow overwrite
}
// Key doesn't exist or is expired, set it
s.data[key] = entry{
value: value,
writtenAt: now,
expiresAt: expiresAt,
}
s.mu.Unlock()
if s.delegate != nil {
s.delegate.OnSet(key, valueJSON, now, expiresAt)
}
return true, nil
}
// SetRemote applies a remotely-gossiped entry without triggering OnSet.
// writtenAt and expiresAt must be absolute Unix nanosecond timestamps.
// If the local entry was written more recently than writtenAt the update is
// silently skipped (last-write-wins by wall clock on the writing node).
func (s *Store) SetRemote(key string, valueJSON []byte, writtenAt int64, expiresAt int64) error {
if key == "" {
return ErrEmptyKey
}
if s.closed.Load() {
return ErrClosed
}
value := s.decodeValue(key, valueJSON)
s.mu.Lock()
if existing, ok := s.data[key]; ok && existing.writtenAt > writtenAt {
s.mu.Unlock()
return nil // stale gossip — local entry is newer
}
s.data[key] = entry{value: value, writtenAt: writtenAt, expiresAt: expiresAt}
s.mu.Unlock()
return nil
}
// Get retrieves a value by key.
func (s *Store) Get(key string) (any, error) {
if key == "" {
return nil, ErrEmptyKey
}
if s.closed.Load() {
return nil, ErrClosed
}
now := time.Now().UnixNano()
s.mu.RLock()
e, ok := s.data[key]
s.mu.RUnlock()
if !ok {
return nil, ErrNotFound
}
if isExpired(e, now) {
s.mu.Lock()
if latest, exists := s.data[key]; exists && isExpired(latest, time.Now().UnixNano()) {
delete(s.data, key)
}
s.mu.Unlock()
return nil, ErrNotFound
}
return e.value, nil
}
// GetAndDelete retrieves and deletes a key atomically.
func (s *Store) GetAndDelete(key string) (any, error) {
if key == "" {
return nil, ErrEmptyKey
}
if s.closed.Load() {
return nil, ErrClosed
}
now := time.Now().UnixNano()
s.mu.Lock()
e, ok := s.data[key]
if ok {
delete(s.data, key)
}
s.mu.Unlock()
if !ok || isExpired(e, now) {
return nil, ErrNotFound
}
if s.delegate != nil {
s.delegate.OnDelete(key, now)
}
return e.value, nil
}
// Delete removes a key.
func (s *Store) Delete(key string) (bool, error) {
if key == "" {
return false, ErrEmptyKey
}
if s.closed.Load() {
return false, ErrClosed
}
deletedAt := time.Now().UnixNano()
s.mu.Lock()
_, ok := s.data[key]
if ok {
delete(s.data, key)
}
s.mu.Unlock()
if !ok {
return false, nil
}
if s.delegate != nil {
s.delegate.OnDelete(key, deletedAt)
}
return true, nil
}
// DeleteRemote applies a remotely-gossiped delete without triggering OnDelete.
// deletedAt is the absolute Unix nanosecond timestamp when the delete was issued.
// The delete is skipped if the local entry was written after the delete intent
// (last-write-wins).
func (s *Store) DeleteRemote(key string, deletedAt int64) error {
if key == "" {
return ErrEmptyKey
}
if s.closed.Load() {
return ErrClosed
}
s.mu.Lock()
if existing, ok := s.data[key]; ok && existing.writtenAt > deletedAt {
s.mu.Unlock()
return nil // entry was written after the delete intent — write wins
}
delete(s.data, key)
s.mu.Unlock()
return nil
}
// Len returns the number of currently non-expired keys.
func (s *Store) Len() int {
if s.closed.Load() {
return 0
}
now := time.Now().UnixNano()
total := 0
s.mu.RLock()
for _, v := range s.data {
if isExpired(v, now) {
continue
}
total++
}
s.mu.RUnlock()
return total
}
// Close stops background cleanup and prevents further operations.
func (s *Store) Close() error {
s.stopOnce.Do(func() {
s.closed.Store(true)
close(s.stopCh)
})
s.cleanupWg.Wait()
return nil
}
func (s *Store) cleanupLoop() {
defer s.cleanupWg.Done()
ticker := time.NewTicker(s.cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.cleanupExpired()
case <-s.stopCh:
return
}
}
}
func (s *Store) cleanupExpired() {
now := time.Now().UnixNano()
s.mu.Lock()
for k, v := range s.data {
if isExpired(v, now) {
delete(s.data, k)
}
}
s.mu.Unlock()
}
func (s *Store) validateMutable(key string, ttl time.Duration) error {
if key == "" {
return ErrEmptyKey
}
if ttl < 0 {
return ErrInvalidTTL
}
if s.closed.Load() {
return ErrClosed
}
return nil
}
// decodeValue uses the registered decoder for the key's prefix, falling back
// to raw []byte if no decoder matches.
func (s *Store) decodeValue(key string, valueJSON []byte) any {
s.decoderMu.RLock()
var bestPrefix string
var bestDecode TypeDecoder
for prefix, decode := range s.decoders {
if strings.HasPrefix(key, prefix) && len(prefix) > len(bestPrefix) {
bestPrefix = prefix
bestDecode = decode
}
}
s.decoderMu.RUnlock()
if bestDecode != nil {
if v, err := bestDecode(valueJSON); err == nil {
return v
}
}
return valueJSON
}
func isExpired(e entry, nowUnixNano int64) bool {
return e.expiresAt != noExpirationUnixNanos && nowUnixNano >= e.expiresAt
}