From b8847ac8ce246161d0ab69ae520aed6a8cb8db86 Mon Sep 17 00:00:00 2001 From: Richard Lavoie Date: Tue, 30 Sep 2025 15:23:27 -0400 Subject: [PATCH 1/6] fix: Allow for itests components to be deferred after init --- cmd/svcinit/main.go | 11 ++- private/itest.bzl | 5 +- runner/runner.go | 15 ++- svcctl/svcctl.go | 60 ++++++++++-- svclib/types.go | 1 + tests/MODULE.bazel | 5 +- tests/deferred/BUILD.bazel | 41 ++++++++ tests/deferred/deferred_service.go | 95 +++++++++++++++++++ tests/deferred/start_deferred_service_test.go | 84 ++++++++++++++++ tests/go.mod | 2 +- tests/svcctl/BUILD.bazel | 9 +- tests/svcctl/client.go | 93 ++++++++++++++++++ 12 files changed, 405 insertions(+), 16 deletions(-) create mode 100644 tests/deferred/BUILD.bazel create mode 100644 tests/deferred/deferred_service.go create mode 100644 tests/deferred/start_deferred_service_test.go create mode 100644 tests/svcctl/client.go diff --git a/cmd/svcinit/main.go b/cmd/svcinit/main.go index 738cac9..0386608 100644 --- a/cmd/svcinit/main.go +++ b/cmd/svcinit/main.go @@ -216,7 +216,7 @@ func main() { fmt.Println(err) } else { timeoutVal -= int(math.Ceil(testStartTime.Sub(start).Seconds())) - testCmd.Env = append(testCmd.Env, "TEST_TIMEOUT=" + strconv.Itoa(timeoutVal)) + testCmd.Env = append(testCmd.Env, "TEST_TIMEOUT="+strconv.Itoa(timeoutVal)) } } @@ -554,7 +554,14 @@ func buildTestEnv(ports svclib.Ports) ([]string, error) { panic(err) } - replacements := make([]Replacement, 0, len(ports)) + tmpDir := os.Getenv("TMPDIR") + socketDir := os.Getenv("SOCKET_DIR") + + replacements := make([]Replacement, 0, 2+len(ports)) + replacements = append(replacements, + Replacement{Old: "$${TMPDIR}", New: tmpDir}, + Replacement{Old: "$${SOCKET_DIR}", New: socketDir}, + ) for label, port := range ports { replacements = append(replacements, Replacement{ Old: "$${" + label + "}", diff --git a/private/itest.bzl b/private/itest.bzl index ab30687..329a06f 100644 --- a/private/itest.bzl +++ b/private/itest.bzl @@ -138,7 +138,6 @@ def _itest_binary_impl(ctx, extra_service_spec_kwargs, extra_exe_runfiles = []): for arg in ctx.attr.args ] - if version_file: extra_service_spec_kwargs["version_file"] = to_rlocation_path(ctx, version_file) @@ -193,6 +192,7 @@ def _itest_service_impl(ctx): "http_health_check_address": ctx.attr.http_health_check_address, "autoassign_port": ctx.attr.autoassign_port, "so_reuseport_aware": ctx.attr.so_reuseport_aware, + "deferred": ctx.attr.deferred, "named_ports": ctx.attr.named_ports, "hot_reloadable": ctx.attr.hot_reloadable, "expected_start_duration": ctx.attr.expected_start_duration, @@ -246,6 +246,9 @@ _itest_service_attrs = _itest_binary_attrs | { Must only be set when `autoassign_port` is enabled or `named_ports` are used.""", ), + "deferred": attr.bool( + doc = """If set, the service manager will not be start on boot up. It can be started using the service manager's control API.""", + ), "expected_start_duration": attr.string( default = "0s", doc = "How long the service expected to take before passing a healthcheck. Any failing health checks before this duration elapses will not be logged.", diff --git a/runner/runner.go b/runner/runner.go index 8a8f61f..be0953d 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -57,6 +57,11 @@ func (r *Runner) StartAll(serviceErrCh chan error) ([]topological.Task, error) { return nil } + if service.Deferred { + log.Printf("Deferring %s\n", colorize(service.VersionedServiceSpec)) + return nil + } + if terseOutput { log.Printf("Starting %s\n", colorize(service.VersionedServiceSpec)) } else { @@ -86,6 +91,10 @@ func (r *Runner) StartAll(serviceErrCh chan error) ([]topological.Task, error) { continue } + if service.Deferred { + continue + } + // TODO(zbarsky): Can remove the loop var once Go is sufficiently upgraded. go func(service *ServiceInstance) { err := service.Wait() @@ -100,7 +109,7 @@ func (r *Runner) StartAll(serviceErrCh chan error) ([]topological.Task, error) { func (r *Runner) StopAll() (map[string]*os.ProcessState, error) { tasks := allTasks(r.serviceInstances, func(ctx context.Context, service *ServiceInstance) error { - if service.Type == "group" { + if service.Type == "group" || service.Deferred { return nil } log.Printf("Stopping %s\n", colorize(service.VersionedServiceSpec)) @@ -113,7 +122,7 @@ func (r *Runner) StopAll() (map[string]*os.ProcessState, error) { states := make(map[string]*os.ProcessState) for _, serviceInstance := range r.serviceInstances { - if serviceInstance.Type == "group" { + if serviceInstance.Type == "group" || serviceInstance.Deferred { continue } states[serviceInstance.Label] = serviceInstance.ProcessState() @@ -265,7 +274,7 @@ func initializeServiceCmd(ctx context.Context, instance *ServiceInstance) error // Even if a child process exits, Wait will block until the I/O pipes are closed. // They may have been forwarded to an orphaned child, so we disable that behavior to unblock exit. - if s.Type == "service" { + if s.Type == "service" && !s.Deferred { // We need a bit of grace period to allow I/O pipes to close on our end. cmd.WaitDelay = 50 * time.Millisecond } diff --git a/svcctl/svcctl.go b/svcctl/svcctl.go index 30a1de1..0681c94 100644 --- a/svcctl/svcctl.go +++ b/svcctl/svcctl.go @@ -4,7 +4,9 @@ package svcctl import ( "context" + "errors" "fmt" + "log" "net" "net/http" "os/exec" @@ -58,6 +60,10 @@ func handleHealthCheck(ctx context.Context, r *runner.Runner, _ chan error, w ht w.WriteHeader(http.StatusOK) } +func colorize(s svclib.VersionedServiceSpec) string { + return s.Colorize(s.Label) +} + func handleStart(ctx context.Context, r *runner.Runner, serviceErrCh chan error, w http.ResponseWriter, req *http.Request) { s, status, err := getService(r, req) if err != nil { @@ -65,22 +71,59 @@ func handleStart(ctx context.Context, r *runner.Runner, serviceErrCh chan error, return } + if s.Deferred { + // make sure all the deferred dependencies are started + for _, dep := range s.Deps { + depService := r.GetInstance(dep) + if depService == nil { + http.Error(w, fmt.Sprintf("dependency %q not found", dep), http.StatusInternalServerError) + return + } + + if depService.Deferred { + continue + } + + depsErr := s.WaitUntilHealthy(ctx) + if depsErr != nil { + http.Error(w, fmt.Sprintf("Failed to wait for %q until healthy", dep), http.StatusInternalServerError) + } + } + } + + log.Printf("Starting %s\n", colorize(s.VersionedServiceSpec)) + err = s.Start(ctx) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + errChan := make(chan error, 1) + // NOTE: it is important to wait here because we started the service without using `StartAll`, // which waits for processes to prevent them from turning into zombies. go func() { - err := s.Wait() - if err != nil && !s.Killed() { - serviceErrCh <- fmt.Errorf(s.Colorize(s.Label) + " exited with error: " + err.Error()) + if s.Deferred { + log.Printf("Waiting for %s to be healthy\n", colorize(s.VersionedServiceSpec)) + errChan <- s.WaitUntilHealthy(ctx) + } else { + waitErr := s.Wait() + if waitErr != nil && !s.Killed() { + serviceErrCh <- fmt.Errorf(s.Colorize(s.Label) + " exited with error: " + err.Error()) + } + errChan <- waitErr } }() - w.WriteHeader(http.StatusOK) + chanErr := <-errChan + if chanErr == nil { + w.WriteHeader(http.StatusOK) + w.Write([]byte("0")) + return + } + + http.Error(w, err.Error(), http.StatusInternalServerError) } func handleKill(ctx context.Context, r *runner.Runner, _ chan error, w http.ResponseWriter, req *http.Request) { @@ -120,6 +163,9 @@ func handleWait(ctx context.Context, r *runner.Runner, _ chan error, w http.Resp } params := req.URL.Query() + + log.Printf("All params : %v\n", params) + timeout := params.Get("timeout") var t time.Duration if timeout != "" { @@ -147,9 +193,11 @@ func handleWait(ctx context.Context, r *runner.Runner, _ chan error, w http.Resp w.Write([]byte("0")) return } - if err, ok := err.(*exec.ExitError); ok { + + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { w.WriteHeader(http.StatusOK) - w.Write([]byte(fmt.Sprintf("%d", err.ExitCode()))) + w.Write([]byte(fmt.Sprintf("%d", exitErr.ExitCode()))) return } http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/svclib/types.go b/svclib/types.go index 29275e1..c574679 100644 --- a/svclib/types.go +++ b/svclib/types.go @@ -21,6 +21,7 @@ type ServiceSpec struct { Deps []string `json:"deps"` AutoassignPort bool `json:"autoassign_port"` SoReuseportAware bool `json:"so_reuseport_aware"` + Deferred bool `json:"deferred"` NamedPorts []string `json:"named_ports"` HotReloadable bool `json:"hot_reloadable"` PortAliases map[string]string `json:"port_aliases"` diff --git a/tests/MODULE.bazel b/tests/MODULE.bazel index 78a8280..090c6ab 100644 --- a/tests/MODULE.bazel +++ b/tests/MODULE.bazel @@ -9,9 +9,10 @@ bazel_dep(name = "aspect_rules_js", version = "1.39.0") bazel_dep(name = "bazel_skylib", version = "1.7.1") bazel_dep(name = "rules_go", version = "0.51.0") bazel_dep(name = "rules_shell", version = "0.5.0") -bazel_dep(name = "gazelle", version = "0.37.0") -bazel_dep(name = "platforms", version = "0.0.10") +bazel_dep(name = "gazelle", version = "0.40.0") +bazel_dep(name = "platforms", version = "0.0.11") bazel_dep(name = "hermetic_cc_toolchain", version = "3.1.0") + toolchains = use_extension("@hermetic_cc_toolchain//toolchain:ext.bzl", "toolchains") use_repo(toolchains, "zig_sdk") diff --git a/tests/deferred/BUILD.bazel b/tests/deferred/BUILD.bazel new file mode 100644 index 0000000..954f163 --- /dev/null +++ b/tests/deferred/BUILD.bazel @@ -0,0 +1,41 @@ +load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test") +load("@rules_itest//:itest.bzl", "itest_service", "service_test") + +go_library( + name = "deferred_lib", + srcs = ["deferred_service.go"], + importpath = "rules_itest/tests/deferred", + visibility = ["//visibility:private"], +) + +go_binary( + name = "deferred", + embed = [":deferred_lib"], + visibility = ["//visibility:public"], +) + +go_test( + name = "deferred_test", + srcs = ["start_deferred_service_test.go"], + embed = [":deferred_lib"], + tags = ["manual"], + deps = ["//svcctl"], +) + +service_test( + name = "deferred_service_test", + services = [":deferred_itest_service"], + test = ":deferred_test", +) + +itest_service( + name = "deferred_itest_service", + args = [ + "$${PORT}", + ], + autoassign_port = True, + deferred = True, + exe = ":deferred", + http_health_check_address = "http://localhost:$${PORT}/healthz", + tags = ["requires-network"], +) diff --git a/tests/deferred/deferred_service.go b/tests/deferred/deferred_service.go new file mode 100644 index 0000000..79f3195 --- /dev/null +++ b/tests/deferred/deferred_service.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "encoding/json" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "sync" + "syscall" + "time" +) + +var ( + mu sync.RWMutex + value string +) + +func main() { + mux := http.NewServeMux() + mux.HandleFunc("/healthz", healthHandler) + mux.HandleFunc("/update", updateHandler) + mux.HandleFunc("/value", valueHandler) + + port, err := strconv.ParseInt(os.Args[1], 10, 64) + if err != nil { + log.Fatalf("Invalid port: %v", err) + } + + server := &http.Server{ + Addr: "0.0.0.0:" + strconv.FormatInt(port, 10), + Handler: mux, + } + + // Listen for SIGTERM for graceful shutdown + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + go func() { + log.Println("Server starting on :" + strconv.FormatInt(port, 10)) + if err := server.ListenAndServe(); err != http.ErrServerClosed { + log.Fatalf("ListenAndServe: %v", err) + } + }() + + <-stop + log.Println("Shutting down...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server Shutdown Failed:%+v", err) + } + log.Println("Server exited gracefully") +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`ok`)) +} + +func updateHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) + return + } + var payload struct { + Value string `json:"value"` + } + err := json.NewDecoder(r.Body).Decode(&payload) + if err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + mu.Lock() + value = payload.Value + mu.Unlock() + w.WriteHeader(http.StatusOK) + w.Write([]byte(`updated`)) +} + +func valueHandler(w http.ResponseWriter, r *http.Request) { + mu.RLock() + defer mu.RUnlock() + resp := struct { + Value string `json:"value"` + }{ + Value: value, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} \ No newline at end of file diff --git a/tests/deferred/start_deferred_service_test.go b/tests/deferred/start_deferred_service_test.go new file mode 100644 index 0000000..53bfbb1 --- /dev/null +++ b/tests/deferred/start_deferred_service_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "testing" + + "github.com/dzbarsky/rules_itest/tests/svcctl" +) + +type payload struct { + Value string `json:"value"` +} + +func getPort(service string) (*string, error) { + cmd := exec.Command(os.Getenv("GET_ASSIGNED_PORT_BIN"), service) + port, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("Failed to get port: %v", err) + } + var portStr = string(port) + return &portStr, nil +} + +func TestStartDeferredService(t *testing.T) { + port := os.Getenv("SVCCTL_PORT") + if port == "" { + t.Errorf("SVCCTL_PORT not set") + } + + client := svcctl.NewSvcctlClient("http://localhost:" + port, http.DefaultClient) + + log.Println("Starting deferred service...") + err := client.StartService(context.Background(), "@@//deferred:deferred_itest_service", true) + if err != nil { + t.Errorf("Failed to start deferred service: %v", err) + } + + log.Println("Getting port for deferred service...") + servicePort, err := getPort("@@//deferred:deferred_itest_service") + if err != nil { + t.Errorf("Failed to get port for deferred service: %v", err) + } + + v := &payload{ + Value: "test", + } + + data, _ := json.Marshal(v) + + log.Printf("Deferred service is running on port %s", *servicePort) + _, err = http.Post("http://localhost:" + *servicePort + "/update", "text/plain", bytes.NewReader(data)) + if err != nil { + t.Errorf("Failed to call update endpoint: %v", err) + return + } + + log.Println("Calling /value endpoint...") + resp, err := http.Get("http://localhost:" + *servicePort + "/value") + if err != nil { + t.Errorf("Failed to call update endpoint: %v", err) + return + } + + bodyContent, err := io.ReadAll(resp.Body) + + var result payload + err = json.Unmarshal(bodyContent, &result) + if err != nil { + t.Errorf("Failed to read response body: %v", err) + } + + if result.Value != "test" { + t.Errorf("Got response %q, want %q", string(data), "test") + } + +} \ No newline at end of file diff --git a/tests/go.mod b/tests/go.mod index 5161e2a..61a75fb 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -4,5 +4,5 @@ go 1.22.0 require ( github.com/bazelbuild/rules_go v0.51.0 - golang.org/x/sys v0.24.0 + golang.org/x/sys v0.26.0 ) diff --git a/tests/svcctl/BUILD.bazel b/tests/svcctl/BUILD.bazel index 093bdad..8dcf4bb 100644 --- a/tests/svcctl/BUILD.bazel +++ b/tests/svcctl/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_go//go:def.bzl", "go_test") +load("@rules_go//go:def.bzl", "go_library", "go_test") load("@rules_itest//:itest.bzl", "service_test") go_test( @@ -14,3 +14,10 @@ service_test( ], test = "_svcctl_test", ) + +go_library( + name = "svcctl", + srcs = ["client.go"], + importpath = "github.com/dzbarsky/rules_itest/tests/svcctl", + visibility = ["//visibility:public"], +) diff --git a/tests/svcctl/client.go b/tests/svcctl/client.go new file mode 100644 index 0000000..075579b --- /dev/null +++ b/tests/svcctl/client.go @@ -0,0 +1,93 @@ +package svcctl + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" +) + +type SvcctlClient struct { + baseURL string + httpClient *http.Client +} + +func NewSvcctlClient(baseURL string, client *http.Client) *SvcctlClient { + return &SvcctlClient{ + baseURL: baseURL, + httpClient: client, + } +} + +func (c *SvcctlClient) StartService(ctx context.Context, service string, waitForHealthy bool) error { + q := url.Values{} + q.Set("service", service) + if waitForHealthy { + q.Set("wait_for_healthy", "1") + } else { + q.Set("wait_for_healthy", "0") + } + + log.Printf(c.baseURL+"/v0/start?" + q.Encode()) + + req, err := http.NewRequest(http.MethodGet, c.baseURL+"/v0/start?" + q.Encode(), nil) + if err != nil { + return err + } + + req.WithContext(ctx) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("Failed to start speedy service: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Got status code %d, want %d", resp.StatusCode, http.StatusOK) + } + + return nil +} + +func (c *SvcctlClient) WaitForService(ctx context.Context, service string) error { + q := url.Values{} + q.Set("service", service) + + req, err := http.NewRequest(http.MethodGet, c.baseURL+"/v0/wait?" + q.Encode(), nil) + if err != nil { + return err + } + + req.WithContext(ctx) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("Failed to start speedy service: %v", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("Got status code %d, want %d", resp.StatusCode, http.StatusOK) + } + + return nil +} + +func (c *SvcctlClient) HealthCheck(ctx context.Context, service string) (int, error) { + q := url.Values{} + q.Set("service", service) + + req, err := http.NewRequest(http.MethodGet, c.baseURL+"/v0/healthcheck?" + q.Encode(), nil) + if err != nil { + return -1, err + } + + req.WithContext(ctx) + + resp, err := c.httpClient.Do(req) + if err != nil { + return -1, err + } + + return resp.StatusCode, nil +} \ No newline at end of file From 11ebca012fca5982852548737b8cb3e84b6203f5 Mon Sep 17 00:00:00 2001 From: Richard Lavoie Date: Thu, 9 Oct 2025 00:32:55 -0400 Subject: [PATCH 2/6] fix test --- svcctl/svcctl.go | 24 ++++--------------- tests/deferred/start_deferred_service_test.go | 13 ++++++++++ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/svcctl/svcctl.go b/svcctl/svcctl.go index 0681c94..1345bca 100644 --- a/svcctl/svcctl.go +++ b/svcctl/svcctl.go @@ -99,31 +99,17 @@ func handleStart(ctx context.Context, r *runner.Runner, serviceErrCh chan error, return } - errChan := make(chan error, 1) - // NOTE: it is important to wait here because we started the service without using `StartAll`, // which waits for processes to prevent them from turning into zombies. go func() { - if s.Deferred { - log.Printf("Waiting for %s to be healthy\n", colorize(s.VersionedServiceSpec)) - errChan <- s.WaitUntilHealthy(ctx) - } else { - waitErr := s.Wait() - if waitErr != nil && !s.Killed() { - serviceErrCh <- fmt.Errorf(s.Colorize(s.Label) + " exited with error: " + err.Error()) - } - errChan <- waitErr + waitErr := s.Wait() + + if waitErr != nil && !s.Killed() { + serviceErrCh <- fmt.Errorf(s.Colorize(s.Label) + " exited with error: " + err.Error()) } }() - chanErr := <-errChan - if chanErr == nil { - w.WriteHeader(http.StatusOK) - w.Write([]byte("0")) - return - } - - http.Error(w, err.Error(), http.StatusInternalServerError) + w.WriteHeader(http.StatusOK) } func handleKill(ctx context.Context, r *runner.Runner, _ chan error, w http.ResponseWriter, req *http.Request) { diff --git a/tests/deferred/start_deferred_service_test.go b/tests/deferred/start_deferred_service_test.go index 53bfbb1..542a81b 100644 --- a/tests/deferred/start_deferred_service_test.go +++ b/tests/deferred/start_deferred_service_test.go @@ -11,6 +11,7 @@ import ( "os" "os/exec" "testing" + "time" "github.com/dzbarsky/rules_itest/tests/svcctl" ) @@ -43,6 +44,18 @@ func TestStartDeferredService(t *testing.T) { t.Errorf("Failed to start deferred service: %v", err) } + for { + code, err := client.HealthCheck(context.Background(), "@@//deferred:deferred_itest_service") + + if err != nil { + t.Errorf("Failed to health check deferred service: %v", err) + } + if code == http.StatusOK { + break + } + time.Sleep(200 * time.Millisecond) + } + log.Println("Getting port for deferred service...") servicePort, err := getPort("@@//deferred:deferred_itest_service") if err != nil { From eafe5db6c69e13e9a68b63e35ef0b62643676ffa Mon Sep 17 00:00:00 2001 From: Richard Lavoie Date: Sun, 12 Oct 2025 19:34:06 -0400 Subject: [PATCH 3/6] more cleanup --- svcctl/svcctl.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/svcctl/svcctl.go b/svcctl/svcctl.go index 1345bca..b54771d 100644 --- a/svcctl/svcctl.go +++ b/svcctl/svcctl.go @@ -103,7 +103,6 @@ func handleStart(ctx context.Context, r *runner.Runner, serviceErrCh chan error, // which waits for processes to prevent them from turning into zombies. go func() { waitErr := s.Wait() - if waitErr != nil && !s.Killed() { serviceErrCh <- fmt.Errorf(s.Colorize(s.Label) + " exited with error: " + err.Error()) } @@ -149,9 +148,6 @@ func handleWait(ctx context.Context, r *runner.Runner, _ chan error, w http.Resp } params := req.URL.Query() - - log.Printf("All params : %v\n", params) - timeout := params.Get("timeout") var t time.Duration if timeout != "" { From 2e0144c4dc4d402256b8ebf59869470d495eec1a Mon Sep 17 00:00:00 2001 From: Richard Lavoie Date: Sun, 9 Nov 2025 13:26:26 -0500 Subject: [PATCH 4/6] Add non-deferred to deferred check at analyais time --- private/itest.bzl | 8 +++++++- tests/deferred/BUILD.bazel | 21 ++++++++++++++++++++- tests/deferred/tests.bzl | 20 ++++++++++++++++++++ tests/go.sum | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/deferred/tests.bzl diff --git a/private/itest.bzl b/private/itest.bzl index bafc37d..1dac38e 100644 --- a/private/itest.bzl +++ b/private/itest.bzl @@ -41,6 +41,7 @@ load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo") _ServiceGroupInfo = provider( doc = "Info about a service group", fields = { + "deferred": "Flag if this service should be deferred or not", "services": "Dict of services/tasks", }, ) @@ -123,6 +124,11 @@ def _compute_env(ctx, underlying_target): return env def _itest_binary_impl(ctx, extra_service_spec_kwargs, extra_exe_runfiles = []): + if not ctx.attr.deferred: + for dep in ctx.attr.deps: + if dep[_ServiceGroupInfo].deferred: + fail("Non-deferred itest_service cannot depend on deferred itest_service: %s depends on %s" % (ctx.label, dep.label)) + exe_runfiles = [ctx.attr.exe.default_runfiles] + extra_exe_runfiles version_file_deps = ctx.files.data + ctx.files.exe @@ -165,7 +171,7 @@ def _itest_binary_impl(ctx, extra_service_spec_kwargs, extra_exe_runfiles = []): return [ RunEnvironmentInfo(environment = _run_environment(ctx, service_specs_file)), DefaultInfo(runfiles = runfiles), - _ServiceGroupInfo(services = services), + _ServiceGroupInfo(services = services, deferred = ctx.attr.deferred), ] def _validate_duration(name, s): diff --git a/tests/deferred/BUILD.bazel b/tests/deferred/BUILD.bazel index 954f163..be8c057 100644 --- a/tests/deferred/BUILD.bazel +++ b/tests/deferred/BUILD.bazel @@ -1,5 +1,8 @@ load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test") -load("@rules_itest//:itest.bzl", "itest_service", "service_test") +load("@rules_itest//:itest.bzl", "itest_service", "itest_task", "service_test") +load(":tests.bzl", "tests") + +tests() go_library( name = "deferred_lib", @@ -39,3 +42,19 @@ itest_service( http_health_check_address = "http://localhost:$${PORT}/healthz", tags = ["requires-network"], ) + +itest_service( + name = "deferred_task", + deferred = True, + exe = "@rules_itest//:exit0", + hygienic = False, +) + +itest_service( + name = "non_deferred_depends_on_deferred_should_fail", + exe = "@rules_itest//:exit0", + tags = ["manual"], + deps = [ + ":deferred_task", + ], +) diff --git a/tests/deferred/tests.bzl b/tests/deferred/tests.bzl new file mode 100644 index 0000000..7055fb4 --- /dev/null +++ b/tests/deferred/tests.bzl @@ -0,0 +1,20 @@ +load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") + +def _failure_testing_test(ctx): + """Test to verify that an analysis test may verify a rule fails with fail().""" + env = analysistest.begin(ctx) + + asserts.expect_failure(env, "Non-deferred itest_service cannot depend on deferred itest_service: @@//deferred:non_deferred_depends_on_deferred_should_fail depends on @@//deferred:deferred_task") + + return analysistest.end(env) + +failure_testing_test = analysistest.make( + _failure_testing_test, + expect_failure = True, +) + +def tests(): + failure_testing_test( + name = "test_non_deferred_depends_on_deferred_should_fail", + target_under_test = ":non_deferred_depends_on_deferred_should_fail", + ) diff --git a/tests/go.sum b/tests/go.sum index d94d1cc..77f16f7 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -2,3 +2,4 @@ github.com/bazelbuild/rules_go v0.51.0 h1:og6AqW7T4uFgcySRYn/EFg5VUHR2KY7jypYmVe github.com/bazelbuild/rules_go v0.51.0/go.mod h1:+jnXOJJO4C+WYH5v1v0SsPTncQ9sHGsCrAOgrflqSUE= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= From 2d554c0f365b28d5bc044f10620939e031a02d79 Mon Sep 17 00:00:00 2001 From: Richard Lavoie Date: Sun, 9 Nov 2025 13:31:24 -0500 Subject: [PATCH 5/6] fix comment message --- svcctl/svcctl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svcctl/svcctl.go b/svcctl/svcctl.go index f7b51c6..3ac5099 100644 --- a/svcctl/svcctl.go +++ b/svcctl/svcctl.go @@ -72,7 +72,7 @@ func handleStart(ctx context.Context, r *runner.Runner, serviceErrCh chan error, } if s.Deferred { - // make sure all the deferred dependencies are started + // make sure all the non-deferred dependencies are started for _, dep := range s.Deps { depService := r.GetInstance(dep) if depService == nil { From eb5b51e6dc41111817ba928405bd912af32c8571 Mon Sep 17 00:00:00 2001 From: Richard Lavoie Date: Tue, 11 Nov 2025 08:15:33 -0500 Subject: [PATCH 6/6] fix deferred for tasks --- private/itest.bzl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/private/itest.bzl b/private/itest.bzl index b26fde1..c27f86a 100644 --- a/private/itest.bzl +++ b/private/itest.bzl @@ -124,7 +124,7 @@ def _compute_env(ctx, underlying_target): return env def _itest_binary_impl(ctx, extra_service_spec_kwargs, extra_exe_runfiles = []): - if not ctx.attr.deferred: + if hasattr(ctx.attr, "deferred") and not ctx.attr.deferred: for dep in ctx.attr.deps: if dep[_ServiceGroupInfo].deferred: fail("Non-deferred itest_service cannot depend on deferred itest_service: %s depends on %s" % (ctx.label, dep.label)) @@ -171,7 +171,7 @@ def _itest_binary_impl(ctx, extra_service_spec_kwargs, extra_exe_runfiles = []): return [ RunEnvironmentInfo(environment = _run_environment(ctx, service_specs_file)), DefaultInfo(runfiles = runfiles), - _ServiceGroupInfo(services = services, deferred = ctx.attr.deferred), + _ServiceGroupInfo(services = services, deferred = getattr(ctx.attr, "deferred", False)), ] def _validate_duration(name, s): @@ -355,7 +355,7 @@ def _itest_service_group_impl(ctx): return [ RunEnvironmentInfo(environment = _run_environment(ctx, service_specs_file)), DefaultInfo(runfiles = runfiles), - _ServiceGroupInfo(services = services), + _ServiceGroupInfo(services = services, deferred = False), ] _itest_service_group_attrs = _svcinit_attrs | {