In Drush or in Devel's PHP execution form, run the following:
(No, you idiot! Don't run arbitrary PHP code that some jerk you've never met tells you to run in some random forum on the internet! But if you're adventurous…)
variable_set('cache_lifetime', '600');
$expire = time() + 3600;
// Set some cached data in the standard "cache" table with an expiry
cache_set('foo', array('bar' => 'baz'), 'cache', $expire);
// …and in the "cache_block" table
cache_set('foo', array('bar' => 'baz'), 'cache_block', $expire);
// Print it back out:
$cache = cache_get('foo', 'cache');
print_r($cache);
$cache = cache_get('foo', 'cache_block');
print_r($cache);
That being executed, in a separate page load, execute this:
cache_clear_all(NULL, 'cache_page');
// Wait two seconds…
usleep(2000000);
// …And try getting the cache data again.
$cache = cache_get('foo', 'cache');
print_r($cache);
$cache = cache_get('foo', 'cache_block');
print_r($cache);
The output will be 00.
The relevant code, in cache_clear_all():
function cache_clear_all($cid = NULL, $table = NULL, $wildcard = FALSE) {
global $user;
// ...
if (empty($cid)) {
if (variable_get('cache_lifetime', 0)) {
// We store the time in the current user's $user->cache variable which
// will be saved into the sessions table by sess_write(). We then
// simulate that the cache was flushed for this user by not returning
// cached data that was cached before the timestamp.
$user->cache = time();
// ...
And in cache_get():
function cache_get($cid, $table = 'cache') {
global $user;
// ...
if (isset($cache->data)) {
// If the data is permanent or we're not enforcing a minimum cache lifetime
// always return the cached data.
if ($cache->expire == CACHE_PERMANENT || !variable_get('cache_lifetime', 0)) {
// ...
}
// If enforcing a minimum cache lifetime, validate that the data is
// currently valid for this user before we return it by making sure the
// cache entry was created before the timestamp in the current session's
// cache timer. The cache variable is loaded into the $user object by
// sess_read() in session.inc.
else {
if (isset($user->cache) && $user->cache > $cache->created) {
// This cache data is too old and thus not valid for us, ignore it.
return 0;
}
// ...
Thus: cache_clear_all(), when called for a particular table, sets $user->cache; then, if cached data is not permanent, cache_get() checks if $user->cache is older than the created timestamp of the cache, regardless of whether that cached data is in the same table that was cleared or not.
We ran into this problem when submission of a node form in one tab was causing issues with AHAH widgets in another, if I'm understanding my colleague correctly.
Not sure if there's a clean way to fix this… also haven't tested this in D7 yet.
Comments
Comment #1
dalinI'm experiencing this bug with some complex AJAX forms. Bumping to critical as this results in data loss.
Would the proper fix be to change $user->cache to an array with keys for each table?
Comment #2
damien tournoud commentedI'm pretty sure that this is a duplicate of #227228: cache_clear_all and cache_get fail to clear caches when Minimum cache lifetime is on.
Comment #3
damien tournoud commentedActually, no. But this has to be fixed in 8.x before anything else.
In a nutshell: yes, the minimum cache lifetime "feature" is broken beyond repair (see #276267: Fix the minimum cache lifetime feature and #730060: Replace CACHE_TEMPORARY, cache_clear_all() and minimum cache lifetime with cache tags support for context). Don't use it. It's useless, and dangerous.
Comment #4
catch$user->cache could be an array of cache bins + timestamps, that would fix the specific bug.
I would not like to remove minimum cache lifetime without some kind of replacement, at least not until #891600: Page and block caches are cleared the same way on cron as on content change is properly fixed.
If we could add some kind of implementation of varnish's grace setting for the page cache (serve stale content on cache misses and build the new one in the background) and/or similar to what memcache 6.x-1.9 has for stampede protection, then I'd also feel a lot better about getting rid of it.
Comment #5
Garrett Albright commentedDamien, "Beyond repair" is strong language.
Re-examining this and the other issues, it looks like there's three main problems:
1. $user->cache doesn't respect the fact that there is more than one database table/bin in use for caching;
2. Using REQUEST_TIME instead of time() isn't a great idea in this case because it means cached items will effectively never be invalidated for long-running processes, like Bot (huh),
3. What CACHE_TEMPORARY really means, and what should really happen with expired cached items, is undefined or inconsistent.
Are these not all fixable? Imma go ahead and work on step 1 this afternoon.
Comment #6
Garrett Albright commentedAnd here's a thing. It avoids the problem in the OP, and all cache-related tests still pass, at least on my end. In the near future I'll try to add a new test to test for exactly this issue, though I'm not sure if it will be possible due to it kind of requiring Drupal to be re-bootstrapped in between; we'll see. It doesn't touch steps 2 and 3 in the list above yet; those are likely jobs for other issues.
Comment #7
catchThis is going to need a backport, we could likely leave the update in D8 and just leave the cache column redundant in D7 to avoid a big schema change on the users table in a minor release.
Comment #8
Garrett Albright commentedOkay, here's a D7 patch which strips out the schema alteration. It also takes out a couple more lingering
global $user;lines.No test today… sorry. Still not sure if it would even be possible. I'll take another look at it over the weekend; if there's a testing system guru who may be able to help out on this, please ping me on IRC or wherever.
Comment #9
Garrett Albright commentedOkay, well, here's an attempt at a test. Unfortunately, the test passes before #6 is applied, so it needs work. Halp plz.
Comment #10
Garrett Albright commentedIn retrospect, it's silly for me to spend my time trying to write a test for a thing which may not be possible to test for when the test isn't going to even make sense when the patch in question is committed anyway. Switching back to needs review.
Comment #12
Garrett Albright commentedwhat
Comment #13
catchHere's a possible test case that might fail:
In one of the test modules, have two page callbacks:
callback_1 does a drupal_set_message() - that only happens on a cache miss, doesn't matter what gets cached.
callback_2 triggers cache_clear_all(NULL, $different_cache_bin);
Visit callback 1 -> assert the message is shown.
Visit callback 1 -> assert the message is not shown.
Visit callback 2 -> assert the cache item for callback 1 exists (because it should - wasn't actually cleared).
Visit callback 1 -> assert the message is not shown.
Last assertion ought to fail without the patch, pass with it.
Comment #14
Garrett Albright commentedAttached is my attempt at implementing the test as catch suggested. Alas, the tests still pass before the patch. Damn damn damn!
Comment #15
Garrett Albright commentedComment #16
Garrett Albright commentedIn this post, Albright tries to goad the testbot into testing the test patch just to show everyone it's still all green.
EDIT: WTF, still "ignored?" Okay, whatev. I give.
Comment #17
catchCNR if you want the bot to test it.
Comment #18
pillarsdotnet commentedI think that also the patch filename must end in ".patch" rather than ".test"
Comment #19
Garrett Albright commentedOh my God, I'm so dumb.
Comment #21
Garrett Albright commentedReroll. Remember, the problem I'm currently facing is that these tests pass when they shouldn't.
Comment #23
Garrett Albright commented(sigh)
Comment #24
catchThis is a duplicate of #1015946: Eliminate $user->cache and {session}.cache in favor of $_SESSION['cache_expiration'][$bin].