Skip to content

Commit d74438c

Browse files
authored
chore(app): handle bootstrap background tasks via dedicated supervisor (#1538)
## Summary This PR updates the bootstrap process/helpers to use supervision trees to handle all necessary background async tasks that arise out of the initialization of various subsystems. The bootstrap phase involves initializes various fundamental subsystems -- logging, metrics, TLS, etc -- prior to the application doing useful work. In many cases, these subsystems require background tasks to run in order to perform routine upkeep, or manage requests to alter behavior, and so on. Currently, these background tasks are all spawned directly on the ambient runtime where they cannot be restarted if they failed, and generally can't participate in the advanced mechanisms available to supervision trees, such as communicating via dataspaces. This PR updates the bootstrap process, and associated helper types, to build a single supervisor that contains all necessary workers (tasks) that must be driven in order to support the subsystems being initialized. The goal is two-fold: - move more async tasks into supervision trees (for reasons of reliability and observability) - gain access to advanced runtime features like dataspaces The work here is meant as a stepping stone towards supporting dynamic log level overrides that come from changes to `log_level` in ADP's primary configuration. By switching the relevant background task that handles log level overrides currently to run in a supervision tree, we're opening up the ability to change how interested code operates with it and communicates with it, allowing us to decouple ADP-specific needs from generic Saluki code. We've worker-ified four distinct background tasks: - `MetricsFlusherWorker`: bridge between our `metrics` recorder and emitting internal metrics to the `internal_metrics` source - `RuntimeMetricsWorker`: collects Tokio runtime metrics and re-emits them into our internal metrics - `MetricsOverrideWorker`: handles API requests to dynamically override/reset the current metrics level - `LoggingOverrideWorker`: handles API requests to dynamically override/reset the current log level ## Change Type - [ ] Bug fix - [ ] New feature - [x] Non-functional (chore, refactoring, docs) - [ ] Performance ## How did you test this PR? Existing unit and integration tests. ## References DADP-52 Co-authored-by: toby.lawrence <toby.lawrence@datadoghq.com>
1 parent 1990718 commit d74438c

8 files changed

Lines changed: 362 additions & 91 deletions

File tree

bin/agent-data-plane/src/cli/run.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use saluki_components::{
3131
};
3232
use saluki_config::{ConfigurationLoader, GenericConfiguration};
3333
use saluki_core::health::HealthRegistry;
34-
use saluki_core::runtime::SupervisorError;
34+
use saluki_core::runtime::{Supervisor, SupervisorError};
3535
use saluki_core::topology::TopologyBlueprint;
3636
use saluki_env::EnvironmentProvider as _;
3737
use saluki_error::{generic_error, ErrorContext as _, GenericError};
@@ -62,6 +62,7 @@ pub struct RunCommand {
6262
/// Entrypoint for the `run` commands.
6363
pub async fn handle_run_command(
6464
started: Instant, bootstrap_config: GenericConfiguration, bootstrap_guard: &mut BootstrapGuard,
65+
bootstrap_supervisor: Supervisor,
6566
) -> Result<(), GenericError> {
6667
let app_details = saluki_metadata::get_app_details();
6768
info!(
@@ -179,6 +180,11 @@ pub async fn handle_run_command(
179180
.await
180181
.error_context("Failed to create internal supervisor.")?;
181182

183+
// Attach the bootstrap supervisor as a child so its workers (logging/metrics override processors,
184+
// metrics flusher, runtime metrics collector) are driven and shut down alongside the rest of the
185+
// internal supervision tree.
186+
internal_supervisor.add_worker(bootstrap_supervisor);
187+
182188
// Create shutdown channel for the internal supervisor - we'll drive it in the main select loop
183189
let (internal_shutdown_tx, internal_shutdown_rx) = tokio::sync::oneshot::channel();
184190
let internal_supervisor_fut = internal_supervisor.run_with_shutdown(internal_shutdown_rx).fuse();

bin/agent-data-plane/src/main.rs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
#![deny(missing_docs)]
88
use std::time::Instant;
99

10-
use saluki_app::bootstrap::{AppBootstrapper, BootstrapGuard};
10+
use saluki_app::bootstrap::{AppBootstrapper, Bootstrap, BootstrapGuard};
1111
use saluki_components::config::{DatadogRemapper, KEY_ALIASES};
1212
use saluki_config::{ConfigurationLoader, GenericConfiguration};
13+
use saluki_core::runtime::Supervisor;
1314
use saluki_error::{ErrorContext as _, GenericError};
1415
use tracing::{error, info, warn};
1516

@@ -64,13 +65,25 @@ async fn main() -> Result<(), GenericError> {
6465
.error_context("Failed to parse bootstrap configuration during bootstrap phase.")?
6566
.with_metrics_prefix("adp")
6667
.with_logging_configuration(bootstrap_logging_config);
67-
let mut bootstrap_guard = bootstrapper
68+
let Bootstrap {
69+
supervisor: bootstrap_supervisor,
70+
guard: mut bootstrap_guard,
71+
} = bootstrapper
6872
.bootstrap()
6973
.await
7074
.error_context("Failed to complete bootstrap phase.")?;
7175

72-
// Run the given subcommand.
73-
let maybe_exit_code = run_inner(cli.action, started, bootstrap_config, &mut bootstrap_guard).await?;
76+
// Run the given subcommand. The bootstrap supervisor is forwarded by value; only the long-lived `run`
77+
// subcommand actually drives it (it is added as a child of the internal supervisor inside
78+
// `handle_run_command`). All other subcommands drop it on entry.
79+
let maybe_exit_code = run_inner(
80+
cli.action,
81+
started,
82+
bootstrap_config,
83+
&mut bootstrap_guard,
84+
bootstrap_supervisor,
85+
)
86+
.await?;
7487

7588
// Drop the bootstrap guard to ensure logs are flushed, etc.
7689
drop(bootstrap_guard);
@@ -85,6 +98,7 @@ async fn main() -> Result<(), GenericError> {
8598

8699
async fn run_inner(
87100
action: Action, started: Instant, bootstrap_config: GenericConfiguration, bootstrap_guard: &mut BootstrapGuard,
101+
bootstrap_supervisor: Supervisor,
88102
) -> Result<Option<i32>, GenericError> {
89103
match action {
90104
Action::Run(cmd) => {
@@ -97,16 +111,17 @@ async fn run_inner(
97111
}
98112
}
99113

100-
let exit_code = match handle_run_command(started, bootstrap_config, bootstrap_guard).await {
101-
Ok(()) => {
102-
info!("Agent Data Plane stopped.");
103-
None
104-
}
105-
Err(e) => {
106-
error!("{:?}", e);
107-
Some(1)
108-
}
109-
};
114+
let exit_code =
115+
match handle_run_command(started, bootstrap_config, bootstrap_guard, bootstrap_supervisor).await {
116+
Ok(()) => {
117+
info!("Agent Data Plane stopped.");
118+
None
119+
}
120+
Err(e) => {
121+
error!("{:?}", e);
122+
Some(1)
123+
}
124+
};
110125

111126
// Remove the PID file, if configured.
112127
if let Some(pid_file) = &cmd.pid_file {

lib/saluki-app/src/bootstrap.rs

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Bootstrap utilities.
22
33
use saluki_config::GenericConfiguration;
4+
use saluki_core::runtime::Supervisor;
45
use saluki_error::{ErrorContext as _, GenericError};
56

67
use crate::{
@@ -9,6 +10,20 @@ use crate::{
910
tls::initialize_tls,
1011
};
1112

13+
/// The result of running [`AppBootstrapper::bootstrap`].
14+
///
15+
/// Bundles together the [`BootstrapGuard`] that must be held for the lifetime of the application with the
16+
/// [`Supervisor`] that drives the background workers spawned during bootstrap. Callers must arrange for the
17+
/// supervisor to be run (either directly or by adding it to a parent supervisor) for those workers to make
18+
/// progress.
19+
pub struct Bootstrap {
20+
/// Supervisor populated with workers for all background async tasks created during bootstrap.
21+
pub supervisor: Supervisor,
22+
23+
/// Drop guard for resources acquired during bootstrap.
24+
pub guard: BootstrapGuard,
25+
}
26+
1227
/// A drop guard for ensuring deferred cleanup of resources acquired during bootstrap.
1328
pub struct BootstrapGuard {
1429
logging_guard: LoggingGuard,
@@ -79,26 +94,40 @@ impl AppBootstrapper {
7994

8095
/// Executes the bootstrap operation, initializing all configured subsystems.
8196
///
82-
/// A [`BootstrapGuard`] is returned, which must be held until the application is ready to shut down. This guard
83-
/// ensures that all relevant resources created during the bootstrap phase are properly cleaned up/flushed before
84-
/// the application exits.
97+
/// Returns a [`Bootstrap`] containing both a [`BootstrapGuard`] (which must be held until the application is
98+
/// ready to shut down) and a [`Supervisor`] populated with workers for all background async tasks created
99+
/// during bootstrap. Callers must arrange for the supervisor to run (typically by adding it to a parent
100+
/// supervisor or calling [`Supervisor::run_with_shutdown`]) for those workers to make progress.
85101
///
86102
/// # Errors
87103
///
88104
/// If any of the bootstrap steps fail, an error will be returned.
89-
pub async fn bootstrap(self) -> Result<BootstrapGuard, GenericError> {
105+
pub async fn bootstrap(self) -> Result<Bootstrap, GenericError> {
90106
// Initialize the logging subsystem first, since we want to make it possible to get any logs from the rest of
91107
// the bootstrap process.
92-
let logging_guard = initialize_logging(self.logging_config)
108+
let (logging_guard, logging_override) = initialize_logging(self.logging_config)
93109
.await
94110
.error_context("Failed to initialize logging subsystem.")?;
95111

96112
// Initialize everything else.
97113
initialize_tls().error_context("Failed to initialize TLS subsystem.")?;
98-
initialize_metrics(self.metrics_config)
114+
let metrics_workers = initialize_metrics(self.metrics_config)
99115
.await
100116
.error_context("Failed to initialize metrics subsystem.")?;
101117

102-
Ok(BootstrapGuard { logging_guard })
118+
// Build the supervisor for all bootstrap-spawned background workers. The default ambient runtime mode is
119+
// appropriate here: these are lightweight tasks that share the parent runtime, and the runtime metrics
120+
// worker has already eagerly captured the parent's `Handle` so it always describes the right runtime.
121+
let mut supervisor =
122+
Supervisor::new("app-bootstrap").error_context("Failed to construct app bootstrap supervisor.")?;
123+
supervisor.add_worker(logging_override);
124+
supervisor.add_worker(metrics_workers.flusher);
125+
supervisor.add_worker(metrics_workers.runtime);
126+
supervisor.add_worker(metrics_workers.override_processor);
127+
128+
Ok(Bootstrap {
129+
supervisor,
130+
guard: BootstrapGuard { logging_guard },
131+
})
103132
}
104133
}

lib/saluki-app/src/logging/api.rs

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ use std::{
33
time::Duration,
44
};
55

6+
use async_trait::async_trait;
67
use saluki_api::{
78
extract::{Query, State},
89
response::IntoResponse,
910
routing::{post, Router},
1011
APIHandler, StatusCode,
1112
};
12-
use saluki_common::task::spawn_traced_named;
13+
use saluki_core::runtime::{InitializationError, ProcessShutdown, Supervisable, SupervisorFuture};
14+
use saluki_error::generic_error;
1315
use serde::Deserialize;
1416
use tokio::{select, sync::mpsc, time::sleep};
1517
use tracing::{error, info};
@@ -59,17 +61,28 @@ pub struct LoggingAPIHandler {
5961
}
6062

6163
impl LoggingAPIHandler {
62-
pub(super) fn new(base_filter: Arc<Mutex<EnvFilter>>, reload_handle: Handle<EnvFilter, Registry>) -> Self {
63-
// Spawn our background task that will handle override requests.
64+
/// Creates a new `LoggingAPIHandler` and a paired [`LoggingOverrideWorker`].
65+
///
66+
/// The worker must be added to a [`Supervisor`][saluki_core::runtime::Supervisor] for the handler's
67+
/// override/reset routes to take effect; without it, requests are accepted but never applied.
68+
pub(super) fn new(
69+
base_filter: Arc<Mutex<EnvFilter>>, reload_handle: Handle<EnvFilter, Registry>,
70+
) -> (Self, LoggingOverrideWorker) {
6471
let (override_tx, override_rx) = mpsc::channel(1);
65-
spawn_traced_named(
66-
"dynamic-logging-override-processor",
67-
process_override_requests(base_filter, reload_handle, override_rx),
68-
);
72+
let worker = LoggingOverrideWorker {
73+
state: Mutex::new(Some(LoggingOverrideWorkerState {
74+
base_filter,
75+
reload_handle,
76+
override_rx,
77+
})),
78+
};
6979

70-
Self {
71-
state: LoggingHandlerState { override_tx },
72-
}
80+
(
81+
Self {
82+
state: LoggingHandlerState { override_tx },
83+
},
84+
worker,
85+
)
7386
}
7487

7588
async fn override_handler(
@@ -125,17 +138,66 @@ impl APIHandler for LoggingAPIHandler {
125138
}
126139
}
127140

141+
/// A worker that processes dynamic log filter override requests sent via [`LoggingAPIHandler`].
142+
///
143+
/// Holds the receiving half of the override channel; the corresponding sender is held by the API handler
144+
/// (which is itself stored in a static after the logging subsystem is initialized). The worker exits cleanly
145+
/// on either supervisor shutdown or channel close.
146+
///
147+
/// One-shot: a successful initialization consumes the receiver. Restart by the supervisor will fail with an
148+
/// initialization error, propagating up to bring the supervisor down.
149+
pub struct LoggingOverrideWorker {
150+
state: Mutex<Option<LoggingOverrideWorkerState>>,
151+
}
152+
153+
struct LoggingOverrideWorkerState {
154+
base_filter: Arc<Mutex<EnvFilter>>,
155+
reload_handle: Handle<EnvFilter, Registry>,
156+
override_rx: mpsc::Receiver<Option<(Duration, EnvFilter)>>,
157+
}
158+
159+
#[async_trait]
160+
impl Supervisable for LoggingOverrideWorker {
161+
fn name(&self) -> &str {
162+
"dynamic-logging-override-processor"
163+
}
164+
165+
async fn initialize(&self, process_shutdown: ProcessShutdown) -> Result<SupervisorFuture, InitializationError> {
166+
let LoggingOverrideWorkerState {
167+
base_filter,
168+
reload_handle,
169+
override_rx,
170+
} = self
171+
.state
172+
.lock()
173+
.unwrap()
174+
.take()
175+
.ok_or_else(|| InitializationError::Failed {
176+
source: generic_error!("logging override worker has already been initialized"),
177+
})?;
178+
179+
Ok(Box::pin(async move {
180+
process_override_requests(base_filter, reload_handle, override_rx, process_shutdown).await;
181+
Ok(())
182+
}))
183+
}
184+
}
185+
128186
async fn process_override_requests(
129187
base_filter: Arc<Mutex<EnvFilter>>, reload_handle: Handle<EnvFilter, Registry>,
130-
mut rx: mpsc::Receiver<Option<(Duration, EnvFilter)>>,
188+
mut rx: mpsc::Receiver<Option<(Duration, EnvFilter)>>, mut process_shutdown: ProcessShutdown,
131189
) {
132190
let mut override_active = false;
133191
let override_timeout = sleep(Duration::from_secs(3600));
134192

135193
tokio::pin!(override_timeout);
136194

195+
let shutdown = process_shutdown.wait_for_shutdown();
196+
tokio::pin!(shutdown);
197+
137198
loop {
138199
select! {
200+
_ = &mut shutdown => break,
139201
maybe_override = rx.recv() => match maybe_override {
140202
Some(Some((duration, new_filter))) => {
141203
info!(directives = %new_filter, "Overriding existing log filtering directives for {} seconds...", duration.as_secs());
@@ -232,6 +294,7 @@ mod tests {
232294
base_filter.clone(),
233295
reload_handle.clone(),
234296
rx,
297+
ProcessShutdown::noop(),
235298
));
236299

237300
tx.send(Some((Duration::from_secs(60), EnvFilter::new("hyper=warn"))))
@@ -258,6 +321,7 @@ mod tests {
258321
base_filter.clone(),
259322
reload_handle.clone(),
260323
rx,
324+
ProcessShutdown::noop(),
261325
));
262326

263327
tx.send(Some((Duration::from_millis(100), EnvFilter::new("hyper=warn"))))

lib/saluki-app/src/logging/mod.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use tracing_subscriber::{layer::SubscriberExt as _, reload, util::SubscriberInit
2020

2121
mod api;
2222
use self::api::set_logging_api_handler;
23-
pub use self::api::{acquire_logging_api_handler, LoggingAPIHandler};
23+
pub use self::api::{acquire_logging_api_handler, LoggingAPIHandler, LoggingOverrideWorker};
2424

2525
mod config;
2626
pub use self::config::{LogLevel, LoggingConfiguration};
@@ -76,13 +76,16 @@ impl LoggingGuard {
7676
/// An API handler can be acquired (via [`acquire_logging_api_handler`]) to install the API routes which allow for
7777
/// dynamically controlling the logging level filtering. See [`LoggingAPIHandler`] for more information.
7878
///
79-
/// Returns a [`LoggingGuard`] which must be held until the application is about to shutdown, ensuring that any
80-
/// configured logging backends are able to completely flush any pending logs before the application exits.
79+
/// Returns a [`LoggingGuard`] which must be held until the application is about to shutdown, plus a
80+
/// [`LoggingOverrideWorker`] that must be added to a [`Supervisor`][saluki_core::runtime::Supervisor] to drive
81+
/// the dynamic override processor; without the worker running, override requests are accepted but never applied.
8182
///
8283
/// # Errors
8384
///
8485
/// If the logging subsystem was already initialized, an error will be returned.
85-
pub(crate) async fn initialize_logging(config: LoggingConfiguration) -> Result<LoggingGuard, GenericError> {
86+
pub(crate) async fn initialize_logging(
87+
config: LoggingConfiguration,
88+
) -> Result<(LoggingGuard, LoggingOverrideWorker), GenericError> {
8689
// TODO: Support for logging to syslog.
8790

8891
// Build the initial output stack from the supplied configuration. This is later swappable via
@@ -97,18 +100,22 @@ pub(crate) async fn initialize_logging(config: LoggingConfiguration) -> Result<L
97100
// The base filter is the level the override-restore should land on after a `/logging/override` timeout. It starts
98101
// as the bootstrap level, and is updated by `LoggingGuard::reload` once the Agent's configuration is applied.
99102
let base_filter = Arc::new(Mutex::new(level_filter));
100-
set_logging_api_handler(LoggingAPIHandler::new(base_filter.clone(), filter_handle.clone()));
103+
let (api_handler, override_worker) = LoggingAPIHandler::new(base_filter.clone(), filter_handle.clone());
104+
set_logging_api_handler(api_handler);
101105

102106
tracing_subscriber::registry()
103107
.with(output_layer.with_filter(filter_layer))
104108
.try_init()?;
105109

106-
Ok(LoggingGuard {
107-
worker_guards,
108-
stack_handle,
109-
filter_handle,
110-
base_filter,
111-
})
110+
Ok((
111+
LoggingGuard {
112+
worker_guards,
113+
stack_handle,
114+
filter_handle,
115+
base_filter,
116+
},
117+
override_worker,
118+
))
112119
}
113120

114121
fn build_output_stack(config: &LoggingConfiguration) -> Result<(OutputStack, Vec<WorkerGuard>), GenericError> {

0 commit comments

Comments
 (0)