FuelLabs/fuel-core · error · anyhow::Error

At least one redis url is required for leader lock

Error message

At least one redis url is required for leader lock

What it means

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.

Source

Thrown at crates/fuel-core/src/service/adapters/consensus_module/poa.rs:205

        lease_key: String,
        lease_ttl: Duration,
        node_timeout: Duration,
        retry_delay: Duration,
        max_retry_delay_offset: Duration,
        max_attempts: u32,
        stream_max_len: u32,
    ) -> anyhow::Result<Self> {
        let redis_nodes = redis_urls
            .into_iter()
            .map(|redis_url| {
                redis::Client::open(redis_url).map(|redis_client| RedisNode {
                    redis_client,
                    cached_connection: Mutex::new(None),
                })
            })
            .collect::<Result<Vec<_>, _>>()?;
        if redis_nodes.is_empty() {
            return Err(anyhow!(
                "At least one redis url is required for leader lock"
            ));
        }
        let quorum_disruption_budget = 0u32;
        let quorum = Self::calculate_quorum(redis_nodes.len(), quorum_disruption_budget);
        let lease_ttl_millis = u64::try_from(lease_ttl.as_millis())?;
        let retry_delay_millis = u64::try_from(retry_delay.as_millis())?;
        let max_retry_delay_offset_millis =
            u64::try_from(max_retry_delay_offset.as_millis())?;
        let max_attempts = usize::try_from(max_attempts)?.max(1);
        let lease_owner_token = uuid::Uuid::new_v4().to_string();
        let epoch_key = format!("{lease_key}:epoch:token");
        let block_stream_key = format!("{lease_key}:block:stream");
        let lease_drift_millis = lease_ttl_millis
            .checked_div(100)
            .unwrap_or(0)
            .saturating_add(2);
        Ok(Self {

View on GitHub (pinned to b9d4d170da)

Solutions

  1. 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
  2. If Redis leader locking is not wanted, configure the consensus without the Redis reconciliation adapter instead of passing an empty list
  3. Validate redis_urls non-empty during config parsing so the failure happens with a pointed config error before node start

Example fix

// before: RedisLeaderLeaseAdapter::new(vec![], key, ..)?
// after:  RedisLeaderLeaseAdapter::new(
//     vec!["redis://10.0.0.1:6379".into(), "redis://10.0.0.2:6379".into(),
//          "redis://10.0.0.3:6379".into()],
//     key, ..)?
Defensive patterns

Strategy: validation

Validate before calling

anyhow::ensure!(!redis_urls.is_empty(),
    "consensus config: redis leader-lock requires at least one redis url");
anyhow::ensure!(redis_urls.len() % 2 == 1 && redis_urls.len() >= 3,
    "recommended: odd number of redis urls >= 3 (quorum = majority)");
// run before RedisLeaderLeaseAdapter::new

Try / catch

if let Err(e) = RedisLeaderLeaseAdapter::new(urls, ..) {
    if e.to_string().contains("At least one redis url") {
        // pure config error: fix config, never retry with same input
    }
}

Prevention

When it happens

Trigger: Constructing the Redis leader-lock/reconciliation adapter with an empty redis_urls Vec — the config field omitted, defaulted to empty, or explicitly set to [].

Common situations: 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).

Related errors


AI-assisted analysis of FuelLabs/fuel-core@b9d4d170da (2026-08-16). Data as JSON: /api/errors/647659e7ce0b2d01. Report an issue: GitHub.