fix(cmd): keep SetArgs ExpireAt milliseconds (#4053)

SetArgs always sent ExpireAt as EXAT with Unix seconds, so any
millisecond part was silently dropped. Send PXAT with Unix milliseconds
when ExpireAt has millisecond precision, and keep EXAT otherwise.
This commit is contained in:
Yingzi Zhang authored and GitHub committed 2026-09-30 21:41:36 +03:00
1 parent d8ee0a5e13
commit a1a0e268ba
2 files changed
+48 -2

No files matched your search

+7 -2
View File
@@ -516,7 +516,8 @@ type SetArgs struct {
MatchDigest uint64
// Zero `TTL` or `Expiration` means that the key has no expiration time.
TTL time.Duration
TTL time.Duration
// ExpireAt is sent as EXAT, or as PXAT when it has millisecond precision.
ExpireAt time.Time
// When Get is true, the command returns the old value stored at key, or nil when key did not exist.
@@ -538,7 +539,11 @@ func (c cmdable) SetArgs(ctx context.Context, key string, value interface{}, a S
}
if !a.ExpireAt.IsZero() {
args = append(args, "exat", a.ExpireAt.Unix())
if a.ExpireAt.Nanosecond() >= int(time.Millisecond) {
args = append(args, "pxat", a.ExpireAt.UnixMilli())
} else {
args = append(args, "exat", a.ExpireAt.Unix())
}
}
if a.TTL > 0 {
if usePrecise(a.TTL) {
+41
View File
@@ -4,6 +4,7 @@ import (
"context"
"reflect"
"testing"
"time"
)
// captureCmdable returns a cmdable that records the Cmder passed to it without
@@ -176,3 +177,43 @@ func TestIncrEXFloat_Args(t *testing.T) {
})
}
}
func TestSetArgs_ExpireAt(t *testing.T) {
tests := []struct {
name string
expireAt time.Time
want []interface{}
}{
{
name: "whole_seconds",
expireAt: time.Unix(1700000000, 0),
want: []interface{}{"set", "key", "value", "exat", int64(1700000000)},
},
{
name: "sub_millisecond_is_truncated",
expireAt: time.Unix(1700000000, 500_000),
want: []interface{}{"set", "key", "value", "exat", int64(1700000000)},
},
{
name: "milliseconds",
expireAt: time.UnixMilli(1700000000123),
want: []interface{}{"set", "key", "value", "pxat", int64(1700000000123)},
},
{
name: "beyond_nanosecond_range",
expireAt: time.Date(2270, time.January, 2, 3, 4, 5, 123000000, time.UTC),
want: []interface{}{"set", "key", "value", "pxat", int64(9467204645123)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cmd Cmder
c := captureCmdable(&cmd)
c.SetArgs(context.Background(), "key", "value", SetArgs{ExpireAt: tt.expireAt})
if got := cmd.Args(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("args = %v, want %v", got, tt.want)
}
})
}
}