Skip to content

Commit d31414b

Browse files
antiguruclaude
andcommitted
compute: publish maintained indexes into the sharing registry
Both export paths now publish their `oks`/`errs` arrangements into the per-process registry when the runtime's role publishes, and the two re-export arms register the alias so a read waiting on the aliased id's seal is woken by the original's publisher. Logging indexes publish the same way, gated strictly on `Maintenance`: an interactive runtime reads maintenance's slot, and its own copy would clobber it, while `Solo` has no registry peer at all. `ComputeRuntimeRole::Interactive` stops being test-only. Nothing constructs it yet, but `publishes()` has to name it, and `pub mod server` keeps the variant reachable so dead-code analysis is satisfied without an attribute. The stale `owns_process_globals` note claiming every constructible role owns the globals goes with it. Carrying the role and the registry to the render path is what the rest of this change is: `Config` and `Worker` gain both, `ComputeState` stores them and exposes `role()`, and clusterd builds one registry per process. Per process, not per runtime, because a reader on one runtime looks up the slot a publisher on another filled. No behavior change. `Solo` is the only role anything constructs and it does not publish, so every added block is skipped and no dataflow gains an operator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 92a68e1 commit d31414b

5 files changed

Lines changed: 309 additions & 17 deletions

File tree

src/clusterd/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use mz_build_info::{BuildInfo, build_info};
2222
use mz_cloud_resources::AwsExternalIdPrefix;
2323
use mz_cluster_client::client::TimelyConfig;
2424
use mz_compute::server::{ComputeInstanceContext, ComputeRuntimeRole};
25+
use mz_compute::sharing::ArrangementSharingRegistry;
2526
use mz_http_util::DynamicFilterTarget;
2627
use mz_orchestrator_tracing::{StaticTracingConfig, TracingCliArgs};
2728
use mz_ore::cli::{self, CliConfig};
@@ -468,11 +469,16 @@ async fn run(args: Args) -> Result<(), anyhow::Error> {
468469
);
469470

470471
// Start compute server.
472+
//
473+
// The sharing registry is per process rather than per runtime: a reader on one runtime looks up
474+
// the slot a publisher on another runtime filled, so both must hold the same registry.
475+
let sharing_registry = ArrangementSharingRegistry::new();
471476
let compute_client_builder = mz_compute::server::serve(
472477
compute_timely_config,
473478
ComputeRuntimeRole::Solo,
474479
&metrics_registry,
475480
persist_clients,
481+
sharing_registry,
476482
txns_ctx,
477483
tracing_handle,
478484
ComputeInstanceContext {

src/compute/src/compute_state.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ use crate::logging::compute::{CollectionLogging, ComputeEvent, PeekEvent};
7474
use crate::logging::initialize::LoggingTraces;
7575
use crate::metrics::{CollectionMetrics, WorkerMetrics};
7676
use crate::render::{LinearJoinSpec, StartSignal};
77-
use crate::server::{ComputeInstanceContext, ResponseSender};
77+
use crate::server::{ComputeInstanceContext, ComputeRuntimeRole, ResponseSender};
78+
use crate::sharing::ArrangementSharingRegistry;
7879

7980
mod peek_result_iterator;
8081
mod peek_stash;
@@ -114,6 +115,11 @@ pub struct ComputeState {
114115
/// A process-global cache of (blob_uri, consensus_uri) -> PersistClient.
115116
/// This is intentionally shared between workers.
116117
pub persist_clients: Arc<PersistClientCache>,
118+
/// A per-process registry of published index arrangements.
119+
///
120+
/// Intentionally shared between all workers of the process, each of which publishes into its own
121+
/// worker-ordinal slot. `Clone` shares the same underlying map.
122+
pub sharing_registry: ArrangementSharingRegistry,
117123
/// Context necessary for rendering txn-wal operators.
118124
pub txns_ctx: TxnsContext,
119125
/// History of commands received by this workers and all its peers.
@@ -172,12 +178,20 @@ pub struct ComputeState {
172178

173179
/// The storage worker forwards its introspection logs to the compute worker.
174180
pub storage_log_reader: Option<crate::server::StorageTimelyLogReader>,
181+
182+
/// Which of the process's compute runtimes this state belongs to.
183+
///
184+
/// Only the maintenance runtime runs the non-idempotent process-global initializers. The
185+
/// interactive runtime shares the same process and inherits those globals.
186+
role: ComputeRuntimeRole,
175187
}
176188

177189
impl ComputeState {
178190
/// Construct a new `ComputeState`.
179191
pub fn new(
192+
role: ComputeRuntimeRole,
180193
persist_clients: Arc<PersistClientCache>,
194+
sharing_registry: ArrangementSharingRegistry,
181195
txns_ctx: TxnsContext,
182196
metrics: WorkerMetrics,
183197
tracing_handle: Arc<TracingHandle>,
@@ -198,6 +212,7 @@ impl ComputeState {
198212
peek_stash_persist_location: None,
199213
compute_logger: None,
200214
persist_clients,
215+
sharing_registry,
201216
txns_ctx,
202217
command_history,
203218
max_result_size: u64::MAX,
@@ -213,9 +228,15 @@ impl ComputeState {
213228
init_system_time: mz_ore::now::SYSTEM_TIME(),
214229
replica_expiration: Antichain::default(),
215230
storage_log_reader,
231+
role,
216232
}
217233
}
218234

235+
/// Which of the process's compute runtimes this state serves.
236+
pub(crate) fn role(&self) -> ComputeRuntimeRole {
237+
self.role
238+
}
239+
219240
/// Return a mutable reference to the identified collection.
220241
///
221242
/// Panics if the collection doesn't exist.
@@ -866,6 +887,8 @@ impl<'a> ActiveComputeState<'a> {
866887
Rc::clone(&self.compute_state.worker_config),
867888
self.compute_state.workers_per_process,
868889
storage_log_reader,
890+
self.compute_state.role(),
891+
self.compute_state.sharing_registry.clone(),
869892
);
870893

871894
let dataflow_index = Rc::new(dataflow_index);

src/compute/src/logging/initialize.rs

Lines changed: 164 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use differential_dataflow::logging::{DifferentialEvent, DifferentialEventBuilder
1616
use mz_compute_client::logging::{LogVariant, LoggingConfig};
1717
use mz_dyncfg::ConfigSet;
1818
use mz_ore::metrics::MetricsRegistry;
19-
use mz_repr::{Diff, Timestamp};
19+
use mz_repr::{Diff, GlobalId, Timestamp};
2020
use mz_storage_operators::persist_source::Subtime;
2121
use mz_timely_util::columnar::Column;
2222
use mz_timely_util::columnar::builder::ColumnBuilder;
@@ -35,7 +35,10 @@ use crate::extensions::arrange::{KeyCollection, MzArrange};
3535
use crate::logging::compute::{ComputeEvent, ComputeEventBuilder};
3636
use crate::logging::{BatchLogger, EventQueue, SharedLoggingState};
3737
use crate::render::errors::DataflowErrorSer;
38-
use crate::typedefs::{ErrBatcher, ErrBuilder};
38+
use crate::server::ComputeRuntimeRole;
39+
use crate::shared_trace::PublishArrangement;
40+
use crate::sharing::ArrangementSharingRegistry;
41+
use crate::typedefs::{ErrAgent, ErrBatcher, ErrBuilder, RowRowAgent};
3942

4043
/// Initialize logging dataflows.
4144
///
@@ -48,6 +51,8 @@ pub fn initialize(
4851
worker_config: Rc<ConfigSet>,
4952
workers_per_process: usize,
5053
storage_log_reader: Option<crate::server::StorageTimelyLogReader>,
54+
role: ComputeRuntimeRole,
55+
sharing_registry: ArrangementSharingRegistry,
5156
) -> LoggingTraces {
5257
let interval_ms = std::cmp::max(1, config.interval.as_millis());
5358

@@ -74,6 +79,8 @@ pub fn initialize(
7479
worker_config,
7580
workers_per_process,
7681
storage_log_reader,
82+
role,
83+
sharing_registry,
7784
};
7885

7986
// Depending on whether we should log the creation of the logging dataflows, we register the
@@ -114,6 +121,11 @@ struct LoggingContext<'a> {
114121
workers_per_process: usize,
115122
/// Optional reader for storage timely logging events.
116123
storage_log_reader: Option<crate::server::StorageTimelyLogReader>,
124+
/// This runtime's role. Only `Maintenance` publishes its logging indexes into the sharing
125+
/// registry.
126+
role: ComputeRuntimeRole,
127+
/// The per-process registry maintenance publishes its logging indexes into.
128+
sharing_registry: ArrangementSharingRegistry,
117129
}
118130

119131
pub(crate) struct LoggingTraces {
@@ -195,6 +207,20 @@ impl LoggingContext<'_> {
195207
let traces = collections
196208
.into_iter()
197209
.map(|(log, collection)| {
210+
// Publish maintenance's logging index into the sharing registry so the
211+
// interactive runtime serves introspection peeks from it. Gated on the
212+
// Maintenance role inside the helper, so this is a no-op (adds no operators) on
213+
// Interactive and Solo.
214+
if let Some(&id) = self.config.index_logs.get(&log) {
215+
publish_logging_index(
216+
self.role,
217+
&self.sharing_registry,
218+
&scope,
219+
id,
220+
&collection.trace,
221+
&errs,
222+
);
223+
}
198224
let bundle = TraceBundle::new(collection.trace, errs.clone())
199225
.with_drop(collection.token);
200226
(log, bundle)
@@ -342,3 +368,139 @@ impl ExtractTimestamp for (Timestamp, Subtime) {
342368
self.0
343369
}
344370
}
371+
372+
/// Publishes a maintenance logging index's `oks`/`errs` arrangements into the sharing registry so
373+
/// the interactive runtime serves introspection peeks from them.
374+
///
375+
/// Gated strictly on the `Maintenance` role. Interactive must not publish: it reads maintenance's
376+
/// slot, and its own (empty) copy would clobber it. Solo has no registry peer. The gate is
377+
/// deliberately stricter than `ComputeRuntimeRole::publishes`, which also admits Interactive.
378+
///
379+
/// The arrangements are re-imported from their trace handles into `scope`. The original arrange
380+
/// streams are consumed inside the per-log construction regions, so only the trace handles survive
381+
/// here, and `Arranged::publish` needs a live arrangement stream on this scope to attach its
382+
/// publisher operator.
383+
fn publish_logging_index(
384+
role: ComputeRuntimeRole,
385+
registry: &ArrangementSharingRegistry,
386+
scope: &timely::dataflow::Scope<'_, Timestamp>,
387+
id: GlobalId,
388+
oks_trace: &RowRowAgent<Timestamp, Diff>,
389+
errs_trace: &ErrAgent<Timestamp, Diff>,
390+
) {
391+
if role != ComputeRuntimeRole::Maintenance {
392+
return;
393+
}
394+
395+
// Re-import the trace handles to obtain live arrangement streams `publish` can attach a
396+
// publisher operator to. The publisher refreshes its published chain from the trace, the
397+
// authoritative source, so the re-import replay only drives the publisher's wakeups.
398+
let oks = oks_trace
399+
.clone()
400+
.import_named(scope.clone(), &format!("PublishLog({id})"));
401+
let errs = errs_trace
402+
.clone()
403+
.import_named(scope.clone(), &format!("PublishLogErr({id})"));
404+
405+
// Adopt the registry's placeholder slot for `id` rather than publishing fresh and inserting:
406+
// whichever side (this maintenance publish, or an interactive import ahead of it) touches `id`
407+
// first creates the slot, so this fills it in place instead of overwriting a placeholder a reader
408+
// has already imported. `get_or_create_placeholder` does not notify on create, so notify
409+
// explicitly once both halves are adopted, mirroring the notification `insert` used to fire.
410+
//
411+
// Each adopt's `on_seal` re-examines an interactive read parked on this introspection index's
412+
// seal, fired by the publisher when the published `upper` advances after its state lock is
413+
// released, so the read reads the advanced upper. It replaces an upstream stream tap, which fired
414+
// before the sink advanced `upper`. Both streams signal: an introspection read whose result is an
415+
// error (for example a division-by-zero surfacing in `mz_compute_error_counts_raw_unified`)
416+
// carries its data on the errs stream, so an oks-only signal would leave it stuck.
417+
let worker_index = scope.index();
418+
let slot = registry.get_or_create_placeholder(id, worker_index, scope.peers());
419+
let oks_registry = registry.clone();
420+
PublishArrangement::adopt(&oks, &slot.oks, move || {
421+
oks_registry.note_frontier(id, worker_index)
422+
});
423+
let errs_registry = registry.clone();
424+
PublishArrangement::adopt(&errs, &slot.errs, move || {
425+
errs_registry.note_frontier(id, worker_index)
426+
});
427+
registry.notify(id, worker_index);
428+
}
429+
430+
#[cfg(test)]
431+
mod tests {
432+
use differential_dataflow::input::Input;
433+
use mz_repr::{Diff, GlobalId, Row, Timestamp};
434+
use mz_row_spine::{RowRowBatcher, RowRowBuilder};
435+
use mz_timely_util::columnation::ColumnationChunker;
436+
437+
use crate::extensions::arrange::{KeyCollection, MzArrange};
438+
use crate::render::errors::DataflowErrorSer;
439+
use crate::server::ComputeRuntimeRole;
440+
use crate::sharing::ArrangementSharingRegistry;
441+
use crate::typedefs::{ErrBatcher, ErrBuilder, ErrSpine, RowRowSpine};
442+
443+
use super::publish_logging_index;
444+
445+
/// A logging/introspection index is a `RowRow` `oks` arrangement plus an (empty) `errs`
446+
/// arrangement, published into the sharing registry only by the maintenance runtime. Interactive
447+
/// and Solo must not publish: interactive reads maintenance's slot rather than clobbering it with
448+
/// its own empty copy, and Solo has no registry peer.
449+
///
450+
/// Builds real `RowRow`/`Err` arrangements (the exact types the logging path produces) and drives
451+
/// [`publish_logging_index`] for each role, asserting only maintenance ends up published.
452+
#[mz_ore::test]
453+
fn maintenance_publishes_logging_index_others_do_not() {
454+
for (role, expect_published) in [
455+
(ComputeRuntimeRole::Maintenance, true),
456+
(ComputeRuntimeRole::Interactive, false),
457+
(ComputeRuntimeRole::Solo, false),
458+
] {
459+
let id = GlobalId::System(1);
460+
let registry = ArrangementSharingRegistry::new();
461+
let registry_in = registry.clone();
462+
463+
timely::execute_directly(move |worker| {
464+
worker.dataflow::<Timestamp, _, _>(|scope| {
465+
let (mut oks_input, oks_collection) =
466+
scope.new_collection::<(Row, Row), Diff>();
467+
let oks = oks_collection.mz_arrange::<
468+
ColumnationChunker<_>,
469+
RowRowBatcher<_, _>,
470+
RowRowBuilder<_, _>,
471+
RowRowSpine<_, _>,
472+
>("test log oks");
473+
474+
let (mut errs_input, errs_collection) =
475+
scope.new_collection::<DataflowErrorSer, Diff>();
476+
let errs = KeyCollection::from(errs_collection).mz_arrange::<
477+
ColumnationChunker<_>,
478+
ErrBatcher<_, _>,
479+
ErrBuilder<_, _>,
480+
ErrSpine<_, _>,
481+
>("test log errs");
482+
483+
publish_logging_index(
484+
role,
485+
&registry_in,
486+
&scope.clone(),
487+
id,
488+
&oks.trace,
489+
&errs.trace,
490+
);
491+
492+
oks_input.advance_to(Timestamp::from(1_u64));
493+
oks_input.flush();
494+
errs_input.advance_to(Timestamp::from(1_u64));
495+
errs_input.flush();
496+
});
497+
});
498+
499+
assert_eq!(
500+
registry.handles(&id, 0).is_some(),
501+
expect_published,
502+
"role {role:?} publication mismatch"
503+
);
504+
}
505+
}
506+
}

0 commit comments

Comments
 (0)