"""Every ordering dependency a handler claims, and what we did about it. The chain primitive is only worth having if new ordering claims actually reach it. The failure mode it replaces is not "someone wrote a bad cron" — it is **someone writing the intent in a docstring and trusting a minute gap to keep it**, which is how the platform ended up with eight such promises and no mechanism behind any of them. So this file is the decision record, and ``tests/test_scheduled_jobs/test_ordering_inventory_*.py`` makes it binding: a handler whose description names another registered handler next to an ordering word must appear here, marked either ``chained`` (and the chain must really exist) or ``deferred`` (with a reason). Add a new "排在 X 之后" comment without recording the decision and the test fails. **Why an inventory and not a scanner alone.** The scanner can only see that a claim exists; it cannot know whether chaining it is safe. Three of the pairs below must *not* be chained, each for a different and entirely non-obvious reason, and those reasons are the valuable part — they are exactly what a future reader would otherwise have to re-derive by reading two handlers and a vendor's billing behaviour. """ from __future__ import annotations from dataclasses import dataclass from typing import Literal OrderingStatus = Literal["chained", "deferred"] @dataclass(slots=True, frozen=True) class OrderingPair: """One "B must run after A" claim found in a handler description.""" successor: str predecessor: str status: OrderingStatus #: The handler whose own text carries the claim. Usually the #: successor — but not always, and the exception is the interesting #: one: ``software_seat.usage_signal_refresh`` declares that it runs #: *before* its consumer, so the claim lives on the predecessor. #: The scanner can only see "this text names that job"; recording who #: declared it is what lets the ratchet match a claim to a decision #: without also having to parse direction out of prose. declared_by: str = "" #: Required for ``deferred``. Says why the clock is still the right #: mechanism for this pair, in terms a reader can check. reason: str = "" #: Which migration created the edge, for ``chained``. migration: str = "" ORDERING_PAIRS: tuple[OrderingPair, ...] = ( # ── Chained ────────────────────────────────────────────────── OrderingPair( successor="ai_spending.cost_drift_check", predecessor="ai_spending.period_rollup_refresh", status="chained", declared_by="ai_spending.cost_drift_check", migration="616", ), OrderingPair( successor="hr.leave_insight_snapshot", predecessor="hr.leave_account_sync", status="chained", declared_by="hr.leave_insight_snapshot", migration="616", ), OrderingPair( successor="offboarding.account_assurance_rosters", predecessor="identity_sync.leaver_assurance", status="chained", declared_by="offboarding.account_assurance_rosters", migration="616", ), OrderingPair( successor="cmdb.leaver_iam_residue_sweep", predecessor="cmdb.leaver_grant_sweep", status="chained", declared_by="cmdb.leaver_iam_residue_sweep", migration="617", ), OrderingPair( successor="software_seat.idle_reclaim", predecessor="software_seat.usage_signal_refresh", status="chained", # The claim lives on the PREDECESSOR here («排在 idle_reclaim # 之前»), and it was wrong: 02:40 is after 02:00. Migration 617 # both records and fixes it. declared_by="software_seat.usage_signal_refresh", migration="617", ), # ── Deliberately left on the clock ─────────────────────────── OrderingPair( successor="ai_spending.period_rollup_refresh", predecessor="cmdb.cost_facts_sync", status="deferred", declared_by="ai_spending.period_rollup_refresh", reason=( "Two predecessors (cost_facts_sync AND bailian.usage_sync) and " "chain_after_job_id is single-parent, so no edge can express it " "faithfully. Both predecessors are also per-account fleets, not " "single rows — 'after cost_facts_sync' means 'after all ten of " "them', which is a fan-in the chain primitive deliberately does " "not model." ), ), OrderingPair( successor="ai_spending.period_rollup_refresh", predecessor="bailian.usage_sync", status="deferred", declared_by="ai_spending.period_rollup_refresh", reason="Second parent of the pair above; same fan-in limitation.", ), OrderingPair( successor="cmdb.cost_recon.aliyun_bq", predecessor="cmdb.cost_facts_monthclose", status="deferred", declared_by="cmdb.cost_recon.aliyun_bq", reason=( "The gap IS the schedule: recon runs on the 14th, two days " "after monthclose's SECOND pass on the 12th, to let the vendor " "finish its own late repricing. Chaining would collapse a " "deliberate two-day wait into seconds." ), ), ) # Not in the inventory, and deliberately so: # # ``cmdb.cost_facts_monthclose`` names ``cmdb.cost.nightly_recompute_allocation`` # but is drawing a *contrast*, not declaring an order ("NOT the same as # ... which re-APPLIES allocation rules to EXISTING facts; this re-PULLS # amounts from the vendor"), and ``hr.leave_reminder_reap`` names # ``notification.outbox_retry`` only to say who delivers rows it has # already queued. Both were caught by the first, window-less version of # the scanner and both would have been recorded as decisions about # orderings that do not exist. The proximity window below is what keeps # them out. def chained_pairs() -> tuple[OrderingPair, ...]: return tuple(p for p in ORDERING_PAIRS if p.status == "chained") def deferred_pairs() -> tuple[OrderingPair, ...]: return tuple(p for p in ORDERING_PAIRS if p.status == "deferred") #: Words that mark a sentence as making an ordering claim. Both scripts, #: because handler descriptions in this repo are written in Chinese with #: English technical terms mixed in. ORDERING_MARKERS: tuple[str, ...] = ( "之后", "之前", "紧随", "先跑完", "排在", "runs after", "runs before", "after the", "follows", ) #: How close an ordering word has to be to the named job for the mention #: to count as a claim about ordering. #: #: Measured, not guessed. Across the eight genuine claims in the tree the #: marker sits **1 to 22 characters** from the named key, because a real #: claim keeps both inside one clause: «排在 X 之后», «(在 X 06:30 之后)», #: «跑在 X (01:30) 之后». The two mentions that are *not* claims sit far #: outside that: ``hr.leave_reminder_reap`` names #: ``notification.outbox_retry`` 59 characters from the nearest marker #: purely to say who delivers already-queued rows, and #: ``cmdb.cost_facts_monthclose`` names #: ``cmdb.cost.nightly_recompute_allocation`` 375 characters away to draw #: a *contrast* ("NOT the same as ... re-attribution; this re-PULLS"). #: #: 30 leaves a comfortable margin above every real claim and excludes #: both non-claims. The margin matters more than the exact number: a #: ratchet that emits pairs nobody can act on is one people learn to #: silence, and a silenced ratchet is worse than none. ORDERING_PROXIMITY_CHARS = 30 def scan_declared_orderings( handlers: dict[str, str], ) -> list[tuple[str, str]]: """Find ``(declaring_handler, named_handler)`` ordering claims. ``handlers`` maps ``handler_key`` → the text to search (description plus docstring). A pair is reported when a handler's text names another registered handler key verbatim **and** an ordering marker appears within :data:`ORDERING_PROXIMITY_CHARS` of that mention. The result says *who declared what about whom*, not which one runs first — direction lives in prose ("之后" vs "之前") and getting it wrong is precisely the bug this whole area is about, so the scanner refuses to guess and the inventory records it explicitly. Deliberately blind in one direction: it can only see claims that name the other job by its exact key. "after the nightly rollup" in prose is invisible — which is an argument for naming the key, not for a fuzzier matcher that would invent pairs nobody can act on. """ keys = sorted(handlers, key=len, reverse=True) found: list[tuple[str, str]] = [] for key, text in handlers.items(): if not text: continue for other in keys: if other == key: continue start = text.find(other) while start != -1: window = text[ max(0, start - ORDERING_PROXIMITY_CHARS): start + len(other) + ORDERING_PROXIMITY_CHARS ] if any(m in window for m in ORDERING_MARKERS): found.append((key, other)) break start = text.find(other, start + 1) return sorted(set(found))