This is an automated email from the ASF dual-hosted git repository. juergbi pushed a commit to branch jbilleter/action-cache in repository https://gitbox.apache.org/repos/asf/buildstream.git
commit 408d0ac53d5e7c7cd6e0be8293798dba10ecc362 Author: Jürg Billeter <[email protected]> AuthorDate: Sat Jun 28 14:23:43 2025 +0200 Support remote action cache for nested execution --- src/buildstream/_frontend/widget.py | 3 ++- src/buildstream/_remotespec.py | 35 ++++++++++++++++++++------ src/buildstream/element.py | 2 +- src/buildstream/sandbox/_reremote.py | 6 +++++ src/buildstream/sandbox/_sandboxbuildboxrun.py | 31 +++++++++++++++++++++-- src/buildstream/sandbox/_sandboxremote.py | 2 +- 6 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/buildstream/_frontend/widget.py b/src/buildstream/_frontend/widget.py index 09b824ff3..e061f0c2f 100644 --- a/src/buildstream/_frontend/widget.py +++ b/src/buildstream/_frontend/widget.py @@ -534,7 +534,8 @@ class LogLine(Widget): text += "\n" text += self.content_profile.fmt("Remote Execution Configuration\n", bold=True) values = {} - values["Execution Service"] = format_spec(specs.exec_spec) + if specs.exec_spec: + values["Execution Service"] = format_spec(specs.exec_spec) re_storage_spec = specs.storage_spec or context.remote_cache_spec values["Storage Service"] = format_spec(re_storage_spec) if specs.action_spec: diff --git a/src/buildstream/_remotespec.py b/src/buildstream/_remotespec.py index 1a22ee043..7bb43ae16 100644 --- a/src/buildstream/_remotespec.py +++ b/src/buildstream/_remotespec.py @@ -239,6 +239,7 @@ class RemoteSpec: # spec_node: The configuration node describing the spec. # basedir: The base directory from which to find certificates. # remote_execution: Whether this spec is used for remote execution (some keys are invalid) + # action_cache: Whether this spec is used for remote execution action cache # # Returns: # The described RemoteSpec instance. @@ -248,7 +249,12 @@ class RemoteSpec: # @classmethod def new_from_node( - cls, spec_node: MappingNode, basedir: Optional[str] = None, *, remote_execution: bool = False + cls, + spec_node: MappingNode, + basedir: Optional[str] = None, + *, + remote_execution: bool = False, + action_cache: bool = False, ) -> "RemoteSpec": server_cert: Optional[str] = None client_key: Optional[str] = None @@ -261,8 +267,10 @@ class RemoteSpec: valid_keys: List[str] = ["url", "instance-name", "auth", "connection-config"] if not remote_execution: remote_type = cast(str, spec_node.get_enum("type", RemoteType, default=RemoteType.ALL)) + valid_keys += ["type"] + if not remote_execution or action_cache: push = spec_node.get_bool("push", default=False) - valid_keys += ["push", "type"] + valid_keys += ["push"] spec_node.validate_keys(valid_keys) @@ -500,9 +508,9 @@ class RemoteSpec: # class RemoteExecutionSpec: def __init__( - self, exec_spec: RemoteSpec, storage_spec: Optional[RemoteSpec], action_spec: Optional[RemoteSpec] + self, exec_spec: Optional[RemoteSpec], storage_spec: Optional[RemoteSpec], action_spec: Optional[RemoteSpec] ) -> None: - self.exec_spec: RemoteSpec = exec_spec + self.exec_spec: Optional[RemoteSpec] = exec_spec self.storage_spec: Optional[RemoteSpec] = storage_spec self.action_spec: Optional[RemoteSpec] = action_spec @@ -526,7 +534,7 @@ class RemoteExecutionSpec: ) -> "RemoteExecutionSpec": node.validate_keys(["execution-service", "storage-service", "action-cache-service"]) - exec_node = node.get_mapping("execution-service") + exec_node = node.get_mapping("execution-service", default=None) storage_node = node.get_mapping("storage-service", default=None) if not storage_node and not remote_cache: provenance = node.get_provenance() @@ -538,7 +546,20 @@ class RemoteExecutionSpec: ) action_node = node.get_mapping("action-cache-service", default=None) - exec_spec = RemoteSpec.new_from_node(exec_node, basedir, remote_execution=True) + if not exec_node and not action_node: + provenance = node.get_provenance() + raise LoadError( + "{}: At least one of `execution-service` or `action-service` need to be specified in the 'remote-execution' section".format( + provenance + ), + LoadErrorReason.INVALID_DATA, + ) + + exec_spec: Optional[RemoteSpec] + if exec_node: + exec_spec = RemoteSpec.new_from_node(exec_node, basedir, remote_execution=True) + else: + exec_spec = None storage_spec: Optional[RemoteSpec] if storage_node: @@ -548,7 +569,7 @@ class RemoteExecutionSpec: action_spec: Optional[RemoteSpec] if action_node: - action_spec = RemoteSpec.new_from_node(action_node, basedir, remote_execution=True) + action_spec = RemoteSpec.new_from_node(action_node, basedir, remote_execution=True, action_cache=True) else: action_spec = None diff --git a/src/buildstream/element.py b/src/buildstream/element.py index ca8080314..f02f45f1a 100644 --- a/src/buildstream/element.py +++ b/src/buildstream/element.py @@ -2822,7 +2822,7 @@ class Element(Plugin): else: output_node_properties = None - if allow_remote and context.remote_execution_specs: + if allow_remote and context.remote_execution_specs and context.remote_execution_specs.exec_spec: with SandboxRemote( context, project, diff --git a/src/buildstream/sandbox/_reremote.py b/src/buildstream/sandbox/_reremote.py index 4c7773fec..b9bfd8860 100644 --- a/src/buildstream/sandbox/_reremote.py +++ b/src/buildstream/sandbox/_reremote.py @@ -39,10 +39,16 @@ class RERemote(CASRemote): self.remote_execution_specs.storage_spec.to_localcas_remote(request.content_addressable_storage) else: self.spec.to_localcas_remote(request.content_addressable_storage) + request.content_addressable_storage.read_only = True if self.remote_execution_specs.exec_spec: self.remote_execution_specs.exec_spec.to_localcas_remote(request.execution) + request.content_addressable_storage.read_only = False if self.remote_execution_specs.action_spec: self.remote_execution_specs.action_spec.to_localcas_remote(request.action_cache) + if self.remote_execution_specs.action_spec.push: + request.content_addressable_storage.read_only = False + else: + request.action_cache.read_only = True response = local_cas.GetInstanceNameForRemotes(request) self.local_cas_instance_name = response.instance_name diff --git a/src/buildstream/sandbox/_sandboxbuildboxrun.py b/src/buildstream/sandbox/_sandboxbuildboxrun.py index d79f54b2f..632a166ca 100644 --- a/src/buildstream/sandbox/_sandboxbuildboxrun.py +++ b/src/buildstream/sandbox/_sandboxbuildboxrun.py @@ -17,6 +17,7 @@ import subprocess import sys from contextlib import ExitStack +import grpc import psutil from .. import utils, _signals @@ -24,6 +25,7 @@ from . import _SandboxFlags from .._exceptions import SandboxError, SandboxUnavailableError from .._platform import Platform from .._protos.build.bazel.remote.execution.v2 import remote_execution_pb2 +from ._reremote import RERemote from ._sandboxreapi import SandboxREAPI @@ -32,6 +34,26 @@ from ._sandboxreapi import SandboxREAPI # BuildBox-based sandbox implementation. # class SandboxBuildBoxRun(SandboxREAPI): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + context = self._get_context() + cascache = context.get_cascache() + + re_specs = context.remote_execution_specs + if re_specs and re_specs.action_spec and not re_specs.exec_spec: + self.re_remote = RERemote(context.remote_cache_spec, re_specs, cascache) + try: + self.re_remote.init() + except grpc.RpcError as e: + urls = set() + if re_specs.storage_spec: + urls.add(re_specs.storage_spec.url) + urls.add(re_specs.action_spec.url) + raise SandboxError("Failed to contact remote cache endpoint at {}: {}".format(sorted(urls), e)) from e + else: + self.re_remote = None + @classmethod def __buildbox_run(cls): return utils._get_host_tool_internal("buildbox-run", search_subprojects_dir="buildbox") @@ -91,8 +113,10 @@ class SandboxBuildBoxRun(SandboxREAPI): casd = cascache.get_casd() config = self._get_config() - if config.remote_apis_socket_path and context.remote_cache_spec: - raise SandboxError("'remote-apis-socket' is not currently supported with 'storage-service'.") + if config.remote_apis_socket_path and context.remote_cache_spec and not self.re_remote: + raise SandboxError( + "'remote-apis-socket' is not supported with 'storage-service' without 'action-cache-service'." + ) with utils._tempnamedfile() as action_file, utils._tempnamedfile() as result_file: action_file.write(action.SerializeToString()) @@ -105,6 +129,9 @@ class SandboxBuildBoxRun(SandboxREAPI): "--action-result={}".format(result_file.name), ] + if self.re_remote: + buildbox_command.append("--instance={}".format(self.re_remote.local_cas_instance_name)) + # Do not redirect stdout/stderr if "no-logs-capture" in self._capabilities: buildbox_command.append("--no-logs-capture") diff --git a/src/buildstream/sandbox/_sandboxremote.py b/src/buildstream/sandbox/_sandboxremote.py index dbd967a95..4adf1a77d 100644 --- a/src/buildstream/sandbox/_sandboxremote.py +++ b/src/buildstream/sandbox/_sandboxremote.py @@ -40,7 +40,7 @@ class SandboxRemote(SandboxREAPI): cascache = context.get_cascache() specs = context.remote_execution_specs - if specs is None: + if specs is None or specs.exec_spec is None: return self.storage_spec = specs.storage_spec
