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_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..b608f2f --- /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() (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() (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..6269686 --- /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) AsInt16() (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) AsInt32() (int32, error) { + if e.Type != types.INT32 { + return 0, errors.ErrTypeMismatch(types.INT32, e.Type) + } + return utils.Binary2Int32(e.Data) +} + +func FromInt32(key string, value int32, exp time.Duration) Entry { + return NewEntry(types.INT32, utils.Int32toBinary(value), exp) +} + +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 FromInt64(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..97710ab --- /dev/null +++ b/entry/type_raw.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) AsRaw() ([]byte, error) { + if e.Type != types.RAW { + return nil, errors.ErrTypeMismatch(types.RAW, e.Type) + } + + return e.CopyData(), nil +} + +func (e *Entry) AsRawNoCopy() ([]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..43b4773 --- /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() (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() (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() (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 aba7742..c3c56a0 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -11,6 +11,10 @@ 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") ErrValueNil = errors.New("value cannot be null") ErrDBNotInit = errors.New("database not initialized") ErrFileNameEmpty = errors.New("filename cannot be empty") @@ -28,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/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) } diff --git a/store/batch_operations.go b/store/batch_operations.go deleted file mode 100644 index 595d12d..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.mux.RLock() - defer s.mux.RUnlock() - - for i, key := range keys { - results[i].Key = key - if key == "" { - results[i].Error = errors.ErrKeyEmpty - continue - } - if e, ok := s.memorydb[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.mux.Lock() - defer s.mux.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.memorydb[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.mux.Lock() - defer s.mux.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.memorydb, 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 850d1c7..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.memorydb) - 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) - } - } -} 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..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{ - memorydb: 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.memorydb = data + store.memorydbPersistent = persi + store.memorydbTemporary = temp store.sqlitedb = sqlitedb if cfg.SaveDirtyData { if cfg.DirtyThresholdCount <= 0 { diff --git a/store/read_tx.go b/store/read_tx.go new file mode 100644 index 0000000..eace071 --- /dev/null +++ b/store/read_tx.go @@ -0,0 +1,28 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" +) + +type ReadTransactionFunc func(tx ReadTransaction) error + +type ReadTransaction interface { + Get(key string) (*entry.Entry, 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..2e87630 --- /dev/null +++ b/store/read_tx_lock.go @@ -0,0 +1,89 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" +) + +type LockReadTransaction struct { + parent *CacheStore +} + +func (s *CacheStore) lockReadTx(fn ReadTransactionFunc) error { + tx := &LockReadTransaction{ + parent: s, + } + + s.persistentMux.RLock() + s.temporaryMux.RLock() + defer func() { + s.persistentMux.RUnlock() + s.temporaryMux.RUnlock() + }() + + return fn(tx) +} + +func (tx *LockReadTransaction) Get(key string) (*entry.Entry, error) { + if key == "" { + return nil, errors.ErrKeyEmpty + } + + e, ok := tx.parent.memorydbPersistent[key] + if ok { + return &e, nil + } + + e, ok = tx.parent.memorydbTemporary[key] + if ok { + if e.IsExpired() { + return nil, errors.ErrNoDataForKey(key) + } else { + return &e, nil + } + } + + return nil, errors.ErrNoDataForKey(key) +} + +func (tx *LockReadTransaction) Exists(keys ...string) int { + if 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 { + _, 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..3da7bbd --- /dev/null +++ b/store/read_tx_snapshot.go @@ -0,0 +1,110 @@ +package store + +import ( + "time" + + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" +) + +type SnapshotReadTransaction struct { + memorydb map[string]entry.Entry +} + +func newSnapshotReadTX(s *CacheStore) *SnapshotReadTransaction { + s.persistentMux.RLock() + s.temporaryMux.RLock() + + tx := &SnapshotReadTransaction{ + 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.memorydb[key] = entry.Entry{ + Type: e.Type, + Data: dataCopy, + Expiry: e.Expiry, + } + } + + for key, e := range s.memorydbTemporary { + if e.IsExpired() { + continue + } + dataCopy := make([]byte, len(e.Data)) + copy(dataCopy, e.Data) + + tx.memorydb[key] = entry.Entry{ + Type: e.Type, + Data: dataCopy, + Expiry: e.Expiry, + } + } + + s.persistentMux.RUnlock() + s.temporaryMux.RUnlock() + + return tx +} + +func (s *CacheStore) snapshotReadTx(fn ReadTransactionFunc) error { + tx := newSnapshotReadTX(s) + return fn(tx) +} + +func (tx *SnapshotReadTransaction) Get(key string) (*entry.Entry, error) { + if key == "" { + return nil, errors.ErrKeyEmpty + } + + e, ok := tx.memorydb[key] + if !ok { + return nil, errors.ErrNoDataForKey(key) + } + if e.IsExpired() { + return nil, errors.ErrNoDataForKey(key) + } + + return &e, nil +} + +func (tx *SnapshotReadTransaction) Exists(keys ...string) int { + if len(keys) == 0 { + return 0 + } + + count := 0 + now := time.Now().UnixMilli() + + for _, key := range keys { + if entry, exists := tx.memorydb[key]; exists { + if !entry.IsExpiredWithUnixMilli(now) { + count++ + } + } + } + + return count +} + +func (tx *SnapshotReadTransaction) TTL(key string) time.Duration { + e, ok := tx.memorydb[key] + if !ok { + return TTLExpired + } + + if e.Expiry == 0 { + return TTLNoExpiry + } + + now := time.Now().UnixMilli() + if now >= e.Expiry { + return TTLExpired + } + + remaining := time.Duration(e.Expiry-now) * time.Millisecond + return remaining +} diff --git a/store/rw_tx.go b/store/rw_tx.go new file mode 100644 index 0000000..fb59771 --- /dev/null +++ b/store/rw_tx.go @@ -0,0 +1,27 @@ +package store + +import ( + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" +) + +type RWTransactionFunc func(tx RWTransaction) error + +type RWTransaction interface { + ReadTransaction + Set(key string, entry entry.Entry) 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..32000b7 --- /dev/null +++ b/store/rw_tx_lock.go @@ -0,0 +1,74 @@ +package store + +import ( + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" +) + +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, e entry.Entry) error { + if key == "" { + return errors.ErrKeyEmpty + } + if e.Data == nil { + return errors.ErrValueNil + } + + if e.Expiry <= 0 { + tx.parent.memorydbPersistent[key] = e + delete(tx.parent.memorydbTemporary, key) + } else { + tx.parent.memorydbTemporary[key] = e + 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..4ee7308 --- /dev/null +++ b/store/rw_tx_snapshot.go @@ -0,0 +1,113 @@ +package store + +import ( + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" +) + +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, e entry.Entry) error { + if key == "" { + return errors.ErrKeyEmpty + } + if e.Data == nil { + return errors.ErrValueNil + } + + if e.Expiry <= 0 { + tx.pendingPersistent[key] = &e + tx.pendingTemporary[key] = nil + } else { + 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 +} diff --git a/store/store_core.go b/store/store_core.go index 16a17ff..6e4dbd3 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 + memorydbPersistent map[string]entry.Entry + temporaryMux sync.RWMutex + memorydbTemporary map[string]entry.Entry + dirty *dirtyManager + sqlitedb *sqlite.SqliteStore + done chan struct{} + wg sync.WaitGroup + closed atomic.Bool } const ( @@ -30,41 +32,51 @@ 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] +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.mux.RLock() - defer s.mux.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. @@ -78,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.mux.RLock() - defer s.mux.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.memorydb[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 { @@ -106,16 +102,9 @@ func (s *CacheStore) Set(key string, dataType types.DataType, value []byte, expi if value == nil { return errors.ErrValueNil } - - s.mux.Lock() - s.memorydb[key] = entry.NewEntry(dataType, value, expiry) - s.mux.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 { @@ -123,9 +112,13 @@ func (s *CacheStore) Delete(key string) error { return errors.ErrKeyEmpty } - s.mux.Lock() - delete(s.memorydb, key) - s.mux.Unlock() + s.persistentMux.Lock() + delete(s.memorydbPersistent, key) + s.persistentMux.Unlock() + + s.temporaryMux.Lock() + delete(s.memorydbTemporary, key) + s.temporaryMux.Unlock() if s.dirty != nil { s.dirty.delete(key) @@ -135,9 +128,13 @@ 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.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() if s.dirty != nil { s.dirty.wantFullSync() } @@ -164,10 +161,21 @@ func (s *CacheStore) Close() error { log.Println(err) } }() - err = s.sqlitedb.Save(s.memorydb, true) + 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.memorydb = nil + s.memorydbPersistent = nil + s.memorydbTemporary = nil s.dirty = nil return err @@ -177,11 +185,19 @@ func (s *CacheStore) Exists(keys ...string) int { now := time.Now().UnixMilli() count := 0 - s.mux.RLock() - defer s.mux.RUnlock() + s.persistentMux.RLock() + for _, key := range keys { + if _, ok := s.memorydbPersistent[key]; ok { + count++ + } + } + s.persistentMux.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++ } @@ -191,12 +207,19 @@ func (s *CacheStore) Exists(keys ...string) int { } func (s *CacheStore) Keys() []string { + 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() - s.mux.RLock() - defer s.mux.RUnlock() + defer s.temporaryMux.RUnlock() - keys := make([]string, 0, len(s.memorydb)) - for key, e := range s.memorydb { + for key, e := range s.memorydbTemporary { if !e.IsExpiredWithUnixMilli(now) { keys = append(keys, key) } @@ -205,10 +228,18 @@ func (s *CacheStore) Keys() []string { } func (s *CacheStore) TTL(key string) time.Duration { - s.mux.RLock() - defer s.mux.RUnlock() + { + s.persistentMux.RLock() + defer s.persistentMux.RUnlock() + if _, ok := s.memorydbPersistent[key]; ok { + return TTLNoExpiry + } + } - e, ok := s.memorydb[key] + s.temporaryMux.RLock() + defer s.temporaryMux.RUnlock() + + e, ok := s.memorydbTemporary[key] if !ok { return TTLExpired } @@ -249,9 +280,11 @@ 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.persistentMux.RLock() + s.temporaryMux.RLock() + 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() return @@ -260,19 +293,24 @@ 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 { - dataCopy := make([]byte, len(e.Data)) - copy(dataCopy, e.Data) - + 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 { new_data[key] = entry.Entry{ Type: e.Type, - Data: dataCopy, + Data: e.CopyData(), Expiry: e.Expiry, } } } - s.mux.RUnlock() + s.persistentMux.RUnlock() + s.temporaryMux.RUnlock() s.dirty.unsafeClear() s.dirty.mux.Unlock() @@ -290,27 +328,14 @@ func (s *CacheStore) FullSync() { return } - s.mux.RLock() - snapshot := make(map[string]entry.Entry, len(s.memorydb)) - for key, e := range s.memorydb { - dataCopy := make([]byte, len(e.Data)) - copy(dataCopy, e.Data) - - snapshot[key] = entry.Entry{ - Type: e.Type, - Data: dataCopy, - Expiry: e.Expiry, - } - } - s.mux.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 efc7174..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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return false, err - } - if e.Type != types.BOOLEAN { - return false, errors.ErrTypeMismatch(key, 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 548f1a2..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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return err - } - if e.Type != types.JSON { - return errors.ErrTypeMismatch(key, 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 f6184c8..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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return nil, err - } - if e.Type != types.RAW { - return nil, errors.ErrTypeMismatch(key, 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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return nil, err - } - if e.Type != types.RAW { - return nil, errors.ErrTypeMismatch(key, 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 768157c..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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return "", err - } - if e.Type != types.STRING { - return "", errors.ErrTypeMismatch(key, 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 7bd157f..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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return t, err - } - if e.Type != types.TIME { - return t, errors.ErrTypeMismatch(key, 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 396a245..769f288 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" @@ -10,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 { @@ -37,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 { @@ -64,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 { @@ -97,28 +119,24 @@ func decrUnsigned[T generic.Unsigned]( if key == "" { return errors.ErrKeyEmpty } - s.mux.Lock() - defer s.mux.Unlock() - e, err := s.unsafeGet(key) - if err != nil { - return errors.ErrNoDataForKey(key) - } - if e.Type != data_type { - return errors.ErrTypeMismatch(key, 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 6bb1b17..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.memorydb[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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return 0, err - } - if e.Type != expected { - return 0, errors.ErrTypeMismatch(key, 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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return 0, err - } - if e.Type != expected { - return 0, errors.ErrTypeMismatch(key, 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.mux.RLock() - defer s.mux.RUnlock() - e, err := s.unsafeGet(key) - if err != nil { - return 0, err - } - if e.Type != expected { - return 0, errors.ErrTypeMismatch(key, expected, e.Type) - } - return utils.Binary2UInt64(e.Data) -} - func incrNumber[T generic.Numberic]( s *CacheStore, key string, @@ -83,33 +23,29 @@ func incrNumber[T generic.Numberic]( if key == "" { return errors.ErrKeyEmpty } - s.mux.Lock() - defer s.mux.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 + } + tx.Set(key, entry.NewEntry(data_type, data, exp)) return nil - } - if e.Type != data_type { - return errors.ErrTypeMismatch(key, 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 + }) } diff --git a/store/write_tx.go b/store/write_tx.go new file mode 100644 index 0000000..078d672 --- /dev/null +++ b/store/write_tx.go @@ -0,0 +1,115 @@ +package store + +import ( + "github.com/found-cake/CacheStore/entry" + "github.com/found-cake/CacheStore/errors" +) + +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, e entry.Entry) error { + if key == "" { + return errors.ErrKeyEmpty + } + if e.Data == nil { + return errors.ErrValueNil + } + + if e.Expiry <= 0 { + tx.pendingPersistent[key] = &e + tx.pendingTemporary[key] = nil + } else { + tx.pendingTemporary[key] = &e + 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 +}