Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.d/grpc_connection_age_jitter.enhancement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added the `max_connection_age_jitter_factor` gRPC keepalive option to vary the maximum
connection age for each connection and avoid synchronized reconnects.

authors: hiporox
50 changes: 40 additions & 10 deletions src/sources/util/grpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use futures::{FutureExt, StreamExt, future::BoxFuture};
use http::{HeaderMap, Request, Response};
use hyper::{Body, body::HttpBody};
use pin_project::pin_project;
use rand::Rng;
use tokio::{
io::{AsyncRead, AsyncWrite, ReadBuf},
net::TcpStream,
Expand Down Expand Up @@ -68,7 +69,7 @@ pub(crate) mod test_support {

/// Configuration of gRPC server keepalive parameters.
#[configurable_component]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct GrpcKeepaliveConfig {
/// The maximum amount of time a connection may exist before the server closes it.
Expand All @@ -80,6 +81,14 @@ pub struct GrpcKeepaliveConfig {
#[configurable(metadata(docs::human_name = "Maximum Connection Age"))]
pub max_connection_age_secs: Option<u64>,

/// The factor by which to jitter `max_connection_age_secs` for each connection.
///
/// A value of 0.1 gives each connection an age between 90% and 110% of the configured
/// maximum. The default of zero preserves the configured age exactly.
#[serde(default)]
#[configurable(validation(range(min = 0.0, max = 1.0)))]
pub max_connection_age_jitter_factor: f64,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Regenerate generated docs for gRPC jitter option

This adds a user-facing #[configurable_component] option, but the generated source reference files were not updated: checked website/cue/reference/components/sources/generated/vector.cue and website/cue/reference/components/sources/generated/opentelemetry.cue, and both gRPC keepalive option maps still list only max_connection_age_secs and max_connection_age_grace_secs. As a result, the website/schema-based docs will omit max_connection_age_jitter_factor until the generated docs are committed.

AGENTS.md reference: AGENTS.md:L212-L212

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved with added cue files


/// The grace period added to `max_connection_age_secs` before the server closes the connection.
///
/// This setting only applies when `max_connection_age_secs` is set.
Expand All @@ -93,7 +102,11 @@ pub struct GrpcKeepaliveConfig {
impl GrpcKeepaliveConfig {
fn max_connection_lifetime(&self) -> Option<Duration> {
self.max_connection_age_secs.map(|max_connection_age_secs| {
let age = Duration::from_secs(max_connection_age_secs);
let age = jittered_duration(
Duration::from_secs(max_connection_age_secs),
self.max_connection_age_jitter_factor,
rand::rng().random_range(-1.0..=1.0),
);
let grace = self
.max_connection_age_grace_secs
.map(Duration::from_secs)
Expand All @@ -104,6 +117,10 @@ impl GrpcKeepaliveConfig {
}
}

fn jittered_duration(duration: Duration, jitter_factor: f64, jitter: f64) -> Duration {
duration.mul_f64(1.0 + jitter_factor.clamp(0.0, 1.0) * jitter.clamp(-1.0, 1.0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid panicking on oversized jittered max ages

When max_connection_age_secs is configured near u64::MAX and max_connection_age_jitter_factor allows a positive jitter, this multiplication can overflow Duration::mul_f64 and panic while accepting a connection, taking down the gRPC source instead of saturating like the grace addition does just above. Consider capping or using checked/saturating arithmetic before applying jitter.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed this edge case and the under flow edge case resulting in a zero duration connection age

}

struct MaxConnectionAgeIo {
inner: MaybeTlsIncomingStream<TcpStream>,
state: MaxConnectionAgeState,
Expand Down Expand Up @@ -372,10 +389,9 @@ where
let span = Span::current();
let (tx, rx) = tokio::sync::oneshot::channel::<ShutdownSignalToken>();
let listener = tls_settings.bind_reloadable(&address, tls_reloader).await?;
let max_connection_lifetime = keepalive.max_connection_lifetime();
let stream = listener
.accept_stream()
.map(move |stream| stream.map(|io| MaxConnectionAgeIo::new(io, max_connection_lifetime)));
let stream = listener.accept_stream().map(move |stream| {
stream.map(|io| MaxConnectionAgeIo::new(io, keepalive.max_connection_lifetime()))
});

info!(%address, "Building gRPC server.");

Expand Down Expand Up @@ -414,10 +430,9 @@ pub async fn run_grpc_server_with_routes(
let span = Span::current();
let (tx, rx) = tokio::sync::oneshot::channel::<ShutdownSignalToken>();
let listener = tls_settings.bind_reloadable(&address, tls_reloader).await?;
let max_connection_lifetime = keepalive.max_connection_lifetime();
let stream = listener
.accept_stream()
.map(move |stream| stream.map(|io| MaxConnectionAgeIo::new(io, max_connection_lifetime)));
let stream = listener.accept_stream().map(move |stream| {
stream.map(|io| MaxConnectionAgeIo::new(io, keepalive.max_connection_lifetime()))
});

info!(%address, "Building gRPC server.");

Expand Down Expand Up @@ -499,6 +514,21 @@ mod tests {
}
}

#[test]
fn connection_age_jitter_is_bounded() {
let duration = Duration::from_secs(100);

assert_eq!(jittered_duration(duration, 0.0, 1.0), duration);
assert_eq!(
jittered_duration(duration, 0.2, -1.0),
Duration::from_secs(80)
);
assert_eq!(
jittered_duration(duration, 0.2, 1.0),
Duration::from_secs(120)
);
}

#[tokio::test]
async fn max_connection_age_service_tracks_response_body_until_drop() {
let active_requests = Arc::new(AtomicUsize::new(0));
Expand Down
2 changes: 2 additions & 0 deletions src/sources/vector/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,12 +279,14 @@ mod test {

[keepalive]
max_connection_age_secs = 300
max_connection_age_jitter_factor = 0.2
max_connection_age_grace_secs = 30
"#,
)
.unwrap();

assert_eq!(config.keepalive.max_connection_age_secs, Some(300));
assert_eq!(config.keepalive.max_connection_age_jitter_factor, 0.2);
assert_eq!(config.keepalive.max_connection_age_grace_secs, Some(30));
}

Expand Down
Loading