@@ -16,7 +16,7 @@ use differential_dataflow::logging::{DifferentialEvent, DifferentialEventBuilder
1616use mz_compute_client:: logging:: { LogVariant , LoggingConfig } ;
1717use mz_dyncfg:: ConfigSet ;
1818use mz_ore:: metrics:: MetricsRegistry ;
19- use mz_repr:: { Diff , Timestamp } ;
19+ use mz_repr:: { Diff , GlobalId , Timestamp } ;
2020use mz_storage_operators:: persist_source:: Subtime ;
2121use mz_timely_util:: columnar:: Column ;
2222use mz_timely_util:: columnar:: builder:: ColumnBuilder ;
@@ -35,7 +35,10 @@ use crate::extensions::arrange::{KeyCollection, MzArrange};
3535use crate :: logging:: compute:: { ComputeEvent , ComputeEventBuilder } ;
3636use crate :: logging:: { BatchLogger , EventQueue , SharedLoggingState } ;
3737use 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
119131pub ( 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