Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ Notable changes between releases. Detailed migration notes for storage transitio

### Fixed

- Ordinary rescue ticks retain their cadence under scheduler jitter, while
backlog re-arms keep the heartbeat owner-evidence gate.

- Keep confirmed-owner bulk heartbeat rescue's first cursor window small so
short passes can reach follow-up batches. Unconfirmed owners and
configurations that disable acceleration retain the wider initial scan.

- Queue-storage rescue drains large abandoned-claim backlogs in bounded
transactions, controlled by `rescue_pass_budget` and a soft
`rescue_pass_max_rows` cap. Cursor sweeps continue past closed or freshly
heartbeated history and stop after a full traversal. Accelerated heartbeat
rescue requires settled runtime evidence; ordinary first batches keep their
configured cadence. Retries retain their backoffs, and due times can cluster:
the row cap bounds rescue writes, not promotion throughput. Zero for either
pass control keeps one batch per ordinary interval. See the
[backlog-drain configuration](docs/configuration.md#rescue-backlog).

- Finished attempts keep heartbeating until their durable finalization finishes,
including while completion writes are stalled. Compact receipt heartbeats
address claims by batch identity and skip attempts a closer already holds,
so one stalled completion cannot block the rest of a heartbeat pass. Late
heartbeats cannot recreate state for a completed attempt.

- **Batch enqueues take their lane locks in one order, so they no longer
deadlock with each other.** On queue storage every enqueue holds a
transaction-scoped advisory lock on its `(queue, priority, shard)` lane
Expand Down
51 changes: 50 additions & 1 deletion awa-metrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ pub mod names {
pub const COMPLETION_FLUSH_DURATION: &str = "awa.completion.flush_duration";
pub const HEARTBEAT_BATCHES: &str = "awa.heartbeat.batches";
pub const MAINTENANCE_RESCUES: &str = "awa.maintenance.rescues";
pub const MAINTENANCE_RESCUE_PASS_SIZE: &str = "awa.maintenance.rescue.pass_size";
pub const MAINTENANCE_RESCUE_OLDEST_AGE: &str = "awa.maintenance.rescue.oldest_attempt_age";
pub const MAINTENANCE_PROMOTE_BATCHES: &str = "awa.maintenance.promote_batches";
pub const MAINTENANCE_PROMOTE_BATCH_SIZE: &str = "awa.maintenance.promote_batch_size";
pub const MAINTENANCE_PROMOTE_DURATION: &str = "awa.maintenance.promote_duration";
Expand Down Expand Up @@ -198,6 +200,14 @@ pub struct AwaMetrics {
pub heartbeat_batches: Counter<u64>,
/// Total maintenance rescue operations.
pub maintenance_rescues: Counter<u64>,
/// Attempts rescued per queue-storage rescue pass, attributed by
/// `awa.rescue.kind` and why the pass stopped (`awa.rescue.pass_end`).
pub maintenance_rescue_pass_size: Histogram<u64>,
/// Age of the oldest attempt the latest queue-storage rescue pass
/// rescued, per `awa.rescue.kind`; 0 when it rescued nothing. Rescue
/// works oldest claim first, so a value well above the heartbeat
/// staleness or deadline means a backlog of abandoned claims.
pub maintenance_rescue_oldest_attempt_age_seconds: Gauge<f64>,
/// Total jobs parked for external callback.
pub jobs_waiting_external: Counter<u64>,
/// Current queue depth per state — how many jobs are in each state per queue.
Expand Down Expand Up @@ -494,6 +504,19 @@ impl AwaMetrics {
.with_description("Number of jobs rescued by maintenance")
.with_unit("{job}")
.build(),
maintenance_rescue_pass_size: meter
.u64_histogram(names::MAINTENANCE_RESCUE_PASS_SIZE)
.with_description("Attempts rescued per queue-storage rescue pass")
.with_unit("{job}")
.with_boundaries(COUNT_BUCKETS.to_vec())
.build(),
maintenance_rescue_oldest_attempt_age_seconds: meter
.f64_gauge(names::MAINTENANCE_RESCUE_OLDEST_AGE)
.with_description(
"Age of the oldest attempt rescued by the latest queue-storage rescue pass",
)
.with_unit("s")
.build(),
jobs_waiting_external: meter
.u64_counter(names::JOB_WAITING_EXTERNAL)
.with_description("Number of jobs parked for external callback")
Expand Down Expand Up @@ -894,6 +917,28 @@ impl AwaMetrics {
.record(duration.as_secs_f64(), &attrs);
}

/// Record one queue-storage rescue pass: how many attempts it rescued,
/// why it stopped, and the age of the oldest attempt it rescued.
pub fn record_rescue_pass(
&self,
kind: &'static str,
pass_end: &'static str,
rescued: u64,
oldest_attempt_age: Duration,
) {
self.maintenance_rescue_pass_size.record(
rescued,
&[
opentelemetry::KeyValue::new("awa.rescue.kind", kind),
opentelemetry::KeyValue::new("awa.rescue.pass_end", pass_end),
],
);
self.maintenance_rescue_oldest_attempt_age_seconds.record(
oldest_attempt_age.as_secs_f64(),
&[opentelemetry::KeyValue::new("awa.rescue.kind", kind)],
);
}

/// Record a scheduled/retryable promotion batch.
pub fn record_promotion_batch(&self, state: &str, batch_size: u64, duration: Duration) {
let attrs = [opentelemetry::KeyValue::new(
Expand Down Expand Up @@ -1504,7 +1549,7 @@ mod tests {
&JOB_DURATION_BUCKETS_SECONDS,
),
];
let counts: [(&str, &Histogram<u64>); 7] = [
let counts: [(&str, &Histogram<u64>); 8] = [
(names::ENQUEUE_BATCH_SIZE, &metrics.enqueue_batch_size),
(
names::DISPATCH_CAPACITY_AVAILABLE,
Expand All @@ -1519,6 +1564,10 @@ mod tests {
names::MAINTENANCE_PROMOTE_BATCH_SIZE,
&metrics.promotion_batch_size,
),
(
names::MAINTENANCE_RESCUE_PASS_SIZE,
&metrics.maintenance_rescue_pass_size,
),
(
names::MAINTENANCE_ROTATE_SKIPPED_ROWS,
&metrics.maintenance_rotate_skipped_rows,
Expand Down
3 changes: 2 additions & 1 deletion awa-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ pub use partitioned_queue::{
};
pub use queue_storage::{
ClaimedEntry, ClaimedRuntimeJob, PruneDurations, PruneOutcome, QueueCounts, QueueStorage,
QueueStorageConfig, RingLedgerFoldOutcome, RingRotationCadence, RotateOutcome, SkipReason,
QueueStorageConfig, RescueBatch, RescueBatchOptions, RescueScanCursor, RescueScanProgress,
RescueSlotOrder, RingLedgerFoldOutcome, RingRotationCadence, RotateOutcome, SkipReason,
TerminalDeltaRollupOutcome, TerminalRollupFoldOutcome,
};
pub use storage::StorageStatus;
Expand Down
Loading
Loading