]> git.kaiwu.me - njs.git/commitdiff
Modules: initialize shared dict state entry expiration
authorDmitry Volyntsev <xeioex@nginx.com>
Sat, 10 Oct 2026 01:36:20 +0000 (18:36 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 15 Oct 2026 01:49:05 +0000 (18:49 -0700)
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
nginx/t/js_shared_dict_state_timeout.t

index 17664d207fcd12250e7c3659322dd3081f82a943..22526f5b79b7fa7c98c4d2cc67fd74c1c2d3eb9f 100644 (file)
@@ -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);
index 9adde1278c9d2f52579798bf26756587b7a4880c..deb2d88d928e6a0395567e0a5b106d77365a3502 100644 (file)
@@ -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', <<EOF);
 {"past":{"value":"gone","expire":$past_expire},
  "future":{"value":"here","expire":$future_expire},
- "noexp":{"value":"fresh","expire":0}}
+ "noexp":{"value":"fresh","expire":0},
+ "missing":{"value":"legacy"}}
+EOF
+
+$t->write_file('legacy.json', <<EOF);
+{"missing":{"value":17}}
 EOF
 
 $t->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;
 }