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

dalin’s picture

Priority: Normal » Critical
Issue tags: -cache

I'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?

damien tournoud’s picture

Status: Active » Closed (duplicate)
damien tournoud’s picture

Version: 6.20 » 8.x-dev
Priority: Critical » Normal
Status: Closed (duplicate) » Active

Actually, 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.

catch’s picture

$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.

Garrett Albright’s picture

Damien, "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.

Garrett Albright’s picture

Status: Active » Needs review
StatusFileSize
new4.44 KB

And 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.

catch’s picture

Issue tags: +Needs backport to D7

This 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.

Garrett Albright’s picture

StatusFileSize
new3.6 KB

Okay, 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.

Garrett Albright’s picture

Status: Needs review » Needs work
StatusFileSize
new1.41 KB

Okay, well, here's an attempt at a test. Unfortunately, the test passes before #6 is applied, so it needs work. Halp plz.

Garrett Albright’s picture

Status: Needs work » Needs review

In 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.

Status: Needs review » Needs work

The last submitted patch, 1079518-cache-bins-test.patch, failed testing.

Garrett Albright’s picture

Status: Needs work » Needs review

what

catch’s picture

Here'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.

Garrett Albright’s picture

StatusFileSize
new3.49 KB

Attached is my attempt at implementing the test as catch suggested. Alas, the tests still pass before the patch. Damn damn damn!

Garrett Albright’s picture

Status: Needs review » Needs work
Garrett Albright’s picture

StatusFileSize
new3.49 KB

In 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.

catch’s picture

Status: Needs work » Needs review

CNR if you want the bot to test it.

pillarsdotnet’s picture

I think that also the patch filename must end in ".patch" rather than ".test"

Garrett Albright’s picture

Oh my God, I'm so dumb.

Status: Needs review » Needs work

The last submitted patch, 1079518-cache-bins-test-new-approach_0.patch, failed testing.

Garrett Albright’s picture

Status: Needs work » Needs review
StatusFileSize
new5.66 KB

Reroll. Remember, the problem I'm currently facing is that these tests pass when they shouldn't.

Status: Needs review » Needs work

The last submitted patch, cache-clearing-1079518.patch, failed testing.

Garrett Albright’s picture

Status: Needs work » Needs review
StatusFileSize
new2.97 KB

(sigh)

catch’s picture

Status: Needs review » Closed (duplicate)