Skip to content

Add job_postprocessor hook to Kubernetes Job launchers - #309

Open
morgan-wowk wants to merge 1 commit into
masterfrom
k8s-job-postprocessor-hook
Open

Add job_postprocessor hook to Kubernetes Job launchers#309
morgan-wowk wants to merge 1 commit into
masterfrom
k8s-job-postprocessor-hook

Conversation

@morgan-wowk

@morgan-wowk morgan-wowk commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Is it safe to deploy?

Yes — this is fully backward compatible. job_postprocessor is a new keyword-only parameter defaulting to None, so every existing launcher call site is unaffected and the Job-transform hook stays a no-op until a caller explicitly opts in.

What

Adds a job_postprocessor hook to the Kubernetes Job launchers, symmetric to the existing pod_postprocessor. It lets a caller transform the V1Job object right before it's created, threaded through _KubernetesJobLauncher, _KubernetesPodOrJobLauncher, and both GoogleKubernetesEngine_UsingGoogleCloudStorage_* launchers.

class JobPostProcessor(typing.Protocol):
    def __call__(
        self, *, job: k8s_client_lib.V1Job, annotations: dict[str, str] | None = None
    ) -> k8s_client_lib.V1Job: ...

The default _transform_job_before_launching now applies the injected job_postprocessor when one is set, and is otherwise the same no-op it was before.

Why

The launchers already let callers customize the Pod (via pod_postprocessor), and for multi-node runs that Pod becomes the Job's pod template — so pod-level changes already reach Job pods. But there was no way to set fields on the V1Job itself without editing this repo:

  • _transform_job_before_launching was a no-op override hook living on _KubernetesJobLauncher, but the class callers actually build is _KubernetesPodOrJobLauncher, which constructs a private inner _KubernetesJobLauncher with no hook threaded through. So subclassing the public launcher can't reach it.

The concrete need: a downstream (Tangle/oasis) execution cluster with no Kueue and a janitor that holds no batch/jobs permission, so finished Job objects are never reaped. Stamping spec.ttlSecondsAfterFinished lets Kubernetes' native TTL-after-finished controller clean them up — but that field is on the Job, not the pod template, so pod_postprocessor can't set it. With this hook the caller does:

def _set_job_ttl(*, job, annotations=None):
    if job.spec.ttl_seconds_after_finished is None:
        job.spec.ttl_seconds_after_finished = 7 * 24 * 60 * 60
    return job

launcher = GoogleKubernetesEngine_..._KubernetesPodOrJobLauncher(
    ..., job_postprocessor=_set_job_ttl,
)

System impact

  • No behavior change when unused. job_postprocessor defaults to None; the transform hook stays a no-op, so every existing launch path is byte-identical. New keyword-only param with a default → no call-site is required to change.
  • Applies only to the Job path (multi-node runs); the Pod path is untouched.

Before / after

  • Before: the only way to influence the launched Job was to fork/subclass the inner _KubernetesJobLauncher, which public callers don't instantiate → effectively not extensible.
  • After: callers pass a job_postprocessor the same way they already pass pod_postprocessor.

Tests

tests/test_kubernetes_launchers.py (new, offline — no live K8s API or GCS):

  • _transform_job_before_launching applies the job_postprocessor when set;
  • it's a no-op (returns the same Job unchanged) when unset;
  • _KubernetesPodOrJobLauncher forwards job_postprocessor to its inner Job launcher.

All pass; black-formatted.

The launchers already accept a pod_postprocessor to transform the Pod before
launch, but there is no equivalent hook for the Job wrapper the PodOrJob and
Job launchers create for multi-node runs — the only extension point was the
no-op _transform_job_before_launching, reachable solely by subclassing the
inner _KubernetesJobLauncher, which callers do not construct directly.

Add a symmetric job_postprocessor: JobPostProcessor | None, threaded through
_KubernetesJobLauncher, _KubernetesPodOrJobLauncher, and the two GKE launchers
exactly like pod_postprocessor. The default _transform_job_before_launching
now applies it when set and is otherwise a no-op, so existing launches are
unchanged. This lets a caller set fields on the V1Job it cannot reach via the
pod template — e.g. spec.ttlSecondsAfterFinished for native finished-Job
cleanup on clusters without a Kueue/janitor reaper.
Comment thread cloud_pipelines_backend/launchers/kubernetes_launchers.py Dismissed
@morgan-wowk
morgan-wowk marked this pull request as ready for review July 31, 2026 18:02
@morgan-wowk
morgan-wowk requested a review from a team July 31, 2026 18:02
@morgan-wowk
morgan-wowk requested a review from Ark-kun as a code owner July 31, 2026 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants