In PHP, what does apcu_entry() guarantee when many requests miss the same cached feature-flag list at once, and what does its lock cost?
answer
- fetch or generate, atomically
- callback receives the key
- one exclusive lock for the whole cache
- every other APCu call waits
- only apcu_entry() is safe inside
basics
~20 sapcu_entry($key, $callback, $ttl) returns the cached value or runs the callback once and caches the result, so concurrent misses do not all rebuild it. The callback runs under APCu's exclusive cache lock, blocking every other APCu call on the server.
solid answer
~50 s`apcu_entry(string $key, callable $callback, int $ttl = 0)` looks the key up and, on a miss, calls `$callback($key)`, caches the return value with `$ttl` and returns it. The whole operation runs under APCu's **exclusive cache lock**, so when a flag list expires and 200 requests miss at once, only one runs the loader and the rest get its result: no rebuild storm against the database. The cost is the scope of that lock. It is not per key; while the callback runs, **no other APCu function on that server can proceed**, whatever key it uses. A callback that calls a remote flag service for 300 ms stalls every APCu read on the machine for 300 ms. The manual says `apcu_entry()` is the only APCu function safe to call from inside the callback. Keep callbacks fast and local, or use `apcu_add()` as a lightweight per-key rebuild lock and serve the stale copy meanwhile.
code
php · 21 lines<?php
declare(strict_types=1);
function flags(PDO $pdo): array
{
$flags = apcu_fetch('shop:flags:v1', $hit);
$fresh = apcu_exists('shop:flags:fresh');
if ($hit && $fresh) {
return $flags;
}
// Stale: one request per server wins the rebuild.
if (!$hit || apcu_add('shop:flags:rebuilding', 1, 10)) {
$flags = $pdo->query('SELECT name, enabled FROM feature_flags')
->fetchAll(PDO::FETCH_KEY_PAIR);
apcu_store('shop:flags:v1', $flags, 3600);
apcu_store('shop:flags:fresh', 1, 30);
apcu_delete('shop:flags:rebuilding');
}
return $flags;
}go deeper
Know that apcu_entry() fetches a key or computes and caches it in one call, so you do not write the fetch-then-store pattern by hand.
Explain the atomicity: concurrent misses run the callback once, and the callback gets the key and returns the value to store with the TTL.
Stress the lock scope: the whole server's APCu waits for the callback. Keep callbacks fast and local, and use apcu_add() markers with stale serving when the loader is slow.
Decide where rebuild coordination belongs: per server with APCu primitives, or across the fleet with a shared lock, based on how expensive a duplicate load really is.
## The problem it solves When a popular cache entry expires, every request that arrives before it is rebuilt sees a miss. With the naive fetch-then-store pattern, each of them runs the expensive loader: ```php <?php $flags = apcu_fetch('shop:flags:v1', $hit); if (!$hit) { $flags = loadFlags(); // 200 concurrent requests all run this apcu_store('shop:flags:v1', $flags, 30); } ``` On a busy server that is hundreds of identical database queries at the same moment. The general phenomenon is a cache stampede; APCu offers one specific tool against it. ## What apcu_entry() does ```php <?php declare(strict_types=1); $flags = apcu_entry( 'shop:flags:v1', static fn (string $key): array => loadFlagsFromDatabase(), 30, ); ``` The signature is `apcu_entry(string $key, callable $callback, int $ttl = 0): mixed`. Step by step: 1. APCu acquires its **exclusive lock** on the cache. 2. It looks up `$key`. On a hit, it returns the cached value. 3. On a miss, it calls `$callback` with the key as the only argument. 4. It stores the callback's return value with `$ttl` and returns it. 5. It releases the lock when control leaves `apcu_entry()`. Because steps 2 to 4 happen under one lock, two processes cannot both run the callback for the same miss. The second one waits, then finds the value the first one stored. ## What the lock costs The manual is explicit that the lock is the cache's lock, not the key's. While a callback runs: - **every other APCu operation on the server waits**: `apcu_fetch()`, `apcu_store()`, `apcu_delete()` and any other `apcu_entry()`, on any key; - the callback is in effect a **critical section** for the whole machine's APCu; - calling other APCu functions inside the callback is unsafe; the manual names `apcu_entry()` as the only APCu function that can be called safely from the callback. | Callback does | Lock held for | Effect on the server | |---|---|---| | builds an array from constants or a local file | microseconds | none worth measuring | | runs one indexed database query | a few milliseconds | brief pauses for all APCu users | | calls a remote flag service over HTTP | hundreds of milliseconds, or the full timeout | every request touching APCu stalls | So `apcu_entry()` converts a rebuild storm into a queue, and the queue includes unrelated keys. ## A per-key alternative When the loader is slow, a per-key approach avoids blocking the whole cache: 1. Store the value with a long TTL, and store a separate refresh-marker key with a short TTL. 2. When a request finds the marker missing, it calls `apcu_add('shop:flags:refresh', 1, 10)`. `apcu_add()` stores only if the key does not exist and returns `false` otherwise, so exactly one request wins. 3. The winner rebuilds and stores the new value and marker; everyone else keeps serving the old value in the meantime. This keeps every other APCu read fast, at the price of serving slightly stale data during the refresh and of more code. ## Other points worth knowing - The value is copied into shared memory like any `apcu_store()`, with the same serialization cost for objects. - The guarantee is per server. Four web servers can still run the loader four times, once each; `apcu_entry()` coordinates processes on one machine only. ## Spotting lock contention Contention from a slow callback has a recognisable signature: - latency rises for **many unrelated routes at the same moment**, on one server at a time; - the spikes line up with the expiry of one popular entry; - `hrtime(true)` spans around APCu calls show reads that normally take microseconds taking as long as the loader. If you see that pattern, measure the callback itself. A loader that takes longer than a millisecond or two is a candidate for the per-key approach. Use `apcu_entry()` when the loader is fast and local and the value is hot; reach for a per-key marker when the loader crosses the network.
- Does apcu_entry() stop four web servers from each loading the flag list when it expires?No. Its lock is the shared-memory lock of one server's APCu. Each server has its own store and its own lock, so each of the four runs the callback once. That is usually fine, four loads instead of hundreds, but coordination across servers needs a shared store or a lock in one.
- Why is calling apcu_fetch() inside an apcu_entry() callback a problem?The callback runs while `apcu_entry()` holds the cache's exclusive lock, and the other APCu functions acquire the same lock. The manual names `apcu_entry()` as the only APCu function safe to call from the callback. Load everything the callback needs from outside APCu, or restructure so nested lookups use nested `apcu_entry()` calls.
saying these in an interview costs you the question
- Believes apcu_entry() locks only the one key being generated.
- Puts a slow remote HTTP call inside an apcu_entry() callback.
- Calls apcu_fetch() or apcu_store() from inside an apcu_entry() callback.
- Assumes apcu_entry() prevents duplicate loads across different web servers.
- Thinks apcu_entry() needs a separate apcu_store() to cache the callback's result.