mz_compute_client/controller/
replica.rs1use std::sync::Arc;
13use std::sync::atomic::{self, AtomicBool};
14use std::time::{Duration, Instant};
15
16use anyhow::bail;
17use mz_build_info::BuildInfo;
18use mz_cluster_client::client::ClusterReplicaLocation;
19use mz_compute_types::dyncfgs::ENABLE_COMPUTE_REPLICA_EXPIRATION;
20use mz_dyncfg::{ConfigSet, ConfigUpdates};
21use mz_ore::channel::InstrumentedUnboundedSender;
22use mz_ore::retry::{Retry, RetryState};
23use mz_ore::task::AbortOnDropHandle;
24use mz_service::client::{GenericClient, Partitioned};
25use mz_service::params::GrpcClientParameters;
26use mz_service::transport;
27use tokio::select;
28use tokio::sync::mpsc::error::SendError;
29use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
30use tracing::{debug, info, trace, warn};
31use uuid::Uuid;
32
33use crate::controller::ReplicaId;
34use crate::controller::instance::ReplicaResponse;
35use crate::controller::sequential_hydration::SequentialHydration;
36use crate::logging::LoggingConfig;
37use crate::metrics::IntCounter;
38use crate::metrics::ReplicaMetrics;
39use crate::protocol::command::ComputeCommand;
40use crate::protocol::response::ComputeResponse;
41
42type Client = Partitioned<ComputeCtpClient, ComputeCommand, ComputeResponse>;
43
44#[derive(Clone, Debug)]
46pub(super) struct ReplicaConfig {
47 pub location: ClusterReplicaLocation,
48 pub logging: LoggingConfig,
49 pub grpc_client: GrpcClientParameters,
50 pub expiration_offset: Option<Duration>,
52 pub arrangement_dictionary_compression: bool,
54}
55
56#[derive(Debug)]
58pub(super) struct ReplicaClient {
59 command_tx: UnboundedSender<ComputeCommand>,
61 task: AbortOnDropHandle<()>,
65 metrics: ReplicaMetrics,
67 connected: Arc<AtomicBool>,
69}
70
71impl ReplicaClient {
72 pub(super) fn spawn(
73 id: ReplicaId,
74 build_info: &'static BuildInfo,
75 config: ReplicaConfig,
76 epoch: u64,
77 metrics: ReplicaMetrics,
78 dyncfg: Arc<ConfigSet>,
79 response_tx: InstrumentedUnboundedSender<ReplicaResponse, IntCounter>,
80 ) -> Self {
81 let (command_tx, command_rx) = unbounded_channel();
85 let connected = Arc::new(AtomicBool::new(false));
86
87 let task = mz_ore::task::spawn(
88 || format!("active-replication-replica-{id}"),
89 ReplicaTask {
90 replica_id: id,
91 build_info,
92 config: config.clone(),
93 command_rx,
94 response_tx,
95 epoch,
96 metrics: metrics.clone(),
97 connected: Arc::clone(&connected),
98 replica_dyncfg: seed_replica_dyncfg(&dyncfg),
99 dyncfg,
100 }
101 .run(),
102 );
103
104 Self {
105 command_tx,
106 task: task.abort_on_drop(),
107 metrics,
108 connected,
109 }
110 }
111}
112
113impl ReplicaClient {
114 pub(super) fn send(&self, command: ComputeCommand) -> Result<(), SendError<ComputeCommand>> {
116 self.command_tx.send(command).map(|r| {
117 self.metrics.inner.command_queue_size.inc();
118 r
119 })
120 }
121
122 pub(super) fn is_failed(&self) -> bool {
124 self.task.is_finished()
125 }
126
127 pub(super) fn is_connected(&self) -> bool {
129 self.connected.load(atomic::Ordering::Relaxed)
130 }
131}
132
133type ComputeCtpClient = transport::Client<ComputeCommand, ComputeResponse>;
134
135fn seed_replica_dyncfg(dyncfg: &ConfigSet) -> ConfigSet {
140 let replica_dyncfg = mz_dyncfgs::all_dyncfgs();
141 ConfigUpdates::from(dyncfg).apply(&replica_dyncfg);
142 replica_dyncfg
143}
144
145fn apply_config_command(command: &ComputeCommand, dyncfg: &ConfigSet) {
149 match command {
150 ComputeCommand::CreateInstance(config) => config.initial_config.apply(dyncfg),
151 ComputeCommand::UpdateConfiguration(params) => params.dyncfg_updates.apply(dyncfg),
152 _ => (),
153 }
154}
155
156struct ReplicaTask {
158 replica_id: ReplicaId,
160 config: ReplicaConfig,
162 build_info: &'static BuildInfo,
164 command_rx: UnboundedReceiver<ComputeCommand>,
166 response_tx: InstrumentedUnboundedSender<ReplicaResponse, IntCounter>,
168 epoch: u64,
171 metrics: ReplicaMetrics,
173 connected: Arc<AtomicBool>,
175 dyncfg: Arc<ConfigSet>,
177 replica_dyncfg: ConfigSet,
190}
191
192impl ReplicaTask {
193 async fn run(self) {
195 let replica_id = self.replica_id;
196 info!(replica = ?replica_id, "starting replica task");
197
198 let client = self.connect().await;
199 match self.run_message_loop(client).await {
200 Ok(()) => info!(replica = ?replica_id, "stopped replica task"),
201 Err(error) => warn!(replica = ?replica_id, "replica task failed: {error:#}"),
202 }
203 }
204
205 async fn connect(&self) -> Client {
210 let try_connect = async |retry: RetryState| {
211 let version = self.build_info.semver_version();
212 let client_params = &self.config.grpc_client;
213 let connect_timeout = client_params.connect_timeout.unwrap_or(Duration::MAX);
214 let keepalive_timeout = client_params
215 .http2_keep_alive_timeout
216 .unwrap_or(Duration::MAX);
217
218 let connect_start = Instant::now();
219 let connect_result = ComputeCtpClient::connect_partitioned(
220 self.config.location.ctl_addrs.clone(),
221 version,
222 connect_timeout,
223 keepalive_timeout,
224 self.metrics.clone(),
225 )
226 .await;
227
228 self.metrics.observe_connect_time(connect_start.elapsed());
229
230 connect_result.inspect_err(|error| {
231 let next_backoff = retry.next_backoff.unwrap();
232 if retry.i >= mz_service::retry::INFO_MIN_RETRIES {
233 info!(
234 replica_id = %self.replica_id, ?next_backoff,
235 "error connecting to replica: {error:#}",
236 );
237 } else {
238 debug!(
239 replica_id = %self.replica_id, ?next_backoff,
240 "error connecting to replica: {error:#}",
241 );
242 }
243 })
244 };
245
246 let client = Retry::default()
247 .clamp_backoff(Duration::from_secs(1))
248 .retry_async(try_connect)
249 .await
250 .expect("retry retries forever");
251
252 self.metrics.observe_connect();
253 self.connected.store(true, atomic::Ordering::Relaxed);
254
255 client
256 }
257
258 async fn run_message_loop(mut self, mut client: Client) -> Result<(), anyhow::Error> {
264 let mut hydration = SequentialHydration::new(self.metrics.clone());
268
269 loop {
270 select! {
271 command = self.command_rx.recv() => {
273 let Some(mut command) = command else {
274 break;
276 };
277
278 self.specialize_command(&mut command);
279 self.observe_command(&command);
280 apply_config_command(&command, &self.replica_dyncfg);
281 for command in hydration.absorb_command(command, &self.replica_dyncfg) {
282 client.send(command).await?;
283 }
284 },
285 response = client.recv() => {
287 let Some(response) = response? else {
288 bail!("replica unexpectedly gracefully terminated connection");
289 };
290
291 self.observe_response(&response);
292
293 for command in hydration.observe_response(&response, &self.replica_dyncfg) {
294 client.send(command).await?;
295 }
296
297 if self.response_tx.send((self.replica_id, self.epoch, response)).is_err() {
298 break;
300 }
301 }
302 }
303 }
304
305 Ok(())
306 }
307
308 fn specialize_command(&self, command: &mut ComputeCommand) {
313 match command {
314 ComputeCommand::Hello { nonce } => {
315 *nonce = Uuid::new_v4();
316 }
317 ComputeCommand::CreateInstance(config) => {
318 config.logging = self.config.logging.clone();
319 if ENABLE_COMPUTE_REPLICA_EXPIRATION.get(&self.dyncfg) {
320 config.expiration_offset = self.config.expiration_offset;
321 }
322 config.arrangement_dictionary_compression =
323 self.config.arrangement_dictionary_compression;
324 }
325 _ => {}
326 }
327 }
328
329 #[mz_ore::instrument(level = "debug")]
331 fn observe_command(&self, command: &ComputeCommand) {
332 if let ComputeCommand::Peek(peek) = command {
333 peek.otel_ctx.attach_as_parent();
334 }
335
336 trace!(
337 replica = ?self.replica_id,
338 command = ?command,
339 "sending command to replica",
340 );
341
342 self.metrics.inner.command_queue_size.dec();
343 }
344
345 #[mz_ore::instrument(level = "debug")]
347 fn observe_response(&self, response: &ComputeResponse) {
348 if let ComputeResponse::PeekResponse(_, _, otel_ctx) = response {
349 otel_ctx.attach_as_parent();
350 }
351
352 trace!(
353 replica = ?self.replica_id,
354 response = ?response,
355 "received response from replica",
356 );
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use mz_compute_types::dyncfgs::HYDRATION_CONCURRENCY;
363
364 use crate::protocol::command::{ComputeParameters, InstanceConfig};
365
366 use super::*;
367
368 #[mz_ore::test]
372 fn replica_dyncfg_tracks_config_commands() {
373 let env_wide = mz_dyncfgs::all_dyncfgs();
374 let mut updates = ConfigUpdates::default();
375 updates.add(&HYDRATION_CONCURRENCY, 1);
376 updates.apply(&env_wide);
377
378 let replica_dyncfg = seed_replica_dyncfg(&env_wide);
379 assert_eq!(HYDRATION_CONCURRENCY.get(&replica_dyncfg), 1);
380
381 let mut initial_config = ConfigUpdates::default();
383 initial_config.add(&HYDRATION_CONCURRENCY, 2);
384 let create = ComputeCommand::CreateInstance(Box::new(InstanceConfig {
385 logging: Default::default(),
386 expiration_offset: None,
387 peek_stash_persist_location: mz_persist_client::PersistLocation::new_in_mem(),
388 arrangement_dictionary_compression: false,
389 initial_config,
390 }));
391 apply_config_command(&create, &replica_dyncfg);
392 assert_eq!(HYDRATION_CONCURRENCY.get(&replica_dyncfg), 2);
393
394 let mut dyncfg_updates = ConfigUpdates::default();
396 dyncfg_updates.add(&HYDRATION_CONCURRENCY, 3);
397 let update = ComputeCommand::UpdateConfiguration(Box::new(ComputeParameters {
398 dyncfg_updates,
399 ..Default::default()
400 }));
401 apply_config_command(&update, &replica_dyncfg);
402 assert_eq!(HYDRATION_CONCURRENCY.get(&replica_dyncfg), 3);
403
404 assert_eq!(HYDRATION_CONCURRENCY.get(&env_wide), 1);
406 }
407}