Repository navigation
feat(topology): add opt-in reload sink drain limit #26834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: remove-windows-reload-sleep
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| Add the `--reload-sink-drain-limit-secs` option (environment variable `VECTOR_RELOAD_SINK_DRAIN_LIMIT_SECS`). It sets how long removed and changed sinks can take to finish during a configuration reload, so that a sink that does not stop cannot block the reload. All of these sinks share one deadline. After the deadline, Vector cancels their in-flight work. A replacement sink that reuses the buffer receives the unread events. Other unread and in-flight events are lost. | ||
|
|
||
| The option is not set by default, and Vector then waits until the sinks finish, as before. `--graceful-shutdown-limit-secs` does not apply to reloads. | ||
|
|
||
| authors: thomasqueirozb |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,6 +137,7 @@ struct Builder<'a> { | |
| inputs: HashMap<ComponentKey, (BufferSender<EventArray>, Inputs<OutputId>)>, | ||
| healthchecks: HashMap<ComponentKey, Task>, | ||
| detach_triggers: HashMap<ComponentKey, Trigger>, | ||
| reload_shutdown_triggers: HashMap<ComponentKey, oneshot::Sender<tokio::time::Instant>>, | ||
| extra_context: ExtraContext, | ||
| utilization_emitter: Option<UtilizationEmitter>, | ||
| utilization_registry: UtilizationRegistry, | ||
|
|
@@ -169,6 +170,7 @@ impl<'a> Builder<'a> { | |
| inputs: HashMap::new(), | ||
| healthchecks: HashMap::new(), | ||
| detach_triggers: HashMap::new(), | ||
| reload_shutdown_triggers: HashMap::new(), | ||
| extra_context, | ||
| utilization_emitter: emitter, | ||
| utilization_registry: registry, | ||
|
|
@@ -196,6 +198,7 @@ impl<'a> Builder<'a> { | |
| healthchecks: self.healthchecks, | ||
| shutdown_coordinator: self.shutdown_coordinator, | ||
| detach_triggers: self.detach_triggers, | ||
| reload_shutdown_triggers: self.reload_shutdown_triggers, | ||
| metrics_storage: METRICS_STORAGE.clone(), | ||
| utilization: self | ||
| .utilization_emitter | ||
|
|
@@ -719,6 +722,7 @@ impl<'a> Builder<'a> { | |
| }; | ||
|
|
||
| let (trigger, tripwire) = Tripwire::new(); | ||
| let (reload_shutdown_tx, reload_shutdown_rx) = oneshot::channel(); | ||
|
|
||
| let utilization_sender = self | ||
| .utilization_registry | ||
|
|
@@ -742,24 +746,40 @@ impl<'a> Builder<'a> { | |
| let mut rx = Utilization::new(utilization_sender, component_key.clone(), rx); | ||
|
|
||
| let events_received = register!(EventsReceived); | ||
| sink.run( | ||
| rx.by_ref() | ||
| .filter(|events: &EventArray| ready(filter_events_type(events, input_type))) | ||
| .inspect(|events| { | ||
| events_received.emit(CountByteSize( | ||
| events.len(), | ||
| events.estimated_json_encoded_size_of(), | ||
| )); | ||
| }) | ||
| .take_until_if(tripwire), | ||
| ) | ||
| .await | ||
| .map(|()| { | ||
| debug!("Sink finished normally."); | ||
| TaskOutput::Sink(rx) | ||
| }) | ||
| .inspect_err(|error| debug!(%error, "Sink finished with an error.")) | ||
| .map_err(Into::into) | ||
| let reload_deadline = async move { | ||
| if let Ok(deadline) = reload_shutdown_rx.await { | ||
| tokio::time::sleep_until(deadline).await; | ||
| } else { | ||
| // Dropping the topology during process shutdown must not cancel the sink here. | ||
| futures::future::pending::<()>().await; | ||
| } | ||
| }; | ||
| let result = select! { | ||
| biased; | ||
| result = sink.run( | ||
| rx.by_ref() | ||
| .filter(|events: &EventArray| ready(filter_events_type(events, input_type))) | ||
| .inspect(|events| { | ||
| events_received.emit(CountByteSize( | ||
| events.len(), | ||
| events.estimated_json_encoded_size_of(), | ||
| )); | ||
| }) | ||
| .take_until_if(tripwire), | ||
| ) => result, | ||
| () = reload_deadline => { | ||
| warn!("Sink did not finish before the reload deadline. Cancelling in-flight work."); | ||
| // Drop only the sink future, retaining unread buffered events for its replacement. | ||
| Ok(()) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With the drain limit enabled, changing disk-buffer settings for an influxdb_logs or gcp_pubsub sink while a request remains pending beyond the deadline can terminate Vector. PartitionBatchSink spawns request tasks that survive dropping sink.run. Their event finalizers keep the disk ledger’s buffer.lock held, so both the replacement and rollback fail to open the same buffer, triggering FailedToRestore and shutdown. Cancel and await spawned requests before reporting the sink stopped; this requires changes beyond this cancellation branch. React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it What each reaction means
🤖 Bits Code Review · Open Bits AI session |
||
| } | ||
| }; | ||
| result | ||
| .map(|()| { | ||
| debug!("Sink stopped."); | ||
| TaskOutput::Sink(rx) | ||
| }) | ||
| .inspect_err(|error| debug!(%error, "Sink finished with an error.")) | ||
| .map_err(Into::into) | ||
| }; | ||
|
|
||
| let task = Task::new(key.clone(), typetag, sink); | ||
|
|
@@ -806,6 +826,8 @@ impl<'a> Builder<'a> { | |
| self.healthchecks.insert(key.clone(), healthcheck_task); | ||
| self.tasks.insert(key.clone(), task); | ||
| self.detach_triggers.insert(key.clone(), trigger); | ||
| self.reload_shutdown_triggers | ||
| .insert(key.clone(), reload_shutdown_tx); | ||
| } | ||
|
|
||
| fn build_transform( | ||
|
|
@@ -1104,6 +1126,8 @@ pub struct TopologyPieces { | |
| pub(super) healthchecks: HashMap<ComponentKey, Task>, | ||
| pub(crate) shutdown_coordinator: SourceShutdownCoordinator, | ||
| pub(crate) detach_triggers: HashMap<ComponentKey, Trigger>, | ||
| pub(crate) reload_shutdown_triggers: | ||
| HashMap<ComponentKey, oneshot::Sender<tokio::time::Instant>>, | ||
| pub(crate) metrics_storage: MetricsStorage, | ||
| pub(crate) utilization: Option<(UtilizationEmitter, UtilizationRegistry)>, | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the option is unset, a reload does not wait for every removed sink:
shutdown_diffexplicitly drops the task handle for sinks outsidewait_for_sinks, allowing them to drain in the background while the reload continues (src/topology/running.rs:718-724). This help text, also repeated in the changelog and generated CLI reference, therefore misstates the existing behavior; describe the unset case as having no cancellation deadline, with only resource-conflicting or buffer-reusing sinks awaited.Useful? React with 👍 / 👎.