From: Vadim Zhestikov Date: Fri, 25 Jul 2025 00:10:36 +0000 (-0700) Subject: Modules: improved handling of results of async handlers. X-Git-Tag: 0.9.5~32 X-Git-Url: http://www.kaiwu.me/postgresql/commit/static/gitweb.js?a=commitdiff_plain;h=9a5430e6cc409ebc0323ffd937fcb7096fe7fabf;p=njs.git Modules: improved handling of results of async handlers. Previously, r.setReturnValue() had to be used when returning a value from async js_set handler. async function hash(r) { let hash = await crypto.subtle.digest('SHA-512', r.headersIn.host); r.setReturnValue(Buffer.from(hash).toString('hex')); } Now r.setReturnValue() is not needed: async function hash(r) { let hash = await crypto.subtle.digest('SHA-512', r.headersIn.host); return Buffer.from(hash).toString('hex'); } Also added promise handling in global qjs code. --- diff --git a/nginx/ngx_js.c b/nginx/ngx_js.c index 71df075d..b4e78159 100644 --- a/nginx/ngx_js.c +++ b/nginx/ngx_js.c @@ -54,7 +54,14 @@ static void ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *conf); static ngx_int_t ngx_js_init_preload_vm(njs_vm_t *vm, ngx_js_loc_conf_t *conf); +static ngx_int_t ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_log_t *log); +static njs_int_t ngx_njs_await(njs_vm_t *vm, ngx_log_t *log, + njs_value_t *value); + #if (NJS_HAVE_QUICKJS) +static ngx_int_t ngx_qjs_execute_pending_jobs(JSContext *cx, ngx_log_t *log); +static ngx_int_t ngx_qjs_await(JSContext *cx, ngx_log_t *log, + JSValueConst *value); static ngx_int_t ngx_engine_qjs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts); static ngx_int_t ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, @@ -657,10 +664,61 @@ ngx_engine_njs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, } +static ngx_int_t +ngx_njs_execute_pending_jobs(njs_vm_t *vm, ngx_log_t *log) +{ + njs_int_t ret; + njs_str_t exception; + + for ( ;; ) { + ret = njs_vm_execute_pending_job(vm); + if (ret <= NJS_OK) { + if (ret == NJS_ERROR) { + njs_vm_exception_string(vm, &exception); + ngx_log_error(NGX_LOG_ERR, log, 0, "js job exception: %V", + &exception); + return NGX_ERROR; + } + + break; + } + } + + return NGX_OK; +} + + +static njs_int_t +ngx_njs_await(njs_vm_t *vm, ngx_log_t *log, njs_value_t *value) +{ + ngx_int_t ret; + njs_promise_type_t state; + + ret = ngx_njs_execute_pending_jobs(vm, log); + if (ret != NGX_OK) { + return NGX_ERROR; + } + + if (njs_value_is_promise(value)) { + state = njs_promise_state(value); + + if (state == NJS_PROMISE_FULFILL) { + njs_value_assign(value, njs_promise_result(value)); + + } else if (state == NJS_PROMISE_REJECTED) { + njs_vm_throw(vm, njs_promise_result(value)); + } + } + + return NGX_OK; +} + + ngx_engine_t * ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) { njs_vm_t *vm; + njs_int_t ret; ngx_engine_t *engine; njs_opaque_value_t retval; @@ -671,8 +729,7 @@ ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) engine = njs_mp_alloc(njs_vm_memory_pool(vm), sizeof(ngx_engine_t)); if (engine == NULL) { - njs_vm_destroy(vm); - return NULL; + goto destroy; } memcpy(engine, cf->engine, sizeof(ngx_engine_t)); @@ -681,13 +738,21 @@ ngx_njs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) if (njs_vm_start(vm, njs_value_arg(&retval)) == NJS_ERROR) { ngx_js_log_exception(vm, ctx->log, "exception"); + goto destroy; + } - njs_vm_destroy(vm); - - return NULL; + ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&retval)); + if (ret == NGX_ERROR) { + goto destroy; } return engine; + +destroy: + + njs_vm_destroy(vm); + + return NULL; } @@ -720,17 +785,9 @@ ngx_engine_njs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, return NGX_ERROR; } - for ( ;; ) { - ret = njs_vm_execute_pending_job(vm); - if (ret <= NJS_OK) { - if (ret == NJS_ERROR) { - ngx_js_log_exception(vm, ctx->log, "exception"); - - return NGX_ERROR; - } - - break; - } + ret = ngx_njs_await(vm, ctx->log, njs_value_arg(&ctx->retval)); + if (ret == NGX_ERROR) { + return NGX_ERROR; } return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN; @@ -807,6 +864,66 @@ ngx_engine_njs_destroy(ngx_engine_t *e, ngx_js_ctx_t *ctx, #if (NJS_HAVE_QUICKJS) +static ngx_int_t +ngx_qjs_execute_pending_jobs(JSContext *cx, ngx_log_t *log) +{ + int rc; + JSValue value; + JSContext *cx1; + const char *exception; + + for ( ;; ) { + rc = JS_ExecutePendingJob(JS_GetRuntime(cx), &cx1); + if (rc <= 0) { + if (rc == 0) { + break; + } + + value = JS_GetException(cx); + exception = JS_ToCString(cx, value); + JS_FreeValue(cx, value); + + ngx_log_error(NGX_LOG_ERR, log, 0, "js job exception: %s", + exception); + + JS_FreeCString(cx, exception); + + return NGX_ERROR; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_qjs_await(JSContext *cx, ngx_log_t *log, JSValue *value) +{ + JSValue ret; + ngx_int_t rc; + JSPromiseStateEnum state; + + rc = ngx_qjs_execute_pending_jobs(cx, log); + if (rc != NGX_OK) { + return NGX_ERROR; + } + + state = JS_PromiseState(cx, *value); + if (state == JS_PROMISE_FULFILLED) { + ret = JS_PromiseResult(cx, *value); + JS_FreeValue(cx, *value); + *value = ret; + + } else if (state == JS_PROMISE_REJECTED) { + ret = JS_Throw(cx, JS_PromiseResult(cx, *value)); + JS_FreeValue(cx, *value); + *value = ret; + } + + return NGX_OK; +} + + static ngx_int_t ngx_engine_qjs_init(ngx_engine_t *engine, ngx_engine_opts_t *opts) { @@ -871,48 +988,13 @@ ngx_engine_qjs_compile(ngx_js_loc_conf_t *conf, ngx_log_t *log, u_char *start, } -static JSValue -js_std_await(JSContext *ctx, JSValue obj) -{ - int state, err; - JSValue ret; - JSContext *ctx1; - - for (;;) { - state = JS_PromiseState(ctx, obj); - if (state == JS_PROMISE_FULFILLED) { - ret = JS_PromiseResult(ctx, obj); - JS_FreeValue(ctx, obj); - break; - - } else if (state == JS_PROMISE_REJECTED) { - ret = JS_Throw(ctx, JS_PromiseResult(ctx, obj)); - JS_FreeValue(ctx, obj); - break; - - } else if (state == JS_PROMISE_PENDING) { - err = JS_ExecutePendingJob(JS_GetRuntime(ctx), &ctx1); - if (err < 0) { - /* js_std_dump_error(ctx1); */ - } - - } else { - /* not a promise */ - ret = obj; - break; - } - } - - return ret; -} - - ngx_engine_t * ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) { JSValue rv; njs_mp_t *mp; uint32_t i, length; + ngx_int_t rc; JSRuntime *rt; JSContext *cx; ngx_engine_t *engine; @@ -990,14 +1072,13 @@ ngx_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf, void *external) goto destroy; } - rv = js_std_await(cx, rv); - if (JS_IsException(rv)) { - ngx_qjs_log_exception(engine, ctx->log, "eval exception"); + rc = ngx_qjs_await(cx, ctx->log, &rv); + JS_FreeValue(cx, rv); + if (rc == NGX_ERROR) { + ngx_qjs_log_exception(engine, ctx->log, "await exception"); goto destroy; } - JS_FreeValue(cx, rv); - return engine; destroy: @@ -1014,10 +1095,9 @@ static ngx_int_t ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, njs_opaque_value_t *args, njs_uint_t nargs) { - int rc; JSValue fn, val; - JSRuntime *rt; - JSContext *cx, *cx1; + ngx_int_t rc; + JSContext *cx; cx = ctx->engine->u.qjs.ctx; @@ -1041,19 +1121,10 @@ ngx_engine_qjs_call(ngx_js_ctx_t *ctx, ngx_str_t *fname, JS_FreeValue(cx, ngx_qjs_arg(ctx->retval)); ngx_qjs_arg(ctx->retval) = val; - rt = JS_GetRuntime(cx); - - for ( ;; ) { - rc = JS_ExecutePendingJob(rt, &cx1); - if (rc <= 0) { - if (rc == -1) { - ngx_qjs_log_exception(ctx->engine, ctx->log, "job exception"); - - return NGX_ERROR; - } - - break; - } + rc = ngx_qjs_await(cx, ctx->log, &ngx_qjs_arg(ctx->retval)); + if (rc == NGX_ERROR) { + ngx_qjs_log_exception(ctx->engine, ctx->log, "await exception"); + return NGX_ERROR; } return njs_rbtree_is_empty(&ctx->waiting_events) ? NGX_OK : NGX_AGAIN; @@ -1365,10 +1436,8 @@ ngx_qjs_dump_obj(ngx_engine_t *e, JSValueConst val, ngx_str_t *dst) ngx_int_t ngx_qjs_call(JSContext *cx, JSValue fn, JSValue *argv, int argc) { - int rc; JSValue ret; - JSRuntime *rt; - JSContext *cx1; + ngx_int_t rc; ngx_js_ctx_t *ctx; ctx = ngx_qjs_external_ctx(cx, JS_GetContextOpaque(cx)); @@ -1382,19 +1451,9 @@ ngx_qjs_call(JSContext *cx, JSValue fn, JSValue *argv, int argc) JS_FreeValue(cx, ret); - rt = JS_GetRuntime(cx); - - for ( ;; ) { - rc = JS_ExecutePendingJob(rt, &cx1); - if (rc <= 0) { - if (rc == -1) { - ngx_qjs_log_exception(ctx->engine, ctx->log, "job exception"); - - return NGX_ERROR; - } - - break; - } + rc = ngx_qjs_execute_pending_jobs(cx, ctx->log); + if (rc != NGX_OK) { + return NGX_ERROR; } return NGX_OK; @@ -2202,6 +2261,7 @@ ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args, njs_uint_t nargs) { njs_int_t ret; + ngx_js_ctx_t *ctx; ngx_connection_t *c; ret = njs_vm_call(vm, func, njs_value_arg(args), nargs); @@ -2214,19 +2274,11 @@ ngx_js_call(njs_vm_t *vm, njs_function_t *func, njs_opaque_value_t *args, return NGX_ERROR; } - for ( ;; ) { - ret = njs_vm_execute_pending_job(vm); - if (ret <= NJS_OK) { - c = ngx_external_connection(vm, njs_vm_external_ptr(vm)); - - if (ret == NJS_ERROR) { - ngx_js_log_exception(vm, c->log, "job exception"); - - return NGX_ERROR; - } + ctx = ngx_external_ctx(vm, njs_vm_external_ptr(vm)); - break; - } + ret = ngx_njs_execute_pending_jobs(vm, ctx->log); + if (ret != NGX_OK) { + return NGX_ERROR; } return NGX_OK; diff --git a/nginx/t/js_async.t b/nginx/t/js_async.t index 32d1e0a4..3cbe283a 100644 --- a/nginx/t/js_async.t +++ b/nginx/t/js_async.t @@ -35,9 +35,10 @@ events { http { %%TEST_GLOBALS_HTTP%% - js_set $test_async test.set_timeout; - js_set $context_var test.context_var; - js_set $test_set_rv_var test.set_rv_var; + js_set $test_async test.set_timeout; + js_set $context_var test.context_var; + js_set $test_set_rv_var test.set_rv_var; + js_set $test_promise_var test.set_promise_var; js_import test.js; @@ -85,6 +86,10 @@ http { return 200 $test_set_rv_var; } + location /promise_var { + return 200 $test_promise_var; + } + location /await_reject { js_content test.await_reject; } @@ -198,6 +203,13 @@ $t->write_file('test.js', < { setTimeout(() => { @@ -213,11 +225,11 @@ $t->write_file('test.js', <try_run('no njs available')->plan(10); +$t->try_run('no njs available')->plan(11); ############################################################################### @@ -229,6 +241,7 @@ like(http_get('/limit_rate'), qr/A{50}/, 'limit_rate'); like(http_get('/async_content'), qr/retval: AB/, 'async content'); like(http_get('/set_rv_var'), qr/retval: 30/, 'set return value variable'); +like(http_get('/promise_var'), qr/retval: 30/, 'fulfilled promise variable'); http_get('/async_var'); http_get('/await_reject'); diff --git a/nginx/t/js_promise_top_level_await.t b/nginx/t/js_promise_top_level_await.t new file mode 100644 index 00000000..29dc6322 --- /dev/null +++ b/nginx/t/js_promise_top_level_await.t @@ -0,0 +1,92 @@ +#!/usr/bin/perl + +# (C) F5, Inc. +# Vadim Zhestikov + +# Tests for top-level await of promises in QuickJS engine. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +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 rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_import test.js; + js_import fulfilled_test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /resolved { + js_content test.test; + } + + location /fulfilled { + js_content fulfilled_test.test; + } + } +} + +EOF + +$t->write_file('test.js', <<'EOF'); + var globalResult = await Promise.resolve("resolved value"); + + function test(r) { + r.return(200, "global result: " + globalResult); + } + + export default {test}; + +EOF + +$t->write_file('fulfilled_test.js', <<'EOF'); + var globalResult = await new Promise((resolve) => { + Promise.resolve().then(() => { + resolve("fulfilled value"); + }); + }); + + function test(r) { + r.return(200, "fulfilled result: " + globalResult); + } + + export default {test}; + +EOF + +$t->try_run('no top-level await support')->plan(2); + +############################################################################### + +like(http_get('/resolved'), qr/global result: resolved value/, + 'basic global await works'); +like(http_get('/fulfilled'), qr/fulfilled result: fulfilled value/, + 'fulfilled promise via microtask works'); + +############################################################################### diff --git a/nginx/t/stream_js.t b/nginx/t/stream_js.t index 0834b68a..d442045a 100644 --- a/nginx/t/stream_js.t +++ b/nginx/t/stream_js.t @@ -61,14 +61,15 @@ http { stream { %%TEST_GLOBALS_STREAM%% - js_set $js_addr test.addr; - js_set $js_var test.variable; - js_set $js_log test.log; - js_set $js_unk test.unk; - js_set $js_req_line test.req_line; - js_set $js_sess_unk test.sess_unk; - js_set $js_async test.asyncf; - js_set $js_buffer test.buffer; + js_set $js_addr test.addr; + js_set $js_var test.variable; + js_set $js_log test.log; + js_set $js_unk test.unk; + js_set $js_req_line test.req_line; + js_set $js_sess_unk test.sess_unk; + js_set $js_async test.asyncf; + js_set $js_async_direct test.asyncf_direct; + js_set $js_buffer test.buffer; js_import test.js; @@ -192,6 +193,11 @@ stream { return $js_async; } + server { + listen 127.0.0.1:8102; + return $js_async_direct; + } + server { listen 127.0.0.1:8101; return $js_buffer; @@ -384,17 +390,24 @@ $t->write_file('test.js', <run_daemon(\&stream_daemon, port(8090)); -$t->try_run('no stream njs available')->plan(25); +$t->try_run('no stream njs available')->plan(26); $t->waitforsocket('127.0.0.1:' . port(8090)); ############################################################################### @@ -427,7 +440,10 @@ stream('127.0.0.1:' . port(8097))->io('x'); stream('127.0.0.1:' . port(8098))->io('x'); stream('127.0.0.1:' . port(8099))->io('x'); -is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30', 'asyncf'); +is(stream('127.0.0.1:' . port(8100))->read(), 'retval: 30', + 'async var handler setReturnValue'); +is(stream('127.0.0.1:' . port(8102))->read(), 'retval: 30', + 'async var handler direct'); TODO: { local $TODO = 'not yet' unless has_version('0.8.3');