From: Dmitry Volyntsev Date: Thu, 12 Mar 2026 23:50:34 +0000 (-0700) Subject: Modules: preserved per-entry TTL on shared dict incr() calls. X-Git-Tag: 0.9.7~12 X-Git-Url: http://www.kaiwu.me/postgresql/commit/static/gitweb.js?a=commitdiff_plain;h=8f42e991a2528ed64663599ffd76e66988b8f126;p=njs.git Modules: preserved per-entry TTL on shared dict incr() calls. 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. --- diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c index 1963e24d..b15c6951 100644 --- a/nginx/ngx_js_shared_dict.c +++ b/nginx/ngx_js_shared_dict.c @@ -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); diff --git a/nginx/t/js_shared_dict_state_timeout.t b/nginx/t/js_shared_dict_state_timeout.t index 245b084f..9adde127 100644 --- a/nginx/t/js_shared_dict_state_timeout.t +++ b/nginx/t/js_shared_dict_state_timeout.t @@ -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; }