Skip to content
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ See the [Usage Guide](./docs/usage.md) for installation, connecting, and a walkt
| [CONFIG](./docs/config.md) | Read and modify server configuration |
| [TYPE](./docs/type.md) | Return the type of the value stored at a key |
| [EXISTS](./docs/exists.md) | Check if one or more keys exist |
| [TTL](./docs/ttl.md) | Get remaining TTL of a key in seconds |
| [PTTL](./docs/pttl.md) | Get remaining TTL of a key in milliseconds |
52 changes: 52 additions & 0 deletions cmd/ttl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package cmd

import (
"HelixDB/common"
"HelixDB/db"
"time"
)

// TTL returns the remaining time-to-live of a key in seconds.
// Returns -1 if the key exists but has no TTL.
// Returns -2 if the key does not exist or has expired.
func TTL(command common.Cmd) ([]byte, error) {
if len(command.Args) != 1 {
err := common.WrongNumberOfArgsError(command.Name)
return common.RespError(err.Error()), err
}
return ttlMillis(command.Args[0], false), nil
}

// PTTL returns the remaining time-to-live of a key in milliseconds.
// Returns -1 if the key exists but has no TTL.
// Returns -2 if the key does not exist or has expired.
func PTTL(command common.Cmd) ([]byte, error) {
if len(command.Args) != 1 {
err := common.WrongNumberOfArgsError(command.Name)
return common.RespError(err.Error()), err
}
return ttlMillis(command.Args[0], true), nil
}

// ttlMillis looks up the TTL for key and returns it as a RESP integer.
// If millis is true the value is in milliseconds, otherwise seconds.
func ttlMillis(key string, millis bool) []byte {
expiry, ok := db.KeyTTL.Load(key)
if !ok {
// Key does not exist at all.
return common.RespInteger(-2)
}
if expiry == nil {
// Key exists with no TTL.
return common.RespInteger(-1)
}
remaining := expiry.(int64) - time.Now().UnixMilli()
if remaining <= 0 {
// Key has expired (passive check).
return common.RespInteger(-2)
}
if millis {
return common.RespInteger(int(remaining))
}
return common.RespInteger(int(remaining / 1000))
}
89 changes: 89 additions & 0 deletions cmd/ttl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package cmd

import (
"HelixDB/common"
"HelixDB/db"
"errors"
"reflect"
"testing"
)

func TestTTL(t *testing.T) {
// Key with no TTL
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"ttl_persist", "v"}})
// Key with TTL (~100 seconds from now)
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"ttl_expiring", "v", "EX", "100"}})
// Expired key (TTL set to the past)
db.DB.Store("ttl_expired", db.NewValue("v"))
db.KeyTTL.Store("ttl_expired", int64(1)) // epoch ms well in the past

tests := []struct {
command common.Cmd
want []byte
wantErr error
}{
// Key with no TTL returns -1
{common.Cmd{Name: "TTL", Args: []string{"ttl_persist"}}, common.RespInteger(-1), nil},
// Non-existent key returns -2
{common.Cmd{Name: "TTL", Args: []string{"no_such_key"}}, common.RespInteger(-2), nil},
// Expired key returns -2
{common.Cmd{Name: "TTL", Args: []string{"ttl_expired"}}, common.RespInteger(-2), nil},
// Wrong number of arguments
{common.Cmd{Name: "TTL"}, common.RespError("wrong number of arguments for 'ttl' command"), common.ErrWrongNumberOfArgs},
{common.Cmd{Name: "TTL", Args: []string{"a", "b"}}, common.RespError("wrong number of arguments for 'ttl' command"), common.ErrWrongNumberOfArgs},
}
for _, test := range tests {
if got, gotErr := TTL(test.command); !reflect.DeepEqual(got, test.want) || !errors.Is(gotErr, test.wantErr) {
t.Errorf("TTL(%v) = %v, %v; want %v, %v", test.command, got, gotErr, test.want, test.wantErr)
}
}

// Key with TTL returns a positive integer (exact value is timing-sensitive)
got, gotErr := TTL(common.Cmd{Name: "TTL", Args: []string{"ttl_expiring"}})
if gotErr != nil {
t.Errorf("TTL(ttl_expiring) unexpected error: %v", gotErr)
}
if reflect.DeepEqual(got, common.RespInteger(-1)) || reflect.DeepEqual(got, common.RespInteger(-2)) {
t.Errorf("TTL(ttl_expiring) = %v; want a positive integer", got)
}
}

func TestPTTL(t *testing.T) {
// Key with no TTL
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"pttl_persist", "v"}})
// Key with TTL (~100 seconds from now)
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"pttl_expiring", "v", "EX", "100"}})
// Expired key
db.DB.Store("pttl_expired", db.NewValue("v"))
db.KeyTTL.Store("pttl_expired", int64(1))

tests := []struct {
command common.Cmd
want []byte
wantErr error
}{
// Key with no TTL returns -1
{common.Cmd{Name: "PTTL", Args: []string{"pttl_persist"}}, common.RespInteger(-1), nil},
// Non-existent key returns -2
{common.Cmd{Name: "PTTL", Args: []string{"no_such_key"}}, common.RespInteger(-2), nil},
// Expired key returns -2
{common.Cmd{Name: "PTTL", Args: []string{"pttl_expired"}}, common.RespInteger(-2), nil},
// Wrong number of arguments
{common.Cmd{Name: "PTTL"}, common.RespError("wrong number of arguments for 'pttl' command"), common.ErrWrongNumberOfArgs},
{common.Cmd{Name: "PTTL", Args: []string{"a", "b"}}, common.RespError("wrong number of arguments for 'pttl' command"), common.ErrWrongNumberOfArgs},
}
for _, test := range tests {
if got, gotErr := PTTL(test.command); !reflect.DeepEqual(got, test.want) || !errors.Is(gotErr, test.wantErr) {
t.Errorf("PTTL(%v) = %v, %v; want %v, %v", test.command, got, gotErr, test.want, test.wantErr)
}
}

// Key with TTL returns a positive integer
got, gotErr := PTTL(common.Cmd{Name: "PTTL", Args: []string{"pttl_expiring"}})
if gotErr != nil {
t.Errorf("PTTL(pttl_expiring) unexpected error: %v", gotErr)
}
if reflect.DeepEqual(got, common.RespInteger(-1)) || reflect.DeepEqual(got, common.RespInteger(-2)) {
t.Errorf("PTTL(pttl_expiring) = %v; want a positive integer", got)
}
}
2 changes: 2 additions & 0 deletions common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const Keys = "KEYS"
const Config = "CONFIG"
const Type = "TYPE"
const Exists = "EXISTS"
const TTL = "TTL"
const PTTL = "PTTL"

// Command Args
// Expiration Args
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@
| [CONFIG](./config.md) | Read and modify server configuration |
| [TYPE](./type.md) | Return the type of the value stored at a key |
| [EXISTS](./exists.md) | Check if one or more keys exist |
| [TTL](./ttl.md) | Get remaining TTL of a key in seconds |
| [PTTL](./pttl.md) | Get remaining TTL of a key in milliseconds |
40 changes: 40 additions & 0 deletions docs/pttl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# PTTL

Returns the remaining time-to-live of a key in milliseconds.

## Syntax

```
PTTL key
```

## Return value

| Value | Meaning |
|-------|------------------------------------------|
| `N` | Remaining TTL in milliseconds (N ≥ 1) |
| `-1` | Key exists but has no TTL |
| `-2` | Key does not exist or has expired |

## Errors

- `wrong number of arguments for 'pttl' command` — incorrect number of arguments.

## Examples

```
> SET counter 42 EX 100
OK

> PTTL counter
99743

> SET greeting hello
OK

> PTTL greeting
-1

> PTTL missing
-2
```
40 changes: 40 additions & 0 deletions docs/ttl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# TTL

Returns the remaining time-to-live of a key in seconds.

## Syntax

```
TTL key
```

## Return value

| Value | Meaning |
|-------|-------------------------------------|
| `N` | Remaining TTL in seconds (N ≥ 1) |
| `-1` | Key exists but has no TTL |
| `-2` | Key does not exist or has expired |

## Errors

- `wrong number of arguments for 'ttl' command` — incorrect number of arguments.

## Examples

```
> SET counter 42 EX 100
OK

> TTL counter
99

> SET greeting hello
OK

> TTL greeting
-1

> TTL missing
-2
```
4 changes: 4 additions & 0 deletions exc/command_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func ExecuteCommand(command common.Cmd) ([]byte, error) {
return cmd.Type(command)
case common.Exists:
return cmd.Exists(command)
case common.TTL:
return cmd.TTL(command)
case common.PTTL:
return cmd.PTTL(command)
}
return []byte("-unsupported command\r\n"), UnsupportedCommand
}
Loading