Skip to content
Open
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 src/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -2127,6 +2127,7 @@ void createSharedObjects(void) {
shared.hpexpireat = createStringObject("HPEXPIREAT",10);
shared.hpersist = createStringObject("HPERSIST",8);
shared.hdel = createStringObject("HDEL",4);
shared.hsetex = createStringObject("HSETEX",6);

/* Shared command argument */
shared.left = createStringObject("left",4);
Expand All @@ -2149,6 +2150,7 @@ void createSharedObjects(void) {
shared.special_asterick = createStringObject("*",1);
shared.special_equals = createStringObject("=",1);
shared.redacted = makeObjectShared(createStringObject("(redacted)",10));
shared.fields = createStringObject("FIELDS",6);

for (j = 0; j < OBJ_SHARED_INTEGERS; j++) {
shared.integers[j] =
Expand Down
4 changes: 2 additions & 2 deletions src/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -1526,9 +1526,9 @@ struct sharedObjectsStruct {
*rpop, *lpop, *lpush, *rpoplpush, *lmove, *blmove, *zpopmin, *zpopmax,
*emptyscan, *multi, *exec, *left, *right, *hset, *srem, *xgroup, *xclaim,
*script, *replconf, *eval, *persist, *set, *pexpireat, *pexpire,
*hdel, *hpexpireat, *hpersist,
*hdel, *hpexpireat, *hpersist, *hsetex,
*time, *pxat, *absttl, *retrycount, *force, *justid, *entriesread,
*lastid, *ping, *setid, *keepttl, *load, *createconsumer,
*lastid, *ping, *setid, *keepttl, *load, *createconsumer, *fields,
*getack, *special_asterick, *special_equals, *default_username, *redacted,
*ssubscribebulk,*sunsubscribebulk, *smessagebulk,
*select[PROTO_SHARED_SELECT_CMDS],
Expand Down
13 changes: 7 additions & 6 deletions src/t_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -2556,23 +2556,24 @@ void hincrbyfloatCommand(client *c) {
char buf[MAX_LONG_DOUBLE_CHARS];
int len = ld2string(buf,sizeof(buf),value,LD_STR_HUMAN);
new = sdsnewlen(buf,len);
hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_TTL);
hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing HASH_SET_KEEP_TTL causes TTL removal on master.

The HASH_SET_KEEP_TTL flag has been removed from the hashTypeSet call. Looking at the hashTypeSet implementation (lines 959-964 and 988-991), when this flag is NOT set and the field has a TTL, the TTL is removed.

This creates master/replica inconsistency:

  • Master: TTL is removed (no HASH_SET_KEEP_TTL)
  • Replica: TTL is preserved (receives HSETEX ... KEEPTTL)

Additionally, the existing test at lines 851-857 expects TTL to be preserved on master:

test {HINCRBYFLOAT - preserve expiration time of the field} {
    ...
    assert_range [r HPTTL h1 FIELDS 1 f1] 1 20
}

Compare with HINCRBY at line 2506 which correctly uses HASH_SET_TAKE_VALUE | HASH_SET_KEEP_TTL.

🐛 Proposed fix
-    hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE);
+    hashTypeSet(c->db, o,c->argv[2]->ptr,new,HASH_SET_TAKE_VALUE | HASH_SET_KEEP_TTL);
🤖 Prompt for AI Agents
In `@src/t_hash.c` at line 2559, The call to hashTypeSet should preserve existing
field TTLs: modify the invocation of hashTypeSet in the HINCRBYFLOAT (the line
calling hashTypeSet(c->db, o, c->argv[2]->ptr, new, HASH_SET_TAKE_VALUE)) to
include the HASH_SET_KEEP_TTL flag (i.e., pass HASH_SET_TAKE_VALUE |
HASH_SET_KEEP_TTL) so masters do not drop TTLs and remain consistent with
replicas; update the single call site referencing hashTypeSet in the
HINCRBYFLOAT handling code accordingly.

addReplyBulkCBuffer(c,buf,len);
signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id);
server.dirty++;

/* Always replicate HINCRBYFLOAT as an HSET command with the final value
/* Always replicate HINCRBYFLOAT as an HSETEX command with the final value
* in order to make sure that differences in float precision or formatting
* will not create differences in replicas or after an AOF restart. */
* will not create differences in replicas or after an AOF restart.
* The KEEPTTL flag is used to make sure the field TTL is preserved. */
robj *newobj;
newobj = createRawStringObject(buf,len);
rewriteClientCommandArgument(c,0,shared.hset);
rewriteClientCommandArgument(c,3,newobj);
rewriteClientCommandVector(c, 6, shared.hsetex, c->argv[1], shared.keepttl,
shared.fields, shared.integers[1], c->argv[2], newobj);
Comment on lines +2571 to +2572

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Wrong argc value causes value to be missing from replicated command.

The rewriteClientCommandVector call specifies argc=6, but 7 robj* arguments are passed:

  1. shared.hsetex - "HSETEX"
  2. c->argv[1] - key
  3. shared.keepttl - "KEEPTTL"
  4. shared.fields - "FIELDS"
  5. shared.integers[1] - "1"
  6. c->argv[2] - field
  7. newobj - value ← This will be excluded!

The expected HSETEX command format requires 7 arguments: HSETEX <key> KEEPTTL FIELDS 1 <field> <value>

With argc=6, the replicated command will be missing the value, causing data corruption on replicas.

🐛 Proposed fix
-    rewriteClientCommandVector(c, 6, shared.hsetex, c->argv[1], shared.keepttl,
+    rewriteClientCommandVector(c, 7, shared.hsetex, c->argv[1], shared.keepttl,
                         shared.fields, shared.integers[1], c->argv[2], newobj);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rewriteClientCommandVector(c, 6, shared.hsetex, c->argv[1], shared.keepttl,
shared.fields, shared.integers[1], c->argv[2], newobj);
rewriteClientCommandVector(c, 7, shared.hsetex, c->argv[1], shared.keepttl,
shared.fields, shared.integers[1], c->argv[2], newobj);
🤖 Prompt for AI Agents
In `@src/t_hash.c` around lines 2571 - 2572, The call to
rewriteClientCommandVector currently uses argc=6 but passes seven robj*
arguments for the HSETEX command, causing the final newobj (value) to be
omitted; update the argc parameter to 7 in the rewriteClientCommandVector
invocation so the args (shared.hsetex, c->argv[1], shared.keepttl,
shared.fields, shared.integers[1], c->argv[2], newobj) are all included, and
keep the argument order as shown to match the expected HSETEX <key> KEEPTTL
FIELDS 1 <field> <value> replication format.

decrRefCount(newobj);
}

static GetFieldRes addHashFieldToReply(client *c, kvobj *o, sds field, int hfeFlags) {
GetFieldRes addHashFieldToReply(client *c, kvobj *o, sds field, int hfeFlags) {
if (o == NULL) {
addReplyNull(c);
return GETF_NOT_FOUND;
Expand Down
46 changes: 46 additions & 0 deletions tests/unit/type/hash-field-expire.tcl
Original file line number Diff line number Diff line change
Expand Up @@ -1956,5 +1956,51 @@ start_server {tags {"external:skip needs:debug"}} {
}
close_replication_stream $repl
} {} {needs:repl}

test "HINCRBYFLOAT command won't remove field expiration on replica ($type)" {
r flushall
set repl [attach_to_replication_stream]

r hsetex h1 EX 100 FIELDS 1 f1 1
r hset h1 f2 1
r hincrbyfloat h1 f1 1.1
r hincrbyfloat h1 f2 1.1

# HINCRBYFLOAT will be replicated as HSETEX with KEEPTTL flag
assert_replication_stream $repl {
{select *}
{hsetex h1 PXAT * FIELDS 1 f1 1}
{hset h1 f2 1}
{hsetex h1 KEEPTTL FIELDS 1 f1 *}
{hsetex h1 KEEPTTL FIELDS 1 f2 *}
}
close_replication_stream $repl

start_server {tags {external:skip}} {
r -1 flushall
r slaveof [srv -1 host] [srv -1 port]
wait_for_sync r

r -1 hsetex h1 EX 100 FIELDS 1 f1 1
r -1 hset h1 f2 1
wait_for_ofs_sync [srv -1 client] [srv 0 client]
assert_range [r httl h1 FIELDS 1 f1] 90 100
assert_equal {-1} [r httl h1 FIELDS 1 f2]

r -1 hincrbyfloat h1 f1 1.1
r -1 hincrbyfloat h1 f2 1.1

# Expiration time should not be removed on replica and the value
# should be equal to the master.
wait_for_ofs_sync [srv -1 client] [srv 0 client]
assert_range [r httl h1 FIELDS 1 f1] 90 100
assert_equal [r -1 hget h1 f1] [r hget h1 f1]

# The field f2 should not have any expiration on replica either even
# though it was set using HSET with KEEPTTL flag.
assert_equal {-1} [r httl h1 FIELDS 1 f2]
assert_equal [r -1 hget h1 f2] [r hget h1 f2]
}
} {} {needs:repl external:skip}
}
}
Loading