cloudflare/pingora · error

take_write_lock() called without lock

Error message

take_write_lock() called without lock

What it means

The second .expect() inside HttpCache::take_write_lock() (lib.rs:1568): the request does have a lock_ctx, but lock_ctx.lock is already None because Option::take() consumed the permit earlier. That is the exact post-transfer state, so this panic ('take_write_lock() called without lock') means take_write_lock() was called a second time, or another component already transferred the permit while leaving the context behind.

Source

Thrown at pingora-cache/src/lib.rs:1568

        self.inner_enabled()
            .lock_ctx
            .as_ref()
            .and_then(|l| l.max_retries)
    }

    /// Take the write lock from this request to transfer it to another one.
    /// # Panic
    ///  Call is_cache_lock_writer() to check first, will panic otherwise.
    pub fn take_write_lock(&mut self) -> (WritePermit, &'static CacheKeyLockImpl) {
        let lock_ctx = self
            .inner_enabled_mut()
            .lock_ctx
            .as_mut()
            .expect("take_write_lock() called without cache lock");
        let lock = lock_ctx
            .lock
            .take()
            .expect("take_write_lock() called without lock");
        match lock {
            Locked::Write(w) => (w, lock_ctx.cache_lock),
            Locked::Read(_) => panic!("take_write_lock() called on read lock"),
        }
    }

    /// Set the write lock, which is usually transferred from [Self::take_write_lock()]
    ///
    /// # Panic
    /// Panics if cache lock was not originally configured for this request.
    // TODO: it may make sense to allow configuring the CacheKeyLock here too that the write permit
    // is associated with
    // (The WritePermit comes from the CacheKeyLock and should be used when releasing from the CacheKeyLock,
    // shouldn't be possible to give a WritePermit to a request using a different CacheKeyLock)
    pub fn set_write_lock(&mut self, write_lock: WritePermit) {
        if let Some(lock_ctx) = self.inner_enabled_mut().lock_ctx.as_mut() {
            lock_ctx.lock.replace(Locked::Write(write_lock));
        }

View on GitHub (pinned to 0046038bd4)

Solutions

  1. Call take_write_lock() exactly once per request and move the returned (WritePermit, lock) pair onward
  2. If a handoff can fail midway, restore the permit with session.cache.set_write_lock(permit) before any retry
  3. Use is_cache_lock_writer() as the single guard: it is false once the permit was taken

Example fix

// before
let (p1, lock) = session.cache.take_write_lock();
let (p2, _) = session.cache.take_write_lock(); // panic: lock already taken

// after
let (permit, lock) = session.cache.take_write_lock();
// re-arm before any second take:
session.cache.set_write_lock(permit);
let (permit, lock) = session.cache.take_write_lock();
Defensive patterns

Strategy: validation

Validate before calling

// is_cache_lock_writer() is false once the permit was taken;
// use it to make double-takes impossible
fn take_write_lock_once(cache: &mut HttpCache) -> Option<(WritePermit, &'static CacheKeyLockImpl)> {
    if cache.is_cache_lock_writer() {
        return Some(cache.take_write_lock());
    }
    None
}

Prevention

When it happens

Trigger: Calling take_write_lock() twice on the same request; a retry loop that re-invokes the handoff after a partial failure; two filters both attempting to transfer the write permit.

Common situations: Error paths that took the lock, failed to hand it off, and then retry the take; refactors where set_write_lock() was dropped so the context is never re-armed; duplicated transfer logic in request and response filters.

Related errors


AI-assisted analysis of cloudflare/pingora@0046038bd4 (2026-08-16). Data as JSON: /api/errors/5f505f05181a07e1. Report an issue: GitHub.