From 1fdfec8ecfe89b6ed852f9c18a859d3e8bec4b4b Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Fri, 9 Oct 2026 18:36:20 -0700 Subject: [PATCH] Modules: initialize shared dict state entry expiration When a shared dict state entry omits the optional expire field, its expiration remains uninitialized. In a zone with timeout=, the entry can be discarded as expired or loaded with an arbitrary expiration instead of receiving the zone timeout. Initialize expiration to zero before parsing the entry so an omitted expire field is treated like an explicit zero. The entry then receives the zone timeout and the state file is scheduled for saving. --- nginx/ngx_js_shared_dict.c | 1 + nginx/t/js_shared_dict_state_timeout.t | 62 ++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c index 17664d20..22526f5b 100644 --- a/nginx/ngx_js_shared_dict.c +++ b/nginx/ngx_js_shared_dict.c @@ -2355,6 +2355,7 @@ ngx_js_dict_parse_entry(ngx_js_dict_t *dict, ngx_pool_t *pool, } see_value = 0; + entry->expire = 0; while (1) { p = ngx_js_skip_space(p, end); diff --git a/nginx/t/js_shared_dict_state_timeout.t b/nginx/t/js_shared_dict_state_timeout.t index 9adde127..deb2d88d 100644 --- a/nginx/t/js_shared_dict_state_timeout.t +++ b/nginx/t/js_shared_dict_state_timeout.t @@ -43,6 +43,8 @@ http { js_shared_dict_zone zone=waka:32k timeout=1000s type=number state=waka.json; js_shared_dict_zone zone=exp:32k timeout=1000s type=string state=exp.json; + js_shared_dict_zone zone=legacy:32k timeout=1000s type=number + state=legacy.json; server { listen 127.0.0.1:8080; @@ -60,6 +62,10 @@ http { js_content test.incr; } + location /njs { + js_content test.njs; + } + location /set { js_content test.set; } @@ -79,10 +85,19 @@ my $future_expire = $now_ms + 3600000; $t->write_file('exp.json', <write_file('legacy.json', <write_file('test.js', <<'EOF'); + function test_njs(r) { + r.return(200, njs.version); + } + function convertToValue(dict, v) { if (dict.type == 'number') { return parseInt(v); @@ -155,18 +170,42 @@ $t->write_file('test.js', <<'EOF'); r.return(200, val === undefined ? 'undefined' : val); } - export default { add, get, incr, set, ttl }; + export default { add, get, incr, njs: test_njs, set, ttl }; EOF $t->try_run('js_shared_dict_zone state with timeout no support on 32-bit') - ->plan(18); + ->plan(24); ############################################################################### +TODO: { +local $TODO = 'not yet' unless has_version('1.0.3'); + +like(http_get('/get?dict=exp&key=missing'), qr/^legacy$/m, + 'string entry without expire loaded'); +like(http_get('/get?dict=legacy&key=missing'), qr/^17$/m, + 'number entry without expire loaded'); + +for my $dict ('exp', 'legacy') { + my $ttl = get_ttl("/ttl?dict=$dict&key=missing"); + ok($ttl >= 990000 && $ttl <= 1000000, + "$dict entry without expire gets zone timeout"); +} + http_get('/set?dict=waka&key=foo&value=42'); select undef, undef, undef, 1.1; +for my $dict ('exp', 'legacy') { + my $state = read_state($t, "$dict.json"); + my $expire = $state->{missing}->{expire}; + ok(defined $expire && $expire >= $now_ms + 990000 + && $expire <= time_ms() + 1000000, + "$dict entry without expire resaved without mutation"); +} + +} + $t->reload(); my $waka_state = read_state($t, 'waka.json'); @@ -268,6 +307,23 @@ sub get_ttl { ($resp =~ /\x0d\x0a\x0d\x0a(\d+)/) ? $1 : -1; } +sub has_version { + my $need = shift; + + http_get('/njs') =~ /^([.0-9]+)$/m; + + my @v = split(/\./, $1); + my ($n, $v); + + for $n (split(/\./, $need)) { + $v = shift @v || 0; + return 0 if $n > $v; + return 1 if $v > $n; + } + + return 1; +} + sub time_ms { return time() * 1000; } -- 2.47.3