Skip to content

Commit 916530d

Browse files
authored
Merge pull request #108 from cankush625/ankushchavan/ttl-pttl-commands
feat(cmd): implement TTL and PTTL commands
2 parents 3b45c8b + e47bd84 commit 916530d

8 files changed

Lines changed: 231 additions & 0 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,5 @@ See the [Usage Guide](./docs/usage.md) for installation, connecting, and a walkt
2727
| [CONFIG](./docs/config.md) | Read and modify server configuration |
2828
| [TYPE](./docs/type.md) | Return the type of the value stored at a key |
2929
| [EXISTS](./docs/exists.md) | Check if one or more keys exist |
30+
| [TTL](./docs/ttl.md) | Get remaining TTL of a key in seconds |
31+
| [PTTL](./docs/pttl.md) | Get remaining TTL of a key in milliseconds |

cmd/ttl.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cmd
2+
3+
import (
4+
"HelixDB/common"
5+
"HelixDB/db"
6+
"time"
7+
)
8+
9+
// TTL returns the remaining time-to-live of a key in seconds.
10+
// Returns -1 if the key exists but has no TTL.
11+
// Returns -2 if the key does not exist or has expired.
12+
func TTL(command common.Cmd) ([]byte, error) {
13+
if len(command.Args) != 1 {
14+
err := common.WrongNumberOfArgsError(command.Name)
15+
return common.RespError(err.Error()), err
16+
}
17+
return ttlMillis(command.Args[0], false), nil
18+
}
19+
20+
// PTTL returns the remaining time-to-live of a key in milliseconds.
21+
// Returns -1 if the key exists but has no TTL.
22+
// Returns -2 if the key does not exist or has expired.
23+
func PTTL(command common.Cmd) ([]byte, error) {
24+
if len(command.Args) != 1 {
25+
err := common.WrongNumberOfArgsError(command.Name)
26+
return common.RespError(err.Error()), err
27+
}
28+
return ttlMillis(command.Args[0], true), nil
29+
}
30+
31+
// ttlMillis looks up the TTL for key and returns it as a RESP integer.
32+
// If millis is true the value is in milliseconds, otherwise seconds.
33+
func ttlMillis(key string, millis bool) []byte {
34+
expiry, ok := db.KeyTTL.Load(key)
35+
if !ok {
36+
// Key does not exist at all.
37+
return common.RespInteger(-2)
38+
}
39+
if expiry == nil {
40+
// Key exists with no TTL.
41+
return common.RespInteger(-1)
42+
}
43+
remaining := expiry.(int64) - time.Now().UnixMilli()
44+
if remaining <= 0 {
45+
// Key has expired (passive check).
46+
return common.RespInteger(-2)
47+
}
48+
if millis {
49+
return common.RespInteger(int(remaining))
50+
}
51+
return common.RespInteger(int(remaining / 1000))
52+
}

cmd/ttl_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package cmd
2+
3+
import (
4+
"HelixDB/common"
5+
"HelixDB/db"
6+
"errors"
7+
"reflect"
8+
"testing"
9+
)
10+
11+
func TestTTL(t *testing.T) {
12+
// Key with no TTL
13+
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"ttl_persist", "v"}})
14+
// Key with TTL (~100 seconds from now)
15+
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"ttl_expiring", "v", "EX", "100"}})
16+
// Expired key (TTL set to the past)
17+
db.DB.Store("ttl_expired", db.NewValue("v"))
18+
db.KeyTTL.Store("ttl_expired", int64(1)) // epoch ms well in the past
19+
20+
tests := []struct {
21+
command common.Cmd
22+
want []byte
23+
wantErr error
24+
}{
25+
// Key with no TTL returns -1
26+
{common.Cmd{Name: "TTL", Args: []string{"ttl_persist"}}, common.RespInteger(-1), nil},
27+
// Non-existent key returns -2
28+
{common.Cmd{Name: "TTL", Args: []string{"no_such_key"}}, common.RespInteger(-2), nil},
29+
// Expired key returns -2
30+
{common.Cmd{Name: "TTL", Args: []string{"ttl_expired"}}, common.RespInteger(-2), nil},
31+
// Wrong number of arguments
32+
{common.Cmd{Name: "TTL"}, common.RespError("wrong number of arguments for 'ttl' command"), common.ErrWrongNumberOfArgs},
33+
{common.Cmd{Name: "TTL", Args: []string{"a", "b"}}, common.RespError("wrong number of arguments for 'ttl' command"), common.ErrWrongNumberOfArgs},
34+
}
35+
for _, test := range tests {
36+
if got, gotErr := TTL(test.command); !reflect.DeepEqual(got, test.want) || !errors.Is(gotErr, test.wantErr) {
37+
t.Errorf("TTL(%v) = %v, %v; want %v, %v", test.command, got, gotErr, test.want, test.wantErr)
38+
}
39+
}
40+
41+
// Key with TTL returns a positive integer (exact value is timing-sensitive)
42+
got, gotErr := TTL(common.Cmd{Name: "TTL", Args: []string{"ttl_expiring"}})
43+
if gotErr != nil {
44+
t.Errorf("TTL(ttl_expiring) unexpected error: %v", gotErr)
45+
}
46+
if reflect.DeepEqual(got, common.RespInteger(-1)) || reflect.DeepEqual(got, common.RespInteger(-2)) {
47+
t.Errorf("TTL(ttl_expiring) = %v; want a positive integer", got)
48+
}
49+
}
50+
51+
func TestPTTL(t *testing.T) {
52+
// Key with no TTL
53+
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"pttl_persist", "v"}})
54+
// Key with TTL (~100 seconds from now)
55+
_, _ = Set(common.Cmd{Name: "SET", Args: []string{"pttl_expiring", "v", "EX", "100"}})
56+
// Expired key
57+
db.DB.Store("pttl_expired", db.NewValue("v"))
58+
db.KeyTTL.Store("pttl_expired", int64(1))
59+
60+
tests := []struct {
61+
command common.Cmd
62+
want []byte
63+
wantErr error
64+
}{
65+
// Key with no TTL returns -1
66+
{common.Cmd{Name: "PTTL", Args: []string{"pttl_persist"}}, common.RespInteger(-1), nil},
67+
// Non-existent key returns -2
68+
{common.Cmd{Name: "PTTL", Args: []string{"no_such_key"}}, common.RespInteger(-2), nil},
69+
// Expired key returns -2
70+
{common.Cmd{Name: "PTTL", Args: []string{"pttl_expired"}}, common.RespInteger(-2), nil},
71+
// Wrong number of arguments
72+
{common.Cmd{Name: "PTTL"}, common.RespError("wrong number of arguments for 'pttl' command"), common.ErrWrongNumberOfArgs},
73+
{common.Cmd{Name: "PTTL", Args: []string{"a", "b"}}, common.RespError("wrong number of arguments for 'pttl' command"), common.ErrWrongNumberOfArgs},
74+
}
75+
for _, test := range tests {
76+
if got, gotErr := PTTL(test.command); !reflect.DeepEqual(got, test.want) || !errors.Is(gotErr, test.wantErr) {
77+
t.Errorf("PTTL(%v) = %v, %v; want %v, %v", test.command, got, gotErr, test.want, test.wantErr)
78+
}
79+
}
80+
81+
// Key with TTL returns a positive integer
82+
got, gotErr := PTTL(common.Cmd{Name: "PTTL", Args: []string{"pttl_expiring"}})
83+
if gotErr != nil {
84+
t.Errorf("PTTL(pttl_expiring) unexpected error: %v", gotErr)
85+
}
86+
if reflect.DeepEqual(got, common.RespInteger(-1)) || reflect.DeepEqual(got, common.RespInteger(-2)) {
87+
t.Errorf("PTTL(pttl_expiring) = %v; want a positive integer", got)
88+
}
89+
}

common/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const Keys = "KEYS"
1515
const Config = "CONFIG"
1616
const Type = "TYPE"
1717
const Exists = "EXISTS"
18+
const TTL = "TTL"
19+
const PTTL = "PTTL"
1820

1921
// Command Args
2022
// Expiration Args

docs/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@
1818
| [CONFIG](./config.md) | Read and modify server configuration |
1919
| [TYPE](./type.md) | Return the type of the value stored at a key |
2020
| [EXISTS](./exists.md) | Check if one or more keys exist |
21+
| [TTL](./ttl.md) | Get remaining TTL of a key in seconds |
22+
| [PTTL](./pttl.md) | Get remaining TTL of a key in milliseconds |

docs/pttl.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# PTTL
2+
3+
Returns the remaining time-to-live of a key in milliseconds.
4+
5+
## Syntax
6+
7+
```
8+
PTTL key
9+
```
10+
11+
## Return value
12+
13+
| Value | Meaning |
14+
|-------|------------------------------------------|
15+
| `N` | Remaining TTL in milliseconds (N ≥ 1) |
16+
| `-1` | Key exists but has no TTL |
17+
| `-2` | Key does not exist or has expired |
18+
19+
## Errors
20+
21+
- `wrong number of arguments for 'pttl' command` — incorrect number of arguments.
22+
23+
## Examples
24+
25+
```
26+
> SET counter 42 EX 100
27+
OK
28+
29+
> PTTL counter
30+
99743
31+
32+
> SET greeting hello
33+
OK
34+
35+
> PTTL greeting
36+
-1
37+
38+
> PTTL missing
39+
-2
40+
```

docs/ttl.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# TTL
2+
3+
Returns the remaining time-to-live of a key in seconds.
4+
5+
## Syntax
6+
7+
```
8+
TTL key
9+
```
10+
11+
## Return value
12+
13+
| Value | Meaning |
14+
|-------|-------------------------------------|
15+
| `N` | Remaining TTL in seconds (N ≥ 1) |
16+
| `-1` | Key exists but has no TTL |
17+
| `-2` | Key does not exist or has expired |
18+
19+
## Errors
20+
21+
- `wrong number of arguments for 'ttl' command` — incorrect number of arguments.
22+
23+
## Examples
24+
25+
```
26+
> SET counter 42 EX 100
27+
OK
28+
29+
> TTL counter
30+
99
31+
32+
> SET greeting hello
33+
OK
34+
35+
> TTL greeting
36+
-1
37+
38+
> TTL missing
39+
-2
40+
```

exc/command_executor.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ func ExecuteCommand(command common.Cmd) ([]byte, error) {
3232
return cmd.Type(command)
3333
case common.Exists:
3434
return cmd.Exists(command)
35+
case common.TTL:
36+
return cmd.TTL(command)
37+
case common.PTTL:
38+
return cmd.PTTL(command)
3539
}
3640
return []byte("-unsupported command\r\n"), UnsupportedCommand
3741
}

0 commit comments

Comments
 (0)