{"record":{"id":"c40ddfd00a585abd","repo":"block/buzz","slug":"git-pack-cache-path-must-be-available","errorCode":null,"errorMessage":"git pack cache path must be available","messagePattern":"git pack cache path must be available","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/buzz-relay/src/state.rs","lineNumber":859,"sourceCode":"            }\n            tracing::warn!(\"audit log worker exited (expected on shutdown)\");\n        });\n\n        let git_max_concurrent_ops = config.git_max_concurrent_ops;\n        let media_max_concurrent_uploads = config.media_max_concurrent_uploads;\n        let git_store = crate::api::git::store::GitStore::new(\n            &config.media.s3_endpoint,\n            &config.media.s3_access_key,\n            &config.media.s3_secret_key,\n            &config.media.s3_bucket,\n            &config.media.s3_region,\n            config.media.s3_addressing_style,\n        )\n        .expect(\"media storage was already constructed with this S3 config\");\n        let git_pack_cache = Arc::new(\n            crate::api::git::pack_cache::GitPackCache::new(\n                &config.git_pack_cache_path,\n                config.git_pack_cache_max_bytes,\n                config.git_pack_cache_max_concurrent_populations,\n            )\n            .expect(\"git pack cache path must be available\"),\n        );\n        let nip98_replay: Arc<dyn Nip98ReplayGuard> =\n            Arc::new(RedisNip98ReplayGuard::new(redis_pool.clone()));\n        let gif_http_client = crate::api::gifs::build_gif_http_client();\n        let admission_rate_limiter = Arc::new(RedisRateLimiter::new(redis_pool.clone()));\n        let audit_enabled = audit_arc.is_some();\n        let state = Self {\n            config: Arc::new(config),\n            db,\n            redis_pool,\n            audit: audit_arc,\n            pubsub,\n            auth: Arc::new(auth),\n            search: search_arc,\n            sub_registry: Arc::new(SubscriptionRegistry::new()),","sourceCodeStart":841,"sourceCodeEnd":877,"githubUrl":"https://github.com/block/buzz/blob/dad5a33865fc81a2e55b3b60746632f615ec1e3a/crates/buzz-relay/src/state.rs#L841-L877","documentation":"AppState::new constructs a GitPackCache under config.git_pack_cache_path. GitPackCache::new (crates/buzz-relay/src/api/git/pack_cache.rs:107) fails when git_pack_cache_max_concurrent_populations is 0, the cache directory cannot be created (permission, missing parent, ENOSPC), stat fails, or the path is a symlink (explicitly rejected for safety). The expect turns any of those into a startup panic of the whole relay.","triggerScenarios":"git_pack_cache_path on a read-only volume or a directory the relay user cannot write; the path being a symlink (e.g. /var/cache/buzz -> /mnt/data); git_pack_cache_max_concurrent_populations set to 0; disk full when creating the session tempdir.","commonSituations":"Kubernetes deployments mounting a read-only or wrongly-owned cache volume; operators symlinking the cache to a larger disk; container runs where the configured path exists but belongs to root; env-var typos falling back to a default path that is unwritable in the image.","solutions":["Point git_pack_cache_path at a writable, real (non-symlink) directory owned by the relay user","Set git_pack_cache_max_concurrent_populations to at least 1","Check disk space and filesystem health (df, mount flags) on the cache volume","Propagate the String error out of AppState construction so misconfiguration produces a clean startup error instead of a panic"],"exampleFix":"// before\nlet git_pack_cache = Arc::new(\n    GitPackCache::new(\n        &config.git_pack_cache_path,\n        config.git_pack_cache_max_bytes,\n        config.git_pack_cache_max_concurrent_populations,\n    )\n    .expect(\"git pack cache path must be available\"),\n);\n\n// after\nlet git_pack_cache = Arc::new(\n    GitPackCache::new(\n        &config.git_pack_cache_path,\n        config.git_pack_cache_max_bytes,\n        config.git_pack_cache_max_concurrent_populations,\n    )\n    .map_err(|e| anyhow::anyhow!(\"git pack cache init: {e}\"))?,\n);","handlingStrategy":"validation","validationCode":"// before constructing AppState\nlet p = std::path::Path::new(&config.git_pack_cache_path);\nstd::fs::create_dir_all(p).map_err(|e| format!(\"create git pack cache dir: {e}\"))?;\nif std::fs::symlink_metadata(p).map_err(|e| e.to_string())?.file_type().is_symlink() {\n    return Err(\"git pack cache path must not be a symlink\".into());\n}\nif config.git_pack_cache_max_concurrent_populations == 0 {\n    return Err(\"git_pack_cache_max_concurrent_populations must be >= 1\".into());\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Provision the cache directory in deployment manifests (emptyDir/hostPath with correct fsGroup) rather than relying on runtime creation","Never symlink the cache path — mount the real volume directly","Add a startup readiness check that validates writable cache, DB, and Redis together so config failures surface before traffic"],"tags":["rust","config","filesystem","git","cache","startup","symlink"],"backgroundTag":"cache-dir-unavailable","analyzedSha":"dad5a33865fc81a2e55b3b60746632f615ec1e3a","analyzedAt":"2026-08-20T04:38:24.874Z","contentChangedAt":"2026-08-20T04:38:24.874Z","schemaVersion":2},"datasetVersion":"2026-09-08T15:18:49.778Z"}