Skip to content

feat(orchestrator): Refresh a container execution as soon as its pod or job changes - #428

Open
morgan-wowk wants to merge 1 commit into
masterfrom
orchestrator-informer-changed-first
Open

morgan-wowk wants to merge 1 commit into
masterfrom
orchestrator-informer-changed-first

Conversation

@morgan-wowk

@morgan-wowk morgan-wowk commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The running queue visits container executions round-robin by last_processed_at. A pod that started or finished waits for every other active container execution to be visited first. The Kubernetes launchers already run pod and job informers, so the orchestrator could know right away which container executions changed.

Changes

  • ContainerTaskLauncher.pop_changed_container_execution_ids() returns the container executions whose containers changed since the last call. The default returns [].
  • Kubernetes launchers:
    • Launched pods carry common_annotations.CONTAINER_EXECUTION_ID_ANNOTATION_KEY, and so do job pod templates. The launcher's own pod_annotations dict is not mutated.
    • MultiNamespaceInformer takes an optional on_change callback. It is registered for ADDED, MODIFIED and DELETED after the informer's initial list, so startup does not mark every existing object.
    • The pod and job informers record each changed object's container execution id. Ids are recorded only from the first pop_changed_container_execution_ids() call on, so a launcher nobody asks keeps no ids.
    • _KubernetesPodOrJobLauncher returns the pod launcher's ids plus the job launcher's.
  • internal_process_running_executions_queue() first moves the changed container executions that are still PENDING or RUNNING to the front of the queue. That is one UPDATE ... SET last_processed_at = 1970-01-01.

System impact

  • Container executions whose pod or job changed are refreshed on the next sweep, not after a full round-robin cycle.
  • Launchers without informers are unaffected.
  • Pods and jobs launched before this change carry no id annotation and stay on the round-robin until they end.
  • Ended container executions are never moved.

Production test

All three orchestrator PRs ran together in production (https://github.com/Shopify/oasis-backend/pull/843). That test patched the informer in; this PR builds the same behavior into the launchers. Before/after:

  • Completion lag (container ended → status committed): p50 128s → 1.0s, p90 182s → 5.2s.
  • Start detection (pod running → RUNNING committed): p50 81–250s → 4.8–14s.
  • Container executions never seen as RUNNING before they ended: 34–80% → 0–0.4%.

Related PRs

Testing

  • pytest tests: 593 passed.
  • New tests/test_orchestrator_changed_container_executions.py covers:
    • a changed container execution is refreshed first
    • an ended one is left alone
    • the default launcher returns []
  • New tests/launchers/kubernetes/test_kubernetes_launchers_changed_container_executions.py covers:
    • the pod and job template annotations
    • the launcher's annotations are not mutated
    • each change is reported once
    • objects without an id are ignored
    • nothing is recorded until asked
    • handlers are added after the initial list
    • no handlers are added without on_change

…or job changes

The running queue visits container executions round-robin by
last_processed_at, so a pod that finished waits for every other active
container execution to be visited first.

ContainerTaskLauncher gets pop_changed_container_execution_ids(), which
returns the container executions whose containers changed since the last
call; it returns nothing unless a launcher overrides it. The Kubernetes
launchers now record the container execution id annotation on the pods
they launch (and so on job pod templates), and their pod and job informers
record each changed object's container execution. Before selecting a
running container execution, the orchestrator moves the changed ones that
are still PENDING or RUNNING to the front of the queue with one UPDATE.

Pods and jobs launched before this change carry no id and keep the
round-robin.
) -> _TLaunchedContainer:
raise NotImplementedError()

def pop_changed_container_execution_ids(self) -> list[str]:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interface change

Only the launcher knows which containers changed (its pod and job informers already see every change), while only the orchestrator decides which container executions to refresh next, so a single method on the launcher interface is the smallest way to pass that signal from one to the other. It defaults to [], so launchers that can't tell (Davies, local and Docker launchers, routers that don't delegate) keep the current round-robin with no changes, and it doesn't commit the interface to anything Kubernetes-specific.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatives that leave ContainerTaskLauncher unchanged:

  • Orchestrator kwarg: OrchestratorService_Sql takes an optional function that returns the changed ids, and the deployment passes in the Kubernetes launcher's method.
  • Push callback: Kubernetes launchers take an on_container_execution_changed callback that feeds a set the orchestrator owns.
  • Optional capability: a separate Protocol, or a getattr check, that only Kubernetes launchers implement.
  • Launcher writes to the DB: the informer moves the row to the front itself, but that breaks layering because launchers have no DB session.
  • Downstream patch: keep it as a deployment-side monkeypatch, which is fragile because it depends on informer internals.

Why an interface addition?

It's the only option where the launcher reports changes and the orchestrator consumes them through the same typed contract as the rest of the launcher API. The default [] means no other launcher or deployment needs any change, while the alternatives either spread the wiring across each deployment or hide the same method behind a looser contract.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ark-kun Do you want any alternative?

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.

1 participant