diff --git a/cache/stores/session/lib.php b/cache/stores/session/lib.php index 18e0ccfa73c..82215023ef6 100644 --- a/cache/stores/session/lib.php +++ b/cache/stores/session/lib.php @@ -116,6 +116,18 @@ class cachestore_session extends session_data_store implements cache_is_key_awar */ protected $ttl = 0; + /** + * The maximum size for the store, or false if there isn't one. + * @var bool + */ + protected $maxsize = false; + + /** + * The number of items currently being stored. + * @var int + */ + protected $storecount = 0; + /** * Constructs the store instance. * @@ -192,6 +204,12 @@ class cachestore_session extends session_data_store implements cache_is_key_awar $this->storeid = $definition->generate_definition_hash(); $this->store = &self::register_store_id($definition->get_id()); $this->ttl = $definition->get_ttl(); + $maxsize = $definition->get_maxsize(); + if ($maxsize !== null) { + // Must be a positive int. + $this->maxsize = abs((int)$maxsize); + $this->storecount = count($this->store); + } } /** @@ -261,14 +279,25 @@ class cachestore_session extends session_data_store implements cache_is_key_awar * * @param string $key The key to use. * @param mixed $data The data to set. + * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required. * @return bool True if the operation was a success false otherwise. */ - public function set($key, $data) { + public function set($key, $data, $testmaxsize = true) { + $testmaxsize = ($testmaxsize && $this->maxsize !== false); + if ($testmaxsize) { + $increment = (!isset($this->store[$key])); + } if ($this->ttl == 0) { $this->store[$key][0] = $data; } else { $this->store[$key] = array($data, cache::now()); } + if ($testmaxsize && $increment) { + $this->storecount++; + if ($this->storecount > $this->maxsize) { + $this->reduce_for_maxsize(); + } + } return true; } @@ -283,9 +312,15 @@ class cachestore_session extends session_data_store implements cache_is_key_awar public function set_many(array $keyvaluearray) { $count = 0; foreach ($keyvaluearray as $pair) { - $this->set($pair['key'], $pair['value']); + $this->set($pair['key'], $pair['value'], false); $count++; } + if ($this->maxsize !== false) { + $this->storecount += $count; + if ($this->storecount > $this->maxsize) { + $this->reduce_for_maxsize(); + } + } return $count; } @@ -356,6 +391,9 @@ class cachestore_session extends session_data_store implements cache_is_key_awar public function delete($key) { $result = isset($this->store[$key]); unset($this->store[$key]); + if ($this->maxsize !== false) { + $this->storecount--; + } return $result; } @@ -373,6 +411,9 @@ class cachestore_session extends session_data_store implements cache_is_key_awar } unset($this->store[$key]); } + if ($this->maxsize !== false) { + $this->storecount -= $count; + } return $count; } @@ -383,9 +424,34 @@ class cachestore_session extends session_data_store implements cache_is_key_awar */ public function purge() { $this->store = array(); + // Don't worry about checking if we're using max size just set it as thats as fast as the check. + $this->storecount = 0; return true; } + /** + * Reduces the size of the array if maxsize has been hit. + * + * This function reduces the size of the store reducing it by 10% of its maxsize. + * It removes the oldest items in the store when doing this. + * The reason it does this an doesn't use a least recently used system is purely the overhead such a system + * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches + * and avoiding more is of benefit. + * + * @return int + */ + protected function reduce_for_maxsize() { + $diff = $this->storecount - $this->maxsize; + if ($diff < 1) { + return 0; + } + // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop. + $diff += floor($this->maxsize / 10); + $this->store = array_slice($this->store, $diff, null, true); + $this->storecount -= $diff; + return $diff; + } + /** * Returns true if the user can add an instance of the store plugin. * @@ -406,7 +472,7 @@ class cachestore_session extends session_data_store implements cache_is_key_awar * Generates an instance of the cache store that can be used for testing. * * @param cache_definition $definition - * @return false + * @return cachestore_session */ public static function initialise_test_instance(cache_definition $definition) { // Do something here perhaps. diff --git a/cache/stores/session/tests/session_test.php b/cache/stores/session/tests/session_test.php index bec028995f5..3ee20f2e2d1 100644 --- a/cache/stores/session/tests/session_test.php +++ b/cache/stores/session/tests/session_test.php @@ -44,4 +44,76 @@ class cachestore_session_test extends cachestore_tests { protected function get_class_name() { return 'cachestore_session'; } + + /** + * Test the maxsize option. + */ + public function test_maxsize() { + $defid = 'phpunit/testmaxsize'; + $config = cache_config_phpunittest::instance(); + $config->phpunit_add_definition($defid, array( + 'mode' => cache_store::MODE_SESSION, + 'component' => 'phpunit', + 'area' => 'testmaxsize', + 'maxsize' => 3 + )); + $definition = cache_definition::load($defid, $config->get_definition_by_id($defid)); + $instance = cachestore_session::initialise_test_instance($definition); + + $this->assertTrue($instance->set('key1', 'value1')); + $this->assertTrue($instance->set('key2', 'value2')); + $this->assertTrue($instance->set('key3', 'value3')); + + $this->assertTrue($instance->has('key1')); + $this->assertTrue($instance->has('key2')); + $this->assertTrue($instance->has('key3')); + + $this->assertTrue($instance->set('key4', 'value4')); + $this->assertTrue($instance->set('key5', 'value5')); + + $this->assertFalse($instance->has('key1')); + $this->assertFalse($instance->has('key2')); + $this->assertTrue($instance->has('key3')); + $this->assertTrue($instance->has('key4')); + $this->assertTrue($instance->has('key5')); + + $this->assertFalse($instance->get('key1')); + $this->assertFalse($instance->get('key2')); + $this->assertEquals('value3', $instance->get('key3')); + $this->assertEquals('value4', $instance->get('key4')); + $this->assertEquals('value5', $instance->get('key5')); + + // Test adding one more. + $this->assertTrue($instance->set('key6', 'value6')); + $this->assertFalse($instance->get('key3')); + + // Test reducing and then adding to make sure we don't lost one. + $this->assertTrue($instance->delete('key6')); + $this->assertTrue($instance->set('key7', 'value7')); + $this->assertEquals('value4', $instance->get('key4')); + + // Set the same key three times to make sure it doesn't count overrides. + for ($i = 0; $i < 3; $i++) { + $this->assertTrue($instance->set('key8', 'value8')); + } + $this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size'); + + // Test adding many. + $this->assertEquals(3, $instance->set_many(array( + array('key' => 'keyA', 'value' => 'valueA'), + array('key' => 'keyB', 'value' => 'valueB'), + array('key' => 'keyC', 'value' => 'valueC') + ))); + $this->assertEquals(array( + 'key4' => false, + 'key5' => false, + 'key6' => false, + 'key7' => false, + 'keyA' => 'valueA', + 'keyB' => 'valueB', + 'keyC' => 'valueC' + ), $instance->get_many(array( + 'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC' + ))); + } } \ No newline at end of file diff --git a/cache/stores/static/lib.php b/cache/stores/static/lib.php index 65c76db3306..e54ee838466 100644 --- a/cache/stores/static/lib.php +++ b/cache/stores/static/lib.php @@ -112,6 +112,18 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, */ protected $ttl = 0; + /** + * The maximum size for the store, or false if there isn't one. + * @var bool + */ + protected $maxsize = false; + + /** + * The number of items currently being stored. + * @var int + */ + protected $storecount = 0; + /** * Constructs the store instance. * @@ -188,6 +200,12 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, $this->storeid = $definition->generate_definition_hash(); $this->store = &self::register_store_id($this->storeid); $this->ttl = $definition->get_ttl(); + $maxsize = $definition->get_maxsize(); + if ($maxsize !== null) { + // Must be a positive int. + $this->maxsize = abs((int)$maxsize); + $this->storecount = count($this->store); + } } /** @@ -257,14 +275,25 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, * * @param string $key The key to use. * @param mixed $data The data to set. + * @param bool $testmaxsize If set to true then we test the maxsize arg and reduce if required. * @return bool True if the operation was a success false otherwise. */ - public function set($key, $data) { + public function set($key, $data, $testmaxsize = true) { + $testmaxsize = ($testmaxsize && $this->maxsize !== false); + if ($testmaxsize) { + $increment = (!isset($this->store[$key])); + } if ($this->ttl == 0) { $this->store[$key][0] = $data; } else { $this->store[$key] = array($data, cache::now()); } + if ($testmaxsize && $increment) { + $this->storecount++; + if ($this->storecount > $this->maxsize) { + $this->reduce_for_maxsize(); + } + } return true; } @@ -279,9 +308,16 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, public function set_many(array $keyvaluearray) { $count = 0; foreach ($keyvaluearray as $pair) { - $this->set($pair['key'], $pair['value']); + // Don't test the maxsize here. We'll do it once when we are done. + $this->set($pair['key'], $pair['value'], false); $count++; } + if ($this->maxsize !== false) { + $this->storecount += $count; + if ($this->storecount > $this->maxsize) { + $this->reduce_for_maxsize(); + } + } return $count; } @@ -352,6 +388,9 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, public function delete($key) { $result = isset($this->store[$key]); unset($this->store[$key]); + if ($this->maxsize !== false) { + $this->storecount--; + } return $result; } @@ -369,6 +408,9 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, } unset($this->store[$key]); } + if ($this->maxsize !== false) { + $this->storecount -= $count; + } return $count; } @@ -380,9 +422,34 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, public function purge() { $this->flush_store_by_id($this->storeid); $this->store = &self::register_store_id($this->storeid); + // Don't worry about checking if we're using max size just set it as thats as fast as the check. + $this->storecount = 0; return true; } + /** + * Reduces the size of the array if maxsize has been hit. + * + * This function reduces the size of the store reducing it by 10% of its maxsize. + * It removes the oldest items in the store when doing this. + * The reason it does this an doesn't use a least recently used system is purely the overhead such a system + * requires. The current approach is focused on speed, MUC already adds enough overhead to static/session caches + * and avoiding more is of benefit. + * + * @return int + */ + protected function reduce_for_maxsize() { + $diff = $this->storecount - $this->maxsize; + if ($diff < 1) { + return 0; + } + // Reduce it by an extra 10% to avoid calling this repetitively if we are in a loop. + $diff += floor($this->maxsize / 10); + $this->store = array_slice($this->store, $diff, null, true); + $this->storecount -= $diff; + return $diff; + } + /** * Returns true if the user can add an instance of the store plugin. * @@ -403,7 +470,7 @@ class cachestore_static extends static_data_store implements cache_is_key_aware, * Generates an instance of the cache store that can be used for testing. * * @param cache_definition $definition - * @return false + * @return cachestore_static */ public static function initialise_test_instance(cache_definition $definition) { // Do something here perhaps. diff --git a/cache/stores/static/tests/static_test.php b/cache/stores/static/tests/static_test.php index b1ebc1990aa..c9795d176b8 100644 --- a/cache/stores/static/tests/static_test.php +++ b/cache/stores/static/tests/static_test.php @@ -1,5 +1,5 @@ phpunit_add_definition($defid, array( + 'mode' => cache_store::MODE_REQUEST, + 'component' => 'phpunit', + 'area' => 'testmaxsize', + 'maxsize' => 3 + )); + $definition = cache_definition::load($defid, $config->get_definition_by_id($defid)); + $instance = cachestore_static::initialise_test_instance($definition); + + $this->assertTrue($instance->set('key1', 'value1')); + $this->assertTrue($instance->set('key2', 'value2')); + $this->assertTrue($instance->set('key3', 'value3')); + + $this->assertTrue($instance->has('key1')); + $this->assertTrue($instance->has('key2')); + $this->assertTrue($instance->has('key3')); + + $this->assertTrue($instance->set('key4', 'value4')); + $this->assertTrue($instance->set('key5', 'value5')); + + $this->assertFalse($instance->has('key1')); + $this->assertFalse($instance->has('key2')); + $this->assertTrue($instance->has('key3')); + $this->assertTrue($instance->has('key4')); + $this->assertTrue($instance->has('key5')); + + $this->assertFalse($instance->get('key1')); + $this->assertFalse($instance->get('key2')); + $this->assertEquals('value3', $instance->get('key3')); + $this->assertEquals('value4', $instance->get('key4')); + $this->assertEquals('value5', $instance->get('key5')); + + // Test adding one more. + $this->assertTrue($instance->set('key6', 'value6')); + $this->assertFalse($instance->get('key3')); + + // Test reducing and then adding to make sure we don't lost one. + $this->assertTrue($instance->delete('key6')); + $this->assertTrue($instance->set('key7', 'value7')); + $this->assertEquals('value4', $instance->get('key4')); + + // Set the same key three times to make sure it doesn't count overrides. + for ($i = 0; $i < 3; $i++) { + $this->assertTrue($instance->set('key8', 'value8')); + } + + $this->assertEquals('value7', $instance->get('key7'), 'Overrides are incorrectly incrementing size'); + + // Test adding many. + $this->assertEquals(3, $instance->set_many(array( + array('key' => 'keyA', 'value' => 'valueA'), + array('key' => 'keyB', 'value' => 'valueB'), + array('key' => 'keyC', 'value' => 'valueC') + ))); + $this->assertEquals(array( + 'key4' => false, + 'key5' => false, + 'key6' => false, + 'key7' => false, + 'keyA' => 'valueA', + 'keyB' => 'valueB', + 'keyC' => 'valueC' + ), $instance->get_many(array( + 'key4', 'key5', 'key6', 'key7', 'keyA', 'keyB', 'keyC' + ))); + } } \ No newline at end of file