@@ -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 {
@@ -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,135 @@ 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 for `id` rather than publishing fresh and inserting: whichever
406+ // side, this maintenance publish or an interactive import ahead of it, touches `id` first creates
407+ // the slot, so filling it in place cannot overwrite a placeholder a reader has already imported.
408+ //
409+ // Both halves signal on seal. An introspection read whose result is an error (a division-by-zero
410+ // surfacing in `mz_compute_error_counts_raw_unified`) carries its data on the errs stream, so an
411+ // oks-only signal would leave it stuck.
412+ let worker_index = scope. index ( ) ;
413+ let slot = registry. get_or_create_placeholder ( id, worker_index, scope. peers ( ) ) ;
414+ let oks_registry = registry. clone ( ) ;
415+ PublishArrangement :: adopt ( & oks, & slot. oks , move || {
416+ oks_registry. note_frontier ( id, worker_index)
417+ } ) ;
418+ let errs_registry = registry. clone ( ) ;
419+ PublishArrangement :: adopt ( & errs, & slot. errs , move || {
420+ errs_registry. note_frontier ( id, worker_index)
421+ } ) ;
422+ // `get_or_create_placeholder` does not notify on create.
423+ registry. notify ( id, worker_index) ;
424+ }
425+
426+ #[ cfg( test) ]
427+ mod tests {
428+ use differential_dataflow:: input:: Input ;
429+ use mz_repr:: { Diff , GlobalId , Row , Timestamp } ;
430+ use mz_row_spine:: { RowRowBatcher , RowRowBuilder } ;
431+ use mz_timely_util:: columnation:: ColumnationChunker ;
432+
433+ use crate :: extensions:: arrange:: { KeyCollection , MzArrange } ;
434+ use crate :: render:: errors:: DataflowErrorSer ;
435+ use crate :: server:: ComputeRuntimeRole ;
436+ use crate :: sharing:: ArrangementSharingRegistry ;
437+ use crate :: typedefs:: { ErrBatcher , ErrBuilder , ErrSpine , RowRowSpine } ;
438+
439+ use super :: publish_logging_index;
440+
441+ /// A logging/introspection index is a `RowRow` `oks` arrangement plus an (empty) `errs`
442+ /// arrangement, published into the sharing registry only by the maintenance runtime. Interactive
443+ /// and Solo must not publish: interactive reads maintenance's slot rather than clobbering it with
444+ /// its own empty copy, and Solo has no registry peer.
445+ ///
446+ /// Builds real `RowRow`/`Err` arrangements (the exact types the logging path produces) and drives
447+ /// [`publish_logging_index`] for each role, asserting only maintenance ends up published.
448+ #[ mz_ore:: test]
449+ fn maintenance_publishes_logging_index_others_do_not ( ) {
450+ for ( role, expect_published) in [
451+ ( ComputeRuntimeRole :: Maintenance , true ) ,
452+ ( ComputeRuntimeRole :: Interactive , false ) ,
453+ ( ComputeRuntimeRole :: Solo , false ) ,
454+ ] {
455+ let id = GlobalId :: System ( 1 ) ;
456+ let registry = ArrangementSharingRegistry :: new ( ) ;
457+ let registry_in = registry. clone ( ) ;
458+
459+ timely:: execute_directly ( move |worker| {
460+ worker. dataflow :: < Timestamp , _ , _ > ( |scope| {
461+ let ( mut oks_input, oks_collection) =
462+ scope. new_collection :: < ( Row , Row ) , Diff > ( ) ;
463+ let oks = oks_collection. mz_arrange :: <
464+ ColumnationChunker < _ > ,
465+ RowRowBatcher < _ , _ > ,
466+ RowRowBuilder < _ , _ > ,
467+ RowRowSpine < _ , _ > ,
468+ > ( "test log oks" ) ;
469+
470+ let ( mut errs_input, errs_collection) =
471+ scope. new_collection :: < DataflowErrorSer , Diff > ( ) ;
472+ let errs = KeyCollection :: from ( errs_collection) . mz_arrange :: <
473+ ColumnationChunker < _ > ,
474+ ErrBatcher < _ , _ > ,
475+ ErrBuilder < _ , _ > ,
476+ ErrSpine < _ , _ > ,
477+ > ( "test log errs" ) ;
478+
479+ publish_logging_index (
480+ role,
481+ & registry_in,
482+ & scope. clone ( ) ,
483+ id,
484+ & oks. trace ,
485+ & errs. trace ,
486+ ) ;
487+
488+ oks_input. advance_to ( Timestamp :: from ( 1_u64 ) ) ;
489+ oks_input. flush ( ) ;
490+ errs_input. advance_to ( Timestamp :: from ( 1_u64 ) ) ;
491+ errs_input. flush ( ) ;
492+ } ) ;
493+ } ) ;
494+
495+ assert_eq ! (
496+ registry. handles( & id, 0 ) . is_some( ) ,
497+ expect_published,
498+ "role {role:?} publication mismatch"
499+ ) ;
500+ }
501+ }
502+ }
0 commit comments