diff --git a/nginx/ngx_http_js_module.c b/nginx/ngx_http_js_module.c index 064292414..3e43ac7d7 100644 --- a/nginx/ngx_http_js_module.c +++ b/nginx/ngx_http_js_module.c @@ -1136,6 +1136,7 @@ static JSClassDef ngx_http_qjs_headers_out_class = { qjs_module_t *njs_http_qjs_addon_modules[] = { &ngx_qjs_ngx_module, + &ngx_qjs_ngx_shared_dict_module, /* * Shared addons should be in the same order and the same positions * in all nginx modules. diff --git a/nginx/ngx_js.h b/nginx/ngx_js.h index 8319dc856..3a51aef0c 100644 --- a/nginx/ngx_js.h +++ b/nginx/ngx_js.h @@ -60,6 +60,9 @@ #define NGX_QJS_CLASS_ID_STREAM_PERIODIC (NGX_QJS_CLASS_ID_OFFSET + 8) #define NGX_QJS_CLASS_ID_STREAM_FLAGS (NGX_QJS_CLASS_ID_OFFSET + 9) #define NGX_QJS_CLASS_ID_STREAM_VARS (NGX_QJS_CLASS_ID_OFFSET + 10) +#define NGX_QJS_CLASS_ID_SHARED (NGX_QJS_CLASS_ID_OFFSET + 11) +#define NGX_QJS_CLASS_ID_SHARED_DICT (NGX_QJS_CLASS_ID_OFFSET + 12) +#define NGX_QJS_CLASS_ID_SHARED_DICT_ERROR (NGX_QJS_CLASS_ID_OFFSET + 13) typedef struct ngx_js_loc_conf_s ngx_js_loc_conf_t; @@ -377,6 +380,7 @@ ngx_int_t ngx_qjs_string(ngx_engine_t *e, JSValueConst val, ngx_str_t *str); extern qjs_module_t qjs_zlib_module; extern qjs_module_t ngx_qjs_ngx_module; +extern qjs_module_t ngx_qjs_ngx_shared_dict_module; #endif diff --git a/nginx/ngx_js_shared_dict.c b/nginx/ngx_js_shared_dict.c index 19f6f4bd4..e317211e0 100644 --- a/nginx/ngx_js_shared_dict.c +++ b/nginx/ngx_js_shared_dict.c @@ -113,6 +113,68 @@ static njs_int_t ngx_js_shared_dict_init(njs_vm_t *vm); static void ngx_js_dict_node_free(ngx_js_dict_t *dict, ngx_js_dict_node_t *node); +#if (NJS_HAVE_QUICKJS) +static int ngx_qjs_shared_own_property(JSContext *cx, + JSPropertyDescriptor *pdesc, JSValueConst obj, JSAtom prop); +static int ngx_qjs_shared_own_property_names(JSContext *ctx, + JSPropertyEnum **ptab, uint32_t *plen, JSValueConst obj); + +static JSValue ngx_qjs_ext_ngx_shared(JSContext *cx, JSValueConst this_val); +static JSValue ngx_qjs_ext_shared_dict_capacity(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_shared_dict_clear(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_delete(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_free_space(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_get(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_has(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_incr(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_items(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_keys(JSContext *cx, + JSValueConst this_val, int argc, JSValueConst *argv); +static JSValue ngx_qjs_ext_shared_dict_name(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_shared_dict_pop(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv); +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_tag(JSContext *cx, + JSValueConst this_val); +static JSValue ngx_qjs_ext_shared_dict_type(JSContext *cx, + JSValueConst this_val); + +static JSValue ngx_qjs_dict_copy_value_locked(JSContext *cx, + ngx_js_dict_t *dict, ngx_js_dict_node_t *node); +static ngx_js_dict_node_t *ngx_qjs_dict_lookup(ngx_js_dict_t *dict, + ngx_str_t *key); +static ngx_int_t ngx_qjs_dict_add(JSContext *cx, ngx_js_dict_t *dict, + ngx_str_t *key, JSValue value, ngx_msec_t timeout, ngx_msec_t now); +static JSValue ngx_qjs_dict_delete(JSContext *cx, ngx_js_dict_t *dict, + ngx_str_t *key, int retval); +static JSValue ngx_qjs_dict_get(JSContext *cx, ngx_js_dict_t *dict, + ngx_str_t *key); +static JSValue ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict, + ngx_str_t *key, double delta, double init, ngx_msec_t timeout); +static JSValue ngx_qjs_dict_set(JSContext *cx, ngx_js_dict_t *dict, + ngx_str_t *key, JSValue value, ngx_msec_t timeout, unsigned flags); +static ngx_int_t ngx_qjs_dict_update(JSContext *cx, ngx_js_dict_t *dict, + ngx_js_dict_node_t *node, JSValue value, ngx_msec_t timeout, + ngx_msec_t now); + +static JSValue ngx_qjs_throw_shared_memory_error(JSContext *cx); + +static JSModuleDef *ngx_qjs_ngx_shared_dict_init(JSContext *cx, + const char *name); +#endif + static njs_external_t ngx_js_ext_shared_dict[] = { @@ -356,6 +418,62 @@ njs_module_t ngx_js_shared_dict_module = { }; +#if (NJS_HAVE_QUICKJS) + +static const JSCFunctionListEntry ngx_qjs_ext_ngx[] = { + JS_CGETSET_DEF("shared", ngx_qjs_ext_ngx_shared, NULL), +}; + +static const JSCFunctionListEntry ngx_qjs_ext_shared_dict[] = { + JS_CGETSET_DEF("[Symbol.toStringTag]", ngx_qjs_ext_shared_dict_tag, NULL), + JS_CFUNC_MAGIC_DEF("add", 3, ngx_qjs_ext_shared_dict_set, + NGX_JS_DICT_FLAG_MUST_NOT_EXIST), + JS_CGETSET_DEF("capacity", ngx_qjs_ext_shared_dict_capacity, NULL), + JS_CFUNC_DEF("clear", 0, ngx_qjs_ext_shared_dict_clear), + JS_CFUNC_DEF("delete", 1, ngx_qjs_ext_shared_dict_delete), + JS_CFUNC_DEF("freeSpace", 0, ngx_qjs_ext_shared_dict_free_space), + JS_CFUNC_DEF("get", 1, ngx_qjs_ext_shared_dict_get), + JS_CFUNC_DEF("has", 1, ngx_qjs_ext_shared_dict_has), + JS_CFUNC_DEF("incr", 3, ngx_qjs_ext_shared_dict_incr), + JS_CFUNC_DEF("items", 0, ngx_qjs_ext_shared_dict_items), + JS_CFUNC_DEF("keys", 0, ngx_qjs_ext_shared_dict_keys), + JS_CGETSET_DEF("name", ngx_qjs_ext_shared_dict_name, NULL), + JS_CFUNC_DEF("pop", 1, ngx_qjs_ext_shared_dict_pop), + JS_CFUNC_MAGIC_DEF("replace", 3, ngx_qjs_ext_shared_dict_set, + 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_CGETSET_DEF("type", ngx_qjs_ext_shared_dict_type, NULL), +}; + +static const JSCFunctionListEntry ngx_qjs_ext_shared_dict_error[] = { + JS_PROP_STRING_DEF("name", "SharedMemoryError", + JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE), + JS_PROP_STRING_DEF("message", "", JS_PROP_WRITABLE | JS_PROP_CONFIGURABLE), +}; + +static JSClassDef ngx_qjs_shared_dict_class = { + "SharedDict", + .finalizer = NULL, +}; + +static JSClassDef ngx_qjs_shared_class = { + "Shared", + .finalizer = NULL, + .exotic = & (JSClassExoticMethods) { + .get_own_property = ngx_qjs_shared_own_property, + .get_own_property_names = ngx_qjs_shared_own_property_names, + }, +}; + +qjs_module_t ngx_qjs_ngx_shared_dict_module = { + .name = "shared_dict", + .init = ngx_qjs_ngx_shared_dict_init, +}; + +#endif + + 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) @@ -1786,3 +1904,1197 @@ ngx_js_shared_dict_init(njs_vm_t *vm) return NJS_OK; } + + +#if (NJS_HAVE_QUICKJS) + +static int +ngx_qjs_shared_own_property(JSContext *cx, JSPropertyDescriptor *pdesc, + JSValueConst obj, JSAtom prop) +{ + int ret; + ngx_str_t name; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + ngx_js_main_conf_t *conf; + + name.data = (u_char *) JS_AtomToCString(cx, prop); + if (name.data == NULL) { + return -1; + } + + name.len = ngx_strlen(name.data); + + ret = 0; + conf = ngx_qjs_main_conf(cx); + + for (dict = conf->dicts; dict != NULL; dict = dict->next) { + shm_zone = dict->shm_zone; + + if (shm_zone->shm.name.len == name.len + && ngx_strncmp(shm_zone->shm.name.data, name.data, name.len) + == 0) + { + if (pdesc != NULL) { + pdesc->flags = JS_PROP_ENUMERABLE; + pdesc->getter = JS_UNDEFINED; + pdesc->setter = JS_UNDEFINED; + pdesc->value = JS_NewObjectClass(cx, + NGX_QJS_CLASS_ID_SHARED_DICT); + if (JS_IsException(pdesc->value)) { + ret = -1; + break; + } + + JS_SetOpaque(pdesc->value, shm_zone); + } + + ret = 1; + break; + } + } + + JS_FreeCString(cx, (char *) name.data); + + return ret; +} + + +static int +ngx_qjs_shared_own_property_names(JSContext *cx, JSPropertyEnum **ptab, + uint32_t *plen, JSValueConst obj) +{ + int ret; + JSAtom key; + JSValue keys; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + ngx_js_main_conf_t *conf; + + keys = JS_NewObject(cx); + if (JS_IsException(keys)) { + return -1; + } + + conf = ngx_qjs_main_conf(cx); + + for (dict = conf->dicts; dict != NULL; dict = dict->next) { + shm_zone = dict->shm_zone; + + key = JS_NewAtomLen(cx, (const char *) shm_zone->shm.name.data, + shm_zone->shm.name.len); + if (key == JS_ATOM_NULL) { + return -1; + } + + if (JS_DefinePropertyValue(cx, keys, key, JS_UNDEFINED, + JS_PROP_ENUMERABLE) < 0) + { + JS_FreeAtom(cx, key); + return -1; + } + + JS_FreeAtom(cx, key); + } + + ret = JS_GetOwnPropertyNames(cx, ptab, plen, keys, JS_GPN_STRING_MASK); + + JS_FreeValue(cx, keys); + + return ret; +} + + +static JSValue +ngx_qjs_ext_ngx_shared(JSContext *cx, JSValueConst this_val) +{ + return JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_SHARED); +} + + +static JSValue +ngx_qjs_ext_shared_dict_capacity(JSContext *cx, JSValueConst this_val) +{ + ngx_shm_zone_t *shm_zone; + + shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT); + if (shm_zone == NULL) { + return JS_UNDEFINED; + } + + return JS_NewInt32(cx, shm_zone->shm.size); +} + + +static JSValue +ngx_qjs_ext_shared_dict_clear(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_rbtree_t *rbtree; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + ngx_rbtree_node_t *rn, *next; + + 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; + + ngx_rwlock_wlock(&dict->sh->rwlock); + + if (dict->timeout) { + ngx_js_dict_evict(dict, 0x7fffffff /* INT_MAX */); + + } else { + rbtree = &dict->sh->rbtree; + + if (rbtree->root == rbtree->sentinel) { + goto done; + } + + for (rn = ngx_rbtree_min(rbtree->root, rbtree->sentinel); + rn != NULL; + rn = next) + { + next = ngx_rbtree_next(rbtree, rn); + + ngx_rbtree_delete(rbtree, rn); + + ngx_js_dict_node_free(dict, (ngx_js_dict_node_t *) rn); + } + } + +done: + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_ext_shared_dict_delete(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_str_t key; + ngx_js_ctx_t *ctx; + ngx_shm_zone_t *shm_zone; + + 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"); + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) { + return JS_EXCEPTION; + } + + return ngx_qjs_dict_delete(cx, shm_zone->data, &key, 0); +} + + +static JSValue +ngx_qjs_ext_shared_dict_free_space(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + size_t bytes; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + + 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; + + ngx_rwlock_rlock(&dict->sh->rwlock); + bytes = dict->shpool->pfree * ngx_pagesize; + ngx_rwlock_unlock(&dict->sh->rwlock); + + return JS_NewInt32(cx, bytes); +} + + +static JSValue +ngx_qjs_ext_shared_dict_get(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_str_t key; + ngx_js_ctx_t *ctx; + ngx_shm_zone_t *shm_zone; + + 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"); + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) { + return JS_EXCEPTION; + } + + return ngx_qjs_dict_get(cx, shm_zone->data, &key); +} + + +static JSValue +ngx_qjs_ext_shared_dict_has(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_str_t key; + ngx_msec_t now; + ngx_time_t *tp; + ngx_js_ctx_t *ctx; + 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"); + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) { + return JS_EXCEPTION; + } + + dict = shm_zone->data; + + ngx_rwlock_rlock(&dict->sh->rwlock); + + node = ngx_qjs_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); + + return JS_NewBool(cx, node != NULL); +} + + +static JSValue +ngx_qjs_ext_shared_dict_incr(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + double delta, init; + uint32_t timeout; + ngx_str_t key; + ngx_js_ctx_t *ctx; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + + 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->type != NGX_JS_DICT_TYPE_NUMBER) { + return JS_ThrowTypeError(cx, "shared dict is not a number dict"); + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) { + return JS_EXCEPTION; + } + + if (JS_ToFloat64(cx, &delta, argv[1]) < 0) { + return JS_EXCEPTION; + } + + if (JS_ToFloat64(cx, &init, argv[2]) < 0) { + return JS_EXCEPTION; + } + + if (argc > 3) { + if (JS_ToUint32(cx, &timeout, argv[3]) < 0) { + return JS_EXCEPTION; + } + + if (!dict->timeout) { + return JS_ThrowTypeError(cx, + "shared dict must be declared with timeout"); + } + + if (timeout < 1) { + return JS_ThrowRangeError(cx, + "timeout must be greater than or equal to 1"); + } + + } else { + timeout = dict->timeout; + } + + return ngx_qjs_dict_incr(cx, dict, &key, delta, init, timeout); +} + + +static JSValue +ngx_qjs_ext_shared_dict_items(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue arr, kv, v; + uint32_t max_count, i; + ngx_msec_t now; + ngx_time_t *tp; + 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 = 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; + + max_count = 1024; + + if (argc > 0) { + if (JS_ToUint32(cx, &max_count, argv[0]) < 0) { + return JS_EXCEPTION; + } + } + + rbtree = &dict->sh->rbtree; + + ngx_rwlock_rlock(&dict->sh->rwlock); + + if (dict->timeout) { + tp = ngx_timeofday(); + now = tp->sec * 1000 + tp->msec; + ngx_js_dict_expire(dict, now); + } + + if (rbtree->root == rbtree->sentinel) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_NewArray(cx); + } + + arr = JS_NewArray(cx); + if (JS_IsException(arr)) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_EXCEPTION; + } + + i = 0; + + 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; + + kv = JS_NewArray(cx); + if (JS_IsException(kv)) { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, arr); + return JS_EXCEPTION; + } + + v = JS_NewStringLen(cx, (const char *) node->sn.str.data, + node->sn.str.len); + if (JS_IsException(v)) { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, kv); + JS_FreeValue(cx, arr); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, kv, 0, v, JS_PROP_C_W_E) < 0) { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, v); + JS_FreeValue(cx, kv); + JS_FreeValue(cx, arr); + return JS_EXCEPTION; + } + + v = ngx_qjs_dict_copy_value_locked(cx, dict, node); + + if (JS_DefinePropertyValueUint32(cx, kv, 1, v, JS_PROP_C_W_E) < 0) { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, v); + JS_FreeValue(cx, kv); + JS_FreeValue(cx, arr); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, arr, i++, kv, JS_PROP_C_W_E) < 0) { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, kv); + JS_FreeValue(cx, arr); + return JS_EXCEPTION; + } + } + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return arr; +} + + +static JSValue +ngx_qjs_ext_shared_dict_keys(JSContext *cx, JSValueConst this_val, int argc, + JSValueConst *argv) +{ + JSValue arr, key; + uint32_t max_count, i; + ngx_msec_t now; + ngx_time_t *tp; + 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 = 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; + + max_count = 1024; + + if (argc > 0) { + if (JS_ToUint32(cx, &max_count, argv[0]) < 0) { + return JS_EXCEPTION; + } + } + + rbtree = &dict->sh->rbtree; + + ngx_rwlock_rlock(&dict->sh->rwlock); + + if (dict->timeout) { + tp = ngx_timeofday(); + now = tp->sec * 1000 + tp->msec; + ngx_js_dict_expire(dict, now); + } + + if (rbtree->root == rbtree->sentinel) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_NewArray(cx); + } + + arr = JS_NewArray(cx); + if (JS_IsException(arr)) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_EXCEPTION; + } + + i = 0; + + 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; + + key = JS_NewStringLen(cx, (const char *) node->sn.str.data, + node->sn.str.len); + if (JS_IsException(key)) { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, arr); + return JS_EXCEPTION; + } + + if (JS_DefinePropertyValueUint32(cx, arr, i++, key, + JS_PROP_C_W_E) < 0) + { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, key); + JS_FreeValue(cx, arr); + return JS_EXCEPTION; + } + } + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return arr; +} + + +static JSValue +ngx_qjs_ext_shared_dict_name(JSContext *cx, JSValueConst this_val) +{ + ngx_shm_zone_t *shm_zone; + + shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT); + if (shm_zone == NULL) { + return JS_UNDEFINED; + } + + return JS_NewStringLen(cx, (const char *) shm_zone->shm.name.data, + shm_zone->shm.name.len); +} + + +static JSValue +ngx_qjs_ext_shared_dict_pop(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + ngx_str_t key; + ngx_js_ctx_t *ctx; + ngx_shm_zone_t *shm_zone; + + 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"); + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) { + return JS_EXCEPTION; + } + + return ngx_qjs_dict_delete(cx, shm_zone->data, &key, 1); +} + + +static JSValue +ngx_qjs_ext_shared_dict_set(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv, int flags) +{ + JSValue ret; + uint32_t timeout; + ngx_str_t key; + ngx_js_ctx_t *ctx; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + + 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"); + } + + ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); + + if (ngx_qjs_string(ctx->engine, argv[0], &key) != NGX_OK) { + return JS_EXCEPTION; + } + + dict = shm_zone->data; + + if (dict->type == NGX_JS_DICT_TYPE_STRING) { + if (!JS_IsString(argv[1])) { + return JS_ThrowTypeError(cx, "string value is expected"); + } + + } else { + if (!JS_IsNumber(argv[1])) { + return JS_ThrowTypeError(cx, "number value is expected"); + } + } + + if (!JS_IsUndefined(argv[2])) { + if (!JS_IsNumber(argv[2])) { + return JS_ThrowTypeError(cx, "timeout is not a number"); + } + + if (!dict->timeout) { + return JS_ThrowTypeError(cx, + "shared dict must be declared with timeout"); + } + + if (JS_ToUint32(cx, &timeout, argv[2]) < 0) { + return JS_EXCEPTION; + } + + if (timeout < 1) { + return JS_ThrowTypeError(cx, + "timeout must be greater than or equal to 1"); + } + + } else { + timeout = dict->timeout; + } + + ret = ngx_qjs_dict_set(cx, shm_zone->data, &key, argv[1], timeout, flags); + if (JS_IsException(ret)) { + return JS_EXCEPTION; + } + + if (flags) { + /* add() or replace(). */ + return ret; + } + + return JS_DupValue(cx, this_val); +} + + +static JSValue +ngx_qjs_ext_shared_dict_size(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + njs_int_t items; + ngx_msec_t now; + ngx_time_t *tp; + ngx_rbtree_t *rbtree; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + ngx_rbtree_node_t *rn; + + 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; + + ngx_rwlock_rlock(&dict->sh->rwlock); + + if (dict->timeout) { + tp = ngx_timeofday(); + now = tp->sec * 1000 + tp->msec; + ngx_js_dict_expire(dict, now); + } + + rbtree = &dict->sh->rbtree; + + if (rbtree->root == rbtree->sentinel) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_NewInt32(cx, 0); + } + + 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); + + return JS_NewInt32(cx, items); +} + + +static JSValue +ngx_qjs_ext_shared_dict_tag(JSContext *cx, JSValueConst this_val) +{ + return JS_NewString(cx, "SharedDict"); +} + + +static JSValue +ngx_qjs_ext_shared_dict_type(JSContext *cx, JSValueConst this_val) +{ + ngx_str_t type; + ngx_js_dict_t *dict; + ngx_shm_zone_t *shm_zone; + + shm_zone = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_SHARED_DICT); + if (shm_zone == NULL) { + return JS_UNDEFINED; + } + + dict = shm_zone->data; + + switch (dict->type) { + case NGX_JS_DICT_TYPE_STRING: + ngx_str_set(&type, "string"); + break; + + default: + ngx_str_set(&type, "number"); + break; + } + + return JS_NewStringLen(cx, (const char *) type.data, type.len); +} + + +static JSValue +ngx_qjs_dict_copy_value_locked(JSContext *cx, ngx_js_dict_t *dict, + ngx_js_dict_node_t *node) +{ + if (dict->type == NGX_JS_DICT_TYPE_STRING) { + return JS_NewStringLen(cx, (const char *) node->u.value.data, + node->u.value.len); + } + + /* NGX_JS_DICT_TYPE_NUMBER */ + + return JS_NewFloat64(cx, node->u.number); +} + + +static ngx_js_dict_node_t * +ngx_qjs_dict_lookup(ngx_js_dict_t *dict, ngx_str_t *key) +{ + uint32_t hash; + ngx_rbtree_t *rbtree; + + rbtree = &dict->sh->rbtree; + + hash = ngx_crc32_long(key->data, key->len); + + return (ngx_js_dict_node_t *) ngx_str_rbtree_lookup(rbtree, key, hash); +} + + +static ngx_int_t +ngx_qjs_dict_add(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, + JSValue value, ngx_msec_t timeout, ngx_msec_t now) +{ + size_t n; + uint32_t hash; + ngx_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->len; + hash = ngx_crc32_long(key->data, key->len); + + 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) { + string.data = (u_char *) JS_ToCStringLen(cx, &string.len, value); + if (string.data == NULL) { + ngx_slab_free_locked(dict->shpool, node); + return NGX_ERROR; + } + + node->u.value.data = ngx_js_dict_alloc(dict, string.len); + if (node->u.value.data == NULL) { + ngx_slab_free_locked(dict->shpool, node); + JS_FreeCString(cx, (char *) string.data); + return NGX_ERROR; + } + + ngx_memcpy(node->u.value.data, string.data, string.len); + node->u.value.len = string.len; + + JS_FreeCString(cx, (char *) string.data); + + } else { + if (JS_ToFloat64(cx, &node->u.number, value) < 0) { + ngx_slab_free_locked(dict->shpool, node); + return NGX_ERROR; + } + } + + node->sn.node.key = hash; + + ngx_memcpy(node->sn.str.data, key->data, key->len); + node->sn.str.len = key->len; + + ngx_rbtree_insert(&dict->sh->rbtree, &node->sn.node); + + if (dict->timeout) { + node->expire.key = now + timeout; + ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire); + } + + return NGX_OK; +} + + +static JSValue +ngx_qjs_dict_delete(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, + int retval) +{ + JSValue ret; + ngx_msec_t now; + ngx_time_t *tp; + ngx_js_dict_node_t *node; + + ngx_rwlock_wlock(&dict->sh->rwlock); + + node = ngx_qjs_dict_lookup(dict, key); + + if (node == NULL) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_UNDEFINED; + } + + 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) { + tp = ngx_timeofday(); + now = tp->sec * 1000 + tp->msec; + + if (!dict->timeout || now < node->expire.key) { + ret = ngx_qjs_dict_copy_value_locked(cx, dict, node); + + } else { + ret = JS_UNDEFINED; + } + + } else { + ret = JS_TRUE; + } + + ngx_js_dict_node_free(dict, node); + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return ret; +} + + +static JSValue +ngx_qjs_dict_get(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key) +{ + JSValue ret; + ngx_msec_t now; + ngx_time_t *tp; + ngx_js_dict_node_t *node; + + ngx_rwlock_rlock(&dict->sh->rwlock); + + node = ngx_qjs_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; + } + } + + ret = ngx_qjs_dict_copy_value_locked(cx, dict, node); + ngx_rwlock_unlock(&dict->sh->rwlock); + + return ret; + +not_found: + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return JS_UNDEFINED; +} + + +static JSValue +ngx_qjs_dict_incr(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, + double delta, double init, ngx_msec_t timeout) +{ + JSValue 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_qjs_dict_lookup(dict, key); + + if (node == NULL) { + value = JS_NewFloat64(cx, init + delta); + if (ngx_qjs_dict_add(cx, dict, key, value, timeout, now) != NGX_OK) { + ngx_rwlock_unlock(&dict->sh->rwlock); + JS_FreeValue(cx, value); + return ngx_qjs_throw_shared_memory_error(cx); + } + + } else { + node->u.number += delta; + value = JS_NewFloat64(cx, node->u.number); + + if (dict->timeout) { + ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire); + node->expire.key = now + timeout; + ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire); + } + } + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return value; +} + + +static JSValue +ngx_qjs_dict_set(JSContext *cx, ngx_js_dict_t *dict, ngx_str_t *key, + JSValue value, ngx_msec_t timeout, 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_qjs_dict_lookup(dict, key); + + if (node == NULL) { + if (flags & NGX_JS_DICT_FLAG_MUST_EXIST) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_FALSE; + } + + if (ngx_qjs_dict_add(cx, dict, key, value, timeout, now) != NGX_OK) { + goto memory_error; + } + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return JS_TRUE; + } + + if (flags & NGX_JS_DICT_FLAG_MUST_NOT_EXIST) { + if (!dict->timeout || now < node->expire.key) { + ngx_rwlock_unlock(&dict->sh->rwlock); + return JS_FALSE; + } + } + + if (ngx_qjs_dict_update(cx, dict, node, value, timeout, now) != NGX_OK) { + goto memory_error; + } + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return JS_TRUE; + +memory_error: + + ngx_rwlock_unlock(&dict->sh->rwlock); + + return ngx_qjs_throw_shared_memory_error(cx); +} + + +static ngx_int_t +ngx_qjs_dict_update(JSContext *cx, ngx_js_dict_t *dict, + ngx_js_dict_node_t *node, JSValue value, ngx_msec_t timeout, ngx_msec_t now) +{ + u_char *p; + ngx_str_t string; + + if (dict->type == NGX_JS_DICT_TYPE_STRING) { + string.data = (u_char *) JS_ToCStringLen(cx, &string.len, value); + if (string.data == NULL) { + return NGX_ERROR; + } + + p = ngx_js_dict_alloc(dict, string.len); + if (p == NULL) { + JS_FreeCString(cx, (char *) string.data); + return NGX_ERROR; + } + + ngx_slab_free_locked(dict->shpool, node->u.value.data); + ngx_memcpy(p, string.data, string.len); + + node->u.value.data = p; + node->u.value.len = string.len; + + JS_FreeCString(cx, (char *) string.data); + + } else { + if (JS_ToFloat64(cx, &node->u.number, value) < 0) { + return NGX_ERROR; + } + } + + if (dict->timeout) { + ngx_rbtree_delete(&dict->sh->rbtree_expire, &node->expire); + node->expire.key = now + timeout; + ngx_rbtree_insert(&dict->sh->rbtree_expire, &node->expire); + } + + return NGX_OK; +} + + +static JSValue +ngx_qjs_shared_dict_error_constructor(JSContext *cx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + JSValue proto, error_ctor, result, global_obj; + + global_obj = JS_GetGlobalObject(cx); + + error_ctor = JS_GetPropertyStr(cx, global_obj, "Error"); + if (JS_IsException(error_ctor)) { + JS_FreeValue(cx, global_obj); + return error_ctor; + } + + result = JS_CallConstructor(cx, error_ctor, argc, argv); + JS_FreeValue(cx, error_ctor); + JS_FreeValue(cx, global_obj); + + if (!JS_IsException(result)) { + proto = JS_GetClassProto(cx, NGX_QJS_CLASS_ID_SHARED_DICT_ERROR); + if (JS_SetPrototype(cx, result, proto) < 0) { + JS_FreeValue(cx, result); + JS_FreeValue(cx, proto); + return JS_EXCEPTION; + } + + JS_FreeValue(cx, proto); + } + + return result; +} + + +static JSValue +ngx_qjs_throw_shared_memory_error(JSContext *cx) +{ + JSValue ctor, global_obj, err; + + global_obj = JS_GetGlobalObject(cx); + + ctor = JS_GetPropertyStr(cx, global_obj, "SharedMemoryError"); + JS_FreeValue(cx, global_obj); + + if (JS_IsException(ctor)) { + return ctor; + } + + err = JS_CallConstructor(cx, ctor, 0, NULL); + + JS_FreeValue(cx, ctor); + + return JS_Throw(cx, err); +} + + +static JSModuleDef * +ngx_qjs_ngx_shared_dict_init(JSContext *cx, const char *name) +{ + JSValue global_obj, ngx_obj, proto, error_ctor, error_proto, shared_ctor; + + if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_SHARED, + &ngx_qjs_shared_class) < 0) + { + return NULL; + } + + if (JS_NewClass(JS_GetRuntime(cx), NGX_QJS_CLASS_ID_SHARED_DICT, + &ngx_qjs_shared_dict_class) < 0) + { + return NULL; + } + + proto = JS_NewObject(cx); + if (JS_IsException(proto)) { + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, ngx_qjs_ext_shared_dict, + njs_nitems(ngx_qjs_ext_shared_dict)); + + JS_SetClassProto(cx, NGX_QJS_CLASS_ID_SHARED_DICT, proto); + + global_obj = JS_GetGlobalObject(cx); + + error_ctor = JS_GetPropertyStr(cx, global_obj, "Error"); + if (JS_IsException(error_ctor)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + error_proto = JS_GetPropertyStr(cx, error_ctor, "prototype"); + if (JS_IsException(error_proto)) { + JS_FreeValue(cx, error_ctor); + JS_FreeValue(cx, global_obj); + return NULL; + } + + proto = JS_NewObjectProto(cx, error_proto); + + JS_FreeValue(cx, error_ctor); + JS_FreeValue(cx, error_proto); + + if (JS_IsException(proto)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetPropertyFunctionList(cx, proto, ngx_qjs_ext_shared_dict_error, + njs_nitems(ngx_qjs_ext_shared_dict_error)); + + JS_SetClassProto(cx, NGX_QJS_CLASS_ID_SHARED_DICT_ERROR, proto); + + shared_ctor = JS_NewCFunction2(cx, ngx_qjs_shared_dict_error_constructor, + "SharedDictError", 1, JS_CFUNC_constructor, + 0); + if (JS_IsException(shared_ctor)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetConstructor(cx, shared_ctor, proto); + + if (JS_SetPropertyStr(cx, global_obj, "SharedMemoryError", shared_ctor) + < 0) + { + JS_FreeValue(cx, shared_ctor); + JS_FreeValue(cx, global_obj); + return NULL; + } + + ngx_obj = JS_GetPropertyStr(cx, global_obj, "ngx"); + if (JS_IsException(ngx_obj)) { + JS_FreeValue(cx, global_obj); + return NULL; + } + + JS_SetPropertyFunctionList(cx, ngx_obj, ngx_qjs_ext_ngx, + njs_nitems(ngx_qjs_ext_ngx)); + + JS_FreeValue(cx, ngx_obj); + JS_FreeValue(cx, global_obj); + + return JS_NewCModule(cx, name, NULL); +} + +#endif diff --git a/nginx/t/js_shared_dict.t b/nginx/t/js_shared_dict.t index ae3321d71..8be2831f6 100644 --- a/nginx/t/js_shared_dict.t +++ b/nginx/t/js_shared_dict.t @@ -42,6 +42,7 @@ http { js_shared_dict_zone zone=bar:64k type=string; js_shared_dict_zone zone=waka:32k timeout=1000s type=number; js_shared_dict_zone zone=no_timeout:32k; + js_shared_dict_zone zone=overflow:32k; server { listen 127.0.0.1:8080; @@ -103,6 +104,10 @@ http { js_content test.name; } + location /overflow { + js_content test.overflow; + } + location /pop { js_content test.pop; } @@ -264,6 +269,27 @@ $t->write_file('test.js', <<'EOF'); r.return(200, dict.replace(r.args.key, value)); } + function overflow(r) { + var dict = ngx.shared.overflow; + + var v = 'x'.repeat(1024); + try { + for (var i = 0; i < 1024; i++) { + dict.set('key' + i, v); + } + } catch (e) { + if (e instanceof SharedMemoryError) { + r.return(200, 'SharedMemoryError'); + return; + } + + r.return(200, e.toString()); + return; + } + + r.return(200, 'no exception'); + } + function pop(r) { var dict = ngx.shared[r.args.dict]; var val = dict.pop(r.args.key); @@ -311,14 +337,12 @@ $t->write_file('test.js', <<'EOF'); export default { add, capacity, chain, clear, del, free_space, get, has, incr, items, keys, name, njs: test_njs, pop, replace, set, - set_clear, size, zones, engine }; + set_clear, size, zones, engine, overflow }; EOF $t->try_run('no js_shared_dict_zone'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - -$t->plan(51); +$t->plan(52); ############################################################################### @@ -415,6 +439,13 @@ like(http_get('/size?dict=foo'), qr/size: 0/, 'no of items in foo after clear'); like(http_get('/set_clear'), qr/size: 0/, 'no of items in no_timeout after clear'); +TODO: { +local $TODO = 'not yet' unless has_version('0.8.8'); + +like(http_get('/overflow'), qr/SharedMemoryError/, 'overflow exception'); + +} + ############################################################################### sub has_version { diff --git a/nginx/t/stream_js_shared_dict.t b/nginx/t/stream_js_shared_dict.t index 0bdfaeb77..915cc40b8 100644 --- a/nginx/t/stream_js_shared_dict.t +++ b/nginx/t/stream_js_shared_dict.t @@ -127,8 +127,6 @@ EOF $t->try_run('no js_shared_dict_zone'); -plan(skip_all => 'not yet') if http_get('/engine') =~ /QuickJS$/m; - $t->plan(9); $t->run_daemon(\&stream_daemon, port(8090));