Skip to content

feat(redis source): add reconnecting pubsub session for channel data_type#25892

Open
artusmode wants to merge 7 commits into
vectordotdev:masterfrom
artusmode:redis-channel-reconnect-finish
Open

feat(redis source): add reconnecting pubsub session for channel data_type#25892
artusmode wants to merge 7 commits into
vectordotdev:masterfrom
artusmode:redis-channel-reconnect-finish

Conversation

@artusmode

Copy link
Copy Markdown

Supersedes #24100 (rebased onto master; original work by @gibranbadrul, preserved as the first commit). Closes #22615.

The redis source with data_type = "channel" currently stops receiving after any Redis connection drop and only recovers on a Vector restart. This adds a reconnect loop that re-connects and re-subscribes automatically, with exponential backoff.

Addresses the review feedback on #24100:

  • Uses the shared common::backoff::ExponentialBackoff (from_millis(2).factor(250), capped at 30s), matching aws_s3/sqs, instead of a bespoke helper. Backoff resets on a successful connect.
  • Emits reconnection internal events: RedisConnectionErrorcomponent_errors_total (error_type=connection_failed) on connect/subscribe failure, and RedisConnectionEstablishedconnection_established_total on initial connect and recovery.
  • Adds a changelog fragment.

Also unifies the list data_type source onto the same shared ExponentialBackoff (removing its duplicate backoff_exponential) and makes its retry sleep shutdown-aware.

gibranbadrul and others added 2 commits July 18, 2026 21:54
…_type`

- Rework channel source to maintain a pubsub session that auto-reconnects
  and re-subscribes after Redis disconnects.
- Add shutdown-aware backoff and best-effort unsubscribe on exit.
- Log when the connection is re-established ("re-established and resubscribed").

Resolves vectordotdev#22615
Follow-up to the reconnecting pub/sub work:

- Replace the ad-hoc `backoff_exponential` helper with the shared
  `common::backoff::ExponentialBackoff` (from_millis(2).factor(250),
  capped at 30s), matching the strategy used by other reconnecting
  sources such as `aws_s3`/`sqs`. Backoff resets on a successful connect.
- Emit internal events for reconnection so operators can observe/alert:
  `RedisConnectionError` (increments `component_errors_total` with
  error_type=connection_failed) on connect/subscribe failure, and
  `RedisConnectionEstablished` (increments `connection_established_total`)
  on initial connect and recovery.
- Unify the `list` data_type source (`list.rs`) onto the same shared
  `ExponentialBackoff`, removing its duplicate `backoff_exponential`
  helper and making its retry sleep shutdown-aware.
- Add a changelog fragment.

Co-authored-by: Gibran <gibran.zaman@finaccel.co>
@artusmode
artusmode requested a review from a team as a code owner July 19, 2026 01:58
@github-actions github-actions Bot added the domain: sources Anything related to the Vector's sources label Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@datadog-vectordotdev

This comment has been minimized.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa85aeb2f1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sources/redis/channel.rs Outdated
loop {
// connect + SUBSCRIBE
let mut pubsub_conn =
match connect_and_subscribe(&client, &endpoint, &channel).await {

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 Race Redis reconnect attempts with shutdown

When the channel source is in the reconnect loop and a Redis connect/subscribe attempt stalls, for example against a black-holed TCP/TLS endpoint, this await is not raced with shutdown. A shutdown signal that arrives after the backoff completes or during the connect will not be observed until the Redis client/OS connect attempt returns, so the source misses graceful shutdown and Vector waits for the force-shutdown deadline. Please wrap connect_and_subscribe in a tokio::select! with the shutdown signal and/or apply a per-attempt timeout.

Useful? React with 👍 / 👎.

Comment thread src/sources/redis/list.rs Outdated
// `aws_s3`/`sqs`. Reset once a value is successfully received.
let mut backoff = ExponentialBackoff::from_millis(2)
.factor(250)
.max_delay(Duration::from_secs(30));

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 Keep Redis list retries capped at the previous 1s

For existing data_type = "list" configurations, this reused backoff now grows to 30 seconds after repeated Redis I/O errors. During a Redis outage the list source can reach that cap, and once Redis is back it may still wait up to 30 seconds before the next BLPOP/BRPOP; the removed helper capped retries at 1 second, so this unrelated list change is a significant recovery regression. Keep the list source's prior cap or avoid changing its retry policy while adding channel reconnects.

Useful? React with 👍 / 👎.

Comment thread src/sources/redis/channel.rs Outdated
match end_reason {
SessionEnd::Shutdown | SessionEnd::DownstreamClosed => {
// shutting down cleanly, or downstream closed: stop for good.
let _ = pubsub_conn.unsubscribe(&channel).await;

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 awaiting Redis unsubscribe during shutdown

When shutdown is selected while the Pub/Sub session is still connected, this branch now waits for Redis to acknowledge UNSUBSCRIBE before the source can exit. If Redis is slow or the TCP connection is half-open during shutdown, graceful shutdown can block until the force-shutdown deadline; the previous take_until(shutdown) path stopped immediately by dropping the connection. Since the component is already stopping, avoid a network round trip here or race it with the shutdown/force path.

Useful? React with 👍 / 👎.

- Race `connect_and_subscribe` with the shutdown signal (biased select) so a
  connect against a black-holed endpoint can't block graceful shutdown until
  the force-shutdown deadline.
- Drop the explicit `UNSUBSCRIBE` round trips on the shutdown/disconnect paths;
  dropping the pubsub connection closes it and Redis releases the subscription
  automatically, so shutdown no longer waits on a network round trip.
- Keep the `list` source's retry backoff capped at 1s (its prior behavior)
  rather than the channel source's 30s cap, avoiding a recovery-latency
  regression for existing `data_type = "list"` users.
@artusmode
artusmode force-pushed the redis-channel-reconnect-finish branch from 9f6f014 to ac72174 Compare July 19, 2026 11:18
@artusmode

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac72174894

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sources/redis/channel.rs Outdated
};

// Connected: reset backoff so the next outage starts from the shortest delay.
backoff.reset();

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 Preserve backoff across short-lived Pub/Sub sessions

When Redis or an intervening load balancer accepts SUBSCRIBE and then immediately closes the connection, this reset makes every SessionEnd::Disconnected retry use the first 500 ms delay again, so the reconnect loop never backs off exponentially during a flapping outage. Keep the backoff advanced until the session has stayed healthy long enough or has actually delivered data; otherwise the source can produce an endless fast reconnect/log loop despite the intended capped exponential backoff.

Useful? React with 👍 / 👎.

Comment thread src/sources/redis/channel.rs Outdated
Comment on lines +186 to +189
Err(err) => {
// failed to connect or SUBSCRIBE
emit!(RedisConnectionError::from(err.into_source()));
reconnecting = true;

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 Fail fast on permanent Redis subscription errors

For channel sources with a permanent Redis error such as bad credentials or ACLs that reject SUBSCRIBE, this branch now logs and retries forever inside the source future instead of returning the Connection/Subscribe build error as before. That makes an invalid configuration appear to start successfully and only surface as repeated runtime errors; keep transient reconnects, but preserve fail-fast behavior for non-recoverable connect/subscribe errors.

Useful? React with 👍 / 👎.

Second round of automated review feedback:

- Do the initial connect + SUBSCRIBE during source build and fail fast on
  non-recoverable errors (bad auth/ACL/config) by returning the error, instead
  of entering the reconnect loop. An invalid configuration now surfaces at
  startup rather than looking like it started and only erroring at runtime.
  Transient (I/O) errors are still tolerated and handled by the reconnect loop.
- Only reset the reconnect backoff after a session has actually delivered a
  message, not merely on a successful connect. A connection that is accepted and
  then immediately dropped (a flapping outage) now keeps backing off
  exponentially instead of hot-looping at the 500ms floor.
- Classify permanent vs transient connect errors during reconnect too: a
  non-recoverable error stops the source rather than retrying forever.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 780538f5c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sources/redis/channel.rs Outdated
/// `SUBSCRIBE`) is treated as permanent, so an invalid configuration fails fast instead of
/// silently retrying forever.
fn is_transient(error: &redis::RedisError) -> bool {
error.kind() == redis::ErrorKind::IoError

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 Treat Redis loading errors as transient

When Redis is coming back from a restart and accepts TCP connections but replies to SUBSCRIBE with a server-side retryable error such as LOADING/BUSYLOADING or TRYAGAIN, redis-rs reports that as a non-I/O Redis error. Because this helper only treats IoError as transient, the reconnect path will return Err(()) (or fail the source build) instead of backing off and retrying, so data_type = "channel" can still stop permanently during the Redis-restart scenario this change is meant to recover from. Please classify retryable Redis server states as transient while keeping auth/ACL errors fatal.

Useful? React with 👍 / 👎.

Use redis-rs's own `RetryMethod` classification instead of only treating
`ErrorKind::IoError` as transient. A Redis node coming back from a restart may
accept the TCP connection but reply to `SUBSCRIBE` with a server-side retryable
error such as `LOADING`/`BUSYLOADING`/`TRYAGAIN`, which redis-rs reports as a
non-I/O error. The previous check treated those as permanent and stopped the
source, defeating the restart recovery this change is meant to provide. Now only
`RetryMethod::NoRetry` (auth/ACL/config rejections) is fatal; all other errors,
including those retryable server states, back off and reconnect.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48254413e6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sources/redis/channel.rs Outdated
Comment on lines +41 to +42
fn is_transient(error: &redis::RedisError) -> bool {
!matches!(error.retry_method(), redis::RetryMethod::NoRetry)

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 Fail fast on Redis auth failures

Fresh evidence for the earlier fail-fast concern: this new RetryMethod-based classification still treats bad Redis credentials as transient because redis-rs 0.32.4 maps ErrorKind::AuthenticationFailed to RetryMethod::Reconnect, not NoRetry. With an invalid password, the initial get_async_pubsub() failure is accepted as retryable, build() returns Ok, and the source retries forever instead of surfacing the configuration error; handle AuthenticationFailed explicitly as permanent before falling back to retry_method().

Useful? React with 👍 / 👎.

Comment thread src/sources/redis/channel.rs Outdated
Comment on lines +142 to +146
// A message was delivered downstream, so the connection is
// healthy: reset the reconnect backoff. Resetting only here
// (not on connect) means a connection that drops before
// delivering anything keeps backing off exponentially.
backoff.reset();

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 Reset Pub/Sub backoff for quiet healthy sessions

With this reset only happening after a message is delivered downstream, a low-volume channel can reconnect successfully and remain healthy for hours without ever resetting a backoff that was previously driven to the 30s cap. If Redis drops again during that quiet period, the source still waits up to 30s before its first resubscribe attempt, leaving the Pub/Sub source unsubscribed and losing messages despite the prior session being stable; reset after a minimum healthy session duration (or another connection-health signal) rather than only on delivered data.

Useful? React with 👍 / 👎.

… sessions

Third round of automated review feedback:

- redis-rs maps `ErrorKind::AuthenticationFailed` to `RetryMethod::Reconnect`,
  so the `RetryMethod`-based classifier treated bad credentials as transient and
  retried forever. Treat `AuthenticationFailed` as permanent explicitly so an
  invalid password/ACL fails the source build as intended.
- Reset the reconnect backoff after a session has either delivered a message or
  stayed connected for a minimum healthy duration, rather than only on delivered
  data. A low-volume channel that stays healthy but quiet now clears a backoff
  that a previous flapping period drove up to the cap, so a later drop resubscribes
  promptly instead of waiting up to 30s.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de4b3cba8d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/sources/redis/channel.rs Outdated
if error.kind() == redis::ErrorKind::AuthenticationFailed {
return false;
}
!matches!(error.retry_method(), redis::RetryMethod::NoRetry)

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 Fail fast on permanent TLS connection errors

For rediss:// configs with an invalid certificate or TLS hostname, redis-rs reports the handshake failure as ErrorKind::IoError with retry_method() returning RetryImmediately, so this predicate treats it as transient. That makes a permanent TLS misconfiguration build successfully and retry forever instead of surfacing the config error like the previous build-time connect did; classify TLS/handshake failures as permanent before falling back to retry_method() or switch to an allowlist of known transient Redis/socket errors.

Useful? React with 👍 / 👎.

Comment thread src/sources/redis/channel.rs Outdated
Comment on lines +188 to +190
warn!(
endpoint,
channel, "Redis pubsub stream ended; will reconnect."

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 Emit an error metric when Pub/Sub disconnects

When Redis closes an established Pub/Sub connection and the next reconnect succeeds on the first attempt, this branch only logs a warning and returns Disconnected; no component_errors_total is emitted because RedisConnectionError is only used for connect/subscribe failures. In that common Redis-restart/load-balancer-close scenario, Vector is unsubscribed and can lose Pub/Sub messages, but metric-based alerts see no component error. Emit a disconnect internal event before returning so successful reconnects still record the failure.

Useful? React with 👍 / 👎.

Replace the error-classification approach with the source's original build-time
semantics, which is both simpler and more robust:

- The initial connect + SUBSCRIBE fails fast on ANY error (auth, TLS, ACL,
  unreachable, LOADING), surfacing misconfiguration at startup as the source did
  before reconnect support. This avoids fragile transient-vs-permanent
  classification: redis-rs reports TLS handshake failures and transient network
  timeouts both as IoError/RetryImmediately, so no kind/retry_method predicate can
  separate them. Removing `is_transient` also drops the auth special-case.
- Once the source is running, every reconnect failure is retried; a permanent
  problem was already ruled out by the successful build-time connect. Failures are
  still recorded via RedisConnectionError for alerting.
- Emit a new RedisConnectionDropped internal event (component_errors_total) when an
  established connection drops, so alerts fire even when the reconnect succeeds on
  the first attempt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: sources Anything related to the Vector's sources

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Redis doesn't resubscribe/reconnect to channel

2 participants