Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iss 69 - Implement GETEX #101

Merged
merged 5 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,003 changes: 7,489 additions & 2,514 deletions coverage/coverage.out

Large diffs are not rendered by default.

70 changes: 69 additions & 1 deletion echovault/api_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,28 @@ type PExpireOptions ExpireOptions
type ExpireAtOptions ExpireOptions
type PExpireAtOptions ExpireOptions

// GetExOptions modifies the behaviour of
//
// EX - Set the specified expire time, in seconds.
//
// PX - Set the specified expire time, in milliseconds.
//
// EXAT - Set the specified Unix time at which the key will expire, in seconds.
//
// PXAT - Set the specified Unix time at which the key will expire, in milliseconds.
//
// PERSIST - Remove the time to live associated with the key.
//
// UNIXTIME - Number of seconds or miliseconds from now
type GetExOptions struct {
EX bool
PX bool
EXAT bool
PXAT bool
PERSIST bool
UNIXTIME int
}

// Set creates or modifies the value at the given key.
//
// Parameters:
Expand Down Expand Up @@ -323,7 +345,7 @@ func (server *EchoVault) Expire(key string, seconds int, options ExpireOptions)
//
// `key` - string.
//
// `milliseconds` - int - number of seconds from now.
// `milliseconds` - int - number of milliseconds from now.
//
// `options` - PExpireOptions
//
Expand Down Expand Up @@ -578,3 +600,49 @@ func (server *EchoVault) GetDel(key string) (string, error) {
}
return internal.ParseStringResponse(b)
}

// GetEx retrieves the value of the provided key and optionally sets its expiration
//
// Parameters:
//
// `key` - string - the key whose value should be retrieved and expiry set.
//
// `opts` - GetExOptions.
//
// Returns: A string representing the value at the specified key. If the value does not exist, an empty string is returned.
func (server *EchoVault) GetEx(key string, opts GetExOptions) (string, error) {

cmd := make([]string, 2)

cmd[0] = "GETEX"
cmd[1] = key

var command string

switch {
case opts.EX:
command = "EX"
case opts.PX:
command = "PX"
case opts.EXAT:
command = "EXAT"
case opts.PXAT:
command = "PXAT"
case opts.PERSIST:
command = "PERSIST"
default:
}

if command != "" {
cmd = append(cmd, command)
}
if opts.UNIXTIME != 0 {
cmd = append(cmd, strconv.Itoa(opts.UNIXTIME))
}

b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return "", err
}
return internal.ParseStringResponse(b)
}
137 changes: 137 additions & 0 deletions echovault/api_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1400,3 +1400,140 @@ func TestEchoVault_GETDEL(t *testing.T) {
})
}
}

func TestEchoVault_GETEX(t *testing.T) {
mockClock := clock.NewClock()
server := createEchoVault()

tests := []struct {
name string
presetValue interface{}
getExOpts GetExOptions
key string
want string
wantEx int
wantErr bool
}{
{
name: "1. Return string from existing key, no expire options",
presetValue: "value1",
getExOpts: GetExOptions{},
key: "key1",
want: "value1",
wantEx: -1,
wantErr: false,
},
{
name: "2. Return empty string if the key does not exist",
presetValue: nil,
getExOpts: GetExOptions{EX: true, UNIXTIME: int(mockClock.Now().Add(100 * time.Second).Unix())},
key: "key2",
want: "",
wantEx: 0,
wantErr: false,
},
{
name: "3. Return key set expiry with EX",
presetValue: "value3",
getExOpts: GetExOptions{EX: true, UNIXTIME: 100},
key: "key3",
want: "value3",
wantEx: 100,
wantErr: false,
},
{
name: "4. Return key set expiry with PX",
presetValue: "value4",
getExOpts: GetExOptions{PX: true, UNIXTIME: 100000},
key: "key4",
want: "value4",
wantEx: 100,
wantErr: false,
},
{
name: "5. Return key set expiry with EXAT",
presetValue: "value5",
getExOpts: GetExOptions{EXAT: true, UNIXTIME: int(mockClock.Now().Add(100 * time.Second).Unix())},
key: "key5",
want: "value5",
wantEx: 100,
wantErr: false,
},
{
name: "6. Return key set expiry with PXAT",
presetValue: "value6",
getExOpts: GetExOptions{PXAT: true, UNIXTIME: int(mockClock.Now().Add(100 * time.Second).UnixMilli())},
key: "key6",
want: "value6",
wantEx: 100,
wantErr: false,
},
{
name: "7. Return key passing PERSIST",
presetValue: "value7",
getExOpts: GetExOptions{PERSIST: true},
key: "key7",
want: "value7",
wantEx: -1,
wantErr: false,
},
{
name: "8. Return key passing PERSIST, and include a UNIXTIME",
presetValue: "value8",
getExOpts: GetExOptions{PERSIST: true, UNIXTIME: int(mockClock.Now().Add(100 * time.Second).Unix())},
key: "key8",
want: "value8",
wantEx: -1,
wantErr: false,
},
{
name: "9. Return key and attempt to set expiry with EX without providing UNIXTIME",
presetValue: "value9",
getExOpts: GetExOptions{EX: true},
key: "key9",
want: "value9",
wantEx: -1,
wantErr: false,
},
{
name: "10. Return key and attempt to set expiry with PXAT without providing UNIXTIME",
presetValue: "value10",
getExOpts: GetExOptions{PXAT: true},
key: "key10",
want: "value10",
wantEx: -1,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.presetValue != nil {
err := presetValue(server, context.Background(), tt.key, tt.presetValue)
if err != nil {
t.Error(err)
return
}
}
//Check value received
got, err := server.GetEx(tt.key, tt.getExOpts)
if (err != nil) != tt.wantErr {
t.Errorf("GETEX() GET error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GETEX() GET - got = %v, want %v", got, tt.want)
}
//Check expiry was set
if tt.presetValue != nil {
actual, err := server.TTL(tt.key)
if (err != nil) != tt.wantErr {
t.Errorf("GETEX() EXPIRY error = %v, wantErr %v", err, tt.wantErr)
return
}
if actual != tt.wantEx {
t.Errorf("GETEX() EXPIRY - got = %v, want %v", actual, tt.wantEx)
}
}
})
}
}
86 changes: 84 additions & 2 deletions internal/modules/generic/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,12 @@ func handleExpire(params internal.HandlerFuncParams) ([]byte, error) {
if err != nil {
return nil, errors.New("expire time must be integer")
}
expireAt := params.GetClock().Now().Add(time.Duration(n) * time.Second)

var expireAt time.Time
if strings.ToLower(params.Command[0]) == "pexpire" {
expireAt = params.GetClock().Now().Add(time.Duration(n) * time.Millisecond)
} else {
expireAt = params.GetClock().Now().Add(time.Duration(n) * time.Second)
}

if !keyExists {
Expand Down Expand Up @@ -333,9 +336,12 @@ func handleExpireAt(params internal.HandlerFuncParams) ([]byte, error) {
if err != nil {
return nil, errors.New("expire time must be integer")
}
expireAt := time.Unix(n, 0)

var expireAt time.Time
if strings.ToLower(params.Command[0]) == "pexpireat" {
expireAt = time.UnixMilli(n)
} else {
expireAt = time.Unix(n, 0)
}

if !keyExists {
Expand Down Expand Up @@ -712,6 +718,73 @@ func handleGetdel(params internal.HandlerFuncParams) ([]byte, error) {
return []byte(fmt.Sprintf("+%v\r\n", value)), nil
}

func handleGetex(params internal.HandlerFuncParams) ([]byte, error) {
keys, err := getExKeyFunc(params.Command)
if err != nil {
return nil, err
}

key := keys.ReadKeys[0]
keyExists := params.KeysExist(params.Context, []string{key})[key]

if !keyExists {
return []byte("$-1\r\n"), nil
}

value := params.GetValues(params.Context, []string{key})[key]

exkey := keys.WriteKeys[0]

cmdLen := len(params.Command)

// Handle no expire options provided
if cmdLen == 2 {
return []byte(fmt.Sprintf("+%v\r\n", value)), nil
}

// Handle persist
exCommand := strings.ToUpper(params.Command[2])
// If time is provided with PERSIST it is effectively ignored
if exCommand == "persist" {
// getValues will update key access so no need here
params.SetExpiry(params.Context, exkey, time.Time{}, false)
return []byte(fmt.Sprintf("+%v\r\n", value)), nil
}

// Handle exipre command passed but no time provided
if cmdLen == 3 {
return []byte(fmt.Sprintf("+%v\r\n", value)), nil
}

// Extract time
exTimeString := params.Command[3]
n, err := strconv.ParseInt(exTimeString, 10, 64)
if err != nil {
return []byte("$-1\r\n"), errors.New("expire time must be integer")
}

var expireAt time.Time
switch exCommand {
case "EX":
expireAt = params.GetClock().Now().Add(time.Duration(n) * time.Second)
case "PX":
expireAt = params.GetClock().Now().Add(time.Duration(n) * time.Millisecond)
case "EXAT":
expireAt = time.Unix(n, 0)
case "PXAT":
expireAt = time.UnixMilli(n)
case "PERSIST":
expireAt = time.Time{}
default:
return nil, fmt.Errorf("unknown option %s -- '%v'", strings.ToUpper(exCommand), params.Command)
}

params.SetExpiry(params.Context, exkey, expireAt, false)

return []byte(fmt.Sprintf("+%v\r\n", value)), nil

}

func Commands() []internal.Command {
return []internal.Command{
{
Expand Down Expand Up @@ -1004,5 +1077,14 @@ Delete all the keys in the currently selected database. This command is always s
KeyExtractionFunc: getDelKeyFunc,
HandlerFunc: handleGetdel,
},
{
Command: "getex",
Module: constants.GenericModule,
Categories: []string{constants.WriteCategory, constants.FastCategory},
Description: "(GETEX key [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | PERSIST]) Get the value of key and optionally set its expiration. GETEX is similar to [GET], but is a write command with additional options.",
Sync: true,
KeyExtractionFunc: getExKeyFunc,
HandlerFunc: handleGetex,
},
}
}
Loading
Loading