diff --git a/docs/itest.md b/docs/itest.md index 74374e4..d9f9d05 100644 --- a/docs/itest.md +++ b/docs/itest.md @@ -78,7 +78,7 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio ## itest_service_group
-itest_service_group(name, services) +itest_service_group(name, autoassign_port, named_ports, services, so_reuseport_aware)A service group is a collection of services/tasks. @@ -94,7 +94,10 @@ It can bring up multiple services with a single `bazel run` command, which is us | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | +| autoassign_port | If true, the service manager will pick a free port and assign it to the service. The port will be interpolated into
$${PORT}
in the service's http_health_check_address
and args
. It will also be exported under the target's fully qualified label in the service-port mapping.http_health_check_address
and args
(in case one service needs the address for another one.) For example, the following substitution: args = ["-client-addr", "127.0.0.1:$${@@//label/for:service}"]
ASSIGNED_PORTS
env var. For example, a port can be retrieved with the following JS code: JSON.parse(process.env["ASSIGNED_PORTS"])["@@//label/for:service"]
.PORT=$($GET_ASSIGNED_PORT_BIN @@//label/for:service)
| Boolean | optional | False
|
+| named_ports | For each element of the list, the service manager will pick a free port and assign it to the service. The port's fully-qualified name is the service's fully-qualified label and the port name, separated by a colon. For example, a port assigned with named_ports = ["http_port"]
will be assigned a fully-qualified name of @@//label/for:service:http_port
.autoassign_port
. | List of strings | optional | []
|
| services | Services/tasks that comprise this group. Can be itest_service
, itest_task
, or itest_service_group
. | List of labels | optional | []
|
+| so_reuseport_aware | If set, the service manager will not release the autoassigned port. The service binary must use SO_REUSEPORT when binding it. This reduces the possibility of port collisions when running many service_tests in parallel, or when code binds port 0 without being aware of the port assignment mechanism.autoassign_port
is enabled or named_ports
are used. | Boolean | optional | False
|
@@ -127,7 +130,7 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio
## service_test
-service_test(name, data, env, services, test) +service_test(name, autoassign_port, data, env, named_ports, services, so_reuseport_aware, test)Brings up a set of services/tasks and runs a test target against them. @@ -162,9 +165,12 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this target. | Name | required | | +| autoassign_port | If true, the service manager will pick a free port and assign it to the service. The port will be interpolated into
$${PORT}
in the service's http_health_check_address
and args
. It will also be exported under the target's fully qualified label in the service-port mapping.http_health_check_address
and args
(in case one service needs the address for another one.) For example, the following substitution: args = ["-client-addr", "127.0.0.1:$${@@//label/for:service}"]
ASSIGNED_PORTS
env var. For example, a port can be retrieved with the following JS code: JSON.parse(process.env["ASSIGNED_PORTS"])["@@//label/for:service"]
.PORT=$($GET_ASSIGNED_PORT_BIN @@//label/for:service)
| Boolean | optional | False
|
| data | - | List of labels | optional | []
|
| env | The service manager will merge these variables into the environment when spawning the underlying binary. | Dictionary: String -> String | optional | {}
|
+| named_ports | For each element of the list, the service manager will pick a free port and assign it to the service. The port's fully-qualified name is the service's fully-qualified label and the port name, separated by a colon. For example, a port assigned with named_ports = ["http_port"]
will be assigned a fully-qualified name of @@//label/for:service:http_port
.autoassign_port
. | List of strings | optional | []
|
| services | Services/tasks that comprise this group. Can be itest_service
, itest_task
, or itest_service_group
. | List of labels | optional | []
|
+| so_reuseport_aware | If set, the service manager will not release the autoassigned port. The service binary must use SO_REUSEPORT when binding it. This reduces the possibility of port collisions when running many service_tests in parallel, or when code binds port 0 without being aware of the port assignment mechanism.autoassign_port
is enabled or named_ports
are used. | Boolean | optional | False
|
| test | The underlying test target to execute once the services have been brought up and healthchecked. | Label | optional | None
|
diff --git a/private/itest.bzl b/private/itest.bzl
index 7ebb00e..4af6dbf 100644
--- a/private/itest.bzl
+++ b/private/itest.bzl
@@ -56,7 +56,7 @@ def _run_environment(ctx, service_specs_file):
# Flags
"SVCINIT_ALLOW_CONFIGURING_TMPDIR": str(ctx.attr._allow_configuring_tmpdir[BuildSettingInfo].value),
"SVCINIT_ENABLE_PER_SERVICE_RELOAD": str(ctx.attr._enable_per_service_reload[BuildSettingInfo].value),
- "SVCINIT_KEEP_SERVICES_UP": str(ctx.attr.keep_services_up[BuildSettingInfo].value),
+ "SVCINIT_KEEP_SERVICES_UP": str(ctx.attr._keep_services_up[BuildSettingInfo].value),
"SVCINIT_TERSE_OUTPUT": str(ctx.attr._terse_svcinit_output[BuildSettingInfo].value),
# Other configuration
@@ -217,8 +217,8 @@ def _itest_service_impl(ctx):
return _itest_binary_impl(ctx, extra_service_spec_kwargs, extra_exe_runfiles)
-_itest_service_attrs = _itest_binary_attrs | {
- # Note, autoassigning a port is a little racy. If you can stick to hardcoded ports and network namespace, you should prefer that.
+_port_assignment_attrs = {
+ # Note, autoassigning a port is a little racy. If you can stick to hardcoded ports and network namespace, you should prefer that.
"autoassign_port": attr.bool(
doc = """If true, the service manager will pick a free port and assign it to the service.
The port will be interpolated into `$${PORT}` in the service's `http_health_check_address` and `args`.
@@ -242,6 +242,16 @@ _itest_service_attrs = _itest_binary_attrs | {
Named ports are accessible through the service-port mapping. For more details, see `autoassign_port`.""",
),
+ "so_reuseport_aware": attr.bool(
+ doc = """If set, the service manager will not release the autoassigned port. The service binary must use SO_REUSEPORT when binding it.
+ This reduces the possibility of port collisions when running many service_tests in parallel, or when code binds port 0 without being
+ aware of the port assignment mechanism.
+
+ Must only be set when `autoassign_port` is enabled or `named_ports` are used.""",
+ ),
+}
+
+_itest_service_attrs = _itest_binary_attrs | _port_assignment_attrs | {
"health_check": attr.label(
cfg = "target",
mandatory = False,
@@ -274,13 +284,6 @@ _itest_service_attrs = _itest_binary_attrs | {
This check will be retried until it returns a 200 HTTP code. When used in conjunction with autoassigned ports, `$${@@//label/for:service:port_name}` can be used in the address.
Example: `http_health_check_address = "http://127.0.0.1:$${@@//label/for:service:port_name}",`""",
),
- "so_reuseport_aware": attr.bool(
- doc = """If set, the service manager will not release the autoassigned port. The service binary must use SO_REUSEPORT when binding it.
- This reduces the possibility of port collisions when running many service_tests in parallel, or when code binds port 0 without being
- aware of the port assignment mechanism.
-
- Must only be set when `autoassign_port` is enabled or `named_ports` are used.""",
- ),
}
itest_service = rule(
@@ -309,10 +312,17 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio
def _itest_service_group_impl(ctx):
services = _collect_services(ctx.attr.services)
+
+ if ctx.attr.so_reuseport_aware and not (ctx.attr.autoassign_port or ctx.attr.named_ports):
+ fail("SO_REUSEPORT awareness only makes sense when using port autoassignment")
+
service = struct(
type = "group",
label = str(ctx.label),
deps = [str(service.label) for service in ctx.attr.services],
+ autoassign_port = ctx.attr.autoassign_port,
+ so_reuseport_aware = ctx.attr.so_reuseport_aware,
+ named_ports = ctx.attr.named_ports,
)
services[service.label] = service
@@ -327,12 +337,12 @@ def _itest_service_group_impl(ctx):
_ServiceGroupInfo(services = services),
]
-_itest_service_group_attrs = {
+_itest_service_group_attrs = _port_assignment_attrs | _svcinit_attrs | {
"services": attr.label_list(
providers = [_ServiceGroupInfo],
doc = "Services/tasks that comprise this group. Can be `itest_service`, `itest_task`, or `itest_service_group`.",
),
-} | _svcinit_attrs
+}
itest_service_group = rule(
implementation = _itest_service_group_impl,
diff --git a/runner/service_instance.go b/runner/service_instance.go
index 5ad0852..0c54096 100644
--- a/runner/service_instance.go
+++ b/runner/service_instance.go
@@ -104,14 +104,18 @@ var httpClient = http.Client{
}
func (s *ServiceInstance) HealthCheck(ctx context.Context, expectedStartDuration time.Duration) bool {
- httpHealthCheckReq, _ := http.NewRequestWithContext(ctx, "GET", s.HttpHealthCheckAddress, nil)
coloredLabel := s.Colorize(s.Label)
-
shouldSilence := s.startTime.Add(expectedStartDuration).After(time.Now())
isHealthy := true
var err error
if s.HttpHealthCheckAddress != "" {
+ httpHealthCheckReq, err := http.NewRequestWithContext(ctx, "GET", s.HttpHealthCheckAddress, nil)
+ if err != nil {
+ log.Printf("Failed to construct healthcheck request for %s: %v\n", coloredLabel, err)
+ return false
+ }
+
if !s.HealthcheckAttempted() || !shouldSilence {
log.Printf("HTTP Healthchecking %s (pid %d) : %s\n", coloredLabel, s.Pid(), s.HttpHealthCheckAddress)
}
diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel
index c593b43..59a9666 100644
--- a/tests/BUILD.bazel
+++ b/tests/BUILD.bazel
@@ -112,3 +112,22 @@ itest_service(
exe = "//go_service",
http_health_check_address = "http://127.0.0.1:$${PORT}",
)
+
+# Test port assignment through a group
+itest_service(
+ name = "_speedy_service2",
+ args = [
+ "-port",
+ "$${@@//:speedy_service2:port}",
+ ],
+ env = {"foo": "bar"},
+ exe = "//go_service",
+ http_health_check_address = "http://127.0.0.1:$${@@//:speedy_service2:port}",
+ tags = ["manual"],
+)
+
+itest_service_group(
+ name = "speedy_service2",
+ services = [":_speedy_service"],
+ named_ports = ["port"],
+)
\ No newline at end of file