mz_storage/source/mysql/
statistics.rs1use futures::StreamExt;
13use timely::container::CapacityContainerBuilder;
14use timely::dataflow::channels::pact::Pipeline;
15use timely::dataflow::operators::vec::Map;
16use timely::dataflow::{Scope, StreamVec};
17use timely::progress::Antichain;
18
19use mz_mysql_util::query_sys_var;
20use mz_ore::future::InTask;
21use mz_storage_types::sources::MySqlSourceConnection;
22use mz_storage_types::sources::mysql::{GtidPartition, GtidState, gtid_set_frontier};
23use mz_timely_util::builder_async::{
24 Event as AsyncEvent, OperatorBuilder as AsyncOperatorBuilder, PressOnDropButton,
25};
26
27use crate::source::types::{Probe, ResumeUppers};
28use crate::source::{RawSourceCreationConfig, probe};
29
30use super::{ReplicationError, TransientError};
31
32static STATISTICS: &str = "statistics";
33
34pub(crate) fn render<'scope>(
36 scope: Scope<'scope, GtidPartition>,
37 config: RawSourceCreationConfig,
38 connection: MySqlSourceConnection,
39 resume_uppers: impl futures::Stream<Item = ResumeUppers<GtidPartition>> + 'static,
40 replication_errors: StreamVec<'scope, GtidPartition, ReplicationError>,
41) -> (
42 StreamVec<'scope, GtidPartition, ReplicationError>,
43 StreamVec<'scope, GtidPartition, Probe<GtidPartition>>,
44 PressOnDropButton,
45) {
46 let op_name = format!("MySqlStatistics({})", config.id);
47 let mut builder = AsyncOperatorBuilder::new(op_name, scope);
48
49 let mut error_handle = builder.new_disconnected_input(replication_errors, Pipeline);
50
51 let (probe_output, probe_stream) = builder.new_output::<CapacityContainerBuilder<_>>();
52
53 let (button, transient_errors) = builder.build_fallible::<TransientError, _>(move |caps| {
55 Box::pin(async move {
56 let worker_id = config.worker_id;
57 let [probe_cap]: &mut [_; 1] = caps.try_into().unwrap();
58
59 if !config.responsible_for(STATISTICS) {
61 for stat in config.statistics.values() {
63 stat.set_offset_known(0);
64 stat.set_offset_committed(0);
65 }
66 return Ok(());
67 }
68
69 let connection_config = connection
70 .connection
71 .config(
72 &config.config.connection_context.secrets_reader,
73 &config.config,
74 InTask::Yes,
75 )
76 .await?;
77
78 let mut stats_conn = connection_config
79 .connect(
80 &format!("timely-{worker_id} MySQL replication statistics"),
81 &config.config.connection_context.ssh_tunnel_manager,
82 )
83 .await?;
84
85 tokio::pin!(resume_uppers);
86 let timestamp_interval = config.timestamp_interval;
87 let mut probe_ticker = probe::Ticker::new(move || timestamp_interval, config.now_fn);
88
89 let probe_loop = async {
90 loop {
91 let probe_ts = probe_ticker.tick().await;
92
93 let gtid_executed =
94 query_sys_var(&mut stats_conn, "global.gtid_executed").await?;
95 let upstream_frontier =
98 gtid_set_frontier(>id_executed).map_err(TransientError::from)?;
99
100 let offset_known = aggregate_mysql_frontier(&upstream_frontier);
101 for stat in config.statistics.values() {
102 stat.set_offset_known(offset_known);
103 }
104 probe_output.give(
105 &probe_cap[0],
106 Probe {
107 probe_ts,
108 upstream_frontier,
109 },
110 );
111 }
112 };
113 let commit_loop = async {
114 while let Some(uppers) = resume_uppers.next().await {
115 for (id, frontier) in &uppers.exports {
116 if let Some(stat) = config.statistics.get(id) {
117 stat.set_offset_committed(aggregate_mysql_frontier(frontier));
118 }
119 }
120 }
121 };
122 let error_loop = async {
123 while let Some(event) = error_handle.next().await {
124 if let AsyncEvent::Data(ts, err_data) = event {
125 for err in err_data {
126 if let ReplicationError::Definite(def_err) = err {
127 tracing::info!(
128 "ts: {:?} Definite replication error detected in statistics operator: {def_err}, exiting", ts
129 );
130 break;
131 }
132 }
133 }
134 }
135 tracing::info!("Replication error stream closed, exiting statistics loop");
136 Ok(())
137 };
138 let res = tokio::select! {
139 res = probe_loop => res,
140 res = commit_loop => Ok(res),
141 res = error_loop => res,
142 };
143 tracing::info!("Statistics loop exited, shutting down");
144 res
145 })
146 });
147
148 (
149 transient_errors.map(ReplicationError::from),
150 probe_stream,
151 button.press_on_drop(),
152 )
153}
154
155fn aggregate_mysql_frontier(frontier: &Antichain<GtidPartition>) -> u64 {
158 let mut progress_stat = 0;
159 for ts in frontier.iter() {
160 if let Some(_uuid) = ts.interval().singleton() {
161 let ts = match ts.timestamp() {
163 GtidState::Absent => 0,
164 GtidState::Active(id) => id.get().saturating_sub(1),
167 };
168 progress_stat += ts;
169 }
170 }
171 progress_stat
172}