The GitLab project lives at simpl-open/data/supporting-data-services/distributed-execution/distributed-execution, so CI_REGISTRY_IMAGE ends in the project name twice. The pipeline pushes to CI_REGISTRY_IMAGE and nowhere else, so the earlier one-segment path named an image nobody builds - the same mismatch that field-level-pseudo-anonymisation and dataframe-level-anonymisation carry today. Changelog: fixed
170 lines
5.7 KiB
Python
170 lines
5.7 KiB
Python
"""Loosely coupled reference implementations.
|
|
|
|
Dagster does not run the work here. It dispatches an external payload and then
|
|
acts as a listener: the payload writes structured messages over the
|
|
dagster-pipes protocol, which Dagster materialises as logs and metadata. The
|
|
payload never connects to the metadata database.
|
|
|
|
Two transports are provided. Both dispatch the *same* payload script and produce
|
|
the same output shape as the tightly coupled ``process_work_unit`` op, so the
|
|
graphs differ by exactly one node - see the guide's section 5.3.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from dagster import (
|
|
Failure,
|
|
OpExecutionContext,
|
|
Out,
|
|
PipesSubprocessClient,
|
|
graph,
|
|
in_process_executor,
|
|
multiprocess_executor,
|
|
op,
|
|
)
|
|
from dagster_k8s import PipesK8sClient
|
|
|
|
from distributed_execution.ops import (
|
|
generate_work_units,
|
|
report_execution_target,
|
|
summarise_results,
|
|
)
|
|
|
|
# Payload lives outside src/ so it can be built into its own image.
|
|
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
PAYLOAD_SCRIPT = os.environ.get("PIPES_PAYLOAD_SCRIPT", str(_REPO_ROOT / "payload" / "work.py"))
|
|
|
|
PAYLOAD_IMAGE = os.environ.get(
|
|
"PIPES_PAYLOAD_IMAGE",
|
|
"code.europa.eu:4567/simpl/simpl-open/data/supporting-data-services"
|
|
"/distributed-execution/distributed-execution/payload:0.0.1",
|
|
)
|
|
PAYLOAD_NAMESPACE = os.environ.get("PIPES_PAYLOAD_NAMESPACE", "dagster")
|
|
|
|
COMMON_TAGS = {
|
|
"execution_target": "loosely_coupled",
|
|
"business_operation": "DISTRIBUTED_EXECUTION_REFERENCE",
|
|
}
|
|
|
|
|
|
def _result_from_pipes(context: OpExecutionContext, completed) -> dict:
|
|
"""Read one unit's result off the message channel.
|
|
|
|
A broken message path is the defining failure mode of this pattern: the
|
|
external workload can exit 0 while reporting nothing, so silence is treated
|
|
as a failure rather than an empty result.
|
|
"""
|
|
messages = completed.get_custom_messages()
|
|
if not messages:
|
|
raise Failure(
|
|
description=(
|
|
"No pipes messages received from the external payload. The workload may have "
|
|
"run successfully while its messages were lost. Verify the message path: for "
|
|
"the Kubernetes transport, confirm the pod log stream is readable and not "
|
|
"being truncated or intercepted by a log shipper."
|
|
)
|
|
)
|
|
|
|
payload = messages[-1]
|
|
leaked = payload.get("orchestration_env_visible") or []
|
|
if leaked:
|
|
context.log.warning(
|
|
"Payload could see orchestration runtime credentials: %s. "
|
|
"This defeats the isolation the loosely coupled target is chosen for.",
|
|
", ".join(leaked),
|
|
)
|
|
|
|
row = payload["results"][0]
|
|
context.log.info("Unit %s computed by external worker %s", row["unit"], row["worker"])
|
|
return row
|
|
|
|
|
|
@op(
|
|
description="Dispatches one external payload as a local subprocess and listens on the pipes channel.",
|
|
out=Out(dict),
|
|
)
|
|
def dispatch_external_work_subprocess(
|
|
context: OpExecutionContext,
|
|
unit: int,
|
|
pipes_subprocess_client: PipesSubprocessClient,
|
|
) -> dict:
|
|
completed = pipes_subprocess_client.run(
|
|
context=context,
|
|
command=[sys.executable, PAYLOAD_SCRIPT],
|
|
extras={"units": [unit]},
|
|
)
|
|
return _result_from_pipes(context, completed)
|
|
|
|
|
|
@op(
|
|
description="Dispatches one external payload as a Kubernetes Job and listens on the pod log stream.",
|
|
out=Out(dict),
|
|
)
|
|
def dispatch_external_work_k8s(
|
|
context: OpExecutionContext,
|
|
unit: int,
|
|
pipes_k8s_client: PipesK8sClient,
|
|
) -> dict:
|
|
completed = pipes_k8s_client.run(
|
|
context=context,
|
|
image=PAYLOAD_IMAGE,
|
|
command=["python", "/app/work.py"],
|
|
namespace=PAYLOAD_NAMESPACE,
|
|
extras={"units": [unit]},
|
|
base_pod_meta={
|
|
"labels": {
|
|
"app.kubernetes.io/name": "distributed-execution-payload",
|
|
"dagster/execution-target": "loosely-coupled",
|
|
}
|
|
},
|
|
)
|
|
return _result_from_pipes(context, completed)
|
|
|
|
|
|
@graph
|
|
def loosely_coupled_subprocess_reference():
|
|
target_report = report_execution_target()
|
|
units = generate_work_units()
|
|
results = units.map(dispatch_external_work_subprocess).collect()
|
|
return summarise_results(results, target_report)
|
|
|
|
|
|
@graph
|
|
def loosely_coupled_k8s_reference():
|
|
target_report = report_execution_target()
|
|
units = generate_work_units()
|
|
results = units.map(dispatch_external_work_k8s).collect()
|
|
return summarise_results(results, target_report)
|
|
|
|
|
|
loosely_coupled_subprocess_job = loosely_coupled_subprocess_reference.to_job(
|
|
name="loosely_coupled_subprocess_job",
|
|
description=(
|
|
"Loosely coupled via subprocess transport. Demonstrates the pipes contract end to end "
|
|
"on a laptop, with no cluster and no metadata database access from the payload."
|
|
),
|
|
# Dispatch is I/O-bound and the real work happens in the payload, so orchestrating
|
|
# in one process keeps the laptop demo free of platform-specific spawn behaviour.
|
|
executor_def=in_process_executor,
|
|
resource_defs={"pipes_subprocess_client": PipesSubprocessClient()},
|
|
tags={**COMMON_TAGS, "transport": "subprocess"},
|
|
)
|
|
|
|
loosely_coupled_k8s_job = loosely_coupled_k8s_reference.to_job(
|
|
name="loosely_coupled_k8s_job",
|
|
description=(
|
|
"Loosely coupled via Kubernetes Job dispatch. Messages return over the pod log stream. "
|
|
"Requires a cluster and RBAC to create Jobs in the target namespace."
|
|
),
|
|
executor_def=multiprocess_executor.configured({"max_concurrent": 2}),
|
|
resource_defs={"pipes_k8s_client": PipesK8sClient()},
|
|
tags={**COMMON_TAGS, "transport": "k8s_pod_logs"},
|
|
)
|
|
|
|
LOOSELY_COUPLED_JOBS = [
|
|
loosely_coupled_subprocess_job,
|
|
loosely_coupled_k8s_job,
|
|
]
|