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

View File

@@ -19,6 +19,7 @@ import (
"bytes"
"errors"
"fmt"
"github.com/echovault/echovault/internal/clock"
"io"
"log"
"os"
@@ -36,6 +37,7 @@ type AppendReadWriter interface {
}
type AppendStore struct {
clock clock.Clock
strategy string // Append file sync strategy. Can only be "always", "everysec", or "no
mut sync.Mutex // Store mutex
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
}
func WithClock(clock clock.Clock) func(store *AppendStore) {
return func(store *AppendStore) {
store.clock = clock
}
}
func WithStrategy(strategy string) func(store *AppendStore) {
return func(store *AppendStore) {
store.strategy = strategy
@@ -69,6 +77,7 @@ func WithHandleCommandFunc(f func(command []byte)) func(store *AppendStore) {
func NewAppendStore(options ...func(store *AppendStore)) *AppendStore {
store := &AppendStore{
clock: clock.NewClock(),
directory: "",
strategy: "everysec",
rw: nil,
@@ -103,7 +112,7 @@ func NewAppendStore(options ...func(store *AppendStore)) *AppendStore {
log.Println(fmt.Errorf("new append store error: %+v", err))
break
}
<-time.After(1 * time.Second)
<-store.clock.After(1 * time.Second)
}
}()
}

View File

@@ -18,12 +18,12 @@ import (
"encoding/json"
"fmt"
"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/clock"
"io"
"log"
"os"
"path"
"sync"
"time"
)
type PreambleReadWriter interface {
@@ -34,6 +34,7 @@ type PreambleReadWriter interface {
}
type PreambleStore struct {
clock clock.Clock
rw PreambleReadWriter
mut sync.Mutex
directory string
@@ -41,6 +42,12 @@ type PreambleStore struct {
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) {
return func(store *PreambleStore) {
store.rw = rw
@@ -67,6 +74,7 @@ func WithDirectory(directory string) func(store *PreambleStore) {
func NewPreambleStore(options ...func(store *PreambleStore)) *PreambleStore {
store := &PreambleStore{
clock: clock.NewClock(),
rw: nil,
mut: sync.Mutex{},
directory: "",
@@ -166,7 +174,7 @@ func (store *PreambleStore) Close() error {
func (store *PreambleStore) filterExpiredKeys(state map[string]internal.KeyData) map[string]internal.KeyData {
var keysToDelete []string
for k, v := range state {
if v.ExpireAt.Before(time.Now()) {
if v.ExpireAt.Before(store.clock.Now()) {
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"
"fmt"
"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/clock"
"io"
"io/fs"
"log"
@@ -37,6 +38,7 @@ type Manifest struct {
}
type Engine struct {
clock clock.Clock
changeCount uint64
directory string
snapshotInterval time.Duration
@@ -49,6 +51,12 @@ type Engine struct {
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) {
return func(engine *Engine) {
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 {
engine := &Engine{
clock: clock.NewClock(),
changeCount: 0,
directory: "",
snapshotInterval: 5 * time.Minute,
@@ -128,7 +137,7 @@ func NewSnapshotEngine(options ...func(engine *Engine)) *Engine {
if engine.snapshotInterval != 0 {
go func() {
for {
<-time.After(engine.snapshotInterval)
<-engine.clock.After(engine.snapshotInterval)
if engine.changeCount == engine.snapshotThreshold {
if err := engine.TakeSnapshot(); err != nil {
log.Println(err)
@@ -146,7 +155,7 @@ func (engine *Engine) TakeSnapshot() error {
defer engine.finishSnapshotFunc()
// Extract current time
now := time.Now()
now := engine.clock.Now()
msec := now.UnixNano() / int64(time.Millisecond)
// Update manifest file to indicate the latest snapshot.

View File

@@ -16,6 +16,7 @@ package echovault
import (
"github.com/echovault/echovault/internal"
"github.com/echovault/echovault/internal/clock"
"github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/pkg/commands"
"github.com/echovault/echovault/pkg/constants"
@@ -26,13 +27,6 @@ import (
"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) {
server, _ := NewEchoVault(
WithCommands(commands.All()),
@@ -81,6 +75,8 @@ func TestEchoVault_DEL(t *testing.T) {
}
func TestEchoVault_EXPIRE(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault(
WithCommands(commands.All()),
WithConfig(config.Config{
@@ -142,7 +138,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000,
expireOpts: EXPIREOptions{NX: true},
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,
wantErr: false,
@@ -154,7 +150,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000,
expireOpts: EXPIREOptions{XX: true},
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,
wantErr: false,
@@ -178,7 +174,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 100000,
expireOpts: EXPIREOptions{GT: true},
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,
wantErr: false,
@@ -190,7 +186,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000,
expireOpts: EXPIREOptions{GT: true},
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,
wantErr: false,
@@ -214,7 +210,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 1000,
expireOpts: EXPIREOptions{LT: true},
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,
wantErr: false,
@@ -226,7 +222,7 @@ func TestEchoVault_EXPIRE(t *testing.T) {
time: 50000,
expireOpts: EXPIREOptions{LT: true},
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,
wantErr: false,
@@ -258,6 +254,8 @@ func TestEchoVault_EXPIRE(t *testing.T) {
}
func TestEchoVault_EXPIREAT(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault(
WithCommands(commands.All()),
WithConfig(config.Config{
@@ -281,7 +279,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
cmd: "EXPIREAT",
key: "key1",
expireAtOpts: EXPIREATOptions{},
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
presetValues: map[string]internal.KeyData{
"key1": {Value: "value1", ExpireAt: time.Time{}},
},
@@ -293,7 +291,7 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
cmd: "PEXPIREAT",
key: "key2",
pexpireAtOpts: PEXPIREATOptions{},
time: int(timeNow().Add(1000 * time.Second).UnixMilli()),
time: int(mockClock.Now().Add(1000 * time.Second).UnixMilli()),
presetValues: map[string]internal.KeyData{
"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",
cmd: "EXPIREAT",
key: "key3",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{NX: true},
presetValues: map[string]internal.KeyData{
"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",
cmd: "EXPIREAT",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{NX: true},
key: "key4",
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,
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",
cmd: "EXPIREAT",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
key: "key5",
expireAtOpts: EXPIREATOptions{XX: true},
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,
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",
cmd: "EXPIREAT",
key: "key6",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{XX: true},
presetValues: map[string]internal.KeyData{
"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",
cmd: "EXPIREAT",
key: "key7",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{GT: true},
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,
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",
cmd: "EXPIREAT",
key: "key8",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{GT: true},
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,
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",
cmd: "EXPIREAT",
key: "key9",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{GT: true},
presetValues: map[string]internal.KeyData{
"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",
cmd: "EXPIREAT",
key: "key10",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{LT: true},
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,
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",
cmd: "EXPIREAT",
key: "key11",
time: int(timeNow().Add(3000 * time.Second).Unix()),
time: int(mockClock.Now().Add(3000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{LT: true},
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,
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",
cmd: "EXPIREAT",
key: "key12",
time: int(timeNow().Add(1000 * time.Second).Unix()),
time: int(mockClock.Now().Add(1000 * time.Second).Unix()),
expireAtOpts: EXPIREATOptions{LT: true},
presetValues: map[string]internal.KeyData{
"key12": {Value: "value12", ExpireAt: time.Time{}},
@@ -446,6 +444,8 @@ func TestEchoVault_EXPIREAT(t *testing.T) {
}
func TestEchoVault_EXPIRETIME(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault(
WithCommands(commands.All()),
WithConfig(config.Config{
@@ -465,20 +465,20 @@ func TestEchoVault_EXPIRETIME(t *testing.T) {
name: "Return expire time in seconds",
key: "key1",
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,
want: int(timeNow().Add(100 * time.Second).Unix()),
want: int(mockClock.Now().Add(100 * time.Second).Unix()),
wantErr: false,
},
{
name: "Return expire time in milliseconds",
key: "key2",
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,
want: int(timeNow().Add(4096 * time.Millisecond).UnixMilli()),
want: int(mockClock.Now().Add(4096 * time.Millisecond).UnixMilli()),
wantErr: false,
},
{
@@ -623,6 +623,8 @@ func TestEchoVault_MGET(t *testing.T) {
}
func TestEchoVault_SET(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault(
WithCommands(commands.All()),
WithConfig(config.Config{
@@ -717,7 +719,7 @@ func TestEchoVault_SET(t *testing.T) {
presetValues: nil,
key: "key8",
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",
wantErr: false,
},
@@ -725,7 +727,7 @@ func TestEchoVault_SET(t *testing.T) {
name: "Set exact expiry time in milliseconds from unix epoch",
key: "key9",
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,
want: "OK",
wantErr: false,
@@ -809,6 +811,8 @@ func TestEchoVault_MSET(t *testing.T) {
}
func TestEchoVault_PERSIST(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault(
WithCommands(commands.All()),
WithConfig(config.Config{
@@ -827,7 +831,7 @@ func TestEchoVault_PERSIST(t *testing.T) {
name: "Successfully persist a volatile key",
key: "key1",
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,
wantErr: false,
@@ -869,6 +873,8 @@ func TestEchoVault_PERSIST(t *testing.T) {
}
func TestEchoVault_TTL(t *testing.T) {
mockClock := clock.NewClock()
server, _ := NewEchoVault(
WithCommands(commands.All()),
WithConfig(config.Config{
@@ -888,10 +894,10 @@ func TestEchoVault_TTL(t *testing.T) {
name: "Return TTL time in seconds",
key: "key1",
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,
want: 19930,
want: 100,
wantErr: false,
},
{
@@ -899,9 +905,9 @@ func TestEchoVault_TTL(t *testing.T) {
key: "key2",
ttlFunc: server.PTTL,
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,
},
{

View File

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

View File

@@ -118,7 +118,7 @@ func (server *EchoVault) KeyExists(ctx context.Context, key string) bool {
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 in standalone mode, delete the key directly.
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 server.store[k].ExpireAt.After(time.Now()) {
if server.store[k].ExpireAt.After(server.clock.Now()) {
server.KeyRUnlock(ctx, k)
continue
}

View File

@@ -42,8 +42,9 @@ func handleSet(ctx context.Context, cmd []string, server types.EchoVault, _ *net
key := keys[0]
value := cmd[2]
res := []byte(constants.OkResponse)
clock := server.GetClock()
params, err := getSetCommandParams(cmd[3:], SetParams{})
params, err := getSetCommandParams(clock, cmd[3:], SetParams{})
if err != nil {
return nil, err
}
@@ -308,6 +309,8 @@ func handleTTL(ctx context.Context, cmd []string, server types.EchoVault, _ *net
key := keys[0]
clock := server.GetClock()
if !server.KeyExists(ctx, key) {
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
}
t := expireAt.Unix() - time.Now().Unix()
t := expireAt.Unix() - clock.Now().Unix()
if strings.ToLower(cmd[0]) == "pttl" {
t = expireAt.UnixMilli() - time.Now().UnixMilli()
t = expireAt.UnixMilli() - clock.Now().UnixMilli()
}
if t <= 0 {
@@ -348,9 +351,9 @@ func handleExpire(ctx context.Context, cmd []string, server types.EchoVault, _ *
if err != nil {
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" {
expireAt = time.Now().Add(time.Duration(n) * time.Millisecond)
expireAt = server.GetClock().Now().Add(time.Duration(n) * time.Millisecond)
}
if !server.KeyExists(ctx, key) {

View File

@@ -19,6 +19,7 @@ import (
"context"
"errors"
"fmt"
"github.com/echovault/echovault/internal/clock"
"github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/pkg/constants"
"github.com/echovault/echovault/pkg/echovault"
@@ -29,12 +30,16 @@ import (
var mockServer *echovault.EchoVault
var mockClock clock.Clock
type KeyData struct {
Value interface{}
ExpireAt time.Time
}
func init() {
mockClock = clock.NewClock()
mockServer, _ = echovault.NewEchoVault(
echovault.WithConfig(config.Config{
DataDir: "",
@@ -139,7 +144,7 @@ func Test_HandleSET(t *testing.T) {
presetValues: nil,
expectedResponse: "OK",
expectedValue: "value10",
expectedExpiry: time.Now().Add(100 * time.Second),
expectedExpiry: mockClock.Now().Add(100 * time.Second),
expectedErr: nil,
},
{ // 11. Return error when EX flag is passed without seconds value
@@ -171,7 +176,7 @@ func Test_HandleSET(t *testing.T) {
presetValues: nil,
expectedResponse: "OK",
expectedValue: "value14",
expectedExpiry: time.Now().Add(4096 * time.Millisecond),
expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond),
expectedErr: nil,
},
{ // 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
command: []string{
"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,
expectedResponse: "OK",
expectedValue: "value18",
expectedExpiry: time.Now().Add(200 * time.Second),
expectedExpiry: mockClock.Now().Add(200 * time.Second),
expectedErr: nil,
},
{ // 19. Return error when trying to set exact seconds expiry time when expiry time is already provided
command: []string{
"SET", "SetKey19", "value19",
"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,
expectedResponse: nil,
@@ -240,19 +245,19 @@ func Test_HandleSET(t *testing.T) {
{ // 22. Set exact expiry time in milliseconds from unix epoch
command: []string{
"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,
expectedResponse: "OK",
expectedValue: "value22",
expectedExpiry: time.Now().Add(4096 * time.Millisecond),
expectedExpiry: mockClock.Now().Add(4096 * time.Millisecond),
expectedErr: nil,
},
{ // 23. Return error when trying to set exact milliseconds expiry time when expiry time is already provided
command: []string{
"SET", "SetKey23", "value23",
"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,
expectedResponse: nil,
@@ -286,7 +291,7 @@ func Test_HandleSET(t *testing.T) {
},
expectedResponse: "previous-value",
expectedValue: "value26",
expectedExpiry: time.Now().Add(1000 * time.Second),
expectedExpiry: mockClock.Now().Add(1000 * time.Second),
expectedErr: nil,
},
{ // 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,
expectedResponse: nil,
expectedValue: "value27",
expectedExpiry: time.Now().Add(1000 * time.Second),
expectedExpiry: mockClock.Now().Add(1000 * time.Second),
expectedErr: nil,
},
{ // 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 {
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 {
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)
}
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
command: []string{"PERSIST", "PersistKey1"},
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,
expectedValues: map[string]KeyData{
@@ -827,17 +832,17 @@ func Test_HandleEXPIRETIME(t *testing.T) {
{ // 1. Return expire time in seconds
command: []string{"EXPIRETIME", "ExpireTimeKey1"},
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,
},
{ // 2. Return expire time in milliseconds
command: []string{"PEXPIRETIME", "ExpireTimeKey2"},
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,
},
{ // 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
command: []string{"TTL", "TTLKey1"},
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,
expectedError: nil,
@@ -928,7 +933,7 @@ func Test_HandleTTL(t *testing.T) {
{ // 2. Return TTL time in milliseconds
command: []string{"PTTL", "TTLKey2"},
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,
expectedError: nil,
@@ -1018,7 +1023,7 @@ func Test_HandleEXPIRE(t *testing.T) {
},
expectedResponse: 1,
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,
},
@@ -1029,7 +1034,7 @@ func Test_HandleEXPIRE(t *testing.T) {
},
expectedResponse: 1,
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,
},
@@ -1040,29 +1045,29 @@ func Test_HandleEXPIRE(t *testing.T) {
},
expectedResponse: 1,
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,
},
{ // 4. Return 0, when NX flag is provided and key already has an expiry time
command: []string{"EXPIRE", "ExpireKey4", "1000", "NX"},
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,
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,
},
{ // 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"},
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,
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,
},
@@ -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
command: []string{"EXPIRE", "ExpireKey7", "1000", "GT"},
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,
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,
},
{ // 8. Return 0 when GT flag is passed and current expiry time is greater than provided time
command: []string{"EXPIRE", "ExpireKey8", "1000", "GT"},
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,
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,
},
@@ -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
command: []string{"EXPIRE", "ExpireKey10", "1000", "LT"},
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,
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,
},
{ // 11. Return 0 when LT flag is passed and current expiry time is less than provided time
command: []string{"EXPIRE", "ExpireKey11", "5000", "LT"},
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,
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,
},
@@ -1139,7 +1144,7 @@ func Test_HandleEXPIRE(t *testing.T) {
},
expectedResponse: 1,
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,
},
@@ -1245,67 +1250,67 @@ func Test_HandleEXPIREAT(t *testing.T) {
expectedError error
}{
{ // 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{
"ExpireAtKey1": {Value: "value1", ExpireAt: time.Time{}},
},
expectedResponse: 1,
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,
},
{ // 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{
"ExpireAtKey2": {Value: "value2", ExpireAt: time.Time{}},
},
expectedResponse: 1,
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,
},
{ // 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{
"ExpireAtKey3": {Value: "value3", ExpireAt: time.Time{}},
},
expectedResponse: 1,
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,
},
{ // 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{
"ExpireAtKey4": {Value: "value4", ExpireAt: time.Now().Add(1000 * time.Second)},
"ExpireAtKey4": {Value: "value4", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
},
expectedResponse: 0,
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,
},
{ // 5. Set new expire time from now key only when the key already has an expiry time with XX flag
command: []string{
"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{
"ExpireAtKey5": {Value: "value5", ExpireAt: time.Now().Add(30 * time.Second)},
"ExpireAtKey5": {Value: "value5", ExpireAt: mockClock.Now().Add(30 * time.Second)},
},
expectedResponse: 1,
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,
},
{ // 6. Return 0 when key does not have an expiry and the XX flag is provided
command: []string{
"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{
"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
command: []string{
"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{
"ExpireAtKey7": {Value: "value7", ExpireAt: time.Now().Add(30 * time.Second)},
"ExpireAtKey7": {Value: "value7", ExpireAt: mockClock.Now().Add(30 * time.Second)},
},
expectedResponse: 1,
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,
},
{ // 8. Return 0 when GT flag is passed and current expiry time is greater than provided time
command: []string{
"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{
"ExpireAtKey8": {Value: "value8", ExpireAt: time.Now().Add(3000 * time.Second)},
"ExpireAtKey8": {Value: "value8", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
},
expectedResponse: 0,
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,
},
{ // 9. Return 0 when GT flag is passed and key does not have an expiry time
command: []string{
"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{
"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
command: []string{
"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{
"ExpireAtKey10": {Value: "value10", ExpireAt: time.Now().Add(3000 * time.Second)},
"ExpireAtKey10": {Value: "value10", ExpireAt: mockClock.Now().Add(3000 * time.Second)},
},
expectedResponse: 1,
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,
},
{ // 11. Return 0 when LT flag is passed and current expiry time is less than provided time
command: []string{
"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{
"ExpireAtKey11": {Value: "value11", ExpireAt: time.Now().Add(1000 * time.Second)},
"ExpireAtKey11": {Value: "value11", ExpireAt: mockClock.Now().Add(1000 * time.Second)},
},
expectedResponse: 0,
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,
},
{ // 12. Return 0 when LT flag is passed and key does not have an expiry time
command: []string{
"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{
"ExpireAtKey12": {Value: "value12", ExpireAt: time.Time{}},
},
expectedResponse: 1,
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,
},

View File

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

View File

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