Skip to content

Commit 2f3d585

Browse files
committed
Working on #433
1 parent 145efa9 commit 2f3d585

File tree

6 files changed

+290
-43
lines changed

6 files changed

+290
-43
lines changed

src/phpFastCache/CacheManager.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@ class CacheManager
138138
* (Memcache(d) drivers only)
139139
*/
140140
'compress_data' => false,
141+
142+
/**
143+
* Prevent cache slams when
144+
* making use of heavy cache
145+
* items
146+
*/
147+
'preventCacheSlams' => false,
148+
149+
/**
150+
* Cache slams timeout
151+
* in seconds
152+
*/
153+
'cacheSlamsTimeout' => 15,
154+
141155
];
142156

143157
/**

src/phpFastCache/Core/Pool/CacheItemPoolTrait.php

Lines changed: 105 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
use phpFastCache\Core\Item\ExtendedCacheItemInterface;
1818
use phpFastCache\CacheManager;
19+
use phpFastCache\Entities\ItemBatch;
1920
use phpFastCache\EventManager;
2021
use phpFastCache\Exceptions\phpFastCacheCoreException;
2122
use phpFastCache\Exceptions\phpFastCacheInvalidArgumentException;
@@ -70,64 +71,103 @@ public function getItem($key)
7071
* @var $item ExtendedCacheItemInterface
7172
*/
7273
CacheManager::$ReadHits++;
74+
$cacheSlamsSpendSeconds = 0;
7375
$class = new \ReflectionClass((new \ReflectionObject($this))->getNamespaceName() . '\Item');
7476
$item = $class->newInstanceArgs([$this, $key]);
7577
$item->setEventManager($this->eventManager);
76-
$driverArray = $this->driverRead($item);
7778

78-
if ($driverArray) {
79-
if(!is_array($driverArray)){
80-
throw new phpFastCacheCoreException(sprintf('The driverRead method returned an unexpected variable type: %s', gettype($driverArray)));
81-
}
82-
$item->set($this->driverUnwrapData($driverArray));
83-
$item->expiresAt($this->driverUnwrapEdate($driverArray));
84-
85-
if($this->config['itemDetailedDate']){
86-
87-
/**
88-
* If the itemDetailedDate has been
89-
* set after caching, we MUST inject
90-
* a new DateTime object on the fly
91-
*/
92-
$item->setCreationDate($this->driverUnwrapCdate($driverArray) ?: new \DateTime());
93-
$item->setModificationDate($this->driverUnwrapMdate($driverArray) ?: new \DateTime());
94-
}
79+
getItemDriverRead:
80+
{
81+
$driverArray = $this->driverRead($item);
82+
83+
if ($driverArray) {
84+
if(!is_array($driverArray)){
85+
throw new phpFastCacheCoreException(sprintf('The driverRead method returned an unexpected variable type: %s', gettype($driverArray)));
86+
}
87+
$driverData = $this->driverUnwrapData($driverArray);
88+
89+
if($this->getConfig()[ 'preventCacheSlams' ]){
90+
while($driverData instanceof ItemBatch) {
91+
if($driverData->getItemDate()->getTimestamp() + $this->getConfig()[ 'cacheSlamsTimeout' ] < time()){
92+
/**
93+
* The timeout has been reached
94+
* Consider that the batch has
95+
* failed and serve an empty item
96+
* to avoid to get stuck with a
97+
* batch item stored in driver
98+
*/
99+
goto getItemDriverExpired;
100+
}
101+
/**
102+
* @eventName CacheGetItem
103+
* @param $this ExtendedCacheItemPoolInterface
104+
* @param $driverData ItemBatch
105+
* @param $cacheSlamsSpendSeconds int
106+
*/
107+
$this->eventManager->dispatch('CacheGetItemInSlamBatch', $this, $driverData, $cacheSlamsSpendSeconds);
108+
109+
/**
110+
* Wait for a second before
111+
* attempting to get exit
112+
* the current batch process
113+
*/
114+
sleep(1);
115+
$cacheSlamsSpendSeconds++;
116+
goto getItemDriverRead;
117+
}
118+
}
119+
120+
$item->set($driverData);
121+
$item->expiresAt($this->driverUnwrapEdate($driverArray));
95122

96-
$item->setTags($this->driverUnwrapTags($driverArray));
97-
if ($item->isExpired()) {
98-
/**
99-
* Using driverDelete() instead of delete()
100-
* to avoid infinite loop caused by
101-
* getItem() call in delete() method
102-
* As we MUST return an item in any
103-
* way, we do not de-register here
104-
*/
105-
$this->driverDelete($item);
106-
107-
/**
108-
* Reset the Item
109-
*/
110-
$item->set(null)
111-
->expiresAfter(abs((int) $this->getConfig()[ 'defaultTtl' ]))
112-
->setHit(false)
113-
->setTags([]);
114123
if($this->config['itemDetailedDate']){
115124

116125
/**
117126
* If the itemDetailedDate has been
118127
* set after caching, we MUST inject
119128
* a new DateTime object on the fly
120129
*/
121-
$item->setCreationDate(new \DateTime());
122-
$item->setModificationDate(new \DateTime());
130+
$item->setCreationDate($this->driverUnwrapCdate($driverArray) ?: new \DateTime());
131+
$item->setModificationDate($this->driverUnwrapMdate($driverArray) ?: new \DateTime());
132+
}
133+
134+
$item->setTags($this->driverUnwrapTags($driverArray));
135+
136+
getItemDriverExpired:
137+
if ($item->isExpired()) {
138+
/**
139+
* Using driverDelete() instead of delete()
140+
* to avoid infinite loop caused by
141+
* getItem() call in delete() method
142+
* As we MUST return an item in any
143+
* way, we do not de-register here
144+
*/
145+
$this->driverDelete($item);
146+
147+
/**
148+
* Reset the Item
149+
*/
150+
$item->set(null)
151+
->expiresAfter(abs((int) $this->getConfig()[ 'defaultTtl' ]))
152+
->setHit(false)
153+
->setTags([]);
154+
if($this->config['itemDetailedDate']){
155+
156+
/**
157+
* If the itemDetailedDate has been
158+
* set after caching, we MUST inject
159+
* a new DateTime object on the fly
160+
*/
161+
$item->setCreationDate(new \DateTime());
162+
$item->setModificationDate(new \DateTime());
163+
}
164+
} else {
165+
$item->setHit(true);
123166
}
124-
} else {
125-
$item->setHit(true);
167+
}else{
168+
$item->expiresAfter(abs((int) $this->getConfig()[ 'defaultTtl' ]));
126169
}
127-
}else{
128-
$item->expiresAfter(abs((int) $this->getConfig()[ 'defaultTtl' ]));
129170
}
130-
131171
}
132172
} else {
133173
throw new phpFastCacheInvalidArgumentException(sprintf('$key must be a string, got type "%s" instead.', gettype($key)));
@@ -277,6 +317,28 @@ public function save(CacheItemInterface $item)
277317
*/
278318
$this->eventManager->dispatch('CacheSaveItem', $this, $item);
279319

320+
321+
if($this->getConfig()[ 'preventCacheSlams' ]){
322+
/**
323+
* @var $itemBatch ExtendedCacheItemInterface
324+
*/
325+
$class = new \ReflectionClass((new \ReflectionObject($this))->getNamespaceName() . '\Item');
326+
$itemBatch = $class->newInstanceArgs([$this, $item->getKey()]);
327+
$itemBatch->setEventManager($this->eventManager)
328+
->set(new ItemBatch($item->getKey(), new \DateTime()))
329+
->expiresAfter($this->getConfig()[ 'cacheSlamsTimeout' ]);
330+
331+
/**
332+
* To avoid SPL mismatches
333+
* we have to re-attach the
334+
* original item to the pool
335+
*/
336+
$this->driverWrite($itemBatch);
337+
$this->detachItem($itemBatch);
338+
$this->attachItem($item);
339+
}
340+
341+
280342
if ($this->driverWrite($item) && $this->driverWriteTags($item)) {
281343
$item->setHit(true);
282344
CacheManager::$WriteHits++;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
/**
3+
*
4+
* This file is part of phpFastCache.
5+
*
6+
* @license MIT License (MIT)
7+
*
8+
* For full copyright and license information, please see the docs/CREDITS.txt file.
9+
*
10+
* @author Khoa Bui (khoaofgod) <[email protected]> http://www.phpfastcache.com
11+
* @author Georges.L (Geolim4) <[email protected]>
12+
*
13+
*/
14+
namespace phpFastCache\Entities;
15+
16+
use phpFastCache\Exceptions\phpFastCacheInvalidArgumentException;
17+
18+
/**
19+
* Class ItemBatch
20+
* @package phpFastCache\Entities
21+
*/
22+
class ItemBatch
23+
{
24+
/**
25+
* @var string
26+
*/
27+
protected $itemKey;
28+
29+
/**
30+
* @var \DateTime
31+
*/
32+
protected $itemDate;
33+
34+
/**
35+
* ItemBatch constructor.
36+
* @param $itemKey
37+
* @param \DateTime $itemDate
38+
* @throws \phpFastCache\Exceptions\phpFastCacheInvalidArgumentException
39+
*/
40+
public function __construct($itemKey, \DateTime $itemDate)
41+
{
42+
if(is_string($itemKey)){
43+
$this->itemKey = $itemKey;
44+
$this->itemDate = $itemDate;
45+
}else{
46+
throw new phpFastCacheInvalidArgumentException(sprintf('$itemKey must be a string, got "%s" instead', gettype($itemKey)));
47+
}
48+
}
49+
50+
/**
51+
* @return string
52+
*/
53+
public function getItemKey()
54+
{
55+
return $this->itemKey;
56+
}
57+
58+
/**
59+
* @return \DateTime
60+
*/
61+
public function getItemDate()
62+
{
63+
return $this->itemDate;
64+
}
65+
}

src/phpFastCache/Helper/TestHelper.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,28 @@ public function printText($string, $strtoupper = false)
118118
return $this;
119119
}
120120

121+
/**
122+
* @param string $cmd
123+
*/
124+
public function runAsyncProcess($cmd)
125+
{
126+
if (substr(php_uname(), 0, 7) === 'Windows'){
127+
pclose(popen('start /B '. $cmd, 'r'));
128+
}
129+
else {
130+
exec($cmd . ' > /dev/null &');
131+
}
132+
}
133+
134+
/**
135+
* @param string $file
136+
* @param string $ext
137+
*/
138+
public function runSubProcess($file, $ext = '.php')
139+
{
140+
$this->runAsyncProcess('php ' . getcwd() . DIRECTORY_SEPARATOR . 'subprocess' . DIRECTORY_SEPARATOR . $file . '.subprocess' . $ext);
141+
}
142+
121143
/**
122144
* @return void
123145
*/
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/**
4+
* @author Khoa Bui (khoaofgod) <[email protected]> http://www.phpfastcache.com
5+
* @author Georges.L (Geolim4) <[email protected]>
6+
*/
7+
8+
use phpFastCache\CacheManager;
9+
use phpFastCache\Core\Item\ExtendedCacheItemInterface;
10+
use phpFastCache\Core\Pool\ExtendedCacheItemPoolInterface;
11+
use phpFastCache\Entities\ItemBatch;
12+
use phpFastCache\EventManager;
13+
use phpFastCache\Helper\TestHelper;
14+
15+
chdir(__DIR__);
16+
require_once __DIR__ . '/../src/autoload.php';
17+
$testHelper = new TestHelper('Cache Slams Protection');
18+
$defaultDriver = (!empty($argv[ 1 ]) ? ucfirst($argv[ 1 ]) : 'Files');
19+
$driverInstance = CacheManager::getInstance($defaultDriver, [
20+
'preventCacheSlams' => true,
21+
'cacheSlamsTimeout' => 20
22+
]);
23+
24+
EventManager::getInstance()->onCacheGetItemInSlamBatch(function(ExtendedCacheItemPoolInterface $itemPool, ItemBatch $driverData, $cacheSlamsSpendSeconds) use ($testHelper){
25+
$testHelper->printText("Looping in batch for {$cacheSlamsSpendSeconds} second(s) with a batch from " . $driverData->getItemDate()->format(\DateTime::W3C));
26+
});
27+
28+
$testHelper->runSubProcess('CacheSlamsProtection');
29+
/**
30+
* Give some time to the
31+
* subprocess to start
32+
* just like a concurrent
33+
* php request
34+
*/
35+
usleep(mt_rand(250000, 800000));
36+
37+
$item = $driverInstance->getItem('TestCacheSlamsProtection');
38+
39+
/**
40+
* @see CacheSlamsProtection.subprocess.php:28
41+
*/
42+
if($item->isHit() && $item->get() === 1337){
43+
$testHelper->printPassText('The batch has expired and returned a non-empty item with expected value: ' . $item->get());
44+
}else{
45+
$testHelper->printFailText('The batch has expired and returned an empty item with expected value: ' . print_r($item->get(), true));
46+
}
47+
48+
/**
49+
* Cleanup the driver
50+
*/
51+
$driverInstance->deleteItem($item->getKey());
52+
53+
$testHelper->terminateTest();
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/**
4+
* @author Khoa Bui (khoaofgod) <[email protected]> http://www.phpfastcache.com
5+
* @author Georges.L (Geolim4) <[email protected]>
6+
*/
7+
8+
use phpFastCache\CacheManager;
9+
use phpFastCache\Entities\ItemBatch;
10+
11+
chdir(__DIR__);
12+
require_once __DIR__ . '/../../src/autoload.php';
13+
14+
$driverInstance = CacheManager::getInstance('Files', [
15+
'preventCacheSlams' => true,
16+
'cacheSlamsTimeout' => 15
17+
]);
18+
19+
/**
20+
* Emulate an active ItemBatch
21+
*/
22+
$batchItem = $driverInstance->getItem('TestCacheSlamsProtection');
23+
$batchItem->set(new ItemBatch($batchItem->getKey(), new \DateTime()))->expiresAfter(3600);
24+
$driverInstance->save($batchItem);
25+
26+
sleep(mt_rand(5, 15));
27+
28+
$batchItem->set(1337);
29+
$driverInstance->save($batchItem);
30+
31+
exit(0);

0 commit comments

Comments
 (0)