From 52286f51115c4621d4cad6bb1e80ae3048ab253d Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 18 Aug 2025 12:04:57 -0700 Subject: [PATCH 1/3] refactor(examples): rename async example to tokio To reduce some confusion. Since this example was introduced, we introduced a smaller nginx-native async executor directly to ngx --- .github/workflows/nginx.yaml | 2 +- examples/Cargo.toml | 4 +-- examples/config | 4 +-- examples/t/{async.t => tokio.t} | 4 +-- examples/{async.conf => tokio.conf} | 4 +-- examples/{async.rs => tokio.rs} | 44 ++++++++++++++--------------- 6 files changed, 31 insertions(+), 31 deletions(-) rename examples/t/{async.t => tokio.t} (91%) rename examples/{async.conf => tokio.conf} (87%) rename examples/{async.rs => tokio.rs} (86%) diff --git a/.github/workflows/nginx.yaml b/.github/workflows/nginx.yaml index 02fedf1a..5f62f875 100644 --- a/.github/workflows/nginx.yaml +++ b/.github/workflows/nginx.yaml @@ -49,7 +49,7 @@ env: NGX_TEST_FILES: examples/t NGX_TEST_GLOBALS_DYNAMIC: >- - load_module ${{ github.workspace }}/nginx/objs/ngx_http_async_module.so; + load_module ${{ github.workspace }}/nginx/objs/ngx_http_tokio_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_awssigv4_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_curl_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_shared_dict_module.so; diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 9006aba7..2c796c39 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -47,8 +47,8 @@ path = "upstream.rs" crate-type = ["cdylib"] [[example]] -name = "async" -path = "async.rs" +name = "tokio" +path = "tokio.rs" crate-type = ["cdylib"] [[example]] diff --git a/examples/config b/examples/config index 6b763652..2dc7995c 100644 --- a/examples/config +++ b/examples/config @@ -16,9 +16,9 @@ if [ $HTTP = YES ]; then ngx_rust_target_features= if :; then - ngx_module_name=ngx_http_async_module + ngx_module_name=ngx_http_tokio_module ngx_module_libs="-lm" - ngx_rust_target_name=async + ngx_rust_target_name=tokio ngx_rust_module fi diff --git a/examples/t/async.t b/examples/t/tokio.t similarity index 91% rename from examples/t/async.t rename to examples/t/tokio.t index 98505fdb..a8371831 100644 --- a/examples/t/async.t +++ b/examples/t/tokio.t @@ -39,7 +39,7 @@ http { server_name localhost; location / { - async on; + tokio on; } } } @@ -51,6 +51,6 @@ $t->run(); ############################################################################### -like(http_get('/index.html'), qr/X-Async-Time:/, 'async handler'); +like(http_get('/index.html'), qr/X-Tokio-Time:/, 'tokio handler'); ############################################################################### diff --git a/examples/async.conf b/examples/tokio.conf similarity index 87% rename from examples/async.conf rename to examples/tokio.conf index d96876e0..aea1e4a3 100644 --- a/examples/async.conf +++ b/examples/tokio.conf @@ -2,7 +2,7 @@ daemon off; master_process off; # worker_processes 1; -load_module modules/libasync.so; +load_module modules/libtokio.so; error_log error.log debug; events { } @@ -14,7 +14,7 @@ http { location / { root html; index index.html index.htm; - async on; + tokio on; } error_page 500 502 503 504 /50x.html; location = /50x.html { diff --git a/examples/async.rs b/examples/tokio.rs similarity index 86% rename from examples/async.rs rename to examples/tokio.rs index 47f5de8e..85d9f34a 100644 --- a/examples/async.rs +++ b/examples/tokio.rs @@ -20,7 +20,7 @@ struct Module; impl http::HttpModule for Module { fn module() -> &'static ngx_module_t { - unsafe { &*::core::ptr::addr_of!(ngx_http_async_module) } + unsafe { &*::core::ptr::addr_of!(ngx_http_tokio_module) } } unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t { @@ -35,7 +35,7 @@ impl http::HttpModule for Module { return core::Status::NGX_ERROR.into(); } // set an Access phase handler - *h = Some(async_access_handler); + *h = Some(tokio_access_handler); core::Status::NGX_OK.into() } } @@ -49,11 +49,11 @@ unsafe impl HttpModuleLocationConf for Module { type LocationConf = ModuleConfig; } -static mut NGX_HTTP_ASYNC_COMMANDS: [ngx_command_t; 2] = [ +static mut NGX_HTTP_TOKIO_COMMANDS: [ngx_command_t; 2] = [ ngx_command_t { - name: ngx_string!("async"), + name: ngx_string!("tokio"), type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, - set: Some(ngx_http_async_commands_set_enable), + set: Some(ngx_http_tokio_commands_set_enable), conf: NGX_HTTP_LOC_CONF_OFFSET, offset: 0, post: std::ptr::null_mut(), @@ -61,7 +61,7 @@ static mut NGX_HTTP_ASYNC_COMMANDS: [ngx_command_t; 2] = [ ngx_command_t::empty(), ]; -static NGX_HTTP_ASYNC_MODULE_CTX: ngx_http_module_t = ngx_http_module_t { +static NGX_HTTP_TOKIO_MODULE_CTX: ngx_http_module_t = ngx_http_module_t { preconfiguration: Some(Module::preconfiguration), postconfiguration: Some(Module::postconfiguration), create_main_conf: None, @@ -75,14 +75,14 @@ static NGX_HTTP_ASYNC_MODULE_CTX: ngx_http_module_t = ngx_http_module_t { // Generate the `ngx_modules` table with exported modules. // This feature is required to build a 'cdylib' dynamic module outside of the NGINX buildsystem. #[cfg(feature = "export-modules")] -ngx::ngx_modules!(ngx_http_async_module); +ngx::ngx_modules!(ngx_http_tokio_module); #[used] #[allow(non_upper_case_globals)] #[cfg_attr(not(feature = "export-modules"), no_mangle)] -pub static mut ngx_http_async_module: ngx_module_t = ngx_module_t { - ctx: std::ptr::addr_of!(NGX_HTTP_ASYNC_MODULE_CTX) as _, - commands: unsafe { &NGX_HTTP_ASYNC_COMMANDS[0] as *const _ as *mut _ }, +pub static mut ngx_http_tokio_module: ngx_module_t = ngx_module_t { + ctx: std::ptr::addr_of!(NGX_HTTP_TOKIO_MODULE_CTX) as _, + commands: unsafe { &NGX_HTTP_TOKIO_COMMANDS[0] as *const _ as *mut _ }, type_: NGX_HTTP_MODULE as _, ..ngx_module_t::default() }; @@ -96,12 +96,12 @@ impl http::Merge for ModuleConfig { } } -unsafe extern "C" fn check_async_work_done(event: *mut ngx_event_t) { +unsafe extern "C" fn check_tokio_work_done(event: *mut ngx_event_t) { let ctx = ngx::ngx_container_of!(event, RequestCTX, event); let c: *mut ngx_connection_t = (*event).data.cast(); if (*ctx).done.load(Ordering::Relaxed) { - // Triggering async_access_handler again + // Triggering tokio_access_handler again ngx_post_event((*c).write, addr_of_mut!(ngx_posted_events)); } else { // this doesn't have have good performance but works as a simple thread-safe example and @@ -139,17 +139,17 @@ impl Drop for RequestCTX { } } -http_request_handler!(async_access_handler, |request: &mut http::Request| { +http_request_handler!(tokio_access_handler, |request: &mut http::Request| { let co = Module::location_conf(request).expect("module config is none"); - ngx_log_debug_http!(request, "async module enabled: {}", co.enable); + ngx_log_debug_http!(request, "tokio module enabled: {}", co.enable); if !co.enable { return core::Status::NGX_DECLINED; } if let Some(ctx) = - unsafe { request.get_module_ctx::(&*addr_of!(ngx_http_async_module)) } + unsafe { request.get_module_ctx::(&*addr_of!(ngx_http_tokio_module)) } { if !ctx.done.load(Ordering::Relaxed) { return core::Status::NGX_AGAIN; @@ -162,10 +162,10 @@ http_request_handler!(async_access_handler, |request: &mut http::Request| { if ctx.is_null() { return core::Status::NGX_ERROR; } - request.set_module_ctx(ctx.cast(), unsafe { &*addr_of!(ngx_http_async_module) }); + request.set_module_ctx(ctx.cast(), unsafe { &*addr_of!(ngx_http_tokio_module) }); let ctx = unsafe { &mut *ctx }; - ctx.event.handler = Some(check_async_work_done); + ctx.event.handler = Some(check_tokio_work_done); ctx.event.data = request.connection().cast(); ctx.event.log = unsafe { (*request.connection()).log }; unsafe { ngx_post_event(&mut ctx.event, addr_of_mut!(ngx_posted_next_events)) }; @@ -174,7 +174,7 @@ http_request_handler!(async_access_handler, |request: &mut http::Request| { let req = AtomicPtr::new(request.into()); let done_flag = ctx.done.clone(); - let rt = ngx_http_async_runtime(); + let rt = ngx_http_tokio_runtime(); ctx.task = Some(rt.spawn(async move { let start = Instant::now(); tokio::time::sleep(std::time::Duration::from_secs(2)).await; @@ -183,7 +183,7 @@ http_request_handler!(async_access_handler, |request: &mut http::Request| { // but this is just an example. proper way would be storing these headers in the request ctx // and apply them when we get back to the nginx thread. req.add_header_out( - "X-Async-Time", + "X-Tokio-Time", start.elapsed().as_millis().to_string().as_str(), ); @@ -197,7 +197,7 @@ http_request_handler!(async_access_handler, |request: &mut http::Request| { core::Status::NGX_AGAIN }); -extern "C" fn ngx_http_async_commands_set_enable( +extern "C" fn ngx_http_tokio_commands_set_enable( cf: *mut ngx_conf_t, _cmd: *mut ngx_command_t, conf: *mut c_void, @@ -208,7 +208,7 @@ extern "C" fn ngx_http_async_commands_set_enable( let val = match args[1].to_str() { Ok(s) => s, Err(_) => { - ngx_conf_log_error!(NGX_LOG_EMERG, cf, "`async` argument is not utf-8 encoded"); + ngx_conf_log_error!(NGX_LOG_EMERG, cf, "`tokio` argument is not utf-8 encoded"); return ngx::core::NGX_CONF_ERROR; } }; @@ -226,7 +226,7 @@ extern "C" fn ngx_http_async_commands_set_enable( ngx::core::NGX_CONF_OK } -fn ngx_http_async_runtime() -> &'static Runtime { +fn ngx_http_tokio_runtime() -> &'static Runtime { // Should not be called from the master process assert_ne!( unsafe { ngx::ffi::ngx_process }, From 4043c1fb73464ef2fee57d4fdb18215c59449e5a Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 18 Aug 2025 16:38:18 -0700 Subject: [PATCH 2/3] fix(CI): windows CI can't cache nginx/objs because the windows nmake program doesn't support .PHONY, and therefore it may reuse stale items from the cache instead of building them --- .github/workflows/nginx.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nginx.yaml b/.github/workflows/nginx.yaml index 5f62f875..dff9b49a 100644 --- a/.github/workflows/nginx.yaml +++ b/.github/workflows/nginx.yaml @@ -185,9 +185,8 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ - nginx/objs/**/CACHEDIR.TAG - nginx/objs/**/ngx-debug - nginx/objs/**/ngx-release + # Windows nmake implementation doesn't support .PHONY. Don't cache + # anything make creates because it might not rebuild correctly key: ${{ runner.os }}-nginx-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-nginx- From 0eb092f77101593902521c78668b6bf2b90354a6 Mon Sep 17 00:00:00 2001 From: Pat Hickey Date: Mon, 18 Aug 2025 16:41:23 -0700 Subject: [PATCH 3/3] feat(examples): add example using ngx::async_ --- .github/workflows/nginx.yaml | 1 + examples/Cargo.toml | 8 ++ examples/async.conf | 24 ++++ examples/async.rs | 248 +++++++++++++++++++++++++++++++++++ examples/config | 9 ++ examples/t/async.t | 56 ++++++++ 6 files changed, 346 insertions(+) create mode 100644 examples/async.conf create mode 100644 examples/async.rs create mode 100644 examples/t/async.t diff --git a/.github/workflows/nginx.yaml b/.github/workflows/nginx.yaml index dff9b49a..1f10490f 100644 --- a/.github/workflows/nginx.yaml +++ b/.github/workflows/nginx.yaml @@ -49,6 +49,7 @@ env: NGX_TEST_FILES: examples/t NGX_TEST_GLOBALS_DYNAMIC: >- + load_module ${{ github.workspace }}/nginx/objs/ngx_http_async_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_tokio_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_awssigv4_module.so; load_module ${{ github.workspace }}/nginx/objs/ngx_http_curl_module.so; diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 2c796c39..c87586c2 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -25,6 +25,13 @@ idna_adapter = "=1.1.0" libc = "0.2.140" tokio = { version = "1.33.0", features = ["full"] } + +[[example]] +name = "async" +path = "async.rs" +crate-type = ["cdylib"] +required-features = [ "async" ] + [[example]] name = "curl" path = "curl.rs" @@ -65,3 +72,4 @@ default = ["export-modules", "ngx/vendored"] # See https://github.com/rust-lang/rust/issues/20267 export-modules = [] linux = [] +async = [ "ngx/async" ] diff --git a/examples/async.conf b/examples/async.conf new file mode 100644 index 00000000..d96876e0 --- /dev/null +++ b/examples/async.conf @@ -0,0 +1,24 @@ +daemon off; +master_process off; +# worker_processes 1; + +load_module modules/libasync.so; +error_log error.log debug; + +events { } + +http { + server { + listen *:8000; + server_name localhost; + location / { + root html; + index index.html index.htm; + async on; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + } +} diff --git a/examples/async.rs b/examples/async.rs new file mode 100644 index 00000000..72d93ef3 --- /dev/null +++ b/examples/async.rs @@ -0,0 +1,248 @@ +use std::ffi::{c_char, c_void}; +use std::time::Instant; + +use ngx::async_::{sleep, spawn, Task}; +use ngx::core; +use ngx::ffi::{ + ngx_array_push, ngx_buf_t, ngx_chain_t, ngx_command_t, ngx_conf_t, ngx_http_finalize_request, + ngx_http_handler_pt, ngx_http_module_t, ngx_http_phases_NGX_HTTP_ACCESS_PHASE, + ngx_http_read_client_request_body, ngx_http_request_t, ngx_int_t, ngx_module_t, ngx_str_t, + ngx_uint_t, NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, NGX_HTTP_LOC_CONF_OFFSET, NGX_HTTP_MODULE, + NGX_HTTP_SPECIAL_RESPONSE, NGX_LOG_EMERG, +}; +use ngx::http::{self, HttpModule, MergeConfigError}; +use ngx::http::{HttpModuleLocationConf, HttpModuleMainConf, NgxHttpCoreModule}; +use ngx::{http_request_handler, ngx_conf_log_error, ngx_log_debug_http, ngx_string}; + +struct Module; + +impl http::HttpModule for Module { + fn module() -> &'static ngx_module_t { + unsafe { &*std::ptr::addr_of!(ngx_http_async_module) } + } + + unsafe extern "C" fn postconfiguration(cf: *mut ngx_conf_t) -> ngx_int_t { + // SAFETY: this function is called with non-NULL cf always + let cf = &mut *cf; + let cmcf = NgxHttpCoreModule::main_conf_mut(cf).expect("http core main conf"); + + let h = ngx_array_push( + &mut cmcf.phases[ngx_http_phases_NGX_HTTP_ACCESS_PHASE as usize].handlers, + ) as *mut ngx_http_handler_pt; + if h.is_null() { + return core::Status::NGX_ERROR.into(); + } + // set an Access phase handler + *h = Some(async_access_handler); + core::Status::NGX_OK.into() + } +} + +#[derive(Debug, Default)] +struct ModuleConfig { + enable: bool, +} + +unsafe impl HttpModuleLocationConf for Module { + type LocationConf = ModuleConfig; +} + +static mut NGX_HTTP_ASYNC_COMMANDS: [ngx_command_t; 2] = [ + ngx_command_t { + name: ngx_string!("async"), + type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t, + set: Some(ngx_http_async_commands_set_enable), + conf: NGX_HTTP_LOC_CONF_OFFSET, + offset: 0, + post: std::ptr::null_mut(), + }, + ngx_command_t::empty(), +]; + +static NGX_HTTP_ASYNC_MODULE_CTX: ngx_http_module_t = ngx_http_module_t { + preconfiguration: Some(Module::preconfiguration), + postconfiguration: Some(Module::postconfiguration), + create_main_conf: None, + init_main_conf: None, + create_srv_conf: None, + merge_srv_conf: None, + create_loc_conf: Some(Module::create_loc_conf), + merge_loc_conf: Some(Module::merge_loc_conf), +}; + +// Generate the `ngx_modules` table with exported modules. +// This feature is required to build a 'cdylib' dynamic module outside of the NGINX buildsystem. +#[cfg(feature = "export-modules")] +ngx::ngx_modules!(ngx_http_async_module); + +#[used] +#[allow(non_upper_case_globals)] +#[cfg_attr(not(feature = "export-modules"), no_mangle)] +pub static mut ngx_http_async_module: ngx_module_t = ngx_module_t { + ctx: std::ptr::addr_of!(NGX_HTTP_ASYNC_MODULE_CTX) as _, + commands: unsafe { &NGX_HTTP_ASYNC_COMMANDS[0] as *const _ as *mut _ }, + type_: NGX_HTTP_MODULE as _, + ..ngx_module_t::default() +}; + +impl http::Merge for ModuleConfig { + fn merge(&mut self, prev: &ModuleConfig) -> Result<(), MergeConfigError> { + if prev.enable { + self.enable = true; + }; + Ok(()) + } +} + +extern "C" fn ngx_http_async_commands_set_enable( + cf: *mut ngx_conf_t, + _cmd: *mut ngx_command_t, + conf: *mut c_void, +) -> *mut c_char { + unsafe { + let conf = &mut *(conf as *mut ModuleConfig); + let args: &[ngx_str_t] = (*(*cf).args).as_slice(); + let val = match args[1].to_str() { + Ok(s) => s, + Err(_) => { + ngx_conf_log_error!(NGX_LOG_EMERG, cf, "`async` argument is not utf-8 encoded"); + return ngx::core::NGX_CONF_ERROR; + } + }; + + // set default value optionally + conf.enable = false; + + if val.eq_ignore_ascii_case("on") { + conf.enable = true; + } else if val.eq_ignore_ascii_case("off") { + conf.enable = false; + } + }; + + ngx::core::NGX_CONF_OK +} + +http_request_handler!(async_access_handler, |request: &mut http::Request| { + let co = Module::location_conf(request).expect("module config is none"); + + ngx_log_debug_http!(request, "async module enabled: {}", co.enable); + + if !co.enable { + return core::Status::NGX_DECLINED; + } + + if request + .get_module_ctx::>(unsafe { &*std::ptr::addr_of!(ngx_http_async_module) }) + .is_some() + { + return core::Status::NGX_DONE; + } + + let rc = + unsafe { ngx_http_read_client_request_body(request.into(), Some(content_event_handler)) }; + if rc as u32 >= NGX_HTTP_SPECIAL_RESPONSE { + return core::Status(rc); + } + + core::Status::NGX_DONE +}); + +extern "C" fn content_event_handler(request: *mut ngx_http_request_t) { + let task = spawn(async move { + let start = Instant::now(); + sleep(std::time::Duration::from_secs(2)).await; + + let req = unsafe { http::Request::from_ngx_http_request(request) }; + req.add_header_out( + "X-Async-Time", + start.elapsed().as_millis().to_string().as_str(), + ); + req.set_status(http::HTTPStatus::OK); + req.send_header(); + let buf = req.pool().calloc(std::mem::size_of::()) as *mut ngx_buf_t; + unsafe { + (*buf).set_last_buf(if req.is_main() { 1 } else { 0 }); + (*buf).set_last_in_chain(1); + } + req.output_filter(&mut ngx_chain_t { + buf, + next: std::ptr::null_mut(), + }); + + unsafe { + ngx::ffi::ngx_post_event( + (*(*request).connection).write, + std::ptr::addr_of_mut!(ngx::ffi::ngx_posted_events), + ); + } + }); + + let req = unsafe { http::Request::from_ngx_http_request(request) }; + + let ctx = req.pool().allocate::>(task); + if ctx.is_null() { + unsafe { ngx_http_finalize_request(request, core::Status::NGX_ERROR.into()) }; + return; + } + req.set_module_ctx(ctx.cast(), unsafe { + &*std::ptr::addr_of!(ngx_http_async_module) + }); + unsafe { (*request).write_event_handler = Some(write_event_handler) }; +} + +extern "C" fn write_event_handler(request: *mut ngx_http_request_t) { + let req = unsafe { http::Request::from_ngx_http_request(request) }; + if let Some(task) = + req.get_module_ctx::>(unsafe { &*std::ptr::addr_of!(ngx_http_async_module) }) + { + if task.is_finished() { + unsafe { ngx_http_finalize_request(request, core::Status::NGX_OK.into()) }; + return; + } + } + + let write_event = + unsafe { (*(*request).connection).write.as_ref() }.expect("write event is not null"); + if write_event.timedout() != 0 { + unsafe { + ngx::ffi::ngx_connection_error( + (*request).connection, + ngx::ffi::NGX_ETIMEDOUT as i32, + c"client timed out".as_ptr() as *mut _, + ) + }; + return; + } + + if unsafe { ngx::ffi::ngx_http_output_filter(request, std::ptr::null_mut()) } + == ngx::ffi::NGX_ERROR as isize + { + // Client error + return; + } + let clcf = + NgxHttpCoreModule::location_conf(unsafe { request.as_ref().expect("request not null") }) + .expect("http core server conf"); + + if unsafe { + ngx::ffi::ngx_handle_write_event(std::ptr::from_ref(write_event) as *mut _, clcf.send_lowat) + } != ngx::ffi::NGX_OK as isize + { + // Client error + return; + } + + if write_event.delayed() == 0 { + if (write_event.active() != 0) && (write_event.ready() == 0) { + unsafe { + ngx::ffi::ngx_add_timer( + std::ptr::from_ref(write_event) as *mut _, + clcf.send_timeout, + ) + } + } else if write_event.timer_set() != 0 { + unsafe { ngx::ffi::ngx_del_timer(std::ptr::from_ref(write_event) as *mut _) } + } + } +} diff --git a/examples/config b/examples/config index 2dc7995c..45aa9a24 100644 --- a/examples/config +++ b/examples/config @@ -15,6 +15,15 @@ if [ $HTTP = YES ]; then ngx_rust_target_type=EXAMPLE ngx_rust_target_features= + if :; then + ngx_module_name=ngx_http_async_module + ngx_module_libs="-lm" + ngx_rust_target_name=async + ngx_rust_target_features=async + + ngx_rust_module + fi + if :; then ngx_module_name=ngx_http_tokio_module ngx_module_libs="-lm" diff --git a/examples/t/async.t b/examples/t/async.t new file mode 100644 index 00000000..98505fdb --- /dev/null +++ b/examples/t/async.t @@ -0,0 +1,56 @@ +#!/usr/bin/perl + +# (C) Nginx, Inc + +# Tests for ngx-rust example modules. + +############################################################################### + +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/)->plan(1) + ->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 / { + async on; + } + } +} + +EOF + +$t->write_file('index.html', ''); +$t->run(); + +############################################################################### + +like(http_get('/index.html'), qr/X-Async-Time:/, 'async handler'); + +###############################################################################