{"record":{"id":"647659e7ce0b2d01","repo":"FuelLabs/fuel-core","slug":"at-least-one-redis-url-is-required-for-leader-lock","errorCode":null,"errorMessage":"At least one redis url is required for leader lock","messagePattern":"At least one redis url is required for leader lock","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/fuel-core/src/service/adapters/consensus_module/poa.rs","lineNumber":205,"sourceCode":"        lease_key: String,\n        lease_ttl: Duration,\n        node_timeout: Duration,\n        retry_delay: Duration,\n        max_retry_delay_offset: Duration,\n        max_attempts: u32,\n        stream_max_len: u32,\n    ) -> anyhow::Result<Self> {\n        let redis_nodes = redis_urls\n            .into_iter()\n            .map(|redis_url| {\n                redis::Client::open(redis_url).map(|redis_client| RedisNode {\n                    redis_client,\n                    cached_connection: Mutex::new(None),\n                })\n            })\n            .collect::<Result<Vec<_>, _>>()?;\n        if redis_nodes.is_empty() {\n            return Err(anyhow!(\n                \"At least one redis url is required for leader lock\"\n            ));\n        }\n        let quorum_disruption_budget = 0u32;\n        let quorum = Self::calculate_quorum(redis_nodes.len(), quorum_disruption_budget);\n        let lease_ttl_millis = u64::try_from(lease_ttl.as_millis())?;\n        let retry_delay_millis = u64::try_from(retry_delay.as_millis())?;\n        let max_retry_delay_offset_millis =\n            u64::try_from(max_retry_delay_offset.as_millis())?;\n        let max_attempts = usize::try_from(max_attempts)?.max(1);\n        let lease_owner_token = uuid::Uuid::new_v4().to_string();\n        let epoch_key = format!(\"{lease_key}:epoch:token\");\n        let block_stream_key = format!(\"{lease_key}:block:stream\");\n        let lease_drift_millis = lease_ttl_millis\n            .checked_div(100)\n            .unwrap_or(0)\n            .saturating_add(2);\n        Ok(Self {","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/FuelLabs/fuel-core/blob/b9d4d170da3a31c9ace5f963d633b326348e0d42/crates/fuel-core/src/service/adapters/consensus_module/poa.rs#L187-L223","documentation":"RedisLeaderLeaseAdapter::new (crates/fuel-core/src/service/adapters/consensus_module/poa.rs:204) constructs the Redis-backed leader lock for multi-leader PoA. It first maps every URL through redis::Client::open (which itself fails on malformed URLs), then rejects an empty node list: quorum is nodes/2 + 1, so with zero nodes leadership could never be established.","triggerScenarios":"Constructing the Redis leader-lock/reconciliation adapter with an empty redis_urls Vec — the config field omitted, defaulted to empty, or explicitly set to [].","commonSituations":"Chain config missing the redis leader-lock section; a YAML/JSON typo making the urls field parse as empty; attempting to disable locking by passing an empty list instead of using the non-Redis consensus setup (Noop adapter).","solutions":["Provide at least one valid Redis URL (redis://host:6379); for real fault tolerance use an odd count of 3 or more since quorum = majority","If Redis leader locking is not wanted, configure the consensus without the Redis reconciliation adapter instead of passing an empty list","Validate redis_urls non-empty during config parsing so the failure happens with a pointed config error before node start"],"exampleFix":"// before: RedisLeaderLeaseAdapter::new(vec![], key, ..)?\n// after:  RedisLeaderLeaseAdapter::new(\n//     vec![\"redis://10.0.0.1:6379\".into(), \"redis://10.0.0.2:6379\".into(),\n//          \"redis://10.0.0.3:6379\".into()],\n//     key, ..)?","handlingStrategy":"validation","validationCode":"anyhow::ensure!(!redis_urls.is_empty(),\n    \"consensus config: redis leader-lock requires at least one redis url\");\nanyhow::ensure!(redis_urls.len() % 2 == 1 && redis_urls.len() >= 3,\n    \"recommended: odd number of redis urls >= 3 (quorum = majority)\");\n// run before RedisLeaderLeaseAdapter::new","typeGuard":null,"tryCatchPattern":"if let Err(e) = RedisLeaderLeaseAdapter::new(urls, ..) {\n    if e.to_string().contains(\"At least one redis url\") {\n        // pure config error: fix config, never retry with same input\n    }\n}","preventionTips":["Make redis_urls a required (non-default) field in the consensus config schema","Fail config validation at load time with a clear message instead of at adapter construction","Prefer 3+ odd-count deployments; a single URL is a single point of failure for leadership"],"tags":["fuel-core","poa","redis","leader-election","config"],"backgroundTag":null,"analyzedSha":"b9d4d170da3a31c9ace5f963d633b326348e0d42","analyzedAt":"2026-08-16T08:56:42.692Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}