]> git.kaiwu.me - njs.git/commitdiff
Modules: preserved per-entry TTL on shared dict incr() calls.
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 12 Mar 2026 23:50:34 +0000 (16:50 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Wed, 18 Mar 2026 01:14:53 +0000 (18:14 -0700)
Previously, incr() without an explicit timeout argument always
reset the entry expiry to the directive default, discarding any
per-entry timeout set by a prior add(), set(), or incr() call.

This aligns the behavior with Redis INCR and OpenResty
ngx.shared.DICT:incr() where value mutation does not touch the
existing TTL.  An explicit timeout argument still updates it.

nginx/ngx_js_shared_dict.c
nginx/t/js_shared_dict_state_timeout.t

index 1963e24df594cc011a63639db28d995fb4809d21..b15c69514afcf4964bdccf4fa4330e2fb8e0aa10 100644 (file)
@@ -958,7 +958,7 @@ njs_js_ext_shared_dict_incr(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
         }
 
     } else {
-        timeout = dict->timeout;
+        timeout = 0;
     }
 
     rc = ngx_js_dict_incr(vm, shm_zone->data, &key, delta, init, &value,
@@ -1638,7 +1638,10 @@ ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key,
     if (node == NULL) {
         njs_value_number_set(init, njs_value_number(init)
                                    + njs_value_number(delta));
-        if (ngx_js_dict_add(vm, dict, key, init, timeout, now) != NGX_OK) {
+        if (ngx_js_dict_add(vm, dict, key, init,
+                            timeout ? timeout : dict->timeout, now)
+            != NGX_OK)
+        {
             ngx_rwlock_unlock(&dict->sh->rwlock);
             return NGX_ERROR;
         }
@@ -1649,7 +1652,7 @@ ngx_js_dict_incr(njs_vm_t *vm, ngx_js_dict_t *dict, ngx_str_t *key,
         node->value.number += njs_value_number(delta);
         *value = node->value.number;
 
-        if (dict->timeout) {
+        if (dict->timeout && timeout) {
             ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
             node->expire.key = now + timeout;
             ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
@@ -3445,7 +3448,7 @@ ngx_qjs_ext_shared_dict_incr(JSContext *cx, JSValueConst this_val,
         }
 
     } else {
-        timeout = dict->timeout;
+        timeout = 0;
     }
 
     key.data = (u_char *) JS_ToCStringLen(cx, &key.len, argv[0]);
@@ -4073,7 +4076,10 @@ ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key,
 
     if (node == NULL) {
         value = JS_NewFloat64(cx, init + delta);
-        if (ngx_qjs_dict_add(cx, dict, key, value, timeout, now) != NGX_OK) {
+        if (ngx_qjs_dict_add(cx, dict, key, value,
+                              timeout ? timeout : dict->timeout, now)
+            != NGX_OK)
+        {
             ngx_rwlock_unlock(&dict->sh->rwlock);
             JS_FreeValue(cx, value);
             return ngx_qjs_throw_shared_memory_error(cx);
@@ -4083,7 +4089,7 @@ ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key,
         node->value.number += delta;
         value = JS_NewFloat64(cx, node->value.number);
 
-        if (dict->timeout) {
+        if (dict->timeout && timeout) {
             ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
             node->expire.key = now + timeout;
             ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
index 245b084f5e10a786177eaa038e95005cff8b0920..9adde1278c9d2f52579798bf26756587b7a4880c 100644 (file)
@@ -48,6 +48,10 @@ http {
         listen       127.0.0.1:8080;
         server_name  localhost;
 
+        location /add {
+            js_content test.add;
+        }
+
         location /get {
             js_content test.get;
         }
@@ -59,6 +63,10 @@ http {
         location /set {
             js_content test.set;
         }
+
+        location /ttl {
+            js_content test.ttl;
+        }
     }
 }
 
@@ -86,6 +94,19 @@ $t->write_file('test.js', <<'EOF');
         return v;
     }
 
+    function add(r) {
+        var dict = ngx.shared[r.args.dict];
+        var value = convertToValue(dict, r.args.value);
+
+        if (r.args.timeout) {
+            var timeout = Number(r.args.timeout);
+            r.return(200, dict.add(r.args.key, value, timeout));
+
+        } else {
+            r.return(200, dict.add(r.args.key, value));
+        }
+    }
+
     function get(r) {
         var dict = ngx.shared[r.args.dict];
         var val = dict.get(r.args.key);
@@ -128,11 +149,17 @@ $t->write_file('test.js', <<'EOF');
         }
     }
 
-    export default { get, incr, set };
+    function ttl(r) {
+        var dict = ngx.shared[r.args.dict];
+        var val = dict.ttl(r.args.key);
+        r.return(200, val === undefined ? 'undefined' : val);
+    }
+
+    export default { add, get, incr, set, ttl };
 EOF
 
 $t->try_run('js_shared_dict_zone state with timeout no support on 32-bit')
-       ->plan(13);
+       ->plan(18);
 
 ###############################################################################
 
@@ -155,6 +182,39 @@ $waka_state = read_state($t, 'waka.json');
 
 is($waka_state->{foo}->{value}, '43', 'get waka.foo from state');
 
+# incr() without timeout preserves existing TTL
+
+http_get('/set?dict=waka&key=prs&value=100&timeout=30000');
+
+my $ttl_before = get_ttl('/ttl?dict=waka&key=prs');
+ok($ttl_before >= 25000 && $ttl_before <= 30000,
+       'incr preserve: initial ttl in 30s range');
+
+http_get('/incr?dict=waka&key=prs&by=-10');
+
+my $ttl_after = get_ttl('/ttl?dict=waka&key=prs');
+ok($ttl_after >= 20000 && $ttl_after <= $ttl_before,
+       'incr preserve: ttl not reset after incr without timeout');
+
+# incr() with explicit timeout updates TTL
+
+http_get('/incr?dict=waka&key=prs&by=5&timeout=60000');
+
+my $ttl_explicit = get_ttl('/ttl?dict=waka&key=prs');
+ok($ttl_explicit >= 55000 && $ttl_explicit <= 60000,
+       'incr explicit: ttl updated to 60s range');
+
+like(http_get('/get?dict=waka&key=prs'), qr/^95$/m,
+       'incr preserve: value correct after operations');
+
+# add() per-entry timeout overrides directive default (waka: timeout=1000s)
+
+http_get('/add?dict=waka&key=add_ttl&value=77&timeout=30000');
+
+my $ttl_add = get_ttl('/ttl?dict=waka&key=add_ttl');
+ok($ttl_add >= 25000 && $ttl_add <= 30000,
+       'add per-entry timeout overrides directive default');
+
 like(http_get('/get?dict=exp&key=past'), qr/undefined/,
        'expired entry cleaned on load');
 
@@ -202,6 +262,12 @@ ok(defined $exp_state->{noexp}->{expire}
 
 ###############################################################################
 
+sub get_ttl {
+       my ($uri) = @_;
+       my $resp = http_get($uri);
+       ($resp =~ /\x0d\x0a\x0d\x0a(\d+)/) ? $1 : -1;
+}
+
 sub time_ms {
        return time() * 1000;
 }