Skip to content

Commit b2d1b33

Browse files
author
Mateu Aguiló Bosch
committed
[BUGFIX] Remove cache fragments during shutdown
When an entity is updated or deleted we need to clear caches and remove the cache fragments. The problem is that depending on the site configuration, that operation may be affected for a rolling back transaction. Defer the clearing to the shutdown instead.
1 parent 52f2ce4 commit b2d1b33

File tree

3 files changed

+106
-74
lines changed

3 files changed

+106
-74
lines changed

restful.entity.inc

Lines changed: 82 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use Drupal\restful\Plugin\resource\Decorators\CacheDecoratedResource;
99
use Drupal\restful\Plugin\resource\Decorators\CacheDecoratedResourceInterface;
1010
use Drupal\restful\RenderCache\Entity\CacheFragmentController;
1111
use Drupal\restful\RenderCache\RenderCache;
12-
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
1312
use Doctrine\Common\Collections\ArrayCollection;
1413

1514
/**
@@ -77,48 +76,22 @@ function _restful_entity_cache_hashes($entity, $type) {
7776
* Implements hook_entity_update().
7877
*/
7978
function restful_entity_update($entity, $type) {
80-
$resource_manager = restful()->getResourceManager();
81-
foreach (_restful_entity_cache_hashes($entity, $type) as $hash) {
82-
if (!$instance_id = CacheFragmentController::resourceIdFromHash($hash)) {
83-
continue;
84-
}
85-
$handler = $resource_manager->getPlugin($instance_id);
86-
if (!$handler instanceof CacheDecoratedResourceInterface) {
87-
continue;
88-
}
89-
if (!$handler->hasSimpleInvalidation()) {
90-
continue;
91-
}
92-
// You can get away without the fragments for a clear.
93-
$cache_object = new RenderCache(new ArrayCollection(), $hash, $handler->getCacheController());
94-
// Do a clear with the RenderCache object to also remove the cache fragment
95-
// entities.
96-
$cache_object->clear();
97-
}
79+
$hashes = &drupal_static('restful_entity_clear_hashes', array());
80+
$new_hashes = _restful_entity_cache_hashes($entity, $type);
81+
array_walk($new_hashes, '_restful_entity_clear_all_resources');
82+
$hashes += $new_hashes;
83+
restful_register_shutdown_function_once('restful_entity_clear_render_cache');
9884
}
9985

10086
/**
10187
* Implements hook_entity_delete().
10288
*/
10389
function restful_entity_delete($entity, $type) {
104-
$resource_manager = restful()->getResourceManager();
105-
foreach (_restful_entity_cache_hashes($entity, $type) as $hash) {
106-
if (!$instance_id = CacheFragmentController::resourceIdFromHash($hash)) {
107-
continue;
108-
}
109-
$handler = $resource_manager->getPlugin($instance_id);
110-
if (!$handler instanceof CacheDecoratedResourceInterface) {
111-
continue;
112-
}
113-
if (!$handler->hasSimpleInvalidation()) {
114-
continue;
115-
}
116-
// You can get away without the fragments for a clear.
117-
$cache_object = new RenderCache(new ArrayCollection(), $hash, $handler->getCacheController());
118-
// Do a clear with the RenderCache object to also remove the cache fragment
119-
// entities.
120-
$cache_object->clear();
121-
}
90+
$hashes = &drupal_static('restful_entity_clear_hashes', array());
91+
$new_hashes = _restful_entity_cache_hashes($entity, $type);
92+
array_walk($new_hashes, '_restful_entity_clear_all_resources');
93+
$hashes += $new_hashes;
94+
restful_register_shutdown_function_once('restful_entity_clear_render_cache');
12295
}
12396

12497
/**
@@ -131,29 +104,11 @@ function restful_user_update(&$edit, $account, $category) {
131104
->entityCondition('entity_type', 'cache_fragment')
132105
->propertyCondition('type', 'user_id')
133106
->propertyCondition('value', $account->uid);
134-
$resource_manager = restful()->getResourceManager();
135-
foreach (CacheFragmentController::lookUpHashes($query) as $hash) {
136-
if (!$plugin_id = CacheFragmentController::resourceIdFromHash($hash)) {
137-
continue;
138-
}
139-
try {
140-
$handler = $resource_manager->getPlugin($plugin_id);
141-
}
142-
catch (PluginNotFoundException $e) {
143-
continue;
144-
}
145-
if (!$handler instanceof CacheDecoratedResourceInterface) {
146-
return;
147-
}
148-
if (!$handler->hasSimpleInvalidation()) {
149-
return;
150-
}
151-
// You can get away without the fragments for a clear.
152-
$cache_object = new RenderCache(new ArrayCollection(), $hash, $handler->getCacheController());
153-
// Do a clear with the RenderCache object to also remove the cache fragment
154-
// entities.
155-
$cache_object->clear();
156-
}
107+
$hashes = &drupal_static('restful_entity_clear_hashes', array());
108+
$new_hashes = CacheFragmentController::lookUpHashes($query);
109+
array_walk($new_hashes, '_restful_entity_clear_all_resources');
110+
$hashes += $new_hashes;
111+
restful_register_shutdown_function_once('restful_entity_clear_render_cache');
157112
}
158113

159114
/**
@@ -166,19 +121,73 @@ function restful_user_delete($account) {
166121
->entityCondition('entity_type', 'cache_fragment')
167122
->propertyCondition('type', 'user_id')
168123
->propertyCondition('value', $account->uid);
169-
$resource_manager = restful()->getResourceManager();
170-
foreach (CacheFragmentController::lookUpHashes($query) as $hash) {
171-
$handler = $resource_manager->getPlugin(CacheFragmentController::resourceIdFromHash($hash));
172-
if (!$handler instanceof CacheDecoratedResourceInterface) {
173-
return;
174-
}
175-
if (!$handler->hasSimpleInvalidation()) {
176-
return;
124+
$hashes = &drupal_static('restful_entity_clear_hashes', array());
125+
$new_hashes = CacheFragmentController::lookUpHashes($query);
126+
array_walk($new_hashes, '_restful_entity_clear_all_resources');
127+
$hashes += $new_hashes;
128+
restful_register_shutdown_function_once('restful_entity_clear_render_cache');
129+
}
130+
131+
/**
132+
* Helper function to schedule a shutdown once.
133+
*
134+
* @param callable $callback
135+
* The callback.
136+
*/
137+
function restful_register_shutdown_function_once($callback) {
138+
$existing_callbacks = drupal_register_shutdown_function();
139+
$added = (bool) array_filter($existing_callbacks, function ($item) use ($callback) {
140+
return $item['callback'] == $callback;
141+
});
142+
if (!$added) {
143+
drupal_register_shutdown_function($callback);
144+
}
145+
}
146+
147+
/**
148+
* Clear the cache back ends for the given hash.
149+
*
150+
* @param string $cid
151+
* The cache ID to clear.
152+
*/
153+
function _restful_entity_clear_all_resources($cid) {
154+
if (!$instance_id = CacheFragmentController::resourceIdFromHash($cid)) {
155+
return;
156+
}
157+
$handler = restful()
158+
->getResourceManager()
159+
->getPlugin($instance_id);
160+
if (!$handler instanceof CacheDecoratedResourceInterface) {
161+
return;
162+
}
163+
// Clear the cache bin.
164+
$handler->getCacheController()->clear($cid);
165+
}
166+
167+
/**
168+
* Shutdown function that deletes the scheduled fragments and caches on shutdown.
169+
*/
170+
function restful_entity_clear_render_cache() {
171+
if ($hashes = drupal_static('restful_entity_clear_hashes', array())) {
172+
$hashes = array_unique($hashes);
173+
drupal_static_reset('restful_entity_clear_hashes');
174+
$resource_manager = restful()->getResourceManager();
175+
foreach ($hashes as $hash) {
176+
if (!$instance_id = CacheFragmentController::resourceIdFromHash($hash)) {
177+
continue;
178+
}
179+
$handler = $resource_manager->getPlugin($instance_id);
180+
if (!$handler instanceof CacheDecoratedResourceInterface) {
181+
continue;
182+
}
183+
if (!$handler->hasSimpleInvalidation()) {
184+
continue;
185+
}
186+
// You can get away without the fragments for a clear.
187+
$cache_object = new RenderCache(new ArrayCollection(), $hash, $handler->getCacheController());
188+
// Do a clear with the RenderCache object to also remove the cache fragment
189+
// entities.
190+
$cache_object->clear();
177191
}
178-
// You can get away without the fragments for a clear.
179-
$cache_object = new RenderCache(NULL, $hash, $handler->getCacheController());
180-
// Do a clear with the RenderCache object to also remove the cache fragment
181-
// entities.
182-
$cache_object->clear();
183192
}
184193
}

src/RenderCache/RenderCache.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,21 @@ public static function create(ArrayCollection $cache_fragments, \DrupalCacheInte
6969
* {@inheritdoc}
7070
*/
7171
public function get() {
72-
return $this->cacheObject->get($this->generateCacheId());
72+
$cid = $this->generateCacheId();
73+
$query = new \EntityFieldQuery();
74+
$count = $query
75+
->entityCondition('entity_type', 'cache_fragment')
76+
->propertyCondition('hash', $cid)
77+
->count()
78+
->execute();
79+
80+
if ($count) {
81+
return $this->cacheObject->get($cid);
82+
}
83+
// If there are no cache fragments for the given hash then clear the cache
84+
// and return NULL.
85+
$this->cacheObject->clear($cid);
86+
return NULL;
7387
}
7488

7589
/**

tests/RestfulRenderCacheTestCase.test

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ use Drupal\restful\RenderCache\Entity\CacheFragmentController;
1313

1414
class RestfulRenderCacheTestCase extends \RestfulCurlBaseTestCase {
1515

16+
/**
17+
* {@inheritdoc}
18+
*/
1619
public static function getInfo() {
1720
return array(
1821
'name' => 'Render Cache',
@@ -21,6 +24,9 @@ class RestfulRenderCacheTestCase extends \RestfulCurlBaseTestCase {
2124
);
2225
}
2326

27+
/**
28+
* {@inheritdoc}
29+
*/
2430
public function setUp() {
2531
parent::setUp('restful_test');
2632
}
@@ -111,6 +117,9 @@ class RestfulRenderCacheTestCase extends \RestfulCurlBaseTestCase {
111117
$account->name .= ' updated';
112118
user_save($account);
113119
$this->assertFalse($cache_object->get(), 'Cache object has been cleared after updating a user.');
120+
// The cache fragment garbage collection happens on shutdown. For testing
121+
// purposes we'll call the function directly here.
122+
restful_entity_clear_render_cache();
114123
// Make sure that the cache fragment entities have been deleted.
115124
$query = new \EntityFieldQuery();
116125
/* @var CacheFragmentController $controller */

0 commit comments

Comments
 (0)