Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(proxy-wasm) implement response body buffering #381

Merged
merged 3 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/DIRECTIVES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ By alphabetical order:
- [tls_verify_cert](#tls_verify_cert)
- [tls_verify_host](#tls_verify_host)
- [wasm_call](#wasm_call)
- [wasm_response_body_buffers](#wasm_response_body_buffers)
- [wasm_socket_buffer_reuse](#wasm_socket_buffer_reuse)
- [wasm_socket_buffer_size](#wasm_socket_buffer_size)
- [wasm_socket_connect_timeout](#wasm_socket_connect_timeout)
Expand Down Expand Up @@ -66,6 +67,7 @@ By context:
- [proxy_wasm_request_headers_in_access](#proxy_wasm_request_headers_in_access)
- [resolver_add](#resolver_add)
- [wasm_call](#wasm_call)
- [wasm_response_body_buffers](#wasm_response_body_buffers)
- [wasm_socket_buffer_reuse](#wasm_socket_buffer_reuse)
- [wasm_socket_buffer_size](#wasm_socket_buffer_size)
- [wasm_socket_connect_timeout](#wasm_socket_connect_timeout)
Expand Down Expand Up @@ -758,6 +760,20 @@ return `HTTP 500`.

[Back to TOC](#directives)

wasm_response_body_buffers
--------------------------

**usage** | `wasm_response_body_buffers <number> <size>;`
------------:|:----------------------------------------------------------------
**contexts** | `http{}`, `server{}`, `location{}`
**default** | `4 4096`
**example** | `wasm_response_body_buffers 2 16k;`

Set the maximum `number` and `size` of buffers used for [response body
buffering](PROXY_WASM.md#response-body-buffering).

[Back to TOC](#directives)

wasm_socket_buffer_reuse
------------------------

Expand Down
59 changes: 57 additions & 2 deletions docs/PROXY_WASM.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [Supported Entrypoints](#supported-entrypoints)
- [Supported Host ABI](#supported-host-abi)
- [Supported Properties](#supported-properties)
- [Response Body Buffering](#response-body-buffering)
- [Examples]
- [Current Limitations]

Expand Down Expand Up @@ -383,6 +384,8 @@ specifications and different SDK libraries:
- [Tested SDKs](#tested-sdks)
- [Supported Entrypoints](#supported-entrypoints)
- [Supported Host ABI](#supported-host-abi)
- [Supported Properties](#supported-properties)
- [Response Body Buffering](#response-body-buffering)

[Back to TOC](#table-of-contents)

Expand Down Expand Up @@ -578,7 +581,7 @@ implementation state in ngx_wasm_module:
-------------------------------------------:|:------------------:|:-------------------:|:----------------
*Request properties* | |
`request.path` | :heavy_check_mark: | :x: | Maps to [ngx.request_uri](https://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_uri).
`request.url_path` | :heavy_check_mark: | :x: | Maps to [ngx.uri](https://nginx.org/en/docs/http/ngx_http_core_module.html#uri).
`request.url_path` | :heavy_check_mark: | :x: | Maps to [ngx.uri](https://nginx.org/en/docs/http/ngx_http_core_module.html#uri).
`request.host` | :heavy_check_mark: | :x: | Maps to [ngx.hostname](https://nginx.org/en/docs/http/ngx_http_core_module.html#hostname).
`request.scheme` | :heavy_check_mark: | :x: | Maps to [ngx.scheme](https://nginx.org/en/docs/http/ngx_http_core_module.html#scheme).
`request.method` | :heavy_check_mark: | :x: | Maps to [ngx.request_method](https://nginx.org/en/docs/http/ngx_http_core_module.html#request_method).
Expand Down Expand Up @@ -651,6 +654,55 @@ ngx_wasm_module, most likely due to a Host incompatibility.

[Back to TOC](#table-of-contents)

### Response Body Buffering

Buffering of response body chunks is supported within ngx_wasm_module so filters
don't have to implement buffering themselves. This allows the `on_response_body`
step to be invoked with the full response body available for read via
`get_http_response_body`.

When response buffering is enabled, response chunks will be copied to buffers
defined by the [wasm_response_body_buffers] directive while execution of the
Proxy-Wasm filter chain is temporarily suspended until buffering is complete, at
which point `on_response_body` will be invoked again.

To enable this behavior from a filter based on Proxy-Wasm ABI v0.2.1, the filter
must return `Action::Pause` from `on_response_body`. Once enabled,
ngx_wasm_module will accumulate subsequent body chunks until either `eof` is
reached, or the buffers are full. When either of these conditions are met,
`on_response_body` will be invoked again and the body buffer will contain the
buffered chunks.

In other words, once body buffering is enabled, the next `on_response_body`
invocation will contain the buffered body **and may be invoked again**
if `eof` was not reached but the buffers are full.

A typical response buffering flow could be:

1. 1st `on_response_body` call: *ignore 1st chunk, requesting buffering.*
1. Check for `eof=false`.
2. Ensure buffering was not already requested.
3. Return `Action::Pause`, requesting buffering.
2. 2nd `on_response_body` call: *buffering ended, but how?*
1. If `eof=true`, the full response body is in the buffers.
2. If `eof=false`, the buffers are full, but more chunks are expected
(users should treat the buffers as if it were a single, non-buffered
chunk).
3. nth `on_response_body` call: *next chunks, if any.*

Returning `Action::Pause` when buffering has already taken place will be ignored
(i.e. treated as `Action::Continue`) and an error log will be printed.

> Notes

Keep in mind there are fundamental issues with buffering bodies at scale due to
the nature of the workload, hard buffer limits defined by
[wasm_response_body_buffers], and Wasm memory limits themselves (loading and
manipulating the body in filters). This feature should be used with extreme
caution in production environments.

[Back to TOC](#table-of-contents)

## Examples

- Functional filters written by the WasmX team:
Expand All @@ -672,12 +724,13 @@ factors are at play when porting the SDK to a new Host proxy runtime.

Proxy-Wasm's design was primarily influenced by Envoy concepts and features, but
because Envoy and Nginx differ in underlying implementations there remains a few
limitations on some supported features:
limitations on some supported features (non-exhaustive list):

1. Pausing a filter (i.e. `Action::Pause`) can only be done in the following
steps:
- `on_http_request_headers`
- `on_http_request_body`
- `on_http_response_body` (to enable body buffering)
- `on_http_call_response`

2. The "queue" shared memory implementation does not implement an automatic
Expand All @@ -698,6 +751,8 @@ limitations and increasing overall surface support for the Proxy-Wasm SDK.
[Examples]: #examples
[Current Limitations]: #current-limitations

[wasm_response_body_buffers]: DIRECTIVES.md#wasm_response_body_buffers

[WebAssembly]: https://webassembly.org/
[Nginx Variables]: https://nginx.org/en/docs/varindex.html
[Envoy Attributes]: https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/advanced/attributes.html?highlight=properties#request-attributes
Expand Down
10 changes: 9 additions & 1 deletion src/common/proxy_wasm/ngx_proxy_wasm.c
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,13 @@ action2rc(ngx_proxy_wasm_ctx_t *pwctx,
case NGX_PROXY_WASM_ACTION_PAUSE:
switch (pwctx->phase->index) {
#ifdef NGX_WASM_HTTP
case NGX_HTTP_WASM_BODY_FILTER_PHASE:
ngx_log_debug3(NGX_LOG_DEBUG_WASM, pwctx->log, 0,
"proxy_wasm buffering response after "
"\"ResponseBody\" step "
"(filter: %l/%l, pwctx: %p)",
pwexec->index + 1, pwctx->nfilters, pwctx);
goto yield;
case NGX_HTTP_REWRITE_PHASE:
case NGX_HTTP_ACCESS_PHASE:
case NGX_HTTP_CONTENT_PHASE:
Expand Down Expand Up @@ -640,7 +647,8 @@ ngx_proxy_wasm_resume(ngx_proxy_wasm_ctx_t *pwctx,
rc = action2rc(pwctx, pwexec);
if (rc != NGX_OK) {
if (rc == NGX_AGAIN
&& pwctx->exec_index + 1 <= pwctx->nfilters)
&& pwctx->exec_index + 1 <= pwctx->nfilters
&& step != NGX_PROXY_WASM_STEP_RESP_BODY)
{
dd("yield: resume on next filter "
"(idx: %ld -> %ld, nelts: %ld)",
Expand Down
10 changes: 7 additions & 3 deletions src/http/ngx_http_wasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ struct ngx_http_wasm_req_ctx_s {

ngx_http_handler_pt r_content_handler;
ngx_array_t resp_shim_headers;
ngx_uint_t resp_bufs_count; /* response buffers count */
ngx_chain_t *resp_bufs; /* response buffers */
ngx_chain_t *resp_buf_last; /* last response buffers */
ngx_chain_t *resp_chunk;
off_t resp_chunk_len;
unsigned resp_chunk_eof; /* seen last buf flag */

off_t resp_chunk_len;
off_t req_content_length_n;
off_t resp_content_length_n;

Expand All @@ -73,6 +75,7 @@ struct ngx_http_wasm_req_ctx_s {

unsigned in_wev:1; /* in wev_handler */
unsigned resp_content_chosen:1; /* content handler has an output to produce */
unsigned resp_buffering:1; /* enable response buffering */
unsigned resp_content_sent:1; /* has started sending output (may have yielded) */
unsigned resp_finalized:1; /* finalized connection (ourselves) */
unsigned fake_request:1;
Expand All @@ -91,8 +94,9 @@ typedef struct {
ngx_msec_t recv_timeout;

size_t socket_buffer_size; /* wasm_socket_buffer_size */
ngx_bufs_t socket_large_buffers; /* wasm_socket_large_buffer_size */
ngx_flag_t socket_buffer_reuse; /* wasm_socket_buffer_reuse */
ngx_bufs_t socket_large_buffers; /* wasm_socket_large_buffer_size */
ngx_bufs_t resp_body_buffers; /* wasm_response_body_buffers */

ngx_flag_t pwm_req_headers_in_access;
ngx_flag_t pwm_lua_resolver;
Expand Down
Loading
Loading