Added multi-database support to snapshot module

This commit is contained in:
Kelvin Mwinuka
2024-06-29 23:31:55 +08:00
parent 56f0a5ce61
commit 182195ebc3
6 changed files with 307 additions and 278 deletions

View File

@@ -250,34 +250,36 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) {
ApplyDeleteKey: echovault.raftApplyDeleteKey, ApplyDeleteKey: echovault.raftApplyDeleteKey,
}) })
} else { } else {
// TODO: Update snapshot engine to support multiple databases.
// Set up standalone snapshot engine // Set up standalone snapshot engine
// echovault.snapshotEngine = snapshot.NewSnapshotEngine( echovault.snapshotEngine = snapshot.NewSnapshotEngine(
// snapshot.WithClock(echovault.clock), snapshot.WithClock(echovault.clock),
// snapshot.WithDirectory(echovault.config.DataDir), snapshot.WithDirectory(echovault.config.DataDir),
// snapshot.WithThreshold(echovault.config.SnapShotThreshold), snapshot.WithThreshold(echovault.config.SnapShotThreshold),
// snapshot.WithInterval(echovault.config.SnapshotInterval), snapshot.WithInterval(echovault.config.SnapshotInterval),
// snapshot.WithStartSnapshotFunc(echovault.startSnapshot), snapshot.WithStartSnapshotFunc(echovault.startSnapshot),
// snapshot.WithFinishSnapshotFunc(echovault.finishSnapshot), snapshot.WithFinishSnapshotFunc(echovault.finishSnapshot),
// snapshot.WithSetLatestSnapshotTimeFunc(echovault.setLatestSnapshot), snapshot.WithSetLatestSnapshotTimeFunc(echovault.setLatestSnapshot),
// snapshot.WithGetLatestSnapshotTimeFunc(echovault.getLatestSnapshotTime), snapshot.WithGetLatestSnapshotTimeFunc(echovault.getLatestSnapshotTime),
// snapshot.WithGetStateFunc(func() map[string]internal.KeyData { snapshot.WithGetStateFunc(func() map[int]map[string]internal.KeyData {
// state := make(map[string]internal.KeyData) state := make(map[int]map[string]internal.KeyData)
// for k, v := range echovault.getState() { for database, data := range echovault.getState() {
// if data, ok := v.(internal.KeyData); ok { state[database] = make(map[string]internal.KeyData)
// state[k] = data for key, value := range data {
// } if keyData, ok := value.(internal.KeyData); ok {
// } state[database][key] = keyData
// return state }
// }), }
// snapshot.WithSetKeyDataFunc(func(key string, data internal.KeyData) { }
// ctx := context.Background() return state
// if err := echovault.setValues(ctx, map[string]interface{}{key: data.Value}); err != nil { }),
// log.Println(err) snapshot.WithSetKeyDataFunc(func(database int, key string, data internal.KeyData) {
// } ctx := context.WithValue(context.Background(), "Database", database)
// echovault.setExpiry(ctx, key, data.ExpireAt, false) if err := echovault.setValues(ctx, map[string]interface{}{key: data.Value}); err != nil {
// }), log.Println(err)
// ) }
echovault.setExpiry(ctx, key, data.ExpireAt, false)
}),
)
// TODO: Update AOF engine to support multiple databases. // TODO: Update AOF engine to support multiple databases.
// Set up standalone AOF engine // Set up standalone AOF engine

View File

@@ -883,7 +883,7 @@ func Test_Standalone(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
dataDir string dataDir string
values map[string]string values map[int]map[string]string
snapshotFunc func(mockServer *EchoVault) error snapshotFunc func(mockServer *EchoVault) error
lastSaveFunc func(mockServer *EchoVault) (int, error) lastSaveFunc func(mockServer *EchoVault) (int, error)
wantLastSave int wantLastSave int
@@ -891,11 +891,9 @@ func Test_Standalone(t *testing.T) {
{ {
name: "1. Snapshot in embedded instance", name: "1. Snapshot in embedded instance",
dataDir: path.Join(dataDir, "embedded_instance"), dataDir: path.Join(dataDir, "embedded_instance"),
values: map[string]string{ values: map[int]map[string]string{
"key5": "value5", 0: {"key5": "value-05", "key6": "value-06", "key7": "value-07", "key8": "value-08"},
"key6": "value6", 1: {"key5": "value-15", "key6": "value-16", "key7": "value-17", "key8": "value-18"},
"key7": "value7",
"key8": "value8",
}, },
snapshotFunc: func(mockServer *EchoVault) error { snapshotFunc: func(mockServer *EchoVault) error {
if _, err := mockServer.Save(); err != nil { if _, err := mockServer.Save(); err != nil {
@@ -937,10 +935,13 @@ func Test_Standalone(t *testing.T) {
}() }()
// Trigger some write commands // Trigger some write commands
for key, value := range test.values { for database, data := range test.values {
if _, _, err = mockServer.Set(key, value, SetOptions{}); err != nil { _ = mockServer.SelectDB(database)
t.Error(err) for key, value := range data {
return if _, _, err = mockServer.Set(key, value, SetOptions{}); err != nil {
t.Error(err)
return
}
} }
} }
@@ -962,15 +963,18 @@ func Test_Standalone(t *testing.T) {
} }
// Check that all the key/value pairs have been restored into the store. // Check that all the key/value pairs have been restored into the store.
for key, value := range test.values { for database, data := range test.values {
res, err := mockServer.Get(key) _ = mockServer.SelectDB(database)
if err != nil { for key, value := range data {
t.Error(err) res, err := mockServer.Get(key)
return if err != nil {
} t.Error(err)
if res != value { return
t.Errorf("expected value at key \"%s\" to be \"%s\", got \"%s\"", key, value, res) }
return if res != value {
t.Errorf("expected value at key \"%s\" to be \"%s\", got \"%s\"", key, value, res)
return
}
} }
} }

View File

@@ -215,8 +215,7 @@ func (server *EchoVault) setValues(ctx context.Context, entries map[string]inter
ExpireAt: expireAt, ExpireAt: expireAt,
} }
if !server.isInCluster() { if !server.isInCluster() {
// TODO: Enable this when snapshot engine has support for multiple databases. server.snapshotEngine.IncrementChangeCount()
// server.snapshotEngine.IncrementChangeCount()
} }
} }
@@ -318,6 +317,7 @@ func (server *EchoVault) getState() map[int]map[string]interface{} {
} }
data := make(map[int]map[string]interface{}) data := make(map[int]map[string]interface{})
for db, store := range server.store { for db, store := range server.store {
data[db] = make(map[string]interface{})
for k, v := range store { for k, v := range store {
data[db][k] = v data[db][k] = v
} }

View File

@@ -25,7 +25,7 @@ func createEchoVaultWithConfig(conf config.Config) *EchoVault {
} }
func presetValue(server *EchoVault, ctx context.Context, key string, value interface{}) error { func presetValue(server *EchoVault, ctx context.Context, key string, value interface{}) error {
ctx = context.WithValue(ctx, "Database", "0") ctx = context.WithValue(ctx, "Database", 0)
if err := server.setValues(ctx, map[string]interface{}{key: value}); err != nil { if err := server.setValues(ctx, map[string]interface{}{key: value}); err != nil {
return err return err
} }
@@ -33,7 +33,7 @@ func presetValue(server *EchoVault, ctx context.Context, key string, value inter
} }
func presetKeyData(server *EchoVault, ctx context.Context, key string, data internal.KeyData) { func presetKeyData(server *EchoVault, ctx context.Context, key string, data internal.KeyData) {
ctx = context.WithValue(ctx, "Database", "0") ctx = context.WithValue(ctx, "Database", 0)
_ = server.setValues(ctx, map[string]interface{}{key: data.Value}) _ = server.setValues(ctx, map[string]interface{}{key: data.Value})
server.setExpiry(ctx, key, data.ExpireAt, false) server.setExpiry(ctx, key, data.ExpireAt, false)
} }

View File

@@ -15,9 +15,17 @@
package snapshot package snapshot
import ( import (
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/clock" "github.com/echovault/echovault/internal/clock"
"io"
"io/fs"
"log" "log"
"os"
"path"
"sync/atomic" "sync/atomic"
"time" "time"
) )
@@ -38,10 +46,10 @@ type Engine struct {
snapshotThreshold uint64 snapshotThreshold uint64
startSnapshotFunc func() startSnapshotFunc func()
finishSnapshotFunc func() finishSnapshotFunc func()
getStateFunc func() map[string]internal.KeyData getStateFunc func() map[int]map[string]internal.KeyData
setLatestSnapshotTimeFunc func(msec int64) setLatestSnapshotTimeFunc func(msec int64)
getLatestSnapshotTimeFunc func() int64 getLatestSnapshotTimeFunc func() int64
setKeyDataFunc func(key string, data internal.KeyData) setKeyDataFunc func(database int, key string, data internal.KeyData)
} }
func WithClock(clock clock.Clock) func(engine *Engine) { func WithClock(clock clock.Clock) func(engine *Engine) {
@@ -80,7 +88,7 @@ func WithFinishSnapshotFunc(f func()) func(engine *Engine) {
} }
} }
func WithGetStateFunc(f func() map[string]internal.KeyData) func(engine *Engine) { func WithGetStateFunc(f func() map[int]map[string]internal.KeyData) func(engine *Engine) {
return func(engine *Engine) { return func(engine *Engine) {
engine.getStateFunc = f engine.getStateFunc = f
} }
@@ -98,7 +106,7 @@ func WithGetLatestSnapshotTimeFunc(f func() int64) func(engine *Engine) {
} }
} }
func WithSetKeyDataFunc(f func(key string, data internal.KeyData)) func(engine *Engine) { func WithSetKeyDataFunc(f func(database int, key string, data internal.KeyData)) func(engine *Engine) {
return func(engine *Engine) { return func(engine *Engine) {
engine.setKeyDataFunc = f engine.setKeyDataFunc = f
} }
@@ -113,10 +121,10 @@ func NewSnapshotEngine(options ...func(engine *Engine)) *Engine {
snapshotThreshold: 1000, snapshotThreshold: 1000,
startSnapshotFunc: func() {}, startSnapshotFunc: func() {},
finishSnapshotFunc: func() {}, finishSnapshotFunc: func() {},
getStateFunc: func() map[string]internal.KeyData { getStateFunc: func() map[int]map[string]internal.KeyData {
return map[string]internal.KeyData{} return make(map[int]map[string]internal.KeyData)
}, },
setKeyDataFunc: func(key string, data internal.KeyData) {}, setKeyDataFunc: func(database int, key string, data internal.KeyData) {},
setLatestSnapshotTimeFunc: func(msec int64) {}, setLatestSnapshotTimeFunc: func(msec int64) {},
getLatestSnapshotTimeFunc: func() int64 { getLatestSnapshotTimeFunc: func() int64 {
return 0 return 0
@@ -148,213 +156,211 @@ func NewSnapshotEngine(options ...func(engine *Engine)) *Engine {
} }
func (engine *Engine) TakeSnapshot() error { func (engine *Engine) TakeSnapshot() error {
// TODO: Update to support multiple databases. engine.startSnapshotFunc()
defer engine.finishSnapshotFunc()
//engine.startSnapshotFunc() // Extract current time
//defer engine.finishSnapshotFunc() msec := engine.clock.Now().UnixMilli()
//
//// Extract current time // Update manifest file to indicate the latest snapshot.
//msec := engine.clock.Now().UnixMilli() // If manifest file does not exist, create it.
// // Manifest object will contain the following information:
//// Update manifest file to indicate the latest snapshot. // 1. Hash of the snapshot contents.
//// If manifest file does not exist, create it. // 2. Unix time of the latest snapshot taken.
//// Manifest object will contain the following information: // The information above will be used to determine whether a snapshot should be taken.
//// 1. Hash of the snapshot contents. // If the hash of the current state equals the hash in the manifest file, skip the snapshot.
//// 2. Unix time of the latest snapshot taken. // Otherwise, take the snapshot and update the latest snapshot timestamp and hash in the manifest file.
//// The information above will be used to determine whether a snapshot should be taken.
//// If the hash of the current state equals the hash in the manifest file, skip the snapshot. var firstSnapshot bool // Tracks whether the snapshot being attempted is the first one
//// Otherwise, take the snapshot and update the latest snapshot timestamp and hash in the manifest file.
// dirname := path.Join(engine.directory, "snapshots")
//var firstSnapshot bool // Tracks whether the snapshot being attempted is the first one if err := os.MkdirAll(dirname, os.ModePerm); err != nil {
// log.Println(err)
//dirname := path.Join(engine.directory, "snapshots") return err
//if err := os.MkdirAll(dirname, os.ModePerm); err != nil { }
// log.Println(err)
// return err // Open manifest file
//} var mf *os.File
// mf, err := os.Open(path.Join(dirname, "manifest.bin"))
//// Open manifest file if err != nil {
//var mf *os.File if errors.Is(err, fs.ErrNotExist) {
//mf, err := os.Open(path.Join(dirname, "manifest.bin")) // Create file if it does not exist
//if err != nil { mf, err = os.Create(path.Join(dirname, "manifest.bin"))
// if errors.Is(err, fs.ErrNotExist) { if err != nil {
// // Create file if it does not exist log.Println(err)
// mf, err = os.Create(path.Join(dirname, "manifest.bin")) return err
// if err != nil { }
// log.Println(err) firstSnapshot = true
// return err } else {
// } log.Println(err)
// firstSnapshot = true return err
// } else { }
// log.Println(err) }
// return err
// } md, err := io.ReadAll(mf)
//} if err != nil {
// log.Println(err)
//md, err := io.ReadAll(mf) return err
//if err != nil { }
// log.Println(err) if err := mf.Close(); err != nil {
// return err log.Println(err)
//} return err
//if err := mf.Close(); err != nil { }
// log.Println(err)
// return err manifest := new(Manifest)
//}
// if !firstSnapshot {
//manifest := new(Manifest) if err = json.Unmarshal(md, manifest); err != nil {
// log.Println(err)
//if !firstSnapshot { return err
// if err = json.Unmarshal(md, manifest); err != nil { }
// log.Println(err) }
// return err
// } // Get current state
//} snapshotObject := internal.SnapshotObject{
// State: internal.FilterExpiredKeys(engine.clock.Now(), engine.getStateFunc()),
//// Get current state LatestSnapshotMilliseconds: engine.getLatestSnapshotTimeFunc(),
//snapshotObject := internal.SnapshotObject{ }
// State: internal.FilterExpiredKeys(engine.clock.Now(), engine.getStateFunc()), out, err := json.Marshal(snapshotObject)
// LatestSnapshotMilliseconds: engine.getLatestSnapshotTimeFunc(), if err != nil {
//} log.Println(err)
//out, err := json.Marshal(snapshotObject) return err
//if err != nil { }
// log.Println(err)
// return err snapshotHash := md5.Sum(out)
//} if snapshotHash == manifest.LatestSnapshotHash {
// return errors.New("nothing new to snapshot")
//snapshotHash := md5.Sum(out) }
//if snapshotHash == manifest.LatestSnapshotHash {
// return errors.New("nothing new to snapshot") // Update the snapshotObject
//} snapshotObject.LatestSnapshotMilliseconds = msec
// // Marshal the updated snapshotObject
//// Update the snapshotObject out, err = json.Marshal(snapshotObject)
//snapshotObject.LatestSnapshotMilliseconds = msec if err != nil {
//// Marshal the updated snapshotObject log.Println(err)
//out, err = json.Marshal(snapshotObject) return err
//if err != nil { }
// log.Println(err)
// return err // os.Create will replace the old manifest file
//} mf, err = os.Create(path.Join(dirname, "manifest.bin"))
// if err != nil {
//// os.Create will replace the old manifest file log.Println(err)
//mf, err = os.Create(path.Join(dirname, "manifest.bin")) return err
//if err != nil { }
// log.Println(err)
// return err // Write the latest manifest data
//} manifest = &Manifest{
// LatestSnapshotHash: md5.Sum(out),
//// Write the latest manifest data LatestSnapshotMilliseconds: msec,
//manifest = &Manifest{ }
// LatestSnapshotHash: md5.Sum(out), mo, err := json.Marshal(manifest)
// LatestSnapshotMilliseconds: msec, if err != nil {
//} log.Println(err)
//mo, err := json.Marshal(manifest) return err
//if err != nil { }
// log.Println(err) if _, err = mf.Write(mo); err != nil {
// return err log.Println(err)
//} return err
//if _, err = mf.Write(mo); err != nil { }
// log.Println(err) if err = mf.Sync(); err != nil {
// return err log.Println(err)
//} }
//if err = mf.Sync(); err != nil { if err = mf.Close(); err != nil {
// log.Println(err) log.Println(err)
//} return err
//if err = mf.Close(); err != nil { }
// log.Println(err)
// return err // Create snapshot directory
//} dirname = path.Join(engine.directory, "snapshots", fmt.Sprintf("%d", msec))
// if err := os.MkdirAll(dirname, os.ModePerm); err != nil {
//// Create snapshot directory return err
//dirname = path.Join(engine.directory, "snapshots", fmt.Sprintf("%d", msec)) }
//if err := os.MkdirAll(dirname, os.ModePerm); err != nil {
// return err // Create snapshot file
//} f, err := os.OpenFile(path.Join(dirname, "state.bin"), os.O_WRONLY|os.O_CREATE, os.ModePerm)
// if err != nil {
//// Create snapshot file log.Println(err)
//f, err := os.OpenFile(path.Join(dirname, "state.bin"), os.O_WRONLY|os.O_CREATE, os.ModePerm) return err
//if err != nil { }
// log.Println(err) defer func() {
// return err if err := f.Close(); err != nil {
//} log.Println(err)
//defer func() { }
// if err := f.Close(); err != nil { }()
// log.Println(err)
// } // Write state to file
//}() if _, err = f.Write(out); err != nil {
// return err
//// Write state to file }
//if _, err = f.Write(out); err != nil { if err = f.Sync(); err != nil {
// return err log.Println(err)
//} }
//if err = f.Sync(); err != nil {
// log.Println(err) // Set the latest snapshot in unix milliseconds
//} engine.setLatestSnapshotTimeFunc(msec)
//
//// Set the latest snapshot in unix milliseconds // Reset the change count
//engine.setLatestSnapshotTimeFunc(msec) engine.resetChangeCount()
//
//// Reset the change count
//engine.resetChangeCount()
return nil return nil
} }
func (engine *Engine) Restore() error { func (engine *Engine) Restore() error {
// TODO: Update to support multiple databases. mf, err := os.Open(path.Join(engine.directory, "snapshots", "manifest.bin"))
if err != nil && errors.Is(err, fs.ErrNotExist) {
return errors.New("no snapshot manifest, skipping snapshot restore")
}
if err != nil {
return err
}
//mf, err := os.Open(path.Join(engine.directory, "snapshots", "manifest.bin")) manifest := new(Manifest)
//if err != nil && errors.Is(err, fs.ErrNotExist) {
// return errors.New("no snapshot manifest, skipping snapshot restore") md, err := io.ReadAll(mf)
//} if err != nil {
//if err != nil { return err
// return err }
//}
// if err = json.Unmarshal(md, manifest); err != nil {
//manifest := new(Manifest) return err
// }
//md, err := io.ReadAll(mf)
//if err != nil { if manifest.LatestSnapshotMilliseconds == 0 {
// return err return errors.New("no snapshot to restore")
//} }
//
//if err = json.Unmarshal(md, manifest); err != nil { sf, err := os.Open(path.Join(
// return err engine.directory,
//} "snapshots",
// fmt.Sprintf("%d", manifest.LatestSnapshotMilliseconds),
//if manifest.LatestSnapshotMilliseconds == 0 { "state.bin"))
// return errors.New("no snapshot to restore") if err != nil && errors.Is(err, fs.ErrNotExist) {
//} return fmt.Errorf("snapshot file %d/state.bin not found, skipping snapshot", manifest.LatestSnapshotMilliseconds)
// }
//sf, err := os.Open(path.Join( if err != nil {
// engine.directory, return err
// "snapshots", }
// fmt.Sprintf("%d", manifest.LatestSnapshotMilliseconds),
// "state.bin")) sd, err := io.ReadAll(sf)
//if err != nil && errors.Is(err, fs.ErrNotExist) { if err != nil {
// return fmt.Errorf("snapshot file %d/state.bin not found, skipping snapshot", manifest.LatestSnapshotMilliseconds) return nil
//} }
//if err != nil {
// return err snapshotObject := new(internal.SnapshotObject)
//}
// if err = json.Unmarshal(sd, snapshotObject); err != nil {
//sd, err := io.ReadAll(sf) return err
//if err != nil { }
// return nil
//} engine.setLatestSnapshotTimeFunc(snapshotObject.LatestSnapshotMilliseconds)
//
//snapshotObject := new(internal.SnapshotObject) for database, data := range internal.FilterExpiredKeys(engine.clock.Now(), snapshotObject.State) {
// for key, value := range data {
//if err = json.Unmarshal(sd, snapshotObject); err != nil { engine.setKeyDataFunc(database, key, value)
// return err }
//} }
//
//engine.setLatestSnapshotTimeFunc(snapshotObject.LatestSnapshotMilliseconds) log.Println("successfully restored latest snapshot")
//
//for key, data := range internal.FilterExpiredKeys(engine.clock.Now(), snapshotObject.State) {
// engine.setKeyDataFunc(key, data)
//}
//
//log.Println("successfully restored latest snapshot")
return nil return nil
} }

View File

@@ -44,20 +44,33 @@ func Test_SnapshotEngine(t *testing.T) {
snapshotInProgress.Store(false) snapshotInProgress.Store(false)
} }
state := map[string]internal.KeyData{ state := map[int]map[string]internal.KeyData{
"key1": {Value: "value1", ExpireAt: clock.NewClock().Now().Add(13 * time.Second)}, 0: {
"key2": {Value: "value2", ExpireAt: clock.NewClock().Now().Add(43 * time.Minute)}, "key1": {Value: "value-01", ExpireAt: clock.NewClock().Now().Add(13 * time.Second)},
"key3": {Value: "value3", ExpireAt: clock.NewClock().Now().Add(112 * time.Millisecond)}, "key2": {Value: "value-02", ExpireAt: clock.NewClock().Now().Add(43 * time.Minute)},
"key4": {Value: "value4", ExpireAt: clock.NewClock().Now().Add(23 * time.Second)}, "key3": {Value: "value-03", ExpireAt: clock.NewClock().Now().Add(112 * time.Millisecond)},
"key5": {Value: "value5", ExpireAt: clock.NewClock().Now().Add(121 * time.Millisecond)}, "key4": {Value: "value-04", ExpireAt: clock.NewClock().Now().Add(23 * time.Second)},
"key5": {Value: "value-45", ExpireAt: clock.NewClock().Now().Add(121 * time.Millisecond)},
},
1: {
"key1": {Value: "value1", ExpireAt: clock.NewClock().Now().Add(13 * time.Second)},
"key2": {Value: "value2", ExpireAt: clock.NewClock().Now().Add(43 * time.Minute)},
"key3": {Value: "value3", ExpireAt: clock.NewClock().Now().Add(112 * time.Millisecond)},
"key4": {Value: "value4", ExpireAt: clock.NewClock().Now().Add(23 * time.Second)},
"key5": {Value: "value5", ExpireAt: clock.NewClock().Now().Add(121 * time.Millisecond)},
},
} }
getStateFunc := func() map[string]internal.KeyData {
getStateFunc := func() map[int]map[string]internal.KeyData {
return state return state
} }
restoredState := map[string]internal.KeyData{} restoredState := make(map[int]map[string]internal.KeyData)
setKeyDataFunc := func(key string, data internal.KeyData) { setKeyDataFunc := func(database int, key string, data internal.KeyData) {
restoredState[key] = data if restoredState[database] == nil {
restoredState[database] = make(map[string]internal.KeyData)
}
restoredState[database][key] = data
} }
var latestSnapshotTime int64 var latestSnapshotTime int64
@@ -85,11 +98,13 @@ func Test_SnapshotEngine(t *testing.T) {
t.Error(err) t.Error(err)
} }
// Add more records to the state // Add more records to each database in the state
for i := 0; i < 5; i++ { for database, _ := range state {
state[fmt.Sprintf("key%d", i)] = internal.KeyData{ for i := 0; i < 5; i++ {
Value: fmt.Sprintf("value%d", i), state[database][fmt.Sprintf("key%d", i)] = internal.KeyData{
ExpireAt: clock.NewClock().Now().Add(time.Duration(i) * time.Second), Value: fmt.Sprintf("value%d", i),
ExpireAt: clock.NewClock().Now().Add(time.Duration(i) * time.Second),
}
} }
} }
@@ -106,12 +121,14 @@ func Test_SnapshotEngine(t *testing.T) {
t.Errorf("expected restored state to be length %d, got %d", len(state), len(restoredState)) t.Errorf("expected restored state to be length %d, got %d", len(state), len(restoredState))
} }
for key, data := range restoredState { for database, data := range restoredState {
if state[key].Value != data.Value { for key, keyData := range data {
t.Errorf("expected value %v for key %s, got %v", state[key].Value, key, data.Value) if state[database][key].Value != keyData.Value {
} t.Errorf("expected value %v for key %s, got %v", state[database][key].Value, key, keyData.Value)
if !state[key].ExpireAt.Equal(data.ExpireAt) { }
t.Errorf("expected expiry time %v for key %s, got %v", state[key].ExpireAt, key, data.ExpireAt) if !state[database][key].ExpireAt.Equal(keyData.ExpireAt) {
t.Errorf("expected expiry time %v for key %s, got %v", state[database][key].ExpireAt, key, keyData.ExpireAt)
}
} }
} }