Skip to content

Commit a4480c6

Browse files
antiguruclaude
andcommitted
compute: serve fast-path peeks on the interactive runtime
The interactive runtime holds no local traces, so an index peek there resolves against the sharing registry instead. `PeekScan` and the error walk beneath it become generic over the traces they read, and an interactive peek opens its scan over the registry's `SharedOksHandle` and `SharedErrsHandle`. Both flavours of index peek then spend one budget, report one set of metrics, and reach the peek stash through the one offload driver. A shared peek whose arrangement is not yet published, or whose upper has not sealed the peek timestamp, waits in `pending_work` keyed by a `WorkId` and indexed by the id it waits on. A publication or seal marks that id dirty and wakes the worker, which gives a turn to exactly the items indexed under the ids that changed, so wakeups scale with what changed rather than with total pending work. Past that gate a shared peek is an ordinary index peek: it queues for a turn like any other, and a walk that outruns the activation's fuel or fills a batch bound for the stash leaves for a driver. The sweep still runs on every step, because a persist read and an offloaded walk each wake the worker through a channel of their own rather than through the dirty set, but nothing waiting on a publication or a seal is ever swept. Interactive dataflows build immediately in command arrival order rather than deferring until their dependency is published. An import over an unadopted placeholder produces no data and holds its output frontier at the minimum until a publisher adopts the same slot, so late binding replaces the deferral. The interactive runtime reports only its transient collections' frontiers. It shares the identity of every non-transient collection with maintenance, which owns and reports the real frontiers, and the controller keeps one frontier stream per collection, so reporting the shared ones would race the owner and regress it. For the same reason its logging is forced off: it serves introspection peeks from maintenance's published copies, and its own empty copies would clobber them. The consequence is that nothing the interactive runtime does appears in introspection, tracked as CPU-222. Reconciliation drops `pending_work` and `dep_index`, whose peeks belong to the reconciled-away connection. The standing holds in the registry deliberately survive: one is per collection, carries no dataflow identity, and only rises, so clearing it would drop the arrangement's bound to the minimum until replayed compactions raised it again. Reachable only on a runtime holding the `Interactive` role, which requires the dyncfg that is still off everywhere. Tests are out of line in `compute_state/tests.rs`, per the convention in `src/compute/AGENTS.md`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 840ff11 commit a4480c6

13 files changed

Lines changed: 1909 additions & 238 deletions

File tree

src/compute/src/compute_state.rs

Lines changed: 463 additions & 173 deletions
Large diffs are not rendered by default.

src/compute/src/compute_state/error_scan.rs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,60 @@
88
99
use std::time::{Duration, Instant};
1010

11-
use differential_dataflow::trace::{Cursor, TraceReader};
11+
use differential_dataflow::trace::{Cursor, Navigable, TraceReader};
1212
use mz_compute_client::protocol::response::PeekError;
1313
use mz_repr::{Diff, GlobalId, Timestamp};
1414
use timely::order::PartialOrder;
1515
use tracing::error;
1616

1717
use crate::arrangement::manager::PaddedTrace;
1818
use crate::compute_state::{PeekRowIterationTracker, peek_result_iterator};
19+
use crate::render::errors::DataflowErrorSer;
1920
use crate::typedefs::ErrAgent;
2021

2122
/// The error trace of an index, as
2223
/// [`TraceBundle::errs_mut`](crate::arrangement::manager::TraceBundle::errs_mut) hands it out.
2324
pub(super) type ErrsHandle = PaddedTrace<ErrAgent<Timestamp, Diff>>;
2425

26+
/// A trace an index peek's error walk can read.
27+
///
28+
/// The bound is spelled once here, so the walk and everything that carries it name the shape
29+
/// rather than restate it.
30+
pub(super) trait PeekErrsTrace:
31+
TraceReader<
32+
Time = Timestamp,
33+
Batch: Navigable<
34+
Cursor: for<'a> Cursor<
35+
Key<'a> = &'a DataflowErrorSer,
36+
TimeGat<'a>: PartialOrder<Timestamp>,
37+
DiffGat<'a> = &'a Diff,
38+
>,
39+
>,
40+
>
41+
{
42+
}
43+
44+
impl<Tr> PeekErrsTrace for Tr where
45+
Tr: TraceReader<
46+
Time = Timestamp,
47+
Batch: Navigable<
48+
Cursor: for<'a> Cursor<
49+
Key<'a> = &'a DataflowErrorSer,
50+
TimeGat<'a>: PartialOrder<Timestamp>,
51+
DiffGat<'a> = &'a Diff,
52+
>,
53+
>,
54+
>
55+
{
56+
}
57+
2558
/// A walk over an index peek's error trace, suspendable between cursor positions.
2659
///
2760
/// Holds nothing of the ok trace or of the rows a peek returns. A peek reaches those only once
2861
/// this walk reports [`ErrorScanStep::Finished`] with an `Ok`.
29-
pub(super) struct ErrorScan {
30-
cursor: peek_result_iterator::TraceCursor<ErrsHandle>,
31-
storage: peek_result_iterator::TraceStorage<ErrsHandle>,
62+
pub(super) struct ErrorScan<Tr: PeekErrsTrace> {
63+
cursor: peek_result_iterator::TraceCursor<Tr>,
64+
storage: peek_result_iterator::TraceStorage<Tr>,
3265
/// The limit spans this walk and the ok scan after it, so the count accrued here is handed
3366
/// on with [`ErrorScanStep::Finished`].
3467
row_iteration_tracker: PeekRowIterationTracker,
@@ -48,12 +81,12 @@ pub(super) enum ErrorScanStep {
4881
OutOfFuel,
4982
}
5083

51-
impl ErrorScan {
84+
impl<Tr: PeekErrsTrace> ErrorScan<Tr> {
5285
/// Opens a walk over `errs`.
5386
///
5487
/// The walk starts without a row-iteration limit. The limit in effect is the caller's to
5588
/// supply through [`ErrorScan::set_row_iteration_limit`] before each step.
56-
pub(super) fn new(errs: &mut ErrsHandle) -> Self {
89+
pub(super) fn new(errs: &mut Tr) -> Self {
5790
let scan_start = Instant::now();
5891
let (cursor, storage) = errs.cursor();
5992
let mut scan = Self::from_cursor(cursor, storage);
@@ -63,8 +96,8 @@ impl ErrorScan {
6396

6497
/// Opens a walk over an already-opened cursor.
6598
pub(super) fn from_cursor(
66-
cursor: peek_result_iterator::TraceCursor<ErrsHandle>,
67-
storage: peek_result_iterator::TraceStorage<ErrsHandle>,
99+
cursor: peek_result_iterator::TraceCursor<Tr>,
100+
storage: peek_result_iterator::TraceStorage<Tr>,
68101
) -> Self {
69102
Self {
70103
cursor,

src/compute/src/compute_state/error_scan/tests.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ pub(crate) fn error_batch(
5151

5252
/// Builds a walk over a single-batch error trace holding `updates`, bounded by
5353
/// `row_iteration_limit`.
54-
pub(crate) fn error_scan(updates: ErrorUpdates, row_iteration_limit: Option<usize>) -> ErrorScan {
54+
pub(crate) fn error_scan(
55+
updates: ErrorUpdates,
56+
row_iteration_limit: Option<usize>,
57+
) -> ErrorScan<ErrsHandle> {
5558
let storage = vec![error_batch(updates)];
5659
let cursor = CursorList::new(vec![storage[0].cursor()], &storage);
5760
let mut scan = ErrorScan::from_cursor(cursor, storage);
@@ -79,7 +82,10 @@ pub(crate) fn holding(error: &DataflowErrorSer) -> ErrorUpdates {
7982

8083
/// Runs `scan` to an answer in slices of `fuel_per_step` units, and returns that answer, the
8184
/// fuel the walk spent, and the number of calls it took.
82-
fn run_sliced(scan: &mut ErrorScan, fuel_per_step: usize) -> (ErrorScanStep, usize, usize) {
85+
fn run_sliced(
86+
scan: &mut ErrorScan<ErrsHandle>,
87+
fuel_per_step: usize,
88+
) -> (ErrorScanStep, usize, usize) {
8389
let mut consumed = 0;
8490
// Bounded so that a walk which restarts from the first key on each resumption fails the
8591
// test instead of hanging it.

src/compute/src/compute_state/index_peek_tests.rs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use super::error_scan::tests::{
2727
ErrorUpdates, PEEK_TIMESTAMP, cancelling, error, error_batch, holding,
2828
};
2929
use super::*;
30+
use crate::arrangement::manager::TraceBundle;
3031

3132
/// The collection the peeks in these tests read.
3233
pub(crate) const TARGET_ID: GlobalId = GlobalId::User(1);
@@ -198,10 +199,10 @@ impl TestMetrics {
198199
}
199200
}
200201

201-
/// How often each metric that `collect_finished_data` can observe into was observed.
202+
/// How often each metric that a walk can observe into was observed.
202203
///
203204
/// The two histograms the enclosing `seek_fulfillment` owns are left out, because the
204-
/// tests that read this call `collect_finished_data` directly.
205+
/// tests that read this call [`collect`] directly.
205206
fn observations(&self) -> BTreeMap<&'static str, u64> {
206207
let metrics = &self.metrics;
207208
BTreeMap::from([
@@ -282,8 +283,8 @@ enum Answer {
282283
Ready(PeekResponse),
283284
}
284285

285-
impl From<PeekStatus> for Answer {
286-
fn from(status: PeekStatus) -> Self {
286+
impl From<PeekStatus<IndexPeekScan>> for Answer {
287+
fn from(status: PeekStatus<IndexPeekScan>) -> Self {
287288
match status {
288289
PeekStatus::NotReady => Answer::NotReady,
289290
// The scan an offload carries has no comparison of its own. What is comparable
@@ -295,10 +296,35 @@ impl From<PeekStatus> for Answer {
295296
}
296297

297298
/// An index peek of `peek` over an index holding `keys` and `errors`.
299+
/// Walks `subject` without the frontier gate, so the observations are the walk's alone.
300+
fn collect(
301+
subject: &mut IndexPeek,
302+
max_result_size: u64,
303+
stash: StashBounds,
304+
row_iteration_limit: Option<usize>,
305+
fuel: &mut usize,
306+
metrics: &IndexPeekMetrics<'_>,
307+
) -> PeekStatus<IndexPeekScan> {
308+
let (oks, errs) = subject
309+
.traces
310+
.resolve(subject.peek.target.id())
311+
.expect("local traces resolve");
312+
IndexPeek::walk_traces(
313+
&subject.peek,
314+
oks,
315+
errs,
316+
max_result_size,
317+
stash,
318+
row_iteration_limit,
319+
fuel,
320+
metrics,
321+
)
322+
}
323+
298324
fn index_peek_over(peek: Peek, keys: &[Row], errors: ErrorUpdates) -> IndexPeek {
299325
IndexPeek {
300326
peek,
301-
trace_bundle: trace_bundle(keys, errors),
327+
traces: IndexTraces::Local(trace_bundle(keys, errors)),
302328
span: tracing::Span::none(),
303329
}
304330
}
@@ -330,7 +356,8 @@ fn a_completed_scan_answers_with_rows_and_reports_every_phase() {
330356
);
331357
let metrics = TestMetrics::new();
332358

333-
let answer = subject.collect_finished_data(
359+
let answer = collect(
360+
&mut subject,
334361
u64::MAX,
335362
NO_STASH,
336363
None,
@@ -356,7 +383,8 @@ fn an_error_answered_peek_reports_no_phase_timers() {
356383
let mut subject = index_peek_over(index_peek(trivial_finishing(), None), &keys, errors);
357384
let metrics = TestMetrics::new();
358385

359-
let answer = subject.collect_finished_data(
386+
let answer = collect(
387+
&mut subject,
360388
u64::MAX,
361389
NO_STASH,
362390
None,
@@ -390,7 +418,8 @@ fn a_scan_that_fills_a_batch_leaves_the_worker_with_fuel_to_spare() {
390418
// A threshold of zero bytes is crossed by the first row, so the scan fills a batch well
391419
// before the trace runs out and well before unbounded fuel could run out.
392420
let mut fuel = unbounded_fuel();
393-
let answer = subject.collect_finished_data(
421+
let answer = collect(
422+
&mut subject,
394423
u64::MAX,
395424
STASH_EVERYTHING,
396425
None,
@@ -420,7 +449,8 @@ fn a_batch_ready_suspension_out_of_fuel_is_offloaded_too() {
420449
// suspends holding a full batch and out of fuel, with both causes of a suspension in force
421450
// at once.
422451
let mut fuel = 1;
423-
let answer = subject.collect_finished_data(
452+
let answer = collect(
453+
&mut subject,
424454
u64::MAX,
425455
STASH_EVERYTHING,
426456
None,
@@ -451,8 +481,14 @@ fn a_scan_that_outruns_its_fuel_leaves_the_worker_reporting_nothing() {
451481
// An empty error trace is walked out within a position or two, so this fuel is spent
452482
// inside the ok walk with most of the six keys still ahead of it.
453483
let mut fuel = 2;
454-
let answer =
455-
subject.collect_finished_data(u64::MAX, NO_STASH, None, &mut fuel, &metrics.as_metrics());
484+
let answer = collect(
485+
&mut subject,
486+
u64::MAX,
487+
NO_STASH,
488+
None,
489+
&mut fuel,
490+
&metrics.as_metrics(),
491+
);
456492

457493
assert_eq!(Answer::from(answer), Answer::Offload);
458494
assert_eq!(
@@ -482,7 +518,8 @@ fn an_ok_phase_failure_reports_the_phases_the_walk_reached() {
482518
// A ceiling of one byte is crossed by the first row the ok walk produces, so the peek
483519
// fails inside that walk rather than in the error walk before it.
484520
let max_result_size = 1;
485-
let answer = subject.collect_finished_data(
521+
let answer = collect(
522+
&mut subject,
486523
max_result_size,
487524
NO_STASH,
488525
None,
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright Materialize, Inc. and contributors. All rights reserved.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the LICENSE file.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0.
9+
10+
//! The traces an index peek reads, from an arrangement this runtime maintains or one the sharing
11+
//! registry publishes.
12+
13+
use differential_dataflow::trace::TraceReader;
14+
use mz_repr::{Diff, GlobalId, Timestamp};
15+
use timely::progress::frontier::AntichainRef;
16+
17+
use crate::arrangement::manager::{PaddedTrace, TraceBundle};
18+
use crate::compute_state::error_scan::ErrsHandle;
19+
use crate::shared_trace::{SharedErrsHandle, SharedOksHandle};
20+
use crate::sharing::ArrangementSharingRegistry;
21+
use crate::typedefs::{ErrSpine, RowRowAgent, RowRowSpine};
22+
23+
/// Where an index peek finds the traces that answer it.
24+
pub(super) enum IndexTraces {
25+
/// Traces this runtime maintains, pinned for the peek's life.
26+
Local(TraceBundle),
27+
/// An arrangement the sharing registry publishes, resolved on every attempt. A parked peek
28+
/// holds nothing of the arrangement, so an unpublished slot registers no hold at the minimum.
29+
Shared {
30+
registry: ArrangementSharingRegistry,
31+
worker_index: usize,
32+
},
33+
}
34+
35+
impl IndexTraces {
36+
/// Handles on the traces of `id` for one attempt, or `None` while a shared index is
37+
/// unpublished.
38+
///
39+
/// Both variants hand out owned handles so the scan can carry them off the worker. A local
40+
/// handle is a clone of the pinned one, which registers a hold the pinned one already keeps.
41+
pub(super) fn resolve(&mut self, id: GlobalId) -> Option<(PeekOks, PeekErrs)> {
42+
match self {
43+
IndexTraces::Local(bundle) => {
44+
let (oks, errs) = bundle.oks_errs_mut();
45+
Some((PeekOks::Local(oks.clone()), PeekErrs::Local(errs.clone())))
46+
}
47+
IndexTraces::Shared {
48+
registry,
49+
worker_index,
50+
} => registry
51+
.handles(&id, *worker_index)
52+
.map(|(oks, errs)| (PeekOks::Shared(oks), PeekErrs::Shared(errs))),
53+
}
54+
}
55+
}
56+
57+
/// The ok trace an index peek reads.
58+
pub(super) enum PeekOks {
59+
Local(PaddedTrace<RowRowAgent<Timestamp, Diff>>),
60+
Shared(SharedOksHandle),
61+
}
62+
63+
/// The error trace an index peek reads.
64+
pub(super) enum PeekErrs {
65+
Local(ErrsHandle),
66+
Shared(SharedErrsHandle),
67+
}
68+
69+
/// Both variants read the same batch type, so the enum is a `TraceReader` by delegation.
70+
macro_rules! delegate_trace_reader {
71+
($ty:ident, $spine:ty) => {
72+
impl TraceReader for $ty {
73+
type Time = Timestamp;
74+
type Batch = <$spine as TraceReader>::Batch;
75+
76+
fn set_logical_compaction(&mut self, frontier: AntichainRef<Timestamp>) {
77+
match self {
78+
$ty::Local(trace) => trace.set_logical_compaction(frontier),
79+
$ty::Shared(trace) => trace.set_logical_compaction(frontier),
80+
}
81+
}
82+
83+
fn get_logical_compaction(&mut self) -> AntichainRef<'_, Timestamp> {
84+
match self {
85+
$ty::Local(trace) => trace.get_logical_compaction(),
86+
$ty::Shared(trace) => trace.get_logical_compaction(),
87+
}
88+
}
89+
90+
fn set_physical_compaction(&mut self, frontier: AntichainRef<Timestamp>) {
91+
match self {
92+
$ty::Local(trace) => trace.set_physical_compaction(frontier),
93+
$ty::Shared(trace) => trace.set_physical_compaction(frontier),
94+
}
95+
}
96+
97+
fn get_physical_compaction(&mut self) -> AntichainRef<'_, Timestamp> {
98+
match self {
99+
$ty::Local(trace) => trace.get_physical_compaction(),
100+
$ty::Shared(trace) => trace.get_physical_compaction(),
101+
}
102+
}
103+
104+
fn map_batches<F: FnMut(&Self::Batch)>(&self, f: F) {
105+
match self {
106+
$ty::Local(trace) => trace.map_batches(f),
107+
$ty::Shared(trace) => trace.map_batches(f),
108+
}
109+
}
110+
111+
fn batches_through(
112+
&mut self,
113+
upper: AntichainRef<Timestamp>,
114+
) -> Option<Vec<Self::Batch>> {
115+
match self {
116+
$ty::Local(trace) => trace.batches_through(upper),
117+
$ty::Shared(trace) => trace.batches_through(upper),
118+
}
119+
}
120+
}
121+
};
122+
}
123+
124+
delegate_trace_reader!(PeekOks, RowRowSpine<Timestamp, Diff>);
125+
delegate_trace_reader!(PeekErrs, ErrSpine<Timestamp, Diff>);

0 commit comments

Comments
 (0)