Skip to main content

vector/topology/
running.rs

1use std::{
2    collections::{HashMap, HashSet},
3    sync::{Arc, Mutex},
4};
5
6use futures::{Future, FutureExt, future};
7use metrics::Gauge;
8use snafu::Snafu;
9use stream_cancel::Trigger;
10use tokio::{
11    sync::{mpsc, watch},
12    time::{Duration, Instant, interval, sleep_until},
13};
14use tracing::Instrument;
15use vector_lib::{
16    buffers::topology::channel::BufferSender,
17    gauge,
18    internal_event::GaugeName,
19    shutdown::ShutdownSignal,
20    tap::topology::{TapOutput, TapResource, WatchRx, WatchTx},
21    trigger::DisabledTrigger,
22};
23
24use super::{
25    BuiltBuffer, TaskHandle, TaskResult,
26    builder::{self, TopologyPieces, TopologyPiecesBuilder, reload_enrichment_tables},
27    fanout::{ControlChannel, ControlMessage},
28    handle_errors, retain, take_healthchecks,
29    task::{Task, TaskOutput},
30};
31use crate::{
32    config::{ComponentKey, Config, ConfigDiff, HealthcheckOptions, Inputs, OutputId, Resource},
33    event::EventArray,
34    extra_context::ExtraContext,
35    shutdown::SourceShutdownCoordinator,
36    signal::ShutdownError,
37    spawn_named,
38    utilization::UtilizationRegistry,
39};
40
41pub type ShutdownErrorReceiver = mpsc::UnboundedReceiver<ShutdownError>;
42
43#[derive(Debug, Snafu)]
44pub enum ReloadError {
45    #[snafu(display("global options changed: {}", changed_fields.join(", ")))]
46    GlobalOptionsChanged { changed_fields: Vec<String> },
47    #[snafu(display("failed to compute global diff: {}", source))]
48    GlobalDiffFailed { source: serde_json::Error },
49    #[snafu(display("topology build failed"))]
50    TopologyBuildFailed,
51    #[snafu(display("failed to restore previous config"))]
52    FailedToRestore,
53}
54
55#[allow(dead_code)]
56pub struct RunningTopology {
57    inputs: HashMap<ComponentKey, BufferSender<EventArray>>,
58    inputs_tap_metadata: HashMap<ComponentKey, Inputs<OutputId>>,
59    outputs: HashMap<OutputId, ControlChannel>,
60    outputs_tap_metadata: HashMap<ComponentKey, (&'static str, String)>,
61    component_type_names: HashMap<ComponentKey, String>,
62    source_tasks: HashMap<ComponentKey, TaskHandle>,
63    tasks: HashMap<ComponentKey, TaskHandle>,
64    shutdown_coordinator: SourceShutdownCoordinator,
65    detach_triggers: HashMap<ComponentKey, DisabledTrigger>,
66    pub(crate) config: Config,
67    pub(crate) abort_tx: mpsc::UnboundedSender<ShutdownError>,
68    watch: (WatchTx, WatchRx),
69    graceful_shutdown_duration: Option<Duration>,
70    utilization_registry: Option<UtilizationRegistry>,
71    utilization_task: Option<TaskHandle>,
72    utilization_task_shutdown_trigger: Option<Trigger>,
73    metrics_task: Option<TaskHandle>,
74    metrics_task_shutdown_trigger: Option<Trigger>,
75    pending_reload: Option<HashSet<ComponentKey>>,
76    sink_confinement_gauges: HashMap<ComponentKey, Gauge>,
77}
78
79impl RunningTopology {
80    pub fn new(config: Config, abort_tx: mpsc::UnboundedSender<ShutdownError>) -> Self {
81        Self {
82            inputs: HashMap::new(),
83            inputs_tap_metadata: HashMap::new(),
84            outputs: HashMap::new(),
85            outputs_tap_metadata: HashMap::new(),
86            component_type_names: HashMap::new(),
87            shutdown_coordinator: SourceShutdownCoordinator::default(),
88            detach_triggers: HashMap::new(),
89            source_tasks: HashMap::new(),
90            tasks: HashMap::new(),
91            abort_tx,
92            watch: watch::channel(TapResource::default()),
93            graceful_shutdown_duration: config.graceful_shutdown_duration,
94            config,
95            utilization_registry: None,
96            utilization_task: None,
97            utilization_task_shutdown_trigger: None,
98            metrics_task: None,
99            metrics_task_shutdown_trigger: None,
100            pending_reload: None,
101            sink_confinement_gauges: HashMap::new(),
102        }
103    }
104
105    /// Gets the configuration that represents this running topology.
106    pub const fn config(&self) -> &Config {
107        &self.config
108    }
109
110    /// Adds a set of component keys to the pending reload set if one exists. Otherwise, it
111    /// initializes the pending reload set.
112    pub fn extend_reload_set(&mut self, new_set: HashSet<ComponentKey>) {
113        match &mut self.pending_reload {
114            None => self.pending_reload = Some(new_set.clone()),
115            Some(existing) => existing.extend(new_set),
116        }
117    }
118
119    /// Creates a subscription to topology changes.
120    ///
121    /// This is used by the tap API to observe configuration changes, and re-wire tap sinks.
122    pub fn watch(&self) -> watch::Receiver<TapResource> {
123        self.watch.1.clone()
124    }
125
126    /// Signal that all sources in this topology are ended.
127    ///
128    /// The future returned by this function will finish once all the sources in
129    /// this topology have finished. This allows the caller to wait for or
130    /// detect that the sources in the topology are no longer
131    /// producing. [`Application`][crate::app::Application], as an example, uses this as a
132    /// shutdown signal.
133    pub fn sources_finished(&self) -> future::BoxFuture<'static, ()> {
134        self.shutdown_coordinator.shutdown_tripwire()
135    }
136
137    /// Shut down all topology components.
138    ///
139    /// This function sends the shutdown signal to all sources in this topology
140    /// and returns a future that resolves once all components (sources,
141    /// transforms, and sinks) have finished shutting down. Transforms and sinks
142    /// will shut down automatically once their input tasks finish.
143    ///
144    /// This function takes ownership of `self`, so once it returns everything
145    /// in the [`RunningTopology`] instance has been dropped except for the
146    /// `tasks` map. This map gets moved into the returned future and is used to
147    /// poll for when the tasks have completed. Once the returned future is
148    /// dropped then everything from this RunningTopology instance is fully
149    /// dropped.
150    ///
151    /// The returned future resolves to `true` if every component finished on its own before
152    /// the graceful shutdown deadline, or `false` if any component had to be forcefully killed.
153    pub fn stop(self) -> impl Future<Output = bool> {
154        // Create handy handles collections of all tasks for the subsequent
155        // operations.
156        let mut wait_handles = Vec::new();
157        // We need a Vec here since source components have two tasks. One for
158        // pump in self.tasks, and the other for source in self.source_tasks.
159        let mut check_handles = HashMap::<ComponentKey, Vec<_>>::new();
160
161        let map_closure =
162            |result: Result<TaskResult, tokio::task::JoinError>| matches!(result, Ok(Ok(_)));
163
164        // We need to give some time to the sources to gracefully shutdown, so
165        // we will merge them with other tasks.
166        for (key, task) in self.tasks.into_iter().chain(self.source_tasks) {
167            let task = task.map(map_closure).shared();
168
169            wait_handles.push(task.clone());
170            check_handles.entry(key).or_default().push(task);
171        }
172
173        if let Some(utilization_task) = self.utilization_task {
174            wait_handles.push(utilization_task.map(map_closure).shared());
175        }
176
177        if let Some(metrics_task) = self.metrics_task {
178            wait_handles.push(metrics_task.map(map_closure).shared());
179        }
180
181        // If we reach this, we will forcefully shutdown the sources. If None, we will never force shutdown.
182        let deadline = self
183            .graceful_shutdown_duration
184            .map(|grace_period| Instant::now() + grace_period);
185
186        let timeout = if let Some(deadline) = deadline {
187            // If we reach the deadline, this future will print out which components
188            // won't gracefully shutdown since we will start to forcefully shutdown
189            // the sources.
190            let mut check_handles2 = check_handles.clone();
191            Box::pin(async move {
192                sleep_until(deadline).await;
193                // Remove all tasks that have shutdown.
194                check_handles2.retain(|_key, handles| {
195                    retain(handles, |handle| handle.peek().is_none());
196                    !handles.is_empty()
197                });
198                let remaining_components = check_handles2
199                    .keys()
200                    .map(|item| item.to_string())
201                    .collect::<Vec<_>>()
202                    .join(", ");
203
204                error!(
205                    components = ?remaining_components,
206                    message = "Failed to gracefully shut down in time. Killing components.",
207                    internal_log_rate_limit = false
208                );
209                false
210            }) as future::BoxFuture<'static, bool>
211        } else {
212            Box::pin(future::pending()) as future::BoxFuture<'static, bool>
213        };
214
215        // Reports in intervals which components are still running.
216        let mut interval = interval(Duration::from_secs(5));
217        let reporter = async move {
218            loop {
219                interval.tick().await;
220
221                // Remove all tasks that have shutdown.
222                check_handles.retain(|_key, handles| {
223                    retain(handles, |handle| handle.peek().is_none());
224                    !handles.is_empty()
225                });
226                let remaining_components = check_handles
227                    .keys()
228                    .map(|item| item.to_string())
229                    .collect::<Vec<_>>()
230                    .join(", ");
231
232                let (deadline_passed, time_remaining) = match deadline {
233                    Some(d) => match d.checked_duration_since(Instant::now()) {
234                        Some(remaining) => (false, format!("{} seconds left", remaining.as_secs())),
235                        None => (true, "overdue".to_string()),
236                    },
237                    None => (false, "no time limit".to_string()),
238                };
239
240                info!(
241                    remaining_components = ?remaining_components,
242                    time_remaining = ?time_remaining,
243                    "Shutting down... Waiting on running components."
244                );
245
246                let all_done = check_handles.is_empty();
247
248                if all_done {
249                    info!("Shutdown reporter exiting: all components shut down.");
250                    break true;
251                } else if deadline_passed {
252                    error!(remaining_components = ?remaining_components, "Shutdown reporter: deadline exceeded.");
253                    break false;
254                }
255            }
256        };
257
258        // Finishes once all tasks have shutdown.
259        let success =
260            futures::future::join_all(wait_handles).map(|results| results.into_iter().all(|ok| ok));
261
262        // Aggregate future that ends once anything detects that all tasks have shutdown.
263        // Resolves to `true` only if the winning branch got there without forcing anything.
264        let shutdown_complete_future = future::select_all(vec![
265            Box::pin(timeout) as future::BoxFuture<'static, bool>,
266            Box::pin(reporter) as future::BoxFuture<'static, bool>,
267            Box::pin(success) as future::BoxFuture<'static, bool>,
268        ])
269        .map(|(graceful, _index, _remaining)| graceful);
270
271        // Now kick off the shutdown process by shutting down the sources.
272        let source_shutdown_complete = self.shutdown_coordinator.shutdown_all(deadline);
273        if let Some(trigger) = self.utilization_task_shutdown_trigger {
274            trigger.cancel();
275        }
276        if let Some(trigger) = self.metrics_task_shutdown_trigger {
277            trigger.cancel();
278        }
279
280        futures::future::join(source_shutdown_complete, shutdown_complete_future)
281            .map(|(_, graceful)| graceful)
282    }
283
284    /// Attempts to load a new configuration and update this running topology.
285    ///
286    /// If the new configuration was valid, and all changes were able to be made -- removing of
287    /// old components, changing of existing components, adding of new components -- then
288    /// `Ok(())` is returned.
289    ///
290    /// If the new configuration is not valid, or not all of the changes in the new configuration
291    /// were able to be made, then this method will attempt to undo the changes made and bring the
292    /// topology back to its previous state, returning the appropriate error.
293    ///
294    /// If the restore also fails, `ReloadError::FailedToRestore` is returned.
295    pub async fn reload_config_and_respawn(
296        &mut self,
297        new_config: Config,
298        extra_context: ExtraContext,
299    ) -> Result<(), ReloadError> {
300        info!("Reloading running topology with new configuration.");
301
302        if self.config.global != new_config.global {
303            return match self.config.global.diff(&new_config.global) {
304                Ok(changed_fields) => Err(ReloadError::GlobalOptionsChanged { changed_fields }),
305                Err(source) => Err(ReloadError::GlobalDiffFailed { source }),
306            };
307        }
308
309        // Calculate the change between the current configuration and the new configuration, and
310        // shutdown any components that are changing so that we can reclaim their buffers before
311        // spawning the new version of the component.
312        //
313        // We also shutdown any component that is simply being removed entirely.
314        let diff = if let Some(components) = &self.pending_reload {
315            ConfigDiff::new(&self.config, &new_config, components.clone())
316        } else {
317            ConfigDiff::new(&self.config, &new_config, HashSet::new())
318        };
319        let buffers = self.shutdown_diff(&diff, &new_config).await;
320
321        // Gives windows some time to make available any port
322        // released by shutdown components.
323        // Issue: https://github.com/vectordotdev/vector/issues/3035
324        if cfg!(windows) {
325            // This value is guess work.
326            tokio::time::sleep(Duration::from_millis(200)).await;
327        }
328
329        // Try to build all of the new components coming from the new configuration.  If we can
330        // successfully build them, we'll attempt to connect them up to the topology and spawn their
331        // respective component tasks.
332        if let Some(mut new_pieces) = TopologyPiecesBuilder::new(&new_config, &diff)
333            .with_buffers(buffers.clone())
334            .with_extra_context(extra_context.clone())
335            .with_utilization_registry(self.utilization_registry.clone())
336            .build_or_log_errors()
337            .await
338        {
339            // If healthchecks are configured for any of the changing/new components, try running
340            // them before moving forward with connecting and spawning.  In some cases, healthchecks
341            // failing may be configured as a non-blocking issue and so we'll still continue on.
342            if self
343                .run_healthchecks(&diff, &mut new_pieces, new_config.healthchecks)
344                .await
345            {
346                self.connect_diff(&diff, &mut new_pieces).await;
347                self.spawn_diff(&diff, new_pieces);
348                self.config = new_config;
349                self.refresh_confinement_gauges();
350
351                info!("New configuration loaded successfully.");
352
353                return Ok(());
354            }
355        }
356
357        // We failed to build, connect, and spawn all of the changed/new components, so we flip
358        // around the configuration differential to generate all the components that we need to
359        // bring back to restore the current configuration.
360        warn!("Failed to completely load new configuration. Restoring old configuration.");
361
362        let diff = diff.flip();
363        if let Some(mut new_pieces) = TopologyPiecesBuilder::new(&self.config, &diff)
364            .with_buffers(buffers)
365            .with_extra_context(extra_context.clone())
366            .with_utilization_registry(self.utilization_registry.clone())
367            .build_or_log_errors()
368            .await
369            && self
370                .run_healthchecks(&diff, &mut new_pieces, self.config.healthchecks)
371                .await
372        {
373            self.connect_diff(&diff, &mut new_pieces).await;
374            self.spawn_diff(&diff, new_pieces);
375            // `self.config` still holds the old config on the rollback path, so
376            // this restores the gauges for the re-spawned old sinks.
377            self.refresh_confinement_gauges();
378
379            info!("Old configuration restored successfully.");
380
381            return Err(ReloadError::TopologyBuildFailed);
382        }
383
384        error!(
385            message = "Failed to restore old configuration.",
386            internal_log_rate_limit = false
387        );
388
389        Err(ReloadError::FailedToRestore)
390    }
391
392    /// Attempts to reload enrichment tables.
393    pub(crate) async fn reload_enrichment_tables(&self) {
394        reload_enrichment_tables(&self.config).await;
395    }
396
397    pub(crate) async fn run_healthchecks(
398        &mut self,
399        diff: &ConfigDiff,
400        pieces: &mut TopologyPieces,
401        options: HealthcheckOptions,
402    ) -> bool {
403        if options.enabled {
404            let healthchecks = take_healthchecks(diff, pieces)
405                .into_iter()
406                .map(|(_, task)| task);
407            let healthchecks = future::try_join_all(healthchecks);
408
409            info!("Running healthchecks.");
410            if options.require_healthy {
411                let success = healthchecks.await;
412
413                if success.is_ok() {
414                    info!("All healthchecks passed.");
415                    true
416                } else {
417                    error!(
418                        message = "Sinks unhealthy.",
419                        internal_log_rate_limit = false
420                    );
421                    false
422                }
423            } else {
424                tokio::spawn(healthchecks);
425                true
426            }
427        } else {
428            true
429        }
430    }
431
432    /// Shuts down any changed/removed component in the given configuration diff.
433    ///
434    /// If buffers for any of the changed/removed components can be recovered, they'll be returned.
435    async fn shutdown_diff(
436        &mut self,
437        diff: &ConfigDiff,
438        new_config: &Config,
439    ) -> HashMap<ComponentKey, BuiltBuffer> {
440        // First, we shutdown any changed/removed sources. This ensures that we can allow downstream
441        // components to terminate naturally by virtue of the flow of events stopping.
442        if diff.sources.any_changed_or_removed()
443            || diff.enrichment_tables.sources.any_changed_or_removed()
444        {
445            let timeout = Duration::from_secs(30);
446            let mut source_shutdown_handles = Vec::new();
447
448            let deadline = Instant::now() + timeout;
449            for key in diff
450                .sources
451                .to_remove
452                .iter()
453                .chain(diff.enrichment_tables.sources.to_remove.iter())
454            {
455                debug!(component_id = %key, "Removing source.");
456
457                let previous = self.tasks.remove(key).unwrap();
458                drop(previous); // detach and forget
459
460                self.remove_outputs(key);
461                source_shutdown_handles
462                    .push(self.shutdown_coordinator.shutdown_source(key, deadline));
463            }
464
465            for key in diff
466                .sources
467                .to_change
468                .iter()
469                .chain(diff.enrichment_tables.sources.to_change.iter())
470            {
471                debug!(component_id = %key, "Changing source.");
472
473                self.remove_outputs(key);
474                source_shutdown_handles
475                    .push(self.shutdown_coordinator.shutdown_source(key, deadline));
476            }
477
478            debug!(
479                "Waiting for up to {} seconds for source(s) to finish shutting down.",
480                timeout.as_secs()
481            );
482            futures::future::join_all(source_shutdown_handles).await;
483
484            // Final cleanup pass now that all changed/removed sources have signalled as having shutdown.
485            for key in diff.sources.removed_and_changed() {
486                if let Some(task) = self.source_tasks.remove(key) {
487                    task.await.unwrap().unwrap();
488                }
489            }
490        }
491
492        // Next, we shutdown any changed/removed transforms.  Same as before: we want allow
493        // downstream components to terminate naturally by virtue of the flow of events stopping.
494        //
495        // Since transforms are entirely driven by the flow of events into them from upstream
496        // components, the shutdown of sources they depend on, or the shutdown of transforms they
497        // depend on, and thus the closing of their buffer, will naturally cause them to shutdown,
498        // which is why we don't do any manual triggering of shutdown here.
499        for key in &diff.transforms.to_remove {
500            debug!(component_id = %key, "Removing transform.");
501
502            let previous = self.tasks.remove(key).unwrap();
503            drop(previous); // detach and forget
504
505            self.remove_inputs(key, diff, new_config).await;
506            self.remove_outputs(key);
507
508            if let Some(registry) = self.utilization_registry.as_ref() {
509                registry.remove_component(key);
510            }
511        }
512
513        for key in &diff.transforms.to_change {
514            debug!(component_id = %key, "Changing transform.");
515
516            self.remove_inputs(key, diff, new_config).await;
517            self.remove_outputs(key);
518        }
519
520        // Now we'll process any changed/removed sinks.
521        //
522        // At this point both the old and the new config don't have conflicts in their resource
523        // usage. So if we combine their resources, all found conflicts are between to be removed
524        // and to be added components.
525        let removed_table_sinks = diff
526            .enrichment_tables
527            .sinks
528            .removed_and_changed()
529            .map(|key| {
530                (
531                    key.clone(),
532                    enrichment_table_sink_resources(&self.config, key),
533                )
534            })
535            .collect::<Vec<_>>();
536        let remove_sink = diff
537            .sinks
538            .removed_and_changed()
539            .map(|key| {
540                (
541                    key,
542                    self.config
543                        .sink(key)
544                        .map(|s| s.resources(key))
545                        .unwrap_or_default(),
546                )
547            })
548            .chain(removed_table_sinks.iter().map(|(k, s)| (k, s.clone())));
549        let add_source = diff
550            .sources
551            .changed_and_added()
552            .map(|key| (key, new_config.source(key).unwrap().inner.resources()));
553        let added_table_sinks = diff
554            .enrichment_tables
555            .sinks
556            .changed_and_added()
557            .map(|key| {
558                (
559                    key.clone(),
560                    enrichment_table_sink_resources(new_config, key),
561                )
562            })
563            .collect::<Vec<_>>();
564        let add_sink = diff
565            .sinks
566            .changed_and_added()
567            .map(|key| {
568                (
569                    key,
570                    new_config
571                        .sink(key)
572                        .map(|s| s.resources(key))
573                        .unwrap_or_default(),
574                )
575            })
576            .chain(added_table_sinks.iter().map(|(k, s)| (k, s.clone())));
577        let conflicts = Resource::conflicts(
578            remove_sink.map(|(key, value)| ((true, key), value)).chain(
579                add_sink
580                    .chain(add_source)
581                    .map(|(key, value)| ((false, key), value)),
582            ),
583        )
584        .into_values()
585        .flatten()
586        .collect::<HashSet<_>>();
587        // Existing conflicting sinks
588        let conflicting_sinks = conflicts
589            .into_iter()
590            .filter(|&(existing_sink, _)| existing_sink)
591            .map(|(_, key)| key.clone());
592
593        // For any sink whose buffer configuration didn't change, we can reuse their buffer.
594        let reuse_buffers = diff
595            .sinks
596            .to_change
597            .iter()
598            .chain(diff.enrichment_tables.sinks.to_change.iter())
599            .filter(|&key| {
600                if diff.components_to_reload.contains(key) {
601                    return false;
602                }
603                self.config
604                    .sink(key)
605                    .map(|s| s.buffer.clone())
606                    .or_else(|| enrichment_table_sink_buffer(&self.config, key))
607                    == new_config
608                        .sink(key)
609                        .map(|s| s.buffer.clone())
610                        .or_else(|| enrichment_table_sink_buffer(new_config, key))
611            })
612            .cloned()
613            .collect::<HashSet<_>>();
614
615        // For any existing sink that has a conflicting resource dependency with a changed/added
616        // sink, for any sink that we want to reuse their buffer, or for any changed sink with
617        // a disk buffer that is not being reused, we need to explicitly wait for them to finish
618        // processing so we can reclaim ownership of those resources/buffers.
619        let changed_disk_buffer_sinks = diff
620            .sinks
621            .to_change
622            .iter()
623            .filter(|key| {
624                !reuse_buffers.contains(*key)
625                    && (self
626                        .config
627                        .sink(key)
628                        .is_some_and(|s| s.buffer.has_disk_stage())
629                        || enrichment_table_sink_buffer(&self.config, key)
630                            .is_some_and(|buffer| buffer.has_disk_stage()))
631            })
632            .cloned()
633            .collect::<HashSet<_>>();
634
635        let wait_for_sinks = conflicting_sinks
636            .chain(reuse_buffers.iter().cloned())
637            .chain(changed_disk_buffer_sinks.iter().cloned())
638            .collect::<HashSet<_>>();
639
640        // First, we remove any inputs to removed sinks so they can naturally shut down.
641        let removed_sinks = diff
642            .sinks
643            .to_remove
644            .iter()
645            .chain(diff.enrichment_tables.sinks.to_remove.iter())
646            .collect::<Vec<_>>();
647        for key in &removed_sinks {
648            debug!(component_id = %key, "Removing sink.");
649            self.remove_inputs(key, diff, new_config).await;
650
651            if let Some(registry) = self.utilization_registry.as_ref() {
652                registry.remove_component(key);
653            }
654        }
655
656        // After that, for any changed sinks, we temporarily detach their inputs (not remove) so
657        // they can naturally shutdown and allow us to recover their buffers if possible.
658        let mut buffer_tx = HashMap::new();
659
660        let sinks_to_change = diff
661            .sinks
662            .to_change
663            .iter()
664            .chain(diff.enrichment_tables.sinks.to_change.iter())
665            .collect::<Vec<_>>();
666
667        for key in &sinks_to_change {
668            debug!(component_id = %key, "Changing sink.");
669            if reuse_buffers.contains(key) || changed_disk_buffer_sinks.contains(key) {
670                self.detach_triggers
671                    .remove(key)
672                    .unwrap()
673                    .into_inner()
674                    .cancel();
675
676                if reuse_buffers.contains(key) {
677                    // We explicitly clone the input side of the buffer and store it so we don't lose
678                    // it when we remove the inputs below.
679                    //
680                    // We clone instead of removing here because otherwise the input will be missing for
681                    // the rest of the reload process, which violates the assumption that all previous
682                    // inputs for components not being removed are still available. It's simpler to
683                    // allow the "old" input to stick around and be replaced (even though that's
684                    // basically a no-op since we're reusing the same buffer) than it is to pass around
685                    // info about which sinks are having their buffers reused and treat them differently
686                    // at other stages.
687                    buffer_tx.insert((*key).clone(), self.inputs.get(key).unwrap().clone());
688                }
689            }
690            self.remove_inputs(key, diff, new_config).await;
691        }
692
693        // Now that we've disconnected or temporarily detached the inputs to all changed/removed
694        // sinks, we can actually wait for them to shutdown before collecting any buffers that are
695        // marked for reuse.
696        //
697        // If a sink we're removing isn't tying up any resource that a changed/added sink depends
698        // on, we don't bother waiting for it to shutdown.
699        for key in &removed_sinks {
700            let previous = self.tasks.remove(key).unwrap();
701            if wait_for_sinks.contains(key) {
702                debug!(message = "Waiting for sink to shutdown.", component_id = %key);
703                previous.await.unwrap().unwrap();
704            } else {
705                drop(previous); // detach and forget
706            }
707        }
708
709        let mut buffers = HashMap::<ComponentKey, BuiltBuffer>::new();
710        for key in &sinks_to_change {
711            if wait_for_sinks.contains(key) {
712                let previous = self.tasks.remove(key).unwrap();
713                debug!(message = "Waiting for sink to shutdown.", component_id = %key);
714                let buffer = previous.await.unwrap().unwrap();
715
716                if reuse_buffers.contains(key) {
717                    // We clone instead of removing here because otherwise the input will be
718                    // missing for the rest of the reload process, which violates the assumption
719                    // that all previous inputs for components not being removed are still
720                    // available. It's simpler to allow the "old" input to stick around and be
721                    // replaced (even though that's basically a no-op since we're reusing the same
722                    // buffer) than it is to pass around info about which sinks are having their
723                    // buffers reused and treat them differently at other stages.
724                    let tx = buffer_tx.remove(key).unwrap();
725                    let rx = match buffer {
726                        TaskOutput::Sink(rx) => rx.into_inner(),
727                        _ => unreachable!(),
728                    };
729
730                    buffers.insert((*key).clone(), (tx, Arc::new(Mutex::new(Some(rx)))));
731                }
732            }
733        }
734
735        buffers
736    }
737
738    /// Connects all changed/added components in the given configuration diff.
739    pub(crate) async fn connect_diff(
740        &mut self,
741        diff: &ConfigDiff,
742        new_pieces: &mut TopologyPieces,
743    ) {
744        debug!("Connecting changed/added component(s).");
745
746        // Update tap metadata
747        if !self.watch.0.is_closed() {
748            for key in &diff.sources.to_remove {
749                // Sources only have outputs
750                self.outputs_tap_metadata.remove(key);
751                self.component_type_names.remove(key);
752            }
753
754            for key in &diff.transforms.to_remove {
755                // Transforms can have both inputs and outputs
756                self.outputs_tap_metadata.remove(key);
757                self.inputs_tap_metadata.remove(key);
758                self.component_type_names.remove(key);
759            }
760
761            for key in &diff.sinks.to_remove {
762                // Sinks only have inputs
763                self.inputs_tap_metadata.remove(key);
764                self.component_type_names.remove(key);
765            }
766
767            for key in &diff.enrichment_tables.sinks.to_remove {
768                // Sinks only have inputs
769                self.inputs_tap_metadata.remove(key);
770                self.component_type_names.remove(key);
771            }
772
773            for key in &diff.enrichment_tables.sources.to_remove {
774                // Sources only have outputs
775                self.outputs_tap_metadata.remove(key);
776                self.component_type_names.remove(key);
777            }
778
779            for key in diff.sources.changed_and_added() {
780                if let Some(task) = new_pieces.tasks.get(key) {
781                    let typetag = task.typetag().to_string();
782                    self.outputs_tap_metadata
783                        .insert(key.clone(), ("source", typetag.clone()));
784                    self.component_type_names.insert(key.clone(), typetag);
785                }
786            }
787
788            for key in diff.enrichment_tables.sources.changed_and_added() {
789                if let Some(task) = new_pieces.tasks.get(key) {
790                    self.outputs_tap_metadata
791                        .insert(key.clone(), ("source", task.typetag().to_string()));
792                    self.component_type_names
793                        .insert(key.clone(), task.typetag().to_string());
794                }
795            }
796
797            for key in diff.transforms.changed_and_added() {
798                if let Some(task) = new_pieces.tasks.get(key) {
799                    let typetag = task.typetag().to_string();
800                    self.outputs_tap_metadata
801                        .insert(key.clone(), ("transform", typetag.clone()));
802                    self.component_type_names.insert(key.clone(), typetag);
803                }
804            }
805
806            for key in diff.sinks.changed_and_added() {
807                if let Some(task) = new_pieces.tasks.get(key) {
808                    self.component_type_names
809                        .insert(key.clone(), task.typetag().to_string());
810                }
811            }
812
813            for key in diff.enrichment_tables.sinks.changed_and_added() {
814                if let Some(task) = new_pieces.tasks.get(key) {
815                    self.component_type_names
816                        .insert(key.clone(), task.typetag().to_string());
817                }
818            }
819
820            for (key, input) in &new_pieces.inputs {
821                self.inputs_tap_metadata
822                    .insert(key.clone(), input.1.clone());
823            }
824        }
825
826        // We configure the outputs of any changed/added sources first, so they're available to any
827        // transforms and sinks that come afterwards.
828        for key in diff.sources.changed_and_added() {
829            debug!(component_id = %key, "Configuring outputs for source.");
830            self.setup_outputs(key, new_pieces).await;
831        }
832
833        let added_changed_table_sources: Vec<ComponentKey> = diff
834            .enrichment_tables
835            .sources
836            .changed_and_added()
837            .cloned()
838            .collect();
839        for key in &added_changed_table_sources {
840            debug!(component_id = %key, "Connecting outputs for enrichment table source.");
841            self.setup_outputs(key, new_pieces).await;
842        }
843
844        // We configure the outputs of any changed/added transforms next, for the same reason: we
845        // need them to be available to any transforms and sinks that come afterwards.
846        for key in diff.transforms.changed_and_added() {
847            debug!(component_id = %key, "Configuring outputs for transform.");
848            self.setup_outputs(key, new_pieces).await;
849        }
850
851        // Now that all possible outputs are configured, we can start wiring up inputs, starting
852        // with transforms.
853        for key in diff.transforms.changed_and_added() {
854            debug!(component_id = %key, "Connecting inputs for transform.");
855            self.setup_inputs(key, diff, new_pieces).await;
856        }
857
858        // Now that all sources and transforms are fully configured, we can wire up sinks.
859        for key in diff.sinks.changed_and_added() {
860            debug!(component_id = %key, "Connecting inputs for sink.");
861            self.setup_inputs(key, diff, new_pieces).await;
862        }
863        let added_changed_tables: Vec<ComponentKey> = diff
864            .enrichment_tables
865            .sinks
866            .changed_and_added()
867            .cloned()
868            .collect();
869        for key in &added_changed_tables {
870            debug!(component_id = %key, "Connecting inputs for enrichment table sink.");
871            self.setup_inputs(key, diff, new_pieces).await;
872        }
873
874        // We do a final pass here to reconnect unchanged components.
875        //
876        // Why would we reconnect unchanged components?  Well, as sources and transforms will
877        // recreate their fanouts every time they're changed, we can run into a situation where a
878        // transform/sink, which we'll call B, is pointed at a source/transform that was changed, which
879        // we'll call A, but because B itself didn't change at all, we haven't yet reconnected it.
880        //
881        // Instead of propagating connections forward -- B reconnecting A forcefully -- we only
882        // connect components backwards i.e. transforms to sources/transforms, and sinks to
883        // sources/transforms, to ensure we're connecting components in order.
884        self.reattach_severed_inputs(diff);
885
886        // Broadcast any topology changes to subscribers.
887        if !self.watch.0.is_closed() {
888            let outputs = self
889                .outputs
890                .clone()
891                .into_iter()
892                .flat_map(|(output_id, control_tx)| {
893                    self.outputs_tap_metadata.get(&output_id.component).map(
894                        |(component_kind, component_type)| {
895                            (
896                                TapOutput {
897                                    output_id,
898                                    component_kind,
899                                    component_type: component_type.clone(),
900                                },
901                                control_tx,
902                            )
903                        },
904                    )
905                })
906                .collect::<HashMap<_, _>>();
907
908            let mut removals = diff.sources.to_remove.clone();
909            removals.extend(diff.transforms.to_remove.iter().cloned());
910            self.watch
911                .0
912                .send(TapResource {
913                    outputs,
914                    inputs: self.inputs_tap_metadata.clone(),
915                    source_keys: diff
916                        .sources
917                        .changed_and_added()
918                        .map(|key| key.to_string())
919                        .chain(
920                            added_changed_table_sources
921                                .iter()
922                                .map(|key| key.to_string()),
923                        )
924                        .collect(),
925                    sink_keys: diff
926                        .sinks
927                        .changed_and_added()
928                        .map(|key| key.to_string())
929                        .chain(added_changed_tables.iter().map(|key| key.to_string()))
930                        .collect(),
931                    // Note, only sources and transforms are relevant. Sinks do
932                    // not have outputs to tap.
933                    removals,
934                    type_names: self
935                        .component_type_names
936                        .iter()
937                        .map(|(k, v)| (k.to_string(), v.clone()))
938                        .collect(),
939                })
940                .expect("Couldn't broadcast config changes.");
941        }
942    }
943
944    async fn setup_outputs(
945        &mut self,
946        key: &ComponentKey,
947        new_pieces: &mut builder::TopologyPieces,
948    ) {
949        let outputs = new_pieces.outputs.remove(key).unwrap();
950        for (port, output) in outputs {
951            debug!(component_id = %key, output_id = ?port, "Configuring output for component.");
952
953            let id = OutputId {
954                component: key.clone(),
955                port,
956            };
957
958            self.outputs.insert(id, output);
959        }
960    }
961
962    async fn setup_inputs(
963        &mut self,
964        key: &ComponentKey,
965        diff: &ConfigDiff,
966        new_pieces: &mut builder::TopologyPieces,
967    ) {
968        let (tx, inputs) = new_pieces.inputs.remove(key).unwrap();
969
970        let old_inputs = self
971            .config
972            .inputs_for_node(key)
973            .into_iter()
974            .flatten()
975            .cloned()
976            .collect::<HashSet<_>>();
977
978        let new_inputs = inputs.iter().cloned().collect::<HashSet<_>>();
979        let inputs_to_add = &new_inputs - &old_inputs;
980
981        for input in inputs {
982            let output = self.outputs.get_mut(&input).expect("unknown output");
983
984            if diff.contains(&input.component) || inputs_to_add.contains(&input) {
985                // If the input we're connecting to is changing, that means its outputs will have been
986                // recreated, so instead of replacing a paused sink, we have to add it to this new
987                // output for the first time, since there's nothing to actually replace at this point.
988                debug!(component_id = %key, fanout_id = %input, "Adding component input to fanout.");
989
990                _ = output.send(ControlMessage::Add(key.clone(), tx.clone()));
991            } else {
992                // We know that if this component is connected to a given input, and neither
993                // components were changed, then the output must still exist, which means we paused
994                // this component's connection to its output, so we have to replace that connection
995                // now:
996                debug!(component_id = %key, fanout_id = %input, "Replacing component input in fanout.");
997
998                _ = output.send(ControlMessage::Replace(key.clone(), tx.clone()));
999            }
1000        }
1001
1002        self.inputs.insert(key.clone(), tx);
1003        new_pieces
1004            .detach_triggers
1005            .remove(key)
1006            .map(|trigger| self.detach_triggers.insert(key.clone(), trigger.into()));
1007    }
1008
1009    fn remove_outputs(&mut self, key: &ComponentKey) {
1010        self.outputs.retain(|id, _output| &id.component != key);
1011    }
1012
1013    async fn remove_inputs(&mut self, key: &ComponentKey, diff: &ConfigDiff, new_config: &Config) {
1014        self.inputs.remove(key);
1015        self.detach_triggers.remove(key);
1016
1017        let old_inputs = self.config.inputs_for_node(key).expect("node exists");
1018        let new_inputs = new_config
1019            .inputs_for_node(key)
1020            .unwrap_or_default()
1021            .iter()
1022            .collect::<HashSet<_>>();
1023
1024        for input in old_inputs {
1025            if let Some(output) = self.outputs.get_mut(input) {
1026                if diff.contains(&input.component)
1027                    || diff.is_removed(key)
1028                    || !new_inputs.contains(input)
1029                {
1030                    // 3 cases to remove the input:
1031                    //
1032                    // Case 1: If the input we're removing ourselves from is changing, that means its
1033                    // outputs will be recreated, so instead of pausing the sink, we just delete it
1034                    // outright to ensure things are clean.
1035                    //
1036                    // Case 2: If this component itself is being removed, then pausing makes no sense
1037                    // because it isn't coming back.
1038                    //
1039                    // Case 3: This component is no longer connected to the input from new config.
1040                    debug!(component_id = %key, fanout_id = %input, "Removing component input from fanout.");
1041
1042                    _ = output.send(ControlMessage::Remove(key.clone()));
1043                } else {
1044                    // We know that if this component is connected to a given input, and it isn't being
1045                    // changed, then it will exist when we reconnect inputs, so we should pause it
1046                    // now to pause further sends through that component until we reconnect:
1047                    debug!(component_id = %key, fanout_id = %input, "Pausing component input in fanout.");
1048
1049                    _ = output.send(ControlMessage::Pause(key.clone()));
1050                }
1051            }
1052        }
1053    }
1054
1055    fn reattach_severed_inputs(&mut self, diff: &ConfigDiff) {
1056        let unchanged_transforms = self
1057            .config
1058            .transforms()
1059            .filter(|(key, _)| !diff.transforms.contains(key));
1060        for (transform_key, transform) in unchanged_transforms {
1061            let changed_outputs = get_changed_outputs(diff, transform.inputs.clone());
1062            for output_id in changed_outputs {
1063                debug!(component_id = %transform_key, fanout_id = %output_id.component, "Reattaching component input to fanout.");
1064
1065                let input = self.inputs.get(transform_key).cloned().unwrap();
1066                let output = self.outputs.get_mut(&output_id).unwrap();
1067                _ = output.send(ControlMessage::Add(transform_key.clone(), input));
1068            }
1069        }
1070
1071        let unchanged_table_sinks = self
1072            .config
1073            .enrichment_tables()
1074            .filter_map(|(key, table)| table.as_sink(key))
1075            .filter(|(key, _)| !diff.enrichment_tables.sinks.contains(key))
1076            .collect::<Vec<_>>();
1077        let unchanged_sinks = self
1078            .config
1079            .sinks()
1080            .filter(|(key, _)| !diff.sinks.contains(key));
1081        for (sink_key, sink) in
1082            unchanged_sinks.chain(unchanged_table_sinks.iter().map(|(k, v)| (k, v)))
1083        {
1084            let changed_outputs = get_changed_outputs(diff, sink.inputs.clone());
1085            for output_id in changed_outputs {
1086                debug!(component_id = %sink_key, fanout_id = %output_id.component, "Reattaching component input to fanout.");
1087
1088                let input = self.inputs.get(sink_key).cloned().unwrap();
1089                let output = self.outputs.get_mut(&output_id).unwrap();
1090                _ = output.send(ControlMessage::Add(sink_key.clone(), input));
1091            }
1092        }
1093    }
1094
1095    /// Reconcile the `vector_security_confinement_disabled` gauges with the
1096    /// currently active topology.
1097    ///
1098    /// The topology is the single owner of this gauge: it holds a handle for
1099    /// every confinement-aware sink for the sink's whole lifetime, which keeps
1100    /// the metric from expiring out of the registry (a dropped handle would let
1101    /// it age out after the idle timeout even while the sink runs). The value
1102    /// is `1` when the sink opted out of confinement, `0` otherwise.
1103    ///
1104    /// This rebuilds from `self.config`, so it must be called *after* `self.config`
1105    /// reflects the active topology (updated to the new config on a successful
1106    /// reload, or left as the old config on a rollback). Handles for sinks no
1107    /// longer present are dropped, allowing their series to expire naturally.
1108    fn refresh_confinement_gauges(&mut self) {
1109        // Rebuild the handle set from the active config on every call. Reusing a
1110        // handle keyed only by `ComponentKey` would be wrong: a reloaded sink
1111        // can keep its id but change `type`, and a cached handle carries the old
1112        // `component_type` label. Recreating is cheap — `gauge!` returns a
1113        // handle to the same registry entry for a given label set.
1114        let mut gauges = HashMap::with_capacity(self.sink_confinement_gauges.len());
1115        for (key, outer) in self.config.sinks() {
1116            let Some(confinement) = outer.inner.confinement_config() else {
1117                continue;
1118            };
1119            let value = f64::from(confinement.dangerously_allow_unconfined_template_resolution);
1120            let handle = gauge!(
1121                GaugeName::SecurityConfinementDisabled,
1122                "component_kind" => "sink",
1123                "component_id" => key.id().to_string(),
1124                "component_type" => outer.inner.get_component_name(),
1125            );
1126            handle.set(value);
1127            gauges.insert(key.clone(), handle);
1128        }
1129
1130        // Replacing the map drops handles for sinks that are gone (or changed
1131        // type), letting their now-stale series expire instead of reporting a
1132        // stale value forever.
1133        self.sink_confinement_gauges = gauges;
1134    }
1135
1136    /// Starts any new or changed components in the given configuration diff.
1137    pub(crate) fn spawn_diff(&mut self, diff: &ConfigDiff, mut new_pieces: TopologyPieces) {
1138        for key in &diff.sources.to_change {
1139            debug!(message = "Spawning changed source.", component_id = %key);
1140            self.spawn_source(key, &mut new_pieces);
1141        }
1142
1143        for key in &diff.sources.to_add {
1144            debug!(message = "Spawning new source.", component_id = %key);
1145            self.spawn_source(key, &mut new_pieces);
1146        }
1147
1148        let changed_table_sources: Vec<&ComponentKey> = diff
1149            .enrichment_tables
1150            .sources
1151            .to_change
1152            .iter()
1153            .filter(|k| new_pieces.source_tasks.contains_key(k))
1154            .collect();
1155
1156        let added_table_sources: Vec<&ComponentKey> = diff
1157            .enrichment_tables
1158            .sources
1159            .to_add
1160            .iter()
1161            .filter(|k| new_pieces.source_tasks.contains_key(k))
1162            .collect();
1163
1164        for key in changed_table_sources {
1165            debug!(message = "Spawning changed enrichment table source.", component_id = %key);
1166            self.spawn_source(key, &mut new_pieces);
1167        }
1168
1169        for key in added_table_sources {
1170            debug!(message = "Spawning new enrichment table source.", component_id = %key);
1171            self.spawn_source(key, &mut new_pieces);
1172        }
1173
1174        for key in &diff.transforms.to_change {
1175            debug!(message = "Spawning changed transform.", component_id = %key);
1176            self.spawn_transform(key, &mut new_pieces);
1177        }
1178
1179        for key in &diff.transforms.to_add {
1180            debug!(message = "Spawning new transform.", component_id = %key);
1181            self.spawn_transform(key, &mut new_pieces);
1182        }
1183
1184        for key in &diff.sinks.to_change {
1185            debug!(message = "Spawning changed sink.", component_id = %key);
1186            self.spawn_sink(key, &mut new_pieces);
1187        }
1188
1189        for key in &diff.sinks.to_add {
1190            trace!(message = "Spawning new sink.", component_id = %key);
1191            self.spawn_sink(key, &mut new_pieces);
1192        }
1193
1194        let changed_tables: Vec<&ComponentKey> = diff
1195            .enrichment_tables
1196            .sinks
1197            .to_change
1198            .iter()
1199            .filter(|k| new_pieces.tasks.contains_key(k))
1200            .collect();
1201
1202        let added_tables: Vec<&ComponentKey> = diff
1203            .enrichment_tables
1204            .sinks
1205            .to_add
1206            .iter()
1207            .filter(|k| new_pieces.tasks.contains_key(k))
1208            .collect();
1209
1210        for key in changed_tables {
1211            debug!(message = "Spawning changed enrichment table sink.", component_id = %key);
1212            self.spawn_sink(key, &mut new_pieces);
1213        }
1214
1215        for key in added_tables {
1216            debug!(message = "Spawning enrichment table new sink.", component_id = %key);
1217            self.spawn_sink(key, &mut new_pieces);
1218        }
1219    }
1220
1221    fn spawn_sink(&mut self, key: &ComponentKey, new_pieces: &mut builder::TopologyPieces) {
1222        let task = new_pieces.tasks.remove(key).unwrap();
1223        let span = error_span!(
1224            "sink",
1225            component_kind = "sink",
1226            component_id = %task.id(),
1227            component_type = %task.typetag(),
1228        );
1229
1230        let task_span = span.or_current();
1231        #[cfg(unix)]
1232        if crate::internal_telemetry::allocations::is_allocation_tracing_enabled() {
1233            let group_id = crate::internal_telemetry::allocations::acquire_allocation_group_id(
1234                task.id().to_string(),
1235                "sink".to_string(),
1236                task.typetag().to_string(),
1237            );
1238            debug!(
1239                component_kind = "sink",
1240                component_type = task.typetag(),
1241                component_id = task.id(),
1242                group_id = group_id.as_raw().to_string(),
1243                "Registered new allocation group."
1244            );
1245            group_id.attach_to_span(&task_span);
1246        }
1247
1248        let task_name = format!(">> {} ({})", task.typetag(), task.id());
1249        let task = {
1250            let key = key.clone();
1251            handle_errors(task, self.abort_tx.clone(), |error| {
1252                ShutdownError::SinkAborted { key, error }
1253            })
1254        }
1255        .instrument(task_span);
1256        let spawned = spawn_named(task, task_name.as_ref());
1257        if let Some(previous) = self.tasks.insert(key.clone(), spawned) {
1258            drop(previous); // detach and forget
1259        }
1260    }
1261
1262    fn spawn_transform(&mut self, key: &ComponentKey, new_pieces: &mut builder::TopologyPieces) {
1263        let task = new_pieces.tasks.remove(key).unwrap();
1264        let span = error_span!(
1265            "transform",
1266            component_kind = "transform",
1267            component_id = %task.id(),
1268            component_type = %task.typetag(),
1269        );
1270
1271        let task_span = span.or_current();
1272        #[cfg(unix)]
1273        if crate::internal_telemetry::allocations::is_allocation_tracing_enabled() {
1274            let group_id = crate::internal_telemetry::allocations::acquire_allocation_group_id(
1275                task.id().to_string(),
1276                "transform".to_string(),
1277                task.typetag().to_string(),
1278            );
1279            debug!(
1280                component_kind = "transform",
1281                component_type = task.typetag(),
1282                component_id = task.id(),
1283                group_id = group_id.as_raw().to_string(),
1284                "Registered new allocation group."
1285            );
1286            group_id.attach_to_span(&task_span);
1287        }
1288
1289        let task_name = format!(">> {} ({}) >>", task.typetag(), task.id());
1290        let task = {
1291            let key = key.clone();
1292            handle_errors(task, self.abort_tx.clone(), |error| {
1293                ShutdownError::TransformAborted { key, error }
1294            })
1295        }
1296        .instrument(task_span);
1297        let spawned = spawn_named(task, task_name.as_ref());
1298        if let Some(previous) = self.tasks.insert(key.clone(), spawned) {
1299            drop(previous); // detach and forget
1300        }
1301    }
1302
1303    fn spawn_source(&mut self, key: &ComponentKey, new_pieces: &mut builder::TopologyPieces) {
1304        let task = new_pieces.tasks.remove(key).unwrap();
1305        let span = error_span!(
1306            "source",
1307            component_kind = "source",
1308            component_id = %task.id(),
1309            component_type = %task.typetag(),
1310        );
1311
1312        let task_span = span.or_current();
1313        #[cfg(unix)]
1314        if crate::internal_telemetry::allocations::is_allocation_tracing_enabled() {
1315            let group_id = crate::internal_telemetry::allocations::acquire_allocation_group_id(
1316                task.id().to_string(),
1317                "source".to_string(),
1318                task.typetag().to_string(),
1319            );
1320
1321            debug!(
1322                component_kind = "source",
1323                component_type = task.typetag(),
1324                component_id = task.id(),
1325                group_id = group_id.as_raw().to_string(),
1326                "Registered new allocation group."
1327            );
1328            group_id.attach_to_span(&task_span);
1329        }
1330
1331        let task_name = format!("{} ({}) >>", task.typetag(), task.id());
1332        let task = {
1333            let key = key.clone();
1334            handle_errors(task, self.abort_tx.clone(), |error| {
1335                ShutdownError::SourceAborted { key, error }
1336            })
1337        }
1338        .instrument(task_span.clone());
1339        let spawned = spawn_named(task, task_name.as_ref());
1340        if let Some(previous) = self.tasks.insert(key.clone(), spawned) {
1341            drop(previous); // detach and forget
1342        }
1343
1344        self.shutdown_coordinator
1345            .takeover_source(key, &mut new_pieces.shutdown_coordinator);
1346
1347        // Now spawn the actual source task.
1348        let source_task = new_pieces.source_tasks.remove(key).unwrap();
1349        let source_task = {
1350            let key = key.clone();
1351            handle_errors(source_task, self.abort_tx.clone(), |error| {
1352                ShutdownError::SourceAborted { key, error }
1353            })
1354        }
1355        .instrument(task_span);
1356        self.source_tasks
1357            .insert(key.clone(), spawn_named(source_task, task_name.as_ref()));
1358    }
1359
1360    pub async fn start_init_validated(
1361        config: Config,
1362        extra_context: ExtraContext,
1363    ) -> Option<(Self, ShutdownErrorReceiver)> {
1364        let diff = ConfigDiff::initial(&config);
1365        let pieces = TopologyPiecesBuilder::new(&config, &diff)
1366            .with_extra_context(extra_context)
1367            .build_or_log_errors()
1368            .await?;
1369        Self::start_validated(config, diff, pieces).await
1370    }
1371
1372    pub async fn start_validated(
1373        config: Config,
1374        diff: ConfigDiff,
1375        mut pieces: TopologyPieces,
1376    ) -> Option<(Self, ShutdownErrorReceiver)> {
1377        let (abort_tx, abort_rx) = mpsc::unbounded_channel();
1378
1379        let expire_metrics = match (
1380            config.global.expire_metrics,
1381            config.global.expire_metrics_secs,
1382        ) {
1383            (Some(e), None) => {
1384                warn!(
1385                    "DEPRECATED: `expire_metrics` setting is deprecated and will be removed in a future version. Use `expire_metrics_secs` instead."
1386                );
1387                if e < Duration::from_secs(0) {
1388                    None
1389                } else {
1390                    Some(e.as_secs_f64())
1391                }
1392            }
1393            (Some(_), Some(_)) => {
1394                error!(
1395                    message = "Cannot set both `expire_metrics` and `expire_metrics_secs`.",
1396                    internal_log_rate_limit = false
1397                );
1398                return None;
1399            }
1400            (None, Some(e)) => {
1401                if e < 0f64 {
1402                    None
1403                } else {
1404                    Some(e)
1405                }
1406            }
1407            (None, None) => Some(300f64),
1408        };
1409
1410        if let Err(error) = crate::metrics::Controller::get()
1411            .expect("Metrics must be initialized")
1412            .set_expiry(
1413                expire_metrics,
1414                config
1415                    .global
1416                    .expire_metrics_per_metric_set
1417                    .clone()
1418                    .unwrap_or_default(),
1419            )
1420        {
1421            error!(message = "Invalid metrics expiry.", %error, internal_log_rate_limit = false);
1422            return None;
1423        }
1424
1425        let (utilization_emitter, utilization_registry) = pieces
1426            .utilization
1427            .take()
1428            .expect("Topology is missing the utilization metric emitter!");
1429        let metrics_storage = pieces.metrics_storage.clone();
1430        let metrics_refresh_period = config
1431            .global
1432            .metrics_storage_refresh_period
1433            .map(Duration::from_secs_f64);
1434        let mut running_topology = Self::new(config, abort_tx);
1435
1436        if !running_topology
1437            .run_healthchecks(&diff, &mut pieces, running_topology.config.healthchecks)
1438            .await
1439        {
1440            return None;
1441        }
1442        running_topology.connect_diff(&diff, &mut pieces).await;
1443        running_topology.spawn_diff(&diff, pieces);
1444        // `running_topology.config` was set from the initial config in `new()`.
1445        running_topology.refresh_confinement_gauges();
1446
1447        let (utilization_task_shutdown_trigger, utilization_shutdown_signal, _) =
1448            ShutdownSignal::new_wired();
1449        running_topology.utilization_registry = Some(utilization_registry.clone());
1450        running_topology.utilization_task_shutdown_trigger =
1451            Some(utilization_task_shutdown_trigger);
1452        running_topology.utilization_task = Some(tokio::spawn(Task::new(
1453            "utilization_heartbeat".into(),
1454            "",
1455            async move {
1456                utilization_emitter
1457                    .run_utilization(utilization_shutdown_signal)
1458                    .await;
1459                Ok(TaskOutput::Healthcheck)
1460            },
1461        )));
1462        if let Some(metrics_refresh_period) = metrics_refresh_period {
1463            let (metrics_task_shutdown_trigger, metrics_shutdown_signal, _) =
1464                ShutdownSignal::new_wired();
1465            running_topology.metrics_task_shutdown_trigger = Some(metrics_task_shutdown_trigger);
1466            running_topology.metrics_task = Some(tokio::spawn(Task::new(
1467                "metrics_heartbeat".into(),
1468                "",
1469                async move {
1470                    metrics_storage
1471                        .run_periodic_refresh(metrics_refresh_period, metrics_shutdown_signal)
1472                        .await;
1473                    Ok(TaskOutput::Healthcheck)
1474                },
1475            )));
1476        }
1477
1478        Some((running_topology, abort_rx))
1479    }
1480}
1481
1482/// Returns the subset of `output_ids` whose upstream fanout was replaced during this reload and so
1483/// must be reattached to the still-running downstream consumer.
1484///
1485/// A producer's fanout is replaced when its key was respawned in place (`to_change`) or when it
1486/// disappeared as one kind of producer and reappeared as another (e.g. an enrichment-table-derived
1487/// source removed while a regular source with the same key was added, or a transform removed while
1488/// a source with the same key was added).
1489fn get_changed_outputs(diff: &ConfigDiff, output_ids: Inputs<OutputId>) -> Vec<OutputId> {
1490    let producer_destroyed = |key: &ComponentKey| {
1491        diff.sources.to_change.contains(key)
1492            || diff.sources.to_remove.contains(key)
1493            || diff.transforms.to_change.contains(key)
1494            || diff.transforms.to_remove.contains(key)
1495            || diff.enrichment_tables.sources.to_change.contains(key)
1496            || diff.enrichment_tables.sources.to_remove.contains(key)
1497    };
1498    let producer_recreated = |key: &ComponentKey| {
1499        diff.sources.to_change.contains(key)
1500            || diff.sources.to_add.contains(key)
1501            || diff.transforms.to_change.contains(key)
1502            || diff.transforms.to_add.contains(key)
1503            || diff.enrichment_tables.sources.to_change.contains(key)
1504            || diff.enrichment_tables.sources.to_add.contains(key)
1505    };
1506
1507    output_ids
1508        .iter()
1509        .filter(|id| producer_destroyed(&id.component) && producer_recreated(&id.component))
1510        .cloned()
1511        .collect()
1512}
1513
1514fn enrichment_table_sink_resources(config: &Config, sink_key: &ComponentKey) -> Vec<Resource> {
1515    config
1516        .enrichment_tables()
1517        .filter_map(|(table_key, table)| table.as_sink(table_key))
1518        .find(|(key, _)| key == sink_key)
1519        .map(|(key, sink)| sink.resources(&key))
1520        .unwrap_or_default()
1521}
1522
1523fn enrichment_table_sink_buffer(
1524    config: &Config,
1525    sink_key: &ComponentKey,
1526) -> Option<vector_lib::buffers::BufferConfig> {
1527    config
1528        .enrichment_tables()
1529        .filter_map(|(table_key, table)| table.as_sink(table_key))
1530        .find(|(key, _)| key == sink_key)
1531        .map(|(_, sink)| sink.buffer)
1532}