Skip to content

Commit

Permalink
Add support for 'port_aliases' to service_group
Browse files Browse the repository at this point in the history
  • Loading branch information
dzbarsky committed Sep 23, 2024
1 parent edd8893 commit 9cf8b2d
Show file tree
Hide file tree
Showing 5 changed files with 37 additions and 28 deletions.
11 changes: 11 additions & 0 deletions cmd/svcinit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,17 @@ func assignPorts(
}
}

for label, spec := range serviceSpecs {
for portName, aliasedTo := range spec.PortAliases {
qualifiedPortName := label
if portName != "" {
qualifiedPortName += ":" + portName
}

ports.Set(qualifiedPortName, ports[aliasedTo])
}
}

// Complete hack - we have observed that the ports may not be ready immediately after closing, even with SO_LINGER set to 0.
// Give the kernel a bit of time to figure out what we've done.
time.Sleep(10 * time.Millisecond)
Expand Down
12 changes: 4 additions & 8 deletions docs/itest.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio
## itest_service_group

<pre>
itest_service_group(<a href="#itest_service_group-name">name</a>, <a href="#itest_service_group-autoassign_port">autoassign_port</a>, <a href="#itest_service_group-named_ports">named_ports</a>, <a href="#itest_service_group-services">services</a>, <a href="#itest_service_group-so_reuseport_aware">so_reuseport_aware</a>)
itest_service_group(<a href="#itest_service_group-name">name</a>, <a href="#itest_service_group-port_aliases">port_aliases</a>, <a href="#itest_service_group-services">services</a>)
</pre>

A service group is a collection of services/tasks.
Expand All @@ -94,10 +94,8 @@ It can bring up multiple services with a single `bazel run` command, which is us
| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="itest_service_group-name"></a>name | A unique name for this target. | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required | |
| <a id="itest_service_group-autoassign_port"></a>autoassign_port | If true, the service manager will pick a free port and assign it to the service. The port will be interpolated into <code>$${PORT}</code> in the service's <code>http_health_check_address</code> and <code>args</code>. It will also be exported under the target's fully qualified label in the service-port mapping.<br><br> The assigned ports for all services are available for substitution in <code>http_health_check_address</code> and <code>args</code> (in case one service needs the address for another one.) For example, the following substitution: <code>args = ["-client-addr", "127.0.0.1:$${@@//label/for:service}"]</code><br><br> The service-port mapping is a JSON string -&gt; int map propagated through the <code>ASSIGNED_PORTS</code> env var. For example, a port can be retrieved with the following JS code: <code>JSON.parse(process.env["ASSIGNED_PORTS"])["@@//label/for:service"]</code>.<br><br> Alternately, the env will also contain the location of a binary that can return the port, for contexts without a readily-accessible JSON parser. For example, the following Bash command: <code>PORT=$($GET_ASSIGNED_PORT_BIN @@//label/for:service)</code> | Boolean | optional | <code>False</code> |
| <a id="itest_service_group-named_ports"></a>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 <code>named_ports = ["http_port"]</code> will be assigned a fully-qualified name of <code>@@//label/for:service:http_port</code>.<br><br> Named ports are accessible through the service-port mapping. For more details, see <code>autoassign_port</code>. | List of strings | optional | <code>[]</code> |
| <a id="itest_service_group-port_aliases"></a>port_aliases | Port aliases allow you to 're-export' another service's port as belonging to this service group. This can be used to create abstractions (such as an itest_service combined with an itest_task) but not leak their implementation through how client code accesses port names. | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional | <code>{}</code> |
| <a id="itest_service_group-services"></a>services | Services/tasks that comprise this group. Can be <code>itest_service</code>, <code>itest_task</code>, or <code>itest_service_group</code>. | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[]</code> |
| <a id="itest_service_group-so_reuseport_aware"></a>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.<br><br> Must only be set when <code>autoassign_port</code> is enabled or <code>named_ports</code> are used. | Boolean | optional | <code>False</code> |


<a id="itest_task"></a>
Expand Down Expand Up @@ -130,7 +128,7 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio
## service_test

<pre>
service_test(<a href="#service_test-name">name</a>, <a href="#service_test-autoassign_port">autoassign_port</a>, <a href="#service_test-data">data</a>, <a href="#service_test-env">env</a>, <a href="#service_test-named_ports">named_ports</a>, <a href="#service_test-services">services</a>, <a href="#service_test-so_reuseport_aware">so_reuseport_aware</a>, <a href="#service_test-test">test</a>)
service_test(<a href="#service_test-name">name</a>, <a href="#service_test-data">data</a>, <a href="#service_test-env">env</a>, <a href="#service_test-port_aliases">port_aliases</a>, <a href="#service_test-services">services</a>, <a href="#service_test-test">test</a>)
</pre>

Brings up a set of services/tasks and runs a test target against them.
Expand Down Expand Up @@ -165,12 +163,10 @@ All [common binary attributes](https://bazel.build/reference/be/common-definitio
| Name | Description | Type | Mandatory | Default |
| :------------- | :------------- | :------------- | :------------- | :------------- |
| <a id="service_test-name"></a>name | A unique name for this target. | <a href="https://bazel.build/concepts/labels#target-names">Name</a> | required | |
| <a id="service_test-autoassign_port"></a>autoassign_port | If true, the service manager will pick a free port and assign it to the service. The port will be interpolated into <code>$${PORT}</code> in the service's <code>http_health_check_address</code> and <code>args</code>. It will also be exported under the target's fully qualified label in the service-port mapping.<br><br> The assigned ports for all services are available for substitution in <code>http_health_check_address</code> and <code>args</code> (in case one service needs the address for another one.) For example, the following substitution: <code>args = ["-client-addr", "127.0.0.1:$${@@//label/for:service}"]</code><br><br> The service-port mapping is a JSON string -&gt; int map propagated through the <code>ASSIGNED_PORTS</code> env var. For example, a port can be retrieved with the following JS code: <code>JSON.parse(process.env["ASSIGNED_PORTS"])["@@//label/for:service"]</code>.<br><br> Alternately, the env will also contain the location of a binary that can return the port, for contexts without a readily-accessible JSON parser. For example, the following Bash command: <code>PORT=$($GET_ASSIGNED_PORT_BIN @@//label/for:service)</code> | Boolean | optional | <code>False</code> |
| <a id="service_test-data"></a>data | - | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[]</code> |
| <a id="service_test-env"></a>env | The service manager will merge these variables into the environment when spawning the underlying binary. | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional | <code>{}</code> |
| <a id="service_test-named_ports"></a>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 <code>named_ports = ["http_port"]</code> will be assigned a fully-qualified name of <code>@@//label/for:service:http_port</code>.<br><br> Named ports are accessible through the service-port mapping. For more details, see <code>autoassign_port</code>. | List of strings | optional | <code>[]</code> |
| <a id="service_test-port_aliases"></a>port_aliases | Port aliases allow you to 're-export' another service's port as belonging to this service group. This can be used to create abstractions (such as an itest_service combined with an itest_task) but not leak their implementation through how client code accesses port names. | <a href="https://bazel.build/rules/lib/dict">Dictionary: String -> String</a> | optional | <code>{}</code> |
| <a id="service_test-services"></a>services | Services/tasks that comprise this group. Can be <code>itest_service</code>, <code>itest_task</code>, or <code>itest_service_group</code>. | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | <code>[]</code> |
| <a id="service_test-so_reuseport_aware"></a>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.<br><br> Must only be set when <code>autoassign_port</code> is enabled or <code>named_ports</code> are used. | Boolean | optional | <code>False</code> |
| <a id="service_test-test"></a>test | The underlying test target to execute once the services have been brought up and healthchecked. | <a href="https://bazel.build/concepts/labels">Label</a> | optional | <code>None</code> |


29 changes: 13 additions & 16 deletions private/itest.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ def _itest_service_impl(ctx):

return _itest_binary_impl(ctx, extra_service_spec_kwargs, extra_exe_runfiles)

_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.
_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.
"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`.
Expand Down Expand Up @@ -249,9 +249,10 @@ _port_assignment_attrs = {
Must only be set when `autoassign_port` is enabled or `named_ports` are used.""",
),
}

_itest_service_attrs = _itest_binary_attrs | _port_assignment_attrs | {
"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.",
),
"health_check": attr.label(
cfg = "target",
mandatory = False,
Expand All @@ -263,10 +264,6 @@ _itest_service_attrs = _itest_binary_attrs | _port_assignment_attrs | {
"health_check_args": attr.string_list(
doc = """Arguments to pass to the health_check binary. The various defined ports will be substituted prior to being given to the health_check binary.""",
),
"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.",
),
"health_check_interval": attr.string(
default = "200ms",
doc = "The duration between each health check. The syntax is based on common time duration with a number, followed by the time unit. For example, `200ms`, `1s`, `2m`, `3h`, `4d`.",
Expand Down Expand Up @@ -313,16 +310,11 @@ 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,
port_aliases = ctx.attr.port_aliases
)
services[service.label] = service

Expand All @@ -337,7 +329,12 @@ def _itest_service_group_impl(ctx):
_ServiceGroupInfo(services = services),
]

_itest_service_group_attrs = _port_assignment_attrs | _svcinit_attrs | {
_itest_service_group_attrs = _svcinit_attrs | {
"port_aliases": attr.string_dict(
doc = """Port aliases allow you to 're-export' another service's port as belonging to this service group.
This can be used to create abstractions (such as an itest_service combined with an itest_task) but not leak
their implementation through how client code accesses port names.""",
),
"services": attr.label_list(
providers = [_ServiceGroupInfo],
doc = "Services/tasks that comprise this group. Can be `itest_service`, `itest_task`, or `itest_service_group`.",
Expand Down
1 change: 1 addition & 0 deletions svclib/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type ServiceSpec struct {
SoReuseportAware bool `json:"so_reuseport_aware"`
NamedPorts []string `json:"named_ports"`
HotReloadable bool `json:"hot_reloadable"`
PortAliases map[string]string `json:"port_aliases"`
}

// Our internal representation.
Expand Down
12 changes: 8 additions & 4 deletions tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,14 @@ itest_service(
http_health_check_address = "http://127.0.0.1:$${PORT}",
)

# Test port assignment through a group
# Test port aliases
itest_service(
name = "_speedy_service2",
args = [
"-port",
"$${@@//:speedy_service2:port}",
"$${@@//:speedy_service2}",
],
autoassign_port = True,
env = {"foo": "bar"},
exe = "//go_service",
http_health_check_address = "http://127.0.0.1:$${@@//:speedy_service2:port}",
Expand All @@ -128,6 +129,9 @@ itest_service(

itest_service_group(
name = "speedy_service2",
services = [":_speedy_service"],
named_ports = ["port"],
services = [":_speedy_service2"],
port_aliases = {
"": "@@//:_speedy_service2",
"port": "@@//:_speedy_service2",
},
)

0 comments on commit 9cf8b2d

Please sign in to comment.