Skip to content

distributed: gallery job status is process-local, so an install's outcome is unknowable after a controller roll #11052

Description

@localai-bot

Summary

POST /models/apply returns HTTP 200 with a job id and a status URL. If the controller replica that owns the job restarts or is replaced, the status becomes permanently unqueryable — from any replica — and the caller has no way to learn whether the install succeeded, failed, or is still running.

Same class as the Responses API cross-replica gap fixed in #11000, in the gallery op path.

Reproduction

POST /models/apply {"id":"localai@longcat-video-avatar-1.5"}
-> HTTP 200 {"uuid":"92911516-8546-11f1-b61e-e21c92fcd5ba", "status":".../models/jobs/92911516-..."}

(controller pods roll — a routine Flux image update)

GET /models/jobs/92911516-8546-11f1-b61e-e21c92fcd5ba
-> HTTP 500 {"error":{"code":500,"message":"could not find any status for ID"}}

On a cluster where the deployment rolls regularly (image automation, autoscaling, node drain), any install taking longer than the interval between rolls is affected. Model materialization routinely takes tens of minutes, so this is the common case, not an edge case.

Why it matters beyond the missing status

The status is the only signal that an install did anything. During the incident that surfaced this:

  • an install was submitted and returned HTTP 200
  • the config on disk was never rewritten (verified by mtime — unchanged from a previous day)
  • the job status was gone, so there was nothing to consult
  • the operator's only evidence of success was the HTTP 200, which had been true from the start

The stale model config that this install was meant to replace had been wrong for two days without anyone noticing. A reinstall that silently no-ops while reporting success is precisely how that persists.

Checking the filesystem (does the config exist, what is its mtime) turned out to be the only reliable way to determine the outcome, which is not a reasonable API contract.

Scope

  • /models/apply and /models/jobs/:uuid
  • /backends/apply, /backends/upgrade/:name and /backends/jobs/:uuid appear to share the pattern — a backend upgrade job submitted in the same session was later unqueryable the same way, though I did not isolate that as carefully.

Possible directions

Not proposing an implementation. The obvious options:

  • persist job state so any replica can answer (the cluster already has Postgres; syncstate.SyncedMap already exists and is used for exactly this kind of cross-replica state)
  • or make the outcome discoverable without the job: an install that completed should be evident from the installed-model listing, so a caller could reconcile by polling that instead

The second is weaker — it cannot distinguish "still running" from "failed" — but it is much cheaper and would have prevented the specific confusion here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions