1
0
mirror of https://github.com/redis/go-redis.git synced 2026-10-22 19:49:00 +03:00

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
2026-09-30 14:41:36 -04:00
committed by GitHub
parent d8ee0a5e13
commit a1a0e268ba
2 changed files with 48 additions and 2 deletions

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) {

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)
}
})
}
}