Skip to content

Commit 3f24fa0

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 ea5a4a5 commit 3f24fa0

5 files changed

Lines changed: 311 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};
@@ -473,11 +474,16 @@ async fn run(args: Args) -> Result<(), anyhow::Error> {
473474
);
474475

475476
// Start compute server.
477+
//
478+
// The sharing registry is per process rather than per runtime: a reader on one runtime looks up
479+
// the slot a publisher on another runtime filled, so both must hold the same registry.
480+
let sharing_registry = ArrangementSharingRegistry::new();
476481
let compute_client_builder = mz_compute::server::serve(
477482
compute_timely_config,
478483
ComputeRuntimeRole::Solo,
479484
&metrics_registry,
480485
persist_clients,
486+
sharing_registry,
481487
txns_ctx,
482488
tracing_handle,
483489
ComputeInstanceContext {

src/compute/src/compute_state.rs

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

8081
mod peek_result_iterator;
8182
mod peek_stash;
@@ -189,6 +190,11 @@ pub struct ComputeState {
189190
/// A process-global cache of (blob_uri, consensus_uri) -> PersistClient.
190191
/// This is intentionally shared between workers.
191192
pub persist_clients: Arc<PersistClientCache>,
193+
/// A per-process registry of published index arrangements.
194+
///
195+
/// Intentionally shared between all workers of the process, each of which publishes into its own
196+
/// worker-ordinal slot. `Clone` shares the same underlying map.
197+
pub sharing_registry: ArrangementSharingRegistry,
192198
/// Context necessary for rendering txn-wal operators.
193199
pub txns_ctx: TxnsContext,
194200
/// History of commands received by this workers and all its peers.
@@ -247,12 +253,20 @@ pub struct ComputeState {
247253

248254
/// The storage worker forwards its introspection logs to the compute worker.
249255
pub storage_log_reader: Option<crate::server::StorageTimelyLogReader>,
256+
257+
/// Which of the process's compute runtimes this state belongs to.
258+
///
259+
/// Only the maintenance runtime runs the non-idempotent process-global initializers. The
260+
/// interactive runtime shares the same process and inherits those globals.
261+
role: ComputeRuntimeRole,
250262
}
251263

252264
impl ComputeState {
253265
/// Construct a new `ComputeState`.
254266
pub fn new(
267+
role: ComputeRuntimeRole,
255268
persist_clients: Arc<PersistClientCache>,
269+
sharing_registry: ArrangementSharingRegistry,
256270
txns_ctx: TxnsContext,
257271
metrics: WorkerMetrics,
258272
tracing_handle: Arc<TracingHandle>,
@@ -273,6 +287,7 @@ impl ComputeState {
273287
peek_stash_persist_location: None,
274288
compute_logger: None,
275289
persist_clients,
290+
sharing_registry,
276291
txns_ctx,
277292
command_history,
278293
max_result_size: u64::MAX,
@@ -288,9 +303,15 @@ impl ComputeState {
288303
init_system_time: mz_ore::now::SYSTEM_TIME(),
289304
replica_expiration: Antichain::default(),
290305
storage_log_reader,
306+
role,
291307
}
292308
}
293309

310+
/// Which of the process's compute runtimes this state serves.
311+
pub(crate) fn role(&self) -> ComputeRuntimeRole {
312+
self.role
313+
}
314+
294315
/// Return a mutable reference to the identified collection.
295316
///
296317
/// Panics if the collection doesn't exist.
@@ -955,6 +976,8 @@ impl<'a> ActiveComputeState<'a> {
955976
Rc::clone(&self.compute_state.worker_config),
956977
self.compute_state.workers_per_process,
957978
storage_log_reader,
979+
self.compute_state.role(),
980+
self.compute_state.sharing_registry.clone(),
958981
);
959982

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

src/compute/src/logging/initialize.rs

Lines changed: 160 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 {
@@ -206,6 +218,20 @@ impl LoggingContext<'_> {
206218
let traces = collections
207219
.into_iter()
208220
.map(|(log, collection)| {
221+
// Publish maintenance's logging index into the sharing registry so the
222+
// interactive runtime serves introspection peeks from it. Gated on the
223+
// Maintenance role inside the helper, so this is a no-op (adds no operators) on
224+
// Interactive and Solo.
225+
if let Some(&id) = self.config.index_logs.get(&log) {
226+
publish_logging_index(
227+
self.role,
228+
&self.sharing_registry,
229+
&scope,
230+
id,
231+
&collection.trace,
232+
&errs,
233+
);
234+
}
209235
let bundle = TraceBundle::new(collection.trace, errs.clone())
210236
.with_drop(collection.token);
211237
(log, bundle)
@@ -353,3 +379,135 @@ impl ExtractTimestamp for (Timestamp, Subtime) {
353379
self.0
354380
}
355381
}
382+
383+
/// Publishes a maintenance logging index's `oks`/`errs` arrangements into the sharing registry so
384+
/// the interactive runtime serves introspection peeks from them.
385+
///
386+
/// Gated strictly on the `Maintenance` role. Interactive must not publish: it reads maintenance's
387+
/// slot, and its own (empty) copy would clobber it. Solo has no registry peer. The gate is
388+
/// deliberately stricter than `ComputeRuntimeRole::publishes`, which also admits Interactive.
389+
///
390+
/// The arrangements are re-imported from their trace handles into `scope`. The original arrange
391+
/// streams are consumed inside the per-log construction regions, so only the trace handles survive
392+
/// here, and `Arranged::publish` needs a live arrangement stream on this scope to attach its
393+
/// publisher operator.
394+
fn publish_logging_index(
395+
role: ComputeRuntimeRole,
396+
registry: &ArrangementSharingRegistry,
397+
scope: &timely::dataflow::Scope<'_, Timestamp>,
398+
id: GlobalId,
399+
oks_trace: &RowRowAgent<Timestamp, Diff>,
400+
errs_trace: &ErrAgent<Timestamp, Diff>,
401+
) {
402+
if role != ComputeRuntimeRole::Maintenance {
403+
return;
404+
}
405+
406+
// Re-import the trace handles to obtain live arrangement streams `publish` can attach a
407+
// publisher operator to. The publisher refreshes its published chain from the trace, the
408+
// authoritative source, so the re-import replay only drives the publisher's wakeups.
409+
let oks = oks_trace
410+
.clone()
411+
.import_named(scope.clone(), &format!("PublishLog({id})"));
412+
let errs = errs_trace
413+
.clone()
414+
.import_named(scope.clone(), &format!("PublishLogErr({id})"));
415+
416+
// Adopt the registry's placeholder for `id` rather than publishing fresh and inserting: whichever
417+
// side, this maintenance publish or an interactive import ahead of it, touches `id` first creates
418+
// the slot, so backing it in place cannot overwrite a point a reader has already imported.
419+
//
420+
// Both halves signal on seal. An introspection read whose result is an error (a division-by-zero
421+
// surfacing in `mz_compute_error_counts_raw_unified`) carries its data on the errs stream, so an
422+
// oks-only signal would leave it stuck.
423+
let worker_index = scope.index();
424+
let slot = registry.get_or_create(id, worker_index, scope.peers());
425+
let oks_registry = registry.clone();
426+
PublishArrangement::adopt(&oks, &slot.oks, &format!("{id} oks"), move || {
427+
oks_registry.note_frontier(id, worker_index)
428+
});
429+
let errs_registry = registry.clone();
430+
PublishArrangement::adopt(&errs, &slot.errs, &format!("{id} errs"), move || {
431+
errs_registry.note_frontier(id, worker_index)
432+
});
433+
// `get_or_create` does not notify on create.
434+
registry.notify(id, worker_index);
435+
}
436+
437+
#[cfg(test)]
438+
mod tests {
439+
use differential_dataflow::input::Input;
440+
use mz_repr::{Diff, GlobalId, Row, Timestamp};
441+
use mz_row_spine::{RowRowBatcher, RowRowBuilder};
442+
use mz_timely_util::columnation::ColumnationChunker;
443+
444+
use crate::extensions::arrange::{KeyCollection, MzArrange};
445+
use crate::render::errors::DataflowErrorSer;
446+
use crate::server::ComputeRuntimeRole;
447+
use crate::sharing::ArrangementSharingRegistry;
448+
use crate::typedefs::{ErrBatcher, ErrBuilder, ErrSpine, RowRowSpine};
449+
450+
use super::publish_logging_index;
451+
452+
/// A logging/introspection index is a `RowRow` `oks` arrangement plus an (empty) `errs`
453+
/// arrangement, published into the sharing registry only by the maintenance runtime. Interactive
454+
/// and Solo must not publish: interactive reads maintenance's slot rather than clobbering it with
455+
/// its own empty copy, and Solo has no registry peer.
456+
///
457+
/// Builds real `RowRow`/`Err` arrangements (the exact types the logging path produces) and drives
458+
/// [`publish_logging_index`] for each role, asserting only maintenance ends up published.
459+
#[mz_ore::test]
460+
fn maintenance_publishes_logging_index_others_do_not() {
461+
for (role, expect_published) in [
462+
(ComputeRuntimeRole::Maintenance, true),
463+
(ComputeRuntimeRole::Interactive, false),
464+
(ComputeRuntimeRole::Solo, false),
465+
] {
466+
let id = GlobalId::System(1);
467+
let registry = ArrangementSharingRegistry::new();
468+
let registry_in = registry.clone();
469+
470+
timely::execute_directly(move |worker| {
471+
worker.dataflow::<Timestamp, _, _>(|scope| {
472+
let (mut oks_input, oks_collection) =
473+
scope.new_collection::<(Row, Row), Diff>();
474+
let oks = oks_collection.mz_arrange::<
475+
ColumnationChunker<_>,
476+
RowRowBatcher<_, _>,
477+
RowRowBuilder<_, _>,
478+
RowRowSpine<_, _>,
479+
>("test log oks");
480+
481+
let (mut errs_input, errs_collection) =
482+
scope.new_collection::<DataflowErrorSer, Diff>();
483+
let errs = KeyCollection::from(errs_collection).mz_arrange::<
484+
ColumnationChunker<_>,
485+
ErrBatcher<_, _>,
486+
ErrBuilder<_, _>,
487+
ErrSpine<_, _>,
488+
>("test log errs");
489+
490+
publish_logging_index(
491+
role,
492+
&registry_in,
493+
&scope.clone(),
494+
id,
495+
&oks.trace,
496+
&errs.trace,
497+
);
498+
499+
oks_input.advance_to(Timestamp::from(1_u64));
500+
oks_input.flush();
501+
errs_input.advance_to(Timestamp::from(1_u64));
502+
errs_input.flush();
503+
});
504+
});
505+
506+
assert_eq!(
507+
registry.handles(&id, 0).is_some(),
508+
expect_published,
509+
"role {role:?} publication mismatch"
510+
);
511+
}
512+
}
513+
}

0 commit comments

Comments
 (0)