]> git.kaiwu.me - njs.git/commitdiff
Modules: introduced js_shared_dict_zone directive.
authorDmitry Volyntsev <xeioex@nginx.com>
Mon, 3 Jul 2023 20:32:41 +0000 (13:32 -0700)
committerDmitry Volyntsev <xeioex@nginx.com>
Mon, 3 Jul 2023 20:32:41 +0000 (13:32 -0700)
The directive allows to declare a dictionary that is shared among the
working processes. A dictionary expects strings as keys. It stores
string or number as values. The value type is declared using
type= argument of the directive. The default type is string.

example.conf:
    # Declares a shared dictionary of strings of size 1 Mb that
    # removes key-value after 60 seconds of inactivity.
    js_shared_dict_zone zone=foo:1M timeout=60s;

    # Declares a shared dictionary of strings of size 512Kb that
    # forcibly remove oldest key-value pairs when memory is not enough.
    js_shared_dict_zone zone=bar:512K timeout=30s evict;

    # Declares a permanent number shared dictionary of size 32Kb.
    js_shared_dict_zone zone=num:32k type=number;

example.js:
    function get(r) {
        r.return(200, ngx.shared.foo.get(r.args.key));
    }

    function set(r) {
        r.return(200, ngx.shared.foo.set(r.args.key, r.args.value));
    }

    function delete(r) {
        r.return(200, ngx.shared.bar.delete(r.args.key));
    }

    function increment(r) {
        r.return(200, ngx.shared.num.incr(r.args.key, 2));
    }

In collaboration with Artem S. Povalyukhin, Jakub Jirutka and
洪志道 (Hong Zhi Dao).

This closes #437 issue on Github.

nginx/config
nginx/ngx_http_js_module.c
nginx/ngx_js.c
nginx/ngx_js.h
nginx/ngx_js_shared_dict.c [new file with mode: 0644]
nginx/ngx_js_shared_dict.h [new file with mode: 0644]
nginx/ngx_stream_js_module.c
nginx/t/js_shared_dict.t [new file with mode: 0644]
nginx/t/stream_js_shared_dict.t [new file with mode: 0644]
ts/ngx_core.d.ts

index 1bd922f42226621df89ff47b7c27ec6846adefbd..700ae4abfd380f7354a8c6f074686305d59775e0 100644 (file)
@@ -5,10 +5,12 @@ NJS_LIBXSLT=${NJS_LIBXSLT:-YES}
 NJS_ZLIB=${NJS_ZLIB:-YES}
 
 NJS_DEPS="$ngx_addon_dir/ngx_js.h \
-    $ngx_addon_dir/ngx_js_fetch.h"
+    $ngx_addon_dir/ngx_js_fetch.h \
+    $ngx_addon_dir/ngx_js_shared_dict.h"
 NJS_SRCS="$ngx_addon_dir/ngx_js.c \
     $ngx_addon_dir/ngx_js_fetch.c \
-    $ngx_addon_dir/ngx_js_regex.c"
+    $ngx_addon_dir/ngx_js_regex.c \
+    $ngx_addon_dir/ngx_js_shared_dict.c"
 
 NJS_OPENSSL_LIB=
 NJS_XSLT_LIB=
index df3a0e5a7974f9a7b6fadf88e0dcab649ea1d37b..5f6c73da35706f1518b03ce10e0b51a333f47828 100644 (file)
@@ -252,10 +252,13 @@ static char *ngx_http_js_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
 static char *ngx_http_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
 static char *ngx_http_js_content(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
+static char *ngx_http_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf);
 static char *ngx_http_js_body_filter_set(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
 static ngx_int_t ngx_http_js_init_conf_vm(ngx_conf_t *cf,
     ngx_js_loc_conf_t *conf);
+static void *ngx_http_js_create_main_conf(ngx_conf_t *cf);
 static void *ngx_http_js_create_loc_conf(ngx_conf_t *cf);
 static char *ngx_http_js_merge_loc_conf(ngx_conf_t *cf, void *parent,
     void *child);
@@ -393,6 +396,13 @@ static ngx_command_t  ngx_http_js_commands[] = {
 
 #endif
 
+    { ngx_string("js_shared_dict_zone"),
+      NGX_HTTP_MAIN_CONF|NGX_CONF_1MORE,
+      ngx_http_js_shared_dict_zone,
+      0,
+      0,
+      NULL },
+
       ngx_null_command
 };
 
@@ -401,7 +411,7 @@ static ngx_http_module_t  ngx_http_js_module_ctx = {
     NULL,                          /* preconfiguration */
     ngx_http_js_init,              /* postconfiguration */
 
-    NULL,                          /* create main configuration */
+    ngx_http_js_create_main_conf,  /* create main configuration */
     NULL,                          /* init main configuration */
 
     NULL,                          /* create server configuration */
@@ -775,6 +785,7 @@ static uintptr_t ngx_http_js_uptr[] = {
     (uintptr_t) ngx_http_js_fetch_timeout,
     (uintptr_t) ngx_http_js_buffer_size,
     (uintptr_t) ngx_http_js_max_response_buffer_size,
+    (uintptr_t) 0 /* main_conf ptr */,
 };
 
 
@@ -798,6 +809,7 @@ njs_module_t *njs_http_js_addon_modules[] = {
      */
     &ngx_js_ngx_module,
     &ngx_js_fetch_module,
+    &ngx_js_shared_dict_module,
 #ifdef NJS_HAVE_OPENSSL
     &njs_webcrypto_module,
 #endif
@@ -4149,10 +4161,14 @@ ngx_js_http_init(njs_vm_t *vm)
 static ngx_int_t
 ngx_http_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)
 {
-    njs_vm_opt_t  options;
+    njs_vm_opt_t         options;
+    ngx_js_main_conf_t  *jmcf;
 
     njs_vm_opt_init(&options);
 
+    jmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_js_module);
+    ngx_http_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf;
+
     options.backtrace = 1;
     options.unhandled_rejection = NJS_VM_OPT_UNHANDLED_REJECTION_THROW;
     options.ops = &ngx_http_js_ops;
@@ -4292,6 +4308,14 @@ ngx_http_js_content(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 }
 
 
+static char *
+ngx_http_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf)
+{
+    return ngx_js_shared_dict_zone(cf, cmd, conf, &ngx_http_js_module);
+}
+
+
 static char *
 ngx_http_js_body_filter_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 {
@@ -4330,6 +4354,26 @@ ngx_http_js_body_filter_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 }
 
 
+static void *
+ngx_http_js_create_main_conf(ngx_conf_t *cf)
+{
+    ngx_js_main_conf_t  *jmcf;
+
+    jmcf = ngx_pcalloc(cf->pool, sizeof(ngx_js_main_conf_t));
+    if (jmcf == NULL) {
+        return NULL;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     jmcf->dicts = NULL;
+     */
+
+    return jmcf;
+}
+
+
 static void *
 ngx_http_js_create_loc_conf(ngx_conf_t *cf)
 {
index a3b91ca471832ffc1ca51157d53f3fdb245af35b..f98676c8fd91215bbc47b5c229bce5ed7f4e46e9 100644 (file)
@@ -32,6 +32,28 @@ static void ngx_js_cleanup_vm(void *data);
 static njs_int_t ngx_js_core_init(njs_vm_t *vm);
 
 
+static njs_external_t  ngx_js_ext_global_shared[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "GlobalShared",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_SELF,
+        .u.object = {
+            .enumerable = 1,
+            .prop_handler = njs_js_ext_global_shared_prop,
+            .keys = njs_js_ext_global_shared_keys,
+        }
+    },
+
+};
+
+
 static njs_external_t  ngx_js_ext_core[] = {
 
     {
@@ -112,6 +134,18 @@ static njs_external_t  ngx_js_ext_core[] = {
         }
     },
 
+    {
+        .flags = NJS_EXTERN_OBJECT,
+        .name.string = njs_str("shared"),
+        .enumerable = 1,
+        .writable = 1,
+        .u.object = {
+            .enumerable = 1,
+            .properties = ngx_js_ext_global_shared,
+            .nproperties = njs_nitems(ngx_js_ext_global_shared),
+        }
+    },
+
     {
         .flags = NJS_EXTERN_PROPERTY,
         .name.string = njs_str("prefix"),
index 4ed00249601481441a5e0c7b115886c6f797321b..0febe83647d6f561a1b411e182b8902f89a91108 100644 (file)
@@ -14,6 +14,7 @@
 #include <ngx_core.h>
 #include <njs.h>
 #include "ngx_js_fetch.h"
+#include "ngx_js_shared_dict.h"
 
 
 #define NGX_JS_UNSET        0
@@ -43,6 +44,9 @@ typedef ngx_flag_t (*ngx_external_size_pt)(njs_vm_t *vm,
     njs_external_ptr_t e);
 typedef ngx_ssl_t *(*ngx_external_ssl_pt)(njs_vm_t *vm, njs_external_ptr_t e);
 
+
+typedef struct ngx_js_dict_s  ngx_js_dict_t;
+
 typedef struct {
     ngx_str_t              name;
     ngx_str_t              path;
@@ -51,6 +55,10 @@ typedef struct {
 } ngx_js_named_path_t;
 
 
+#define NGX_JS_COMMON_MAIN_CONF                                               \
+    ngx_js_dict_t         *dicts                                              \
+
+
 #define _NGX_JS_COMMON_LOC_CONF                                               \
     njs_vm_t              *vm;                                                \
     ngx_array_t           *imports;                                           \
@@ -80,6 +88,11 @@ typedef struct {
 #endif
 
 
+typedef struct {
+    NGX_JS_COMMON_MAIN_CONF;
+} ngx_js_main_conf_t;
+
+
 typedef struct {
     NGX_JS_COMMON_LOC_CONF;
 } ngx_js_loc_conf_t;
@@ -105,6 +118,9 @@ typedef struct {
     ((ngx_external_size_pt) njs_vm_meta(vm, 8))(vm, e)
 #define ngx_external_max_response_buffer_size(vm, e)                          \
     ((ngx_external_size_pt) njs_vm_meta(vm, 9))(vm, e)
+#define NGX_JS_MAIN_CONF_INDEX  10
+#define ngx_main_conf(vm)                                                     \
+       ((ngx_js_main_conf_t *) njs_vm_meta(vm, NGX_JS_MAIN_CONF_INDEX))
 
 
 #define ngx_js_prop(vm, type, value, start, len)                              \
@@ -134,6 +150,8 @@ ngx_int_t ngx_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf,
 ngx_js_loc_conf_t *ngx_js_create_conf(ngx_conf_t *cf, size_t size);
 char * ngx_js_merge_conf(ngx_conf_t *cf, void *parent, void *child,
    ngx_int_t (*init_vm)(ngx_conf_t *cf, ngx_js_loc_conf_t *conf));
+char *ngx_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf,
+    void *tag);
 
 njs_int_t ngx_js_ext_string(njs_vm_t *vm, njs_object_prop_t *prop,
     njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c
new file mode 100644 (file)
index 0000000..30cbd1c
--- /dev/null
@@ -0,0 +1,1586 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#include <ngx_config.h>
+#include <ngx_core.h>
+#include "ngx_js.h"
+#include "ngx_js_shared_dict.h"
+
+
+typedef struct {
+    ngx_rbtree_t           rbtree;
+    ngx_rbtree_node_t      sentinel;
+    ngx_atomic_t           rwlock;
+
+    ngx_rbtree_t           rbtree_expire;
+    ngx_rbtree_node_t      sentinel_expire;
+} ngx_js_dict_sh_t;
+
+
+typedef struct {
+    ngx_str_node_t         sn;
+    ngx_rbtree_node_t      expire;
+    union {
+        ngx_str_t          value;
+        double             number;
+    } u;
+} ngx_js_dict_node_t;
+
+
+struct ngx_js_dict_s {
+    ngx_shm_zone_t        *shm_zone;
+    ngx_js_dict_sh_t      *sh;
+    ngx_slab_pool_t       *shpool;
+
+    ngx_msec_t             timeout;
+    ngx_flag_t             evict;
+#define NGX_JS_DICT_TYPE_STRING  0
+#define NGX_JS_DICT_TYPE_NUMBER  1
+    ngx_uint_t             type;
+
+    ngx_js_dict_t         *next;
+};
+
+
+static njs_int_t njs_js_ext_shared_dict_capacity(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t njs_js_ext_shared_dict_clear(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_delete(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_free_space(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_get(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_has(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_keys(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_incr(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_name(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static njs_int_t njs_js_ext_shared_dict_pop(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_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_type(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+static ngx_js_dict_node_t *ngx_js_dict_lookup(ngx_js_dict_t *dict,
+    njs_str_t *key);
+
+#define NGX_JS_DICT_FLAG_MUST_EXIST       1
+#define NGX_JS_DICT_FLAG_MUST_NOT_EXIST   2
+
+static ngx_int_t ngx_js_dict_set(njs_vm_t *vm, ngx_js_dict_t *dict,
+    njs_str_t *key, njs_value_t *value, unsigned flags);
+static ngx_int_t ngx_js_dict_add(ngx_js_dict_t *dict, njs_str_t *key,
+    njs_value_t *value, ngx_msec_t now);
+static ngx_int_t ngx_js_dict_update(ngx_js_dict_t *dict,
+    ngx_js_dict_node_t *node, njs_value_t *value, ngx_msec_t now);
+static ngx_int_t ngx_js_dict_get(njs_vm_t *vm, ngx_js_dict_t *dict,
+    njs_str_t *key, njs_value_t *retval);
+static ngx_int_t ngx_js_dict_incr(ngx_js_dict_t *dict, njs_str_t *key,
+    njs_value_t *delta, njs_value_t *init, double *value);
+static ngx_int_t ngx_js_dict_delete(njs_vm_t *vm, ngx_js_dict_t *dict,
+    njs_str_t *key, njs_value_t *retval);
+static ngx_int_t ngx_js_dict_copy_value_locked(njs_vm_t *vm,
+    ngx_js_dict_t *dict, ngx_js_dict_node_t *node, njs_value_t *retval);
+
+static void ngx_js_dict_expire(ngx_js_dict_t *dict, ngx_msec_t now);
+static void ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_int_t count);
+
+static njs_int_t ngx_js_dict_shared_error_name(njs_vm_t *vm,
+    njs_object_prop_t *prop, njs_value_t *value, njs_value_t *setval,
+    njs_value_t *retval);
+
+static ngx_int_t ngx_js_dict_init_zone(ngx_shm_zone_t *shm_zone, void *data);
+static njs_int_t ngx_js_shared_dict_preinit(njs_vm_t *vm);
+static njs_int_t ngx_js_shared_dict_init(njs_vm_t *vm);
+
+
+static njs_external_t  ngx_js_ext_shared_dict[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY | NJS_EXTERN_SYMBOL,
+        .name.symbol = NJS_SYMBOL_TO_STRING_TAG,
+        .u.property = {
+            .value = "SharedDict",
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("add"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_set,
+            .magic8 = NGX_JS_DICT_FLAG_MUST_NOT_EXIST,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("capacity"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_js_ext_shared_dict_capacity,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("clear"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_clear,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("freeSpace"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_free_space,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("delete"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_delete,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("incr"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_incr,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("get"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_get,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("has"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_has,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("keys"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_keys,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("name"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_js_ext_shared_dict_name,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("pop"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_pop,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("replace"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_set,
+            .magic8 = NGX_JS_DICT_FLAG_MUST_EXIST,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("set"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_set,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_METHOD,
+        .name.string = njs_str("size"),
+        .writable = 1,
+        .configurable = 1,
+        .enumerable = 1,
+        .u.method = {
+            .native = njs_js_ext_shared_dict_size,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("type"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_js_ext_shared_dict_type,
+        }
+    },
+
+};
+
+
+static njs_external_t  ngx_js_ext_error_ctor_props[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("name"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_js_dict_shared_error_name,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("prototype"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_object_prototype_create,
+        }
+    },
+
+};
+
+
+static njs_external_t  ngx_js_ext_error_proto_props[] = {
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("name"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = ngx_js_dict_shared_error_name,
+        }
+    },
+
+    {
+        .flags = NJS_EXTERN_PROPERTY,
+        .name.string = njs_str("prototype"),
+        .enumerable = 1,
+        .u.property = {
+            .handler = njs_object_prototype_create,
+        }
+    },
+
+};
+
+
+static njs_int_t    ngx_js_shared_dict_proto_id;
+static njs_int_t    ngx_js_shared_dict_error_id;
+
+
+njs_module_t  ngx_js_shared_dict_module = {
+    .name = njs_str("shared_dict"),
+    .preinit = ngx_js_shared_dict_preinit,
+    .init = ngx_js_shared_dict_init,
+};
+
+
+njs_int_t
+njs_js_ext_global_shared_prop(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    njs_int_t            ret;
+    njs_str_t            name;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_js_main_conf_t  *conf;
+
+    ret = njs_vm_prop_name(vm, prop, &name);
+    if (ret != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    conf = ngx_main_conf(vm);
+
+    for (dict = conf->dicts; dict != NULL; dict = dict->next) {
+        shm_zone = dict->shm_zone;
+
+        if (shm_zone->shm.name.len == name.length
+            && ngx_strncmp(shm_zone->shm.name.data, name.start,
+                           name.length)
+               == 0)
+        {
+            ret = njs_vm_external_create(vm, retval,
+                                         ngx_js_shared_dict_proto_id,
+                                         shm_zone, 0);
+            if (ret != NJS_OK) {
+                njs_vm_internal_error(vm, "sharedDict creation failed");
+                return NJS_ERROR;
+            }
+
+            return NJS_OK;
+        }
+    }
+
+    njs_value_null_set(retval);
+
+    return NJS_DECLINED;
+}
+
+
+njs_int_t
+njs_js_ext_global_shared_keys(njs_vm_t *vm, njs_value_t *unused,
+    njs_value_t *keys)
+{
+    njs_int_t            rc;
+    njs_value_t         *value;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_js_main_conf_t  *conf;
+
+    conf = ngx_main_conf(vm);
+
+    rc = njs_vm_array_alloc(vm, keys, 4);
+    if (rc != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    for (dict = conf->dicts; dict != NULL; dict = dict->next) {
+        shm_zone = dict->shm_zone;
+
+        value = njs_vm_array_push(vm, keys);
+        if (value == NULL) {
+            return NJS_ERROR;
+        }
+
+        rc = njs_vm_value_string_set(vm, value, shm_zone->shm.name.data,
+                                     shm_zone->shm.name.len);
+        if (rc != NJS_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_capacity(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = njs_vm_external(vm, ngx_js_shared_dict_proto_id, value);
+    if (shm_zone == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    njs_value_number_set(retval, shm_zone->shm.size);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_clear(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    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;
+
+    ngx_rwlock_wlock(&dict->sh->rwlock);
+
+    ngx_js_dict_evict(dict, 0x7fffffff /* INT_MAX */);
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    njs_value_undefined_set(retval);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_free_space(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+    size_t           bytes;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    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;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+    bytes = dict->shpool->pfree * ngx_pagesize;
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    njs_value_number_set(retval, bytes);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_delete(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    ngx_int_t        rc;
+    njs_str_t        key;
+    ngx_shm_zone_t  *shm_zone;
+
+    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;
+    }
+
+    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    rc = ngx_js_dict_delete(vm, shm_zone->data, &key, NULL);
+
+    njs_value_boolean_set(retval, rc == NGX_OK);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_get(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    ngx_int_t        rc;
+    njs_str_t        key;
+    ngx_shm_zone_t  *shm_zone;
+
+    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;
+    }
+
+    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    rc = ngx_js_dict_get(vm, shm_zone->data, &key, retval);
+    if (njs_slow_path(rc == NGX_ERROR)) {
+        njs_vm_error(vm, "failed to get value from shared dict");
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_has(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    njs_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;
+    }
+
+    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    dict = shm_zone->data;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    node = ngx_js_dict_lookup(dict, &key);
+
+    if (node != NULL && dict->timeout) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (now >= node->expire.key) {
+            node = NULL;
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    njs_value_boolean_set(retval, node != NULL);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_keys(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    njs_int_t            rc;
+    ngx_int_t            max_count;
+    njs_value_t         *value;
+    ngx_rbtree_t        *rbtree;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    ngx_rbtree_node_t   *rn;
+    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;
+
+    max_count = 1024;
+
+    if (nargs > 1) {
+        if (ngx_js_integer(vm, njs_arg(args, nargs, 1), &max_count) != NGX_OK) {
+            return NJS_ERROR;
+        }
+    }
+
+    rc = njs_vm_array_alloc(vm, retval, 8);
+    if (rc != NJS_OK) {
+        return NJS_ERROR;
+    }
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    rbtree = &dict->sh->rbtree;
+
+    if (rbtree->root == rbtree->sentinel) {
+        goto done;
+    }
+
+    for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+         rn != NULL;
+         rn = ngx_rbtree_next(rbtree, rn))
+    {
+        if (max_count-- == 0) {
+            break;
+        }
+
+        node = (ngx_js_dict_node_t *) rn;
+
+        value = njs_vm_array_push(vm, retval);
+        if (value == NULL) {
+            goto fail;
+        }
+
+        rc = njs_vm_value_string_set(vm, value, node->sn.str.data,
+                                     node->sn.str.len);
+        if (rc != NJS_OK) {
+            goto fail;
+        }
+    }
+
+done:
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return NJS_OK;
+
+fail:
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return NJS_ERROR;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_incr(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    double               value;
+    ngx_int_t            rc;
+    njs_str_t            key;
+    njs_value_t         *delta, *init;
+    ngx_js_dict_t       *dict;
+    ngx_shm_zone_t      *shm_zone;
+    njs_opaque_value_t   lvalue;
+
+    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->type != NGX_JS_DICT_TYPE_NUMBER) {
+        njs_vm_type_error(vm, "shared dict is not a number dict");
+        return NJS_ERROR;
+    }
+
+    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    delta = njs_arg(args, nargs, 2);
+    if (!njs_value_is_number(delta)) {
+        njs_vm_type_error(vm, "delta is not a number");
+        return NJS_ERROR;
+    }
+
+    init = njs_lvalue_arg(njs_value_arg(&lvalue), args, nargs, 3);
+    if (!njs_value_is_number(init) && !njs_value_is_undefined(init)) {
+        njs_vm_type_error(vm, "init value is not a number");
+        return NJS_ERROR;
+    }
+
+    if (njs_value_is_undefined(init)) {
+        njs_value_number_set(init, 0);
+    }
+
+    rc = ngx_js_dict_incr(shm_zone->data, &key, delta, init, &value);
+    if (rc == NGX_ERROR) {
+        njs_vm_error(vm, "failed to increment value in shared dict");
+        return NJS_ERROR;
+    }
+
+    njs_value_number_set(retval, value);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_name(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = njs_vm_external(vm, ngx_js_shared_dict_proto_id, value);
+    if (shm_zone == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    return njs_vm_value_string_set(vm, retval, shm_zone->shm.name.data,
+                                   shm_zone->shm.name.len);
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_pop(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
+    njs_index_t unused, njs_value_t *retval)
+{
+    ngx_int_t        rc;
+    njs_str_t        key;
+    ngx_shm_zone_t  *shm_zone;
+
+    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;
+    }
+
+    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    rc = ngx_js_dict_delete(vm, shm_zone->data, &key, retval);
+
+    if (rc == NGX_DECLINED) {
+        njs_value_undefined_set(retval);
+    }
+
+    return (rc != NGX_ERROR) ? NJS_OK : NJS_ERROR;
+}
+
+
+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)
+{
+    njs_str_t        key;
+    ngx_int_t        rc;
+    njs_value_t     *value;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    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;
+    }
+
+    if (ngx_js_string(vm, njs_arg(args, nargs, 1), &key) != NGX_OK) {
+        return NJS_ERROR;
+    }
+
+    dict = shm_zone->data;
+    value = njs_arg(args, nargs, 2);
+
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        if (!njs_value_is_string(value)) {
+            njs_vm_type_error(vm, "string value is expected");
+            return NJS_ERROR;
+        }
+
+    } else {
+        if (!njs_value_is_number(value)) {
+            njs_vm_type_error(vm, "number value is expected");
+            return NJS_ERROR;
+        }
+    }
+
+    rc = ngx_js_dict_set(vm, shm_zone->data, &key, value, flags);
+    if (rc == NGX_ERROR) {
+        return NJS_ERROR;
+    }
+
+    if (flags) {
+        /* add() or replace(). */
+        njs_value_boolean_set(retval, rc == NGX_OK);
+
+    } else {
+        njs_value_assign(retval, njs_argument(args, 0));
+    }
+
+    return NJS_OK;
+}
+
+
+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)
+{
+    njs_int_t           items;
+    ngx_rbtree_t       *rbtree;
+    ngx_js_dict_t      *dict;
+    ngx_shm_zone_t     *shm_zone;
+    ngx_rbtree_node_t  *rn;
+
+    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;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    rbtree = &dict->sh->rbtree;
+
+    if (rbtree->root == rbtree->sentinel) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        njs_value_number_set(retval, 0);
+        return NJS_OK;
+    }
+
+    items = 0;
+
+    for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+         rn != NULL;
+         rn = ngx_rbtree_next(rbtree, rn))
+    {
+        items++;
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+    njs_value_number_set(retval, items);
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+njs_js_ext_shared_dict_type(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    njs_str_t        type;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    shm_zone = njs_vm_external(vm, ngx_js_shared_dict_proto_id, value);
+    if (shm_zone == NULL) {
+        njs_value_undefined_set(retval);
+        return NJS_DECLINED;
+    }
+
+    dict = shm_zone->data;
+
+    switch (dict->type) {
+    case NGX_JS_DICT_TYPE_STRING:
+        type = njs_str_value("string");
+        break;
+
+    default:
+        type = njs_str_value("number");
+        break;
+    }
+
+    return njs_vm_value_string_set(vm, retval, type.start, type.length);
+}
+
+
+static ngx_js_dict_node_t *
+ngx_js_dict_lookup(ngx_js_dict_t *dict, njs_str_t *key)
+{
+    uint32_t       hash;
+    ngx_str_t      k;
+    ngx_rbtree_t  *rbtree;
+
+    rbtree = &dict->sh->rbtree;
+
+    hash = ngx_crc32_long(key->start, key->length);
+
+    k.data = key->start;
+    k.len = key->length;
+
+    return (ngx_js_dict_node_t *) ngx_str_rbtree_lookup(rbtree, &k, hash);
+}
+
+
+static void *
+ngx_js_dict_alloc(ngx_js_dict_t *dict, size_t n)
+{
+    void  *p;
+
+    p = ngx_slab_alloc_locked(dict->shpool, n);
+
+    if (p == NULL && dict->evict) {
+        ngx_js_dict_evict(dict, 16);
+        p = ngx_slab_alloc_locked(dict->shpool, n);
+    }
+
+    return p;
+}
+
+
+static void
+ngx_js_dict_node_free(ngx_js_dict_t *dict, ngx_js_dict_node_t *node)
+{
+    ngx_slab_pool_t  *shpool;
+
+    shpool = dict->shpool;
+
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        ngx_slab_free_locked(shpool, node->u.value.data);
+    }
+
+    ngx_slab_free_locked(shpool, node);
+}
+
+
+static ngx_int_t
+ngx_js_dict_set(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key,
+    njs_value_t *value, unsigned flags)
+{
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    tp = ngx_timeofday();
+    now = tp->sec * 1000 + tp->msec;
+
+    ngx_rwlock_wlock(&dict->sh->rwlock);
+
+    node = ngx_js_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        if (flags & NGX_JS_DICT_FLAG_MUST_EXIST) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            return NGX_DECLINED;
+        }
+
+        if (ngx_js_dict_add(dict, key, value, now) != NGX_OK) {
+            goto memory_error;
+        }
+
+    } else {
+        if (flags & NGX_JS_DICT_FLAG_MUST_NOT_EXIST) {
+            if (!dict->timeout || now < node->expire.key) {
+                ngx_rwlock_unlock(&dict->sh->rwlock);
+                return NGX_DECLINED;
+            }
+        }
+
+        if (ngx_js_dict_update(dict, node, value, now) != NGX_OK) {
+            goto memory_error;
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return NGX_OK;
+
+memory_error:
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    njs_vm_error3(vm, ngx_js_shared_dict_error_id, "", 0);
+
+    return NGX_ERROR;
+}
+
+
+static ngx_int_t
+ngx_js_dict_add(ngx_js_dict_t *dict, njs_str_t *key, njs_value_t *value,
+    ngx_msec_t now)
+{
+    size_t               n;
+    uint32_t             hash;
+    njs_str_t            string;
+    ngx_js_dict_node_t  *node;
+
+    if (dict->timeout) {
+        ngx_js_dict_expire(dict, now);
+    }
+
+    n = sizeof(ngx_js_dict_node_t) + key->length;
+    hash = ngx_crc32_long(key->start, key->length);
+
+    node = ngx_js_dict_alloc(dict, n);
+    if (node == NULL) {
+        return NGX_ERROR;
+    }
+
+    node->sn.str.data = (u_char *) node + sizeof(ngx_js_dict_node_t);
+
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        njs_value_string_get(value, &string);
+        node->u.value.data = ngx_js_dict_alloc(dict, string.length);
+        if (node->u.value.data == NULL) {
+            ngx_slab_free_locked(dict->shpool, node);
+            return NGX_ERROR;
+        }
+
+        ngx_memcpy(node->u.value.data, string.start, string.length);
+        node->u.value.len = string.length;
+
+    } else {
+        node->u.number = njs_value_number(value);
+    }
+
+    node->sn.node.key = hash;
+
+    ngx_memcpy(node->sn.str.data, key->start, key->length);
+    node->sn.str.len = key->length;
+
+    ngx_rbtree_insert(&dict->sh->rbtree, &node->sn.node);
+
+    if (dict->timeout) {
+        node->expire.key = now + dict->timeout;
+        ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_js_dict_update(ngx_js_dict_t *dict, ngx_js_dict_node_t *node,
+    njs_value_t *value, ngx_msec_t now)
+{
+    u_char     *p;
+    njs_str_t   string;
+
+    if (dict->type == NGX_JS_DICT_TYPE_STRING) {
+        njs_value_string_get(value, &string);
+
+        p = ngx_js_dict_alloc(dict, string.length);
+        if (p == NULL) {
+            return NGX_ERROR;
+        }
+
+        ngx_slab_free_locked(dict->shpool, node->u.value.data);
+        ngx_memcpy(p, string.start, string.length);
+
+        node->u.value.data = p;
+        node->u.value.len = string.length;
+
+    } else {
+        node->u.number = njs_value_number(value);
+    }
+
+    if (dict->timeout) {
+        ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
+        node->expire.key = now + dict->timeout;
+        ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
+    }
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_js_dict_delete(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key,
+    njs_value_t *retval)
+{
+    ngx_int_t            rc;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    node = ngx_js_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        ngx_rwlock_unlock(&dict->sh->rwlock);
+        return NGX_DECLINED;
+    }
+
+    if (dict->timeout) {
+        ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
+    }
+
+    ngx_rbtree_delete(&dict->sh->rbtree, (ngx_rbtree_node_t *) node);
+
+    if (retval != NULL) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (!dict->timeout || now < node->expire.key) {
+            rc = ngx_js_dict_copy_value_locked(vm, dict, node, retval);
+
+        } else {
+            rc = NGX_DECLINED;
+        }
+
+    } else {
+        rc = NGX_OK;
+    }
+
+    ngx_js_dict_node_free(dict, node);
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return rc;
+}
+
+
+static ngx_int_t
+ngx_js_dict_incr(ngx_js_dict_t *dict, njs_str_t *key, njs_value_t *delta,
+    njs_value_t *init, double *value)
+{
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    tp = ngx_timeofday();
+    now = tp->sec * 1000 + tp->msec;
+
+    ngx_rwlock_wlock(&dict->sh->rwlock);
+
+    node = ngx_js_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        njs_value_number_set(init, njs_value_number(init)
+                                   + njs_value_number(delta));
+        if (ngx_js_dict_add(dict, key, init, now) != NGX_OK) {
+            ngx_rwlock_unlock(&dict->sh->rwlock);
+            return NGX_ERROR;
+        }
+
+        *value = njs_value_number(init);
+
+    } else {
+        node->u.number += njs_value_number(delta);
+        *value = node->u.number;
+
+        if (dict->timeout) {
+            ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire);
+            node->expire.key = now + dict->timeout;
+            ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire);
+        }
+    }
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_js_dict_get(njs_vm_t *vm, ngx_js_dict_t *dict, njs_str_t *key,
+    njs_value_t *retval)
+{
+    ngx_int_t            rc;
+    ngx_msec_t           now;
+    ngx_time_t          *tp;
+    ngx_js_dict_node_t  *node;
+
+    ngx_rwlock_rlock(&dict->sh->rwlock);
+
+    node = ngx_js_dict_lookup(dict, key);
+
+    if (node == NULL) {
+        goto not_found;
+    }
+
+    if (dict->timeout) {
+        tp = ngx_timeofday();
+        now = tp->sec * 1000 + tp->msec;
+
+        if (now >= node->expire.key) {
+            goto not_found;
+        }
+    }
+
+    rc = ngx_js_dict_copy_value_locked(vm, dict, node, retval);
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+
+    return rc;
+
+not_found:
+
+    ngx_rwlock_unlock(&dict->sh->rwlock);
+    njs_value_undefined_set(retval);
+
+    return NGX_OK;
+}
+
+
+static ngx_int_t
+ngx_js_dict_copy_value_locked(njs_vm_t *vm, ngx_js_dict_t *dict,
+    ngx_js_dict_node_t *node, njs_value_t *retval)
+{
+    njs_int_t   ret;
+    njs_str_t   string;
+    ngx_uint_t  type;
+    ngx_pool_t  *pool;
+
+    type = dict->type;
+
+    if (type == NGX_JS_DICT_TYPE_STRING) {
+        pool = ngx_external_pool(vm, njs_vm_external_ptr(vm));
+
+        string.length = node->u.value.len;
+        string.start = ngx_pstrdup(pool, &node->u.value);
+        if (string.start == NULL) {
+            return NGX_ERROR;
+        }
+
+        ret = njs_vm_value_string_set(vm, retval, string.start, string.length);
+        if (ret != NJS_OK) {
+            return NGX_ERROR;
+        }
+
+    } else {
+        njs_value_number_set(retval, node->u.number);
+    }
+
+    return NGX_OK;
+}
+
+
+static void
+ngx_js_dict_expire(ngx_js_dict_t *dict, ngx_msec_t now)
+{
+    ngx_rbtree_t        *rbtree;
+    ngx_rbtree_node_t   *rn, *next;
+    ngx_js_dict_node_t  *node;
+
+    rbtree = &dict->sh->rbtree_expire;
+
+    if (rbtree->root == rbtree->sentinel) {
+        return;
+    }
+
+    for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+         rn != NULL;
+         rn = next)
+    {
+        if (rn->key > now) {
+            return;
+        }
+
+        node = (ngx_js_dict_node_t *)
+                   ((char *) rn - offsetof(ngx_js_dict_node_t, expire));
+
+        next = ngx_rbtree_next(rbtree, rn);
+
+        ngx_rbtree_delete(rbtree, rn);
+
+        ngx_rbtree_delete(&dict->sh->rbtree, (ngx_rbtree_node_t *) node);
+
+        ngx_js_dict_node_free(dict, node);
+    }
+}
+
+
+static void
+ngx_js_dict_evict(ngx_js_dict_t *dict, ngx_int_t count)
+{
+    ngx_rbtree_t        *rbtree;
+    ngx_rbtree_node_t   *rn, *next;
+    ngx_js_dict_node_t  *node;
+
+    rbtree = &dict->sh->rbtree_expire;
+
+    if (rbtree->root == rbtree->sentinel) {
+        return;
+    }
+
+    for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel);
+         rn != NULL;
+         rn = next)
+    {
+        if (count-- == 0) {
+            return;
+        }
+
+        node = (ngx_js_dict_node_t *)
+                   ((char *) rn - offsetof(ngx_js_dict_node_t, expire));
+
+        next = ngx_rbtree_next(rbtree, rn);
+
+        ngx_rbtree_delete(rbtree, rn);
+
+        ngx_rbtree_delete(&dict->sh->rbtree, (ngx_rbtree_node_t *) node);
+
+        ngx_js_dict_node_free(dict, node);
+    }
+}
+
+
+static njs_int_t
+ngx_js_dict_shared_error_name(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
+{
+    return njs_vm_value_string_set(vm, retval, (u_char *) "SharedMemoryError",
+                                   17);
+}
+
+
+static ngx_int_t
+ngx_js_dict_init_zone(ngx_shm_zone_t *shm_zone, void *data)
+{
+    ngx_js_dict_t  *prev = data;
+
+    size_t          len;
+    ngx_js_dict_t  *dict;
+
+    dict = shm_zone->data;
+
+    if (prev) {
+
+        if (dict->timeout && !prev->timeout) {
+            ngx_log_error(NGX_LOG_EMERG, shm_zone->shm.log, 0,
+                          "js_shared_dict_zone \"%V\" uses timeout %M "
+                          "while previously it did not use timeout",
+                          &shm_zone->shm.name, dict->timeout);
+            return NGX_ERROR;
+        }
+
+        if (dict->type != prev->type) {
+            ngx_log_error(NGX_LOG_EMERG, shm_zone->shm.log, 0,
+                          "js_shared_dict_zone \"%V\" had previously a "
+                          "different type", &shm_zone->shm.name, dict->timeout);
+            return NGX_ERROR;
+        }
+
+        dict->sh = prev->sh;
+        dict->shpool = prev->shpool;
+
+        return NGX_OK;
+    }
+
+    dict->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr;
+
+    if (shm_zone->shm.exists) {
+        dict->sh = dict->shpool->data;
+        return NGX_OK;
+    }
+
+    dict->sh = ngx_slab_calloc(dict->shpool, sizeof(ngx_js_dict_sh_t));
+    if (dict->sh == NULL) {
+        return NGX_ERROR;
+    }
+
+    dict->shpool->data = dict->sh;
+
+    ngx_rbtree_init(&dict->sh->rbtree, &dict->sh->sentinel,
+                    ngx_str_rbtree_insert_value);
+
+    if (dict->timeout) {
+        ngx_rbtree_init(&dict->sh->rbtree_expire,
+                        &dict->sh->sentinel_expire,
+                        ngx_rbtree_insert_timer_value);
+    }
+
+    len = sizeof(" in js shared dict zone \"\"") + shm_zone->shm.name.len;
+
+    dict->shpool->log_ctx = ngx_slab_alloc(dict->shpool, len);
+    if (dict->shpool->log_ctx == NULL) {
+        return NGX_ERROR;
+    }
+
+    ngx_sprintf(dict->shpool->log_ctx, " in js shared zone \"%V\"%Z",
+                &shm_zone->shm.name);
+
+    return NGX_OK;
+}
+
+
+char *
+ngx_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf,
+    void *tag)
+{
+    ngx_js_main_conf_t  *jmcf = conf;
+
+    u_char          *p;
+    ssize_t          size;
+    ngx_str_t       *value, name, s;
+    ngx_flag_t       evict;
+    ngx_msec_t       timeout;
+    ngx_uint_t       i, type;
+    ngx_js_dict_t   *dict;
+    ngx_shm_zone_t  *shm_zone;
+
+    size = 0;
+    evict = 0;
+    timeout = 0;
+    name.len = 0;
+    type = NGX_JS_DICT_TYPE_STRING;
+
+    value = cf->args->elts;
+
+    for (i = 1; i < cf->args->nelts; i++) {
+
+        if (ngx_strncmp(value[i].data, "zone=", 5) == 0) {
+
+            name.data = value[i].data + 5;
+
+            p = (u_char *) ngx_strchr(name.data, ':');
+
+            if (p == NULL) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid zone size \"%V\"", &value[i]);
+                return NGX_CONF_ERROR;
+            }
+
+            name.len = p - name.data;
+
+            if (name.len == 0) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid zone name \"%V\"", &value[i]);
+                return NGX_CONF_ERROR;
+            }
+
+            s.data = p + 1;
+            s.len = value[i].data + value[i].len - s.data;
+
+            size = ngx_parse_size(&s);
+
+            if (size == NGX_ERROR) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid zone size \"%V\"", &value[i]);
+                return NGX_CONF_ERROR;
+            }
+
+            if (size < (ssize_t) (8 * ngx_pagesize)) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "zone \"%V\" is too small", &value[i]);
+                return NGX_CONF_ERROR;
+            }
+
+            continue;
+        }
+
+        if (ngx_strncmp(value[i].data, "evict", 5) == 0) {
+            evict = 1;
+            continue;
+        }
+
+        if (ngx_strncmp(value[i].data, "timeout=", 8) == 0) {
+
+            s.data = value[i].data + 8;
+            s.len = value[i].len - 8;
+
+            timeout = ngx_parse_time(&s, 0);
+            if (timeout == (ngx_msec_t) NGX_ERROR) {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid timeout value \"%V\"", &value[i]);
+                return NGX_CONF_ERROR;
+            }
+
+            continue;
+        }
+
+        if (ngx_strncmp(value[i].data, "type=", 5) == 0) {
+
+            if (ngx_strcmp(&value[i].data[5], "string") == 0) {
+                type = NGX_JS_DICT_TYPE_STRING;
+
+            } else if (ngx_strcmp(&value[i].data[5], "number") == 0) {
+                type = NGX_JS_DICT_TYPE_NUMBER;
+
+            } else {
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                                   "invalid dict type \"%s\"",
+                                   &value[i].data[5]);
+                return NGX_CONF_ERROR;
+            }
+
+            continue;
+        }
+    }
+
+    if (name.len == 0) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "\"%V\" must have \"zone\" parameter", &cmd->name);
+        return NGX_CONF_ERROR;
+    }
+
+    if (evict && timeout == 0) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                           "evict requires timeout=");
+        return NGX_CONF_ERROR;
+    }
+
+    shm_zone = ngx_shared_memory_add(cf, &name, size, tag);
+    if (shm_zone == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
+    if (shm_zone->data) {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "duplicate zone \"%V\"",
+                           &name);
+        return NGX_CONF_ERROR;
+    }
+
+    dict = ngx_pcalloc(cf->pool, sizeof(ngx_js_dict_t));
+    if (dict == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
+    dict->shm_zone = shm_zone;
+    dict->next = jmcf->dicts;
+    jmcf->dicts = dict;
+
+    shm_zone->data = dict;
+    shm_zone->init = ngx_js_dict_init_zone;
+
+    dict->evict = evict;
+    dict->timeout = timeout;
+    dict->type = type;
+
+    return NGX_CONF_OK;
+}
+
+
+static njs_int_t
+ngx_js_shared_dict_preinit(njs_vm_t *vm)
+{
+    static const njs_str_t  error_name = njs_str("SharedMemoryError");
+
+    ngx_js_shared_dict_error_id =
+        njs_vm_external_constructor(vm, &error_name,
+                          njs_error_constructor, ngx_js_ext_error_ctor_props,
+                          njs_nitems(ngx_js_ext_error_ctor_props),
+                          ngx_js_ext_error_proto_props,
+                          njs_nitems(ngx_js_ext_error_ctor_props));
+    if (njs_slow_path(ngx_js_shared_dict_error_id < 0)) {
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
+
+
+static njs_int_t
+ngx_js_shared_dict_init(njs_vm_t *vm)
+{
+    ngx_js_shared_dict_proto_id = njs_vm_external_prototype(vm,
+                                          ngx_js_ext_shared_dict,
+                                          njs_nitems(ngx_js_ext_shared_dict));
+    if (ngx_js_shared_dict_proto_id < 0) {
+        return NJS_ERROR;
+    }
+
+    return NJS_OK;
+}
diff --git a/nginx/ngx_js_shared_dict.h b/nginx/ngx_js_shared_dict.h
new file mode 100644 (file)
index 0000000..4110c6c
--- /dev/null
@@ -0,0 +1,19 @@
+
+/*
+ * Copyright (C) Dmitry Volyntsev
+ * Copyright (C) NGINX, Inc.
+ */
+
+
+#ifndef _NGX_JS_SHARED_DICT_H_INCLUDED_
+#define _NGX_JS_SHARED_DICT_H_INCLUDED_
+
+njs_int_t njs_js_ext_global_shared_prop(njs_vm_t *vm, njs_object_prop_t *prop,
+    njs_value_t *value, njs_value_t *setval, njs_value_t *retval);
+njs_int_t njs_js_ext_global_shared_keys(njs_vm_t *vm, njs_value_t *value,
+    njs_value_t *keys);
+
+extern njs_module_t  ngx_js_shared_dict_module;
+
+
+#endif /* _NGX_JS_FETCH_H_INCLUDED_ */
index 3e2a2dab36d27dd805d2eaa37a7c1e0d8520c640..b9bba7d6f2f35aca5ed00b6b69be91a4d8fe1448 100644 (file)
@@ -122,9 +122,12 @@ static char *ngx_stream_js_var(ngx_conf_t *cf, ngx_command_t *cmd,
     void *conf);
 static ngx_int_t ngx_stream_js_init_conf_vm(ngx_conf_t *cf,
     ngx_js_loc_conf_t *conf);
+static void *ngx_stream_js_create_main_conf(ngx_conf_t *cf);
 static void *ngx_stream_js_create_srv_conf(ngx_conf_t *cf);
 static char *ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent,
     void *child);
+static char *ngx_stream_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf);
 
 static ngx_ssl_t *ngx_stream_js_ssl(njs_vm_t *vm, ngx_stream_session_t *s);
 static ngx_flag_t ngx_stream_js_ssl_verify(njs_vm_t *vm,
@@ -260,6 +263,13 @@ static ngx_command_t  ngx_stream_js_commands[] = {
 
 #endif
 
+    { ngx_string("js_shared_dict_zone"),
+      NGX_STREAM_MAIN_CONF|NGX_CONF_1MORE,
+      ngx_stream_js_shared_dict_zone,
+      0,
+      0,
+      NULL },
+
       ngx_null_command
 };
 
@@ -268,7 +278,7 @@ static ngx_stream_module_t  ngx_stream_js_module_ctx = {
     NULL,                           /* preconfiguration */
     ngx_stream_js_init,             /* postconfiguration */
 
-    NULL,                           /* create main configuration */
+    ngx_stream_js_create_main_conf, /* create main configuration */
     NULL,                           /* init main configuration */
 
     ngx_stream_js_create_srv_conf,  /* create server configuration */
@@ -550,6 +560,7 @@ static uintptr_t ngx_stream_js_uptr[] = {
     (uintptr_t) ngx_stream_js_fetch_timeout,
     (uintptr_t) ngx_stream_js_buffer_size,
     (uintptr_t) ngx_stream_js_max_response_buffer_size,
+    (uintptr_t) 0 /* main_conf ptr */,
 };
 
 
@@ -580,6 +591,7 @@ njs_module_t *njs_stream_js_addon_modules[] = {
      */
     &ngx_js_ngx_module,
     &ngx_js_fetch_module,
+    &ngx_js_shared_dict_module,
 #ifdef NJS_HAVE_OPENSSL
     &njs_webcrypto_module,
 #endif
@@ -1722,10 +1734,14 @@ ngx_js_stream_init(njs_vm_t *vm)
 static ngx_int_t
 ngx_stream_js_init_conf_vm(ngx_conf_t *cf, ngx_js_loc_conf_t *conf)
 {
-    njs_vm_opt_t  options;
+    njs_vm_opt_t        options;
+    ngx_js_main_conf_t  *jmcf;
 
     njs_vm_opt_init(&options);
 
+    jmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_js_module);
+    ngx_stream_js_uptr[NGX_JS_MAIN_CONF_INDEX] = (uintptr_t) jmcf;
+
     options.backtrace = 1;
     options.unhandled_rejection = NJS_VM_OPT_UNHANDLED_REJECTION_THROW;
     options.ops = &ngx_stream_js_ops;
@@ -1830,6 +1846,26 @@ ngx_stream_js_var(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
 }
 
 
+static void *
+ngx_stream_js_create_main_conf(ngx_conf_t *cf)
+{
+    ngx_js_main_conf_t  *jmcf;
+
+    jmcf = ngx_pcalloc(cf->pool, sizeof(ngx_js_main_conf_t));
+    if (jmcf == NULL) {
+        return NULL;
+    }
+
+    /*
+     * set by ngx_pcalloc():
+     *
+     *     jmcf->dicts = NULL;
+     */
+
+    return jmcf;
+}
+
+
 static void *
 ngx_stream_js_create_srv_conf(ngx_conf_t *cf)
 {
@@ -1862,6 +1898,14 @@ ngx_stream_js_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
 }
 
 
+static char *
+ngx_stream_js_shared_dict_zone(ngx_conf_t *cf, ngx_command_t *cmd,
+    void *conf)
+{
+    return ngx_js_shared_dict_zone(cf, cmd, conf, &ngx_stream_js_module);
+}
+
+
 static ngx_int_t
 ngx_stream_js_init(ngx_conf_t *cf)
 {
diff --git a/nginx/t/js_shared_dict.t b/nginx/t/js_shared_dict.t
new file mode 100644 (file)
index 0000000..867b2ad
--- /dev/null
@@ -0,0 +1,299 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for js_shared_dict_zone directive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+use Socket qw/ CRLF /;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_import test.js;
+
+    js_shared_dict_zone zone=foo:32k timeout=2s evict;
+    js_shared_dict_zone zone=bar:64k type=string;
+    js_shared_dict_zone zone=waka:32k type=number;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /add {
+            js_content test.add;
+        }
+
+        location /capacity {
+            js_content test.capacity;
+        }
+
+        location /chain {
+            js_content test.chain;
+        }
+
+        location /clear {
+            js_content test.clear;
+        }
+
+        location /delete {
+            js_content test.del;
+        }
+
+        location /free_space {
+            js_content test.free_space;
+        }
+
+        location /get {
+            js_content test.get;
+        }
+
+        location /has {
+            js_content test.has;
+        }
+
+        location /incr {
+            js_content test.incr;
+        }
+
+        location /keys {
+            js_content test.keys;
+        }
+
+        location /name {
+            js_content test.name;
+        }
+
+        location /pop {
+            js_content test.pop;
+        }
+
+        location /replace {
+            js_content test.replace;
+        }
+
+        location /set {
+            js_content test.set;
+        }
+
+        location /size {
+            js_content test.size;
+        }
+
+        location /zones {
+            js_content test.zones;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<'EOF');
+    function convertToValue(dict, v) {
+        if (dict.type == 'number') {
+            return parseInt(v);
+
+        } else if (v == 'empty') {
+            v = '';
+        }
+
+        return v;
+    }
+
+    function add(r) {
+        var dict = ngx.shared[r.args.dict];
+        var value = convertToValue(dict, r.args.value);
+        r.return(200, dict.add(r.args.key, value));
+    }
+
+    function capacity(r) {
+        var dict = ngx.shared[r.args.dict];
+        r.return(200, dict.capacity);
+    }
+
+    function chain(r) {
+        var dict = ngx.shared[r.args.dict];
+        var val = dict.set(r.args.key, r.args.value).get(r.args.key);
+        r.return(200, val);
+    }
+
+    function clear(r) {
+        var dict = ngx.shared[r.args.dict];
+        var result = dict.clear();
+        r.return(200, result === undefined ? 'undefined' : result);
+    }
+
+    function del(r) {
+        var dict = ngx.shared[r.args.dict];
+        r.return(200, dict.delete(r.args.key));
+    }
+
+    function free_space(r) {
+        var dict = ngx.shared[r.args.dict];
+        var free_space = dict.freeSpace();
+
+        r.return(200, free_space >= 0 && free_space <= dict.capacity);
+    }
+
+    function get(r) {
+        var dict = ngx.shared[r.args.dict];
+        var val = dict.get(r.args.key);
+
+        if (val == '') {
+            val = 'empty';
+
+        } else if (val === undefined) {
+            val = 'undefined';
+        }
+
+        r.return(200, val);
+    }
+
+    function has(r) {
+        var dict = ngx.shared[r.args.dict];
+        r.return(200, dict.has(r.args.key));
+    }
+
+    function incr(r) {
+        var dict = ngx.shared[r.args.dict];
+        var val = dict.incr(r.args.key, parseInt(r.args.by),
+                            parseInt(r.args.def));
+        r.return(200, val);
+    }
+
+    function keys(r) {
+        var ks;
+
+        if (r.args.max) {
+            ks = ngx.shared[r.args.dict].keys(parseInt(r.args.max));
+
+        } else {
+            ks = ngx.shared[r.args.dict].keys();
+        }
+
+        r.return(200, ks.toSorted());
+    }
+
+    function name(r) {
+        r.return(200, ngx.shared[r.args.dict].name);
+    }
+
+    function replace(r) {
+        var dict = ngx.shared[r.args.dict];
+        var value = convertToValue(dict, r.args.value);
+        r.return(200, dict.replace(r.args.key, value));
+    }
+
+    function pop(r) {
+        var dict = ngx.shared[r.args.dict];
+               var val = dict.pop(r.args.key);
+        if (val == '') {
+            val = 'empty';
+
+        } else if (val === undefined) {
+            val = 'undefined';
+        }
+
+        r.return(200, val);
+    }
+
+    function set(r) {
+        var dict = ngx.shared[r.args.dict];
+        var value = convertToValue(dict, r.args.value);
+        r.return(200, dict.set(r.args.key, value) === dict);
+    }
+
+    function size(r) {
+        var dict = ngx.shared[r.args.dict];
+        r.return(200, `size: ${dict.size()}`);
+    }
+
+    function zones(r) {
+        r.return(200, Object.keys(ngx.shared).sort());
+    }
+
+    export default { add, capacity, chain, clear, del, free_space, get, has,
+                     incr, keys, name, pop, replace, set, size, zones };
+EOF
+
+$t->try_run('no js_shared_dict_zone')->plan(38);
+
+###############################################################################
+
+like(http_get('/zones'), qr/bar,foo/, 'available zones');
+like(http_get('/capacity?dict=foo'), qr/32768/, 'foo capacity');
+like(http_get('/capacity?dict=bar'), qr/65536/, 'bar capacity');
+like(http_get('/free_space?dict=foo'), qr/true/, 'foo free space');
+like(http_get('/name?dict=foo'), qr/foo/, 'foo name');
+like(http_get('/size?dict=foo'), qr/size: 0/, 'no of items in foo');
+
+like(http_get('/add?dict=foo&key=FOO&value=xxx'), qr/true/, 'add foo.FOO');
+like(http_get('/add?dict=foo&key=FOO&value=xxx'), qr/false/,
+       'failed add foo.FOO');
+like(http_get('/set?dict=foo&key=FOO2&value=yyy'), qr/true/, 'set foo.FOO2');
+like(http_get('/set?dict=foo&key=FOO3&value=empty'), qr/true/, 'set foo.FOO3');
+like(http_get('/set?dict=bar&key=FOO&value=zzz'), qr/true/, 'set bar.FOO');
+like(http_get('/set?dict=waka&key=FOO&value=42'), qr/true/, 'set waka.FOO');
+like(http_get('/chain?dict=bar&key=FOO2&value=aaa'), qr/aaa/, 'chain bar.FOO2');
+
+like(http_get('/incr?dict=waka&key=FOO&by=5'), qr/47/, 'incr waka.FOO');
+like(http_get('/incr?dict=waka&key=FOO2&by=1'), qr/1/, 'incr waka.FOO2');
+like(http_get('/incr?dict=waka&key=FOO2&by=2'), qr/3/, 'incr waka.FOO2');
+like(http_get('/incr?dict=waka&key=FOO3&by=3&def=5'), qr/8/, 'incr waka.FOO3');
+
+like(http_get('/has?dict=foo&key=FOO'), qr/true/, 'has foo.FOO');
+like(http_get('/has?dict=foo&key=NOT_EXISTING'), qr/false/,
+       'failed has foo.NOT_EXISTING');
+like(http_get('/has?dict=waka&key=FOO'), qr/true/, 'has waka.FOO');
+
+$t->reload();
+
+like(http_get('/keys?dict=foo'), qr/FOO\,FOO2\,FOO3/, 'foo keys');
+like(http_get('/keys?dict=foo&max=2'), qr/FOO\,FOO3/, 'foo keys max 2');
+like(http_get('/get?dict=foo&key=FOO2'), qr/yyy/, 'get foo.FOO2');
+like(http_get('/get?dict=bar&key=FOO'), qr/zzz/, 'get bar.FOO');
+like(http_get('/get?dict=foo&key=FOO'), qr/xxx/, 'get foo.FOO');
+like(http_get('/get?dict=waka&key=FOO'), qr/47/, 'get waka.FOO');
+like(http_get('/delete?dict=foo&key=FOO'), qr/true/, 'delete foo.FOO');
+like(http_get('/get?dict=foo&key=FOO'), qr/undefined/, 'get foo.FOO');
+like(http_get('/get?dict=foo&key=FOO3'), qr/empty/, 'get foo.FOO3');
+like(http_get('/replace?dict=foo&key=FOO2&value=aaa'), qr/true/,
+       'replace foo.FOO2');
+like(http_get('/replace?dict=foo&key=NOT_EXISTING&value=aaa'), qr/false/,
+       'failed replace foo.NOT_EXISTING');
+
+select undef, undef, undef, 2.1;
+
+like(http_get('/get?dict=foo&key=FOO'), qr/undefined/, 'get expired foo.FOO');
+like(http_get('/pop?dict=foo&key=FOO'), qr/undefined/, 'pop expired foo.FOO');
+like(http_get('/size?dict=foo'), qr/size: 2/, 'no of items in foo');
+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');
+like(http_get('/clear?dict=foo'), qr/undefined/, 'clear foo');
+like(http_get('/size?dict=foo'), qr/size: 0/, 'no of items in foo after clear');
diff --git a/nginx/t/stream_js_shared_dict.t b/nginx/t/stream_js_shared_dict.t
new file mode 100644 (file)
index 0000000..e8e482f
--- /dev/null
@@ -0,0 +1,188 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) Nginx, Inc.
+
+# Tests for js_shared_dict_zone directive.
+
+###############################################################################
+
+use warnings;
+use strict;
+
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite stream/)
+       ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location / {
+            return 200;
+        }
+    }
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_import test.js;
+
+    js_shared_dict_zone zone=foo:32k;
+
+    server {
+        listen      127.0.0.1:8081;
+        js_preread  test.preread_verify;
+        proxy_pass  127.0.0.1:8090;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        js_preread  test.control_access;
+        proxy_pass  127.0.0.1:8080;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<EOF);
+    import qs from 'querystring';
+
+    function preread_verify(s) {
+        var collect = Buffer.from([]);
+
+        s.on('upstream', function (data, flags) {
+            collect = Buffer.concat([collect, data]);
+
+            if (collect.length >= 4 && collect.readUInt16BE(0) == 0xabcd) {
+                let id = collect.slice(2,4);
+
+                ngx.shared.foo.get(id) ? s.done(): s.deny();
+
+            } else if (collect.length) {
+                s.deny();
+            }
+        });
+    }
+
+    function control_access(s) {
+        var req = '';
+
+        s.on('upload', function(data, flags) {
+            req += data;
+
+            var n = req.search('\\n');
+            if (n != -1) {
+                var params = req.substr(0, n).split(' ')[1].split('?')[1];
+
+                var args = qs.parse(params);
+                switch (args.action) {
+                case 'set':
+                    ngx.shared.foo.set(args.key, args.value);
+                    break;
+                case 'del':
+                    ngx.shared.foo.delete(args.key);
+                    break;
+                }
+
+                s.done();
+            }
+        });
+    }
+
+    export default { preread_verify, control_access };
+
+EOF
+
+$t->try_run('no js_shared_dict_zone')->plan(9);
+
+$t->run_daemon(\&stream_daemon, port(8090));
+$t->waitforsocket('127.0.0.1:' . port(8090));
+
+###############################################################################
+
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQZ##"), "",
+       'access failed, QZ is not in the shared dict');
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQQ##"), "",
+       'access failed, QQ is not in the shared dict');
+like(get('/?action=set&key=QZ&value=1'), qr/200/, 'set foo.QZ');
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQZ##"), "\xAB\xCDQZ##",
+       'access granted');
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQQ##"), "",
+       'access failed, QQ is not in the shared dict');
+like(get('/?action=del&key=QZ'), qr/200/, 'del foo.QZ');
+like(get('/?action=set&key=QQ&value=1'), qr/200/, 'set foo.QQ');
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQZ##"), "",
+       'access failed, QZ is not in the shared dict');
+is(stream('127.0.0.1:' . port(8081))->io("\xAB\xCDQQ##"), "\xAB\xCDQQ##",
+       'access granted');
+
+###############################################################################
+
+sub stream_daemon {
+       my $server = IO::Socket::INET->new(
+               Proto => 'tcp',
+               LocalAddr => '127.0.0.1:' . port(8090),
+               Listen => 5,
+               Reuse => 1
+       )
+               or die "Can't create listening socket: $!\n";
+
+       local $SIG{PIPE} = 'IGNORE';
+
+       while (my $client = $server->accept()) {
+               $client->autoflush(1);
+
+               log2c("(new connection $client)");
+
+               $client->sysread(my $buffer, 65536) or next;
+
+               log2i("$client $buffer");
+
+               log2o("$client $buffer");
+
+               $client->syswrite($buffer);
+
+               close $client;
+       }
+}
+
+sub log2i { Test::Nginx::log_core('|| <<', @_); }
+sub log2o { Test::Nginx::log_core('|| >>', @_); }
+sub log2c { Test::Nginx::log_core('||', @_); }
+
+sub get {
+       my ($url, %extra) = @_;
+
+       my $s = IO::Socket::INET->new(
+               Proto => 'tcp',
+               PeerAddr => '127.0.0.1:' . port(8082)
+       ) or die "Can't connect to nginx: $!\n";
+
+       return http_get($url, socket => $s);
+}
+
+###############################################################################
index 42fce1b62e4d3f633e763bcccc3ce37039360a23..fd06e3027ff931026ec52f84878e192a7a6e935f 100644 (file)
@@ -242,6 +242,150 @@ interface NgxFetchOptions {
     verify?: boolean;
 }
 
+/**
+ * This Error object is thrown when adding an item to a shared dictionary
+ * that does not have enough free space.
+ * @since 0.8.0
+ */
+declare class SharedMemoryError extends Error {}
+
+type NgxSharedDictValue = string | number;
+
+/**
+ * Interface of a dictionary shared among the working processes.
+ * It can store either `string` or `number` values which is specified when
+ * declaring the zone.
+ *
+ * @template {V} The type of stored values.
+ * @since 0.8.0
+ */
+interface NgxSharedDict<V extends string | number = string | number> {
+    /**
+     * The capacity of this shared dictionary in bytes.
+     */
+    readonly capacity: number;
+    /**
+     * The name of this shared dictionary.
+     */
+    readonly name: string;
+
+    /**
+     * Sets the `value` for the specified `key` in the dictionary only if the
+     * `key` does not exist yet.
+     *
+     * @param key The key of the item to add.
+     * @param value The value of the item to add.
+     * @returns `true` if the value has been added successfully, `false`
+     *   if the `key` already exists in this dictionary.
+     * @throws {SharedMemoryError} if there's not enough free space in this
+     *   dictionary.
+     * @throws {TypeError} if the `value` is of a different type than expected
+     *   by this dictionary.
+     */
+    add(key: string, value: V): boolean;
+    /**
+     * Removes all items from this dictionary.
+     */
+    clear(): void;
+    /**
+     * Removes the item associated with the specified `key` from the dictionary.
+     *
+     * @param key The key of the item to remove.
+     * @returns `true` if the item in the dictionary existed and has been
+     *   removed, `false` otherwise.
+     */
+    delete(key: string): boolean;
+    /**
+     * Increments the value associated with the `key` by the given `delta`.
+     * If the `key` doesn't exist, the item will be initialized to `init`.
+     *
+     * **Important:** This method can be used only if the dictionary was
+     * declared with `type=number`!
+     *
+     * @param key is a string key.
+     * @param delta The number to increment/decrement the value by.
+     * @param init The number to initialize the item with if it didn't exist
+     *   (default is `0`).
+     * @returns The new value.
+     * @throws {SharedMemoryError} if there's not enough free space in this
+     *   dictionary.
+     * @throws {TypeError} if this dictionary does not expect numbers.
+     */
+    incr: V extends number
+      ? (key: string, delta: V, init?: number) => number
+      : never;
+    /**
+     * @returns The free page size in bytes.
+     *   Note that even if the free page is zero the dictionary may still accept
+     *   new values if there is enough space in the occupied pages.
+     */
+    freeSpace(): number;
+    /**
+     * @param key The key of the item to retrieve.
+     * @returns The value associated with the `key`, or `undefined` if there
+     *   is none.
+     */
+    get(key: string): V | undefined;
+    /**
+     * @param key The key to search for.
+     * @returns `true` if an item with the specified `key` exists, `false`
+     *   otherwise.
+     */
+    has(key: string): boolean;
+    /**
+     * @param maxCount The maximum number of keys to retrieve (default is 1024).
+     * @returns An array of the dictionary keys.
+     */
+    keys(maxCount?: number): string[];
+    /**
+     * Removes the item associated with the specified `key` from the dictionary
+     * and returns its value.
+     *
+     * @param key The key of the item to remove.
+     * @returns The value associated with the `key`, or `undefined` if there
+     *   is none.
+     */
+    pop(key: string): V | undefined;
+     /**
+     * Sets the `value` for the specified `key` in the dictionary only if the
+     * `key` already exists.
+     *
+     * @param key The key of the item to replace.
+     * @param value The new value of the item.
+     * @returns `true` if the value has been replaced successfully, `false`
+     *   if the key doesn't exist in this dictionary.
+     * @throws {SharedMemoryError} if there's not enough free space in this
+     *   dictionary.
+     * @throws {TypeError} if the `value` is of a different type than expected
+     *   by this dictionary.
+     */
+    replace(key: string, value: V): boolean;
+    /**
+     * Sets the `value` for the specified `key` in the dictionary.
+     *
+     * @param key The key of the item to set.
+     * @param value The value of the item to set.
+     * @returns This dictionary (for method chaining).
+     * @throws {SharedMemoryError} if there's not enough free space in this
+     *   dictionary.
+     * @throws {TypeError} if the `value` is of a different type than expected
+     *   by this dictionary.
+     */
+    set(key: string, value: V): this;
+    /**
+     * @returns The number of items in this shared dictionary.
+     */
+    size(): number;
+}
+
+interface NgxGlobalShared {
+    /**
+     * Shared dictionaries.
+     * @since 0.8.0
+     */
+    readonly [prop: string]: NgxSharedDict;
+}
+
 interface NgxObject {
     /**
      * A string containing an optional nginx build name, corresponds to the
@@ -296,6 +440,12 @@ interface NgxObject {
      * @since 0.8.0
      */
     readonly prefix: string;
+
+    /**
+     * An object containing shared data between all worker processes.
+     * @since 0.8.0
+     */
+    readonly shared: NgxGlobalShared;
     /**
      * A string containing nginx version, for example: "1.25.0"
      * @since 0.8.0