From d13ec52f1442e9273dc0e9c9ee1f340c46ac4bdc Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Tue, 8 Aug 2023 12:08:13 +0800 Subject: [PATCH] Adds once mechanism for Go 1.21 GOOS=js This adds an alternative to pooling called once. Specifically, this uses a module once per request, and in doing so supports guests who cannot export functions. The implementation adds `await_response` with adapter glue which the host side implements with Go channels. As you can see below, this is far less performant than using the pool model. However, this allows use of Go 1.21 GOOS=wasip1, which does not support pooling because it cannot export host functions. ``` Benchmark/example_log_once Benchmark/example_log_once/example_log_once Benchmark/example_log_once/example_log_once-12 52648 23117 ns/op Benchmark/example_log Benchmark/example_log/example_log Benchmark/example_log/example_log-12 2100068 550.4 ns/op ``` See https://github.com/golang/go/issues/42372 Signed-off-by: Adrian Cole --- api/handler/wasm.go | 3 + examples/log_once.wat | 72 ++++++ handler/middleware.go | 238 ++++++++---------- handler/middleware_test.go | 4 +- handler/nethttp/benchmark_test.go | 15 +- handler/nethttp/middleware_test.go | 46 +++- handler/once.go | 108 ++++++++ handler/pool.go | 135 ++++++++++ handler/state.go | 13 - internal/test/testdata.go | 7 + .../test/testdata/bench/add_header_value.wasm | Bin 347 -> 344 bytes .../test/testdata/bench/add_header_value.wat | 2 +- .../test/testdata/bench/get_header_names.wasm | Bin 272 -> 269 bytes .../test/testdata/bench/get_header_names.wat | 2 +- .../testdata/bench/get_header_values.wasm | Bin 338 -> 335 bytes .../test/testdata/bench/get_header_values.wat | 2 +- internal/test/testdata/bench/get_uri.wasm | Bin 219 -> 216 bytes internal/test/testdata/bench/get_uri.wat | 2 +- internal/test/testdata/bench/log.wasm | Bin 255 -> 252 bytes internal/test/testdata/bench/log.wat | 2 +- internal/test/testdata/bench/read_body.wasm | Bin 267 -> 264 bytes internal/test/testdata/bench/read_body.wat | 2 +- .../test/testdata/bench/read_body_stream.wasm | Bin 267 -> 264 bytes .../test/testdata/bench/read_body_stream.wat | 2 +- .../test/testdata/bench/remove_header.wasm | Bin 255 -> 252 bytes .../test/testdata/bench/remove_header.wat | 2 +- .../test/testdata/bench/set_header_value.wasm | Bin 356 -> 353 bytes .../test/testdata/bench/set_header_value.wat | 2 +- .../test/testdata/bench/set_status_code.wasm | Bin 206 -> 203 bytes .../test/testdata/bench/set_status_code.wat | 2 +- internal/test/testdata/bench/set_uri.wasm | Bin 239 -> 236 bytes internal/test/testdata/bench/set_uri.wat | 2 +- internal/test/testdata/bench/write_body.wasm | Bin 267 -> 264 bytes internal/test/testdata/bench/write_body.wat | 2 +- internal/test/testdata/e2e/await_response.wat | 57 +++++ .../test/testdata/e2e/handle_response.wasm | Bin 208 -> 199 bytes .../test/testdata/e2e/handle_response.wat | 16 +- internal/test/testdata/e2e/header_names.wasm | Bin 376 -> 373 bytes internal/test/testdata/e2e/header_names.wat | 2 +- internal/test/testdata/e2e/header_value.wasm | Bin 351 -> 348 bytes internal/test/testdata/e2e/header_value.wat | 2 +- internal/test/testdata/e2e/method.wasm | Bin 411 -> 408 bytes internal/test/testdata/e2e/method.wat | 2 +- .../test/testdata/e2e/protocol_version.wasm | Bin 320 -> 317 bytes .../test/testdata/e2e/protocol_version.wat | 2 +- internal/test/testdata/e2e/uri.wasm | Bin 412 -> 409 bytes internal/test/testdata/e2e/uri.wat | 2 +- .../error/panic_on_handle_request.wasm | Bin 315 -> 312 bytes .../error/panic_on_handle_request.wat | 2 +- .../error/panic_on_handle_response.wasm | Bin 319 -> 316 bytes .../error/panic_on_handle_response.wat | 2 +- .../test/testdata/error/panic_on_start.wasm | Bin 333 -> 330 bytes .../test/testdata/error/panic_on_start.wat | 2 +- .../error/set_request_header_after_next.wasm | Bin 401 -> 398 bytes .../error/set_request_header_after_next.wat | 2 +- 55 files changed, 555 insertions(+), 201 deletions(-) create mode 100644 examples/log_once.wat create mode 100644 handler/once.go create mode 100644 handler/pool.go create mode 100644 internal/test/testdata/e2e/await_response.wat diff --git a/api/handler/wasm.go b/api/handler/wasm.go index 48c2e48..ce73569 100644 --- a/api/handler/wasm.go +++ b/api/handler/wasm.go @@ -300,6 +300,9 @@ const ( // TODO: document on http-wasm-abi FuncWriteBody = "write_body" + // FuncAwaitResponse TODO + FuncAwaitResponse = "await_response" + // FuncGetStatusCode returns the status code produced by FuncNext. This // requires FeatureBufferResponse. // diff --git a/examples/log_once.wat b/examples/log_once.wat new file mode 100644 index 0000000..abdacd7 --- /dev/null +++ b/examples/log_once.wat @@ -0,0 +1,72 @@ +;; This example module is written in WebAssembly Text Format to show the +;; how a handler works when the guest compiler doesn't support function +;; exports, such as GOOS=wasip1 in Go 1.21. +(module $log_once + + ;; log_enabled returns 1 if the $level is enabled. This value may be cached + ;; at request granularity. + (import "http_handler" "log_enabled" (func $log_enabled + (param $level i32) + (result (; 0 or enabled(1) ;) i32))) + + ;; logs a message to the host's logs at the given $level. + (import "http_handler" "log" (func $log + (param $level i32) + (param $buf i32) (param $buf_limit i32))) + +(; begin adapter logic + + Below is generic and can convert any normal handler to a single synchronous + call, provided $handle_request and $handle_response are not exported. ;) + + ;; import $await_response, which blocks until the response is ready. + (import "http_handler" "await_response" (func $await_response + (param $ctx_next i64) + (result (; is_error ;) i32))) + + ;; define a start function that performs a request-response without exports. + ;; note: this logic is generic and can convert any exported $handle_request/ + ;; $handle_response pair to a synchronous call without exports. + (func $start + (local $ctx_next i64) + (local $is_error i32) + (local $ctx i32) + + ;; ctxNext := handleRequest() + (local.set $ctx_next (call $handle_request)) + + ;; isError := awaitResponse(ctxNext()) + (local.set $is_error (call $await_response (local.get $ctx_next))) + + ;; expected_count = uint32(result >> 32) + (local.set $ctx + (i32.wrap_i64 (i64.shr_u (local.get $ctx_next) (i64.const 32)))) + + (call $handle_response (local.get $ctx) (local.get $is_error)) + ) + +(; end adapter logic ;) + + (memory (export "memory") 1 1 (; 1 page==64KB ;)) + (global $message i32 (i32.const 0)) + (data (i32.const 0) "hello world") + (global $message_len i32 (i32.const 11)) + + (func $handle_request (result (; ctx_next ;) i64) + ;; We expect debug logging to be disabled. Panic otherwise! + (if (i32.eq + (call $log_enabled (i32.const -1)) ;; log_level_debug + (i32.const 1)) ;; true + (then unreachable)) + + (call $log + (i32.const 0) ;; log_level_info + (global.get $message) + (global.get $message_len)) + + ;; uint32(ctx_next) == 1 means proceed to the next handler on the host. + (return (i64.const 1))) + + ;; handle_response is no-op as this is a request-only handler. + (func $handle_response (param $reqCtx i32) (param $is_error i32)) +) diff --git a/handler/middleware.go b/handler/middleware.go index cfdcd37..0abeaa3 100644 --- a/handler/middleware.go +++ b/handler/middleware.go @@ -7,7 +7,6 @@ import ( "io" "strconv" "strings" - "sync" "sync/atomic" "github.com/tetratelabs/wazero" @@ -43,9 +42,9 @@ type Middleware interface { Features() handler.Features api.Closer -} -var _ Middleware = (*middleware)(nil) + instantiateHost(context.Context) error +} type middleware struct { host handler.Host @@ -54,7 +53,6 @@ type middleware struct { moduleConfig wazero.ModuleConfig guestConfig []byte logger api.Logger - pool sync.Pool features handler.Features instanceCounter uint64 } @@ -78,109 +76,103 @@ func NewMiddleware(ctx context.Context, guest []byte, host handler.Host, opts .. return nil, fmt.Errorf("wasm: error creating middleware: %w", err) } - m := &middleware{ - host: host, - runtime: wr, - moduleConfig: o.moduleConfig, - guestConfig: o.guestConfig, - logger: o.logger, + guestModule, err := wr.CompileModule(ctx, guest) + if err != nil { + _ = wr.Close(ctx) + return nil, fmt.Errorf("wasm: error compiling guest: %w", err) } - if m.guestModule, err = m.compileGuest(ctx, guest); err != nil { + once, err := validateGuest(guestModule) + if err != nil { _ = wr.Close(ctx) return nil, err } - // Detect and handle any host imports or lack thereof. - imports := detectImports(m.guestModule.ImportedFunctions()) - switch { - case imports&importWasiP1 != 0: - if _, err = wasi_snapshot_preview1.Instantiate(ctx, m.runtime); err != nil { + // Detect if wasip1 is used and import it. + imports := detectImports(guestModule.ImportedFunctions()) + if imports&importWasiP1 != 0 { + if _, err = wasi_snapshot_preview1.Instantiate(ctx, wr); err != nil { _ = wr.Close(ctx) - return nil, fmt.Errorf("wasm: error instantiating wasi: %w", err) - } - fallthrough // proceed to configure any http_handler imports - case imports&importHttpHandler != 0: - if _, err = m.instantiateHost(ctx); err != nil { - _ = wr.Close(ctx) - return nil, fmt.Errorf("wasm: error instantiating host: %w", err) + return nil, fmt.Errorf("wasm: error instantiating wasip1: %w", err) } } - // Eagerly add one instance to the pool. Doing so helps to fail fast. - if g, err := m.newGuest(ctx); err != nil { + m := middleware{ + host: host, + runtime: wr, + guestModule: guestModule, + moduleConfig: o.moduleConfig, + guestConfig: o.guestConfig, + logger: o.logger, + } + + var ret Middleware + if once { + ret = &onceMiddleware{middleware: m} + } else { + ret = &poolMiddleware{middleware: m} + } + + if err = ret.instantiateHost(ctx); err != nil { _ = wr.Close(ctx) return nil, err - } else { - m.pool.Put(g) } - return m, nil + return ret, nil } -func (m *middleware) compileGuest(ctx context.Context, wasm []byte) (wazero.CompiledModule, error) { - if guest, err := m.runtime.CompileModule(ctx, wasm); err != nil { - return nil, fmt.Errorf("wasm: error compiling guest: %w", err) - } else if handleRequest, ok := guest.ExportedFunctions()[handler.FuncHandleRequest]; !ok { - return nil, fmt.Errorf("wasm: guest doesn't export func[%s]", handler.FuncHandleRequest) - } else if len(handleRequest.ParamTypes()) != 0 || !bytes.Equal(handleRequest.ResultTypes(), []wazeroapi.ValueType{wazeroapi.ValueTypeI64}) { - return nil, fmt.Errorf("wasm: guest exports the wrong signature for func[%s]. should be () -> (i64)", handler.FuncHandleRequest) - } else if handleResponse, ok := guest.ExportedFunctions()[handler.FuncHandleResponse]; !ok { - return nil, fmt.Errorf("wasm: guest doesn't export func[%s]", handler.FuncHandleResponse) - } else if !bytes.Equal(handleResponse.ParamTypes(), []wazeroapi.ValueType{wazeroapi.ValueTypeI32, wazeroapi.ValueTypeI32}) || len(handleResponse.ResultTypes()) != 0 { - return nil, fmt.Errorf("wasm: guest exports the wrong signature for func[%s]. should be (i32, 32) -> ()", handler.FuncHandleResponse) - } else if _, ok = guest.ExportedMemories()[api.Memory]; !ok { - return nil, fmt.Errorf("wasm: guest doesn't export memory[%s]", api.Memory) - } else { - return guest, nil +func validateGuest(guest wazero.CompiledModule) (once bool, err error) { + if _, ok := guest.ExportedMemories()[api.Memory]; !ok { + return false, fmt.Errorf("wasm: guest doesn't export memory[%s]", api.Memory) } -} -// HandleRequest implements Middleware.HandleRequest -func (m *middleware) HandleRequest(ctx context.Context) (outCtx context.Context, ctxNext handler.CtxNext, err error) { - g, guestErr := m.getOrCreateGuest(ctx) - if guestErr != nil { - err = guestErr - return - } + var awaitResponse, handleRequest, handleResponse wazeroapi.FunctionDefinition - s := &requestState{features: m.features, putPool: m.pool.Put, g: g} - defer func() { - if ctxNext != 0 { // will call the next handler - if closeErr := s.closeRequest(); err == nil { - err = closeErr - } - } else { // guest errored or returned the response - if closeErr := s.Close(); err == nil { - err = closeErr + // Look through the imports to see if the guest uses await_response + for _, f := range guest.ImportedFunctions() { + moduleName, name, _ := f.Import() + if moduleName == handler.HostModule && name == handler.FuncAwaitResponse { + awaitResponse = f + if !bytes.Equal(awaitResponse.ParamTypes(), []wazeroapi.ValueType{wazeroapi.ValueTypeI64}) || !bytes.Equal(awaitResponse.ResultTypes(), []wazeroapi.ValueType{wazeroapi.ValueTypeI32}) { + return false, fmt.Errorf("wasm: guest imports the wrong signature for func[%s]. should be (i64) -> (i32)", handler.FuncAwaitResponse) + } else { + once = true + break } } - }() + } - outCtx = context.WithValue(ctx, requestStateKey{}, s) - ctxNext, err = g.handleRequest(outCtx) - return -} + // Look through the exports to see if the guest exports handle_request or handle_response + if handleRequest = guest.ExportedFunctions()[handler.FuncHandleRequest]; handleRequest == nil { + } else if len(handleRequest.ParamTypes()) != 0 || !bytes.Equal(handleRequest.ResultTypes(), []wazeroapi.ValueType{wazeroapi.ValueTypeI64}) { + return false, fmt.Errorf("wasm: guest exports the wrong signature for func[%s]. should be () -> (i64)", handler.FuncHandleRequest) + } -func (m *middleware) getOrCreateGuest(ctx context.Context) (*guest, error) { - poolG := m.pool.Get() - if poolG == nil { - if g, createErr := m.newGuest(ctx); createErr != nil { - return nil, createErr - } else { - poolG = g + if handleResponse = guest.ExportedFunctions()[handler.FuncHandleResponse]; handleResponse == nil { + } else if !bytes.Equal(handleResponse.ParamTypes(), []wazeroapi.ValueType{wazeroapi.ValueTypeI32, wazeroapi.ValueTypeI32}) || len(handleResponse.ResultTypes()) != 0 { + return false, fmt.Errorf("wasm: guest exports the wrong signature for func[%s]. should be (i32, 32) -> ()", handler.FuncHandleResponse) + } + + switch { + case awaitResponse == nil && handleRequest == nil && handleResponse == nil: + return false, fmt.Errorf("wasm: guest should import[%s] or both func[%s] and func[%s] ", + handler.FuncAwaitResponse, handler.FuncHandleRequest, handler.FuncHandleResponse) + case awaitResponse != nil: + if handleRequest != nil || handleResponse != nil { + return false, fmt.Errorf("wasm: guest that imports[%s] shouldn't export func[%s] or func[%s] ", + handler.FuncAwaitResponse, handler.FuncHandleRequest, handler.FuncHandleResponse) } + case handleRequest == nil: + return false, fmt.Errorf("wasm: guest doesn't export func[%s]", handler.FuncHandleRequest) + case handleResponse == nil: + return false, fmt.Errorf("wasm: guest doesn't export func[%s]", handler.FuncHandleResponse) } - return poolG.(*guest), nil + return } -// HandleResponse implements Middleware.HandleResponse -func (m *middleware) HandleResponse(ctx context.Context, reqCtx uint32, hostErr error) error { - s := requestStateFromContext(ctx) - defer s.Close() - s.afterNext = true - - return s.g.handleResponse(ctx, reqCtx, hostErr) +func (m *middleware) requestStateFromContext(ctx context.Context) *requestState { + s, _ := ctx.Value(requestStateKey{}).(*requestState) + return s } // Close implements api.Closer @@ -189,13 +181,7 @@ func (m *middleware) Close(ctx context.Context) error { return m.runtime.Close(ctx) } -type guest struct { - guest wazeroapi.Module - handleRequestFn wazeroapi.Function - handleResponseFn wazeroapi.Function -} - -func (m *middleware) newGuest(ctx context.Context) (*guest, error) { +func (m *middleware) newModule(ctx context.Context) (wazeroapi.Module, error) { moduleName := fmt.Sprintf("%d", atomic.AddUint64(&m.instanceCounter, 1)) g, err := m.runtime.InstantiateModule(ctx, m.guestModule, m.moduleConfig.WithName(moduleName)) @@ -203,32 +189,7 @@ func (m *middleware) newGuest(ctx context.Context) (*guest, error) { _ = m.runtime.Close(ctx) return nil, fmt.Errorf("wasm: error instantiating guest: %w", err) } - - return &guest{ - guest: g, - handleRequestFn: g.ExportedFunction(handler.FuncHandleRequest), - handleResponseFn: g.ExportedFunction(handler.FuncHandleResponse), - }, nil -} - -// handleRequest calls the WebAssembly guest function handler.FuncHandleRequest. -func (g *guest) handleRequest(ctx context.Context) (ctxNext handler.CtxNext, err error) { - if results, guestErr := g.handleRequestFn.Call(ctx); guestErr != nil { - err = guestErr - } else { - ctxNext = handler.CtxNext(results[0]) - } - return -} - -// handleResponse calls the WebAssembly guest function handler.FuncHandleResponse. -func (g *guest) handleResponse(ctx context.Context, reqCtx uint32, err error) error { - wasError := uint64(0) - if err != nil { - wasError = 1 - } - _, err = g.handleResponseFn.Call(ctx, uint64(reqCtx), wasError) - return err + return g, nil } // enableFeatures implements the WebAssembly host function handler.FuncEnableFeatures. @@ -236,7 +197,7 @@ func (m *middleware) enableFeatures(ctx context.Context, stack []uint64) { features := handler.Features(stack[0]) var enabled handler.Features - if s, ok := ctx.Value(requestStateKey{}).(*requestState); ok { + if s := m.requestStateFromContext(ctx); s != nil { s.features = m.host.EnableFeatures(ctx, s.features.WithEnabled(features)) enabled = s.features } else { @@ -299,7 +260,7 @@ func (m *middleware) setMethod(ctx context.Context, mod wazeroapi.Module, params method := uint32(params[0]) methodLen := uint32(params[1]) - _ = mustBeforeNext(ctx, "set", "method") + mustBeforeNext(m.requestStateFromContext(ctx), "set", "method") var p string if methodLen == 0 { @@ -325,7 +286,7 @@ func (m *middleware) setURI(ctx context.Context, mod wazeroapi.Module, params [] uri := uint32(params[0]) uriLen := uint32(params[1]) - _ = mustBeforeNext(ctx, "set", "uri") + mustBeforeNext(m.requestStateFromContext(ctx), "set", "uri") var p string if uriLen > 0 { // overwrite with empty is supported @@ -426,7 +387,7 @@ func (m *middleware) setHeaderValue(ctx context.Context, mod wazeroapi.Module, p if nameLen == 0 { panic("HTTP header name cannot be empty") } - mustHeaderMutable(ctx, "set", kind) + mustHeaderMutable(m.requestStateFromContext(ctx), "set", kind) n := mustReadString(mod.Memory(), "name", name, nameLen) v := mustReadString(mod.Memory(), "value", value, valueLen) @@ -456,7 +417,8 @@ func (m *middleware) addHeaderValue(ctx context.Context, mod wazeroapi.Module, p if nameLen == 0 { panic("HTTP header name cannot be empty") } - mustHeaderMutable(ctx, "add", kind) + + mustHeaderMutable(m.requestStateFromContext(ctx), "add", kind) n := mustReadString(mod.Memory(), "name", name, nameLen) v := mustReadString(mod.Memory(), "value", value, valueLen) @@ -484,7 +446,7 @@ func (m *middleware) removeHeader(ctx context.Context, mod wazeroapi.Module, par if nameLen == 0 { panic("HTTP header name cannot be empty") } - mustHeaderMutable(ctx, "remove", kind) + mustHeaderMutable(m.requestStateFromContext(ctx), "remove", kind) n := mustReadString(mod.Memory(), "name", name, nameLen) switch kind { @@ -507,10 +469,11 @@ func (m *middleware) readBody(ctx context.Context, mod wazeroapi.Module, stack [ buf := uint32(stack[1]) bufLimit := handler.BufLimit(stack[2]) + s := m.requestStateFromContext(ctx) var r io.ReadCloser switch kind { case handler.BodyKindRequest: - s := mustBeforeNextOrFeature(ctx, handler.FeatureBufferRequest, "read", "request body") + mustBeforeNextOrFeature(s, handler.FeatureBufferRequest, "read", "request body") // Lazy create the reader. r = s.requestBodyReader if r == nil { @@ -518,7 +481,7 @@ func (m *middleware) readBody(ctx context.Context, mod wazeroapi.Module, stack [ s.requestBodyReader = r } case handler.BodyKindResponse: - s := mustBeforeNextOrFeature(ctx, handler.FeatureBufferResponse, "read", "response body") + mustBeforeNextOrFeature(s, handler.FeatureBufferResponse, "read", "response body") // Lazy create the reader. r = s.responseBodyReader if r == nil { @@ -540,10 +503,11 @@ func (m *middleware) writeBody(ctx context.Context, mod wazeroapi.Module, params buf := uint32(params[1]) bufLen := uint32(params[2]) + s := m.requestStateFromContext(ctx) var w io.Writer switch kind { case handler.BodyKindRequest: - s := mustBeforeNext(ctx, "write", "request body") + mustBeforeNext(s, "write", "request body") // Lazy create the writer. w = s.requestBodyWriter if w == nil { @@ -551,7 +515,7 @@ func (m *middleware) writeBody(ctx context.Context, mod wazeroapi.Module, params s.requestBodyWriter = w } case handler.BodyKindResponse: - s := mustBeforeNextOrFeature(ctx, handler.FeatureBufferResponse, "write", "response body") + mustBeforeNextOrFeature(s, handler.FeatureBufferResponse, "write", "response body") // Lazy create the writer. w = s.responseBodyWriter if w == nil { @@ -589,7 +553,8 @@ func (m *middleware) getStatusCode(ctx context.Context, results []uint64) { func (m *middleware) setStatusCode(ctx context.Context, params []uint64) { statusCode := uint32(params[0]) - _ = mustBeforeNextOrFeature(ctx, handler.FeatureBufferResponse, "set", "status code") + s := m.requestStateFromContext(ctx) + mustBeforeNextOrFeature(s, handler.FeatureBufferResponse, "set", "status code") m.host.SetStatusCode(ctx, statusCode) } @@ -622,15 +587,14 @@ func readBody(mod wazeroapi.Module, buf uint32, bufLimit handler.BufLimit, r io. } } -func mustBeforeNext(ctx context.Context, op, kind string) (s *requestState) { - if s = requestStateFromContext(ctx); s.afterNext { +func mustBeforeNext(s *requestState, op, kind string) { + if s.afterNext { panic(fmt.Errorf("can't %s %s after next handler", op, kind)) } - return } -func mustBeforeNextOrFeature(ctx context.Context, feature handler.Features, op, kind string) (s *requestState) { - if s = requestStateFromContext(ctx); !s.afterNext { +func mustBeforeNextOrFeature(s *requestState, feature handler.Features, op, kind string) { + if !s.afterNext { // Assume this is serving a response from the guest. } else if s.features.IsEnabled(feature) { // Assume the guest is overwriting the response from next. @@ -638,12 +602,11 @@ func mustBeforeNextOrFeature(ctx context.Context, feature handler.Features, op, panic(fmt.Errorf("can't %s %s after next handler unless %s is enabled", op, kind, feature)) } - return } const i32, i64 = wazeroapi.ValueTypeI32, wazeroapi.ValueTypeI64 -func (m *middleware) instantiateHost(ctx context.Context) (wazeroapi.Module, error) { +func (m *middleware) hostModuleBuilder() wazero.HostModuleBuilder { return m.runtime.NewHostModuleBuilder(handler.HostModule). NewFunctionBuilder(). WithGoFunction(wazeroapi.GoFunc(m.enableFeatures), []wazeroapi.ValueType{i32}, []wazeroapi.ValueType{i32}). @@ -698,20 +661,19 @@ func (m *middleware) instantiateHost(ctx context.Context) (wazeroapi.Module, err WithParameterNames().Export(handler.FuncGetStatusCode). NewFunctionBuilder(). WithGoFunction(wazeroapi.GoFunc(m.setStatusCode), []wazeroapi.ValueType{i32}, []wazeroapi.ValueType{}). - WithParameterNames("status_code").Export(handler.FuncSetStatusCode). - Instantiate(ctx) + WithParameterNames("status_code").Export(handler.FuncSetStatusCode) } -func mustHeaderMutable(ctx context.Context, op string, kind handler.HeaderKind) { +func mustHeaderMutable(s *requestState, op string, kind handler.HeaderKind) { switch kind { case handler.HeaderKindRequest: - _ = mustBeforeNext(ctx, op, "request header") + mustBeforeNext(s, op, "request header") case handler.HeaderKindRequestTrailers: - _ = mustBeforeNext(ctx, op, "request trailer") + mustBeforeNext(s, op, "request trailer") case handler.HeaderKindResponse: - _ = mustBeforeNextOrFeature(ctx, handler.FeatureBufferResponse, op, "response header") + mustBeforeNextOrFeature(s, handler.FeatureBufferResponse, op, "response header") case handler.HeaderKindResponseTrailers: - _ = mustBeforeNextOrFeature(ctx, handler.FeatureBufferResponse, op, "response trailer") + mustBeforeNextOrFeature(s, handler.FeatureBufferResponse, op, "response trailer") default: panic("unsupported header kind: " + strconv.Itoa(int(kind))) } diff --git a/handler/middleware_test.go b/handler/middleware_test.go index 213f90d..f8da68a 100644 --- a/handler/middleware_test.go +++ b/handler/middleware_test.go @@ -163,7 +163,7 @@ func requireGlobals(t *testing.T, mw Middleware, wantGlobals ...uint64) { } func getGlobalVals(mw Middleware) []uint64 { - pool := mw.(*middleware).pool + pool := mw.(*poolMiddleware).pool var guests []*guest var globals []uint64 @@ -193,7 +193,7 @@ func requireHandleRequest(t *testing.T, mw Middleware, ctxNext handler.CtxNext, if want, have := expectedCtx, ctxNext>>32; want != have { t.Errorf("unexpected ctx, want: %d, have: %d", want, have) } - if mw.(*middleware).pool.Get() != nil { + if mw.(*poolMiddleware).pool.Get() != nil { t.Error("expected handler to not return guest to the pool") } } diff --git a/handler/nethttp/benchmark_test.go b/handler/nethttp/benchmark_test.go index 9634451..ace10e5 100644 --- a/handler/nethttp/benchmark_test.go +++ b/handler/nethttp/benchmark_test.go @@ -19,7 +19,7 @@ var ( ) func init() { - noopHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + noopHandler = func(w http.ResponseWriter, r *http.Request) {} smallBody = []byte("hello world") largeSize = 4096 // 2x the read buffer size largeBody = make([]byte, largeSize) @@ -42,11 +42,6 @@ func getWithLargeHeader(url string) (req *http.Request) { return } -func getWithQuery(url string) (req *http.Request) { - req, _ = http.NewRequest(http.MethodGet, url+"/v1.0/hi?name=panda", nil) - return -} - func getWithoutHeaders(url string) (req *http.Request) { req, _ = http.NewRequest(http.MethodGet, url+"/v1.0/hi", nil) req.Header = http.Header{} @@ -109,6 +104,14 @@ var benches = map[string]struct { return }, }, + "example log": { + bin: test.BinExampleLog, + request: get, + }, + "example log once": { + bin: test.BinExampleLogOnce, + request: get, + }, "log": { bin: test.BinBenchLog, request: get, diff --git a/handler/nethttp/middleware_test.go b/handler/nethttp/middleware_test.go index 8ea1b35..4ff8cbd 100644 --- a/handler/nethttp/middleware_test.go +++ b/handler/nethttp/middleware_test.go @@ -218,21 +218,41 @@ func TestHeaderValue(t *testing.T) { // TestHandleResponse uses test.BinE2EHandleResponse which ensures reqCtx // propagates from handler.FuncHandleRequest to handler.FuncHandleResponse. func TestHandleResponse(t *testing.T) { - mw, err := wasm.NewMiddleware(testCtx, test.BinE2EHandleResponse) - if err != nil { - t.Fatal(err) + tests := []struct { + name string + guest []byte + }{ + { + name: "pool", + guest: test.BinE2EHandleResponse, + }, + { + name: "once", + guest: test.BinE2EAwaitResponse, + }, } - defer mw.Close(testCtx) - ts := httptest.NewServer(mw.NewHandler(testCtx, noopHandler)) - defer ts.Close() + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + mw, err := wasm.NewMiddleware(testCtx, tc.guest) + if err != nil { + t.Fatal(err) + } + defer mw.Close(testCtx) - resp, err := ts.Client().Get(ts.URL) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatalf("invalid status code: %d, status message: %s", resp.StatusCode, resp.Status) + ts := httptest.NewServer(mw.NewHandler(testCtx, noopHandler)) + defer ts.Close() + + resp, err := ts.Client().Get(ts.URL) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatalf("invalid status code: %d, status message: %s", resp.StatusCode, resp.Status) + } + }) } } diff --git a/handler/once.go b/handler/once.go new file mode 100644 index 0000000..06e2ad1 --- /dev/null +++ b/handler/once.go @@ -0,0 +1,108 @@ +package handler + +import ( + "context" + + wazeroapi "github.com/tetratelabs/wazero/api" + + "github.com/http-wasm/http-wasm-host-go/api/handler" +) + +var _ Middleware = (*onceMiddleware)(nil) + +type onceMiddleware struct { + middleware +} + +type onceRequestStateKey struct{} + +type onceRequestState struct { + requestState + + g wazeroapi.Module + + awaitingResponse chan handler.CtxNext + responseReady, guestDone chan error +} + +func (r *onceRequestState) Close() error { + if g := r.g; g != nil { + _ = g.Close(context.Background()) + } + return r.requestState.Close() +} + +// HandleRequest implements Middleware.HandleRequest +func (m *onceMiddleware) HandleRequest(ctx context.Context) (context.Context, handler.CtxNext, error) { + s := &onceRequestState{ + requestState: requestState{features: m.features}, + awaitingResponse: make(chan handler.CtxNext, 1), + responseReady: make(chan error, 1), + guestDone: make(chan error, 1), + } + ctx = context.WithValue(ctx, requestStateKey{}, &s.requestState) + ctx = context.WithValue(ctx, onceRequestStateKey{}, s) + + // instantiate the module in a new goroutine because it will block on + // awaitResponse. This goroutine might outlive HandleRequest. + go func() { + g, err := m.newModule(ctx) + s.g = g + s.guestDone <- err + }() + + select { + case <-ctx.Done(): // ensure any context timeout applies + _ = s.Close() + return nil, 0, ctx.Err() + case err := <-s.guestDone: + _ = s.Close() + return nil, 0, err + case ctxNext := <-s.awaitingResponse: + if ctxNext != 0 { // will call the next handler + return ctx, ctxNext, s.closeRequest() + } else { // guest returned the response + return nil, 0, s.Close() + } + } +} + +// HandleResponse implements Middleware.HandleResponse +func (m *onceMiddleware) HandleResponse(ctx context.Context, _ uint32, hostErr error) error { + s := ctx.Value(onceRequestStateKey{}).(*onceRequestState) + + s.afterNext = true + s.responseReady <- hostErr // unblock awaitResponse + + // Wait until the goroutine completes. + select { + case <-ctx.Done(): // ensure any context timeout applies + _ = s.Close() + return nil + case err := <-s.guestDone: // block until the guest completes + _ = s.Close() + return err + } +} + +// awaitResponse implements the WebAssembly host function +// handler.FuncAwaitResponse. +func awaitResponse(ctx context.Context, stack []uint64) { + s := ctx.Value(onceRequestStateKey{}).(*onceRequestState) + s.awaitingResponse <- handler.CtxNext(stack[0]) + hostErr := <-s.responseReady + if hostErr != nil { + stack[0] = 1 + } else { + stack[0] = 0 + } +} + +func (m *onceMiddleware) instantiateHost(ctx context.Context) error { + _, err := m.hostModuleBuilder(). + NewFunctionBuilder(). + WithGoFunction(wazeroapi.GoFunc(awaitResponse), []wazeroapi.ValueType{i64}, []wazeroapi.ValueType{i32}). + WithParameterNames().Export(handler.FuncAwaitResponse). + Instantiate(ctx) + return err +} diff --git a/handler/pool.go b/handler/pool.go new file mode 100644 index 0000000..8b2fd9e --- /dev/null +++ b/handler/pool.go @@ -0,0 +1,135 @@ +package handler + +import ( + "context" + "sync" + + wazeroapi "github.com/tetratelabs/wazero/api" + + "github.com/http-wasm/http-wasm-host-go/api/handler" +) + +var _ Middleware = (*poolMiddleware)(nil) + +type poolMiddleware struct { + middleware + pool sync.Pool +} + +func (m *poolMiddleware) instantiateHost(ctx context.Context) error { + if _, err := m.hostModuleBuilder().Instantiate(ctx); err != nil { + return err + } + + // Eagerly add one instance to the pool. Doing so helps to fail fast. + if g, err := m.newGuest(ctx); err != nil { + return err + } else { + m.pool.Put(g) + } + return nil +} + +type poolRequestStateKey struct{} + +type poolRequestState struct { + requestState + + putPool func(x any) + g *guest +} + +func (r *poolRequestState) Close() error { + if g := r.g; g != nil { + r.putPool(r.g) + r.g = nil + } + return r.requestState.Close() +} + +// HandleRequest implements Middleware.HandleRequest +func (m *poolMiddleware) HandleRequest(ctx context.Context) (outCtx context.Context, ctxNext handler.CtxNext, err error) { + g, guestErr := m.getOrCreateGuest(ctx) + if guestErr != nil { + err = guestErr + return + } + + s := &poolRequestState{requestState: requestState{features: m.features}, putPool: m.pool.Put, g: g} + defer func() { + if ctxNext != 0 { // will call the next handler + if closeErr := s.closeRequest(); err == nil { + err = closeErr + } + } else { // guest errored or returned the response + if closeErr := s.Close(); err == nil { + err = closeErr + } + } + }() + + outCtx = context.WithValue(ctx, requestStateKey{}, &s.requestState) + outCtx = context.WithValue(outCtx, poolRequestStateKey{}, s) + ctxNext, err = g.handleRequest(outCtx) + return +} + +// HandleResponse implements Middleware.HandleResponse +func (m *poolMiddleware) HandleResponse(ctx context.Context, reqCtx uint32, hostErr error) error { + s := ctx.Value(poolRequestStateKey{}).(*poolRequestState) + defer s.Close() + s.afterNext = true + + return s.g.handleResponse(ctx, reqCtx, hostErr) +} + +func (m *poolMiddleware) getOrCreateGuest(ctx context.Context) (*guest, error) { + poolG := m.pool.Get() + if poolG == nil { + if g, createErr := m.newGuest(ctx); createErr != nil { + return nil, createErr + } else { + poolG = g + } + } + return poolG.(*guest), nil +} + +type guest struct { + guest wazeroapi.Module + handleRequestFn wazeroapi.Function + handleResponseFn wazeroapi.Function +} + +func (m *poolMiddleware) newGuest(ctx context.Context) (*guest, error) { + g, err := m.newModule(ctx) + if err != nil { + return nil, err + } + + return &guest{ + guest: g, + handleRequestFn: g.ExportedFunction(handler.FuncHandleRequest), + handleResponseFn: g.ExportedFunction(handler.FuncHandleResponse), + }, nil +} + +// handleRequest calls the WebAssembly guest function handler.FuncHandleRequest. +func (g *guest) handleRequest(ctx context.Context) (ctxNext handler.CtxNext, err error) { + if results, guestErr := g.handleRequestFn.Call(ctx); guestErr != nil { + err = guestErr + } else { + ctxNext = handler.CtxNext(results[0]) + } + return +} + +// handleResponse calls the WebAssembly guest function handler.FuncHandleResponse. +func (g *guest) handleResponse(ctx context.Context, reqCtx uint32, err error) error { + wasError := uint64(0) + if err != nil { + wasError = 1 + } + _, err = g.handleResponseFn.Call(ctx, uint64(reqCtx), wasError) + return err +} diff --git a/handler/state.go b/handler/state.go index d619a9b..ec40de6 100644 --- a/handler/state.go +++ b/handler/state.go @@ -1,7 +1,6 @@ package handler import ( - "context" "io" "net/http" @@ -12,10 +11,6 @@ import ( // pointer to the current request. type requestStateKey struct{} -func requestStateFromContext(ctx context.Context) *requestState { - return ctx.Value(requestStateKey{}).(*requestState) -} - type requestState struct { afterNext bool requestBodyReader io.ReadCloser @@ -26,9 +21,6 @@ type requestState struct { // features are the current request's features which may be more than // Middleware.Features. features handler.Features - - putPool func(x any) - g *guest } func (r *requestState) closeRequest() (err error) { @@ -46,14 +38,9 @@ func (r *requestState) closeRequest() (err error) { } // Close releases all resources for the current request, including: -// - putting the guest module back into the pool // - releasing any request body resources // - releasing any response body resources func (r *requestState) Close() (err error) { - if g := r.g; g != nil { - r.putPool(r.g) - r.g = nil - } err = r.closeRequest() if respBW := r.responseBodyWriter; respBW != nil { if f, ok := respBW.(http.Flusher); ok { diff --git a/internal/test/testdata.go b/internal/test/testdata.go index 987be23..f6e8b15 100644 --- a/internal/test/testdata.go +++ b/internal/test/testdata.go @@ -56,6 +56,10 @@ var BinExampleLog = func() []byte { return binExample("log") }() +var BinExampleLogOnce = func() []byte { + return binExample("log_once") +}() + var BinExampleRedact = func() []byte { return binExample("redact") }() @@ -80,6 +84,9 @@ var BinE2EURI []byte //go:embed testdata/e2e/header_value.wasm var BinE2EHeaderValue []byte +//go:embed testdata/e2e/await_response.wasm +var BinE2EAwaitResponse []byte + //go:embed testdata/e2e/handle_response.wasm var BinE2EHandleResponse []byte diff --git a/internal/test/testdata/bench/add_header_value.wasm b/internal/test/testdata/bench/add_header_value.wasm index 6871b749e297882df762e7123281c4f86b5a7d43..a768195d4240fcffcc721f61b42dc97ed127ab92 100644 GIT binary patch delta 23 fcmcc3bc1Qa5ynXqkLokpP5f%W#GE`?i_s7ObgBq& delta 26 icmcb?ben0y5yq(#kLoizPW)=X%2t$G=sa1S(GUQV>ItC$ diff --git a/internal/test/testdata/bench/add_header_value.wat b/internal/test/testdata/bench/add_header_value.wat index e02cb71..0bcf7f8 100644 --- a/internal/test/testdata/bench/add_header_value.wat +++ b/internal/test/testdata/bench/add_header_value.wat @@ -25,6 +25,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/get_header_names.wasm b/internal/test/testdata/bench/get_header_names.wasm index d9eaec8f843a1d176f3fde3217a0eb8b49100ef4..6be94392a13aa416fb7a6695550e18626a0e1f60 100644 GIT binary patch delta 22 ecmbQh)XOws9%J6b`Fe~x6VJ&pF(*&_C=UQwj0eyF delta 25 hcmeBWn!q$+9%JFe`Ff1{6VJ)9vK6HkI!}Bj4*+qc2>}2A diff --git a/internal/test/testdata/bench/get_header_names.wat b/internal/test/testdata/bench/get_header_names.wat index 5478c62..2bc3e32 100644 --- a/internal/test/testdata/bench/get_header_names.wat +++ b/internal/test/testdata/bench/get_header_names.wat @@ -20,7 +20,7 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/get_header_values.wasm b/internal/test/testdata/bench/get_header_values.wasm index 1a59df585f07a7cfef756bdf2bece16eddc09af3..268669e32ef66e12a9454d19eb5fcf93b6779a7f 100644 GIT binary patch delta 23 fcmcb_be?I#Ud9O%_Zc$UO#Gn7#GE`?nNc4Aan1-n delta 26 icmX@lbct!gUdG82_Zc$UPyC?A%2t$G=sa10Q6B)0lL>DC diff --git a/internal/test/testdata/bench/get_header_values.wat b/internal/test/testdata/bench/get_header_values.wat index b53eee3..551a2cf 100644 --- a/internal/test/testdata/bench/get_header_values.wat +++ b/internal/test/testdata/bench/get_header_values.wat @@ -26,6 +26,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/get_uri.wasm b/internal/test/testdata/bench/get_uri.wasm index bf9eebe029b7efcab6a67d61d46a03c8f024195d..62a21a8f4afca9d119e75f8a57338d319f21911d 100644 GIT binary patch delta 50 zcmcc3c!P1mL=88Vyu{p81`hW0)ROqpqD)3EMg|z0NsO6+fsuiUiGewJ;vRVbcg+mk delta 53 zcmcb?c$;y;L~T!&yu{p81`hW0)ROqpqD)3EMg|z0Ns^g?fsuiUiGi&swa|IuPI&;K Cdkzf% diff --git a/internal/test/testdata/bench/get_uri.wat b/internal/test/testdata/bench/get_uri.wat index 66354e8..c510480 100644 --- a/internal/test/testdata/bench/get_uri.wat +++ b/internal/test/testdata/bench/get_uri.wat @@ -17,6 +17,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/log.wasm b/internal/test/testdata/bench/log.wasm index 5b4320f15fdccda8b52d0847275bf66cca216c9e..19c684acff09a6b29e1ddaa264a3c2b3a96d225c 100644 GIT binary patch delta 22 ecmey*_=j=A0>-$B3q=?WCT^2wWh+W8be?!!6##;o35@^% diff --git a/internal/test/testdata/bench/log.wat b/internal/test/testdata/bench/log.wat index 0c7e17e..07d26c8 100644 --- a/internal/test/testdata/bench/log.wat +++ b/internal/test/testdata/bench/log.wat @@ -19,6 +19,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/read_body.wasm b/internal/test/testdata/bench/read_body.wasm index 83b25c0687c642175b42473d1491247cdb65d1a4..079a447384649d976f5556d8f2bcaf80a996a048 100644 GIT binary patch delta 39 ucmeBX>R_6%hB12LS_MXSmg-hB0p9S_MYaiRV-#*@{vNol7bhIWmjmQ;Ul7i`dy18JJV^(*P`o4H*Cc diff --git a/internal/test/testdata/bench/read_body.wat b/internal/test/testdata/bench/read_body.wat index b3ad8a1..b4dc35f 100644 --- a/internal/test/testdata/bench/read_body.wat +++ b/internal/test/testdata/bench/read_body.wat @@ -33,6 +33,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/read_body_stream.wasm b/internal/test/testdata/bench/read_body_stream.wasm index a301fdd9e3704b722cf49ed7e5a2691dfafa0d16..1c6142d46a63a36767264e5411607aba742f3026 100644 GIT binary patch delta 39 ucmeBX>R_6%lrd@IGId6yiRVSmg-lreSUGId7NiRV-#*@{vNol7bhIWmjmQ;Ul7i`dy18JJV^(*P`T4H*Cc diff --git a/internal/test/testdata/bench/read_body_stream.wat b/internal/test/testdata/bench/read_body_stream.wat index ca7c795..49c30d7 100644 --- a/internal/test/testdata/bench/read_body_stream.wat +++ b/internal/test/testdata/bench/read_body_stream.wat @@ -29,6 +29,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/remove_header.wasm b/internal/test/testdata/bench/remove_header.wasm index 01844fc34858930581dab99f23c513444f8928a5..7fea53dd8ba8dea76c7ca8f4e702523a6b343ced 100644 GIT binary patch delta 26 icmey*_=j=AO2(jxt2DU9m>C!t8JL(Dn3E?ykOu&9hzAS+ delta 29 lcmeyv_@8mYO2*KMt2Fo|nHd-u8JL(D*osmMohROv2LOn>2t5D* diff --git a/internal/test/testdata/bench/remove_header.wat b/internal/test/testdata/bench/remove_header.wat index 2b170a8..f0a4e1b 100644 --- a/internal/test/testdata/bench/remove_header.wat +++ b/internal/test/testdata/bench/remove_header.wat @@ -17,6 +17,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/set_header_value.wasm b/internal/test/testdata/bench/set_header_value.wasm index 3d819c9670a1fbd38d83d51bfb5bf2b01103d6b3..0036fa4daa899b55ae49ef5463ddc775a09d882a 100644 GIT binary patch delta 23 fcmaFD^pI)7S;k2d&*?MTP5fuT#GE|Yn9&dbcfts! delta 26 icmaFJ^n_``S;nao&*?KdPW)%U%2t$G=sekg(GUQZi3!&L diff --git a/internal/test/testdata/bench/set_header_value.wat b/internal/test/testdata/bench/set_header_value.wat index 4fcf304..2d927f4 100644 --- a/internal/test/testdata/bench/set_header_value.wat +++ b/internal/test/testdata/bench/set_header_value.wat @@ -25,6 +25,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/set_status_code.wasm b/internal/test/testdata/bench/set_status_code.wasm index a0eb63a536eee2a1989050e11f45206972a6a9e9..459a75f1979c32e3958cfd155a48359b9febea51 100644 GIT binary patch delta 35 qcmX@dc$#rSJEQZ&4jpkZW(EdE1|}v3=H!wJMvlzl_|&4J{2~CPwh1u+ delta 38 tcmX@jc#d&GJEPmg4jm~;W(EdE1|}v3wxZNR=aLFWj?Cit)S{yNA^^nH3S|HQ diff --git a/internal/test/testdata/bench/set_status_code.wat b/internal/test/testdata/bench/set_status_code.wat index 2c75342..87e7d4e 100644 --- a/internal/test/testdata/bench/set_status_code.wat +++ b/internal/test/testdata/bench/set_status_code.wat @@ -11,6 +11,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/set_uri.wasm b/internal/test/testdata/bench/set_uri.wasm index d7fd9969f811b84707cbecebb8d9d0ece149d61b..b122581f142b7a36bdcae3f5627b1f34c871702a 100644 GIT binary patch delta 52 zcmaFQ_=a)9az>YlD`Ygpm>C!t8JL(Dn3GE?7&$VF<5P=@@{8E{fg+_vnT+f}B0eWI F4*+%l4i*3a delta 29 lcmaFE_?~gXaz^)wD`fa2nHd-u8JL(D*osmMohP1`1ptDr2k!s? diff --git a/internal/test/testdata/bench/set_uri.wat b/internal/test/testdata/bench/set_uri.wat index d9ceb3e..2d20914 100644 --- a/internal/test/testdata/bench/set_uri.wat +++ b/internal/test/testdata/bench/set_uri.wat @@ -16,6 +16,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/bench/write_body.wasm b/internal/test/testdata/bench/write_body.wasm index eb97417d94f4fcdfce549778ccead9374bca229c..d1fc33c8ac9649f798b18a88c0bb3a588be4caa5 100644 GIT binary patch delta 22 ecmeBX>R_6%oH1eI3MEFZiKk_ln3E^Ikp}=*A_uYn delta 25 hcmeBR>Smg-oH2Rg3MEF}iKk^)*@{vNohQDO2LNyI2;%?% diff --git a/internal/test/testdata/bench/write_body.wat b/internal/test/testdata/bench/write_body.wat index c1141c4..4ef6aab 100644 --- a/internal/test/testdata/bench/write_body.wat +++ b/internal/test/testdata/bench/write_body.wat @@ -19,6 +19,6 @@ (return (i64.const 0))) ;; handle_response should not be called as handle_request returns zero. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) (unreachable)) ) diff --git a/internal/test/testdata/e2e/await_response.wat b/internal/test/testdata/e2e/await_response.wat new file mode 100644 index 0000000..65b4cb0 --- /dev/null +++ b/internal/test/testdata/e2e/await_response.wat @@ -0,0 +1,57 @@ +(module $await_response + + (import "http_handler" "await_response" (func $await_response + (param $ctx_next i64) + (result (; is_error ;) i32))) + + ;; define a start function that performs a request-response without exports. + (func $start + (local $ctx_next i64) + (local $is_error i32) + (local $ctx i32) + + ;; ctxNext := handleRequest() + (local.set $ctx_next (call $handle_request)) + + ;; isError := awaitResponse(ctxNext()) + (local.set $is_error (call $await_response (local.get $ctx_next))) + + ;; expected_count = uint32(result >> 32) + (local.set $ctx + (i32.wrap_i64 (i64.shr_u (local.get $ctx_next) (i64.const 32)))) + + (call $handle_response (local.get $ctx) (local.get $is_error)) + ) + + (memory (export "memory") 1 1 (; 1 page==64KB ;)) + + ;; reqCtx is the upper 32-bits of the $ctx_next result the host should + ;; propagate from handle_request to handle_response. + (global $ctx (export "reqCtx") (mut i32) (i32.const 42)) + + ;; handle_request sets the request ID to the global then increments the + ;; global. + (func $handle_request (result (; ctx_next ;) i64) + (local $ctx i32) + + ;; reqCtx := global.reqCtx + (local.set $ctx (global.get $ctx)) + + ;; global.reqCtx++ + (global.set $ctx (i32.add (global.get $ctx) (i32.const 1))) + + ;; return uint64(reqCtx) << 32 | uint64(1) + (return + (i64.or + (i64.shl (i64.extend_i32_u (local.get $ctx)) (i64.const 32)) + (i64.const 1)))) + + ;; If propagation works, the current request ID should be one less than the + ;; global. + (func $handle_response (param $ctx i32) (param $is_error i32) + ;; if reqCtx != global.reqCtx - 1 { panic } + (if (i32.ne + (local.get $ctx) + (i32.sub (global.get $ctx) (i32.const 1))) + (then unreachable))) ;; fail as the host didn't propagate the reqCtx +) diff --git a/internal/test/testdata/e2e/handle_response.wasm b/internal/test/testdata/e2e/handle_response.wasm index 5f9e7b0637368d56a3e3bd56a3ad1290795f2954..c620f5298931852bd14bf4514fa11ddd083215e9 100644 GIT binary patch delta 46 tcmcb>c${%U52Mw@UI`^hCI&_Z=H!wJMkWZwky#v{T2z!@#Lfnh001D_3)%nx delta 55 wcmX@kc!6<352N$MUI}e^CI&_ZwxZNR=aLFWCOC^Dvp7Drs3^aPof9q%0J2FBb^rhX diff --git a/internal/test/testdata/e2e/handle_response.wat b/internal/test/testdata/e2e/handle_response.wat index a83e225..8e1f936 100644 --- a/internal/test/testdata/e2e/handle_response.wat +++ b/internal/test/testdata/e2e/handle_response.wat @@ -4,31 +4,31 @@ ;; reqCtx is the upper 32-bits of the $ctx_next result the host should ;; propagate from handle_request to handle_response. - (global $reqCtx (export "reqCtx") (mut i32) (i32.const 42)) + (global $ctx (export "reqCtx") (mut i32) (i32.const 42)) ;; handle_request sets the request ID to the global then increments the ;; global. (func (export "handle_request") (result (; ctx_next ;) i64) - (local $reqCtx i32) + (local $ctx i32) ;; reqCtx := global.reqCtx - (local.set $reqCtx (global.get $reqCtx)) + (local.set $ctx (global.get $ctx)) ;; global.reqCtx++ - (global.set $reqCtx (i32.add (global.get $reqCtx) (i32.const 1))) + (global.set $ctx (i32.add (global.get $ctx) (i32.const 1))) ;; return uint64(reqCtx) << 32 | uint64(1) (return (i64.or - (i64.shl (i64.extend_i32_u (local.get $reqCtx)) (i64.const 32)) + (i64.shl (i64.extend_i32_u (local.get $ctx)) (i64.const 32)) (i64.const 1)))) ;; If propagation works, the current request ID should be one less than the ;; global. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32) + (func (export "handle_response") (param $ctx i32) (param $is_error i32) ;; if reqCtx != global.reqCtx - 1 { panic } (if (i32.ne - (local.get $reqCtx) - (i32.sub (global.get $reqCtx) (i32.const 1))) + (local.get $ctx) + (i32.sub (global.get $ctx) (i32.const 1))) (then unreachable))) ;; fail as the host didn't propagate the reqCtx ) diff --git a/internal/test/testdata/e2e/header_names.wasm b/internal/test/testdata/e2e/header_names.wasm index 2532af5918311e2ab45923e920f54a00ea44018f..72e2d60fb7c4766dc0b0bae15870f9bc99433640 100644 GIT binary patch delta 32 ocmeyt^p$DCd&ZWDAM_bRCo3?z^D!rvR4{U67RRR+73CKJ0MXG4#Q*>R delta 35 rcmey$^n+=_d&c&OAM_a`CMz(y3$hiZ7CM(yFmhxT$EOw*4*&oF delta 27 jcmbQiJezsK2ga2XKbkW}P1a=eWMwN#Ep(n-!l(iOk248C diff --git a/internal/test/testdata/e2e/method.wat b/internal/test/testdata/e2e/method.wat index 2a69658..0d0fba4 100644 --- a/internal/test/testdata/e2e/method.wat +++ b/internal/test/testdata/e2e/method.wat @@ -40,5 +40,5 @@ (return (i64.const 1))) ;; handle_response is no-op as this is a request-only handler. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) + (func (export "handle_response") (param $ctx i32) (param $is_error i32)) ) diff --git a/internal/test/testdata/e2e/protocol_version.wasm b/internal/test/testdata/e2e/protocol_version.wasm index 619f3335054060c4dd9ec8f1b8c9439d74f2b0fe..a79d05a0e4f1487ab354603508c28a99dc93382f 100644 GIT binary patch delta 31 ncmX@Ww3lhZZpMa*d+Zql)$;^y;jDeGl7@e7zlP6a(DggjlcLqHG delta 29 lcmbQqJcoG$6C>k{$;^y;j3JYa7@b+!ic$-mCzmrS0RV7h2W|iW diff --git a/internal/test/testdata/e2e/uri.wat b/internal/test/testdata/e2e/uri.wat index 703b80d..59df3d1 100644 --- a/internal/test/testdata/e2e/uri.wat +++ b/internal/test/testdata/e2e/uri.wat @@ -43,5 +43,5 @@ (return (i64.const 1))) ;; handle_response is no-op as this is a request-only handler. - (func (export "handle_response") (param $reqCtx i32) (param $is_error i32)) + (func (export "handle_response") (param $ctx i32) (param $is_error i32)) ) diff --git a/internal/test/testdata/error/panic_on_handle_request.wasm b/internal/test/testdata/error/panic_on_handle_request.wasm index 14efddc9f72368b3b3fe9c75fecc79a3102d673c..0e681845e59335b684fb3f1914b389ad1fc79ed3 100644 GIT binary patch delta 32 ocmdnZw1a8FYR2w~YeE>!Co?i?@-ZiuR4{U67RRR+73CKJ0JAy@lmGw# delta 35 rcmdnNw3}(dYR0~aYeE>UCNnZ>3bGZY7CM(yFmhxT$EOw*w%f>q8mMC$lnY@-ZiuR4{U67RRR+73CKJ0JT2~r~m)} delta 35 rcmdnPw4Z6hI>!Eq>q8l>CbKeX3bGZY7CM(yFmhxT$EOw*