-
-
Notifications
You must be signed in to change notification settings - Fork 146
feat(etl): add HeartbeatWorker for read replica replication slot support #562
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
Open
JohnCari
wants to merge
12
commits into
supabase:main
Choose a base branch
from
JohnCari:feat/heartbeat-worker-merged
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
d3371e7
feat(etl): add HeartbeatWorker for read replica replication slot support
JohnCari 504bf7d
feat(etl): add heartbeat fields to pipeline config and metrics
JohnCari 8b8b5a5
feat(etl): add HeartbeatWorkerPanic error kind
JohnCari d3dd84f
fix: restore complete error.rs with HeartbeatWorkerPanic
JohnCari 14a2cab
feat(etl): add primary_connection and heartbeat fields to PipelineConfig
JohnCari 0c67938
feat(etl): add connect_regular() and connect_with_tls() for heartbeat…
JohnCari adb3828
fix: address review feedback for HeartbeatWorker
JohnCari 71d5fdb
fix: improve heartbeat options spacing and use rand for jitter
JohnCari dd687e3
feat(etl): add pipeline integration for HeartbeatWorker
JohnCari 7fbad38
feat(etl): update examples and benchmarks for heartbeat config fields
JohnCari 9090086
fix: address CodeRabbit feedback on heartbeat worker handling
JohnCari ca5600c
docs: add inline comments for replica-mode config fields
JohnCari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| //! Heartbeat configuration for read replica replication slot maintenance. | ||
|
|
||
| use crate::shared::ValidationError; | ||
| use serde::{Deserialize, Serialize}; | ||
| #[cfg(feature = "utoipa")] | ||
| use utoipa::ToSchema; | ||
|
|
||
| /// Configuration for the heartbeat worker that maintains replication slot activity. | ||
| /// | ||
| /// When replicating from a read replica, the replication slot on the primary can | ||
| /// become inactive during idle periods. The heartbeat worker periodically emits | ||
| /// WAL messages to keep the slot active. | ||
| #[derive(Clone, Debug, Deserialize, Serialize)] | ||
| #[cfg_attr(feature = "utoipa", derive(ToSchema))] | ||
| pub struct HeartbeatConfig { | ||
| /// Interval in milliseconds between heartbeat emissions. | ||
| /// | ||
| /// Default: 30000 (30 seconds) | ||
| #[serde(default = "default_interval_ms")] | ||
| pub interval_ms: u64, | ||
|
|
||
| /// Minimum backoff duration in milliseconds after a failed heartbeat attempt. | ||
| /// | ||
| /// Default: 1000 (1 second) | ||
| #[serde(default = "default_min_backoff_ms")] | ||
| pub min_backoff_ms: u64, | ||
|
|
||
| /// Maximum backoff duration in milliseconds after repeated failures. | ||
| /// | ||
| /// Default: 60000 (60 seconds) | ||
| #[serde(default = "default_max_backoff_ms")] | ||
| pub max_backoff_ms: u64, | ||
|
|
||
| /// Jitter percentage to apply to backoff duration (0-100). | ||
| /// | ||
| /// Helps prevent thundering herd when multiple workers reconnect. | ||
| /// Default: 25 | ||
| #[serde(default = "default_jitter_percent")] | ||
| pub jitter_percent: u8, | ||
| } | ||
|
|
||
| impl HeartbeatConfig { | ||
| /// Default heartbeat interval: 30 seconds. | ||
| pub const DEFAULT_INTERVAL_MS: u64 = 30_000; | ||
|
|
||
| /// Default minimum backoff: 1 second. | ||
| pub const DEFAULT_MIN_BACKOFF_MS: u64 = 1_000; | ||
|
|
||
| /// Default maximum backoff: 60 seconds. | ||
| pub const DEFAULT_MAX_BACKOFF_MS: u64 = 60_000; | ||
|
|
||
| /// Default jitter percentage: 25%. | ||
| pub const DEFAULT_JITTER_PERCENT: u8 = 25; | ||
|
|
||
| /// Validates the heartbeat configuration. | ||
| /// | ||
| /// Ensures interval_ms > 0, jitter_percent <= 100, min_backoff_ms > 0, | ||
| /// and min_backoff_ms <= max_backoff_ms. | ||
| pub fn validate(&self) -> Result<(), ValidationError> { | ||
| if self.interval_ms == 0 { | ||
| return Err(ValidationError::InvalidFieldValue { | ||
| field: "interval_ms".to_string(), | ||
| constraint: "must be greater than 0".to_string(), | ||
| }); | ||
| } | ||
|
|
||
| if self.jitter_percent > 100 { | ||
| return Err(ValidationError::InvalidFieldValue { | ||
| field: "jitter_percent".to_string(), | ||
| constraint: "must be <= 100".to_string(), | ||
| }); | ||
| } | ||
|
|
||
| if self.min_backoff_ms == 0 { | ||
| return Err(ValidationError::InvalidFieldValue { | ||
| field: "min_backoff_ms".to_string(), | ||
| constraint: "must be greater than 0".to_string(), | ||
| }); | ||
| } | ||
|
|
||
| if self.min_backoff_ms > self.max_backoff_ms { | ||
| return Err(ValidationError::InvalidFieldValue { | ||
| field: "min_backoff_ms".to_string(), | ||
| constraint: "must be <= max_backoff_ms".to_string(), | ||
| }); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
|
|
||
| impl Default for HeartbeatConfig { | ||
| fn default() -> Self { | ||
| Self { | ||
| interval_ms: Self::DEFAULT_INTERVAL_MS, | ||
| min_backoff_ms: Self::DEFAULT_MIN_BACKOFF_MS, | ||
| max_backoff_ms: Self::DEFAULT_MAX_BACKOFF_MS, | ||
| jitter_percent: Self::DEFAULT_JITTER_PERCENT, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn default_interval_ms() -> u64 { | ||
| HeartbeatConfig::DEFAULT_INTERVAL_MS | ||
| } | ||
|
|
||
| fn default_min_backoff_ms() -> u64 { | ||
| HeartbeatConfig::DEFAULT_MIN_BACKOFF_MS | ||
| } | ||
|
|
||
| fn default_max_backoff_ms() -> u64 { | ||
| HeartbeatConfig::DEFAULT_MAX_BACKOFF_MS | ||
| } | ||
|
|
||
| fn default_jitter_percent() -> u8 { | ||
| HeartbeatConfig::DEFAULT_JITTER_PERCENT | ||
| } | ||
|
|
||
| /// Connection options optimized for heartbeat connections. | ||
| /// | ||
| /// Uses shorter timeouts since heartbeat connections are lightweight | ||
| /// health checks that should fail fast. | ||
| pub const ETL_HEARTBEAT_OPTIONS: &str = concat!( | ||
| "application_name=etl_heartbeat", | ||
| " ", "statement_timeout=5000", | ||
| " ", "lock_timeout=5000", | ||
| " ", "idle_in_transaction_session_timeout=30000", | ||
| ); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn test_default_config() { | ||
| let config = HeartbeatConfig::default(); | ||
| assert_eq!(config.interval_ms, 30_000); | ||
| assert_eq!(config.min_backoff_ms, 1_000); | ||
| assert_eq!(config.max_backoff_ms, 60_000); | ||
| assert_eq!(config.jitter_percent, 25); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_heartbeat_options() { | ||
| assert!(ETL_HEARTBEAT_OPTIONS.contains("application_name=etl_heartbeat")); | ||
| assert!(ETL_HEARTBEAT_OPTIONS.contains("statement_timeout=5000")); | ||
| // Verify options are properly space-separated | ||
| assert!(ETL_HEARTBEAT_OPTIONS.contains(" statement_timeout=")); | ||
| assert!(ETL_HEARTBEAT_OPTIONS.contains(" lock_timeout=")); | ||
| assert!(ETL_HEARTBEAT_OPTIONS.contains(" idle_in_transaction_session_timeout=")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_valid_config() { | ||
| let config = HeartbeatConfig::default(); | ||
| assert!(config.validate().is_ok()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_zero_interval() { | ||
| let config = HeartbeatConfig { | ||
| interval_ms: 0, | ||
| ..Default::default() | ||
| }; | ||
| assert!(config.validate().is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_jitter_too_high() { | ||
| let config = HeartbeatConfig { | ||
| jitter_percent: 101, | ||
| ..Default::default() | ||
| }; | ||
| assert!(config.validate().is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_min_greater_than_max() { | ||
| let config = HeartbeatConfig { | ||
| min_backoff_ms: 10_000, | ||
| max_backoff_ms: 1_000, | ||
| ..Default::default() | ||
| }; | ||
| assert!(config.validate().is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_validate_zero_min_backoff() { | ||
| let config = HeartbeatConfig { | ||
| min_backoff_ms: 0, | ||
| ..Default::default() | ||
| }; | ||
| assert!(config.validate().is_err()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.