/** * Removes expired elements. * @return int number of removed elements */ protected function check_ttl() { if ($this->ttl === 0) { return 0; } $maxtime = cache::now() - $this->ttl; $count = 0; for ($value = reset($this->store); $value !== false; $value = next($this->store)) { if ($value[1] >= $maxtime) { // We know that elements are sorted by ttl so no need to continue. break; } $count++; } if ($count) { // Remove first $count elements as they are expired. $this->store = array_slice($this->store, $count, null, true); if ($this->maxsize !== false) { $this->storecount -= $count; } } return $count; }
/** * Purges a cache of all information on a given event. * * @param string $event */ public static function purge_by_event($event) { $instance = cache_config::instance(); $invalidationeventset = false; $factory = cache_factory::instance(); foreach ($instance->get_definitions() as $name => $definitionarr) { $definition = cache_definition::load($name, $definitionarr); if ($definition->invalidates_on_event($event)) { // Create the cache. $cache = $factory->create_cache($definition); // Initialise, in case of a store. if ($cache instanceof cache_store) { $cache->initialise($definition); } // Purge the cache. $cache->purge(); // We need to flag the event in the "Event invalidation" cache if it hasn't already happened. if ($invalidationeventset === false) { // Get the event invalidation cache. $cache = cache::make('core', 'eventinvalidation'); // Create a key to invalidate all. $data = array('purged' => cache::now()); // Set that data back to the cache. $cache->set($event, $data); // This only needs to occur once. $invalidationeventset = true; } } } }
/** * Override the cache::construct method. * * This function gets overriden so that we can process any invalidation events if need be. * If the definition doesn't have any invalidation events then this occurs exactly as it would for the cache class. * Otherwise we look at the last invalidation time and then check the invalidation data for events that have occured * between then now. * * You should not call this method from your code, instead you should use the cache::make methods. * * @param cache_definition $definition * @param cache_store $store * @param cache_loader|cache_data_source $loader */ public function __construct(cache_definition $definition, cache_store $store, $loader = null) { // First up copy the loadeduserid to the current user id. $this->currentuserid = self::$loadeduserid; parent::__construct($definition, $store, $loader); // This will trigger check tracked user. If this gets removed a call to that will need to be added here in its place. $this->set(self::LASTACCESS, cache::now()); if ($definition->has_invalidation_events()) { $lastinvalidation = $this->get('lastsessioninvalidation'); if ($lastinvalidation === false) { // This is a new session, there won't be anything to invalidate. Set the time of the last invalidation and // move on. $this->set('lastsessioninvalidation', cache::now()); return; } else { if ($lastinvalidation == cache::now()) { // We've already invalidated during this request. return; } } // Get the event invalidation cache. $cache = cache::make('core', 'eventinvalidation'); $events = $cache->get_many($definition->get_invalidation_events()); $todelete = array(); $purgeall = false; // Iterate the returned data for the events. foreach ($events as $event => $keys) { if ($keys === false) { // No data to be invalidated yet. continue; } // Look at each key and check the timestamp. foreach ($keys as $key => $timestamp) { // If the timestamp of the event is more than or equal to the last invalidation (happened between the last // invalidation and now)then we need to invaliate the key. if ($timestamp >= $lastinvalidation) { if ($key === 'purged') { $purgeall = true; break; } else { $todelete[] = $key; } } } } if ($purgeall) { $this->purge(); } else { if (!empty($todelete)) { $todelete = array_unique($todelete); $this->delete_many($todelete); } } // Set the time of the last invalidation. $this->set('lastsessioninvalidation', cache::now()); } }
/** * Returns true if the data has expired. * @return int */ public function has_expired() { return $this->expires < cache::now(); }
/** * Purges a cache of all information on a given event. * * @param string $event */ public static function purge_by_event($event) { $instance = cache_config::instance(); $invalidationeventset = false; $factory = cache_factory::instance(); $inuse = $factory->get_caches_in_use(); foreach ($instance->get_definitions() as $name => $definitionarr) { $definition = cache_definition::load($name, $definitionarr); if ($definition->invalidates_on_event($event)) { // First up check if there is a cache loader for this definition already. // If there is we need to invalidate the keys from there. $definitionkey = $definition->get_component() . '/' . $definition->get_area(); if (isset($inuse[$definitionkey])) { $inuse[$definitionkey]->purge(); } else { cache::make($definition->get_component(), $definition->get_area())->purge(); } // We should only log events for application and session caches. // Request caches shouldn't have events as all data is lost at the end of the request. // Events should only be logged once of course and likely several definitions are watching so we // track its logging with $invalidationeventset. $logevent = $invalidationeventset === false && $definition->get_mode() !== cache_store::MODE_REQUEST; // We need to flag the event in the "Event invalidation" cache if it hasn't already happened. if ($logevent && $invalidationeventset === false) { // Get the event invalidation cache. $cache = cache::make('core', 'eventinvalidation'); // Create a key to invalidate all. $data = array('purged' => cache::now()); // Set that data back to the cache. $cache->set($event, $data); // This only needs to occur once. $invalidationeventset = true; } } } }
/** * Tests application cache event invalidation over a distributed setup. */ public function test_distributed_application_event_invalidation() { global $CFG; // This is going to be an intense wee test. // We need to add data the to cache, invalidate it by event, manually force it back without MUC knowing to simulate a // disconnected/distributed setup (think load balanced server using local cache), instantiate the cache again and finally // check that it is not picked up. $instance = cache_config_phpunittest::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array('mode' => cache_store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true, 'invalidationevents' => array('crazyevent'))); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); cache_helper::invalidate_by_event('crazyevent', array('testkey1')); $this->assertFalse($cache->get('testkey1')); // OK data added, data invalidated, and invalidation time has been set. // Now we need to manually add back the data and adjust the invalidation time. $hash = md5(cache_store::MODE_APPLICATION . '/phpunit/eventinvalidationtest/' . $CFG->wwwroot . 'phpunit'); $timefile = $CFG->dataroot . "/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las/lastinvalidation-{$hash}.cache"; // Make sure the file is correct. $this->assertTrue(file_exists($timefile)); $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate. make_writable_directory(dirname($timefile)); file_put_contents($timefile, $timecont); $this->assertTrue(file_exists($timefile)); $datafile = $CFG->dataroot . "/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes/testkey1-{$hash}.cache"; $datacont = serialize("test data 1"); make_writable_directory(dirname($datafile)); file_put_contents($datafile, $datacont); $this->assertTrue(file_exists($datafile)); // Test 1: Rebuild without the event and test its there. cache_factory::reset(); $instance = cache_config_phpunittest::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array('mode' => cache_store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true)); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertEquals('test data 1', $cache->get('testkey1')); // Test 2: Rebuild and test the invalidation of the event via the invalidation cache. cache_factory::reset(); $instance = cache_config_phpunittest::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array('mode' => cache_store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true, 'invalidationevents' => array('crazyevent'))); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertFalse($cache->get('testkey1')); }
/** * Checks if the store has a record for the given key and returns true if so. * * @param string $key * @return bool */ public function has($key) { $filename = $key . '.cache'; $maxtime = cache::now() - $this->definition->get_ttl(); if ($this->prescan) { return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime; } $file = $this->file_path_for_key($key); return file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime); }
/** * Override the cache::construct method. * * This function gets overriden so that we can process any invalidation events if need be. * If the definition doesn't have any invalidation events then this occurs exactly as it would for the cache class. * Otherwise we look at the last invalidation time and then check the invalidation data for events that have occured * between then now. * * You should not call this method from your code, instead you should use the cache::make methods. * * @param cache_definition $definition * @param cache_store $store * @param cache_loader|cache_data_source $loader * @return void */ public function __construct(cache_definition $definition, cache_store $store, $loader = null) { parent::__construct($definition, $store, $loader); if ($definition->has_invalidation_events()) { $lastinvalidation = $this->get('lastsessioninvalidation'); if ($lastinvalidation === false) { // This is a new session, there won't be anything to invalidate. Set the time of the last invalidation and // move on. $this->set('lastsessioninvalidation', cache::now()); return; } else { if ($lastinvalidation == cache::now()) { // We've already invalidated during this request. return; } } // Get the event invalidation cache. $cache = cache::make('core', 'eventinvalidation'); $events = $cache->get_many($definition->get_invalidation_events()); $todelete = array(); // Iterate the returned data for the events. foreach ($events as $event => $keys) { if ($keys === false) { // No data to be invalidated yet. continue; } // Look at each key and check the timestamp. foreach ($keys as $key => $timestamp) { // If the timestamp of the event is more than or equal to the last invalidation (happened between the last // invalidation and now)then we need to invaliate the key. if ($timestamp >= $lastinvalidation) { $todelete[] = $key; } } } if (!empty($todelete)) { $todelete = array_unique($todelete); $this->delete_many($todelete); } // Set the time of the last invalidation. $this->set('lastsessioninvalidation', cache::now()); } }
/** * Tests application cache event invalidation over a distributed setup. */ public function test_distributed_application_event_invalidation() { global $CFG; // This is going to be an intense wee test. // We need to add data the to cache, invalidate it by event, manually force it back without MUC knowing to simulate a // disconnected/distributed setup (think load balanced server using local cache), instantiate the cache again and finally // check that it is not picked up. $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array('mode' => cache_store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true, 'invalidationevents' => array('crazyevent'))); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertTrue($cache->set('testkey1', 'test data 1')); $this->assertEquals('test data 1', $cache->get('testkey1')); cache_helper::invalidate_by_event('crazyevent', array('testkey1')); $this->assertFalse($cache->get('testkey1')); // OK data added, data invalidated, and invalidation time has been set. // Now we need to manually add back the data and adjust the invalidation time. $hash = md5(cache_store::MODE_APPLICATION . '/phpunit/eventinvalidationtest/' . $CFG->wwwroot . 'phpunit'); $timefile = $CFG->dataroot . "/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/las-cache/lastinvalidation-{$hash}.cache"; // Make sure the file is correct. $this->assertTrue(file_exists($timefile)); $timecont = serialize(cache::now() - 60); // Back 60sec in the past to force it to re-invalidate. make_writable_directory(dirname($timefile)); file_put_contents($timefile, $timecont); $this->assertTrue(file_exists($timefile)); $datafile = $CFG->dataroot . "/cache/cachestore_file/default_application/phpunit_eventinvalidationtest/tes-cache/testkey1-{$hash}.cache"; $datacont = serialize("test data 1"); make_writable_directory(dirname($datafile)); file_put_contents($datafile, $datacont); $this->assertTrue(file_exists($datafile)); // Test 1: Rebuild without the event and test its there. cache_factory::reset(); $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array('mode' => cache_store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true)); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertEquals('test data 1', $cache->get('testkey1')); // Test 2: Rebuild and test the invalidation of the event via the invalidation cache. cache_factory::reset(); $instance = cache_config_testing::instance(); $instance->phpunit_add_definition('phpunit/eventinvalidationtest', array('mode' => cache_store::MODE_APPLICATION, 'component' => 'phpunit', 'area' => 'eventinvalidationtest', 'simplekeys' => true, 'simpledata' => true, 'invalidationevents' => array('crazyevent'))); $cache = cache::make('phpunit', 'eventinvalidationtest'); $this->assertFalse($cache->get('testkey1')); // Test 3: Verify that an existing lastinvalidation cache file is updated when needed. // Make a new cache class. This should should invalidate testkey2. $cache = cache::make('phpunit', 'eventinvalidationtest'); // Timestamp should have updated to cache::now(). $this->assertEquals(cache::now(), $cache->get('lastinvalidation')); // Set testkey2 data. $cache->set('testkey2', 'test data 2'); // Backdate the event invalidation time by 30 seconds. $invalidationcache = cache::make('core', 'eventinvalidation'); $invalidationcache->set('crazyevent', array('testkey2' => cache::now() - 30)); // Lastinvalidation should already be cache::now(). $this->assertEquals(cache::now(), $cache->get('lastinvalidation')); // Set it to 15 seconds ago so that we know if it changes. $cache->set('lastinvalidation', cache::now() - 15); // Make a new cache class. This should not invalidate anything. cache_factory::instance()->reset_cache_instances(); $cache = cache::make('phpunit', 'eventinvalidationtest'); // Lastinvalidation shouldn't change since it was already newer than invalidation event. $this->assertEquals(cache::now() - 15, $cache->get('lastinvalidation')); // Now set the event invalidation to newer than the lastinvalidation time. $invalidationcache->set('crazyevent', array('testkey2' => cache::now() - 5)); // Make a new cache class. This should should invalidate testkey2. cache_factory::instance()->reset_cache_instances(); $cache = cache::make('phpunit', 'eventinvalidationtest'); // Lastinvalidation timestamp should have updated to cache::now(). $this->assertEquals(cache::now(), $cache->get('lastinvalidation')); // Now simulate a purge_by_event 5 seconds ago. $invalidationcache = cache::make('core', 'eventinvalidation'); $invalidationcache->set('crazyevent', array('purged' => cache::now() - 5)); // Set our lastinvalidation timestamp to 15 seconds ago. $cache->set('lastinvalidation', cache::now() - 15); // Make a new cache class. This should invalidate the cache. cache_factory::instance()->reset_cache_instances(); $cache = cache::make('phpunit', 'eventinvalidationtest'); // Lastinvalidation timestamp should have updated to cache::now(). $this->assertEquals(cache::now(), $cache->get('lastinvalidation')); }
/** * Purges a cache of all information on a given event. * * @param string $event */ public static function purge_by_event($event) { $instance = cache_config::instance(); $invalidationeventset = false; $factory = cache_factory::instance(); foreach ($instance->get_definitions() as $name => $definitionarr) { $definition = cache_definition::load($name, $definitionarr); if ($definition->invalidates_on_event($event)) { // Check if this definition would result in a persistent loader being in use. if ($definition->should_be_persistent()) { // There may be a persistent cache loader. Lets purge that first so that any persistent data is removed. $cache = $factory->create_cache_from_definition($definition->get_component(), $definition->get_area()); $cache->purge(); } // Get all of the store instances that are in use for this store. $stores = $factory->get_store_instances_in_use($definition); foreach ($stores as $store) { // Purge each store individually. $store->purge(); } // We need to flag the event in the "Event invalidation" cache if it hasn't already happened. if ($invalidationeventset === false) { // Get the event invalidation cache. $cache = cache::make('core', 'eventinvalidation'); // Create a key to invalidate all. $data = array('purged' => cache::now()); // Set that data back to the cache. $cache->set($event, $data); // This only needs to occur once. $invalidationeventset = true; } } } }
/** * Returns true if the store contains records for any of the given keys. * * @param array $keys * @return bool */ public function has_any(array $keys) { $maxtime = cache::now() - $this->ttl; foreach ($keys as $key) { if (array_key_exists($key, $this->store) && ($this->ttl == 0 || $this->store[$key][1] >= $maxtime)) { return true; } } return false; }
/** * Returns true if the store contains records for any of the given keys. * * @param array $keys * @return bool */ public function has_any(array $keys) { if ($this->ttl != 0) { $maxtime = cache::now() - $this->ttl; } foreach ($keys as $key) { if (isset($this->store[$key]) && ($this->ttl == 0 || $this->store[$key][1] >= $maxtime)) { return true; } } return false; }