You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(executor): caller-owned deadline, lockers, and backend PID for concurrent builds (#76)
`BuildIndexConcurrently` gains a caller-owned cancellation mode, the
progress tracker becomes the operator's stop path for a running build,
and progress snapshots report the lockers a concurrent build is waiting
on.
## Why
A concurrent index build on a large table can run for hours. Today the
only bound the executor accepts is a fixed server-side
`statement_timeout` (`ConcurrentBudget.Overall`), which fits a
synchronous attempt but not an orchestrator that keeps a build alive for
as long as it can renew a lease. That orchestrator's decision tree has
three branches — *my lease lapsed* (retry later), *an operator stopped
it* (do not retry), *the server's budget killed it* (retry with a
different bound) — so the executor must report three distinguishable
outcomes. Its operators also need to stop a running build without being
handed a backend PID they could misuse after the build returns, and to
see why a build in the "waiting for old snapshots" phase is not moving,
which requires the lockers columns of `pg_stat_progress_create_index`.
## What
- `ConcurrentBudget.CallerOwned`: the session runs with
`statement_timeout = 0` and the caller's cancellable context is the
statement's only bound. `Overall` must be zero
(`ErrCallerOwnedOverallBudget`), and a context that cannot be cancelled
is refused with `ErrCallerOwnedNeedsCancellableContext` before any
session is acquired, so the statement remains bounded by construction
(LK-2). The bounded mode and every existing caller are unchanged.
- Cancellation is a three-way partition in both modes: the caller's own
context ending is `ErrCancelledByCaller` (`cancelled-by-caller`)
whichever of the server's 57014 or the client's context error arrives
first; a 57014 under a live context is `ErrCancelledExternally`
(`cancelled-externally`); a 57014 at the bounded mode's deadline is
`*BudgetError`.
- `progress.Tracker.CancelBuild(ctx)` signals the active build's backend
over the tracker's reserved session, under the same lock that guards the
build's lifecycle and only while the build is active (`ErrNoActiveBuild`
otherwise; `ErrBuildNotRunning` when the backend had no statement to
cancel). The tracker never exposes the PID itself.
- `dbconn.ConcurrentIndexProgress` reads `lockers_total`,
`lockers_done`, `current_locker_pid`; the snapshot carries them as
`work.lockers_total` / `work.lockers_done` and
`detail.current_locker_pid`. `format_version` bumps from 2 to 3 and
`docs/progress-report.md` documents the new fields and the stop path.
- LK-2 in `docs/invariants.md` records the caller-owned exception;
capability, execution-model and TCB docs describe the caller-owned
context as a bound different in kind, not an absence of one.
- Integration tests on a real server: a caller-owned build completes
once its blocker releases; a build stopped via `tracker.CancelBuild`
returns `ErrCancelledExternally` with its invalid leftover reported and
the tracker then refuses a second cancel; a build whose caller cancels
returns `ErrCancelledByCaller`, never `ErrCancelledExternally` or a
`*BudgetError`; a blocked build publishes `lockers_total ≥ 1` and the
blocker's PID as `current_locker_pid`.
## Before / after
```
Before
caller ── ConcurrentBudget{Overall: 30m} ──▶ SET statement_timeout = 30m ──▶ CREATE INDEX CONCURRENTLY
(no way to say "for as long as my lease holds")
57014 before the deadline ──▶ ErrCancelledExternally (operator? caller? indistinguishable)
tracker.Progress() ──▶ phase, blocks, tuples (no lockers, no stop path)
After
caller ── ConcurrentBudget{CallerOwned: true} + cancellable ctx
├─ ctx.Done() == nil ──▶ ErrCallerOwnedNeedsCancellableContext (refused)
└─ SET statement_timeout = 0 ──▶ CREATE INDEX CONCURRENTLY
├─ caller's ctx ended ──▶ ErrCancelledByCaller (+ catalog verdict)
├─ tracker.CancelBuild ──▶ ErrCancelledExternally (+ catalog verdict)
└─ (bounded mode only) deadline ──▶ *BudgetError
tracker.CancelBuild(ctx) ──▶ pg_cancel_backend on the active build only; PID never leaves the tracker
tracker.Progress() ──▶ phase, blocks, tuples, lockers_total/done, current_locker_pid
```
Copy file name to clipboardExpand all lines: docs/progress-report.md
+10-4Lines changed: 10 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,7 +16,8 @@ phase or operation value is a contract change and bumps `format_version`, even i
16
16
is added or renamed.
17
17
18
18
Adding a field bumps `format_version` so a strict consumer can detect the new shape from the
19
-
version. The current version is **2**: version 2 added `detail.statement`.
19
+
version. The current version is **3**: version 2 added `detail.statement`; version 3 added
20
+
`detail.current_locker_pid`, `work.lockers_total`, and `work.lockers_done`.
20
21
21
22
The [plan report](plan-report.md), [lint report](lint-report.md), and
22
23
[suggest report](suggest-report.md) are separate contracts with their own `format_version`;
@@ -50,14 +51,16 @@ licenses a consumer to intervene in the change itself.
50
51
|`server_phase`| string | active concurrent build only | PostgreSQL's own phase string from `pg_stat_progress_create_index`, verbatim. |
51
52
|`active`| bool | always | Whether an operation is executing now. `false` with `phase: "running"` means a concurrent build's progress row has left the server view. |
52
53
|`attempt`| int | bounded retries only | The current attempt number when the executor is inside its bounded retry loop. |
54
+
|`current_locker_pid`| int | while waiting on a locker | PostgreSQL backend PID currently blocking the concurrent build; omitted when none is published. |
53
55
|`work`| object | server-observed work only | Present exactly when the server published a progress row; then **every** counter below is present, so a fresh build reports honest zeros rather than an empty object. |
54
56
55
57
`statement` is the submitter's statement after qualification and canonicalization, so a
56
58
consumer rendering it into a shared surface must clamp and escape it.
57
59
58
60
### Work counters
59
61
60
-
`blocks_done` / `blocks_total` and `tuples_done` / `tuples_total` come from
62
+
`blocks_done` / `blocks_total`, `tuples_done` / `tuples_total`, and
63
+
`lockers_done` / `lockers_total` come from
61
64
`pg_stat_progress_create_index` during a concurrent index build. `rows_copied` /
62
65
`rows_total` and `bytes_copied` / `bytes_total` are reserved for copy-and-swap and are `0`
63
66
on every native operation — the engine never fabricates copy counters.
@@ -98,7 +101,7 @@ A poll during step 2 of a 3-step sequence, mid concurrent index build:
98
101
99
102
```json
100
103
{
101
-
"format_version": 2,
104
+
"format_version": 3,
102
105
"phase": "running",
103
106
"step": 2,
104
107
"total_steps": 3,
@@ -110,6 +113,7 @@ A poll during step 2 of a 3-step sequence, mid concurrent index build:
110
113
"server_phase": "building index",
111
114
"active": true,
112
115
"attempt": 2,
116
+
"current_locker_pid": 31337,
113
117
"work": {
114
118
"rows_copied": 0,
115
119
"rows_total": 0,
@@ -118,7 +122,9 @@ A poll during step 2 of a 3-step sequence, mid concurrent index build:
Copy file name to clipboardExpand all lines: pkg/executor/native.go
+40-8Lines changed: 40 additions & 8 deletions
Original file line number
Diff line number
Diff line change
@@ -88,6 +88,9 @@ var (
88
88
// connection, every failed build would resolve indeterminate. Like an
89
89
// unbounded budget, an unusable verdict is refused by construction.
90
90
ErrPoolTooSmall=errors.New("concurrent index build needs a pool of at least two connections: one for the build session, one reserved for the catalog verdict")
91
+
// ErrCallerOwnedNeedsCancellableContext is returned when caller-owned
92
+
// mode has no cancellation signal to bound the statement.
93
+
ErrCallerOwnedNeedsCancellableContext=errors.New("a caller-owned build needs a cancellable context: with statement_timeout disabled the context is the statement's only bound")
91
94
// ErrCancelledExternally is returned when the build's statement was
92
95
// cancelled (SQLSTATE 57014) before its overall budget elapsed: the
93
96
// executor's statement_timeout cannot have fired yet, so the
0 commit comments