diff --git a/Makefile b/Makefile index 09207356..615f7803 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,9 @@ test-%-tinygo: TINYGO_TARGET := ./targets/fastly-compute-wasip1.json test-e2e-tinygo: TINYGO_TARGET := ./targets/fastly-compute-wasip1-serve.json build-examples-tinygo: TINYGO_TARGET ?= ./targets/fastly-compute-wasip1.json +# Run e2e tests sequentially to avoid port conflicts: +test-e2e-%: GO_TEST_FLAGS += -p 1 + # Allow `test -exec` and tinygo's emulator target to find `serve.sh`: test-e2e-%: export PATH := $(PWD)/end_to_end_tests:$(PATH) diff --git a/end_to_end_tests/loopback/.gitignore b/end_to_end_tests/.gitignore similarity index 100% rename from end_to_end_tests/loopback/.gitignore rename to end_to_end_tests/.gitignore diff --git a/end_to_end_tests/serve-many/fastly.toml b/end_to_end_tests/serve-many/fastly.toml new file mode 100644 index 00000000..bfcf17b2 --- /dev/null +++ b/end_to_end_tests/serve-many/fastly.toml @@ -0,0 +1,15 @@ +# This file describes a Fastly Compute package. To learn more visit: +# https://www.fastly.com/documentation/reference/compute/fastly-toml + +authors = [] +description = "" +language = "go" +manifest_version = 3 +name = "serve-many test" +service_id = "" + +[scripts] + build = "GOARCH=wasm GOOS=wasip1 go build -tags fastlyinternaldebug -o bin/main.wasm ." + +[local_server.backends.self] + url = "http://127.0.0.1:23456/" diff --git a/end_to_end_tests/serve-many/main.go b/end_to_end_tests/serve-many/main.go new file mode 100644 index 00000000..22f93f79 --- /dev/null +++ b/end_to_end_tests/serve-many/main.go @@ -0,0 +1,34 @@ +//go:build !test + +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/fastly/compute-sdk-go/fsthttp" +) + +func main() { + handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { + if r.Header.Get("Close-Session") == "1" { + opts := fsthttp.ServeManyOptionsFromContext(ctx) + opts.MaxRequests = 1 + } + + sessionID, requestID := os.Getenv("FASTLY_TRACE_ID"), r.RequestID + fmt.Printf("Session ID: %s, Request ID: %s\n", sessionID, requestID) + + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Session-ID", sessionID) + w.Header().Set("Request-ID", requestID) + w.Write([]byte("OK")) + } + fsthttp.ServeMany(handler, &fsthttp.ServeManyOptions{ + NextTimeout: 5 * time.Second, + MaxRequests: 100, + MaxLifetime: 10 * time.Second, + }) +} diff --git a/end_to_end_tests/serve-many/main_test.go b/end_to_end_tests/serve-many/main_test.go new file mode 100644 index 00000000..4c913025 --- /dev/null +++ b/end_to_end_tests/serve-many/main_test.go @@ -0,0 +1,80 @@ +//go:build ((tinygo.wasm && wasi) || wasip1) && !nofastlyhostcalls + +package main + +import ( + "context" + "testing" + + "github.com/fastly/compute-sdk-go/fsthttp" +) + +func TestSessionReuse(t *testing.T) { + // First request. Session ID and request ID should match. + req, err := fsthttp.NewRequest("GET", "http://anyplace.horse", nil) + if err != nil { + t.Fatal(err) + } + resp, err := req.Send(context.Background(), "self") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + sessionID, requestID := resp.Header.Get("Session-ID"), resp.Header.Get("Request-ID") + + if sessionID == "" || requestID == "" { + t.Fatalf("Session-ID and/or Request-ID are empty: %s, %s", sessionID, requestID) + } + if sessionID != requestID { + t.Errorf("sessionID = %s, requestID = %s; expected them to match", sessionID, requestID) + } + prevSessionID := sessionID + + // Second request. This should reuse the session, so the session ID + // should match the previous session ID and the request ID should + // not match. + // + // We also set a header to tell the server to not allow any more + // requests on this session. + req, err = fsthttp.NewRequest("GET", "http://anyplace.horse", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Close-Session", "1") + resp, err = req.Send(context.Background(), "self") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + sessionID, requestID = resp.Header.Get("Session-ID"), resp.Header.Get("Request-ID") + + if sessionID != prevSessionID { + t.Errorf("sessionID = %s, previous sessionID = %s; expected them to match", sessionID, sessionID) + } + if sessionID == requestID { + t.Errorf("sessionID = %s, requestID = %s; expected them to differ", sessionID, requestID) + } + prevSessionID = sessionID + + // Third request, we should have a new session ID and it should match the request ID + req, err = fsthttp.NewRequest("GET", "http://anyplace.horse", nil) + if err != nil { + t.Fatal(err) + } + resp, err = req.Send(context.Background(), "self") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + sessionID, requestID = resp.Header.Get("Session-ID"), resp.Header.Get("Request-ID") + + if sessionID == prevSessionID { + t.Errorf("sessionID = %s, previous sessionID = %s; expected them to differ", sessionID, sessionID) + } + if sessionID != requestID { + t.Errorf("sessionID = %s, requestID = %s; expected them to match", sessionID, requestID) + } +} diff --git a/fsthttp/handle.go b/fsthttp/handle.go index 67235fad..9a813b94 100644 --- a/fsthttp/handle.go +++ b/fsthttp/handle.go @@ -21,14 +21,14 @@ func Serve(h Handler) { panic(fmt.Errorf("get client handles: %w", err)) } - serve(h, abireq, abibody) + serve(context.Background(), h, abireq, abibody) // wait for any stale-while-revalidate goroutines to complete. guestCacheSWRPending.Wait() } -func serve(h Handler, abireq *fastly.HTTPRequest, abibody *fastly.HTTPBody) { - ctx, cancel := context.WithCancel(context.Background()) +func serve(ctx context.Context, h Handler, abireq *fastly.HTTPRequest, abibody *fastly.HTTPBody) { + ctx, cancel := context.WithCancel(ctx) defer cancel() clientRequest, err := newClientRequest(abireq, abibody) @@ -60,11 +60,13 @@ type ServeManyOptions struct { func ServeMany(h HandlerFunc, serveOpts *ServeManyOptions) { start := time.Now() + ctx := context.WithValue(context.Background(), serveManyOptionsKey{}, serveOpts) + abireq, abibody, err := fastly.BodyDownstreamGet() if err != nil { panic(fmt.Errorf("get client handles: %w", err)) } - serve(h, abireq, abibody) + serve(ctx, h, abireq, abibody) // Serve the rest var requests int @@ -96,7 +98,7 @@ func ServeMany(h HandlerFunc, serveOpts *ServeManyOptions) { panic(fmt.Errorf("get client handles: %w", err)) } - serve(h, abireq, abibody) + serve(ctx, h, abireq, abibody) } // wait for any stale-while-revalidate goroutines to complete. @@ -122,3 +124,16 @@ type HandlerFunc func(ctx context.Context, w ResponseWriter, r *Request) func (f HandlerFunc) ServeHTTP(ctx context.Context, w ResponseWriter, r *Request) { f(ctx, w, r) } + +type serveManyOptionsKey struct{} + +// ServeManyOptionsFromContext retrieves the [ServeManyOptions] value +// passed into [ServeMany]. It will return nil if [Serve] or +// [ServeFunc] were used. +func ServeManyOptionsFromContext(ctx context.Context) *ServeManyOptions { + opts, ok := ctx.Value(serveManyOptionsKey{}).(*ServeManyOptions) + if !ok { + return nil + } + return opts +} diff --git a/integration_tests/request_downstream/main_test.go b/integration_tests/request_downstream/main_test.go index b8b506d8..0d0c4179 100644 --- a/integration_tests/request_downstream/main_test.go +++ b/integration_tests/request_downstream/main_test.go @@ -20,7 +20,9 @@ func TestDownstreamRequest(t *testing.T) { // Viceroy constructs a simple GET http://example.com request with // the remote address being 127.0.0.1, so that's what we check for // here. + handlerRun := false fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) { + handlerRun = true if r.Method != "GET" { t.Errorf("Method = %s, want GET", r.Method) return @@ -44,6 +46,9 @@ func TestDownstreamRequest(t *testing.T) { return } }) + if !handlerRun { + t.Errorf("handler was not run") + } } func TestDownstreamResponse(t *testing.T) {