From 0a03940b38ce8deb0160e7f23e864c7e12f87075 Mon Sep 17 00:00:00 2001 From: found-cake Date: Thu, 31 Jul 2025 06:10:55 +0000 Subject: [PATCH 01/20] feat: separate persistent and temporary cache storage with individual locks --- store/batch_operations.go | 18 +++---- store/batch_operations_test.go | 2 +- store/cache_gc_test.go | 6 +-- store/cache_store.go | 6 +-- store/store_core.go | 96 +++++++++++++++++----------------- store/transaction.go | 2 + store/type_boolean.go | 4 +- store/type_json.go | 4 +- store/type_raw.go | 8 +-- store/type_string.go | 4 +- store/type_time.go | 4 +- store/type_uinteger.go | 4 +- store/utils_number.go | 18 +++---- 13 files changed, 90 insertions(+), 86 deletions(-) create mode 100644 store/transaction.go diff --git a/store/batch_operations.go b/store/batch_operations.go index 595d12d..ef8c068 100644 --- a/store/batch_operations.go +++ b/store/batch_operations.go @@ -39,8 +39,8 @@ func (s *CacheStore) MGet(keys ...string) []BatchResult { results := make([]BatchResult, len(keys)) now := time.Now().UnixMilli() - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() for i, key := range keys { results[i].Key = key @@ -48,7 +48,7 @@ func (s *CacheStore) MGet(keys ...string) []BatchResult { results[i].Error = errors.ErrKeyEmpty continue } - if e, ok := s.memorydb[key]; ok { + if e, ok := s.memorydbTemporary[key]; ok { if !e.IsExpiredWithUnixMilli(now) { cData := make([]byte, len(e.Data)) copy(cData, e.Data) @@ -72,8 +72,8 @@ func (s *CacheStore) MSet(items ...BatchItem) []error { errs := make([]error, len(items)) - s.mux.Lock() - defer s.mux.Unlock() + s.temporaryMux.Lock() + defer s.temporaryMux.Unlock() if s.dirty != nil { s.dirty.mux.Lock() defer s.dirty.mux.Unlock() @@ -88,7 +88,7 @@ func (s *CacheStore) MSet(items ...BatchItem) []error { errs[i] = errors.ErrValueNil continue } - s.memorydb[item.Key] = *item.Entry + s.memorydbTemporary[item.Key] = *item.Entry if s.dirty != nil { s.dirty.unsafeSet(item.Key) } @@ -104,8 +104,8 @@ func (s *CacheStore) MDelete(keys ...string) []error { errs := make([]error, len(keys)) - s.mux.Lock() - defer s.mux.Unlock() + s.temporaryMux.Lock() + defer s.temporaryMux.Unlock() if s.dirty != nil { s.dirty.mux.Lock() defer s.dirty.mux.Unlock() @@ -117,7 +117,7 @@ func (s *CacheStore) MDelete(keys ...string) []error { continue } - delete(s.memorydb, key) + delete(s.memorydbTemporary, key) if s.dirty != nil { s.dirty.unsafeDelete(key) } diff --git a/store/batch_operations_test.go b/store/batch_operations_test.go index 850d1c7..fa46bac 100644 --- a/store/batch_operations_test.go +++ b/store/batch_operations_test.go @@ -225,7 +225,7 @@ func TestCacheStore_MSet(t *testing.T) { if errs[i] == nil { dataType, value, err := store.Get(item.Key) if err != nil { - t.Errorf("Get() after MSet() error = %v %v", err, store.memorydb) + t.Errorf("Get() after MSet() error = %v %v", err, store.memorydbTemporary) continue } if dataType != item.Entry.Type { diff --git a/store/cache_gc_test.go b/store/cache_gc_test.go index e873684..2b35f7d 100644 --- a/store/cache_gc_test.go +++ b/store/cache_gc_test.go @@ -33,13 +33,13 @@ func TestCleanExpired(t *testing.T) { t.Errorf("Exists() = %v, want 0 for expired key", count) } - _, ok := store.memorydb[key] + _, ok := store.memorydbTemporary[key] if !ok { t.Error("Want it to exist because haven't called cleanExpired yet.") } store.cleanExpired() - _, ok = store.memorydb[key] + _, ok = store.memorydbTemporary[key] if ok { t.Error("should not exist because cleanExpired was called.") } @@ -74,7 +74,7 @@ func TestGarbageCollector(t *testing.T) { case <-timeout: t.Fatal("timeout: key still exists after expected GC interval") case <-tick: - if _, ok := store.memorydb[key]; !ok { + if _, ok := store.memorydbTemporary[key]; !ok { return } } diff --git a/store/cache_store.go b/store/cache_store.go index 1bb37ac..848bbfa 100644 --- a/store/cache_store.go +++ b/store/cache_store.go @@ -11,8 +11,8 @@ import ( func NewCacheStore(cfg config.Config) (*CacheStore, error) { store := &CacheStore{ - memorydb: make(map[string]entry.Entry), - done: make(chan struct{}), + memorydbTemporary: make(map[string]entry.Entry), + done: make(chan struct{}), } if cfg.DBSave { sqlitedb, err := sqlite.NewSqliteStore(cfg.DBFileName) @@ -23,7 +23,7 @@ func NewCacheStore(cfg config.Config) (*CacheStore, error) { if err != nil { return nil, err } - store.memorydb = data + store.memorydbTemporary = data store.sqlitedb = sqlitedb if cfg.SaveDirtyData { if cfg.DirtyThresholdCount <= 0 { diff --git a/store/store_core.go b/store/store_core.go index 16a17ff..0ad781b 100644 --- a/store/store_core.go +++ b/store/store_core.go @@ -13,13 +13,15 @@ import ( ) type CacheStore struct { - mux sync.RWMutex - memorydb map[string]entry.Entry - dirty *dirtyManager - sqlitedb *sqlite.SqliteStore - done chan struct{} - wg sync.WaitGroup - closed atomic.Bool + persistentMux sync.RWMutex + temporaryMux sync.RWMutex + memorydbPersistent map[string]entry.Entry + memorydbTemporary map[string]entry.Entry + dirty *dirtyManager + sqlitedb *sqlite.SqliteStore + done chan struct{} + wg sync.WaitGroup + closed atomic.Bool } const ( @@ -30,18 +32,18 @@ const ( func (s *CacheStore) cleanExpired() { now := time.Now().UnixMilli() - s.mux.Lock() - defer s.mux.Unlock() + s.temporaryMux.Lock() + defer s.temporaryMux.Unlock() - for key, entry := range s.memorydb { + for key, entry := range s.memorydbTemporary { if entry.IsExpiredWithUnixMilli(now) { - delete(s.memorydb, key) + delete(s.memorydbTemporary, key) } } } func (s *CacheStore) unsafeGet(key string) (entry.Entry, error) { - v, ok := s.memorydb[key] + v, ok := s.memorydbTemporary[key] if !ok { return v, errors.ErrNoDataForKey(key) } @@ -55,8 +57,8 @@ func (s *CacheStore) Get(key string) (types.DataType, []byte, error) { if key == "" { return types.UNKNOWN, nil, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() v, err := s.unsafeGet(key) if err != nil { return types.UNKNOWN, nil, err @@ -81,8 +83,8 @@ func (s *CacheStore) GetNoCopy(key string) (types.DataType, []byte, error) { if key == "" { return types.UNKNOWN, nil, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() v, err := s.unsafeGet(key) if err != nil { return types.UNKNOWN, nil, err @@ -92,7 +94,7 @@ func (s *CacheStore) GetNoCopy(key string) (types.DataType, []byte, error) { } func (s *CacheStore) unsafeSet(key string, dataType types.DataType, value []byte, expiry time.Duration) { - s.memorydb[key] = entry.NewEntry(dataType, value, expiry) + s.memorydbTemporary[key] = entry.NewEntry(dataType, value, expiry) if s.dirty != nil { s.dirty.set(key) @@ -107,9 +109,9 @@ func (s *CacheStore) Set(key string, dataType types.DataType, value []byte, expi return errors.ErrValueNil } - s.mux.Lock() - s.memorydb[key] = entry.NewEntry(dataType, value, expiry) - s.mux.Unlock() + s.temporaryMux.Lock() + s.memorydbTemporary[key] = entry.NewEntry(dataType, value, expiry) + s.temporaryMux.Unlock() if s.dirty != nil { s.dirty.set(key) @@ -123,9 +125,9 @@ func (s *CacheStore) Delete(key string) error { return errors.ErrKeyEmpty } - s.mux.Lock() - delete(s.memorydb, key) - s.mux.Unlock() + s.temporaryMux.Lock() + delete(s.memorydbTemporary, key) + s.temporaryMux.Unlock() if s.dirty != nil { s.dirty.delete(key) @@ -135,9 +137,9 @@ func (s *CacheStore) Delete(key string) error { } func (s *CacheStore) Flush() { - s.mux.Lock() - s.memorydb = make(map[string]entry.Entry) - s.mux.Unlock() + s.temporaryMux.Lock() + s.memorydbTemporary = make(map[string]entry.Entry) + s.temporaryMux.Unlock() if s.dirty != nil { s.dirty.wantFullSync() } @@ -164,10 +166,10 @@ func (s *CacheStore) Close() error { log.Println(err) } }() - err = s.sqlitedb.Save(s.memorydb, true) + err = s.sqlitedb.Save(s.memorydbTemporary, true) } - s.memorydb = nil + s.memorydbTemporary = nil s.dirty = nil return err @@ -177,11 +179,11 @@ func (s *CacheStore) Exists(keys ...string) int { now := time.Now().UnixMilli() count := 0 - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() for _, key := range keys { - if e, ok := s.memorydb[key]; ok { + if e, ok := s.memorydbTemporary[key]; ok { if !e.IsExpiredWithUnixMilli(now) { count++ } @@ -192,11 +194,11 @@ func (s *CacheStore) Exists(keys ...string) int { func (s *CacheStore) Keys() []string { now := time.Now().UnixMilli() - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() - keys := make([]string, 0, len(s.memorydb)) - for key, e := range s.memorydb { + keys := make([]string, 0, len(s.memorydbTemporary)) + for key, e := range s.memorydbTemporary { if !e.IsExpiredWithUnixMilli(now) { keys = append(keys, key) } @@ -205,10 +207,10 @@ func (s *CacheStore) Keys() []string { } func (s *CacheStore) TTL(key string) time.Duration { - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() - e, ok := s.memorydb[key] + e, ok := s.memorydbTemporary[key] if !ok { return TTLExpired } @@ -249,9 +251,9 @@ func (s *CacheStore) Sync() { return } - s.mux.RLock() - if dirtySize > s.dirty.ThresholdCount && dirtySize > int(float64(len(s.memorydb))*s.dirty.ThresholdRatio) { - s.mux.RUnlock() + s.temporaryMux.RLock() + if dirtySize > s.dirty.ThresholdCount && dirtySize > int(float64(len(s.memorydbTemporary))*s.dirty.ThresholdRatio) { + s.temporaryMux.RUnlock() s.dirty.mux.Unlock() s.FullSync() return @@ -260,7 +262,7 @@ func (s *CacheStore) Sync() { set_keys, delete_keys := s.dirty.keys() new_data := make(map[string]entry.Entry, len(set_keys)) for _, key := range set_keys { - if e, ok := s.memorydb[key]; ok { + if e, ok := s.memorydbTemporary[key]; ok { dataCopy := make([]byte, len(e.Data)) copy(dataCopy, e.Data) @@ -272,7 +274,7 @@ func (s *CacheStore) Sync() { } } - s.mux.RUnlock() + s.temporaryMux.RUnlock() s.dirty.unsafeClear() s.dirty.mux.Unlock() @@ -290,9 +292,9 @@ func (s *CacheStore) FullSync() { return } - s.mux.RLock() - snapshot := make(map[string]entry.Entry, len(s.memorydb)) - for key, e := range s.memorydb { + s.temporaryMux.RLock() + snapshot := make(map[string]entry.Entry, len(s.memorydbTemporary)) + for key, e := range s.memorydbTemporary { dataCopy := make([]byte, len(e.Data)) copy(dataCopy, e.Data) @@ -302,7 +304,7 @@ func (s *CacheStore) FullSync() { Expiry: e.Expiry, } } - s.mux.RUnlock() + s.temporaryMux.RUnlock() if s.dirty != nil { s.dirty.clear() } diff --git a/store/transaction.go b/store/transaction.go new file mode 100644 index 0000000..3ed177b --- /dev/null +++ b/store/transaction.go @@ -0,0 +1,2 @@ +package store + diff --git a/store/type_boolean.go b/store/type_boolean.go index efc7174..32893ff 100644 --- a/store/type_boolean.go +++ b/store/type_boolean.go @@ -11,8 +11,8 @@ func (s *CacheStore) GetBool(key string) (bool, error) { if key == "" { return false, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return false, err diff --git a/store/type_json.go b/store/type_json.go index 548f1a2..80177ce 100644 --- a/store/type_json.go +++ b/store/type_json.go @@ -12,8 +12,8 @@ func (s *CacheStore) GetJSON(key string, target interface{}) error { if key == "" { return errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return err diff --git a/store/type_raw.go b/store/type_raw.go index f6184c8..d154d73 100644 --- a/store/type_raw.go +++ b/store/type_raw.go @@ -11,8 +11,8 @@ func (s *CacheStore) GetRaw(key string) ([]byte, error) { if key == "" { return nil, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return nil, err @@ -31,8 +31,8 @@ func (s *CacheStore) GetRawNoCopy(key string) ([]byte, error) { if key == "" { return nil, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return nil, err diff --git a/store/type_string.go b/store/type_string.go index 768157c..6eb3817 100644 --- a/store/type_string.go +++ b/store/type_string.go @@ -11,8 +11,8 @@ func (s *CacheStore) GetString(key string) (string, error) { if key == "" { return "", errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return "", err diff --git a/store/type_time.go b/store/type_time.go index 7bd157f..8f26c1a 100644 --- a/store/type_time.go +++ b/store/type_time.go @@ -12,8 +12,8 @@ func (s *CacheStore) GetTime(key string) (time.Time, error) { if key == "" { return t, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return t, err diff --git a/store/type_uinteger.go b/store/type_uinteger.go index 396a245..910bb96 100644 --- a/store/type_uinteger.go +++ b/store/type_uinteger.go @@ -97,8 +97,8 @@ func decrUnsigned[T generic.Unsigned]( if key == "" { return errors.ErrKeyEmpty } - s.mux.Lock() - defer s.mux.Unlock() + s.temporaryMux.Lock() + defer s.temporaryMux.Unlock() e, err := s.unsafeGet(key) if err != nil { return errors.ErrNoDataForKey(key) diff --git a/store/utils_number.go b/store/utils_number.go index 6bb1b17..247f32d 100644 --- a/store/utils_number.go +++ b/store/utils_number.go @@ -11,7 +11,7 @@ import ( ) func (s *CacheStore) setKeepExp(key string, dataType types.DataType, value []byte, expiry int64) { - s.memorydb[key] = entry.Entry{ + s.memorydbTemporary[key] = entry.Entry{ Type: dataType, Data: value, Expiry: expiry, @@ -25,8 +25,8 @@ func (s *CacheStore) getNum16(key string, expected types.DataType) (uint16, erro if key == "" { return 0, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return 0, err @@ -41,8 +41,8 @@ func (s *CacheStore) getNum32(key string, expected types.DataType) (uint32, erro if key == "" { return 0, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return 0, err @@ -57,8 +57,8 @@ func (s *CacheStore) getNum64(key string, expected types.DataType) (uint64, erro if key == "" { return 0, errors.ErrKeyEmpty } - s.mux.RLock() - defer s.mux.RUnlock() + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() e, err := s.unsafeGet(key) if err != nil { return 0, err @@ -83,8 +83,8 @@ func incrNumber[T generic.Numberic]( if key == "" { return errors.ErrKeyEmpty } - s.mux.Lock() - defer s.mux.Unlock() + s.temporaryMux.Lock() + defer s.temporaryMux.Unlock() e, err := s.unsafeGet(key) if err != nil { data := toBinary(delta) From b6a7c9a0379bebec15e2d9ac8a00f729263ea35c Mon Sep 17 00:00:00 2001 From: found-cake Date: Thu, 31 Jul 2025 12:30:58 +0000 Subject: [PATCH 02/20] feat: write transaction --- errors/errors.go | 2 + store/write_tx.go | 130 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 store/write_tx.go diff --git a/errors/errors.go b/errors/errors.go index aba7742..c9a50de 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -11,6 +11,8 @@ import ( var ( ErrKeyEmpty = errors.New("key cannot be empty") + ErrIsClosed = errors.New("cache store is closed") + ErrAlreadyCommit = errors.New("transaction already committed") ErrValueNil = errors.New("value cannot be null") ErrDBNotInit = errors.New("database not initialized") ErrFileNameEmpty = errors.New("filename cannot be empty") diff --git a/store/write_tx.go b/store/write_tx.go new file mode 100644 index 0000000..f3d2d58 --- /dev/null +++ b/store/write_tx.go @@ -0,0 +1,130 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +type WriteTransactionFunc func(tx *WriteTransaction) error + +type WriteTransaction struct { + parent *CacheStore + pendingPersistent map[string]*entry.Entry + pendingTemporary map[string]*entry.Entry + committed bool +} + +func (s *CacheStore) WriteTransaction(fn WriteTransactionFunc) error { + if s.IsClosed() { + return errors.ErrIsClosed + } + + tx := &WriteTransaction{ + parent: s, + pendingPersistent: make(map[string]*entry.Entry), + pendingTemporary: make(map[string]*entry.Entry), + } + + if err := fn(tx); err != nil { + return err + } + + return tx.commit() +} + +func (tx *WriteTransaction) commit() error { + if tx.committed { + return errors.ErrAlreadyCommit + } + + var delete_keys map[string]struct{} + + tx.parent.persistentMux.Lock() + tx.parent.temporaryMux.Lock() + if tx.parent.dirty != nil { + tx.parent.dirty.mux.Lock() + delete_keys = make(map[string]struct{}, len(tx.pendingPersistent)) + defer tx.parent.dirty.mux.Unlock() + } + for key, entry := range tx.pendingPersistent { + if entry == nil { + delete(tx.parent.memorydbPersistent, key) + if tx.parent.dirty != nil { + delete_keys[key] = struct{}{} + } + } else { + tx.parent.memorydbPersistent[key] = *entry + if tx.parent.dirty != nil { + tx.parent.dirty.unsafeSet(key) + } + } + } + tx.parent.persistentMux.Unlock() + + for key, entry := range tx.pendingTemporary { + if entry == nil { + delete(tx.parent.memorydbTemporary, key) + } else { + tx.parent.memorydbTemporary[key] = *entry + if tx.parent.dirty != nil { + tx.parent.dirty.unsafeSet(key) + delete(delete_keys, key) + } + } + } + tx.parent.temporaryMux.Unlock() + + if tx.parent.dirty != nil { + for key := range delete_keys { + tx.parent.dirty.unsafeDelete(key) + } + } + + tx.committed = true + return nil +} + +func (tx *WriteTransaction) Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error { + if key == "" { + return errors.ErrKeyEmpty + } + if value == nil { + return errors.ErrValueNil + } + + if expiry <= 0 { + tx.pendingPersistent[key] = newEntry(dataType, value, 0) + tx.pendingTemporary[key] = nil + } else { + tx.pendingTemporary[key] = newEntry(dataType, value, expiry) + tx.pendingPersistent[key] = nil + } + + return nil +} + +func (tx *WriteTransaction) Delete(key string) error { + if key == "" { + return errors.ErrKeyEmpty + } + + tx.pendingPersistent[key] = nil + tx.pendingTemporary[key] = nil + + return nil +} + +func newEntry(dataType types.DataType, data []byte, exp time.Duration) *entry.Entry { + var expiry int64 + if exp > 0 { + expiry = time.Now().Add(exp).UnixMilli() + } + return &entry.Entry{ + Type: dataType, + Data: data, + Expiry: expiry, + } +} From f3274c85b5fdf7c441a655617216c024f63da0fb Mon Sep 17 00:00:00 2001 From: found-cake Date: Thu, 31 Jul 2025 15:19:58 +0000 Subject: [PATCH 03/20] feat: read transaction --- errors/errors.go | 1 + store/read_tx.go | 29 +++++++++ store/read_tx_lock.go | 114 ++++++++++++++++++++++++++++++++++ store/read_tx_snapshot.go | 125 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 269 insertions(+) create mode 100644 store/read_tx.go create mode 100644 store/read_tx_lock.go create mode 100644 store/read_tx_snapshot.go diff --git a/errors/errors.go b/errors/errors.go index c9a50de..97400fc 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -13,6 +13,7 @@ var ( ErrKeyEmpty = errors.New("key cannot be empty") ErrIsClosed = errors.New("cache store is closed") ErrAlreadyCommit = errors.New("transaction already committed") + ErrNotLocked = errors.New("read transaction not locked") ErrValueNil = errors.New("value cannot be null") ErrDBNotInit = errors.New("database not initialized") ErrFileNameEmpty = errors.New("filename cannot be empty") diff --git a/store/read_tx.go b/store/read_tx.go new file mode 100644 index 0000000..fd9b2be --- /dev/null +++ b/store/read_tx.go @@ -0,0 +1,29 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +type ReadTransactionFunc func(tx ReadTransaction) error + +type ReadTransaction interface { + Get(key string) (types.DataType, []byte, error) + GetNoCopy(key string) (types.DataType, []byte, error) + Exists(keys ...string) int + TTL(key string) time.Duration +} + +func (s *CacheStore) ReadTransaction(useSnapshot bool, fx ReadTransactionFunc) error { + if s.IsClosed() { + return errors.ErrIsClosed + } + + if useSnapshot { + return s.snapshotReadTx(fx) + } else { + return s.lockReadTx(fx) + } +} diff --git a/store/read_tx_lock.go b/store/read_tx_lock.go new file mode 100644 index 0000000..92905a8 --- /dev/null +++ b/store/read_tx_lock.go @@ -0,0 +1,114 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +type LockReadTransaction struct { + parent *CacheStore + locked bool +} + +func (s *CacheStore) lockReadTx(fn ReadTransactionFunc) error { + tx := &LockReadTransaction{ + parent: s, + locked: false, + } + + s.persistentMux.RLock() + s.temporaryMux.RLock() + tx.locked = true + defer func() { + if tx.locked { + s.persistentMux.RUnlock() + s.temporaryMux.RUnlock() + tx.locked = false + } + }() + + return fn(tx) +} + +func (tx *LockReadTransaction) Get(key string) (types.DataType, []byte, error) { + t, data, err := tx.GetNoCopy(key) + if err != nil { + result := make([]byte, len(data)) + copy(result, data) + return t, result, err + } + return t, data, err +} + +// GetNoCopy retrieves a value without copying data (zero-copy read) +// ⚠️ WARNING: Don't modify the returned value! +func (tx *LockReadTransaction) GetNoCopy(key string) (types.DataType, []byte, error) { + if !tx.locked { + return types.UNKNOWN, nil, errors.ErrNotLocked + } + if key == "" { + return types.UNKNOWN, nil, errors.ErrKeyEmpty + } + + entry, ok := tx.parent.memorydbPersistent[key] + if ok { + return entry.Type, entry.Data, nil + } + + entry, ok = tx.parent.memorydbTemporary[key] + if ok { + if entry.IsExpired() { + return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) + } else { + return entry.Type, entry.Data, nil + } + } + + return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) +} + +func (tx *LockReadTransaction) Exists(keys ...string) int { + if !tx.locked || len(keys) == 0 { + return 0 + } + + count := 0 + now := time.Now().UnixMilli() + + for _, key := range keys { + if _, exists := tx.parent.memorydbPersistent[key]; exists { + count++ + } else if entry, exists := tx.parent.memorydbTemporary[key]; exists { + if !entry.IsExpiredWithUnixMilli(now) { + count++ + } + } + } + + return count +} + +func (tx *LockReadTransaction) TTL(key string) time.Duration { + if !tx.locked { + return TTLExpired + } + + _, ok := tx.parent.memorydbPersistent[key] + if ok { + return TTLNoExpiry + } + + entry, ok := tx.parent.memorydbTemporary[key] + if !ok { + return TTLExpired + } + now := time.Now().UnixMilli() + if now >= entry.Expiry { + return TTLExpired + } + + remaining := time.Duration(entry.Expiry-now) * time.Millisecond + return remaining +} diff --git a/store/read_tx_snapshot.go b/store/read_tx_snapshot.go new file mode 100644 index 0000000..ebec49b --- /dev/null +++ b/store/read_tx_snapshot.go @@ -0,0 +1,125 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +type SnapshotReadTransaction struct { + memorydbPersistent map[string]entry.Entry + memorydbTemporary map[string]entry.Entry +} + +func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { + s.persistentMux.RLock() + s.temporaryMux.RLock() + + tx := &SnapshotReadTransaction{ + memorydbPersistent: make(map[string]entry.Entry, len(s.memorydbPersistent)), + memorydbTemporary: make(map[string]entry.Entry, len(s.memorydbTemporary)), + } + + for key, e := range s.memorydbPersistent { + dataCopy := make([]byte, len(e.Data)) + copy(dataCopy, e.Data) + + tx.memorydbPersistent[key] = entry.Entry{ + Type: e.Type, + Data: dataCopy, + Expiry: e.Expiry, + } + } + + for key, e := range s.memorydbTemporary { + dataCopy := make([]byte, len(e.Data)) + copy(dataCopy, e.Data) + + tx.memorydbTemporary[key] = entry.Entry{ + Type: e.Type, + Data: dataCopy, + Expiry: e.Expiry, + } + } + + s.persistentMux.RUnlock() + s.temporaryMux.RUnlock() + + return fn(tx) +} + +func (tx *SnapshotReadTransaction) Get(key string) (types.DataType, []byte, error) { + t, data, err := tx.GetNoCopy(key) + if err != nil { + result := make([]byte, len(data)) + copy(result, data) + return t, result, err + } + return t, data, err +} + +// GetNoCopy retrieves a value without copying data (zero-copy read) +// ⚠️ WARNING: Don't modify the returned value! +func (tx *SnapshotReadTransaction) GetNoCopy(key string) (types.DataType, []byte, error) { + if key == "" { + return types.UNKNOWN, nil, errors.ErrKeyEmpty + } + + entry, ok := tx.memorydbPersistent[key] + if ok { + return entry.Type, entry.Data, nil + } + + entry, ok = tx.memorydbTemporary[key] + if ok { + if entry.IsExpired() { + return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) + } else { + return entry.Type, entry.Data, nil + } + } + + return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) +} + +func (tx *SnapshotReadTransaction) Exists(keys ...string) int { + if len(keys) == 0 { + return 0 + } + + count := 0 + now := time.Now().UnixMilli() + + for _, key := range keys { + if _, exists := tx.memorydbPersistent[key]; exists { + count++ + } else if entry, exists := tx.memorydbTemporary[key]; exists { + if !entry.IsExpiredWithUnixMilli(now) { + count++ + } + } + } + + return count +} + +func (tx *SnapshotReadTransaction) TTL(key string) time.Duration { + _, ok := tx.memorydbPersistent[key] + if ok { + return TTLNoExpiry + } + + entry, ok := tx.memorydbTemporary[key] + if !ok { + return TTLExpired + } + now := time.Now().UnixMilli() + if now >= entry.Expiry { + return TTLExpired + } + + remaining := time.Duration(entry.Expiry-now) * time.Millisecond + return remaining +} From 8732b863cfc84ea14ee14d3dbf7a8ecd543e9d82 Mon Sep 17 00:00:00 2001 From: found-cake Date: Fri, 1 Aug 2025 10:14:58 +0000 Subject: [PATCH 04/20] refactor: change mux position --- store/store_core.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/store/store_core.go b/store/store_core.go index 0ad781b..b3c966c 100644 --- a/store/store_core.go +++ b/store/store_core.go @@ -14,8 +14,8 @@ import ( type CacheStore struct { persistentMux sync.RWMutex - temporaryMux sync.RWMutex memorydbPersistent map[string]entry.Entry + temporaryMux sync.RWMutex memorydbTemporary map[string]entry.Entry dirty *dirtyManager sqlitedb *sqlite.SqliteStore From 76ef3cab6918da74d6fea3692f72471373c70075 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 3 Aug 2025 14:37:37 +0000 Subject: [PATCH 05/20] refactor: merge persistent and temporary storage maps in snapshot transactions --- store/read_tx_snapshot.go | 53 +++++++++++++++++---------------------- store/transaction.go | 2 -- 2 files changed, 23 insertions(+), 32 deletions(-) delete mode 100644 store/transaction.go diff --git a/store/read_tx_snapshot.go b/store/read_tx_snapshot.go index ebec49b..0231afe 100644 --- a/store/read_tx_snapshot.go +++ b/store/read_tx_snapshot.go @@ -9,8 +9,7 @@ import ( ) type SnapshotReadTransaction struct { - memorydbPersistent map[string]entry.Entry - memorydbTemporary map[string]entry.Entry + memorydb map[string]entry.Entry } func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { @@ -18,15 +17,14 @@ func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { s.temporaryMux.RLock() tx := &SnapshotReadTransaction{ - memorydbPersistent: make(map[string]entry.Entry, len(s.memorydbPersistent)), - memorydbTemporary: make(map[string]entry.Entry, len(s.memorydbTemporary)), + memorydb: make(map[string]entry.Entry, len(s.memorydbPersistent)+len(s.memorydbTemporary)), } for key, e := range s.memorydbPersistent { dataCopy := make([]byte, len(e.Data)) copy(dataCopy, e.Data) - tx.memorydbPersistent[key] = entry.Entry{ + tx.memorydb[key] = entry.Entry{ Type: e.Type, Data: dataCopy, Expiry: e.Expiry, @@ -34,10 +32,13 @@ func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { } for key, e := range s.memorydbTemporary { + if e.IsExpired() { + continue + } dataCopy := make([]byte, len(e.Data)) copy(dataCopy, e.Data) - tx.memorydbTemporary[key] = entry.Entry{ + tx.memorydb[key] = entry.Entry{ Type: e.Type, Data: dataCopy, Expiry: e.Expiry, @@ -52,7 +53,7 @@ func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { func (tx *SnapshotReadTransaction) Get(key string) (types.DataType, []byte, error) { t, data, err := tx.GetNoCopy(key) - if err != nil { + if err == nil { result := make([]byte, len(data)) copy(result, data) return t, result, err @@ -67,21 +68,15 @@ func (tx *SnapshotReadTransaction) GetNoCopy(key string) (types.DataType, []byte return types.UNKNOWN, nil, errors.ErrKeyEmpty } - entry, ok := tx.memorydbPersistent[key] - if ok { - return entry.Type, entry.Data, nil + entry, ok := tx.memorydb[key] + if !ok { + return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) } - - entry, ok = tx.memorydbTemporary[key] - if ok { - if entry.IsExpired() { - return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) - } else { - return entry.Type, entry.Data, nil - } + if entry.IsExpired() { + return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) } - return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) + return entry.Type, entry.Data, nil } func (tx *SnapshotReadTransaction) Exists(keys ...string) int { @@ -93,9 +88,7 @@ func (tx *SnapshotReadTransaction) Exists(keys ...string) int { now := time.Now().UnixMilli() for _, key := range keys { - if _, exists := tx.memorydbPersistent[key]; exists { - count++ - } else if entry, exists := tx.memorydbTemporary[key]; exists { + if entry, exists := tx.memorydb[key]; exists { if !entry.IsExpiredWithUnixMilli(now) { count++ } @@ -106,20 +99,20 @@ func (tx *SnapshotReadTransaction) Exists(keys ...string) int { } func (tx *SnapshotReadTransaction) TTL(key string) time.Duration { - _, ok := tx.memorydbPersistent[key] - if ok { - return TTLNoExpiry - } - - entry, ok := tx.memorydbTemporary[key] + e, ok := tx.memorydb[key] if !ok { return TTLExpired } + + if e.Expiry == 0 { + return TTLNoExpiry + } + now := time.Now().UnixMilli() - if now >= entry.Expiry { + if now >= e.Expiry { return TTLExpired } - remaining := time.Duration(entry.Expiry-now) * time.Millisecond + remaining := time.Duration(e.Expiry-now) * time.Millisecond return remaining } diff --git a/store/transaction.go b/store/transaction.go deleted file mode 100644 index 3ed177b..0000000 --- a/store/transaction.go +++ /dev/null @@ -1,2 +0,0 @@ -package store - From a23a44451d2d94d5d2a33d826aae77bd1b9ca876 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 3 Aug 2025 14:44:19 +0000 Subject: [PATCH 06/20] refactor: simplify LockReadTransaction by removing redundant lock state tracking --- store/read_tx_lock.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/store/read_tx_lock.go b/store/read_tx_lock.go index 92905a8..a2d0f2d 100644 --- a/store/read_tx_lock.go +++ b/store/read_tx_lock.go @@ -9,24 +9,18 @@ import ( type LockReadTransaction struct { parent *CacheStore - locked bool } func (s *CacheStore) lockReadTx(fn ReadTransactionFunc) error { tx := &LockReadTransaction{ parent: s, - locked: false, } s.persistentMux.RLock() s.temporaryMux.RLock() - tx.locked = true defer func() { - if tx.locked { - s.persistentMux.RUnlock() - s.temporaryMux.RUnlock() - tx.locked = false - } + s.persistentMux.RUnlock() + s.temporaryMux.RUnlock() }() return fn(tx) @@ -34,7 +28,7 @@ func (s *CacheStore) lockReadTx(fn ReadTransactionFunc) error { func (tx *LockReadTransaction) Get(key string) (types.DataType, []byte, error) { t, data, err := tx.GetNoCopy(key) - if err != nil { + if err == nil { result := make([]byte, len(data)) copy(result, data) return t, result, err @@ -45,9 +39,6 @@ func (tx *LockReadTransaction) Get(key string) (types.DataType, []byte, error) { // GetNoCopy retrieves a value without copying data (zero-copy read) // ⚠️ WARNING: Don't modify the returned value! func (tx *LockReadTransaction) GetNoCopy(key string) (types.DataType, []byte, error) { - if !tx.locked { - return types.UNKNOWN, nil, errors.ErrNotLocked - } if key == "" { return types.UNKNOWN, nil, errors.ErrKeyEmpty } @@ -70,7 +61,7 @@ func (tx *LockReadTransaction) GetNoCopy(key string) (types.DataType, []byte, er } func (tx *LockReadTransaction) Exists(keys ...string) int { - if !tx.locked || len(keys) == 0 { + if len(keys) == 0 { return 0 } @@ -91,10 +82,6 @@ func (tx *LockReadTransaction) Exists(keys ...string) int { } func (tx *LockReadTransaction) TTL(key string) time.Duration { - if !tx.locked { - return TTLExpired - } - _, ok := tx.parent.memorydbPersistent[key] if ok { return TTLNoExpiry From 0f78d814a754c19bed9ba9d3d67a499aab14d21d Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 3 Aug 2025 14:49:32 +0000 Subject: [PATCH 07/20] refactor: use entry.NewEntry in writeTransaction newEntry helper --- store/write_tx.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/store/write_tx.go b/store/write_tx.go index f3d2d58..ef233ab 100644 --- a/store/write_tx.go +++ b/store/write_tx.go @@ -118,13 +118,6 @@ func (tx *WriteTransaction) Delete(key string) error { } func newEntry(dataType types.DataType, data []byte, exp time.Duration) *entry.Entry { - var expiry int64 - if exp > 0 { - expiry = time.Now().Add(exp).UnixMilli() - } - return &entry.Entry{ - Type: dataType, - Data: data, - Expiry: expiry, - } + e := entry.NewEntry(dataType, data, exp) + return &e } From cb0b7a0c1d1c934bfc6af243e3b22b7b87013f2f Mon Sep 17 00:00:00 2001 From: found-cake Date: Tue, 5 Aug 2025 04:47:39 +0000 Subject: [PATCH 08/20] refactor: extract snapshotReadTx logic into a constructor function --- store/read_tx_snapshot.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/store/read_tx_snapshot.go b/store/read_tx_snapshot.go index 0231afe..31c360a 100644 --- a/store/read_tx_snapshot.go +++ b/store/read_tx_snapshot.go @@ -12,7 +12,7 @@ type SnapshotReadTransaction struct { memorydb map[string]entry.Entry } -func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { +func newSnapshotReadTX(s *CacheStore) *SnapshotReadTransaction { s.persistentMux.RLock() s.temporaryMux.RLock() @@ -48,6 +48,11 @@ func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { s.persistentMux.RUnlock() s.temporaryMux.RUnlock() + return tx +} + +func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { + tx := newSnapshotReadTX(s) return fn(tx) } From 7152a66265287bd5adef6a3b9c4b88d81c6a786c Mon Sep 17 00:00:00 2001 From: found-cake Date: Tue, 5 Aug 2025 05:05:24 +0000 Subject: [PATCH 09/20] remove: batch operations --- store/batch_operations.go | 127 ----------- store/batch_operations_test.go | 395 --------------------------------- 2 files changed, 522 deletions(-) delete mode 100644 store/batch_operations.go delete mode 100644 store/batch_operations_test.go diff --git a/store/batch_operations.go b/store/batch_operations.go deleted file mode 100644 index ef8c068..0000000 --- a/store/batch_operations.go +++ /dev/null @@ -1,127 +0,0 @@ -package store - -import ( - "time" - - "github.com/found-cake/CacheStore/entry" - "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" -) - -type BatchItem struct { - Key string - Entry *entry.Entry -} - -func NewItem(key string, dataType types.DataType, data []byte, expiry time.Duration) BatchItem { - if data == nil { - return BatchItem{Key: key} - } - entry := entry.NewEntry(dataType, data, expiry) - return BatchItem{ - Key: key, - Entry: &entry, - } -} - -type BatchResult struct { - Key string - Type types.DataType - Value []byte - Error error -} - -func (s *CacheStore) MGet(keys ...string) []BatchResult { - if len(keys) == 0 { - return nil - } - - results := make([]BatchResult, len(keys)) - now := time.Now().UnixMilli() - - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - - for i, key := range keys { - results[i].Key = key - if key == "" { - results[i].Error = errors.ErrKeyEmpty - continue - } - if e, ok := s.memorydbTemporary[key]; ok { - if !e.IsExpiredWithUnixMilli(now) { - cData := make([]byte, len(e.Data)) - copy(cData, e.Data) - results[i].Type = e.Type - results[i].Value = cData - } else { - results[i].Error = errors.ErrNoDataForKey(key) - } - } else { - results[i].Error = errors.ErrNoDataForKey(key) - } - } - - return results -} - -func (s *CacheStore) MSet(items ...BatchItem) []error { - if len(items) == 0 { - return nil - } - - errs := make([]error, len(items)) - - s.temporaryMux.Lock() - defer s.temporaryMux.Unlock() - if s.dirty != nil { - s.dirty.mux.Lock() - defer s.dirty.mux.Unlock() - } - - for i, item := range items { - if item.Key == "" { - errs[i] = errors.ErrKeyEmpty - continue - } - if item.Entry == nil { - errs[i] = errors.ErrValueNil - continue - } - s.memorydbTemporary[item.Key] = *item.Entry - if s.dirty != nil { - s.dirty.unsafeSet(item.Key) - } - } - - return errs -} - -func (s *CacheStore) MDelete(keys ...string) []error { - if len(keys) == 0 { - return nil - } - - errs := make([]error, len(keys)) - - s.temporaryMux.Lock() - defer s.temporaryMux.Unlock() - if s.dirty != nil { - s.dirty.mux.Lock() - defer s.dirty.mux.Unlock() - } - - for i, key := range keys { - if key == "" { - errs[i] = errors.ErrKeyEmpty - continue - } - - delete(s.memorydbTemporary, key) - if s.dirty != nil { - s.dirty.unsafeDelete(key) - } - } - - return errs -} diff --git a/store/batch_operations_test.go b/store/batch_operations_test.go deleted file mode 100644 index fa46bac..0000000 --- a/store/batch_operations_test.go +++ /dev/null @@ -1,395 +0,0 @@ -package store - -import ( - "testing" - "time" - - "github.com/found-cake/CacheStore/config" - "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" -) - -func TestCacheStore_MGet(t *testing.T) { - store, err := NewCacheStore(config.Config{DBSave: false}) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } - defer store.Close() - - testData := map[string]struct { - dataType types.DataType - value []byte - expiry time.Duration - }{ - "key1": {types.STRING, []byte("value1"), time.Hour}, - "key2": {types.RAW, []byte("value2"), time.Hour}, - "key3": {types.JSON, []byte(`{"test": "value3"}`), time.Hour}, - } - - for key, data := range testData { - err := store.Set(key, data.dataType, data.value, data.expiry) - if err != nil { - t.Fatalf("Set() error = %v", err) - } - } - - tests := []struct { - name string - keys []string - wantLen int - wantErrs []bool - }{ - { - name: "get existing keys", - keys: []string{"key1", "key2", "key3"}, - wantLen: 3, - wantErrs: []bool{false, false, false}, - }, - { - name: "get mix of existing and non-existing keys", - keys: []string{"key1", "nonexistent", "key2"}, - wantLen: 3, - wantErrs: []bool{false, true, false}, - }, - { - name: "get non-existing keys", - keys: []string{"nonexistent1", "nonexistent2"}, - wantLen: 2, - wantErrs: []bool{true, true}, - }, - { - name: "empty key", - keys: []string{""}, - wantLen: 1, - wantErrs: []bool{true}, - }, - { - name: "no keys", - keys: []string{}, - wantLen: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - results := store.MGet(tt.keys...) - - if tt.wantLen == 0 && results == nil { - return - } - - if len(results) != tt.wantLen { - t.Errorf("MGet() returned %d results, want %d", len(results), tt.wantLen) - return - } - - for i, result := range results { - if result.Key != tt.keys[i] { - t.Errorf("MGet() result[%d].Key = %v, want %v", i, result.Key, tt.keys[i]) - } - - hasError := result.Error != nil - wantError := tt.wantErrs[i] - - if hasError != wantError { - t.Errorf("MGet() result[%d] error = %v, wantError %v", i, result.Error, wantError) - } - - if !hasError { - expectedData := testData[tt.keys[i]] - if result.Type != expectedData.dataType { - t.Errorf("MGet() result[%d].Type = %v, want %v", i, result.Type, expectedData.dataType) - } - if string(result.Value) != string(expectedData.value) { - t.Errorf("MGet() result[%d].Value = %v, want %v", i, string(result.Value), string(expectedData.value)) - } - } - } - }) - } -} - -func TestCacheStore_MGet_ExpiredKeys(t *testing.T) { - store, err := NewCacheStore(config.Config{DBSave: false}) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } - defer store.Close() - - err = store.Set("expired_key", types.STRING, []byte("value"), time.Nanosecond) - if err != nil { - t.Fatalf("Set() error = %v", err) - } - - time.Sleep(time.Millisecond) - - results := store.MGet("expired_key") - if len(results) != 1 { - t.Fatalf("MGet() returned %d results, want 1", len(results)) - } - - if results[0].Error == nil { - t.Error("MGet() should return error for expired key") - } -} - -func TestCacheStore_MSet(t *testing.T) { - store, err := NewCacheStore(config.Config{DBSave: false}) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } - defer store.Close() - - tests := []struct { - name string - items []BatchItem - wantErrs []bool - wantCount int - }{ - { - name: "valid items", - items: []BatchItem{ - NewItem("key1", types.STRING, []byte("value1"), time.Hour), - NewItem("key2", types.RAW, []byte("value2"), time.Hour), - NewItem("key3", types.JSON, []byte(`{"test": "value3"}`), time.Hour), - }, - wantErrs: []bool{false, false, false}, - wantCount: 0, - }, - { - name: "mixed valid and invalid items", - items: []BatchItem{ - NewItem("key1", types.STRING, []byte("value1"), time.Hour), - NewItem("", types.STRING, []byte("value2"), time.Hour), - NewItem("key3", types.STRING, nil, time.Hour), - NewItem("key4", types.RAW, []byte("value4"), time.Hour), - }, - wantErrs: []bool{false, true, true, false}, - wantCount: 2, - }, - { - name: "empty key", - items: []BatchItem{ - NewItem("", types.STRING, []byte("value"), time.Hour), - }, - wantErrs: []bool{true}, - wantCount: 1, - }, - { - name: "nil value", - items: []BatchItem{ - NewItem("key", types.STRING, nil, time.Hour), - }, - wantErrs: []bool{true}, - wantCount: 1, - }, - { - name: "no items", - items: []BatchItem{}, - wantCount: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - errs := store.MSet(tt.items...) - - if tt.wantCount == 0 && errs == nil { - return - } - - if len(errs) != len(tt.items) { - t.Errorf("MSet() returned %d errors, want %d", len(errs), len(tt.items)) - return - } - - errorCount := 0 - for i, err := range errs { - hasError := err != nil - wantError := tt.wantErrs[i] - - if hasError != wantError { - t.Errorf("MSet() error[%d] = %v, wantError %v", i, err, wantError) - } - - if hasError { - errorCount++ - } - } - - if errorCount != tt.wantCount { - t.Errorf("MSet() error count = %d, want %d", errorCount, tt.wantCount) - } - - for i, item := range tt.items { - if errs[i] == nil { - dataType, value, err := store.Get(item.Key) - if err != nil { - t.Errorf("Get() after MSet() error = %v %v", err, store.memorydbTemporary) - continue - } - if dataType != item.Entry.Type { - t.Errorf("Get() after MSet() type = %v, want %v", dataType, item.Entry.Type) - } - if string(value) != string(item.Entry.Data) { - t.Errorf("Get() after MSet() value = %v, want %v", string(value), string(item.Entry.Data)) - } - } - } - }) - } -} - -func TestCacheStore_MDelete(t *testing.T) { - store, err := NewCacheStore(config.Config{DBSave: false}) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } - defer store.Close() - - testKeys := []string{"key1", "key2", "key3"} - for _, key := range testKeys { - err := store.Set(key, types.STRING, []byte("value"), time.Hour) - if err != nil { - t.Fatalf("Set() error = %v", err) - } - } - - tests := []struct { - name string - keys []string - wantErrs []bool - }{ - { - name: "delete existing keys", - keys: []string{"key1", "key2"}, - wantErrs: []bool{false, false}, - }, - { - name: "delete mix of existing and non-existing keys", - keys: []string{"key3", "nonexistent"}, - wantErrs: []bool{false, false}, - }, - { - name: "delete empty key", - keys: []string{""}, - wantErrs: []bool{true}, - }, - { - name: "no keys", - keys: []string{}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - errs := store.MDelete(tt.keys...) - - if len(tt.keys) == 0 && errs == nil { - return - } - - if len(errs) != len(tt.keys) { - t.Errorf("MDelete() returned %d errors, want %d", len(errs), len(tt.keys)) - return - } - - for i, err := range errs { - hasError := err != nil - wantError := tt.wantErrs[i] - - if hasError != wantError { - t.Errorf("MDelete() error[%d] = %v, wantError %v", i, err, wantError) - } - - if tt.keys[i] != "" && err == nil { - _, _, getErr := store.Get(tt.keys[i]) - if getErr == nil { - t.Errorf("Get() after MDelete() should return error for deleted key %v", tt.keys[i]) - } - } - } - }) - } -} - -func TestCacheStore_MDelete_EmptyKey(t *testing.T) { - store, err := NewCacheStore(config.Config{DBSave: false}) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } - defer store.Close() - - errs := store.MDelete("") - if len(errs) != 1 { - t.Fatalf("MDelete() returned %d errors, want 1", len(errs)) - } - - if errs[0] != errors.ErrKeyEmpty { - t.Errorf("MDelete() error = %v, want %v", errs[0], errors.ErrKeyEmpty) - } -} - -func TestBatchOperations_Integration(t *testing.T) { - store, err := NewCacheStore(config.Config{DBSave: false}) - if err != nil { - t.Fatalf("Failed to create store: %v", err) - } - defer store.Close() - - items := []BatchItem{ - NewItem("user:1", types.JSON, []byte(`{"name": "Alice", "age": 30}`), time.Hour), - NewItem("user:2", types.JSON, []byte(`{"name": "Bob", "age": 25}`), time.Hour), - NewItem("counter", types.RAW, []byte("100"), time.Hour), - NewItem("flag", types.STRING, []byte("enabled"), time.Hour), - } - - errs := store.MSet(items...) - for i, err := range errs { - if err != nil { - t.Errorf("MSet() error[%d] = %v", i, err) - } - } - - keys := []string{"user:1", "user:2", "counter", "flag", "nonexistent"} - results := store.MGet(keys...) - - if len(results) != 5 { - t.Fatalf("MGet() returned %d results, want 5", len(results)) - } - - for i := 0; i < 4; i++ { - if results[i].Error != nil { - t.Errorf("MGet() result[%d] error = %v", i, results[i].Error) - } - if string(results[i].Value) != string(items[i].Entry.Data) { - t.Errorf("MGet() result[%d] value = %v, want %v", i, string(results[i].Value), string(items[i].Entry.Data)) - } - } - - if results[4].Error == nil { - t.Error("MGet() should return error for nonexistent key") - } - - deleteKeys := []string{"user:1", "counter"} - deleteErrs := store.MDelete(deleteKeys...) - for i, err := range deleteErrs { - if err != nil { - t.Errorf("MDelete() error[%d] = %v", i, err) - } - } - - afterDeleteResults := store.MGet(deleteKeys...) - for i, result := range afterDeleteResults { - if result.Error == nil { - t.Errorf("MGet() after delete should return error for key %v", deleteKeys[i]) - } - } - - remainingResults := store.MGet("user:2", "flag") - for i, result := range remainingResults { - if result.Error != nil { - t.Errorf("MGet() remaining key[%d] error = %v", i, result.Error) - } - } -} From b71275af3bc7e3897467dd78b5228f562d122c3c Mon Sep 17 00:00:00 2001 From: found-cake Date: Tue, 5 Aug 2025 05:27:11 +0000 Subject: [PATCH 10/20] feat: read and write transaction --- store/rw_tx.go | 29 ++++++++++ store/rw_tx_lock.go | 77 ++++++++++++++++++++++++++ store/rw_tx_snapshot.go | 119 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+) create mode 100644 store/rw_tx.go create mode 100644 store/rw_tx_lock.go create mode 100644 store/rw_tx_snapshot.go diff --git a/store/rw_tx.go b/store/rw_tx.go new file mode 100644 index 0000000..a12ed96 --- /dev/null +++ b/store/rw_tx.go @@ -0,0 +1,29 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +type RWTransactionFunc func(tx RWTransaction) error + +type RWTransaction interface { + ReadTransaction + Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error + Delete(key string) error + commit() error +} + +func (s *CacheStore) RWTransaction(useSnapshot bool, fx RWTransactionFunc) error { + if s.IsClosed() { + return errors.ErrIsClosed + } + + if useSnapshot { + return s.snapshotRwTx(fx) + } else { + return s.lockRWTx(fx) + } +} diff --git a/store/rw_tx_lock.go b/store/rw_tx_lock.go new file mode 100644 index 0000000..c0d4b9e --- /dev/null +++ b/store/rw_tx_lock.go @@ -0,0 +1,77 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +type LockRWTransaction struct { + *LockReadTransaction + parent *CacheStore +} + +func (s *CacheStore) lockRWTx(fn RWTransactionFunc) error { + tx := &LockRWTransaction{ + parent: s, + LockReadTransaction: &LockReadTransaction{parent: s}, + } + + s.persistentMux.Lock() + s.temporaryMux.Lock() + if s.dirty != nil { + s.dirty.mux.Lock() + } + defer tx.commit() + + return fn(tx) +} + +func (tx *LockRWTransaction) commit() error { + tx.parent.persistentMux.Unlock() + tx.parent.temporaryMux.Unlock() + if tx.parent.dirty != nil { + tx.parent.dirty.mux.Unlock() + } + return nil +} + +func (tx *LockRWTransaction) Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error { + if key == "" { + return errors.ErrKeyEmpty + } + if value == nil { + return errors.ErrValueNil + } + + if expiry <= 0 { + tx.parent.memorydbPersistent[key] = entry.NewEntry(dataType, value, 0) + delete(tx.parent.memorydbTemporary, key) + } else { + tx.parent.memorydbTemporary[key] = entry.NewEntry(dataType, value, expiry) + delete(tx.parent.memorydbPersistent, key) + } + + if tx.parent.dirty != nil { + tx.parent.dirty.set(key) + } + + return nil +} + +func (tx *LockRWTransaction) Delete(key string) error { + if key == "" { + return errors.ErrKeyEmpty + } + + delete(tx.parent.memorydbTemporary, key) + delete(tx.parent.memorydbPersistent, key) + + if tx.parent.dirty != nil { + tx.parent.dirty.delete(key) + } + + return nil +} diff --git a/store/rw_tx_snapshot.go b/store/rw_tx_snapshot.go new file mode 100644 index 0000000..f47945f --- /dev/null +++ b/store/rw_tx_snapshot.go @@ -0,0 +1,119 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +type SnapshotRWTransaction struct { + *SnapshotReadTransaction + parent *CacheStore + pendingPersistent map[string]*entry.Entry + pendingTemporary map[string]*entry.Entry + committed bool +} + +func (s *CacheStore) snapshotRwTx(fn RWTransactionFunc) error { + tx := &SnapshotRWTransaction{ + SnapshotReadTransaction: newSnapshotReadTX(s), + parent: s, + pendingPersistent: make(map[string]*entry.Entry), + pendingTemporary: make(map[string]*entry.Entry), + } + + if err := fn(tx); err != nil { + return err + } + + return tx.commit() +} + +func (tx *SnapshotRWTransaction) commit() error { + if tx.committed { + return errors.ErrAlreadyCommit + } + + var delete_keys map[string]struct{} + + tx.parent.persistentMux.Lock() + tx.parent.temporaryMux.Lock() + if tx.parent.dirty != nil { + tx.parent.dirty.mux.Lock() + delete_keys = make(map[string]struct{}, len(tx.pendingPersistent)) + defer tx.parent.dirty.mux.Unlock() + } + for key, entry := range tx.pendingPersistent { + if entry == nil { + delete(tx.parent.memorydbPersistent, key) + if tx.parent.dirty != nil { + delete_keys[key] = struct{}{} + } + } else { + tx.parent.memorydbPersistent[key] = *entry + if tx.parent.dirty != nil { + tx.parent.dirty.unsafeSet(key) + } + } + } + tx.parent.persistentMux.Unlock() + + for key, entry := range tx.pendingTemporary { + if entry == nil { + delete(tx.parent.memorydbTemporary, key) + } else { + tx.parent.memorydbTemporary[key] = *entry + if tx.parent.dirty != nil { + tx.parent.dirty.unsafeSet(key) + delete(delete_keys, key) + } + } + } + tx.parent.temporaryMux.Unlock() + + if tx.parent.dirty != nil { + for key := range delete_keys { + tx.parent.dirty.unsafeDelete(key) + } + } + + tx.committed = true + return nil +} + +func (tx *SnapshotRWTransaction) Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error { + if key == "" { + return errors.ErrKeyEmpty + } + if value == nil { + return errors.ErrValueNil + } + + var e entry.Entry + if expiry <= 0 { + e = entry.NewEntry(dataType, value, 0) + tx.pendingPersistent[key] = &e + tx.pendingTemporary[key] = nil + } else { + e = entry.NewEntry(dataType, value, expiry) + tx.pendingTemporary[key] = &e + tx.pendingPersistent[key] = nil + } + tx.memorydb[key] = e + + return nil +} + +func (tx *SnapshotRWTransaction) Delete(key string) error { + if key == "" { + return errors.ErrKeyEmpty + } + + tx.pendingPersistent[key] = nil + tx.pendingTemporary[key] = nil + delete(tx.memorydb, key) + + return nil +} From 2d296b08a4de046cb48f01073b103f6552d111cf Mon Sep 17 00:00:00 2001 From: found-cake Date: Tue, 5 Aug 2025 10:53:51 +0000 Subject: [PATCH 11/20] feat: Add type conversion helpers (As*, From*) for Entry --- entry/type_boolean.go | 23 +++++++++++++++++++++++ entry/type_float.go | 31 +++++++++++++++++++++++++++++++ entry/type_integer.go | 42 ++++++++++++++++++++++++++++++++++++++++++ entry/type_json.go | 27 +++++++++++++++++++++++++++ entry/type_raw.go | 31 +++++++++++++++++++++++++++++++ entry/type_string.go | 19 +++++++++++++++++++ entry/type_time.go | 28 ++++++++++++++++++++++++++++ entry/type_uinteger.go | 42 ++++++++++++++++++++++++++++++++++++++++++ errors/errors.go | 7 ++++--- store/type_boolean.go | 2 +- store/type_json.go | 2 +- store/type_raw.go | 4 ++-- store/type_string.go | 2 +- store/type_time.go | 2 +- store/type_uinteger.go | 2 +- store/utils_number.go | 8 ++++---- 16 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 entry/type_boolean.go create mode 100644 entry/type_float.go create mode 100644 entry/type_integer.go create mode 100644 entry/type_json.go create mode 100644 entry/type_raw.go create mode 100644 entry/type_string.go create mode 100644 entry/type_time.go create mode 100644 entry/type_uinteger.go diff --git a/entry/type_boolean.go b/entry/type_boolean.go new file mode 100644 index 0000000..a6fd1b6 --- /dev/null +++ b/entry/type_boolean.go @@ -0,0 +1,23 @@ +package entry + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) AsBool() (bool, error) { + if e.Type != types.BOOLEAN { + return false, errors.ErrTypeMismatch(types.BOOLEAN, e.Type) + } + return len(e.Data) > 0 && e.Data[0] == 1, nil +} + +func FromBool(value bool, exp time.Duration) Entry { + v := byte(0) + if value { + v = 1 + } + return NewEntry(types.BOOLEAN, []byte{v}, exp) +} diff --git a/entry/type_float.go b/entry/type_float.go new file mode 100644 index 0000000..de0c3cc --- /dev/null +++ b/entry/type_float.go @@ -0,0 +1,31 @@ +package entry + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) AsFloat32(key string) (float32, error) { + if e.Type != types.FLOAT32 { + return 0, errors.ErrTypeMismatch(types.FLOAT32, e.Type) + } + return utils.Binary2Float32(e.Data) +} + +func FromFloat32(value float32, exp time.Duration) Entry { + return NewEntry(types.FLOAT32, utils.Float32toBinary(value), exp) +} + +func (e *Entry) AsFloat64(key string) (float64, error) { + if e.Type != types.FLOAT64 { + return 0, errors.ErrTypeMismatch(types.FLOAT64, e.Type) + } + return utils.Binary2Float64(e.Data) +} + +func FromFloat64(key string, value float64, exp time.Duration) Entry { + return NewEntry(types.FLOAT64, utils.Float64toBinary(value), exp) +} diff --git a/entry/type_integer.go b/entry/type_integer.go new file mode 100644 index 0000000..5ca2e74 --- /dev/null +++ b/entry/type_integer.go @@ -0,0 +1,42 @@ +package entry + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) GetInt16(key string) (int16, error) { + if e.Type != types.INT16 { + return 0, errors.ErrTypeMismatch(types.INT16, e.Type) + } + return utils.Binary2Int16(e.Data) +} + +func FromInt16(value int16, exp time.Duration) Entry { + return NewEntry(types.INT16, utils.Int16toBinary(value), exp) +} + +func (e *Entry) GetInt32(key string) (int32, error) { + if e.Type != types.INT32 { + return 0, errors.ErrTypeMismatch(types.INT32, e.Type) + } + return utils.Binary2Int32(e.Data) +} + +func SetInt32(key string, value int32, exp time.Duration) Entry { + return NewEntry(types.INT32, utils.Int32toBinary(value), exp) +} + +func (e *Entry) GetInt64(key string) (int64, error) { + if e.Type != types.INT64 { + return 0, errors.ErrTypeMismatch(types.INT64, e.Type) + } + return utils.Binary2Int64(e.Data) +} + +func SetInt64(key string, value int64, exp time.Duration) Entry { + return NewEntry(types.INT64, utils.Int64toBinary(value), exp) +} diff --git a/entry/type_json.go b/entry/type_json.go new file mode 100644 index 0000000..b3cd0ee --- /dev/null +++ b/entry/type_json.go @@ -0,0 +1,27 @@ +package entry + +import ( + "encoding/json" + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) AsJSON(target interface{}) error { + if e.Type != types.JSON { + return errors.ErrTypeMismatch(types.JSON, e.Type) + } + if len(e.Data) == 0 { + return errors.ErrDataEmpty + } + return json.Unmarshal(e.Data, target) +} + +func FromJSON(value interface{}, exp time.Duration) (Entry, error) { + if data, err := json.Marshal(value); err != nil { + return Entry{}, err + } else { + return NewEntry(types.JSON, data, exp), nil + } +} diff --git a/entry/type_raw.go b/entry/type_raw.go new file mode 100644 index 0000000..5622ee2 --- /dev/null +++ b/entry/type_raw.go @@ -0,0 +1,31 @@ +package entry + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) AsRaw(key string) ([]byte, error) { + if e.Type != types.RAW { + return nil, errors.ErrTypeMismatch(types.RAW, e.Type) + } + + result := make([]byte, len(e.Data)) + copy(result, e.Data) + + return result, nil +} + +func (e *Entry) AsRawNoCopy(key string) ([]byte, error) { + if e.Type != types.RAW { + return nil, errors.ErrTypeMismatch(types.RAW, e.Type) + } + + return e.Data, nil +} + +func FromRaw(value []byte, exp time.Duration) Entry { + return NewEntry(types.RAW, value, exp) +} diff --git a/entry/type_string.go b/entry/type_string.go new file mode 100644 index 0000000..d34ca82 --- /dev/null +++ b/entry/type_string.go @@ -0,0 +1,19 @@ +package entry + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) AsString() (string, error) { + if e.Type != types.STRING { + return "", errors.ErrTypeMismatch(types.STRING, e.Type) + } + return string(e.Data), nil +} + +func FromString(value string, exp time.Duration) Entry { + return NewEntry(types.STRING, []byte(value), exp) +} diff --git a/entry/type_time.go b/entry/type_time.go new file mode 100644 index 0000000..f85349d --- /dev/null +++ b/entry/type_time.go @@ -0,0 +1,28 @@ +package entry + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) AsTime() (time.Time, error) { + var t time.Time + if e.Type != types.TIME { + return t, errors.ErrTypeMismatch(types.TIME, e.Type) + } + if len(e.Data) == 0 { + return t, errors.ErrDataEmpty + } + err := t.UnmarshalBinary(e.Data) + return t, err +} + +func FromTime(value time.Time, exp time.Duration) (Entry, error) { + if b, err := value.MarshalBinary(); err != nil { + return Entry{}, err + } else { + return NewEntry(types.TIME, b, exp), nil + } +} diff --git a/entry/type_uinteger.go b/entry/type_uinteger.go new file mode 100644 index 0000000..2e850bc --- /dev/null +++ b/entry/type_uinteger.go @@ -0,0 +1,42 @@ +package entry + +import ( + "time" + + "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/utils" + "github.com/found-cake/CacheStore/utils/types" +) + +func (e *Entry) AsUInt16(key string) (uint16, error) { + if e.Type != types.UINT16 { + return 0, errors.ErrTypeMismatch(types.UINT16, e.Type) + } + return utils.Binary2UInt16(e.Data) +} + +func FromUInt16(value uint16, exp time.Duration) Entry { + return NewEntry(types.UINT16, utils.UInt16toBinary(value), exp) +} + +func (e *Entry) AsUInt32(key string) (uint32, error) { + if e.Type != types.UINT32 { + return 0, errors.ErrTypeMismatch(types.UINT32, e.Type) + } + return utils.Binary2UInt32(e.Data) +} + +func FromUInt32(value uint32, exp time.Duration) Entry { + return NewEntry(types.UINT32, utils.UInt32toBinary(value), exp) +} + +func (e *Entry) AsUInt64(key string) (uint64, error) { + if e.Type != types.UINT64 { + return 0, errors.ErrTypeMismatch(types.UINT64, e.Type) + } + return utils.Binary2UInt64(e.Data) +} + +func FromUInt64(value uint64, exp time.Duration) Entry { + return NewEntry(types.UINT64, utils.UInt64toBinary(value), exp) +} diff --git a/errors/errors.go b/errors/errors.go index 97400fc..c3c56a0 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -11,6 +11,7 @@ import ( var ( ErrKeyEmpty = errors.New("key cannot be empty") + ErrDataEmpty = errors.New("data cannot be empty") ErrIsClosed = errors.New("cache store is closed") ErrAlreadyCommit = errors.New("transaction already committed") ErrNotLocked = errors.New("read transaction not locked") @@ -31,9 +32,9 @@ func ErrNoDataForKey(key string) error { return fmt.Errorf("no data found for key: %s", key) } -func ErrTypeMismatch(key string, expected, actual types.DataType) error { - return fmt.Errorf("type mismatch for key '%s': expected %s, got %s", - key, expected.String(), actual.String()) +func ErrTypeMismatch(expected, actual types.DataType) error { + return fmt.Errorf("type mismatch: expected %s, got %s", + expected.String(), actual.String()) } func ErrUnsignedUnderflow[T generic.Unsigned](key string, current, delta T) error { diff --git a/store/type_boolean.go b/store/type_boolean.go index 32893ff..8cc8bc6 100644 --- a/store/type_boolean.go +++ b/store/type_boolean.go @@ -18,7 +18,7 @@ func (s *CacheStore) GetBool(key string) (bool, error) { return false, err } if e.Type != types.BOOLEAN { - return false, errors.ErrTypeMismatch(key, types.BOOLEAN, e.Type) + return false, errors.ErrTypeMismatch(types.BOOLEAN, e.Type) } return len(e.Data) > 0 && e.Data[0] == 1, nil } diff --git a/store/type_json.go b/store/type_json.go index 80177ce..1a9c1a1 100644 --- a/store/type_json.go +++ b/store/type_json.go @@ -19,7 +19,7 @@ func (s *CacheStore) GetJSON(key string, target interface{}) error { return err } if e.Type != types.JSON { - return errors.ErrTypeMismatch(key, types.JSON, e.Type) + return errors.ErrTypeMismatch(types.JSON, e.Type) } if len(e.Data) == 0 { return errors.ErrNoDataForKey(key) diff --git a/store/type_raw.go b/store/type_raw.go index d154d73..f926c69 100644 --- a/store/type_raw.go +++ b/store/type_raw.go @@ -18,7 +18,7 @@ func (s *CacheStore) GetRaw(key string) ([]byte, error) { return nil, err } if e.Type != types.RAW { - return nil, errors.ErrTypeMismatch(key, types.RAW, e.Type) + return nil, errors.ErrTypeMismatch(types.RAW, e.Type) } result := make([]byte, len(e.Data)) @@ -38,7 +38,7 @@ func (s *CacheStore) GetRawNoCopy(key string) ([]byte, error) { return nil, err } if e.Type != types.RAW { - return nil, errors.ErrTypeMismatch(key, types.RAW, e.Type) + return nil, errors.ErrTypeMismatch(types.RAW, e.Type) } return e.Data, nil diff --git a/store/type_string.go b/store/type_string.go index 6eb3817..e5685cb 100644 --- a/store/type_string.go +++ b/store/type_string.go @@ -18,7 +18,7 @@ func (s *CacheStore) GetString(key string) (string, error) { return "", err } if e.Type != types.STRING { - return "", errors.ErrTypeMismatch(key, types.STRING, e.Type) + return "", errors.ErrTypeMismatch(types.STRING, e.Type) } return string(e.Data), nil } diff --git a/store/type_time.go b/store/type_time.go index 8f26c1a..4f5ef02 100644 --- a/store/type_time.go +++ b/store/type_time.go @@ -19,7 +19,7 @@ func (s *CacheStore) GetTime(key string) (time.Time, error) { return t, err } if e.Type != types.TIME { - return t, errors.ErrTypeMismatch(key, types.TIME, e.Type) + return t, errors.ErrTypeMismatch(types.TIME, e.Type) } if len(e.Data) == 0 { return t, errors.ErrNoDataForKey(key) diff --git a/store/type_uinteger.go b/store/type_uinteger.go index 910bb96..0b3d70c 100644 --- a/store/type_uinteger.go +++ b/store/type_uinteger.go @@ -104,7 +104,7 @@ func decrUnsigned[T generic.Unsigned]( return errors.ErrNoDataForKey(key) } if e.Type != data_type { - return errors.ErrTypeMismatch(key, data_type, e.Type) + return errors.ErrTypeMismatch(data_type, e.Type) } value, err := fromBinary(e.Data) if err != nil { diff --git a/store/utils_number.go b/store/utils_number.go index 247f32d..e1e96e0 100644 --- a/store/utils_number.go +++ b/store/utils_number.go @@ -32,7 +32,7 @@ func (s *CacheStore) getNum16(key string, expected types.DataType) (uint16, erro return 0, err } if e.Type != expected { - return 0, errors.ErrTypeMismatch(key, expected, e.Type) + return 0, errors.ErrTypeMismatch(expected, e.Type) } return utils.Binary2UInt16(e.Data) } @@ -48,7 +48,7 @@ func (s *CacheStore) getNum32(key string, expected types.DataType) (uint32, erro return 0, err } if e.Type != expected { - return 0, errors.ErrTypeMismatch(key, expected, e.Type) + return 0, errors.ErrTypeMismatch(expected, e.Type) } return utils.Binary2UInt32(e.Data) } @@ -64,7 +64,7 @@ func (s *CacheStore) getNum64(key string, expected types.DataType) (uint64, erro return 0, err } if e.Type != expected { - return 0, errors.ErrTypeMismatch(key, expected, e.Type) + return 0, errors.ErrTypeMismatch(expected, e.Type) } return utils.Binary2UInt64(e.Data) } @@ -92,7 +92,7 @@ func incrNumber[T generic.Numberic]( return nil } if e.Type != data_type { - return errors.ErrTypeMismatch(key, data_type, e.Type) + return errors.ErrTypeMismatch(data_type, e.Type) } value, err := fromBinary(e.Data) if err != nil { From 2cf8d4abd529f5c841a56dfd38553e08a83eab6c Mon Sep 17 00:00:00 2001 From: found-cake Date: Tue, 5 Aug 2025 11:15:16 +0000 Subject: [PATCH 12/20] refactor: Use Entry for Get/Set methods instead of raw types --- store/read_tx.go | 5 ++--- store/read_tx_lock.go | 32 ++++++++++---------------------- store/read_tx_snapshot.go | 27 +++++++-------------------- store/write_tx.go | 18 +++++------------- 4 files changed, 24 insertions(+), 58 deletions(-) diff --git a/store/read_tx.go b/store/read_tx.go index fd9b2be..eace071 100644 --- a/store/read_tx.go +++ b/store/read_tx.go @@ -3,15 +3,14 @@ package store import ( "time" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" ) type ReadTransactionFunc func(tx ReadTransaction) error type ReadTransaction interface { - Get(key string) (types.DataType, []byte, error) - GetNoCopy(key string) (types.DataType, []byte, error) + Get(key string) (*entry.Entry, error) Exists(keys ...string) int TTL(key string) time.Duration } diff --git a/store/read_tx_lock.go b/store/read_tx_lock.go index a2d0f2d..2e87630 100644 --- a/store/read_tx_lock.go +++ b/store/read_tx_lock.go @@ -3,8 +3,8 @@ package store import ( "time" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" ) type LockReadTransaction struct { @@ -26,38 +26,26 @@ func (s *CacheStore) lockReadTx(fn ReadTransactionFunc) error { return fn(tx) } -func (tx *LockReadTransaction) Get(key string) (types.DataType, []byte, error) { - t, data, err := tx.GetNoCopy(key) - if err == nil { - result := make([]byte, len(data)) - copy(result, data) - return t, result, err - } - return t, data, err -} - -// GetNoCopy retrieves a value without copying data (zero-copy read) -// ⚠️ WARNING: Don't modify the returned value! -func (tx *LockReadTransaction) GetNoCopy(key string) (types.DataType, []byte, error) { +func (tx *LockReadTransaction) Get(key string) (*entry.Entry, error) { if key == "" { - return types.UNKNOWN, nil, errors.ErrKeyEmpty + return nil, errors.ErrKeyEmpty } - entry, ok := tx.parent.memorydbPersistent[key] + e, ok := tx.parent.memorydbPersistent[key] if ok { - return entry.Type, entry.Data, nil + return &e, nil } - entry, ok = tx.parent.memorydbTemporary[key] + e, ok = tx.parent.memorydbTemporary[key] if ok { - if entry.IsExpired() { - return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) + if e.IsExpired() { + return nil, errors.ErrNoDataForKey(key) } else { - return entry.Type, entry.Data, nil + return &e, nil } } - return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) + return nil, errors.ErrNoDataForKey(key) } func (tx *LockReadTransaction) Exists(keys ...string) int { diff --git a/store/read_tx_snapshot.go b/store/read_tx_snapshot.go index 31c360a..3da7bbd 100644 --- a/store/read_tx_snapshot.go +++ b/store/read_tx_snapshot.go @@ -5,7 +5,6 @@ import ( "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" ) type SnapshotReadTransaction struct { @@ -56,32 +55,20 @@ func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { return fn(tx) } -func (tx *SnapshotReadTransaction) Get(key string) (types.DataType, []byte, error) { - t, data, err := tx.GetNoCopy(key) - if err == nil { - result := make([]byte, len(data)) - copy(result, data) - return t, result, err - } - return t, data, err -} - -// GetNoCopy retrieves a value without copying data (zero-copy read) -// ⚠️ WARNING: Don't modify the returned value! -func (tx *SnapshotReadTransaction) GetNoCopy(key string) (types.DataType, []byte, error) { +func (tx *SnapshotReadTransaction) Get(key string) (*entry.Entry, error) { if key == "" { - return types.UNKNOWN, nil, errors.ErrKeyEmpty + return nil, errors.ErrKeyEmpty } - entry, ok := tx.memorydb[key] + e, ok := tx.memorydb[key] if !ok { - return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) + return nil, errors.ErrNoDataForKey(key) } - if entry.IsExpired() { - return types.UNKNOWN, nil, errors.ErrNoDataForKey(key) + if e.IsExpired() { + return nil, errors.ErrNoDataForKey(key) } - return entry.Type, entry.Data, nil + return &e, nil } func (tx *SnapshotReadTransaction) Exists(keys ...string) int { diff --git a/store/write_tx.go b/store/write_tx.go index ef233ab..078d672 100644 --- a/store/write_tx.go +++ b/store/write_tx.go @@ -1,11 +1,8 @@ package store import ( - "time" - "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" ) type WriteTransactionFunc func(tx *WriteTransaction) error @@ -87,19 +84,19 @@ func (tx *WriteTransaction) commit() error { return nil } -func (tx *WriteTransaction) Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error { +func (tx *WriteTransaction) Set(key string, e entry.Entry) error { if key == "" { return errors.ErrKeyEmpty } - if value == nil { + if e.Data == nil { return errors.ErrValueNil } - if expiry <= 0 { - tx.pendingPersistent[key] = newEntry(dataType, value, 0) + if e.Expiry <= 0 { + tx.pendingPersistent[key] = &e tx.pendingTemporary[key] = nil } else { - tx.pendingTemporary[key] = newEntry(dataType, value, expiry) + tx.pendingTemporary[key] = &e tx.pendingPersistent[key] = nil } @@ -116,8 +113,3 @@ func (tx *WriteTransaction) Delete(key string) error { return nil } - -func newEntry(dataType types.DataType, data []byte, exp time.Duration) *entry.Entry { - e := entry.NewEntry(dataType, data, exp) - return &e -} From 6516ee33fd92a8e5a297391d9943ab6ef0d33d08 Mon Sep 17 00:00:00 2001 From: found-cake Date: Tue, 5 Aug 2025 11:27:37 +0000 Subject: [PATCH 13/20] refactor: Use Entry for Get/Set methods instead of raw types --- store/rw_tx.go | 6 ++---- store/rw_tx_lock.go | 13 +++++-------- store/rw_tx_snapshot.go | 12 +++--------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/store/rw_tx.go b/store/rw_tx.go index a12ed96..fb59771 100644 --- a/store/rw_tx.go +++ b/store/rw_tx.go @@ -1,17 +1,15 @@ package store import ( - "time" - + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" ) type RWTransactionFunc func(tx RWTransaction) error type RWTransaction interface { ReadTransaction - Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error + Set(key string, entry entry.Entry) error Delete(key string) error commit() error } diff --git a/store/rw_tx_lock.go b/store/rw_tx_lock.go index c0d4b9e..32000b7 100644 --- a/store/rw_tx_lock.go +++ b/store/rw_tx_lock.go @@ -1,11 +1,8 @@ package store import ( - "time" - "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" ) type LockRWTransaction struct { @@ -38,19 +35,19 @@ func (tx *LockRWTransaction) commit() error { return nil } -func (tx *LockRWTransaction) Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error { +func (tx *LockRWTransaction) Set(key string, e entry.Entry) error { if key == "" { return errors.ErrKeyEmpty } - if value == nil { + if e.Data == nil { return errors.ErrValueNil } - if expiry <= 0 { - tx.parent.memorydbPersistent[key] = entry.NewEntry(dataType, value, 0) + if e.Expiry <= 0 { + tx.parent.memorydbPersistent[key] = e delete(tx.parent.memorydbTemporary, key) } else { - tx.parent.memorydbTemporary[key] = entry.NewEntry(dataType, value, expiry) + tx.parent.memorydbTemporary[key] = e delete(tx.parent.memorydbPersistent, key) } diff --git a/store/rw_tx_snapshot.go b/store/rw_tx_snapshot.go index f47945f..4ee7308 100644 --- a/store/rw_tx_snapshot.go +++ b/store/rw_tx_snapshot.go @@ -1,11 +1,8 @@ package store import ( - "time" - "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils/types" ) type SnapshotRWTransaction struct { @@ -83,21 +80,18 @@ func (tx *SnapshotRWTransaction) commit() error { return nil } -func (tx *SnapshotRWTransaction) Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error { +func (tx *SnapshotRWTransaction) Set(key string, e entry.Entry) error { if key == "" { return errors.ErrKeyEmpty } - if value == nil { + if e.Data == nil { return errors.ErrValueNil } - var e entry.Entry - if expiry <= 0 { - e = entry.NewEntry(dataType, value, 0) + if e.Expiry <= 0 { tx.pendingPersistent[key] = &e tx.pendingTemporary[key] = nil } else { - e = entry.NewEntry(dataType, value, expiry) tx.pendingTemporary[key] = &e tx.pendingPersistent[key] = nil } From 4240d5d949021f820da1b066a50cb820d9d574ea Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 10 Aug 2025 04:28:09 +0000 Subject: [PATCH 14/20] lol --- entry/type_float.go | 4 ++-- entry/type_integer.go | 10 +++++----- entry/type_uinteger.go | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/entry/type_float.go b/entry/type_float.go index de0c3cc..b608f2f 100644 --- a/entry/type_float.go +++ b/entry/type_float.go @@ -8,7 +8,7 @@ import ( "github.com/found-cake/CacheStore/utils/types" ) -func (e *Entry) AsFloat32(key string) (float32, error) { +func (e *Entry) AsFloat32() (float32, error) { if e.Type != types.FLOAT32 { return 0, errors.ErrTypeMismatch(types.FLOAT32, e.Type) } @@ -19,7 +19,7 @@ func FromFloat32(value float32, exp time.Duration) Entry { return NewEntry(types.FLOAT32, utils.Float32toBinary(value), exp) } -func (e *Entry) AsFloat64(key string) (float64, error) { +func (e *Entry) AsFloat64() (float64, error) { if e.Type != types.FLOAT64 { return 0, errors.ErrTypeMismatch(types.FLOAT64, e.Type) } diff --git a/entry/type_integer.go b/entry/type_integer.go index 5ca2e74..6269686 100644 --- a/entry/type_integer.go +++ b/entry/type_integer.go @@ -8,7 +8,7 @@ import ( "github.com/found-cake/CacheStore/utils/types" ) -func (e *Entry) GetInt16(key string) (int16, error) { +func (e *Entry) AsInt16() (int16, error) { if e.Type != types.INT16 { return 0, errors.ErrTypeMismatch(types.INT16, e.Type) } @@ -19,24 +19,24 @@ func FromInt16(value int16, exp time.Duration) Entry { return NewEntry(types.INT16, utils.Int16toBinary(value), exp) } -func (e *Entry) GetInt32(key string) (int32, error) { +func (e *Entry) AsInt32() (int32, error) { if e.Type != types.INT32 { return 0, errors.ErrTypeMismatch(types.INT32, e.Type) } return utils.Binary2Int32(e.Data) } -func SetInt32(key string, value int32, exp time.Duration) Entry { +func FromInt32(key string, value int32, exp time.Duration) Entry { return NewEntry(types.INT32, utils.Int32toBinary(value), exp) } -func (e *Entry) GetInt64(key string) (int64, error) { +func (e *Entry) AsInt64() (int64, error) { if e.Type != types.INT64 { return 0, errors.ErrTypeMismatch(types.INT64, e.Type) } return utils.Binary2Int64(e.Data) } -func SetInt64(key string, value int64, exp time.Duration) Entry { +func FromInt64(key string, value int64, exp time.Duration) Entry { return NewEntry(types.INT64, utils.Int64toBinary(value), exp) } diff --git a/entry/type_uinteger.go b/entry/type_uinteger.go index 2e850bc..43b4773 100644 --- a/entry/type_uinteger.go +++ b/entry/type_uinteger.go @@ -8,7 +8,7 @@ import ( "github.com/found-cake/CacheStore/utils/types" ) -func (e *Entry) AsUInt16(key string) (uint16, error) { +func (e *Entry) AsUInt16() (uint16, error) { if e.Type != types.UINT16 { return 0, errors.ErrTypeMismatch(types.UINT16, e.Type) } @@ -19,7 +19,7 @@ func FromUInt16(value uint16, exp time.Duration) Entry { return NewEntry(types.UINT16, utils.UInt16toBinary(value), exp) } -func (e *Entry) AsUInt32(key string) (uint32, error) { +func (e *Entry) AsUInt32() (uint32, error) { if e.Type != types.UINT32 { return 0, errors.ErrTypeMismatch(types.UINT32, e.Type) } @@ -30,7 +30,7 @@ func FromUInt32(value uint32, exp time.Duration) Entry { return NewEntry(types.UINT32, utils.UInt32toBinary(value), exp) } -func (e *Entry) AsUInt64(key string) (uint64, error) { +func (e *Entry) AsUInt64() (uint64, error) { if e.Type != types.UINT64 { return 0, errors.ErrTypeMismatch(types.UINT64, e.Type) } From e95334aeae20e156c318d16ee351a9e97924a335 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 10 Aug 2025 07:48:20 +0000 Subject: [PATCH 15/20] feat: Add CopyData Method --- entry/entry.go | 10 ++++++++-- entry/type_raw.go | 9 +++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/entry/entry.go b/entry/entry.go index 9552873..922dd27 100644 --- a/entry/entry.go +++ b/entry/entry.go @@ -12,11 +12,17 @@ type Entry struct { Expiry int64 } -func (e Entry) IsExpired() bool { +func (e *Entry) CopyData() []byte { + result := make([]byte, len(e.Data)) + copy(result, e.Data) + return result +} + +func (e *Entry) IsExpired() bool { return e.IsExpiredWithUnixMilli(time.Now().UnixMilli()) } -func (e Entry) IsExpiredWithUnixMilli(now int64) bool { +func (e *Entry) IsExpiredWithUnixMilli(now int64) bool { return e.Expiry > 0 && e.Expiry <= now } diff --git a/entry/type_raw.go b/entry/type_raw.go index 5622ee2..97710ab 100644 --- a/entry/type_raw.go +++ b/entry/type_raw.go @@ -7,18 +7,15 @@ import ( "github.com/found-cake/CacheStore/utils/types" ) -func (e *Entry) AsRaw(key string) ([]byte, error) { +func (e *Entry) AsRaw() ([]byte, error) { if e.Type != types.RAW { return nil, errors.ErrTypeMismatch(types.RAW, e.Type) } - result := make([]byte, len(e.Data)) - copy(result, e.Data) - - return result, nil + return e.CopyData(), nil } -func (e *Entry) AsRawNoCopy(key string) ([]byte, error) { +func (e *Entry) AsRawNoCopy() ([]byte, error) { if e.Type != types.RAW { return nil, errors.ErrTypeMismatch(types.RAW, e.Type) } From e062dd144fbcc5bfb4fe9f9fc75b88383aa05cd8 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 10 Aug 2025 07:54:54 +0000 Subject: [PATCH 16/20] refactor: use transaction for numeric increment/decrement --- store/type_uinteger.go | 45 +++++++++++++++++------------------- store/utils_number.go | 52 +++++++++++++++++++----------------------- 2 files changed, 45 insertions(+), 52 deletions(-) diff --git a/store/type_uinteger.go b/store/type_uinteger.go index 0b3d70c..bcf287f 100644 --- a/store/type_uinteger.go +++ b/store/type_uinteger.go @@ -3,6 +3,7 @@ package store import ( "time" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" "github.com/found-cake/CacheStore/utils" "github.com/found-cake/CacheStore/utils/generic" @@ -97,28 +98,24 @@ func decrUnsigned[T generic.Unsigned]( if key == "" { return errors.ErrKeyEmpty } - s.temporaryMux.Lock() - defer s.temporaryMux.Unlock() - e, err := s.unsafeGet(key) - if err != nil { - return errors.ErrNoDataForKey(key) - } - if e.Type != data_type { - return errors.ErrTypeMismatch(data_type, e.Type) - } - value, err := fromBinary(e.Data) - if err != nil { - return err - } - if checkUnderflow(value, delta) { - return errors.ErrUnsignedUnderflow(key, value, delta) - } - value -= delta - data := toBinary(value) - if exp > 0 { - s.unsafeSet(key, data_type, data, exp) - } else { - s.setKeepExp(key, data_type, data, e.Expiry) - } - return nil + return s.RWTransaction(false, func(tx RWTransaction) error { + e, err := tx.Get(key) + if err != nil { + return errors.ErrNoDataForKey(key) + } + if e.Type != data_type { + return errors.ErrTypeMismatch(data_type, e.Type) + } + value, err := fromBinary(e.Data) + if err != nil { + return err + } + if checkUnderflow(value, delta) { + return errors.ErrUnsignedUnderflow(key, value, delta) + } + value -= delta + data := toBinary(value) + tx.Set(key, entry.NewEntry(data_type, data, exp)) + return nil + }) } diff --git a/store/utils_number.go b/store/utils_number.go index e1e96e0..24f1aa7 100644 --- a/store/utils_number.go +++ b/store/utils_number.go @@ -83,33 +83,29 @@ func incrNumber[T generic.Numberic]( if key == "" { return errors.ErrKeyEmpty } - s.temporaryMux.Lock() - defer s.temporaryMux.Unlock() - e, err := s.unsafeGet(key) - if err != nil { - data := toBinary(delta) - s.unsafeSet(key, data_type, data, exp) + return s.RWTransaction(false, func(tx RWTransaction) error { + e, err := tx.Get(key) + if err != nil { + data := toBinary(delta) + tx.Set(key, entry.NewEntry(data_type, data, exp)) + return nil + } + if e.Type != data_type { + return errors.ErrTypeMismatch(data_type, e.Type) + } + value, err := fromBinary(e.Data) + if err != nil { + return err + } + if checkOverFlow(value, delta) { + return errors.ErrValueOverflow(key, data_type, value, delta) + } + value += delta + data := toBinary(value) + if checkFloatSpesial != nil && checkFloatSpesial(value) { + return errors.ErrFloatSpecial + } + s.Set(key, data_type, data, exp) return nil - } - if e.Type != data_type { - return errors.ErrTypeMismatch(data_type, e.Type) - } - value, err := fromBinary(e.Data) - if err != nil { - return err - } - if checkOverFlow(value, delta) { - return errors.ErrValueOverflow(key, data_type, value, delta) - } - value += delta - data := toBinary(value) - if checkFloatSpesial != nil && checkFloatSpesial(value) { - return errors.ErrFloatSpecial - } - if exp > 0 { - s.unsafeSet(key, data_type, data, exp) - } else { - s.setKeepExp(key, data_type, data, e.Expiry) - } - return nil + }) } From 81c015c6ebeb42bb6edfbb63f8bfee39025707b9 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sat, 16 Aug 2025 15:14:30 +0000 Subject: [PATCH 17/20] refactor: methods for split memory db --- store/store_core.go | 157 +++++++++++++++++++++++------------------ store/type_boolean.go | 31 ++++---- store/type_float.go | 28 +++++--- store/type_integer.go | 40 +++++++---- store/type_json.go | 40 +++++------ store/type_raw.go | 53 ++++++-------- store/type_string.go | 27 ++++--- store/type_time.go | 40 +++++------ store/type_uinteger.go | 27 ++++++- store/utils_number.go | 62 +--------------- 10 files changed, 233 insertions(+), 272 deletions(-) diff --git a/store/store_core.go b/store/store_core.go index b3c966c..17948d7 100644 --- a/store/store_core.go +++ b/store/store_core.go @@ -42,31 +42,41 @@ func (s *CacheStore) cleanExpired() { } } -func (s *CacheStore) unsafeGet(key string) (entry.Entry, error) { +type entryProcessor[T interface{}] func(*entry.Entry) (types.DataType, T, error) + +func get[T interface{}](s *CacheStore, key string, proc entryProcessor[T]) (t types.DataType, data T, err error) { + if key == "" { + err = errors.ErrKeyEmpty + return + } + + { + s.persistentMux.RLock() + defer s.persistentMux.RUnlock() + v, ok := s.memorydbPersistent[key] + if ok { + return proc(&v) + } + } + + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() v, ok := s.memorydbTemporary[key] if !ok { - return v, errors.ErrNoDataForKey(key) + err = errors.ErrNoDataForKey(key) + return } if v.IsExpired() { - return v, errors.ErrNoDataForKey(key) + err = errors.ErrNoDataForKey(key) + return } - return v, nil + return proc(&v) } func (s *CacheStore) Get(key string) (types.DataType, []byte, error) { - if key == "" { - return types.UNKNOWN, nil, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - v, err := s.unsafeGet(key) - if err != nil { - return types.UNKNOWN, nil, err - } - - result := make([]byte, len(v.Data)) - copy(result, v.Data) - return v.Type, result, nil + return get(s, key, func(e *entry.Entry) (types.DataType, []byte, error) { + return e.Type, e.CopyData(), nil + }) } // ⚠️ WARNING: GetNoCopy returns a reference to internal cache data. @@ -80,25 +90,9 @@ func (s *CacheStore) Get(key string) (types.DataType, []byte, error) { // // use Get() to avoid race conditions and data corruption. func (s *CacheStore) GetNoCopy(key string) (types.DataType, []byte, error) { - if key == "" { - return types.UNKNOWN, nil, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - v, err := s.unsafeGet(key) - if err != nil { - return types.UNKNOWN, nil, err - } - - return v.Type, v.Data, nil -} - -func (s *CacheStore) unsafeSet(key string, dataType types.DataType, value []byte, expiry time.Duration) { - s.memorydbTemporary[key] = entry.NewEntry(dataType, value, expiry) - - if s.dirty != nil { - s.dirty.set(key) - } + return get(s, key, func(e *entry.Entry) (types.DataType, []byte, error) { + return e.Type, e.Data, nil + }) } func (s *CacheStore) Set(key string, dataType types.DataType, value []byte, expiry time.Duration) error { @@ -108,16 +102,9 @@ func (s *CacheStore) Set(key string, dataType types.DataType, value []byte, expi if value == nil { return errors.ErrValueNil } - - s.temporaryMux.Lock() - s.memorydbTemporary[key] = entry.NewEntry(dataType, value, expiry) - s.temporaryMux.Unlock() - - if s.dirty != nil { - s.dirty.set(key) - } - - return nil + return s.WriteTransaction(func(tx *WriteTransaction) error { + return tx.Set(key, entry.NewEntry(dataType, value, expiry)) + }) } func (s *CacheStore) Delete(key string) error { @@ -125,6 +112,10 @@ func (s *CacheStore) Delete(key string) error { return errors.ErrKeyEmpty } + s.persistentMux.Lock() + delete(s.memorydbPersistent, key) + s.persistentMux.Unlock() + s.temporaryMux.Lock() delete(s.memorydbTemporary, key) s.temporaryMux.Unlock() @@ -137,6 +128,10 @@ func (s *CacheStore) Delete(key string) error { } func (s *CacheStore) Flush() { + s.persistentMux.Lock() + s.memorydbPersistent = make(map[string]entry.Entry) + s.persistentMux.Unlock() + s.temporaryMux.Lock() s.memorydbTemporary = make(map[string]entry.Entry) s.temporaryMux.Unlock() @@ -166,9 +161,20 @@ func (s *CacheStore) Close() error { log.Println(err) } }() + s.persistentMux.Lock() + s.temporaryMux.Lock() + defer s.persistentMux.Unlock() + defer s.temporaryMux.Unlock() + for key, v := range s.memorydbPersistent { + s.memorydbTemporary[key] = entry.Entry{ + Type: v.Type, + Data: v.Data, + } + } err = s.sqlitedb.Save(s.memorydbTemporary, true) } + s.memorydbPersistent = nil s.memorydbTemporary = nil s.dirty = nil @@ -179,6 +185,14 @@ func (s *CacheStore) Exists(keys ...string) int { now := time.Now().UnixMilli() count := 0 + s.persistentMux.RLock() + for _, key := range keys { + if _, ok := s.memorydbPersistent[key]; ok { + count++ + } + } + s.persistentMux.RUnlock() + s.temporaryMux.RLock() defer s.temporaryMux.RUnlock() @@ -193,11 +207,18 @@ func (s *CacheStore) Exists(keys ...string) int { } func (s *CacheStore) Keys() []string { - now := time.Now().UnixMilli() + s.persistentMux.RLock() s.temporaryMux.RLock() + + keys := make([]string, 0, len(s.memorydbPersistent)+len(s.memorydbTemporary)) + for key := range s.memorydbPersistent { + keys = append(keys, key) + } + s.persistentMux.RUnlock() + + now := time.Now().UnixMilli() defer s.temporaryMux.RUnlock() - keys := make([]string, 0, len(s.memorydbTemporary)) for key, e := range s.memorydbTemporary { if !e.IsExpiredWithUnixMilli(now) { keys = append(keys, key) @@ -207,6 +228,12 @@ func (s *CacheStore) Keys() []string { } func (s *CacheStore) TTL(key string) time.Duration { + s.persistentMux.RLock() + if _, ok := s.memorydbPersistent[key]; ok { + return TTLNoExpiry + } + s.persistentMux.RUnlock() + s.temporaryMux.RLock() defer s.temporaryMux.RUnlock() @@ -251,8 +278,10 @@ func (s *CacheStore) Sync() { return } + s.persistentMux.RLock() s.temporaryMux.RLock() - if dirtySize > s.dirty.ThresholdCount && dirtySize > int(float64(len(s.memorydbTemporary))*s.dirty.ThresholdRatio) { + if dirtySize > s.dirty.ThresholdCount && dirtySize > int(float64(len(s.memorydbPersistent)+len(s.memorydbTemporary))*s.dirty.ThresholdRatio) { + s.persistentMux.RUnlock() s.temporaryMux.RUnlock() s.dirty.mux.Unlock() s.FullSync() @@ -262,18 +291,23 @@ func (s *CacheStore) Sync() { set_keys, delete_keys := s.dirty.keys() new_data := make(map[string]entry.Entry, len(set_keys)) for _, key := range set_keys { + if e, ok := s.memorydbPersistent[key]; ok { + new_data[key] = entry.Entry{ + Type: e.Type, + Data: e.CopyData(), + } + continue + } if e, ok := s.memorydbTemporary[key]; ok { - dataCopy := make([]byte, len(e.Data)) - copy(dataCopy, e.Data) - new_data[key] = entry.Entry{ Type: e.Type, - Data: dataCopy, + Data: e.CopyData(), Expiry: e.Expiry, } } } + s.persistentMux.RUnlock() s.temporaryMux.RUnlock() s.dirty.unsafeClear() s.dirty.mux.Unlock() @@ -292,27 +326,14 @@ func (s *CacheStore) FullSync() { return } - s.temporaryMux.RLock() - snapshot := make(map[string]entry.Entry, len(s.memorydbTemporary)) - for key, e := range s.memorydbTemporary { - dataCopy := make([]byte, len(e.Data)) - copy(dataCopy, e.Data) - - snapshot[key] = entry.Entry{ - Type: e.Type, - Data: dataCopy, - Expiry: e.Expiry, - } - } - s.temporaryMux.RUnlock() + tx := newSnapshotReadTX(s) if s.dirty != nil { s.dirty.clear() } - s.wg.Add(1) go func() { defer s.wg.Done() - if err := s.sqlitedb.Save(snapshot, false); err != nil { + if err := s.sqlitedb.Save(tx.memorydb, false); err != nil { log.Println(err) } }() diff --git a/store/type_boolean.go b/store/type_boolean.go index 8cc8bc6..1b3f403 100644 --- a/store/type_boolean.go +++ b/store/type_boolean.go @@ -3,30 +3,23 @@ package store import ( "time" - "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/utils/types" ) func (s *CacheStore) GetBool(key string) (bool, error) { - if key == "" { - return false, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return false, err - } - if e.Type != types.BOOLEAN { - return false, errors.ErrTypeMismatch(types.BOOLEAN, e.Type) - } - return len(e.Data) > 0 && e.Data[0] == 1, nil + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data bool, err error) { + data, err = e.AsBool() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetBool(key string, value bool, exp time.Duration) error { - v := byte(0) - if value { - v = 1 - } - return s.Set(key, types.BOOLEAN, []byte{v}, exp) + return s.WriteTransaction(func(tx *WriteTransaction) error { + return tx.Set(key, entry.FromBool(value, exp)) + }) } diff --git a/store/type_float.go b/store/type_float.go index 87010c6..87bde62 100644 --- a/store/type_float.go +++ b/store/type_float.go @@ -1,19 +1,22 @@ package store import ( - "math" "time" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/utils" "github.com/found-cake/CacheStore/utils/types" ) func (s *CacheStore) GetFloat32(key string) (float32, error) { - if v, err := s.getNum32(key, types.FLOAT32); err != nil { - return 0, err - } else { - return math.Float32frombits(v), nil - } + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data float32, err error) { + data, err = e.AsFloat32() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetFloat32(key string, value float32, exp time.Duration) error { @@ -31,11 +34,14 @@ func (s *CacheStore) IncrFloat32(key string, delta float32, exp time.Duration) e } func (s *CacheStore) GetFloat64(key string) (float64, error) { - if v, err := s.getNum64(key, types.FLOAT64); err != nil { - return 0, err - } else { - return math.Float64frombits(v), nil - } + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data float64, err error) { + data, err = e.AsFloat64() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetFloat64(key string, value float64, exp time.Duration) error { diff --git a/store/type_integer.go b/store/type_integer.go index ff0c583..7dc61ed 100644 --- a/store/type_integer.go +++ b/store/type_integer.go @@ -3,16 +3,20 @@ package store import ( "time" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/utils" "github.com/found-cake/CacheStore/utils/types" ) func (s *CacheStore) GetInt16(key string) (int16, error) { - if v, err := s.getNum16(key, types.INT16); err != nil { - return 0, err - } else { - return int16(v), nil - } + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data int16, err error) { + data, err = e.AsInt16() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetInt16(key string, value int16, exp time.Duration) error { @@ -30,11 +34,14 @@ func (s *CacheStore) IncrInt16(key string, delta int16, exp time.Duration) error } func (s *CacheStore) GetInt32(key string) (int32, error) { - if v, err := s.getNum32(key, types.INT32); err != nil { - return 0, err - } else { - return int32(v), nil - } + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data int32, err error) { + data, err = e.AsInt32() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetInt32(key string, value int32, exp time.Duration) error { @@ -52,11 +59,14 @@ func (s *CacheStore) IncrInt32(key string, delta int32, exp time.Duration) error } func (s *CacheStore) GetInt64(key string) (int64, error) { - if v, err := s.getNum64(key, types.INT64); err != nil { - return 0, err - } else { - return int64(v), nil - } + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data int64, err error) { + data, err = e.AsInt64() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetInt64(key string, value int64, exp time.Duration) error { diff --git a/store/type_json.go b/store/type_json.go index 1a9c1a1..4d3a185 100644 --- a/store/type_json.go +++ b/store/type_json.go @@ -1,36 +1,30 @@ package store import ( - "encoding/json" "time" - "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/utils/types" ) func (s *CacheStore) GetJSON(key string, target interface{}) error { - if key == "" { - return errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return err - } - if e.Type != types.JSON { - return errors.ErrTypeMismatch(types.JSON, e.Type) - } - if len(e.Data) == 0 { - return errors.ErrNoDataForKey(key) - } - return json.Unmarshal(e.Data, target) + _, _, err := get(s, key, func(e *entry.Entry) (types.DataType, struct{}, error) { + err := e.AsJSON(target) + if err != nil { + return types.UNKNOWN, struct{}{}, err + } + + return e.Type, struct{}{}, nil + }) + return err } func (s *CacheStore) SetJSON(key string, value interface{}, exp time.Duration) error { - if data, err := json.Marshal(value); err != nil { - return err - } else { - return s.Set(key, types.JSON, data, exp) - } + return s.WriteTransaction(func(tx *WriteTransaction) error { + if e, err := entry.FromJSON(value, exp); err == nil { + return tx.Set(key, e) + } else { + return err + } + }) } diff --git a/store/type_raw.go b/store/type_raw.go index f926c69..fcb0183 100644 --- a/store/type_raw.go +++ b/store/type_raw.go @@ -3,47 +3,34 @@ package store import ( "time" - "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/utils/types" ) func (s *CacheStore) GetRaw(key string) ([]byte, error) { - if key == "" { - return nil, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return nil, err - } - if e.Type != types.RAW { - return nil, errors.ErrTypeMismatch(types.RAW, e.Type) - } - - result := make([]byte, len(e.Data)) - copy(result, e.Data) - - return result, nil + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data []byte, err error) { + data, err = e.AsRaw() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) GetRawNoCopy(key string) ([]byte, error) { - if key == "" { - return nil, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return nil, err - } - if e.Type != types.RAW { - return nil, errors.ErrTypeMismatch(types.RAW, e.Type) - } - - return e.Data, nil + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data []byte, err error) { + data, err = e.AsRawNoCopy() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetRaw(key string, value []byte, exp time.Duration) error { - return s.Set(key, types.RAW, value, exp) + return s.WriteTransaction(func(tx *WriteTransaction) error { + return tx.Set(key, entry.FromRaw(value, exp)) + }) } diff --git a/store/type_string.go b/store/type_string.go index e5685cb..60d4c01 100644 --- a/store/type_string.go +++ b/store/type_string.go @@ -3,26 +3,23 @@ package store import ( "time" - "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/utils/types" ) func (s *CacheStore) GetString(key string) (string, error) { - if key == "" { - return "", errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return "", err - } - if e.Type != types.STRING { - return "", errors.ErrTypeMismatch(types.STRING, e.Type) - } - return string(e.Data), nil + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data string, err error) { + data, err = e.AsString() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetString(key string, value string, exp time.Duration) error { - return s.Set(key, types.STRING, []byte(value), exp) + return s.WriteTransaction(func(tx *WriteTransaction) error { + return tx.Set(key, entry.FromString(value, exp)) + }) } diff --git a/store/type_time.go b/store/type_time.go index 4f5ef02..f6d7268 100644 --- a/store/type_time.go +++ b/store/type_time.go @@ -3,35 +3,27 @@ package store import ( "time" - "github.com/found-cake/CacheStore/errors" + "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/utils/types" ) func (s *CacheStore) GetTime(key string) (time.Time, error) { - var t time.Time - if key == "" { - return t, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return t, err - } - if e.Type != types.TIME { - return t, errors.ErrTypeMismatch(types.TIME, e.Type) - } - if len(e.Data) == 0 { - return t, errors.ErrNoDataForKey(key) - } - err = t.UnmarshalBinary(e.Data) - return t, err + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data time.Time, err error) { + data, err = e.AsTime() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetTime(key string, value time.Time, exp time.Duration) error { - if b, err := value.MarshalBinary(); err != nil { - return err - } else { - return s.Set(key, types.TIME, b, exp) - } + return s.WriteTransaction(func(tx *WriteTransaction) error { + if e, err := entry.FromTime(value, exp); err == nil { + return tx.Set(key, e) + } else { + return err + } + }) } diff --git a/store/type_uinteger.go b/store/type_uinteger.go index bcf287f..769f288 100644 --- a/store/type_uinteger.go +++ b/store/type_uinteger.go @@ -11,7 +11,14 @@ import ( ) func (s *CacheStore) GetUInt16(key string) (uint16, error) { - return s.getNum16(key, types.UINT16) + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data uint16, err error) { + data, err = e.AsUInt16() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetUInt16(key string, value uint16, exp time.Duration) error { @@ -38,7 +45,14 @@ func (s *CacheStore) DecrUInt16(key string, delta uint16, exp time.Duration) err } func (s *CacheStore) GetUInt32(key string) (uint32, error) { - return s.getNum32(key, types.UINT32) + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data uint32, err error) { + data, err = e.AsUInt32() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetUInt32(key string, value uint32, exp time.Duration) error { @@ -65,7 +79,14 @@ func (s *CacheStore) DecrUInt32(key string, delta uint32, exp time.Duration) err } func (s *CacheStore) GetUInt64(key string) (uint64, error) { - return s.getNum64(key, types.UINT64) + _, data, err := get(s, key, func(e *entry.Entry) (t types.DataType, data uint64, err error) { + data, err = e.AsUInt64() + if err == nil { + t = e.Type + } + return + }) + return data, err } func (s *CacheStore) SetUInt64(key string, value uint64, exp time.Duration) error { diff --git a/store/utils_number.go b/store/utils_number.go index 24f1aa7..1c4695c 100644 --- a/store/utils_number.go +++ b/store/utils_number.go @@ -5,70 +5,10 @@ import ( "github.com/found-cake/CacheStore/entry" "github.com/found-cake/CacheStore/errors" - "github.com/found-cake/CacheStore/utils" "github.com/found-cake/CacheStore/utils/generic" "github.com/found-cake/CacheStore/utils/types" ) -func (s *CacheStore) setKeepExp(key string, dataType types.DataType, value []byte, expiry int64) { - s.memorydbTemporary[key] = entry.Entry{ - Type: dataType, - Data: value, - Expiry: expiry, - } - if s.dirty != nil { - s.dirty.set(key) - } -} - -func (s *CacheStore) getNum16(key string, expected types.DataType) (uint16, error) { - if key == "" { - return 0, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return 0, err - } - if e.Type != expected { - return 0, errors.ErrTypeMismatch(expected, e.Type) - } - return utils.Binary2UInt16(e.Data) -} - -func (s *CacheStore) getNum32(key string, expected types.DataType) (uint32, error) { - if key == "" { - return 0, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return 0, err - } - if e.Type != expected { - return 0, errors.ErrTypeMismatch(expected, e.Type) - } - return utils.Binary2UInt32(e.Data) -} - -func (s *CacheStore) getNum64(key string, expected types.DataType) (uint64, error) { - if key == "" { - return 0, errors.ErrKeyEmpty - } - s.temporaryMux.RLock() - defer s.temporaryMux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return 0, err - } - if e.Type != expected { - return 0, errors.ErrTypeMismatch(expected, e.Type) - } - return utils.Binary2UInt64(e.Data) -} - func incrNumber[T generic.Numberic]( s *CacheStore, key string, @@ -105,7 +45,7 @@ func incrNumber[T generic.Numberic]( if checkFloatSpesial != nil && checkFloatSpesial(value) { return errors.ErrFloatSpecial } - s.Set(key, data_type, data, exp) + tx.Set(key, entry.NewEntry(data_type, data, exp)) return nil }) } From 6a977d599f4c8dc76992c936870ed725554b4c10 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 17 Aug 2025 04:24:42 +0000 Subject: [PATCH 18/20] refactor: sqliter load data for split db --- sqlite/sqlite.go | 20 ++++++++++++++------ sqlite/sqlite_test.go | 12 ++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index 7847604..2c475d5 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -52,18 +52,19 @@ func NewSqliteStore(filename string) (*SqliteStore, error) { }, nil } -func (s *SqliteStore) LoadFromDB() (map[string]entry.Entry, error) { +func (s *SqliteStore) LoadFromDB() (map[string]entry.Entry, map[string]entry.Entry, error) { if s.db == nil { - return nil, errors.ErrDBNotInit + return nil, nil, errors.ErrDBNotInit } rows, err := s.db.Query("SELECT key, data_type, data, expiry FROM cache_data") if err != nil { - return nil, err + return nil, nil, err } defer rows.Close() - dbData := make(map[string]entry.Entry) + tempdb := make(map[string]entry.Entry) + persidb := make(map[string]entry.Entry) now := time.Now().UnixMilli() for rows.Next() { var key string @@ -76,18 +77,25 @@ func (s *SqliteStore) LoadFromDB() (map[string]entry.Entry, error) { continue } + if expiry == 0 { + persidb[key] = entry.Entry{ + Type: dataType, + Data: data, + } + } + if expiry > 0 && expiry <= now { continue } - dbData[key] = entry.Entry{ + tempdb[key] = entry.Entry{ Type: dataType, Data: data, Expiry: expiry, } } - return dbData, nil + return tempdb, persidb, nil } func (s *SqliteStore) SaveDirtyData(set_dirtys map[string]entry.Entry, delete_dirtys []string) error { diff --git a/sqlite/sqlite_test.go b/sqlite/sqlite_test.go index 70421b7..4d55992 100644 --- a/sqlite/sqlite_test.go +++ b/sqlite/sqlite_test.go @@ -28,7 +28,7 @@ func TestNewSqliteStore_InvalidFileName(t *testing.T) { func TestNewSqliteStore_NoDBInit(t *testing.T) { s := &SqliteStore{} - _, err := s.LoadFromDB() + _, _, err := s.LoadFromDB() if err == nil { t.Error("expected error for not initialized sql") } @@ -66,7 +66,7 @@ func TestSqliteStore_Save_ForceLock(t *testing.T) { store.mux.Unlock() <-unlocked - loaded, err := store.LoadFromDB() + loaded, _, err := store.LoadFromDB() if err != nil { t.Fatalf("load error: %v", err) } @@ -102,7 +102,7 @@ func TestSqliteStore_SaveDirtyData(t *testing.T) { t.Errorf("force lock Save failed: %v", err) } - loaded, err := store.LoadFromDB() + loaded, _, err := store.LoadFromDB() if err != nil { t.Fatalf("load error: %v", err) } @@ -116,7 +116,7 @@ func TestSqliteStore_SaveDirtyData(t *testing.T) { store.SaveDirtyData(dirtyData, []string{"foo"}) - loaded, err = store.LoadFromDB() + loaded, _, err = store.LoadFromDB() if err != nil { t.Fatalf("load error: %v", err) } @@ -154,7 +154,7 @@ func TestSqliteStore_Load(t *testing.T) { if err != nil { t.Errorf("force lock Save failed: %v", err) } - loaded, err := store.LoadFromDB() + loaded, _, err := store.LoadFromDB() if err != nil { t.Fatalf("load error: %v", err) } @@ -177,7 +177,7 @@ func TestSqliteStore_Load_FilterExpired(t *testing.T) { t.Errorf("force lock Save failed: %v", err) } time.Sleep(200 * time.Millisecond) - loaded, err := store.LoadFromDB() + loaded, _, err := store.LoadFromDB() if err != nil { t.Fatalf("load error: %v", err) } From 55b5df52689ccbeadf5309e39fa313f80f623fa1 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 17 Aug 2025 04:25:04 +0000 Subject: [PATCH 19/20] refactor: store constructor --- store/cache_store.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/store/cache_store.go b/store/cache_store.go index 848bbfa..369f5bd 100644 --- a/store/cache_store.go +++ b/store/cache_store.go @@ -11,19 +11,21 @@ import ( func NewCacheStore(cfg config.Config) (*CacheStore, error) { store := &CacheStore{ - memorydbTemporary: make(map[string]entry.Entry), - done: make(chan struct{}), + memorydbPersistent: make(map[string]entry.Entry), + memorydbTemporary: make(map[string]entry.Entry), + done: make(chan struct{}), } if cfg.DBSave { sqlitedb, err := sqlite.NewSqliteStore(cfg.DBFileName) if err != nil { return nil, err } - data, err := sqlitedb.LoadFromDB() + temp, persi, err := sqlitedb.LoadFromDB() if err != nil { return nil, err } - store.memorydbTemporary = data + store.memorydbPersistent = persi + store.memorydbTemporary = temp store.sqlitedb = sqlitedb if cfg.SaveDirtyData { if cfg.DirtyThresholdCount <= 0 { From 71afae3cf8d022e2bd0a53336f25b691e995b136 Mon Sep 17 00:00:00 2001 From: found-cake Date: Sun, 17 Aug 2025 04:31:21 +0000 Subject: [PATCH 20/20] fix: dead lock --- store/store_core.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/store/store_core.go b/store/store_core.go index 17948d7..6e4dbd3 100644 --- a/store/store_core.go +++ b/store/store_core.go @@ -228,11 +228,13 @@ func (s *CacheStore) Keys() []string { } func (s *CacheStore) TTL(key string) time.Duration { - s.persistentMux.RLock() - if _, ok := s.memorydbPersistent[key]; ok { - return TTLNoExpiry + { + s.persistentMux.RLock() + defer s.persistentMux.RUnlock() + if _, ok := s.memorydbPersistent[key]; ok { + return TTLNoExpiry + } } - s.persistentMux.RUnlock() s.temporaryMux.RLock() defer s.temporaryMux.RUnlock()