Implemented a Clock interface that is injected everywhere time.Now and time.After are used.

This commit is contained in:
Kelvin Mwinuka
2024-04-05 03:11:03 +08:00
parent f4d0f2e468
commit 1e421cb64a
13 changed files with 1171 additions and 1063 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -19,6 +19,7 @@ import (
"github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal"
logstore "github.com/echovault/echovault/internal/aof/log" logstore "github.com/echovault/echovault/internal/aof/log"
"github.com/echovault/echovault/internal/aof/preamble" "github.com/echovault/echovault/internal/aof/preamble"
"github.com/echovault/echovault/internal/clock"
"log" "log"
"sync" "sync"
) )
@@ -27,6 +28,7 @@ import (
// Logging in replication clusters is handled in the raft layer. // Logging in replication clusters is handled in the raft layer.
type Engine struct { type Engine struct {
clock clock.Clock
syncStrategy string syncStrategy string
directory string directory string
preambleRW preamble.PreambleReadWriter preambleRW preamble.PreambleReadWriter
@@ -45,6 +47,12 @@ type Engine struct {
handleCommand func(command []byte) handleCommand func(command []byte)
} }
func WithClock(clock clock.Clock) func(engine *Engine) {
return func(engine *Engine) {
engine.clock = clock
}
}
func WithStrategy(strategy string) func(engine *Engine) { func WithStrategy(strategy string) func(engine *Engine) {
return func(engine *Engine) { return func(engine *Engine) {
engine.syncStrategy = strategy engine.syncStrategy = strategy
@@ -101,6 +109,7 @@ func WithAppendReadWriter(rw logstore.AppendReadWriter) func(engine *Engine) {
func NewAOFEngine(options ...func(engine *Engine)) *Engine { func NewAOFEngine(options ...func(engine *Engine)) *Engine {
engine := &Engine{ engine := &Engine{
clock: clock.NewClock(),
syncStrategy: "everysec", syncStrategy: "everysec",
directory: "", directory: "",
mut: sync.Mutex{}, mut: sync.Mutex{},
@@ -121,6 +130,7 @@ func NewAOFEngine(options ...func(engine *Engine)) *Engine {
// Setup Preamble engine // Setup Preamble engine
engine.preambleStore = preamble.NewPreambleStore( engine.preambleStore = preamble.NewPreambleStore(
preamble.WithClock(engine.clock),
preamble.WithDirectory(engine.directory), preamble.WithDirectory(engine.directory),
preamble.WithReadWriter(engine.preambleRW), preamble.WithReadWriter(engine.preambleRW),
preamble.WithGetStateFunc(engine.getStateFunc), preamble.WithGetStateFunc(engine.getStateFunc),
@@ -129,6 +139,7 @@ func NewAOFEngine(options ...func(engine *Engine)) *Engine {
// Setup AOF log store engine // Setup AOF log store engine
engine.appendStore = logstore.NewAppendStore( engine.appendStore = logstore.NewAppendStore(
logstore.WithClock(engine.clock),
logstore.WithDirectory(engine.directory), logstore.WithDirectory(engine.directory),
logstore.WithStrategy(engine.syncStrategy), logstore.WithStrategy(engine.syncStrategy),
logstore.WithReadWriter(engine.appendRW), logstore.WithReadWriter(engine.appendRW),

View File

@@ -19,6 +19,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"github.com/echovault/echovault/internal/clock"
"io" "io"
"log" "log"
"os" "os"
@@ -36,6 +37,7 @@ type AppendReadWriter interface {
} }
type AppendStore struct { type AppendStore struct {
clock clock.Clock
strategy string // Append file sync strategy. Can only be "always", "everysec", or "no strategy string // Append file sync strategy. Can only be "always", "everysec", or "no
mut sync.Mutex // Store mutex mut sync.Mutex // Store mutex
rw AppendReadWriter // The ReadWriter used to persist and load the log rw AppendReadWriter // The ReadWriter used to persist and load the log
@@ -43,6 +45,12 @@ type AppendStore struct {
handleCommand func(command []byte) // Function to handle command read from AOF log after restore handleCommand func(command []byte) // Function to handle command read from AOF log after restore
} }
func WithClock(clock clock.Clock) func(store *AppendStore) {
return func(store *AppendStore) {
store.clock = clock
}
}
func WithStrategy(strategy string) func(store *AppendStore) { func WithStrategy(strategy string) func(store *AppendStore) {
return func(store *AppendStore) { return func(store *AppendStore) {
store.strategy = strategy store.strategy = strategy
@@ -69,6 +77,7 @@ func WithHandleCommandFunc(f func(command []byte)) func(store *AppendStore) {
func NewAppendStore(options ...func(store *AppendStore)) *AppendStore { func NewAppendStore(options ...func(store *AppendStore)) *AppendStore {
store := &AppendStore{ store := &AppendStore{
clock: clock.NewClock(),
directory: "", directory: "",
strategy: "everysec", strategy: "everysec",
rw: nil, rw: nil,
@@ -103,7 +112,7 @@ func NewAppendStore(options ...func(store *AppendStore)) *AppendStore {
log.Println(fmt.Errorf("new append store error: %+v", err)) log.Println(fmt.Errorf("new append store error: %+v", err))
break break
} }
<-time.After(1 * time.Second) <-store.clock.After(1 * time.Second)
} }
}() }()
} }

View File

@@ -18,12 +18,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/clock"
"io" "io"
"log" "log"
"os" "os"
"path" "path"
"sync" "sync"
"time"
) )
type PreambleReadWriter interface { type PreambleReadWriter interface {
@@ -34,6 +34,7 @@ type PreambleReadWriter interface {
} }
type PreambleStore struct { type PreambleStore struct {
clock clock.Clock
rw PreambleReadWriter rw PreambleReadWriter
mut sync.Mutex mut sync.Mutex
directory string directory string
@@ -41,6 +42,12 @@ type PreambleStore struct {
setKeyDataFunc func(key string, data internal.KeyData) setKeyDataFunc func(key string, data internal.KeyData)
} }
func WithClock(clock clock.Clock) func(store *PreambleStore) {
return func(store *PreambleStore) {
store.clock = clock
}
}
func WithReadWriter(rw PreambleReadWriter) func(store *PreambleStore) { func WithReadWriter(rw PreambleReadWriter) func(store *PreambleStore) {
return func(store *PreambleStore) { return func(store *PreambleStore) {
store.rw = rw store.rw = rw
@@ -67,6 +74,7 @@ func WithDirectory(directory string) func(store *PreambleStore) {
func NewPreambleStore(options ...func(store *PreambleStore)) *PreambleStore { func NewPreambleStore(options ...func(store *PreambleStore)) *PreambleStore {
store := &PreambleStore{ store := &PreambleStore{
clock: clock.NewClock(),
rw: nil, rw: nil,
mut: sync.Mutex{}, mut: sync.Mutex{},
directory: "", directory: "",
@@ -166,7 +174,7 @@ func (store *PreambleStore) Close() error {
func (store *PreambleStore) filterExpiredKeys(state map[string]internal.KeyData) map[string]internal.KeyData { func (store *PreambleStore) filterExpiredKeys(state map[string]internal.KeyData) map[string]internal.KeyData {
var keysToDelete []string var keysToDelete []string
for k, v := range state { for k, v := range state {
if v.ExpireAt.Before(time.Now()) { if v.ExpireAt.Before(store.clock.Now()) {
keysToDelete = append(keysToDelete, k) keysToDelete = append(keysToDelete, k)
} }
} }

41
internal/clock/clock.go Normal file
View File

@@ -0,0 +1,41 @@
package clock
import (
"os"
"strings"
"time"
)
type Clock interface {
Now() time.Time
After(d time.Duration) <-chan time.Time
}
func NewClock() Clock {
// If we're in a test environment, return the mock clock.
if strings.Contains(os.Args[0], ".test") {
return MockClock{}
}
return RealClock{}
}
type RealClock struct{}
func (RealClock) Now() time.Time {
return time.Now()
}
func (RealClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}
type MockClock struct{}
func (MockClock) Now() time.Time {
t, _ := time.Parse(time.RFC3339, "2036-01-02T15:04:05+07:00")
return t
}
func (MockClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}

View File

@@ -20,6 +20,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/clock"
"io" "io"
"io/fs" "io/fs"
"log" "log"
@@ -37,6 +38,7 @@ type Manifest struct {
} }
type Engine struct { type Engine struct {
clock clock.Clock
changeCount uint64 changeCount uint64
directory string directory string
snapshotInterval time.Duration snapshotInterval time.Duration
@@ -49,6 +51,12 @@ type Engine struct {
setKeyDataFunc func(key string, data internal.KeyData) setKeyDataFunc func(key string, data internal.KeyData)
} }
func WithClock(clock clock.Clock) func(engine *Engine) {
return func(engine *Engine) {
engine.clock = clock
}
}
func WithDirectory(directory string) func(engine *Engine) { func WithDirectory(directory string) func(engine *Engine) {
return func(engine *Engine) { return func(engine *Engine) {
engine.directory = directory engine.directory = directory
@@ -105,6 +113,7 @@ func WithSetKeyDataFunc(f func(key string, data internal.KeyData)) func(engine *
func NewSnapshotEngine(options ...func(engine *Engine)) *Engine { func NewSnapshotEngine(options ...func(engine *Engine)) *Engine {
engine := &Engine{ engine := &Engine{
clock: clock.NewClock(),
changeCount: 0, changeCount: 0,
directory: "", directory: "",
snapshotInterval: 5 * time.Minute, snapshotInterval: 5 * time.Minute,
@@ -128,7 +137,7 @@ func NewSnapshotEngine(options ...func(engine *Engine)) *Engine {
if engine.snapshotInterval != 0 { if engine.snapshotInterval != 0 {
go func() { go func() {
for { for {
<-time.After(engine.snapshotInterval) <-engine.clock.After(engine.snapshotInterval)
if engine.changeCount == engine.snapshotThreshold { if engine.changeCount == engine.snapshotThreshold {
if err := engine.TakeSnapshot(); err != nil { if err := engine.TakeSnapshot(); err != nil {
log.Println(err) log.Println(err)
@@ -146,7 +155,7 @@ func (engine *Engine) TakeSnapshot() error {
defer engine.finishSnapshotFunc() defer engine.finishSnapshotFunc()
// Extract current time // Extract current time
now := time.Now() now := engine.clock.Now()
msec := now.UnixNano() / int64(time.Millisecond) msec := now.UnixNano() / int64(time.Millisecond)
// Update manifest file to indicate the latest snapshot. // Update manifest file to indicate the latest snapshot.

View File

@@ -16,6 +16,7 @@ package echovault
import ( import (
"github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/clock"
"github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/pkg/commands" "github.com/echovault/echovault/pkg/commands"
"github.com/echovault/echovault/pkg/constants" "github.com/echovault/echovault/pkg/constants"
@@ -26,13 +27,6 @@ import (
"time" "time"
) )
var timeNow = func() time.Time {
now := time.Now()
return func() time.Time {
return now.Add(5 * time.Hour).Add(30 * time.Minute).Add(30 * time.Second).Add(10 * time.Millisecond)
}()
}
func TestEchoVault_DEL(t *testing.T) { func TestEchoVault_DEL(t *testing.T) {
server, _ := NewEchoVault( server, _ := NewEchoVault(
WithCommands(commands.All()), WithCommands(commands.All()),
@@ -81,6 +75,8 @@ func TestEchoVault_DEL(t *testing.T) {
} }
func TestEchoVault_EXPIRE(t *testing.T) { func TestEchoVault_EXPIRE(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault( server, _ := NewEchoVault(
WithCommands(commands.All()), WithCommands(commands.All()),
WithConfig(config.Config{ WithConfig(config.Config{
@@ -142,7 +138,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000, time: 1000,
expireOpts: EXPIREOptions{NX: true}, expireOpts: EXPIREOptions{NX: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key4": {Value: "value4", ExpireAt: timeNow().Add(1000 * time.Second)}, "key4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
want: 0, want: 0,
wantErr: false, wantErr: false,
@@ -154,7 +150,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000, time: 1000,
expireOpts: EXPIREOptions{XX: true}, expireOpts: EXPIREOptions{XX: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key5": {Value: "value5", ExpireAt: timeNow().Add(30 * time.Second)}, "key5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 1, want: 1,
wantErr: false, wantErr: false,
@@ -178,7 +174,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 100000, time: 100000,
expireOpts: EXPIREOptions{GT: true}, expireOpts: EXPIREOptions{GT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key7": {Value: "value7", ExpireAt: timeNow().Add(30 * time.Second)}, "key7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 1, want: 1,
wantErr: false, wantErr: false,
@@ -190,7 +186,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000, time: 1000,
expireOpts: EXPIREOptions{GT: true}, expireOpts: EXPIREOptions{GT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key8": {Value: "value8", ExpireAt: timeNow().Add(3000 * time.Second)}, "key8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
want: 0, want: 0,
wantErr: false, wantErr: false,
@@ -214,7 +210,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000, time: 1000,
expireOpts: EXPIREOptions{LT: true}, expireOpts: EXPIREOptions{LT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key10": {Value: "value10", ExpireAt: timeNow().Add(3000 * time.Second)}, "key10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
want: 1, want: 1,
wantErr: false, wantErr: false,
@@ -226,7 +222,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 50000, time: 50000,
expireOpts: EXPIREOptions{LT: true}, expireOpts: EXPIREOptions{LT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key11": {Value: "value11", ExpireAt: timeNow().Add(30 * time.Second)}, "key11": {Value: "value11", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 0, want: 0,
wantErr: false, wantErr: false,
@@ -258,6 +254,8 @@ func TestEchoVault_EXPIRE(t *testing.T) {
} }
func TestEchoVault_EXPIREAT(t *testing.T) { func TestEchoVault_EXPIREAT(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault( server, _ := NewEchoVault(
WithCommands(commands.All()), WithCommands(commands.All()),
WithConfig(config.Config{ WithConfig(config.Config{
@@ -281,7 +279,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key1", key: "key1",
expireAtOpts: EXPIREATOptions{}, expireAtOpts: EXPIREATOptions{},
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key1": {Value: "value1", ExpireAt: time.Time{}}, "key1": {Value: "value1", ExpireAt: time.Time{}},
}, },
@@ -293,7 +291,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
cmd: "PEXPIREAT", cmd: "PEXPIREAT",
key: "key2", key: "key2",
pexpireAtOpts: PEXPIREATOptions{}, pexpireAtOpts: PEXPIREATOptions{},
time: int(timeNow().Add(1000 * time.Second).UnixMilli()), time: int(mockClock.Now().Add(1000 * time.Second).UnixMilli()),
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key2": {Value: "value2", ExpireAt: time.Time{}}, "key2": {Value: "value2", ExpireAt: time.Time{}},
}, },
@@ -304,7 +302,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Set new expire only when key does not have an expiry time with NX flag", name: "Set new expire only when key does not have an expiry time with NX flag",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key3", key: "key3",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{NX: true}, expireAtOpts: EXPIREATOptions{NX: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key3": {Value: "value3", ExpireAt: time.Time{}}, "key3": {Value: "value3", ExpireAt: time.Time{}},
@@ -315,11 +313,11 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
{ {
name: "Return 0, when NX flag is provided and key already has an expiry time", name: "Return 0, when NX flag is provided and key already has an expiry time",
cmd: "EXPIREAT", cmd: "EXPIREAT",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{NX: true}, expireAtOpts: EXPIREATOptions{NX: true},
key: "key4", key: "key4",
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key4": {Value: "value4", ExpireAt: timeNow().Add(1000 * time.Second)}, "key4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
want: 0, want: 0,
wantErr: false, wantErr: false,
@@ -327,11 +325,11 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
{ {
name: "Set new expire time from now key only when the key already has an expiry time with XX flag", name: "Set new expire time from now key only when the key already has an expiry time with XX flag",
cmd: "EXPIREAT", cmd: "EXPIREAT",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
key: "key5", key: "key5",
expireAtOpts: EXPIREATOptions{XX: true}, expireAtOpts: EXPIREATOptions{XX: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key5": {Value: "value5", ExpireAt: timeNow().Add(30 * time.Second)}, "key5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 1, want: 1,
wantErr: false, wantErr: false,
@@ -340,7 +338,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Return 0 when key does not have an expiry and the XX flag is provided", name: "Return 0 when key does not have an expiry and the XX flag is provided",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key6", key: "key6",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{XX: true}, expireAtOpts: EXPIREATOptions{XX: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key6": {Value: "value6", ExpireAt: time.Time{}}, "key6": {Value: "value6", ExpireAt: time.Time{}},
@@ -352,10 +350,10 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Set expiry time when the provided time is after the current expiry time when GT flag is provided", name: "Set expiry time when the provided time is after the current expiry time when GT flag is provided",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key7", key: "key7",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{GT: true}, expireAtOpts: EXPIREATOptions{GT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key7": {Value: "value7", ExpireAt: timeNow().Add(30 * time.Second)}, "key7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
want: 1, want: 1,
wantErr: false, wantErr: false,
@@ -364,10 +362,10 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Return 0 when GT flag is passed and current expiry time is greater than provided time", name: "Return 0 when GT flag is passed and current expiry time is greater than provided time",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key8", key: "key8",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{GT: true}, expireAtOpts: EXPIREATOptions{GT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key8": {Value: "value8", ExpireAt: timeNow().Add(3000 * time.Second)}, "key8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
want: 0, want: 0,
wantErr: false, wantErr: false,
@@ -376,7 +374,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Return 0 when GT flag is passed and key does not have an expiry time", name: "Return 0 when GT flag is passed and key does not have an expiry time",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key9", key: "key9",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{GT: true}, expireAtOpts: EXPIREATOptions{GT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key9": {Value: "value9", ExpireAt: time.Time{}}, "key9": {Value: "value9", ExpireAt: time.Time{}},
@@ -387,10 +385,10 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Set expiry time when the provided time is before the current expiry time when LT flag is provided", name: "Set expiry time when the provided time is before the current expiry time when LT flag is provided",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key10", key: "key10",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{LT: true}, expireAtOpts: EXPIREATOptions{LT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key10": {Value: "value10", ExpireAt: timeNow().Add(3000 * time.Second)}, "key10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
want: 1, want: 1,
wantErr: false, wantErr: false,
@@ -399,10 +397,10 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Return 0 when LT flag is passed and current expiry time is less than provided time", name: "Return 0 when LT flag is passed and current expiry time is less than provided time",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key11", key: "key11",
time: int(timeNow().Add(3000 * time.Second).Unix()), time: int(mockClock.Now().Add(3000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{LT: true}, expireAtOpts: EXPIREATOptions{LT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key11": {Value: "value11", ExpireAt: timeNow().Add(1000 * time.Second)}, "key11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
want: 0, want: 0,
wantErr: false, wantErr: false,
@@ -411,7 +409,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
name: "Return 0 when LT flag is passed and key does not have an expiry time", name: "Return 0 when LT flag is passed and key does not have an expiry time",
cmd: "EXPIREAT", cmd: "EXPIREAT",
key: "key12", key: "key12",
time: int(timeNow().Add(1000 * time.Second).Unix()), time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{LT: true}, expireAtOpts: EXPIREATOptions{LT: true},
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key12": {Value: "value12", ExpireAt: time.Time{}}, "key12": {Value: "value12", ExpireAt: time.Time{}},
@@ -446,6 +444,8 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
} }
func TestEchoVault_EXPIRETIME(t *testing.T) { func TestEchoVault_EXPIRETIME(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault( server, _ := NewEchoVault(
WithCommands(commands.All()), WithCommands(commands.All()),
WithConfig(config.Config{ WithConfig(config.Config{
@@ -465,20 +465,20 @@ func TestEchoVault_EXPIRETIME(t *testing.T) {
name: "Return expire time in seconds", name: "Return expire time in seconds",
key: "key1", key: "key1",
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key1": {Value: "value1", ExpireAt: timeNow().Add(100 * time.Second)}, "key1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)},
}, },
expiretimeFunc: server.EXPIRETIME, expiretimeFunc: server.EXPIRETIME,
want: int(timeNow().Add(100 * time.Second).Unix()), want: int(mockClock.Now().Add(100 * time.Second).Unix()),
wantErr: false, wantErr: false,
}, },
{ {
name: "Return expire time in milliseconds", name: "Return expire time in milliseconds",
key: "key2", key: "key2",
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key2": {Value: "value2", ExpireAt: timeNow().Add(4096 * time.Millisecond)}, "key2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)},
}, },
expiretimeFunc: server.PEXPIRETIME, expiretimeFunc: server.PEXPIRETIME,
want: int(timeNow().Add(4096 * time.Millisecond).UnixMilli()), want: int(mockClock.Now().Add(4096 * time.Millisecond).UnixMilli()),
wantErr: false, wantErr: false,
}, },
{ {
@@ -623,6 +623,8 @@ func TestEchoVault_MGET(t *testing.T) {
} }
func TestEchoVault_SET(t *testing.T) { func TestEchoVault_SET(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault( server, _ := NewEchoVault(
WithCommands(commands.All()), WithCommands(commands.All()),
WithConfig(config.Config{ WithConfig(config.Config{
@@ -717,7 +719,7 @@ func TestEchoVault_SET(t *testing.T) {
presetValues: nil, presetValues: nil,
key: "key8", key: "key8",
value: "value8", value: "value8",
options: SETOptions{EXAT: int(timeNow().Add(200 * time.Second).Unix())}, options: SETOptions{EXAT: int(mockClock.Now().Add(200 * time.Second).Unix())},
want: "OK", want: "OK",
wantErr: false, wantErr: false,
}, },
@@ -725,7 +727,7 @@ func TestEchoVault_SET(t *testing.T) {
name: "Set exact expiry time in milliseconds from unix epoch", name: "Set exact expiry time in milliseconds from unix epoch",
key: "key9", key: "key9",
value: "value9", value: "value9",
options: SETOptions{PXAT: int(timeNow().Add(4096 * time.Millisecond).UnixMilli())}, options: SETOptions{PXAT: int(mockClock.Now().Add(4096 * time.Millisecond).UnixMilli())},
presetValues: nil, presetValues: nil,
want: "OK", want: "OK",
wantErr: false, wantErr: false,
@@ -809,6 +811,8 @@ func TestEchoVault_MSET(t *testing.T) {
} }
func TestEchoVault_PERSIST(t *testing.T) { func TestEchoVault_PERSIST(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault( server, _ := NewEchoVault(
WithCommands(commands.All()), WithCommands(commands.All()),
WithConfig(config.Config{ WithConfig(config.Config{
@@ -827,7 +831,7 @@ func TestEchoVault_PERSIST(t *testing.T) {
name: "Successfully persist a volatile key", name: "Successfully persist a volatile key",
key: "key1", key: "key1",
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key1": {Value: "value1", ExpireAt: timeNow().Add(1000 * time.Second)}, "key1": {Value: "value1", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
want: true, want: true,
wantErr: false, wantErr: false,
@@ -869,6 +873,8 @@ func TestEchoVault_PERSIST(t *testing.T) {
} }
func TestEchoVault_TTL(t *testing.T) { func TestEchoVault_TTL(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault( server, _ := NewEchoVault(
WithCommands(commands.All()), WithCommands(commands.All()),
WithConfig(config.Config{ WithConfig(config.Config{
@@ -888,10 +894,10 @@ func TestEchoVault_TTL(t *testing.T) {
name: "Return TTL time in seconds", name: "Return TTL time in seconds",
key: "key1", key: "key1",
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key1": {Value: "value1", ExpireAt: timeNow().Add(100 * time.Second)}, "key1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)},
}, },
ttlFunc: server.TTL, ttlFunc: server.TTL,
want: 19930, want: 100,
wantErr: false, wantErr: false,
}, },
{ {
@@ -899,9 +905,9 @@ func TestEchoVault_TTL(t *testing.T) {
key: "key2", key: "key2",
ttlFunc: server.PTTL, ttlFunc: server.PTTL,
presetValues: map[string]internal.KeyData{ presetValues: map[string]internal.KeyData{
"key2": {Value: "value2", ExpireAt: timeNow().Add(4096 * time.Millisecond)}, "key2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)},
}, },
want: 19834106, want: 4096,
wantErr: false, wantErr: false,
}, },
{ {

View File

@@ -23,6 +23,7 @@ import (
"github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/acl" "github.com/echovault/echovault/internal/acl"
"github.com/echovault/echovault/internal/aof" "github.com/echovault/echovault/internal/aof"
"github.com/echovault/echovault/internal/clock"
"github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/internal/eviction" "github.com/echovault/echovault/internal/eviction"
"github.com/echovault/echovault/internal/memberlist" "github.com/echovault/echovault/internal/memberlist"
@@ -41,6 +42,9 @@ import (
) )
type EchoVault struct { type EchoVault struct {
// clock is an implementation of a time interface that allows mocking of time functions during testing.
clock clock.Clock
// config holds the echovault configuration variables. // config holds the echovault configuration variables.
config config.Config config config.Config
@@ -90,8 +94,8 @@ type EchoVault struct {
} }
// WithContext is an options that for the NewEchoVault function that allows you to // WithContext is an options that for the NewEchoVault function that allows you to
// configure a custom context object to be used in EchoVault. If you don't provide this // configure a custom context object to be used in EchoVault.
// option, EchoVault will create its own internal context object. // If you don't provide this option, EchoVault will create its own internal context object.
func WithContext(ctx context.Context) func(echovault *EchoVault) { func WithContext(ctx context.Context) func(echovault *EchoVault) {
return func(echovault *EchoVault) { return func(echovault *EchoVault) {
echovault.context = ctx echovault.context = ctx
@@ -99,8 +103,8 @@ func WithContext(ctx context.Context) func(echovault *EchoVault) {
} }
// WithConfig is an option for the NewEchoVault function that allows you to pass a // WithConfig is an option for the NewEchoVault function that allows you to pass a
// custom configuration to EchoVault. If not specified, EchoVault will use the default // custom configuration to EchoVault.
// configuration from config.DefaultConfig(). // If not specified, EchoVault will use the default configuration from config.DefaultConfig().
func WithConfig(config config.Config) func(echovault *EchoVault) { func WithConfig(config config.Config) func(echovault *EchoVault) {
return func(echovault *EchoVault) { return func(echovault *EchoVault) {
echovault.config = config echovault.config = config
@@ -108,8 +112,8 @@ func WithConfig(config config.Config) func(echovault *EchoVault) {
} }
// WithCommands is an options for the NewEchoVault function that allows you to pass a // WithCommands is an options for the NewEchoVault function that allows you to pass a
// list of commands that should be supported by your EchoVault instance. If you don't pass // list of commands that should be supported by your EchoVault instance.
// this option, EchoVault will start with no commands loaded. // If you don't pass this option, EchoVault will start with no commands loaded.
func WithCommands(commands []types.Command) func(echovault *EchoVault) { func WithCommands(commands []types.Command) func(echovault *EchoVault) {
return func(echovault *EchoVault) { return func(echovault *EchoVault) {
echovault.commands = commands echovault.commands = commands
@@ -120,6 +124,7 @@ func WithCommands(commands []types.Command) func(echovault *EchoVault) {
// This functions accepts the WithContext, WithConfig and WithCommands options. // This functions accepts the WithContext, WithConfig and WithCommands options.
func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) { func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) {
echovault := &EchoVault{ echovault := &EchoVault{
clock: clock.NewClock(),
context: context.Background(), context: context.Background(),
commands: make([]types.Command, 0), commands: make([]types.Command, 0),
config: config.DefaultConfig(), config: config.DefaultConfig(),
@@ -174,6 +179,7 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) {
} else { } else {
// Set up standalone snapshot engine // Set up standalone snapshot engine
echovault.snapshotEngine = snapshot.NewSnapshotEngine( echovault.snapshotEngine = snapshot.NewSnapshotEngine(
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),
@@ -204,6 +210,7 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) {
) )
// Set up standalone AOF engine // Set up standalone AOF engine
echovault.aofEngine = aof.NewAOFEngine( echovault.aofEngine = aof.NewAOFEngine(
aof.WithClock(echovault.clock),
aof.WithDirectory(echovault.config.DataDir), aof.WithDirectory(echovault.config.DataDir),
aof.WithStrategy(echovault.config.AOFSyncStrategy), aof.WithStrategy(echovault.config.AOFSyncStrategy),
aof.WithStartRewriteFunc(echovault.startRewriteAOF), aof.WithStartRewriteFunc(echovault.startRewriteAOF),
@@ -241,7 +248,7 @@ func NewEchoVault(options ...func(echovault *EchoVault)) (*EchoVault, error) {
if echovault.config.EvictionPolicy != constants.NoEviction { if echovault.config.EvictionPolicy != constants.NoEviction {
go func() { go func() {
for { for {
<-time.After(echovault.config.EvictionInterval) <-echovault.clock.After(echovault.config.EvictionInterval)
if err := echovault.evictKeysWithExpiredTTL(context.Background()); err != nil { if err := echovault.evictKeysWithExpiredTTL(context.Background()); err != nil {
log.Println(err) log.Println(err)
} }
@@ -465,6 +472,11 @@ func (server *EchoVault) TakeSnapshot() error {
return nil return nil
} }
// GetClock returns the server's clock implementation
func (server *EchoVault) GetClock() clock.Clock {
return server.clock
}
func (server *EchoVault) startSnapshot() { func (server *EchoVault) startSnapshot() {
server.snapshotInProgress.Store(true) server.snapshotInProgress.Store(true)
} }

View File

@@ -118,7 +118,7 @@ func (server *EchoVault) KeyExists(ctx context.Context, key string) bool {
return false return false
} }
if entry.ExpireAt != (time.Time{}) && entry.ExpireAt.Before(time.Now()) { if entry.ExpireAt != (time.Time{}) && entry.ExpireAt.Before(server.clock.Now()) {
if !server.isInCluster() { if !server.isInCluster() {
// If in standalone mode, delete the key directly. // If in standalone mode, delete the key directly.
err := server.DeleteKey(ctx, key) err := server.DeleteKey(ctx, key)
@@ -553,7 +553,7 @@ func (server *EchoVault) evictKeysWithExpiredTTL(ctx context.Context) error {
} }
// If the current key is not expired, skip to the next key // If the current key is not expired, skip to the next key
if server.store[k].ExpireAt.After(time.Now()) { if server.store[k].ExpireAt.After(server.clock.Now()) {
server.KeyRUnlock(ctx, k) server.KeyRUnlock(ctx, k)
continue continue
} }

View File

@@ -42,8 +42,9 @@ func handleSet(ctx context.Context, cmd []string, server types.EchoVault, _ *net
key := keys[0] key := keys[0]
value := cmd[2] value := cmd[2]
res := []byte(constants.OkResponse) res := []byte(constants.OkResponse)
clock := server.GetClock()
params, err := getSetCommandParams(cmd[3:], SetParams{}) params, err := getSetCommandParams(clock, cmd[3:], SetParams{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -308,6 +309,8 @@ func handleTTL(ctx context.Context, cmd []string, server types.EchoVault, _ *net
key := keys[0] key := keys[0]
clock := server.GetClock()
if !server.KeyExists(ctx, key) { if !server.KeyExists(ctx, key) {
return []byte(":-2\r\n"), nil return []byte(":-2\r\n"), nil
} }
@@ -323,9 +326,9 @@ func handleTTL(ctx context.Context, cmd []string, server types.EchoVault, _ *net
return []byte(":-1\r\n"), nil return []byte(":-1\r\n"), nil
} }
t := expireAt.Unix() - time.Now().Unix() t := expireAt.Unix() - clock.Now().Unix()
if strings.ToLower(cmd[0]) == "pttl" { if strings.ToLower(cmd[0]) == "pttl" {
t = expireAt.UnixMilli() - time.Now().UnixMilli() t = expireAt.UnixMilli() - clock.Now().UnixMilli()
} }
if t <= 0 { if t <= 0 {
@@ -348,9 +351,9 @@ func handleExpire(ctx context.Context, cmd []string, server types.EchoVault, _ *
if err != nil { if err != nil {
return nil, errors.New("expire time must be integer") return nil, errors.New("expire time must be integer")
} }
expireAt := time.Now().Add(time.Duration(n) * time.Second) expireAt := server.GetClock().Now().Add(time.Duration(n) * time.Second)
if strings.ToLower(cmd[0]) == "pexpire" { if strings.ToLower(cmd[0]) == "pexpire" {
expireAt = time.Now().Add(time.Duration(n) * time.Millisecond) expireAt = server.GetClock().Now().Add(time.Duration(n) * time.Millisecond)
} }
if !server.KeyExists(ctx, key) { if !server.KeyExists(ctx, key) {

View File

@@ -19,6 +19,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/echovault/echovault/internal/clock"
"github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/pkg/constants" "github.com/echovault/echovault/pkg/constants"
"github.com/echovault/echovault/pkg/echovault" "github.com/echovault/echovault/pkg/echovault"
@@ -29,12 +30,16 @@ import (
var mockServer *echovault.EchoVault var mockServer *echovault.EchoVault
var mockClock clock.Clock
type KeyData struct { type KeyData struct {
Value interface{} Value interface{}
ExpireAt time.Time ExpireAt time.Time
} }
func init() { func init() {
mockClock = clock.NewClock()
mockServer, _ = echovault.NewEchoVault( mockServer, _ = echovault.NewEchoVault(
echovault.WithConfig(config.Config{ echovault.WithConfig(config.Config{
DataDir: "", DataDir: "",
@@ -139,7 +144,7 @@ func Test_HandleSET(t *testing.T) {
presetValues: nil, presetValues: nil,
expectedResponse: "OK", expectedResponse: "OK",
expectedValue: "value10", expectedValue: "value10",
expectedExpiry: time.Now().Add(100 * time.Second), expectedExpiry: mockClock.Now().Add(100 * time.Second),
expectedErr: nil, expectedErr: nil,
}, },
{ // 11. Return error when EX flag is passed without seconds value { // 11. Return error when EX flag is passed without seconds value
@@ -171,7 +176,7 @@ func Test_HandleSET(t *testing.T) {
presetValues: nil, presetValues: nil,
expectedResponse: "OK", expectedResponse: "OK",
expectedValue: "value14", expectedValue: "value14",
expectedExpiry: time.Now().Add(4096 * time.Millisecond), expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond),
expectedErr: nil, expectedErr: nil,
}, },
{ // 15. Return error when PX flag is passed without milliseconds value { // 15. Return error when PX flag is passed without milliseconds value
@@ -201,19 +206,19 @@ func Test_HandleSET(t *testing.T) {
{ // 18. Set exact expiry time in seconds from unix epoch { // 18. Set exact expiry time in seconds from unix epoch
command: []string{ command: []string{
"SET", "SetKey18", "value18", "SET", "SetKey18", "value18",
"EXAT", fmt.Sprintf("%d", time.Now().Add(200*time.Second).Unix()), "EXAT", fmt.Sprintf("%d", mockClock.Now().Add(200*time.Second).Unix()),
}, },
presetValues: nil, presetValues: nil,
expectedResponse: "OK", expectedResponse: "OK",
expectedValue: "value18", expectedValue: "value18",
expectedExpiry: time.Now().Add(200 * time.Second), expectedExpiry: mockClock.Now().Add(200 * time.Second),
expectedErr: nil, expectedErr: nil,
}, },
{ // 19. Return error when trying to set exact seconds expiry time when expiry time is already provided { // 19. Return error when trying to set exact seconds expiry time when expiry time is already provided
command: []string{ command: []string{
"SET", "SetKey19", "value19", "SET", "SetKey19", "value19",
"EX", "10", "EX", "10",
"EXAT", fmt.Sprintf("%d", time.Now().Add(200*time.Second).Unix()), "EXAT", fmt.Sprintf("%d", mockClock.Now().Add(200*time.Second).Unix()),
}, },
presetValues: nil, presetValues: nil,
expectedResponse: nil, expectedResponse: nil,
@@ -240,19 +245,19 @@ func Test_HandleSET(t *testing.T) {
{ // 22. Set exact expiry time in milliseconds from unix epoch { // 22. Set exact expiry time in milliseconds from unix epoch
command: []string{ command: []string{
"SET", "SetKey22", "value22", "SET", "SetKey22", "value22",
"PXAT", fmt.Sprintf("%d", time.Now().Add(4096*time.Millisecond).UnixMilli()), "PXAT", fmt.Sprintf("%d", mockClock.Now().Add(4096*time.Millisecond).UnixMilli()),
}, },
presetValues: nil, presetValues: nil,
expectedResponse: "OK", expectedResponse: "OK",
expectedValue: "value22", expectedValue: "value22",
expectedExpiry: time.Now().Add(4096 * time.Millisecond), expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond),
expectedErr: nil, expectedErr: nil,
}, },
{ // 23. Return error when trying to set exact milliseconds expiry time when expiry time is already provided { // 23. Return error when trying to set exact milliseconds expiry time when expiry time is already provided
command: []string{ command: []string{
"SET", "SetKey23", "value23", "SET", "SetKey23", "value23",
"PX", "1000", "PX", "1000",
"PXAT", fmt.Sprintf("%d", time.Now().Add(4096*time.Millisecond).UnixMilli()), "PXAT", fmt.Sprintf("%d", mockClock.Now().Add(4096*time.Millisecond).UnixMilli()),
}, },
presetValues: nil, presetValues: nil,
expectedResponse: nil, expectedResponse: nil,
@@ -286,7 +291,7 @@ func Test_HandleSET(t *testing.T) {
}, },
expectedResponse: "previous-value", expectedResponse: "previous-value",
expectedValue: "value26", expectedValue: "value26",
expectedExpiry: time.Now().Add(1000 * time.Second), expectedExpiry: mockClock.Now().Add(1000 * time.Second),
expectedErr: nil, expectedErr: nil,
}, },
{ // 27. Return nil when GET value is passed and no previous value exists { // 27. Return nil when GET value is passed and no previous value exists
@@ -294,7 +299,7 @@ func Test_HandleSET(t *testing.T) {
presetValues: nil, presetValues: nil,
expectedResponse: nil, expectedResponse: nil,
expectedValue: "value27", expectedValue: "value27",
expectedExpiry: time.Now().Add(1000 * time.Second), expectedExpiry: mockClock.Now().Add(1000 * time.Second),
expectedErr: nil, expectedErr: nil,
}, },
{ // 28. Throw error when unknown optional flag is passed to SET command. { // 28. Throw error when unknown optional flag is passed to SET command.
@@ -320,7 +325,7 @@ func Test_HandleSET(t *testing.T) {
} }
for i, test := range tests { for i, test := range tests {
ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SET, %d", i)) ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SET, %d", i+1))
if test.presetValues != nil { if test.presetValues != nil {
for k, v := range test.presetValues { for k, v := range test.presetValues {
@@ -384,7 +389,7 @@ func Test_HandleSET(t *testing.T) {
t.Errorf("expected value %+v, got %+v", test.expectedValue, value) t.Errorf("expected value %+v, got %+v", test.expectedValue, value)
} }
if test.expectedExpiry.Unix() != expireAt.Unix() { if test.expectedExpiry.Unix() != expireAt.Unix() {
t.Errorf("expected expiry time %d, got %d", test.expectedExpiry.Unix(), expireAt.Unix()) t.Errorf("expected expiry time %d, got %d, cmd: %+v", test.expectedExpiry.Unix(), expireAt.Unix(), test.command)
} }
} }
} }
@@ -714,7 +719,7 @@ func Test_HandlePERSIST(t *testing.T) {
{ // 1. Successfully persist a volatile key { // 1. Successfully persist a volatile key
command: []string{"PERSIST", "PersistKey1"}, command: []string{"PERSIST", "PersistKey1"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"PersistKey1": {Value: "value1", ExpireAt: time.Now().Add(1000 * time.Second)}, "PersistKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
@@ -827,17 +832,17 @@ func Test_HandleEXPIRETIME(t *testing.T) {
{ // 1. Return expire time in seconds { // 1. Return expire time in seconds
command: []string{"EXPIRETIME", "ExpireTimeKey1"}, command: []string{"EXPIRETIME", "ExpireTimeKey1"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireTimeKey1": {Value: "value1", ExpireAt: time.Now().Add(100 * time.Second)}, "ExpireTimeKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)},
}, },
expectedResponse: int(time.Now().Add(100 * time.Second).Unix()), expectedResponse: int(mockClock.Now().Add(100 * time.Second).Unix()),
expectedError: nil, expectedError: nil,
}, },
{ // 2. Return expire time in milliseconds { // 2. Return expire time in milliseconds
command: []string{"PEXPIRETIME", "ExpireTimeKey2"}, command: []string{"PEXPIRETIME", "ExpireTimeKey2"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireTimeKey2": {Value: "value2", ExpireAt: time.Now().Add(4096 * time.Millisecond)}, "ExpireTimeKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)},
}, },
expectedResponse: int(time.Now().Add(4096 * time.Millisecond).UnixMilli()), expectedResponse: int(mockClock.Now().Add(4096 * time.Millisecond).UnixMilli()),
expectedError: nil, expectedError: nil,
}, },
{ // 3. If the key is non-volatile, return -1 { // 3. If the key is non-volatile, return -1
@@ -920,7 +925,7 @@ func Test_HandleTTL(t *testing.T) {
{ // 1. Return TTL time in seconds { // 1. Return TTL time in seconds
command: []string{"TTL", "TTLKey1"}, command: []string{"TTL", "TTLKey1"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"TTLKey1": {Value: "value1", ExpireAt: time.Now().Add(100 * time.Second)}, "TTLKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)},
}, },
expectedResponse: 100, expectedResponse: 100,
expectedError: nil, expectedError: nil,
@@ -928,7 +933,7 @@ func Test_HandleTTL(t *testing.T) {
{ // 2. Return TTL time in milliseconds { // 2. Return TTL time in milliseconds
command: []string{"PTTL", "TTLKey2"}, command: []string{"PTTL", "TTLKey2"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"TTLKey2": {Value: "value2", ExpireAt: time.Now().Add(4096 * time.Millisecond)}, "TTLKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(4096 * time.Millisecond)},
}, },
expectedResponse: 4096, expectedResponse: 4096,
expectedError: nil, expectedError: nil,
@@ -1018,7 +1023,7 @@ func Test_HandleEXPIRE(t *testing.T) {
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey1": {Value: "value1", ExpireAt: time.Now().Add(100 * time.Second)}, "ExpireKey1": {Value: "value1", ExpireAt: mockClock.Now().Add(100 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
@@ -1029,7 +1034,7 @@ func Test_HandleEXPIRE(t *testing.T) {
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey2": {Value: "value2", ExpireAt: time.Now().Add(1000 * time.Millisecond)}, "ExpireKey2": {Value: "value2", ExpireAt: mockClock.Now().Add(1000 * time.Millisecond)},
}, },
expectedError: nil, expectedError: nil,
}, },
@@ -1040,29 +1045,29 @@ func Test_HandleEXPIRE(t *testing.T) {
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey3": {Value: "value3", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireKey3": {Value: "value3", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 4. Return 0, when NX flag is provided and key already has an expiry time { // 4. Return 0, when NX flag is provided and key already has an expiry time
command: []string{"EXPIRE", "ExpireKey4", "1000", "NX"}, command: []string{"EXPIRE", "ExpireKey4", "1000", "NX"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireKey4": {Value: "value4", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedResponse: 0, expectedResponse: 0,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey4": {Value: "value4", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 5. Set new expire time from now key only when the key already has an expiry time with XX flag { // 5. Set new expire time from now key only when the key already has an expiry time with XX flag
command: []string{"EXPIRE", "ExpireKey5", "1000", "XX"}, command: []string{"EXPIRE", "ExpireKey5", "1000", "XX"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireKey5": {Value: "value5", ExpireAt: time.Now().Add(30 * time.Second)}, "ExpireKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey5": {Value: "value5", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
@@ -1080,22 +1085,22 @@ func Test_HandleEXPIRE(t *testing.T) {
{ // 7. Set expiry time when the provided time is after the current expiry time when GT flag is provided { // 7. Set expiry time when the provided time is after the current expiry time when GT flag is provided
command: []string{"EXPIRE", "ExpireKey7", "1000", "GT"}, command: []string{"EXPIRE", "ExpireKey7", "1000", "GT"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireKey7": {Value: "value7", ExpireAt: time.Now().Add(30 * time.Second)}, "ExpireKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey7": {Value: "value7", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 8. Return 0 when GT flag is passed and current expiry time is greater than provided time { // 8. Return 0 when GT flag is passed and current expiry time is greater than provided time
command: []string{"EXPIRE", "ExpireKey8", "1000", "GT"}, command: []string{"EXPIRE", "ExpireKey8", "1000", "GT"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireKey8": {Value: "value8", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedResponse: 0, expectedResponse: 0,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey8": {Value: "value8", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
@@ -1113,22 +1118,22 @@ func Test_HandleEXPIRE(t *testing.T) {
{ // 10. Set expiry time when the provided time is before the current expiry time when LT flag is provided { // 10. Set expiry time when the provided time is before the current expiry time when LT flag is provided
command: []string{"EXPIRE", "ExpireKey10", "1000", "LT"}, command: []string{"EXPIRE", "ExpireKey10", "1000", "LT"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireKey10": {Value: "value10", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey10": {Value: "value10", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 11. Return 0 when LT flag is passed and current expiry time is less than provided time { // 11. Return 0 when LT flag is passed and current expiry time is less than provided time
command: []string{"EXPIRE", "ExpireKey11", "5000", "LT"}, command: []string{"EXPIRE", "ExpireKey11", "5000", "LT"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireKey11": {Value: "value11", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedResponse: 0, expectedResponse: 0,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey11": {Value: "value11", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
@@ -1139,7 +1144,7 @@ func Test_HandleEXPIRE(t *testing.T) {
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireKey12": {Value: "value12", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireKey12": {Value: "value12", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
@@ -1245,67 +1250,67 @@ func Test_HandleEXPIREAT(t *testing.T) {
expectedError error expectedError error
}{ }{
{ // 1. Set new expire by unix seconds { // 1. Set new expire by unix seconds
command: []string{"EXPIREAT", "ExpireAtKey1", fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix())}, command: []string{"EXPIREAT", "ExpireAtKey1", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix())},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey1": {Value: "value1", ExpireAt: time.Time{}}, "ExpireAtKey1": {Value: "value1", ExpireAt: time.Time{}},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey1": {Value: "value1", ExpireAt: time.Unix(time.Now().Add(1000*time.Second).Unix(), 0)}, "ExpireAtKey1": {Value: "value1", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 2. Set new expire by milliseconds { // 2. Set new expire by milliseconds
command: []string{"PEXPIREAT", "ExpireAtKey2", fmt.Sprintf("%d", time.Now().Add(1000*time.Second).UnixMilli())}, command: []string{"PEXPIREAT", "ExpireAtKey2", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).UnixMilli())},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey2": {Value: "value2", ExpireAt: time.Time{}}, "ExpireAtKey2": {Value: "value2", ExpireAt: time.Time{}},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey2": {Value: "value2", ExpireAt: time.UnixMilli(time.Now().Add(1000 * time.Second).UnixMilli())}, "ExpireAtKey2": {Value: "value2", ExpireAt: time.UnixMilli(mockClock.Now().Add(1000 * time.Second).UnixMilli())},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 3. Set new expire only when key does not have an expiry time with NX flag { // 3. Set new expire only when key does not have an expiry time with NX flag
command: []string{"EXPIREAT", "ExpireAtKey3", fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "NX"}, command: []string{"EXPIREAT", "ExpireAtKey3", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "NX"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey3": {Value: "value3", ExpireAt: time.Time{}}, "ExpireAtKey3": {Value: "value3", ExpireAt: time.Time{}},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey3": {Value: "value3", ExpireAt: time.Unix(time.Now().Add(1000*time.Second).Unix(), 0)}, "ExpireAtKey3": {Value: "value3", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 4. Return 0, when NX flag is provided and key already has an expiry time { // 4. Return 0, when NX flag is provided and key already has an expiry time
command: []string{"EXPIREAT", "ExpireAtKey4", fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "NX"}, command: []string{"EXPIREAT", "ExpireAtKey4", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "NX"},
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey4": {Value: "value4", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireAtKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedResponse: 0, expectedResponse: 0,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey4": {Value: "value4", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireAtKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 5. Set new expire time from now key only when the key already has an expiry time with XX flag { // 5. Set new expire time from now key only when the key already has an expiry time with XX flag
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey5", "EXPIREAT", "ExpireAtKey5",
fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "XX", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "XX",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey5": {Value: "value5", ExpireAt: time.Now().Add(30 * time.Second)}, "ExpireAtKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey5": {Value: "value5", ExpireAt: time.Unix(time.Now().Add(1000*time.Second).Unix(), 0)}, "ExpireAtKey5": {Value: "value5", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 6. Return 0 when key does not have an expiry and the XX flag is provided { // 6. Return 0 when key does not have an expiry and the XX flag is provided
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey6", "EXPIREAT", "ExpireAtKey6",
fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "XX", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "XX",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey6": {Value: "value6", ExpireAt: time.Time{}}, "ExpireAtKey6": {Value: "value6", ExpireAt: time.Time{}},
@@ -1319,35 +1324,35 @@ func Test_HandleEXPIREAT(t *testing.T) {
{ // 7. Set expiry time when the provided time is after the current expiry time when GT flag is provided { // 7. Set expiry time when the provided time is after the current expiry time when GT flag is provided
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey7", "EXPIREAT", "ExpireAtKey7",
fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "GT", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey7": {Value: "value7", ExpireAt: time.Now().Add(30 * time.Second)}, "ExpireAtKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey7": {Value: "value7", ExpireAt: time.Unix(time.Now().Add(1000*time.Second).Unix(), 0)}, "ExpireAtKey7": {Value: "value7", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 8. Return 0 when GT flag is passed and current expiry time is greater than provided time { // 8. Return 0 when GT flag is passed and current expiry time is greater than provided time
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey8", "EXPIREAT", "ExpireAtKey8",
fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "GT", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey8": {Value: "value8", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireAtKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedResponse: 0, expectedResponse: 0,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey8": {Value: "value8", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireAtKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 9. Return 0 when GT flag is passed and key does not have an expiry time { // 9. Return 0 when GT flag is passed and key does not have an expiry time
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey9", "EXPIREAT", "ExpireAtKey9",
fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "GT", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "GT",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey9": {Value: "value9", ExpireAt: time.Time{}}, "ExpireAtKey9": {Value: "value9", ExpireAt: time.Time{}},
@@ -1361,42 +1366,42 @@ func Test_HandleEXPIREAT(t *testing.T) {
{ // 10. Set expiry time when the provided time is before the current expiry time when LT flag is provided { // 10. Set expiry time when the provided time is before the current expiry time when LT flag is provided
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey10", "EXPIREAT", "ExpireAtKey10",
fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "LT", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "LT",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey10": {Value: "value10", ExpireAt: time.Now().Add(3000 * time.Second)}, "ExpireAtKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey10": {Value: "value10", ExpireAt: time.Unix(time.Now().Add(1000*time.Second).Unix(), 0)}, "ExpireAtKey10": {Value: "value10", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 11. Return 0 when LT flag is passed and current expiry time is less than provided time { // 11. Return 0 when LT flag is passed and current expiry time is less than provided time
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey11", "EXPIREAT", "ExpireAtKey11",
fmt.Sprintf("%d", time.Now().Add(3000*time.Second).Unix()), "LT", fmt.Sprintf("%d", mockClock.Now().Add(3000*time.Second).Unix()), "LT",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey11": {Value: "value11", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireAtKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedResponse: 0, expectedResponse: 0,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey11": {Value: "value11", ExpireAt: time.Now().Add(1000 * time.Second)}, "ExpireAtKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
}, },
expectedError: nil, expectedError: nil,
}, },
{ // 12. Return 0 when LT flag is passed and key does not have an expiry time { // 12. Return 0 when LT flag is passed and key does not have an expiry time
command: []string{ command: []string{
"EXPIREAT", "ExpireAtKey12", "EXPIREAT", "ExpireAtKey12",
fmt.Sprintf("%d", time.Now().Add(1000*time.Second).Unix()), "LT", fmt.Sprintf("%d", mockClock.Now().Add(1000*time.Second).Unix()), "LT",
}, },
presetValues: map[string]KeyData{ presetValues: map[string]KeyData{
"ExpireAtKey12": {Value: "value12", ExpireAt: time.Time{}}, "ExpireAtKey12": {Value: "value12", ExpireAt: time.Time{}},
}, },
expectedResponse: 1, expectedResponse: 1,
expectedValues: map[string]KeyData{ expectedValues: map[string]KeyData{
"ExpireAtKey12": {Value: "value12", ExpireAt: time.Unix(time.Now().Add(1000*time.Second).Unix(), 0)}, "ExpireAtKey12": {Value: "value12", ExpireAt: time.Unix(mockClock.Now().Add(1000*time.Second).Unix(), 0)},
}, },
expectedError: nil, expectedError: nil,
}, },

View File

@@ -17,6 +17,7 @@ package generic
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/echovault/echovault/internal/clock"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -28,28 +29,28 @@ type SetParams struct {
expireAt interface{} // Exact expireAt time un unix milliseconds expireAt interface{} // Exact expireAt time un unix milliseconds
} }
func getSetCommandParams(cmd []string, params SetParams) (SetParams, error) { func getSetCommandParams(clock clock.Clock, cmd []string, params SetParams) (SetParams, error) {
if len(cmd) == 0 { if len(cmd) == 0 {
return params, nil return params, nil
} }
switch strings.ToLower(cmd[0]) { switch strings.ToLower(cmd[0]) {
case "get": case "get":
params.get = true params.get = true
return getSetCommandParams(cmd[1:], params) return getSetCommandParams(clock, cmd[1:], params)
case "nx": case "nx":
if params.exists != "" { if params.exists != "" {
return SetParams{}, fmt.Errorf("cannot specify NX when %s is already specified", strings.ToUpper(params.exists)) return SetParams{}, fmt.Errorf("cannot specify NX when %s is already specified", strings.ToUpper(params.exists))
} }
params.exists = "NX" params.exists = "NX"
return getSetCommandParams(cmd[1:], params) return getSetCommandParams(clock, cmd[1:], params)
case "xx": case "xx":
if params.exists != "" { if params.exists != "" {
return SetParams{}, fmt.Errorf("cannot specify XX when %s is already specified", strings.ToUpper(params.exists)) return SetParams{}, fmt.Errorf("cannot specify XX when %s is already specified", strings.ToUpper(params.exists))
} }
params.exists = "XX" params.exists = "XX"
return getSetCommandParams(cmd[1:], params) return getSetCommandParams(clock, cmd[1:], params)
case "ex": case "ex":
if len(cmd) < 2 { if len(cmd) < 2 {
@@ -63,8 +64,8 @@ func getSetCommandParams(cmd []string, params SetParams) (SetParams, error) {
if err != nil { if err != nil {
return SetParams{}, errors.New("seconds value should be an integer") return SetParams{}, errors.New("seconds value should be an integer")
} }
params.expireAt = time.Now().Add(time.Duration(seconds) * time.Second) params.expireAt = clock.Now().Add(time.Duration(seconds) * time.Second)
return getSetCommandParams(cmd[2:], params) return getSetCommandParams(clock, cmd[2:], params)
case "px": case "px":
if len(cmd) < 2 { if len(cmd) < 2 {
@@ -78,8 +79,8 @@ func getSetCommandParams(cmd []string, params SetParams) (SetParams, error) {
if err != nil { if err != nil {
return SetParams{}, errors.New("milliseconds value should be an integer") return SetParams{}, errors.New("milliseconds value should be an integer")
} }
params.expireAt = time.Now().Add(time.Duration(milliseconds) * time.Millisecond) params.expireAt = clock.Now().Add(time.Duration(milliseconds) * time.Millisecond)
return getSetCommandParams(cmd[2:], params) return getSetCommandParams(clock, cmd[2:], params)
case "exat": case "exat":
if len(cmd) < 2 { if len(cmd) < 2 {
@@ -94,7 +95,7 @@ func getSetCommandParams(cmd []string, params SetParams) (SetParams, error) {
return SetParams{}, errors.New("seconds value should be an integer") return SetParams{}, errors.New("seconds value should be an integer")
} }
params.expireAt = time.Unix(seconds, 0) params.expireAt = time.Unix(seconds, 0)
return getSetCommandParams(cmd[2:], params) return getSetCommandParams(clock, cmd[2:], params)
case "pxat": case "pxat":
if len(cmd) < 2 { if len(cmd) < 2 {
@@ -109,7 +110,7 @@ func getSetCommandParams(cmd []string, params SetParams) (SetParams, error) {
return SetParams{}, errors.New("milliseconds value should be an integer") return SetParams{}, errors.New("milliseconds value should be an integer")
} }
params.expireAt = time.UnixMilli(milliseconds) params.expireAt = time.UnixMilli(milliseconds)
return getSetCommandParams(cmd[2:], params) return getSetCommandParams(clock, cmd[2:], params)
default: default:
return SetParams{}, fmt.Errorf("unknown option %s for set command", strings.ToUpper(cmd[0])) return SetParams{}, fmt.Errorf("unknown option %s for set command", strings.ToUpper(cmd[0]))

View File

@@ -16,6 +16,7 @@ package types
import ( import (
"context" "context"
"github.com/echovault/echovault/internal/clock"
"net" "net"
"time" "time"
) )
@@ -33,6 +34,7 @@ type EchoVault interface {
SetExpiry(ctx context.Context, key string, expire time.Time, touch bool) SetExpiry(ctx context.Context, key string, expire time.Time, touch bool)
RemoveExpiry(key string) RemoveExpiry(key string)
DeleteKey(ctx context.Context, key string) error DeleteKey(ctx context.Context, key string) error
GetClock() clock.Clock
GetAllCommands() []Command GetAllCommands() []Command
GetACL() interface{} GetACL() interface{}
GetPubSub() interface{} GetPubSub() interface{}