Skip to content

progress: strategy-wide machine-readable execution progress - #41

Merged
Kiran01bm merged 3 commits into
mainfrom
kiran01bm/e6-progress
Aug 17, 2026
Merged

progress: strategy-wide machine-readable execution progress#41
Kiran01bm merged 3 commits into
mainfrom
kiran01bm/e6-progress

Conversation

@Kiran01bm

Copy link
Copy Markdown
Collaborator

Summary

Adds pkg/progress, a strategy-wide machine-readable progress contract, and wires it into the three executor entry points. A caller (the CLI today, an orchestrator adapter next) can now observe a running change — phase, sequence step position, retry attempt, and live server counters for concurrent index builds — instead of staring at a blocking call.

What

  • New pkg/progress.Tracker: caller-owned, concurrency-safe, no goroutines, injected clock. Snapshots carry phase, step/total, elapsed, and operation detail.
  • ExecuteNativeWithProgress, RunSequenceWithProgress, and BuildIndexConcurrentlyWithProgress variants; the existing entry points are unchanged.
  • Concurrent index builds anchor progress to the executing backend PID and read pg_stat_progress_create_index on demand — polling lifetime is exactly the caller's context.
  • Copy counters (rows/bytes/blocks/tuples) exist in the contract but stay empty for native operations; copy-and-swap implements the same contract later rather than a second one.

Why

Progress reporting is the last adapter-facing surface the native path needs: an orchestrator has to distinguish "still building the index, 60% of blocks scanned" from "stuck" without parsing logs. Making the tracker caller-owned keeps the safety-critical core free of goroutines and timers.

Before / after

Before:
  caller ──▶ executor (blocking) ──▶ PostgreSQL
             no visibility until return

After:
  caller ──▶ executor WithProgress (blocking) ──▶ PostgreSQL
    │                │ start/step/attempt/finish        │
    │                ▼                                  │
    └─ poll ──▶ progress.Tracker ── on demand ──▶ pg_stat_progress_create_index
                (caller-owned, no goroutines)     (by backend PID)

Callers (CLI today, the SchemaBot adapter next) need a machine-readable
view of a running change: phase, sequence step position, retry attempt,
and live pg_stat_progress_create_index counters for concurrent builds.
Native operations leave the copy counters empty so copy-and-swap can
implement the same contract later.
…s API

Split the tracker's one RWMutex into a memory-state lock and a poll lock:
concurrent Progress() calls previously shared the reserved pgx connection
under RLock (a pgx.Conn is not safe for concurrent use), and a slow poll
could gate the executor's own state updates. Budget corroboration now reads
the same injected clock that produced its start instant, matching the step
report. Adds the missing test coverage for all three *WithProgress entry
points, the retry-attempt observer wiring, the server-progress merge
branches, and the failing-build-under-polling session handoff.
@Kiran01bm
Kiran01bm marked this pull request as ready for review August 17, 2026 10:24
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@morgo morgo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Approved on Morgan's behalf (agent review, liberal pg-sprite bar).

Verified:

  • The progress path cannot interfere with the schema change: the only query it ever issues is a parameterized SELECT against pg_stat_progress_create_index (stat view, no locks, no transaction), riding the executor's already-reserved verdict session rather than the pool — polling can't starve the build. Optimistic/brief/validate progress is pure in-memory state.
  • Verdict-session handoff is race-free (checked against head sources): StopConcurrentBuild drains in-flight polls after conn.Exec returns and before either verdict path touches the connection; lock order is consistently pollMu → mu; Snapshot is a value copy under RLock with a fresh Work alloc per poll — no torn reads. The polled PID belongs to the executor's own live build connection, so WHERE pid = $1 can't match a recycled foreign backend.
  • No leaks: snapshot JSON is phase/step/elapsed/attempt/counters only — no SQL, DSN, or even table/index names. Nil-tracker paths reduce exactly to the old code, and the −16 lines are doc updates plus mechanical body-moves.

Non-blocking nits:

  1. SAFETY.md says "a slow or hung observation can never gate the executor's own state updates" — true as scoped, but StopConcurrentBuild takes pollMu to drain an in-flight poll running under the poller's context, so a poller with a non-cancellable ctx on a wedged connection delays the executor's verdict handoff (bounded in practice by the pool's baseline statement_timeout). The drain is the right call; the doc should just say the verdict handoff IS observer-gated.
  2. RunSequenceWithProgress seeds Start(len(steps), OperationBrief) — between Start and the first StartStep, polls report step: 0 / operation: "brief" even when step 1 is a concurrent build, and an admission-time failure leaves a failed snapshot carrying the placeholder. Consider an "admitting" operation for that window.
  3. Small reset asymmetries in Tracker: Start doesn't reset step, and StartStep/Finish zero buildPID but leave session non-nil. Harmless today (every build path goes through StopConcurrentBuild), but a reused tracker would briefly show the prior run's step, and the retained session pointer is a footgun for future edits.

@Kiran01bm

Copy link
Copy Markdown
Collaborator Author

Review response from Kiran's (@Kiran01bm) AI code review assessment agent (Amp, Claude Opus 4.5)

All three non-blocking nits are fixed in the follow-up commit; the verified sections needed no action.

# Finding Status Explanation
1 SAFETY.md claims a hung observation "can never gate" the executor, but the verdict handoff drains an in-flight poll fixed SAFETY.md now states the handoff is observer-gated: StopConcurrentBuild deliberately drains the in-flight poll, bounded by the poller's context and the session's statement_timeout
2 Pre-step polls of a sequence report step: 0 / operation: "brief" even when step 1 is a concurrent build fixed New admitting operation seeds RunSequenceWithProgress; an admission-time failure now leaves an honest failed/admitting snapshot. Added as part of contract v1 (format_version ships first in this PR)
3 Reset asymmetries: Start keeps the prior run's step; StartStep/Finish zero buildPID but retain session fixed Start resets step, terminal instant, session, and PID; StartStep and Finish clear the session alongside the PID — pinned by a reuse test that fails the run if the old session is ever polled
Verified section (no-interference query path, race-free handoff, no leaks) no action Confirmations only; noted with thanks

Closes the remaining PR #41 review findings: snapshots carry
format_version with a key-pinning contract test and docs page; terminal
snapshots freeze elapsed at Finish; the tracker fully resets between
runs and drops the build session on step/finish; sequences report an
"admitting" operation before step 1; nil-tracker guards return
ErrInvariantViolation. pkg/progress is reclassified as core in
SAFETY.md — the executors import it — and depguard now mechanically
enforces the recorded core dependency list the docs already claimed.
@Kiran01bm
Kiran01bm merged commit a3008a7 into main Aug 17, 2026
11 checks passed
Kiran01bm added a commit that referenced this pull request Aug 18, 2026
Merging main brought in the #37/#38/#41 format_version bumps; the
smoke test did its job and went red on the stale v1 pins. Lint stays
at 1.
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