Closed (won't fix)
Project:
Drupal core
Version:
7.x-dev
Component:
base system
Priority:
Normal
Category:
Bug report
Assigned:
Reporter:
Created:
14 Aug 2010 at 07:47 UTC
Updated:
17 Sep 2010 at 18:16 UTC
Jump to comment: Most recent file
Comments
Comment #1
chx commentedHere is a tentative patch.
Comment #2
chx commentedCommented.
Comment #3
catchSubcribing.
Comment #4
chx commentedNote that the problem is neither Drupal 7 nor render cache specific. Here is a somewhat convoluted use case that shows this is reproducible with practically every Drupal. You need a text format that has two filters: one inlines the node title, the other queries a third party service. The whole thing is cacheable, of course.
I needed this convoluted example because check_markup is fairly resilient against this sort of problem by using practically the whole source as a cache key. However, it can still be fooled. And, of course, the render cache can't do this.
Another example:
And so on. Endless examples. All you need is a slow running process that gets hit by a save in the middle.
Comment #5
chx commentedHere is a patch that still needs tests (edit: and documentation). It has the render cache and check_markup converted, eventually needs every cache_get / cache_set pair converted. The API change is extension , ie if you dont change a thing then your code works the same as before (albeit still race prone).
Comment #7
chx commentedAdded a ton of comments. WIll add a test later.
Comment #8
jeremy commentedSubscribe.
Comment #10
chx commentedComment #12
moshe weitzman commentedI have no substantive comment for the moment. I did notice that the descriptions in system.install need to be customized from their copy/paste state.
Comment #13
chx commentedThis versioning patch won't work:
I only used history here because it's present in a standard install. See? By the time this drupal_render finishes, the value of timestamp is 2 but the render cache will contain 1.
To fight that, one would need to keep a list of recent deletions in a separate place -- as Damien said, a ban list -- and check on set against that. It's fairly horrible because set will need to take out a lock on the bin first then check against the ban list and then run the db_merge. But it requires less API changes. And, hopefully, sets are rare.
Comment #14
chx commentedThis is the ban list patch. Note that I have not added locking to set() and clear() because that would need to be bin-wide (or so I think) and so now the race window is collapsed into the time between the two queries in set. That's certainly a lot better -- if not perfect -- than having the window thrown wide open (the time of rendering a node).
I think this is a good step ahead. We can try to do better in Drupal 8.
Comment #15
chx commentedLess code, more comments. Lot more comments. Shows very cleanly what's the difference between the two tests.
Edit: this patch can be made better by testing for >= REQUEST_TIME in set(). It's still not ideal, will post more tomorrow.
Comment #17
chx commentedAnother attempt -- it only checks for clears if there was a failed get before. And it uses microseconds.
Comment #19
chx commentedThis one uses a double instead of a float (sigh). Also it matches the clears in PHP because it's impossible to match 'em in SQL. There won't be many if at all.
Comment #21
chx commentedHere is another attempt, I hope this one passes. The CacheClearCase needed to be patched and that caused the patch to inflate a little. That's not a biggie. I have put back the clear-matching into SQL by using an evil trick: I store a LIKE pattern into the column and match $cid against that. It works.
Comment #22
chx commentedandypost asked for a summary of the patch's doings.
Edit: note that there is still a very short race window open between the db_query and the db_merge in set() but it's small enough to not worry me really.
Edit2: the whole issue builds on the presumption that if I add caching to the processing of some object then I will add cache clearing code to the saving of said object. This presumption tends to be true, otherwise you have stale caches.
Comment #23
chx commentedYes! I figured out how to make the race totally go away by going back to the idea of nuking the cache after the act.
Comment #24
chx commentedNote: the performance hacks module (code here) adds render caching to nodes already. It's the poster child of this issue: on node update and delete it issues a
cache_clear_alland it caching tonode_view.Comment #25
kbahey commentedSubscribe.
Comment #26
andypostsubscribe
Comment #27
cwgordon7 commentedI've looked over the current patch pretty thoroughly, and this looks good to me, though I don't feel comfortable marking it rtbc without someone who is more familiar with the caching system than I looking at it. I recognize the necessity of having such a table, since there will always be a time lag between a cache lookup and the cache save, and it's impossible to use tricks with the cache id to circumvent it. The patch worked when I tried it out, and I like that it comes with a test to make sure it's working, but as I said, I don't feel comfortable marking this rtbc without someone else at least looking it over.
Comment #28
cwgordon7 commentedScratch that, found a problem: if you're updating and not installing from scratch, then:
- Can't run update.php, get error
PDOException: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'drupal.clear_cache' doesn't exist: DELETE FROM {clear_cache} WHERE (created < :db_condition_placeholder_0) ; Array ( [:db_condition_placeholder_0] => 1282279748.3037 ) in cache_clear_all() (line 170 of includes/cache.inc).Comment #29
chx commentedI tested upgrade and there is an upgrade test too so I am not sure what did you hit. I moved the upgrade into it's own if table exists just to make sure.
Comment #30
jeremy commentedTalking this through with Chx at Drupalcon. My biggest concern is the inability to index the query, which won't scale well if someone is doing a large number of cache clears (for example, when performing a migration or large data import).
One idea for alternative implementation is how we're handling wildcards with memcache as of today:
#888002: Wildcard clear lock contention
However, that will result in some large MySQL queries, as cid's can be up to 255 characters. Chx is coming up with solutions for this now...
Comment #31
david straussFirst, this is not a bug in the caching system. The core cache API doesn't have a race condition. The example here merely shows you can use it in other code and create a race condition.
Second, changes to the core cache API aren't necessary to solve race conditions like these. I recommend two approaches.
== Approach One: Fingerprinting ==
This approach is applicable when the item being cached has a fingerprint that can validate the freshness of related items pulled from the cache. Node, for example, has such a fingerprint with the "updated" field. When caching a rendered node, one can store the current "updated" field in the cache item. When loading a node out of the cache, it's easy to query the "updated" column for the node to verify freshness. If the race condition here happens, the cache item would not match the "updated" field of the node.
== Approach Two: Write-Through Caching ==
A superior approach is write-through caching. This means populating or updating the cache item synchronously with saving. Such an approach is particularly good because (1) items newly written are often loaded very soon after saving and (2) it severely mitigates the chance of a stampede of requests missing the cache following an update to a popular item.
Re-using the node example above, the code would want to (1) start a transaction, (2) save the node, (3) update the cached node, (4) commit the transaction. In the DB-backed cache, this is completely synchronous. In a memcached- or APC-backed cache, the cache item is very briefly fresher than the item in the database. Theoretically, it would be possible for something to fail between writing to memcached and committing the transaction, but it's basically negligible compared to other risks we already take in core.
Comment #32
david straussJust to clarify, while this is not a bug in the cache API, it *is* a bug in systems that use the cache API in ways that exhibit this race condition. Individual issues should be filed for each instance.