diff --git a/Justfile b/Justfile index 25a739b602..d7ff4a3e63 100644 --- a/Justfile +++ b/Justfile @@ -114,7 +114,7 @@ integration-tests *test: #!/bin/bash set -euo pipefail testName=${1:-} - go test -fullpath -count 1 -v -tags integration -run "$testName" ./... + go test -fullpath -count 1 -v -tags integration -run "$testName" -p 1 ./... # Run README doc tests test-readme *args: diff --git a/backend/controller/controller.go b/backend/controller/controller.go index b379d75cf0..46e96487c3 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -26,6 +26,7 @@ import ( "github.com/jellydator/ttlcache/v3" "github.com/jpillora/backoff" "golang.org/x/exp/maps" + "golang.org/x/sync/errgroup" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -45,6 +46,7 @@ import ( cf "github.com/TBD54566975/ftl/common/configuration" frontend "github.com/TBD54566975/ftl/frontend" "github.com/TBD54566975/ftl/internal/cors" + ftlhttp "github.com/TBD54566975/ftl/internal/http" "github.com/TBD54566975/ftl/internal/log" ftlmaps "github.com/TBD54566975/ftl/internal/maps" "github.com/TBD54566975/ftl/internal/model" @@ -67,6 +69,7 @@ type CommonConfig struct { type Config struct { Bind *url.URL `help:"Socket to bind to." default:"http://localhost:8892" env:"FTL_CONTROLLER_BIND"` + IngressBind *url.URL `help:"Socket to bind to for ingress." default:"http://localhost:8891" env:"FTL_CONTROLLER_INGRESS_BIND"` Key model.ControllerKey `help:"Controller key (auto)."` DSN string `help:"DAL DSN." default:"postgres://localhost:54320/ftl?sslmode=disable&user=postgres&password=secret" env:"FTL_CONTROLLER_DSN"` Advertise *url.URL `help:"Endpoint the Controller should advertise (must be unique across the cluster, defaults to --bind if omitted)." env:"FTL_CONTROLLER_ADVERTISE"` @@ -128,18 +131,26 @@ func Start(ctx context.Context, config Config, runnerScaling scaling.RunnerScali console := NewConsoleService(dal) - ingressHandler := http.StripPrefix("/ingress", svc) + ingressHandler := http.Handler(svc) if len(config.AllowOrigins) > 0 { ingressHandler = cors.Middleware(slices.Map(config.AllowOrigins, func(u *url.URL) string { return u.String() }), ingressHandler) } - return rpc.Serve(ctx, config.Bind, - rpc.GRPC(ftlv1connect.NewVerbServiceHandler, svc), - rpc.GRPC(ftlv1connect.NewControllerServiceHandler, svc), - rpc.GRPC(pbconsoleconnect.NewConsoleServiceHandler, console), - rpc.HTTP("/ingress/", ingressHandler), - rpc.HTTP("/", consoleHandler), - ) + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return ftlhttp.Serve(ctx, config.IngressBind, ingressHandler) + }) + + g.Go(func() error { + return rpc.Serve(ctx, config.Bind, + rpc.GRPC(ftlv1connect.NewVerbServiceHandler, svc), + rpc.GRPC(ftlv1connect.NewControllerServiceHandler, svc), + rpc.GRPC(pbconsoleconnect.NewConsoleServiceHandler, console), + rpc.HTTP("/", consoleHandler), + ) + }) + + return g.Wait() } var _ ftlv1connect.ControllerServiceHandler = (*Service)(nil) diff --git a/cmd/ftl/cmd_serve.go b/cmd/ftl/cmd_serve.go index 27bfd9e1d2..8beb4f5f1a 100644 --- a/cmd/ftl/cmd_serve.go +++ b/cmd/ftl/cmd_serve.go @@ -31,6 +31,7 @@ import ( type serveCmd struct { Bind *url.URL `help:"Starting endpoint to bind to and advertise to. Each controller and runner will increment the port by 1" default:"http://localhost:8892"` + IngressBind *url.URL `help:"Starting endpoint to bind to for http ingress" default:"http://localhost:8891"` DBPort int `help:"Port to use for the database." default:"54320"` Recreate bool `help:"Recreate the database even if it already exists." default:"false"` Controllers int `short:"c" help:"Number of controllers to start." default:"1"` @@ -101,6 +102,7 @@ func (s *serveCmd) Run(ctx context.Context) error { config := controller.Config{ CommonConfig: s.CommonConfig, Bind: controllerAddresses[i], + IngressBind: s.IngressBind, Key: model.NewLocalControllerKey(i), DSN: dsn, } diff --git a/frontend/src/features/verbs/verb.utils.ts b/frontend/src/features/verbs/verb.utils.ts index b5b6fac265..412a52a39d 100644 --- a/frontend/src/features/verbs/verb.utils.ts +++ b/frontend/src/features/verbs/verb.utils.ts @@ -3,7 +3,7 @@ import { Module, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' import { MetadataCalls, MetadataCronJob, MetadataIngress, Ref } from '../../protos/xyz/block/ftl/v1/schema/schema_pb' import { JSONSchemaFaker } from 'json-schema-faker' -const basePath = 'http://localhost:8892/ingress/' +const basePath = 'http://localhost:8891/' export const verbRefString = (verb: Ref): string => { return `${verb.module}.${verb.name}` diff --git a/integration/actions.go b/integration/actions.go index 0c96b1eadf..fec8777ee1 100644 --- a/integration/actions.go +++ b/integration/actions.go @@ -352,7 +352,7 @@ func JsonData(t testing.TB, body interface{}) []byte { func HttpCall(method string, path string, body []byte, onResponse func(t testing.TB, resp *HTTPResponse)) Action { return func(t testing.TB, ic TestContext) { Infof("HTTP %s %s", method, path) - baseURL, err := url.Parse(fmt.Sprintf("http://localhost:8892/ingress")) + baseURL, err := url.Parse(fmt.Sprintf("http://localhost:8891")) assert.NoError(t, err) r, err := http.NewRequestWithContext(ic, method, baseURL.JoinPath(path).String(), bytes.NewReader(body)) diff --git a/internal/http/server.go b/internal/http/server.go new file mode 100644 index 0000000000..eca4ccac80 --- /dev/null +++ b/internal/http/server.go @@ -0,0 +1,45 @@ +package http + +import ( + "context" + "errors" + "net" + "net/http" + "net/url" + "time" + + "github.com/TBD54566975/ftl/internal/log" +) + +const ShutdownGracePeriod = 5 * time.Second + +func Serve(ctx context.Context, listen *url.URL, handler http.Handler) error { + httpServer := &http.Server{ + Addr: listen.Host, + Handler: handler, + ReadHeaderTimeout: 30 * time.Second, + BaseContext: func(_ net.Listener) context.Context { + return ctx + }, + } + + go func() { + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), ShutdownGracePeriod) + defer cancel() + err := httpServer.Shutdown(shutdownCtx) + if err != nil { + if errors.Is(err, context.Canceled) { + _ = httpServer.Close() + return + } + log.FromContext(ctx).Errorf(err, "server shutdown error") + } + }() + + err := httpServer.ListenAndServe() + if errors.Is(err, http.ErrServerClosed) { + return nil + } + return err +}