mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
Merge pull request #8095 from open-sausages/pulls/3/versioned-cache-segmentation
ENHANCEMENT: Add new CacheProxy to segment versioned state
This commit is contained in:
commit
9f57576975
14
cache/Cache.php
vendored
14
cache/Cache.php
vendored
@ -146,7 +146,7 @@ class SS_Cache {
|
|||||||
* @param string $frontend (optional) The type of Zend_Cache frontend
|
* @param string $frontend (optional) The type of Zend_Cache frontend
|
||||||
* @param array $frontendOptions (optional) Any frontend options to use.
|
* @param array $frontendOptions (optional) Any frontend options to use.
|
||||||
*
|
*
|
||||||
* @return Zend_Cache_Core The cache object
|
* @return CacheProxy|Zend_Cache_Core
|
||||||
*/
|
*/
|
||||||
public static function factory($for, $frontend='Output', $frontendOptions=null) {
|
public static function factory($for, $frontend='Output', $frontendOptions=null) {
|
||||||
self::init();
|
self::init();
|
||||||
@ -175,8 +175,8 @@ class SS_Cache {
|
|||||||
$backend = self::$backends[$backend_name];
|
$backend = self::$backends[$backend_name];
|
||||||
|
|
||||||
$basicOptions = array(
|
$basicOptions = array(
|
||||||
'cache_id_prefix' => md5(BASE_PATH) . '_' . $for . '_',
|
'cache_id_prefix' => md5(BASE_PATH) . '_' . $for . '_',
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($cache_lifetime >= 0) {
|
if ($cache_lifetime >= 0) {
|
||||||
$basicOptions['lifetime'] = $cache_lifetime;
|
$basicOptions['lifetime'] = $cache_lifetime;
|
||||||
@ -188,8 +188,14 @@ class SS_Cache {
|
|||||||
|
|
||||||
require_once 'Zend/Cache.php';
|
require_once 'Zend/Cache.php';
|
||||||
|
|
||||||
return Zend_Cache::factory(
|
$cache = Zend_Cache::factory(
|
||||||
$frontend, $backend[0], $frontendOptions, $backend[1]
|
$frontend, $backend[0], $frontendOptions, $backend[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!empty($frontendOptions['disable-segmentation'])) {
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Injector::inst()->createWithArgs('CacheProxy', array($cache));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
123
cache/CacheProxy.php
vendored
Normal file
123
cache/CacheProxy.php
vendored
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
require_once 'Zend/Cache.php';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A decorator for a Zend_Cache_Backend cache service that mutates cache keys
|
||||||
|
* dynamically depending on versioned state
|
||||||
|
*/
|
||||||
|
class CacheProxy extends Zend_Cache_Core {
|
||||||
|
/**
|
||||||
|
* @var Zend_Cache_Backend|Zend_Cache_Backend_ExtendedInterface
|
||||||
|
*/
|
||||||
|
protected $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CacheProxy constructor.
|
||||||
|
* @param Zend_Cache_Core $cache
|
||||||
|
*/
|
||||||
|
public function __construct(Zend_Cache_Core $cache) {
|
||||||
|
$this->cache = $cache;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDirectives($directives) {
|
||||||
|
$this->cache->setDirectives($directives);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConfig(Zend_Config $config) {
|
||||||
|
return $this->cache->setConfig($config);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBackend(Zend_Cache_Backend $backendObject) {
|
||||||
|
return $this->cache->setBackend($backendObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBackend() {
|
||||||
|
return $this->cache->getBackend();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOption($name, $value) {
|
||||||
|
$this->cache->setOption($name, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOption($name) {
|
||||||
|
return $this->cache->getOption($name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLifetime($newLifetime) {
|
||||||
|
return $this->cache->setLifetime($newLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIds() {
|
||||||
|
return $this->cache->getIds();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTags() {
|
||||||
|
return $this->cache->getTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdsMatchingTags($tags = array()) {
|
||||||
|
return $this->cache->getIdsMatchingTags($tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdsNotMatchingTags($tags = array()) {
|
||||||
|
return $this->cache->getIdsNotMatchingTags($tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIdsMatchingAnyTags($tags = array()) {
|
||||||
|
return $this->cache->getIdsMatchingAnyTags($tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFillingPercentage() {
|
||||||
|
return $this->cache->getFillingPercentage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMetadatas($id) {
|
||||||
|
return $this->cache->getMetadatas($this->getKeyID($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function touch($id, $extraLifetime) {
|
||||||
|
return $this->cache->touch($this->getKeyID($id), $extraLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load($id, $doNotTestCacheValidity = false, $doNotUnserialize = false) {
|
||||||
|
return $this->cache->load($this->getKeyID($id), $doNotTestCacheValidity, $doNotUnserialize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test($id) {
|
||||||
|
return $this->cache->test($this->getKeyID($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save($data, $id = null, $tags = array(), $specificLifetime = false, $priority = 8) {
|
||||||
|
return $this->cache->save(
|
||||||
|
$data,
|
||||||
|
$this->getKeyID($id),
|
||||||
|
$tags,
|
||||||
|
$specificLifetime,
|
||||||
|
$priority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove($id) {
|
||||||
|
return $this->cache->remove($this->getKeyID($id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) {
|
||||||
|
return $this->cache->clean($mode, $tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a dynamic key based on versioned state
|
||||||
|
* @param string $key
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getKeyID($key) {
|
||||||
|
$state = Versioned::get_reading_mode();
|
||||||
|
if ($state) {
|
||||||
|
return $key . '_' . md5($state);
|
||||||
|
}
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
}
|
@ -110,7 +110,7 @@ class SS_ConfigManifest {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a hook for mock unit tests despite no DI
|
* Provides a hook for mock unit tests despite no DI
|
||||||
* @return Zend_Cache_Frontend
|
* @return Zend_Cache_Core
|
||||||
*/
|
*/
|
||||||
protected function getCache()
|
protected function getCache()
|
||||||
{
|
{
|
||||||
@ -118,6 +118,7 @@ class SS_ConfigManifest {
|
|||||||
'automatic_serialization' => true,
|
'automatic_serialization' => true,
|
||||||
'lifetime' => null,
|
'lifetime' => null,
|
||||||
'cache_id_prefix' => 'SS_Configuration_',
|
'cache_id_prefix' => 'SS_Configuration_',
|
||||||
|
'disable-segmentation' => true,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,11 @@ class SilverStripeVersionProvider
|
|||||||
|
|
||||||
$lockData = array();
|
$lockData = array();
|
||||||
if ($cache) {
|
if ($cache) {
|
||||||
$cache = SS_Cache::factory('SilverStripeVersionProvider_composerlock');
|
$cache = SS_Cache::factory(
|
||||||
|
'SilverStripeVersionProvider_composerlock',
|
||||||
|
'Output',
|
||||||
|
array('disable-segmentation' => true)
|
||||||
|
);
|
||||||
$cacheKey = filemtime($composerLockPath);
|
$cacheKey = filemtime($composerLockPath);
|
||||||
if ($versions = $cache->load($cacheKey)) {
|
if ($versions = $cache->load($cacheKey)) {
|
||||||
$lockData = json_decode($versions, true);
|
$lockData = json_decode($versions, true);
|
||||||
|
@ -90,6 +90,20 @@ e.g. by including the `LastEdited` value when caching `DataObject` results.
|
|||||||
// set all caches to 3 hours
|
// set all caches to 3 hours
|
||||||
SS_Cache::set_cache_lifetime('any', 60*60*3);
|
SS_Cache::set_cache_lifetime('any', 60*60*3);
|
||||||
|
|
||||||
|
### Versioned cache segmentation
|
||||||
|
|
||||||
|
`SS_Cache` segments caches based on the versioned reading mode. This prevents developers
|
||||||
|
from caching draft data and then accidentally exposing it on the live stage without potentially
|
||||||
|
required authorisation checks. This segmentation is automatic for all caches generated using
|
||||||
|
`SS_Cache::factory` method.
|
||||||
|
|
||||||
|
Data that is not content sensitive can be cached across stages by simply opting out of the
|
||||||
|
segmented cache with the `disable-segmentation` argument.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cache = SS_Cache::factory('myapp', 'Output', array('disable-segmentation' => true));
|
||||||
|
```
|
||||||
|
|
||||||
## Alternative Cache Backends
|
## Alternative Cache Backends
|
||||||
|
|
||||||
By default, SilverStripe uses a file-based caching backend.
|
By default, SilverStripe uses a file-based caching backend.
|
||||||
|
26
docs/en/04_Changelogs/3.7.0.md
Normal file
26
docs/en/04_Changelogs/3.7.0.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# 3.7.0
|
||||||
|
|
||||||
|
### Versioned cache segmentation
|
||||||
|
|
||||||
|
`SS_Cache` now maintains separate cache pools for each versioned stage. This prevents developers from caching draft data and then accidentally exposing it on the live stage without potentially required authorisation checks. Unless you rely on caching across stages, you don't need to change your own code for this change to take effect. Note that cache keys will be internally rewritten, causing any existing cache items to become invalid when this change is deployed.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Before:
|
||||||
|
$cache = SS_Cache::factory('myapp');
|
||||||
|
Versioned::set_reading_mode('Stage.Stage');
|
||||||
|
$cache->save('Some draft content. Not for public viewing yet.', 'my_key');
|
||||||
|
Versioned::set_reading_mode('Stage.Live');
|
||||||
|
$cache->load('my_key'); // 'Some draft content. Not for public viewing yet'
|
||||||
|
|
||||||
|
// After:
|
||||||
|
$cache = SS_Cache::factory('myapp');
|
||||||
|
Versioned::set_reading_mode('Stage.Stage');
|
||||||
|
$cache->save('Some draft content. Not for public viewing yet.', 'my_key');
|
||||||
|
Versioned::set_reading_mode('Stage.Live');
|
||||||
|
$cache->load('my_key'); // null
|
||||||
|
```
|
||||||
|
Data that is not content sensitive can be cached across stages by simply opting out of the segmented cache with the `disable-segmentation` argument.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$cache = SS_Cache::factory('myapp', 'Output', array('disable-segmentation' => true));
|
||||||
|
```
|
@ -39,7 +39,7 @@ class GDBackend extends SS_Object implements Image_Backend {
|
|||||||
// If we're working with image resampling, things could take a while. Bump up the time-limit
|
// If we're working with image resampling, things could take a while. Bump up the time-limit
|
||||||
increase_time_limit_to(300);
|
increase_time_limit_to(300);
|
||||||
|
|
||||||
$this->cache = SS_Cache::factory('GDBackend_Manipulations');
|
$this->cache = SS_Cache::factory('GDBackend_Manipulations', 'Output', array('disable-segmentation' => true));
|
||||||
|
|
||||||
if($filename && is_readable($filename)) {
|
if($filename && is_readable($filename)) {
|
||||||
$this->cacheKey = md5(implode('_', array($filename, filemtime($filename))));
|
$this->cacheKey = md5(implode('_', array($filename, filemtime($filename))));
|
||||||
|
@ -118,10 +118,18 @@ class i18n extends SS_Object implements TemplateGlobalProvider, Flushable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an instance of the cache used for i18n data.
|
* Return an instance of the cache used for i18n data.
|
||||||
* @return Zend_Cache
|
* @return Zend_Cache_Core
|
||||||
*/
|
*/
|
||||||
public static function get_cache() {
|
public static function get_cache() {
|
||||||
return SS_Cache::factory('i18n', 'Output', array('lifetime' => null, 'automatic_serialization' => true));
|
return SS_Cache::factory(
|
||||||
|
'i18n',
|
||||||
|
'Output',
|
||||||
|
array(
|
||||||
|
'lifetime' => null,
|
||||||
|
'automatic_serialization' => true,
|
||||||
|
'disable-segmentation' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,7 +35,7 @@ class CleanImageManipulationCache extends BuildTask {
|
|||||||
$images = DataObject::get('Image');
|
$images = DataObject::get('Image');
|
||||||
|
|
||||||
if($images && Image::get_backend() == "GDBackend") {
|
if($images && Image::get_backend() == "GDBackend") {
|
||||||
$cache = SS_Cache::factory('GDBackend_Manipulations');
|
$cache = SS_Cache::factory('GDBackend_Manipulations', 'Output', array('disable-segmentation' => true));
|
||||||
|
|
||||||
foreach($images as $image) {
|
foreach($images as $image) {
|
||||||
$path = $image->getFullPath();
|
$path = $image->getFullPath();
|
||||||
|
75
tests/cache/CacheTest.php
vendored
75
tests/cache/CacheTest.php
vendored
@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
class CacheTest extends SapphireTest {
|
class CacheTest extends SapphireTest {
|
||||||
|
|
||||||
public function testCacheBasics() {
|
public function setUpOnce() {
|
||||||
|
parent::setUpOnce();
|
||||||
|
Versioned::set_reading_mode('Stage.Live');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCacheBasics() {
|
||||||
$cache = SS_Cache::factory('test');
|
$cache = SS_Cache::factory('test');
|
||||||
|
|
||||||
$cache->save('Good', 'cachekey');
|
$cache->save('Good', 'cachekey');
|
||||||
@ -64,5 +69,73 @@ class CacheTest extends SapphireTest {
|
|||||||
$this->assertEquals(1200, $cache->getOption('lifetime'));
|
$this->assertEquals(1200, $cache->getOption('lifetime'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testVersionedCacheSegmentation() {
|
||||||
|
$cacheInstance = SS_Cache::factory('versioned');
|
||||||
|
$cacheInstance->clean();
|
||||||
|
|
||||||
|
Versioned::set_reading_mode('Stage.Live');
|
||||||
|
$result = $cacheInstance->load('shared_key');
|
||||||
|
$this->assertFalse($result);
|
||||||
|
$cacheInstance->save('uncle', 'shared_key');
|
||||||
|
// Shared key is cached on LIVE
|
||||||
|
$this->assertEquals('uncle', $cacheInstance->load('shared_key'));
|
||||||
|
|
||||||
|
Versioned::set_reading_mode('Stage.Stage');
|
||||||
|
|
||||||
|
// Shared key does not exist on STAGE
|
||||||
|
$this->assertFalse($cacheInstance->load('shared_key'));
|
||||||
|
|
||||||
|
$cacheInstance->save('cheese', 'shared_key');
|
||||||
|
$cacheInstance->save('bar', 'stage_key');
|
||||||
|
|
||||||
|
// Shared key has its own value on STAGE
|
||||||
|
$this->assertEquals('cheese', $cacheInstance->load('shared_key'));
|
||||||
|
// New key is cached on STAGE
|
||||||
|
$this->assertEquals('bar', $cacheInstance->load('stage_key'));
|
||||||
|
|
||||||
|
Versioned::set_reading_mode('Stage.Live');
|
||||||
|
|
||||||
|
// New key does not exist on LIVE
|
||||||
|
$this->assertFalse($cacheInstance->load('stage_key'));
|
||||||
|
// Shared key retains its own value on LIVE
|
||||||
|
$this->assertEquals('uncle', $cacheInstance->load('shared_key'));
|
||||||
|
|
||||||
|
$cacheInstance->clean();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDisableVersionedCacheSegmentation() {
|
||||||
|
$cacheInstance = SS_Cache::factory('versioned_disabled', 'Output', array('disable-segmentation' => true));
|
||||||
|
$cacheInstance->clean();
|
||||||
|
|
||||||
|
Versioned::set_reading_mode('Stage.Live');
|
||||||
|
$result = $cacheInstance->load('shared_key');
|
||||||
|
$this->assertFalse($result);
|
||||||
|
$cacheInstance->save('uncle', 'shared_key');
|
||||||
|
// Shared key is cached on LIVE
|
||||||
|
$this->assertEquals('uncle', $cacheInstance->load('shared_key'));
|
||||||
|
|
||||||
|
Versioned::set_reading_mode('Stage.Stage');
|
||||||
|
|
||||||
|
// Shared key is same on STAGE
|
||||||
|
$this->assertEquals('uncle', $cacheInstance->load('shared_key'));
|
||||||
|
|
||||||
|
$cacheInstance->save('cheese', 'shared_key');
|
||||||
|
$cacheInstance->save('bar', 'stage_key');
|
||||||
|
|
||||||
|
// Shared key is overwritten on STAGE
|
||||||
|
$this->assertEquals('cheese', $cacheInstance->load('shared_key'));
|
||||||
|
// New key is written on STAGE
|
||||||
|
$this->assertEquals('bar', $cacheInstance->load('stage_key'));
|
||||||
|
|
||||||
|
Versioned::set_reading_mode('Stage.Live');
|
||||||
|
// New key has same value on LIVE
|
||||||
|
$this->assertEquals('bar', $cacheInstance->load('stage_key'));
|
||||||
|
// New value for existing key is same on LIVE
|
||||||
|
$this->assertEquals('cheese', $cacheInstance->load('shared_key'));
|
||||||
|
|
||||||
|
$cacheInstance->clean();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,13 +111,21 @@ class DataObjectDuplicationTest extends SapphireTest {
|
|||||||
"Match between relation of copy and the original");
|
"Match between relation of copy and the original");
|
||||||
$this->assertEquals(0, $oneCopy->twos()->Count(),
|
$this->assertEquals(0, $oneCopy->twos()->Count(),
|
||||||
"Many-to-one relation not copied (has_many)");
|
"Many-to-one relation not copied (has_many)");
|
||||||
$this->assertEquals($three->ID, $oneCopy->threes()->First()->ID,
|
$this->assertContains(
|
||||||
"Match between relation of copy and the original");
|
$three->ID,
|
||||||
$this->assertEquals($one->ID, $threeCopy->ones()->First()->ID,
|
$oneCopy->threes()->column('ID'),
|
||||||
"Match between relation of copy and the original");
|
"Match between relation of copy and the original"
|
||||||
|
);
|
||||||
$this->assertEquals('three', $oneCopy->threes()->First()->TestExtra,
|
$this->assertContains(
|
||||||
"Match between extra field of copy and the original");
|
$one->ID,
|
||||||
|
$threeCopy->ones()->column('ID'),
|
||||||
|
"Match between relation of copy and the original"
|
||||||
|
);
|
||||||
|
$this->assertContains(
|
||||||
|
'three',
|
||||||
|
$oneCopy->threes()->column('TestExtra'),
|
||||||
|
"Match between extra field of copy and the original"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1016,6 +1016,14 @@ class SSViewer implements Flushable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Zend_Cache_Core
|
||||||
|
*/
|
||||||
|
protected static function defaultPartialCacheStore()
|
||||||
|
{
|
||||||
|
return SS_Cache::factory('cacheblock', 'Output', array('disable-segmentation' => true));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call this to disable rewriting of <a href="#xxx"> links. This is useful in Ajax applications.
|
* Call this to disable rewriting of <a href="#xxx"> links. This is useful in Ajax applications.
|
||||||
* It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
|
* It returns the SSViewer objects, so that you can call new SSViewer("X")->dontRewriteHashlinks()->process();
|
||||||
@ -1082,7 +1090,7 @@ class SSViewer implements Flushable {
|
|||||||
*/
|
*/
|
||||||
public static function flush_cacheblock_cache($force = false) {
|
public static function flush_cacheblock_cache($force = false) {
|
||||||
if (!self::$cacheblock_cache_flushed || $force) {
|
if (!self::$cacheblock_cache_flushed || $force) {
|
||||||
$cache = SS_Cache::factory('cacheblock');
|
$cache = self::defaultPartialCacheStore();
|
||||||
$backend = $cache->getBackend();
|
$backend = $cache->getBackend();
|
||||||
|
|
||||||
if(
|
if(
|
||||||
@ -1120,7 +1128,7 @@ class SSViewer implements Flushable {
|
|||||||
* @return Zend_Cache_Core
|
* @return Zend_Cache_Core
|
||||||
*/
|
*/
|
||||||
public function getPartialCacheStore() {
|
public function getPartialCacheStore() {
|
||||||
return $this->partialCacheStore ? $this->partialCacheStore : SS_Cache::factory('cacheblock');
|
return $this->partialCacheStore ? $this->partialCacheStore : self::defaultPartialCacheStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user