{"record":{"id":"03f1d1793cd915c7","repo":"neondatabase/neon","slug":"safekeeper-connstrings-should-be-provided","errorCode":null,"errorMessage":"safekeeper connstrings should be provided","messagePattern":"safekeeper connstrings should be provided","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"compute_tools/src/compute.rs","lineNumber":346,"sourceCode":"                    Some(ShardStripeSize(u32::from_str(&guc)?))\n                } else {\n                    None\n                };\n                pageserver_conninfo =\n                    Some(PageserverConnectionInfo::from_connstr(&guc, stripe_size)?);\n            }\n        }\n        let pageserver_conninfo = pageserver_conninfo.ok_or(anyhow::anyhow!(\n            \"pageserver connection information should be provided\"\n        ))?;\n\n        // Similarly for safekeeper connection strings\n        let safekeeper_connstrings = if spec.safekeeper_connstrings.is_empty() {\n            if matches!(spec.mode, ComputeMode::Primary) {\n                spec.cluster\n                    .settings\n                    .find(\"neon.safekeepers\")\n                    .ok_or(anyhow::anyhow!(\"safekeeper connstrings should be provided\"))?\n                    .split(',')\n                    .map(|str| str.to_string())\n                    .collect()\n            } else {\n                vec![]\n            }\n        } else {\n            spec.safekeeper_connstrings.clone()\n        };\n\n        let storage_auth_token = spec.storage_auth_token.clone();\n        let tenant_id: TenantId = if let Some(tenant_id) = spec.tenant_id {\n            tenant_id\n        } else {\n            let guc = spec\n                .cluster\n                .settings\n                .find(\"neon.tenant_id\")","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/neondatabase/neon/blob/8f60b04da47ffefe0e52bda2440134b42874eb75/compute_tools/src/compute.rs#L328-L364","documentation":"For a Primary-mode compute, compute_ctl requires a non-empty list of safekeepers: neither spec.safekeeper_connstrings nor the 'neon.safekeepers' GUC in cluster settings was present. Primary nodes synchronously replicate WAL to safekeepers and cannot start without them. Non-primary modes (e.g. read replicas) are allowed to have an empty list.","triggerScenarios":"ParsedSpec::try_from with spec.mode == ComputeMode::Primary, spec.safekeeper_connstrings empty, and settings.find(\"neon.safekeepers\") returning None. Note the GUC value must be a comma-separated host:port list; an empty-string GUC also fails downstream when split/joined.","commonSituations":"Hand-written or generated spec for a primary endpoint missing the safekeepers GUC; cplane version that stopped populating safekeeper_connstrings; a spec intended for a replica accidentally marked Primary; typo in the GUC name so find() misses it.","solutions":["Add \"neon.safekeepers\": \"host1:5678,host2:5678,...\" to spec.cluster.settings for Primary mode","Or populate spec.safekeeper_connstrings at the control plane so the fallback is unnecessary","Verify spec.mode is actually Primary; replicas must not require safekeepers","Pre-validate the spec: Primary mode implies a non-empty safekeepers list"],"exampleFix":"// before: Primary spec without safekeepers -> error\n// after\n{\n  \"mode\": \"Primary\",\n  \"cluster\": { \"settings\": [\n    { \"name\": \"neon.safekeepers\", \"value\": \"safekeeper-0:5678,safekeeper-1:5678,safekeeper-2:5678\" }\n  ]}\n}","handlingStrategy":"validation","validationCode":"// Primary mode requires safekeepers; validate before starting compute\nif matches!(spec.mode, ComputeMode::Primary) {\n    let has_sks = !spec.safekeeper_connstrings.is_empty()\n        || spec.cluster.settings.find(\"neon.safekeepers\").is_some();\n    if !has_sks { anyhow::bail!(\"Primary spec without safekeepers\"); }\n}","typeGuard":"fn primary_has_safekeepers(spec: &ComputeSpec) -> bool {\n    !matches!(spec.mode, ComputeMode::Primary)\n        || !spec.safekeeper_connstrings.is_empty()\n        || spec.cluster.settings.find(\"neon.safekeepers\").map(|s| !s.trim().is_empty()).unwrap_or(false)\n}","tryCatchPattern":"// Config error: report and stop; retrying cannot succeed until the spec changes\nif let Err(e) = ParsedSpec::try_from(spec) {\n    if e.chain().any(|c| c.to_string().contains(\"safekeeper\")) {\n        eprintln!(\"spec is missing neon.safekeepers; add a comma-separated host:port list\");\n    }\n    return Err(e);\n}","preventionTips":["Make the control plane treat 'Primary implies >=1 safekeeper' as an invariant at spec build time","Lint hand-written specs with a checklist: mode, safekeepers, tenant_id, timeline_id, pageserver connstring","Integration-test primary startup with a minimal correct spec so regressions surface early"],"tags":["rust","compute-ctl","spec","configuration","safekeepers","primary","startup"],"backgroundTag":"missing-configuration","analyzedSha":"8f60b04da47ffefe0e52bda2440134b42874eb75","analyzedAt":"2026-08-16T23:39:28.135Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}