]> git.kaiwu.me - njs.git/commitdiff
Modules: added ttl() method to shared dictionaries.
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 12 Mar 2026 23:48:14 +0000 (16:48 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Wed, 18 Mar 2026 01:14:53 +0000 (18:14 -0700)
The method returns the remaining time-to-live in milliseconds
for a given key, or undefined if the key does not exist or has
expired.  Throws TypeError if the dictionary was declared without
the timeout parameter.

nginx/ngx_js_shared_dict.c
nginx/t/js_shared_dict.t

index a7273df566f3a2987d29b64db8deb45b09ee0146..1963e24df594cc011a63639db28d995fb4809d21 100644 (file)
@@ -99,6 +99,8 @@ static njs_int_t njs_js_ext_shared_dict_set(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t flags, njs_value_t *retval);
 static njs_int_t njs_js_ext_shared_dict_size(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
+static njs_int_t njs_js_ext_shared_dict_ttl(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval);
 static njs_int_t njs_js_ext_shared_dict_type(njs_vm_t *vm,
     njs_object_prop_t *prop, uint32_t unused, njs_value_t *value,
     njs_value_t *setval, njs_value_t *retval);
@@ -171,6 +173,8 @@ static JSValue ngx_qjs_ext_shared_dict_set(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv, int flags);
 static JSValue ngx_qjs_ext_shared_dict_size(JSContext *cx,
     JSValueConst this_val, int argc, JSValueConst *argv);
+static JSValue ngx_qjs_ext_shared_dict_ttl(JSContext *cx,
+    JSValueConst this_val, int argc, JSValueConst *argv);
     static JSValue ngx_qjs_ext_shared_dict_type(JSContext *cx,
     JSValueConst this_val);
 
@@ -372,6 +376,17 @@ static njs_external_t  ngx_js_ext_shared_dict[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("ttl"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_ttl,
+        }
+    },
+
     {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("type"),
@@ -467,6 +482,7 @@ static const JSCFunctionListEntry ngx_qjs_ext_shared_dict[] = {
                        NGX_JS_DICT_FLAG_MUST_EXIST),
     JS_CFUNC_MAGIC_DEF("set", 3, ngx_qjs_ext_shared_dict_set, 0),
     JS_CFUNC_DEF("size", 0, ngx_qjs_ext_shared_dict_size),
+    JS_CFUNC_DEF("ttl", 1, ngx_qjs_ext_shared_dict_ttl),
     JS_CGETSET_DEF("type", ngx_qjs_ext_shared_dict_type, NULL),
 };
 
@@ -1243,6 +1259,59 @@ njs_js_ext_shared_dict_size(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 }
 
 
+static njs_int_t
+njs_js_ext_shared_dict_ttl(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    ngx_str_t            key;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_js_dict_node_t  *node;
+
+    shm_zone = njs_vm_external(vm, ngx_js_shared_dict_proto_id,
+                               njs_argument(args, 0));
+    if (shm_zone == NULL) {
+        njs_vm_type_error(vm, "\"this\" is not a shared dict");
+        return NJS_ERROR;
+    }
+
+    dict = shm_zone->data;
+
+    if (!dict->timeout) {
+        njs_vm_type_error(vm, "shared dict must be declared with timeout");
+        return NJS_ERROR;
+    }
+
+    if (ngx_js_ngx_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    node = ngx_js_dict_lookup(dict, &key);
+
+    if (node != NULL) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (now < node->expire.key) {
+            njs_value_number_set(retval,
+                                 (double) (node->expire.key - now));
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            return NJS_OK;
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    njs_value_undefined_set(retval);
+
+    return NJS_OK;
+}
+
+
 static njs_int_t
 njs_js_ext_shared_dict_type(njs_vm_t *vm, njs_object_prop_t *prop,
     uint32_t unused, njs_value_t *value, njs_value_t *setval,
@@ -3755,6 +3824,57 @@ ngx_qjs_ext_shared_dict_size(JSContext *cx, JSValueConst this_val,
 }
 
 
+static JSValue
+ngx_qjs_ext_shared_dict_ttl(JSContext *cx, JSValueConst this_val,
+    int argc, JSValueConst *argv)
+{
+    ngx_str_t            key;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_js_dict_node_t  *node;
+
+    shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT);
+    if (shm_zone == NULL) {
+        return JS_ThrowTypeError(cx, "\"this\" is not a shared dict");
+    }
+
+    dict = shm_zone->data;
+
+    if (!dict->timeout) {
+        return JS_ThrowTypeError(cx,
+                                 "shared dict must be declared with timeout");
+    }
+
+    key.data = (u_char *) JS_ToCStringLen(cx, &key.len, argv[0]);
+    if (key.data == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    node = ngx_qjs_dict_lookup(dict, &key);
+
+    if (node != NULL) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (now < node->expire.key) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            JS_FreeCString(cx, (char *) key.data);
+            return JS_NewFloat64(cx, (double) (node->expire.key - now));
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    JS_FreeCString(cx, (char *) key.data);
+
+    return JS_UNDEFINED;
+}
+
+
 static JSValue
 ngx_qjs_ext_shared_dict_type(JSContext *cx, JSValueConst this_val)
 {
index 34a2a6421d9f9afc494d3b0b14bae7d7144fa629..a28d09f890cab0e9a24df623ae1258740712d6de 100644 (file)
@@ -128,6 +128,10 @@ http {
             js_content test.size;
         }
 
+        location /ttl {
+            js_content test.ttl;
+        }
+
         location /zones {
             js_content test.zones;
         }
@@ -352,6 +356,23 @@ $t->write_file('test.js', <<'EOF');
     }
 
 
+    function ttl(r) {
+        var dict = ngx.shared[r.args.dict];
+
+        if (r.args.err) {
+            try {
+                dict.ttl(r.args.key);
+                r.return(200, 'no exception');
+            } catch (e) {
+                r.return(200, e.toString());
+            }
+            return;
+        }
+
+        var val = dict.ttl(r.args.key);
+        r.return(200, val === undefined ? 'undefined' : val);
+    }
+
     function zones(r) {
         r.return(200, Object.keys(ngx.shared).sort());
     }
@@ -359,12 +380,12 @@ $t->write_file('test.js', <<'EOF');
     export default { add, capacity, chain, clear, del, evict_stress,
                      free_space, get, has, incr, items, keys, name,
                      njs: test_njs, pop, replace, set, set_clear, size,
-                     zones, overflow };
+                     ttl, zones, overflow };
 EOF
 
 $t->try_run('no js_shared_dict_zone');
 
-$t->plan(56);
+$t->plan(63);
 
 ###############################################################################
 
@@ -452,6 +473,42 @@ like(http_get('/keys?dict=waka'), qr/FOO\,FOO2\,FOO3/, 'waka keys');
 
 }
 
+# ttl() tests
+
+http_get('/clear?dict=waka');
+
+http_get('/set?dict=waka&key=TTL1&value=1&timeout=30000');
+my $ttl_resp = get_ttl('/ttl?dict=waka&key=TTL1');
+ok($ttl_resp >= 25000 && $ttl_resp <= 30000, 'ttl for 30s entry in range');
+
+like(http_get('/ttl?dict=waka&key=NOKEY'), qr/undefined/,
+       'ttl for missing key');
+
+like(http_get('/ttl?dict=no_timeout&key=x&err=1'), qr/TypeError/,
+       'ttl on dict without timeout');
+
+# per-entry timeout overrides directive default (waka: timeout=1000s)
+
+http_get('/add?dict=waka&key=TTL_ADD&value=1&timeout=30000');
+my $ttl_add = get_ttl('/ttl?dict=waka&key=TTL_ADD');
+ok($ttl_add >= 25000 && $ttl_add <= 30000,
+       'add per-entry timeout overrides directive default');
+
+http_get('/set?dict=waka&key=TTL_DEF&value=1');
+my $ttl_def = get_ttl('/ttl?dict=waka&key=TTL_DEF');
+ok($ttl_def >= 900000 && $ttl_def <= 1000000,
+       'set without timeout uses directive default');
+
+http_get('/incr?dict=waka&key=TTL_INCR&by=1&timeout=30000');
+my $ttl_incr = get_ttl('/ttl?dict=waka&key=TTL_INCR');
+ok($ttl_incr >= 25000 && $ttl_incr <= 30000,
+       'incr per-entry timeout overrides directive default');
+
+http_get('/incr?dict=waka&key=TTL_INCR_DEF&by=1');
+my $ttl_incr_def = get_ttl('/ttl?dict=waka&key=TTL_INCR_DEF');
+ok($ttl_incr_def >= 900000 && $ttl_incr_def <= 1000000,
+       'incr without timeout uses directive default');
+
 like(http_get('/pop?dict=bar&key=FOO'), qr/zzz/, 'pop bar.FOO');
 like(http_get('/pop?dict=bar&key=FOO'), qr/undefined/, 'pop deleted bar.FOO');
 http_get('/set?dict=foo&key=BAR&value=xxx');
@@ -477,6 +534,12 @@ unlike($t->read_file('error.log'), qr/no memory in js shared zone "foo"/,
 
 ###############################################################################
 
+sub get_ttl {
+       my ($uri) = @_;
+       my $resp = http_get($uri);
+       ($resp =~ /\x0d\x0a\x0d\x0a(\d+)/) ? $1 : -1;
+}
+
 sub has_version {
        my $need = shift;