@@ -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,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