Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c7c8f9e
feat(worker): make the queue-storage prune lock timeout configurable …
hardbyte Nov 5, 2026
aa9c2fb
test(worker): make the prune metric runtime test independent of ring …
hardbyte Nov 5, 2026
ed89d9f
fix(worker): bound the prune lock timeout and tighten its docs and tests
hardbyte Nov 5, 2026
8a8d653
fix(queue-storage): create queue lanes as the schema owner (0.6)
hardbyte Nov 6, 2026
5c03aed
test: expect restricted runtimes to create lanes through the schema o…
hardbyte Nov 6, 2026
32d671b
fix(queue-storage): accept every lane the invoker path accepts in pro…
hardbyte Nov 6, 2026
2ce3a31
fix(queue-storage): promote each enqueue lane in its own transaction …
hardbyte Nov 6, 2026
7e6259e
fix(worker): drain finalization on graceful shutdown (0.6)
hardbyte Nov 6, 2026
4a46e89
feat(worker): warn when the prune lock timeout reaches deadlock_timeo…
hardbyte Nov 6, 2026
ee17861
fix(queue-storage): skip locked due rows in promotion selection and b…
hardbyte Nov 6, 2026
82ff82d
fix(queue-storage): repair lane sequences per transaction with a boun…
hardbyte Nov 6, 2026
a075705
docs(security): clarify lane sequence repair outcomes
hardbyte Nov 6, 2026
39c6a2c
docs(worker): describe prune timeout per lock acquisition
hardbyte Nov 6, 2026
0b43d40
merge: prepare graceful shutdown after approved queue-storage fixes
hardbyte Nov 6, 2026
b47da52
merge: prepare approved release stack for #638
hardbyte Nov 6, 2026
5304546
merge: prepare approved release stack for #640
hardbyte Nov 6, 2026
a58759a
merge: prepare approved release stack for #630
hardbyte Nov 6, 2026
9dcb8c1
fix(queue-storage): ignore retired ready rows in point reads
hardbyte Nov 6, 2026
8804c72
merge: include live-ready point-read fix before release
hardbyte Nov 6, 2026
e8fbc43
merge: propagate live-ready point-read release fix
hardbyte Nov 6, 2026
310a333
merge: carry point-read correctness through prune timeout config
hardbyte Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,101 @@ Notable changes between releases. Detailed migration notes for storage transitio

## [Unreleased]

### Added

- **The queue-storage prune `lock_timeout` is configurable, and prune
attempts are counted by outcome for every ring.** Prune (queue, lease and
claim rings) and terminal-delta rollup take their exclusive locks under a
10 ms `lock_timeout` that could only be changed through the hidden
`QueueStorage::with_prune_lock_timeout`. When short readers of the ring
partitions held a conflicting lock at that instant, prune reported
`blocked` every tick and the rings stopped recycling. Set it with
`ClientBuilder::prune_lock_timeout` (Rust) or
`queue_storage_prune_lock_timeout_ms` on `client.start()` (Python); the
timeout applies separately to each lock acquisition, not to the whole prune;
queue prune can wait on up to eight partition locks. The default stays 10 ms and zero or a value above PostgreSQL's `lock_timeout`
maximum is rejected. Start logs a warning when the value is at or above the
connection's `deadlock_timeout` (prune waiting that long can make deadlock
detection cancel a claim instead) or `statement_timeout`. `with_prune_lock_timeout` is now
public, with `set_prune_lock_timeout` and `prune_lock_timeout()`.
`awa_maintenance_prune_attempts_total{awa_ring,awa_ring_outcome,awa_ring_reason}`
gains `backed_off` (prune skipped by the backoff that follows consecutive
`skipped_active` or `blocked` results) and
`error` (prune failed with a database error) alongside the existing
`pruned`, `noop`, `blocked` and `skipped_active`. See
[Configuration](docs/configuration.md#what-the-knobs-mean).

### Fixed

- **On queue storage, a queue whose enqueue lane is busy no longer holds
back promotion of other queues, nor the maintenance leader's rescue,
rotation and prune.** Promotion re-enqueues due scheduled and retryable
rows into their lane, which takes the lane's reservation lock that
producers hold until they commit. A promotion batch was one transaction
across every queue it touched, and the leader runs its branches one after
another, so a lane kept busy by producers delayed due jobs of unrelated
queues in the same batch, and rescue started seconds late. Promotion now
gives each `(queue, priority)` lane its own transaction under a 50 ms
`lock_timeout`. A lane that times out keeps its rows deferred and is
handed to a dedicated task that waits up to 30 s for the lock, queued
behind the producers already waiting, so the lane is still promoted in
turn however busy it stays; the leader skips it meanwhile. Each waiter
holds a pool connection, so the leader runs one per eight pool
connections, at least one and at most four; a further contended lane is
retried on the next tick. Batch selection skips rows another transaction
has locked, so locked due rows cannot fill every batch of a tick. `awa.maintenance.promote.lane_contention`
(`awa_maintenance_promote_lane_contention_total{awa_job_state,awa_job_queue,awa_promote_outcome}`)
counts `handed_off`, `waiter_limit` and `waiter_timed_out`. Promoted rows
now take lane sequence numbers in due order within their lane.
`QueueStorage::promote_due` keeps its signature and promotes lane by lane
without a lock timeout; `promote_due_lanes` and `promote_due_lane` expose
the per-lane form.
- **A queue lane is usable by every runtime role, whichever role creates it
first.** A lane's enqueue and claim sequences belonged to the role that
first enqueued into, claimed from or promoted into it, so with several
runtime roles sharing a group role, any role other than the creator whose
default privileges did not cover it failed with `permission denied for
sequence`. That included the maintenance leader promoting into a shard
created after `enqueue_shards` was raised. The new schema patch
`definer_lane_provisioning` creates lanes through two `SECURITY DEFINER`
functions per queue-storage schema, `provision_lane` and
`provision_lane_sequences`, owned by the schema owner. Each new sequence gets
the privileges of the head table it backs, explicitly rather than through
default privileges. A runtime without schema `CREATE` can therefore create
lanes itself, and `awa storage prepare-queue` is optional and may run as any
runtime role. Once the patch commits, `awa migrate` gives existing lane
sequences to the schema owner and adds their missing grants, one sequence
per transaction with a 300 ms lock wait, and only where it is allowed to,
trying the effective role and then the login role. A sequence an open
transaction holds is skipped and reported as busy; one it may not change is
reported with the roles that can repair it. Neither fails the run. The new read-only `awa storage lane-sequences` (or
`awa.lane_sequence_problems(schema)`) lists what remains, with the
statements to run and the roles to run them as, and exits 2 while any
remain; `--repair` (or `awa.repair_lane_sequence(schema, sequence)` per
sequence) repairs what the connected role may. `awa migrate` prints a summary of the leftovers. A binary
running before the patch detects that `provision_lane` is missing and
creates lanes as before, and older binaries create correctly owned lanes once
the patch has refreshed the helpers they call. `awa migrate` applies the
patch on any v040 database; external runners apply the exported
`R__definer_lane_provisioning.sql` after `V40`. See
[`docs/upgrade-0.6.13-to-0.6.14.md`](docs/upgrade-0.6.13-to-0.6.14.md).
- **A graceful shutdown no longer leaves a finished attempt for stale-heartbeat
rescue to run again.** Once a handler returned, its finalize (completion,
retry, cancel or failure, and any ADR-029 follow-up inserts) ran in a task
that `Client::shutdown` did not wait for. When the last handler returned
just as the drain ended, shutdown stopped the completion batcher before
that finalize reached it, and the direct fallback raced the process exit.
The attempt had already left the heartbeat set, so it went stale and was
rescued and executed again `heartbeat_staleness` later; a deploy could
re-run a few jobs on every rolling restart. Shutdown now waits for
pending finalization, within the same drain timeout, before it stops the
completion batcher, heartbeat and maintenance services. A finalize that
outlives the timeout is still left to rescue, as before.
- **Point reads show the live queue and priority after a lane move.** Retained
ready rows from before a priority or queue change could win a read's tie
after the ring wrapped, returning the old fields even though the batch
operation succeeded. Point reads now exclude tombstoned source rows.

- **Callback completion, `get_job`, lifecycle events and the admin reads
find running jobs that priority aging promoted on queue storage** (backport
of #600). Claims and leases record a job's aged effective priority while its
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 77 additions & 1 deletion awa-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ enum StorageCommands {
#[arg(long)]
reset: bool,
},
/// Provision lane sequences as the migrator, before starting a runtime without DDL
/// Provision every priority and shard of a queue ahead of its first enqueue
PrepareQueue {
/// Queue-storage schema to provision
#[arg(long, default_value = "awa")]
Expand All @@ -432,6 +432,17 @@ enum StorageCommands {
#[arg(long, default_value_t = 1)]
queue_stripe_count: usize,
},
/// List lane sequences not owned by the schema owner or missing runtime
/// grants, with the statements that fix them. Exits 2 while any remain.
LaneSequences {
/// Queue-storage schema to check
#[arg(long, default_value = "awa")]
schema: String,
/// Reassign and grant first, as far as the connected role (or, failing
/// that, its login role) is allowed to
#[arg(long)]
repair: bool,
},
/// Abort a prepared or mixed-transition storage rollout before final activation
Abort,
/// Enter mixed transition and begin routing new writes to the prepared engine
Expand Down Expand Up @@ -669,6 +680,26 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
if !report.complete() {
std::process::exit(1);
}
if report.patches.iter().any(|patch| {
patch.name == "definer_lane_provisioning"
&& patch.outcome == awa_model::migrations::PatchOutcome::AlreadyPresent
}) {
// The patch was already in place, so this run repaired
// nothing: report what an earlier repair left behind.
let mut problems = Vec::new();
for (schema, _) in
awa_model::storage::lane_sequence_problem_counts(&pool).await?
{
problems.extend(
awa_model::storage::lane_sequence_problems(&pool, &schema).await?,
);
}
for next_step in awa_model::storage::lane_sequence_next_steps(
problems.iter().map(|problem| (problem, false)),
) {
eprintln!("warning: {next_step}");
}
}
}
}

Expand Down Expand Up @@ -1245,6 +1276,39 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}))?
);
}
StorageCommands::LaneSequences { schema, repair } => {
let next_steps = if repair {
let mut conn = pool.acquire().await?;
let repairs =
awa_model::storage::repair_lane_sequences(&mut conn, &schema)
.await?;
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"schema": schema, "results": repairs,
}))?
);
awa_model::storage::lane_sequence_repair_next_steps(&repairs)
} else {
let problems =
awa_model::storage::lane_sequence_problems(&pool, &schema).await?;
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"schema": schema, "problems": problems,
}))?
);
awa_model::storage::lane_sequence_next_steps(
problems.iter().map(|problem| (problem, false)),
)
};
if !next_steps.is_empty() {
for next_step in next_steps {
eprintln!("{next_step}");
}
std::process::exit(2);
}
}
StorageCommands::Abort => {
awa_model::storage::abort(&pool).await?;
let report = awa_model::storage::status_report(&pool).await?;
Expand Down Expand Up @@ -1420,6 +1484,18 @@ fn print_migration_report(report: &awa_model::migrations::MigrationReport) {
);
}
}
if !report.lane_sequences.is_empty() {
let repaired = report
.lane_sequences
.iter()
.filter(|repair| repair.outcome == awa_model::storage::LaneSequenceOutcome::Repaired)
.count();
// Sequences left behind are logged by the migration runner.
println!(
"Lane sequences repaired: {repaired} of {}",
report.lane_sequences.len()
);
}
match (
&report.storage_routing_before,
&report.storage_routing_after,
Expand Down
11 changes: 5 additions & 6 deletions awa-cli/tests/migrate_patch_cli_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,11 @@ fn psql(database_url: &str, sql: &str, lock_timeout: Option<&str>) -> Output {
let mut child = command
.spawn()
.expect("psql must be on PATH for the exported-SQL test");
child
.stdin
.take()
.unwrap()
.write_all(sql.as_bytes())
.unwrap();
// psql stops reading once ON_ERROR_STOP fires, which can be before the
// whole script is written.
if let Err(err) = child.stdin.take().unwrap().write_all(sql.as_bytes()) {
assert_eq!(err.kind(), std::io::ErrorKind::BrokenPipe, "{err}");
}
child.wait_with_output().unwrap()
}

Expand Down
Loading
Loading