{"record":{"id":"a48a26c68583706b","repo":"cloudflare/pingora","slug":"take-write-lock-called-without-cache-lock","errorCode":null,"errorMessage":"take_write_lock() called without cache lock","messagePattern":"take_write_lock\\(\\) called without cache lock","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pingora-cache/src/lib.rs","lineNumber":1564,"sourceCode":"    }\n\n    /// Maximum number of cache lock retries configured for this request.\n    pub fn cache_lock_max_retries(&self) -> Option<usize> {\n        self.inner_enabled()\n            .lock_ctx\n            .as_ref()\n            .and_then(|l| l.max_retries)\n    }\n\n    /// Take the write lock from this request to transfer it to another one.\n    /// # Panic\n    ///  Call is_cache_lock_writer() to check first, will panic otherwise.\n    pub fn take_write_lock(&mut self) -> (WritePermit, &'static CacheKeyLockImpl) {\n        let lock_ctx = self\n            .inner_enabled_mut()\n            .lock_ctx\n            .as_mut()\n            .expect(\"take_write_lock() called without cache lock\");\n        let lock = lock_ctx\n            .lock\n            .take()\n            .expect(\"take_write_lock() called without lock\");\n        match lock {\n            Locked::Write(w) => (w, lock_ctx.cache_lock),\n            Locked::Read(_) => panic!(\"take_write_lock() called on read lock\"),\n        }\n    }\n\n    /// Set the write lock, which is usually transferred from [Self::take_write_lock()]\n    ///\n    /// # Panic\n    /// Panics if cache lock was not originally configured for this request.\n    // TODO: it may make sense to allow configuring the CacheKeyLock here too that the write permit\n    // is associated with\n    // (The WritePermit comes from the CacheKeyLock and should be used when releasing from the CacheKeyLock,\n    // shouldn't be possible to give a WritePermit to a request using a different CacheKeyLock)","sourceCodeStart":1546,"sourceCodeEnd":1582,"githubUrl":"https://github.com/cloudflare/pingora/blob/0046038bd402bc82912da862dadf9a479f31e9f1/pingora-cache/src/lib.rs#L1546-L1582","documentation":"pingora-cache's HttpCache::take_write_lock() transfers the cache write lock from this request to another (cache-lock leader handoff). The first .expect() (lib.rs:1564) fires when self has no lock_ctx at all: cache locking was never enabled/acquired for this request, so there is nothing to take, and the process panics with 'take_write_lock() called without cache lock'. The method's doc explicitly requires calling is_cache_lock_writer() first.","triggerScenarios":"Calling session.cache.take_write_lock() on a request that never went through the cache-lock acquisition path: cache locking not configured on the cache backend, the request arrived as a cache hit / bypass, or the lock context was already dropped by disable().","commonSituations":"Custom lock-transfer code (streaming miss, background revalidation fill) invoked from filters that also run on cache hits or non-cacheable requests; enabling cache lock only on some keys; upgrading code written against APIs where lock_ctx was unconditional.","solutions":["Gate every call exactly as the docs say: if session.cache.is_cache_lock_writer() { let (permit, lock) = session.cache.take_write_lock(); ... }","Ensure the cache backend actually has a cache lock configured (CacheLock/lock settings) so misses acquire a lock_ctx","Only take the lock from code paths that are guaranteed to run for the cache-miss leader request"],"exampleFix":"// before\nlet (permit, lock) = session.cache.take_write_lock(); // panics without a lock ctx\n\n// after\nif session.cache.is_cache_lock_writer() {\n    let (permit, lock) = session.cache.take_write_lock();\n    // hand off via set_write_lock() on the filler request\n}","handlingStrategy":"validation","validationCode":"// Required guard, straight from the method's doc comment (lib.rs:1556-1558)\nif session.cache.is_cache_lock_writer() {\n    let (permit, lock) = session.cache.take_write_lock();\n    // ...transfer the permit to the filler request...\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never call take_write_lock() without the is_cache_lock_writer() check","Keep all lock-transfer logic in one helper so every path is guarded identically","Test cache hits and non-cacheable requests through the same filter to prove they skip the transfer"],"tags":["rust","pingora-cache","cache-lock","panic","invariant"],"backgroundTag":"cache-lock-misuse","analyzedSha":"0046038bd402bc82912da862dadf9a479f31e9f1","analyzedAt":"2026-08-16T21:33:22.341Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}