mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-10-06 00:16:53 +08:00
Implemented a Clock interface that is injected everywhere time.Now and time.After are used.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
|
@@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@@ -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
41
internal/clock/clock.go
Normal 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)
|
||||
}
|
@@ -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.
|
||||
|
@@ -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,
|
||||
},
|
||||
{
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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,
|
||||
},
|
||||
|
@@ -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]))
|
||||
|
@@ -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{}
|
||||
|
Reference in New Issue
Block a user