Fix KSN for HSETEX command when FXX/FNX is used (#14150)

When HSETEX fails due to FXX/FNX, it may still expire some fields due to
lazy expiry. Though, it does not send “hexpired” notification in this
case.
This commit is contained in:
Stav-Levi 2025-07-21 13:59:01 +03:00 committed by GitHub
parent 1e388d8b95
commit a4ff8d6ab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 8 deletions

View File

@ -2331,6 +2331,7 @@ err_expiration:
void hsetexCommand(client *c) {
int flags = 0, first_field_pos = 0, field_count = 0, expire_time_pos = -1;
int updated = 0, deleted = 0, set_expiry;
int expired = 0, fields_set = 0;
long long expire_time = EB_EXPIRE_TIME_INVALID;
int64_t oldlen, newlen;
HashTypeSetEx setex;
@ -2358,12 +2359,18 @@ void hsetexCommand(client *c) {
int found = 0;
for (int i = 0; i < field_count; i++) {
sds field = c->argv[first_field_pos + (i * 2)]->ptr;
unsigned char *vstr = NULL;
unsigned int vlen = UINT_MAX;
long long vll = LLONG_MAX;
const int opt = HFE_LAZY_NO_NOTIFICATION |
HFE_LAZY_NO_SIGNAL |
HFE_LAZY_AVOID_HASH_DEL |
HFE_LAZY_NO_UPDATE_KEYSIZES;
int exists = hashTypeExists(c->db, o, field, opt, NULL);
found += (exists != 0);
GetFieldRes res = hashTypeGetValue(c->db, o, field, &vstr, &vlen, &vll, opt, NULL);
int exists = (res == GETF_OK);
expired += (res == GETF_EXPIRED);
found += exists;
/* Check for early exit if the condition is already invalid. */
if (((flags & HFE_FXX) && !exists) ||
@ -2400,7 +2407,7 @@ void hsetexCommand(client *c) {
opt |= HASH_SET_KEEP_TTL;
hashTypeSet(c->db, o, field, value, opt);
fields_set = 1;
/* Update the expiration time. */
if (set_expiry) {
int ret = hashTypeSetEx(o, field, expire_time, &setex);
@ -2413,11 +2420,6 @@ void hsetexCommand(client *c) {
hashTypeSetExDone(&setex);
server.dirty += field_count;
signalModifiedKey(c, c->db, c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH, "hset", c->argv[1], c->db->id);
if (deleted || updated)
notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel": "hexpire",
c->argv[1], c->db->id);
if (deleted) {
/* If fields are deleted due to timestamp is being in the past, hdel's
@ -2437,6 +2439,17 @@ void hsetexCommand(client *c) {
addReplyLongLong(c, 1);
out:
/* Emit keyspace notifications based on field expiry, mutation, or key deletion */
if (fields_set || expired) {
signalModifiedKey(c, c->db, c->argv[1]);
if (expired)
notifyKeyspaceEvent(NOTIFY_HASH, "hexpired", c->argv[1], c->db->id);
if (fields_set) {
notifyKeyspaceEvent(NOTIFY_HASH, "hset", c->argv[1], c->db->id);
if (deleted || updated)
notifyKeyspaceEvent(NOTIFY_HASH, deleted ? "hdel" : "hexpire", c->argv[1], c->db->id);
}
}
/* Key may become empty due to lazy expiry in hashTypeExists()
* or the new expiration time is in the past.*/
newlen = (int64_t) hashTypeLength(o, 0);

View File

@ -556,6 +556,82 @@ start_server {tags {"pubsub network"}} {
$rd1 close
}
test "Keyspace notifications:FXX/FNX with HSETEX cmd" {
r config set notify-keyspace-events Khxg
r del myhash
set rd1 [redis_deferring_client]
assert_equal {1} [psubscribe $rd1 *]
r debug set-active-expire 0
# FXX on logically expired field
r hset myhash f v
r hset myhash f2 v
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
r hpexpire myhash 10 FIELDS 1 f
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
after 15
assert_equal [r HSETEX myhash FXX PX 10 FIELDS 1 f v] 0
assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
r hdel myhash f2
assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
assert_equal 0 [r exists myhash]
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
# FXX with past expiry
r HSET myhash f1 v1
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
set past [expr {[clock seconds] - 2}]
assert_equal [r hsetex myhash FXX EXAT $past FIELDS 1 f1 v1] 1
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
# FXX overwrite + full key expiry
r hset myhash f v
r hset myhash f2 v
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
r hpexpire myhash 10 FIELDS 1 f
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
after 15
set past [expr {[clock milliseconds] - 5000}]
assert_equal [r hsetex myhash FXX PXAT $past FIELDS 1 f v] 0
assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
r hpexpire myhash 10 FIELDS 1 f2
after 15
r hget myhash f2
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
# FNX on logically expired field
r del myhash
r hset myhash f v
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
r hpexpire myhash 10 FIELDS 1 f
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
after 15
assert_equal [r HSETEX myhash FNX PX 1000 FIELDS 1 f v] 1
assert_equal "pmessage * __keyspace@${db}__:myhash hexpired" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hexpire" [$rd1 read]
# FNX with past expiry
r del myhash
r hset myhash f v
assert_equal "pmessage * __keyspace@${db}__:myhash del" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
set past [expr {[clock seconds] - 2}]
assert_equal [r hsetex myhash FNX EXAT $past FIELDS 1 f1 v1] 1
# f1 is created and immediately expired
assert_equal "pmessage * __keyspace@${db}__:myhash hset" [$rd1 read]
assert_equal "pmessage * __keyspace@${db}__:myhash hdel" [$rd1 read]
r debug set-active-expire 1
$rd1 close
} {0} {needs:debug}
test "Keyspace notifications: expired events (triggered expire)" {
r config set notify-keyspace-events Ex
r del foo