Skip to content

architecture: job liveness is a second copy of a fact Action Scheduler already owns — the drift class exists because the state is duplicated #3481

Description

@chubes4

The observation

A job row carries status = 'processing'. Action Scheduler independently carries the pending/in-progress action that will actually advance that job. These are two copies of one fact: is this run alive?

Every "stuck job" incident is that duplication drifting. When a worker dies between finishing a step and scheduling the next one, the row still says processing and Action Scheduler has nothing — and nothing reconciles them (#3478). The row is not wrong because of a bug in one write path; it is wrong because the fact has two homes and only one of them was updated.

grep -rn "status.*=.*'processing'" inc --include=*.php → ~25 write sites.

Why file this separately

#3478 proposes a reconciler: a recurring task that detects rows whose claim has no live Action Scheduler action and resumes or terminalizes them. That is a reasonable stopgap and probably worth shipping. But structurally a reconciler is a daemon whose purpose is to keep two copies of one fact agreeing, and that is usually the thing you build immediately before concluding that one of the copies should not exist.

Data Machine currently has ~670 PHP files / ~230k lines in inc/, with inc/Core/Database/Jobs/Jobs.php at 4,440 lines (#3461). Adding permanent machinery to make a duplication maintainable is a real cost, and it forecloses the cheaper question. Worth asking the question once, explicitly, before the reconciler becomes load-bearing.

The question

Split job state into two kinds and ask whether they belong in the same place:

If liveness were derived from Action Scheduler rather than mirrored into the jobs table, the drift class disappears by construction — there is no second copy to go stale, and no reconciler needed for it. The terminal column stays exactly as it is.

What would need to be true

Not asserting this is tractable. Concretely, someone needs to establish:

  1. Every read of status === 'processing' and what it actually needs — a cheap boolean, an ordering key, an index for a list query, or genuine cross-request durability.
  2. Whether Action Scheduler can answer those reads at acceptable cost. Listing jobs by liveness currently indexes a local column; asking AS per row does not scale the same way. A derived-but-cached projection may be the honest middle, and if so, say so — a cache with a single writer is a different animal from two independent writers.
  3. Whether operation_generation / operation_claim_token (already carried in datamachine_execute_step args) are sufficient to identify the owning action for a job without a mirrored column.
  4. Whether the multi-step batch/child-job shapes (datamachine_pipeline_batch_chunk, waiting_children) survive a derived model, or genuinely need their own persisted coordination state.

The acceptable outcomes include "the column earns its place, keep it and keep the reconciler" — that is a real answer, and documenting why is worth as much as changing it. What is not acceptable is never asking, and accumulating reconcilers.

Related

Filed by Extra Chill Bot (AI agent) on behalf of @Chubes, prompted by production forensics on orphaned jobs and a review of whether the proposed fix was the right shape.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions