1use crate::CollectionMetadata;
13use std::collections::{BTreeMap, BTreeSet};
14use std::sync::atomic::AtomicBool;
15use std::sync::{Arc, atomic};
16use std::time::{Duration, Instant};
17
18use anyhow::bail;
19use itertools::Itertools;
20use mz_build_info::BuildInfo;
21use mz_cluster_client::ReplicaId;
22use mz_cluster_client::client::ClusterReplicaLocation;
23use mz_ore::cast::CastFrom;
24use mz_ore::now::NowFn;
25use mz_ore::retry::{Retry, RetryState};
26use mz_ore::task::AbortOnDropHandle;
27use mz_repr::{GlobalId, Timestamp};
28use mz_service::client::{GenericClient, Partitioned};
29use mz_service::params::GrpcClientParameters;
30use mz_service::transport;
31use mz_storage_client::client::{
32 RunIngestionCommand, RunSinkCommand, Status, StatusUpdate, StorageCommand, StorageResponse,
33};
34use mz_storage_client::metrics::{InstanceMetrics, ReplicaMetrics};
35use mz_storage_types::sinks::StorageSinkDesc;
36use mz_storage_types::sources::{IngestionDescription, SourceConnection};
37use timely::progress::Antichain;
38use tokio::select;
39use tokio::sync::mpsc;
40use tracing::{debug, info, warn};
41use uuid::Uuid;
42
43use crate::history::CommandHistory;
44
45#[derive(Debug)]
55pub(crate) struct Instance {
56 pub workload_class: Option<String>,
60 replicas: BTreeMap<ReplicaId, Replica>,
62 active_ingestions: BTreeMap<GlobalId, ActiveIngestion>,
68 ingestion_exports: BTreeMap<GlobalId, GlobalId>,
70 active_exports: BTreeMap<GlobalId, ActiveExport>,
76 history: CommandHistory,
79 metrics: InstanceMetrics,
81 now: NowFn,
83 response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
89}
90
91#[derive(Debug)]
92struct ActiveIngestion {
93 active_replicas: BTreeSet<ReplicaId>,
95}
96
97#[derive(Debug)]
98struct ActiveExport {
99 active_replicas: BTreeSet<ReplicaId>,
101}
102
103enum ActiveReplicas<'a> {
106 Scheduled(&'a BTreeSet<ReplicaId>),
110 All,
112}
113
114impl Instance {
115 pub fn new(
117 workload_class: Option<String>,
118 metrics: InstanceMetrics,
119 now: NowFn,
120 instance_response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
121 ) -> Self {
122 let history = CommandHistory::new(metrics.for_history());
123
124 let mut instance = Self {
125 workload_class,
126 replicas: Default::default(),
127 active_ingestions: Default::default(),
128 ingestion_exports: Default::default(),
129 active_exports: BTreeMap::new(),
130 history,
131 metrics,
132 now,
133 response_tx: instance_response_tx,
134 };
135
136 instance.send(StorageCommand::Hello {
137 nonce: Default::default(),
140 });
141
142 instance
143 }
144
145 pub fn replica_ids(&self) -> impl Iterator<Item = ReplicaId> + '_ {
147 self.replicas.keys().copied()
148 }
149
150 pub fn add_replica(&mut self, id: ReplicaId, config: ReplicaConfig) {
152 self.history.reduce();
155
156 let metrics = self.metrics.for_replica(id);
157 let replica = Replica::new(id, config, metrics, self.response_tx.clone());
158
159 self.replicas.insert(id, replica);
160
161 self.update_scheduling(false);
162
163 self.replay_commands(id);
164 }
165
166 pub fn replay_commands(&mut self, replica_id: ReplicaId) {
168 let commands = self.history.iter().cloned();
169
170 let filtered_commands = commands
171 .filter_map(|command| match command {
172 StorageCommand::RunIngestion(ingestion) => {
173 if self.is_active_replica(&ingestion.id, &replica_id) {
174 Some(StorageCommand::RunIngestion(ingestion))
175 } else {
176 None
177 }
178 }
179 StorageCommand::RunSink(sink) => {
180 if self.is_active_replica(&sink.id, &replica_id) {
181 Some(StorageCommand::RunSink(sink))
182 } else {
183 None
184 }
185 }
186 StorageCommand::AllowCompaction(id, upper) => {
187 if self.is_active_replica(&id, &replica_id) {
188 Some(StorageCommand::AllowCompaction(id, upper))
189 } else {
190 None
191 }
192 }
193 command => Some(command),
194 })
195 .collect::<Vec<_>>();
196
197 let replica = self
198 .replicas
199 .get_mut(&replica_id)
200 .expect("replica must exist");
201
202 for command in filtered_commands {
204 replica.send(command);
205 }
206 }
207
208 pub fn drop_replica(&mut self, id: ReplicaId) {
210 let replica = self.replicas.remove(&id);
211
212 let mut needs_rescheduling = false;
213 for (ingestion_id, ingestion) in self.active_ingestions.iter_mut() {
214 let was_running = ingestion.active_replicas.remove(&id);
215 if was_running {
216 tracing::debug!(
217 %ingestion_id,
218 replica_id = %id,
219 "ingestion was running on dropped replica, updating scheduling decisions"
220 );
221 needs_rescheduling = true;
222 }
223 }
224 for (export_id, export) in self.active_exports.iter_mut() {
225 let was_running = export.active_replicas.remove(&id);
226 if was_running {
227 tracing::debug!(
228 %export_id,
229 replica_id = %id,
230 "export was running on dropped replica, updating scheduling decisions"
231 );
232 needs_rescheduling = true;
233 }
234 }
235
236 tracing::info!(%id, %needs_rescheduling, "dropped replica");
237
238 if needs_rescheduling {
239 self.update_scheduling(true);
240 }
241
242 if replica.is_some() && self.replicas.is_empty() {
243 self.update_paused_statuses();
244 }
245 }
246
247 pub fn rehydrate_failed_replicas(&mut self) {
249 let replicas = self.replicas.iter();
250 let failed_replicas: Vec<_> = replicas
251 .filter_map(|(id, replica)| replica.failed().then_some(*id))
252 .collect();
253
254 for id in failed_replicas {
255 let replica = self.replicas.remove(&id).expect("must exist");
256 self.add_replica(id, replica.config);
257 }
258 }
259
260 pub fn active_ingestions(&self) -> impl Iterator<Item = &GlobalId> {
263 self.active_ingestions.keys()
264 }
265
266 pub fn active_ingestion_exports(&self) -> impl Iterator<Item = &GlobalId> {
274 let ingestion_exports = self.ingestion_exports.keys();
275 self.active_ingestions.keys().chain(ingestion_exports)
276 }
277
278 pub fn active_exports(&self) -> impl Iterator<Item = &GlobalId> {
280 self.active_exports.keys()
281 }
282
283 fn update_paused_statuses(&mut self) {
285 let now = mz_ore::now::to_datetime((self.now)());
286 let make_update = |id, object_type| StatusUpdate {
287 id,
288 status: Status::Paused,
289 timestamp: now,
290 error: None,
291 hints: BTreeSet::from([format!(
292 "There is currently no replica running this {object_type}"
293 )]),
294 namespaced_errors: Default::default(),
295 replica_id: None,
296 };
297
298 self.history.reduce();
299
300 let mut status_updates = Vec::new();
301 for command in self.history.iter() {
302 match command {
303 StorageCommand::RunIngestion(ingestion) => {
304 let old_style_ingestion =
305 ingestion.id != ingestion.description.remap_collection_id;
306 let subsource_ids = ingestion.description.collection_ids().filter(|id| {
307 let should_discard =
312 old_style_ingestion && id == &ingestion.description.remap_collection_id;
313 !should_discard
314 });
315 for id in subsource_ids {
316 status_updates.push(make_update(id, "source"));
317 }
318 }
319 StorageCommand::RunSink(sink) => {
320 status_updates.push(make_update(sink.id, "sink"));
321 }
322 _ => (),
323 }
324 }
325
326 for update in status_updates {
327 let _ = self
331 .response_tx
332 .send((None, StorageResponse::StatusUpdate(update)));
333 }
334 }
335
336 pub fn send(&mut self, command: StorageCommand) {
338 self.history.push(command.clone());
340
341 match command.clone() {
342 StorageCommand::RunIngestion(ingestion) => {
343 self.absorb_ingestion(*ingestion.clone());
347
348 for replica in self.active_replicas(&ingestion.id) {
349 replica.send(StorageCommand::RunIngestion(ingestion.clone()));
350 }
351 }
352 StorageCommand::RunSink(sink) => {
353 self.absorb_export(*sink.clone());
357
358 for replica in self.active_replicas(&sink.id) {
359 replica.send(StorageCommand::RunSink(sink.clone()));
360 }
361 }
362 StorageCommand::AllowCompaction(id, frontier) => {
363 for replica in self.active_replicas(&id) {
366 replica.send(StorageCommand::AllowCompaction(
367 id.clone(),
368 frontier.clone(),
369 ));
370 }
371
372 self.absorb_compaction(id, frontier);
373 }
374 command => {
375 for replica in self.replicas.values_mut() {
376 replica.send(command.clone());
377 }
378 }
379 }
380
381 if command.installs_objects() && self.replicas.is_empty() {
382 self.update_paused_statuses();
383 }
384 }
385
386 fn absorb_ingestion(&mut self, ingestion: RunIngestionCommand) {
391 let existing_ingestion_state = self.active_ingestions.get_mut(&ingestion.id);
392
393 for id in ingestion.description.source_exports.keys() {
395 self.ingestion_exports.insert(id.clone(), ingestion.id);
396 }
397
398 if let Some(ingestion_state) = existing_ingestion_state {
399 tracing::debug!(
404 ingestion_id = %ingestion.id,
405 active_replicas = %ingestion_state.active_replicas.iter().map(|id| id.to_string()).join(", "),
406 "updating ingestion"
407 );
408 } else {
409 let ingestion_state = ActiveIngestion {
411 active_replicas: BTreeSet::new(),
412 };
413 self.active_ingestions.insert(ingestion.id, ingestion_state);
414
415 self.update_scheduling(false);
417 }
418 }
419
420 fn absorb_export(&mut self, export: RunSinkCommand) {
425 let existing_export_state = self.active_exports.get_mut(&export.id);
426
427 if let Some(export_state) = existing_export_state {
428 tracing::debug!(
433 export_id = %export.id,
434 active_replicas = %export_state.active_replicas.iter().map(|id| id.to_string()).join(", "),
435 "updating export"
436 );
437 } else {
438 let export_state = ActiveExport {
440 active_replicas: BTreeSet::new(),
441 };
442 self.active_exports.insert(export.id, export_state);
443
444 self.update_scheduling(false);
446 }
447 }
448
449 fn update_scheduling(&mut self, send_commands: bool) {
468 #[derive(Debug)]
469 enum ObjectId {
470 Ingestion(GlobalId),
471 Export(GlobalId),
472 }
473 let mut scheduling_preferences: Vec<(ObjectId, bool)> = Vec::new();
478
479 for ingestion_id in self.active_ingestions.keys() {
480 let ingestion_description = self
481 .get_ingestion_description(ingestion_id)
482 .expect("missing ingestion description");
483
484 let prefers_single_replica = ingestion_description
485 .desc
486 .connection
487 .prefers_single_replica();
488
489 scheduling_preferences
490 .push((ObjectId::Ingestion(*ingestion_id), prefers_single_replica));
491 }
492
493 for export_id in self.active_exports.keys() {
494 scheduling_preferences.push((ObjectId::Export(*export_id), true));
496 }
497
498 let mut commands_by_replica: BTreeMap<ReplicaId, Vec<ObjectId>> = BTreeMap::new();
500
501 for (object_id, prefers_single_replica) in scheduling_preferences {
502 let active_replicas = match object_id {
503 ObjectId::Ingestion(ingestion_id) => {
504 &mut self
505 .active_ingestions
506 .get_mut(&ingestion_id)
507 .expect("missing ingestion state")
508 .active_replicas
509 }
510 ObjectId::Export(export_id) => {
511 &mut self
512 .active_exports
513 .get_mut(&export_id)
514 .expect("missing ingestion state")
515 .active_replicas
516 }
517 };
518
519 if prefers_single_replica {
520 if active_replicas.is_empty() {
522 let target_replica = self.replicas.keys().min().copied();
523 if let Some(first_replica_id) = target_replica {
524 tracing::info!(
525 object_id = ?object_id,
526 replica_id = %first_replica_id,
527 "scheduling single-replica object");
528 active_replicas.insert(first_replica_id);
529
530 commands_by_replica
531 .entry(first_replica_id)
532 .or_default()
533 .push(object_id);
534 }
535 } else {
536 tracing::info!(
537 ?object_id,
538 active_replicas = %active_replicas.iter().map(|id| id.to_string()).join(", "),
539 "single-replica object already running, not scheduling again",
540 );
541 }
542 } else {
543 let current_replica_ids: BTreeSet<_> = self.replicas.keys().copied().collect();
544 let unscheduled_replicas: Vec<_> = current_replica_ids
545 .difference(active_replicas)
546 .copied()
547 .collect();
548 for replica_id in unscheduled_replicas {
549 tracing::info!(
550 ?object_id,
551 %replica_id,
552 "scheduling multi-replica object"
553 );
554 active_replicas.insert(replica_id);
555 }
556 }
557 }
558
559 if send_commands {
560 for (replica_id, object_ids) in commands_by_replica {
561 let mut ingestion_commands = vec![];
562 let mut export_commands = vec![];
563 for object_id in object_ids {
564 match object_id {
565 ObjectId::Ingestion(id) => {
566 ingestion_commands.push(RunIngestionCommand {
567 id,
568 description: self
569 .get_ingestion_description(&id)
570 .expect("missing ingestion description")
571 .clone(),
572 });
573 }
574 ObjectId::Export(id) => {
575 export_commands.push(RunSinkCommand {
576 id,
577 description: self
578 .get_export_description(&id)
579 .expect("missing export description")
580 .clone(),
581 });
582 }
583 }
584 }
585 for ingestion in ingestion_commands {
586 let replica = self.replicas.get_mut(&replica_id).expect("missing replica");
587 let ingestion = Box::new(ingestion);
588 replica.send(StorageCommand::RunIngestion(ingestion));
589 }
590 for export in export_commands {
591 let replica = self.replicas.get_mut(&replica_id).expect("missing replica");
592 let export = Box::new(export);
593 replica.send(StorageCommand::RunSink(export));
594 }
595 }
596 }
597 }
598
599 pub fn get_ingestion_description(
607 &self,
608 id: &GlobalId,
609 ) -> Option<IngestionDescription<CollectionMetadata>> {
610 if !self.active_ingestions.contains_key(id) {
611 return None;
612 }
613
614 self.history.iter().rev().find_map(|command| {
615 if let StorageCommand::RunIngestion(ingestion) = command {
616 if &ingestion.id == id {
617 Some(ingestion.description.clone())
618 } else {
619 None
620 }
621 } else {
622 None
623 }
624 })
625 }
626
627 pub fn get_export_description(
635 &self,
636 id: &GlobalId,
637 ) -> Option<StorageSinkDesc<CollectionMetadata>> {
638 if !self.active_exports.contains_key(id) {
639 return None;
640 }
641
642 self.history.iter().rev().find_map(|command| {
643 if let StorageCommand::RunSink(sink) = command {
644 if &sink.id == id {
645 Some(sink.description.clone())
646 } else {
647 None
648 }
649 } else {
650 None
651 }
652 })
653 }
654
655 fn absorb_compaction(&mut self, id: GlobalId, frontier: Antichain<Timestamp>) {
657 tracing::debug!(?self.active_ingestions, ?id, ?frontier, "allow_compaction");
658
659 if frontier.is_empty() {
660 self.active_ingestions.remove(&id);
661 self.ingestion_exports.remove(&id);
662 self.active_exports.remove(&id);
663 }
664 }
665
666 fn active_replica_ids(&self, id: &GlobalId) -> ActiveReplicas<'_> {
672 static EMPTY: BTreeSet<ReplicaId> = BTreeSet::new();
674
675 if let Some(ingestion_id) = self.ingestion_exports.get(id) {
676 match self.active_ingestions.get(ingestion_id) {
677 Some(ingestion) => ActiveReplicas::Scheduled(&ingestion.active_replicas),
678 None => ActiveReplicas::Scheduled(&EMPTY),
680 }
681 } else if let Some(ingestion) = self.active_ingestions.get(id) {
682 ActiveReplicas::Scheduled(&ingestion.active_replicas)
685 } else if let Some(export) = self.active_exports.get(id) {
686 ActiveReplicas::Scheduled(&export.active_replicas)
687 } else {
688 ActiveReplicas::All
691 }
692 }
693
694 fn active_replicas(&mut self, id: &GlobalId) -> Box<dyn Iterator<Item = &mut Replica> + '_> {
696 let scheduled = match self.active_replica_ids(id) {
700 ActiveReplicas::All => None,
701 ActiveReplicas::Scheduled(replicas) => Some(replicas.clone()),
702 };
703 match scheduled {
704 None => Box::new(self.replicas.values_mut()),
705 Some(scheduled) => Box::new(self.replicas.iter_mut().filter_map(
706 move |(replica_id, replica)| scheduled.contains(replica_id).then_some(replica),
707 )),
708 }
709 }
710
711 fn is_active_replica(&self, id: &GlobalId, replica_id: &ReplicaId) -> bool {
713 match self.active_replica_ids(id) {
714 ActiveReplicas::All => true,
715 ActiveReplicas::Scheduled(replicas) => replicas.contains(replica_id),
716 }
717 }
718
719 pub(super) fn refresh_state_metrics(&self) {
728 let connected_replica_count = self.replicas.values().filter(|r| r.is_connected()).count();
729
730 self.metrics
731 .connected_replica_count
732 .set(u64::cast_from(connected_replica_count));
733 }
734
735 pub fn get_active_replicas_for_object(&self, id: &GlobalId) -> BTreeSet<ReplicaId> {
738 match self.active_replica_ids(id) {
739 ActiveReplicas::All => self.replicas.keys().copied().collect(),
740 ActiveReplicas::Scheduled(replicas) => replicas.clone(),
741 }
742 }
743}
744
745#[derive(Clone, Debug)]
747pub(super) struct ReplicaConfig {
748 pub build_info: &'static BuildInfo,
749 pub location: ClusterReplicaLocation,
750 pub grpc_client: GrpcClientParameters,
751}
752
753#[derive(Debug)]
755pub struct Replica {
756 config: ReplicaConfig,
758 command_tx: mpsc::UnboundedSender<StorageCommand>,
763 task: AbortOnDropHandle<()>,
765 connected: Arc<AtomicBool>,
767}
768
769impl Replica {
770 fn new(
772 id: ReplicaId,
773 config: ReplicaConfig,
774 metrics: ReplicaMetrics,
775 response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
776 ) -> Self {
777 let (command_tx, command_rx) = mpsc::unbounded_channel();
778 let connected = Arc::new(AtomicBool::new(false));
779
780 let task = mz_ore::task::spawn(
781 || "storage-replica-{id}",
782 ReplicaTask {
783 replica_id: id,
784 config: config.clone(),
785 metrics: metrics.clone(),
786 connected: Arc::clone(&connected),
787 command_rx,
788 response_tx,
789 }
790 .run(),
791 );
792
793 Self {
794 config,
795 command_tx,
796 task: task.abort_on_drop(),
797 connected,
798 }
799 }
800
801 fn send(&self, command: StorageCommand) {
803 let _ = self.command_tx.send(command);
805 }
806
807 fn failed(&self) -> bool {
810 self.task.is_finished()
811 }
812
813 pub(super) fn is_connected(&self) -> bool {
815 self.connected.load(atomic::Ordering::Relaxed)
816 }
817}
818
819type StorageCtpClient = transport::Client<StorageCommand, StorageResponse>;
820type ReplicaClient = Partitioned<StorageCtpClient, StorageCommand, StorageResponse>;
821
822struct ReplicaTask {
824 replica_id: ReplicaId,
826 config: ReplicaConfig,
828 metrics: ReplicaMetrics,
830 connected: Arc<AtomicBool>,
832 command_rx: mpsc::UnboundedReceiver<StorageCommand>,
834 response_tx: mpsc::UnboundedSender<(Option<ReplicaId>, StorageResponse)>,
836}
837
838impl ReplicaTask {
839 async fn run(self) {
841 let replica_id = self.replica_id;
842 info!(%replica_id, "starting replica task");
843
844 let client = self.connect().await;
845 match self.run_message_loop(client).await {
846 Ok(()) => info!(%replica_id, "stopped replica task"),
847 Err(error) => warn!(%replica_id, %error, "replica task failed"),
848 }
849 }
850
851 async fn connect(&self) -> ReplicaClient {
856 let try_connect = async move |retry: RetryState| {
857 let version = self.config.build_info.semver_version();
858 let client_params = &self.config.grpc_client;
859
860 let connect_start = Instant::now();
861 let connect_timeout = client_params.connect_timeout.unwrap_or(Duration::MAX);
862 let keepalive_timeout = client_params
863 .http2_keep_alive_timeout
864 .unwrap_or(Duration::MAX);
865
866 let connect_result = StorageCtpClient::connect_partitioned(
867 self.config.location.ctl_addrs.clone(),
868 version,
869 connect_timeout,
870 keepalive_timeout,
871 self.metrics.clone(),
872 )
873 .await;
874
875 self.metrics.observe_connect_time(connect_start.elapsed());
876
877 connect_result.inspect_err(|error| {
878 let next_backoff = retry.next_backoff.unwrap();
879 if retry.i >= mz_service::retry::INFO_MIN_RETRIES {
880 info!(
881 replica_id = %self.replica_id, ?next_backoff,
882 "error connecting to replica: {error:#}",
883 );
884 } else {
885 debug!(
886 replica_id = %self.replica_id, ?next_backoff,
887 "error connecting to replica: {error:#}",
888 );
889 }
890 })
891 };
892
893 let client = Retry::default()
894 .clamp_backoff(Duration::from_secs(1))
895 .retry_async(try_connect)
896 .await
897 .expect("retries forever");
898
899 self.metrics.observe_connect();
900 self.connected.store(true, atomic::Ordering::Relaxed);
901
902 client
903 }
904
905 async fn run_message_loop(mut self, mut client: ReplicaClient) -> Result<(), anyhow::Error> {
911 loop {
912 select! {
913 command = self.command_rx.recv() => {
916 let Some(mut command) = command else {
917 tracing::debug!(%self.replica_id, "controller is no longer interested in this replica, shutting down message loop");
918 break;
919 };
920
921 self.specialize_command(&mut command);
922 client.send(command).await?;
923 },
924 response = client.recv() => {
927 let Some(response) = response? else {
928 bail!("replica unexpectedly gracefully terminated connection");
929 };
930
931 if self.response_tx.send((Some(self.replica_id), response)).is_err() {
932 tracing::debug!(%self.replica_id, "controller (receiver) is no longer interested in this replica, shutting down message loop");
933 break;
934 }
935 }
936 }
937 }
938
939 Ok(())
940 }
941
942 fn specialize_command(&self, command: &mut StorageCommand) {
947 if let StorageCommand::Hello { nonce } = command {
948 *nonce = Uuid::new_v4();
949 }
950 }
951}