diff --git a/.github/actions/sentinel-cluster/action.yml b/.github/actions/sentinel-cluster/action.yml new file mode 100644 index 00000000000..a12098a286c --- /dev/null +++ b/.github/actions/sentinel-cluster/action.yml @@ -0,0 +1,17 @@ +name: Bring up Redis Sentinel Cluster +description: >- + Installs docker-compose and runs Redis Sentinel cluster + with ports 16379-16380 for redis and + 26379-26380 for sentinel +runs: + using: composite + steps: + - name: Set up Docker Compose + uses: docker/setup-compose-action@364cc21a5de5b1ee4a7f5f9d3fa374ce0ccde746 + + - name: Run Sentinel cluster + env: + ACTION_PATH: ${{ github.action_path }} + shell: bash + run: | + docker compose --file "${ACTION_PATH}/docker-compose.yml" up --detach diff --git a/.github/actions/sentinel-cluster/docker-compose.yml b/.github/actions/sentinel-cluster/docker-compose.yml new file mode 100644 index 00000000000..f31b09f1bab --- /dev/null +++ b/.github/actions/sentinel-cluster/docker-compose.yml @@ -0,0 +1,78 @@ +services: + redis-master: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + container_name: redis-master + command: + - redis-server + - --port + - "16379" + ports: + - "16379:16379" + networks: + - sentinel-cluster + + redis-replica: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + container_name: redis-replica + command: + - redis-server + - --port + - "16380" + - --replicaof + - redis-master + - "16379" + depends_on: + - redis-master + ports: + - "16380:16380" + networks: + - sentinel-cluster + + redis-sentinel-1: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + container_name: redis-sentinel-1 + command: + - sh + - -ec + - | + cat < /sentinel.conf + port 26379 + sentinel monitor redis-sentry redis-master 16379 2 + sentinel down-after-milliseconds redis-sentry 5000 + sentinel parallel-syncs redis-sentry 1 + sentinel failover-timeout redis-sentry 10000 + EOF + exec redis-server /sentinel.conf --sentinel + depends_on: + - redis-master + - redis-replica + ports: + - "26379:26379" + networks: + - sentinel-cluster + + redis-sentinel-2: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + container_name: redis-sentinel-2 + command: + - sh + - -ec + - | + cat < /sentinel.conf + port 26380 + sentinel monitor redis-sentry redis-master 16379 2 + sentinel down-after-milliseconds redis-sentry 5000 + sentinel parallel-syncs redis-sentry 1 + sentinel failover-timeout redis-sentry 10000 + EOF + exec redis-server /sentinel.conf --sentinel + depends_on: + - redis-master + - redis-replica + ports: + - "26380:26380" + networks: + - sentinel-cluster + +networks: + sentinel-cluster: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfd54ce4a12..5c364464162 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -777,6 +777,9 @@ jobs: with: python-version: "3.11" + - name: Start Redis Sentinel cluster + uses: ./.github/actions/sentinel-cluster + - run: make test-integration env: PYTEST_N: 6 diff --git a/Cargo.lock b/Cargo.lock index ccde2392ad0..d29dcee9135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -342,9 +342,9 @@ dependencies = [ [[package]] name = "backon" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "970d91570c01a8a5959b36ad7dd1c30642df24b6b3068710066f6809f7033bb7" +checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" dependencies = [ "fastrand", ] @@ -1003,12 +1003,13 @@ dependencies = [ [[package]] name = "deadpool-redis" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c136f185b3ca9d1f4e4e19c11570e1002f4bfdd592d589053e225716d613851f" +checksum = "667d186d69c8b4dd7f8cf1ac71bec88b312e1752f2d1a63028aa9ea124c3f191" dependencies = [ "deadpool", "redis", + "tokio", ] [[package]] @@ -2279,7 +2280,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -3506,13 +3507,14 @@ dependencies = [ [[package]] name = "redis" -version = "0.29.2" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b110459d6e323b7cda23980c46c77157601199c9da6241552b284cd565a7a133" +checksum = "0bc1ea653e0b2e097db3ebb5b7f678be339620b8041f66b30a308c1d45d36a7f" dependencies = [ "arc-swap", "backon", "bytes", + "cfg-if", "combine", "crc16", "futures-channel", diff --git a/Cargo.toml b/Cargo.toml index 2089721fed4..58f750e7854 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ criterion = "0.5.1" crossbeam-channel = "0.5.15" data-encoding = "2.6.0" deadpool = "0.12.2" -deadpool-redis = "0.20.0" +deadpool-redis = "0.21.1" debugid = "0.8.0" dialoguer = "0.11.0" dynfmt = "0.1.5" @@ -158,7 +158,7 @@ rand = "0.9.1" rand_pcg = "0.9.0" rdkafka = "0.36.2" rdkafka-sys = "4.8.0" -redis = { version = "0.29.2", default-features = false } +redis = { version = "0.31.0", default-features = false } regex = "1.11.1" regex-lite = "0.1.6" reqwest = "0.12.9" diff --git a/devservices/config.yml b/devservices/config.yml index 8cf1f2dec95..eba816072e0 100644 --- a/devservices/config.yml +++ b/devservices/config.yml @@ -9,6 +9,14 @@ x-sentry-service-config: repo_name: sentry-shared-redis branch: main repo_link: https://github.com/getsentry/sentry-shared-redis.git + redis-master: + description: Master instance of redis to be used with sentinel + redis-replica: + description: Replica instance of redis to be used with sentinel + redis-sentinel-1: + description: Instance of redis-sentinel + redis-sentinel-2: + description: Another instance of redis-sentinel kafka: description: Shared instance of kafka used by sentry services remote: @@ -18,8 +26,8 @@ x-sentry-service-config: relay: description: Service that pushes some functionality from the Sentry SDKs as well as the Sentry server into a proxy process. modes: - default: [redis, kafka] - containerized: [redis, kafka, relay] + default: [redis, kafka, redis-sentinel-1, redis-sentinel-2, redis-master, redis-replica] + containerized: [redis, kafka, relay, redis-sentinel-1, redis-sentinel-2, redis-master, redis-replica] x-programs: devserver: @@ -47,6 +55,103 @@ services: labels: - orchestrator=devservices restart: unless-stopped + redis-master: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + healthcheck: + test: redis-cli -p 16379 ping | grep PONG + interval: 5s + timeout: 5s + retries: 3 + command: + - redis-server + - --appendonly + - yes + - --port + - "16379" + ports: + - "16379:16379" + labels: + - orchestrator=devservices + networks: + - devservices + restart: unless-stopped + redis-replica: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + healthcheck: + test: redis-cli -p 16380 ping | grep PONG + interval: 5s + timeout: 5s + retries: 3 + command: + - redis-server + - --appendonly + - yes + - --port + - "16380" + - --replicaof + - redis-master + - "16379" + depends_on: + - redis-master + ports: + - "16380:16380" + labels: + - orchestrator=devservices + networks: + - devservices + restart: unless-stopped + redis-sentinel-1: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + healthcheck: + test: redis-cli -p 26379 ping | grep PONG + interval: 5s + timeout: 5s + retries: 3 + command: + - sh + - -ec + - | + mkdir -p /etc/redis + cp -n /sentinel.bootstrap.conf /etc/redis/sentinel.conf + redis-server /etc/redis/sentinel.conf --sentinel --port 26379 + depends_on: + - redis-master + - redis-replica + ports: + - "26379:26379" + labels: + - orchestrator=devservices + networks: + - devservices + volumes: + - ./config/sentinel.bootstrap.conf:/sentinel.bootstrap.conf:ro + restart: unless-stopped + redis-sentinel-2: + image: ghcr.io/getsentry/image-mirror-library-redis:5.0-alpine + healthcheck: + test: redis-cli -p 26380 ping | grep PONG + interval: 5s + timeout: 5s + retries: 3 + command: + - sh + - -ec + - | + mkdir -p /etc/redis + cp -n /sentinel.bootstrap.conf /etc/redis/sentinel.conf + redis-server /etc/redis/sentinel.conf --sentinel --port 26380 + depends_on: + - redis-master + - redis-replica + ports: + - "26380:26380" + labels: + - orchestrator=devservices + networks: + - devservices + volumes: + - ./config/sentinel.bootstrap.conf:/sentinel.bootstrap.conf:ro + restart: unless-stopped networks: devservices: diff --git a/devservices/config/sentinel.bootstrap.conf b/devservices/config/sentinel.bootstrap.conf new file mode 100644 index 00000000000..5c05d274e86 --- /dev/null +++ b/devservices/config/sentinel.bootstrap.conf @@ -0,0 +1 @@ +sentinel monitor redis-sentry redis-master 16379 2 diff --git a/relay-config/src/redis.rs b/relay-config/src/redis.rs index 6063c613137..d2a1e867314 100644 --- a/relay-config/src/redis.rs +++ b/relay-config/src/redis.rs @@ -92,6 +92,19 @@ enum RedisConfigFromFile { #[serde(flatten)] options: PartialRedisConfigOptions, }, + + /// Connect to a Sentinel cluster. + Sentinel { + // List of redis:// urls of sentinel nodes. + sentinel_nodes: Vec, + + /// Name of a monitored master from sentinel cluster. + master_name: String, + + /// Additional configuration options for the redis client and a connections pool. + #[serde(flatten)] + options: PartialRedisConfigOptions, + }, } /// Redis configuration. @@ -108,6 +121,17 @@ pub enum RedisConfig { }, /// Connect to a single Redis instance. Single(SingleRedisConfig), + + /// Connect to a Sentinel cluster. + Sentinel { + /// List of redis:// urls of sentinel nodes. + sentinel_nodes: Vec, + /// Name of a monitored master from sentinel cluster. + master_name: String, + /// Options of the Redis config. + #[serde(flatten)] + options: PartialRedisConfigOptions, + }, } /// Struct that can serialize a string to a single Redis connection. @@ -155,6 +179,15 @@ impl From for RedisConfig { RedisConfigFromFile::SingleWithOpts { server, options } => { Self::Single(SingleRedisConfig::Detailed { server, options }) } + RedisConfigFromFile::Sentinel { + sentinel_nodes, + master_name, + options, + } => Self::Sentinel { + sentinel_nodes, + master_name, + options, + }, } } } @@ -193,6 +226,15 @@ pub enum RedisConfigRef<'a> { /// Options of the Redis config. options: RedisConfigOptions, }, + /// Connect to a Sentinel cluster. + Sentinel { + /// Reference to the Sentinel nodes urls of the cluster. + sentinel_nodes: &'a [String], + /// Name of a monitored master from sentinel cluster. + master_name: &'a str, + /// Options of the Redis config. + options: RedisConfigOptions, + }, } /// Helper struct bundling connections and options for the various Redis pools. @@ -253,6 +295,15 @@ pub(super) fn build_redis_config( server, options: Default::default(), }, + RedisConfig::Sentinel { + sentinel_nodes, + master_name, + options, + } => RedisConfigRef::Sentinel { + sentinel_nodes, + master_name, + options: build_redis_config_options(options, default_connections), + }, } } @@ -521,6 +572,35 @@ max_connections: 20 ); } + #[test] + fn test_redis_sentinel_nodes_opts() { + let yaml = r#" +sentinel_nodes: + - "redis://127.0.0.1:26379" + - "redis://127.0.0.2:26379" +master_name: sentry-redis +max_connections: 10 +"#; + + let config: RedisConfig = serde_yaml::from_str(yaml) + .expect("Parsed processing redis config: sentinel with options"); + + assert_eq!( + config, + RedisConfig::Sentinel { + sentinel_nodes: vec![ + "redis://127.0.0.1:26379".to_owned(), + "redis://127.0.0.2:26379".to_owned() + ], + master_name: "sentry-redis".to_owned(), + options: PartialRedisConfigOptions { + max_connections: Some(10), + ..Default::default() + }, + } + ); + } + #[test] fn test_redis_cluster_serialize() { let config = RedisConfig::Cluster { @@ -634,4 +714,34 @@ max_connections: 20 } "###); } + + #[test] + fn test_redis_sentinel_serialize() { + let config = RedisConfig::Sentinel { + sentinel_nodes: vec![ + "redis://127.0.0.1:26379".to_owned(), + "redis://127.0.0.2:26379".to_owned(), + ], + master_name: "sentry-redis".to_owned(), + options: PartialRedisConfigOptions { + max_connections: Some(42), + ..Default::default() + }, + }; + + assert_json_snapshot!(config, @r###" + { + "sentinel_nodes": [ + "redis://127.0.0.1:26379", + "redis://127.0.0.2:26379" + ], + "master_name": "sentry-redis", + "max_connections": 42, + "idle_timeout": 60, + "create_timeout": 3, + "recycle_timeout": 2, + "recycle_check_frequency": 100 + } + "###); + } } diff --git a/relay-redis/Cargo.toml b/relay-redis/Cargo.toml index d0206738959..f5638e03b57 100644 --- a/relay-redis/Cargo.toml +++ b/relay-redis/Cargo.toml @@ -14,13 +14,14 @@ workspace = true [dependencies] deadpool = { workspace = true, optional = true } -deadpool-redis = { workspace = true, optional = true, features = ["rt_tokio_1", "cluster", "script"] } +deadpool-redis = { workspace = true, optional = true, features = ["rt_tokio_1", "cluster", "sentinel", "script"] } futures = { workspace = true } redis = { workspace = true, optional = true, features = [ "cluster", "r2d2", "tls-native-tls", "keep-alive", + "sentinel", "script", "tokio-native-tls-comp", "cluster-async", diff --git a/relay-redis/src/pool.rs b/relay-redis/src/pool.rs index ade329cede7..fa4545d8a80 100644 --- a/relay-redis/src/pool.rs +++ b/relay-redis/src/pool.rs @@ -1,6 +1,7 @@ use deadpool::managed::{Manager, Metrics, Object, Pool, RecycleError, RecycleResult}; use deadpool_redis::Manager as SingleManager; use deadpool_redis::cluster::Manager as ClusterManager; +use deadpool_redis::sentinel::{Manager as SentinelManager, SentinelServerType}; use futures::FutureExt; use std::ops::{Deref, DerefMut}; @@ -17,6 +18,9 @@ pub type CustomClusterPool = Pool /// A connection pool for single Redis instance deployments. pub type CustomSinglePool = Pool; +/// A connection pool for single Redis sentinel deployments. +pub type CustomSentinelPool = Pool; + /// A wrapper for a connection that can be tracked with metadata. /// /// A connection is considered detached as soon as it is marked as detached and it can't be @@ -280,3 +284,93 @@ impl From> for CustomSingleConnection { Self(conn) } } + +/// Managed connection to a Redis-master instance +/// +/// This manager handles the creation and recycling of Redis connections, +/// ensuring proper connection health through periodic PING checks. It supports +/// multiplexed connections for efficient handling of multiple operations. +pub struct CustomSentinelConnection(Object); + +impl redis::aio::ConnectionLike for CustomSentinelConnection { + fn req_packed_command<'a>(&'a mut self, cmd: &'a Cmd) -> RedisFuture<'a, Value> { + self.0.req_packed_command(cmd) + } + + fn req_packed_commands<'a>( + &'a mut self, + cmd: &'a Pipeline, + offset: usize, + count: usize, + ) -> RedisFuture<'a, Vec> { + self.0.req_packed_commands(cmd, offset, count) + } + + fn get_db(&self) -> i64 { + self.0.get_db() + } +} + +/// Manages Redis-master connections and their lifecycle. +/// +/// This manager handles the creation and recycling of Redis connections, +/// ensuring proper connection health through periodic PING checks. It supports +/// multiplexed connections for efficient handling of multiple operations. +pub struct CustomSentinelManager { + manager: SentinelManager, + recycle_check_frequency: usize, +} + +impl CustomSentinelManager { + /// Creates a new Sentinel manager for the specified Sentinel nodes. + /// + /// The manager will attempt to connect to each node in the provided list and + /// maintain connections to the Redis cluster. + pub fn new( + params: Vec, + master_name: String, + recycle_check_frequency: usize, + ) -> RedisResult { + Ok(Self { + manager: SentinelManager::new(params, master_name, None, SentinelServerType::Master)?, + recycle_check_frequency, + }) + } +} + +impl Manager for CustomSentinelManager { + type Type = TrackedConnection; + type Error = RedisError; + + async fn create(&self) -> Result, RedisError> { + self.manager.create().await.map(TrackedConnection::from) + } + + async fn recycle( + &self, + conn: &mut TrackedConnection, + metrics: &Metrics, + ) -> RecycleResult { + // If the connection is marked to be detached, we return and error, signaling that this + // connection must be detached from the pool. + if conn.detach { + return Err(RecycleError::Message( + "the tracked connection was marked as detached".into(), + )); + } + + // If the interval has been reached, we optimistically assume the connection is active + // without doing an actual `PING`. + if metrics.recycle_count % self.recycle_check_frequency != 0 { + return Ok(()); + } + + self.manager.recycle(conn, metrics).await + } +} + +impl From> for CustomSentinelConnection { + fn from(conn: Object) -> Self { + Self(conn) + } +} diff --git a/relay-redis/src/real.rs b/relay-redis/src/real.rs index 8cf77604123..9c1aa5f6497 100644 --- a/relay-redis/src/real.rs +++ b/relay-redis/src/real.rs @@ -79,6 +79,8 @@ pub enum AsyncRedisClient { Cluster(pool::CustomClusterPool), /// Contains a connection pool to a single Redis instance. Single(pool::CustomSinglePool), + /// Contains a connection pool to a Redis-master instance. + Sentinel(pool::CustomSentinelPool), } impl AsyncRedisClient { @@ -126,6 +128,37 @@ impl AsyncRedisClient { Ok(AsyncRedisClient::Single(pool)) } + /// Creates a new connection client for a Redis-master instance. + /// + /// This method initializes a connection client that can communicate with multiple Sentinel nodes + /// to retrieve connection to a current Redis-master instance. + /// The client is configured with the specified sentinel servers, master name and options. + /// + /// The client uses a custom Sentinel manager that implements a specific connection recycling + /// strategy, ensuring optimal performance and reliability in single-instance environments. + pub fn sentinel<'a>( + sentinels: impl IntoIterator, + master_name: &str, + opts: &RedisConfigOptions, + ) -> Result { + let sentinels = sentinels + .into_iter() + .map(|s| s.to_owned()) + .collect::>(); + // We use our custom single manager which performs recycling in a different way from the + // default manager. + let manager = pool::CustomSentinelManager::new( + sentinels, + master_name.to_owned(), + opts.recycle_check_frequency, + ) + .map_err(RedisError::Redis)?; + + let pool = Self::build_pool(manager, opts)?; + + Ok(AsyncRedisClient::Sentinel(pool)) + } + /// Acquires a connection from the pool. /// /// Returns a new [`AsyncRedisConnection`] that can be used to execute Redis commands. @@ -138,6 +171,9 @@ impl AsyncRedisClient { Self::Single(pool) => { AsyncRedisConnection::Single(pool.get().await.map_err(RedisError::Pool)?) } + Self::Sentinel(pool) => { + AsyncRedisConnection::Sentinel(pool.get().await.map_err(RedisError::Pool)?) + } }; Ok(connection) @@ -151,6 +187,7 @@ impl AsyncRedisClient { let status = match self { Self::Cluster(pool) => pool.status(), Self::Single(pool) => pool.status(), + Self::Sentinel(pool) => pool.status(), }; RedisClientStats { @@ -172,6 +209,9 @@ impl AsyncRedisClient { Self::Single(pool) => { pool.retain(|_, metrics| predicate(metrics)); } + Self::Sentinel(pool) => { + pool.retain(|_, metrics| predicate(metrics)); + } } } @@ -210,6 +250,7 @@ impl std::fmt::Debug for AsyncRedisClient { match self { AsyncRedisClient::Cluster(_) => write!(f, "AsyncRedisPool::Cluster"), AsyncRedisClient::Single(_) => write!(f, "AsyncRedisPool::Single"), + AsyncRedisClient::Sentinel(_) => write!(f, "AsyncRedisPool::Sentinel"), } } } @@ -225,6 +266,8 @@ pub enum AsyncRedisConnection { Cluster(pool::CustomClusterConnection), /// A connection to a single Redis instance. Single(pool::CustomSingleConnection), + /// A connection to a single Redis-master instance + Sentinel(pool::CustomSentinelConnection), } impl std::fmt::Debug for AsyncRedisConnection { @@ -232,6 +275,7 @@ impl std::fmt::Debug for AsyncRedisConnection { let name = match self { Self::Cluster(_) => "Cluster", Self::Single(_) => "Single", + Self::Sentinel(_) => "Sentinel", }; f.debug_tuple(name).finish() } @@ -242,6 +286,7 @@ impl redis::aio::ConnectionLike for AsyncRedisConnection { match self { Self::Cluster(conn) => conn.req_packed_command(cmd), Self::Single(conn) => conn.req_packed_command(cmd), + Self::Sentinel(conn) => conn.req_packed_command(cmd), } } @@ -254,6 +299,7 @@ impl redis::aio::ConnectionLike for AsyncRedisConnection { match self { Self::Cluster(conn) => conn.req_packed_commands(cmd, offset, count), Self::Single(conn) => conn.req_packed_commands(cmd, offset, count), + Self::Sentinel(conn) => conn.req_packed_commands(cmd, offset, count), } } @@ -261,6 +307,7 @@ impl redis::aio::ConnectionLike for AsyncRedisConnection { match self { Self::Cluster(conn) => conn.get_db(), Self::Single(conn) => conn.get_db(), + Self::Sentinel(conn) => conn.get_db(), } } } diff --git a/relay-server/src/service.rs b/relay-server/src/service.rs index 6b0a4ee36fa..a7d5555cff3 100644 --- a/relay-server/src/service.rs +++ b/relay-server/src/service.rs @@ -460,6 +460,15 @@ fn create_async_redis_client(config: &RedisConfigRef<'_>) -> Result AsyncRedisClient::cluster(cluster_nodes.iter().map(|s| s.as_str()), options), RedisConfigRef::Single { server, options } => AsyncRedisClient::single(server, options), + RedisConfigRef::Sentinel { + sentinel_nodes, + master_name, + options, + } => AsyncRedisClient::sentinel( + sentinel_nodes.iter().map(|s| s.as_str()), + master_name, + options, + ), } } diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 283a7ad9061..b3321c7727b 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -263,3 +263,9 @@ def redis_client(): @pytest.fixture def secondary_redis_client(): return redis.Redis(host="127.0.0.1", port=6380, db=0) + + +@pytest.fixture +def redis_sentinel_client(): + sentinel = redis.sentinel.Sentinel([("127.0.0.1", 26379), ("127.0.0.1", 26380)]) + return sentinel.discover_master("redis-sentry") diff --git a/tests/integration/test_redis_sentinel.py b/tests/integration/test_redis_sentinel.py new file mode 100644 index 00000000000..9a29039e323 --- /dev/null +++ b/tests/integration/test_redis_sentinel.py @@ -0,0 +1,41 @@ +from sentry_sdk.envelope import Envelope + +SENTINEL_CONFIG = { + "processing": { + "redis": { + "sentinel_nodes": [ + "redis://127.0.0.1:26379", + "redis://127.0.0.1:26380", + ], + "master_name": "redis-sentry", + } + } +} + + +def test_redis_sentinel(redis_sentinel_client): + assert redis_sentinel_client, "Failed to discover redis master" + + +def test_startup_with_redis_sentinel(relay_with_processing): + upstream = relay_with_processing(SENTINEL_CONFIG) + assert upstream.wait_relay_health_check() is None, "Failed to start Relay" + + +def test_envelope_with_redis_sentinel(mini_sentry, relay_with_processing): + project_id = 42 + mini_sentry.add_basic_project_config(project_id) + + envelope = Envelope() + envelope.add_event({"message": "Hello, World!"}) + + upstream = relay_with_processing(SENTINEL_CONFIG) + response = upstream.send_envelope( + project_id, + envelope, + ) + + assert response, "Failed to send event" + + event = mini_sentry.captured_events.get(timeout=1).get_event() + assert event["logentry"] == {"formatted": "Hello, World!"}