diff --git a/system/expressionengine/third_party/stash/config.php b/system/expressionengine/third_party/stash/config.php index 32cd627..76dfa4e 100755 --- a/system/expressionengine/third_party/stash/config.php +++ b/system/expressionengine/third_party/stash/config.php @@ -2,7 +2,7 @@ if (! defined('STASH_VER')) { define('STASH_NAME', 'Stash'); - define('STASH_VER', '2.5.3'); + define('STASH_VER', '2.5.4'); define('STASH_AUTHOR', 'Mark Croxton'); define('STASH_DOCS', 'http://github.com/croxton/Stash/'); define('STASH_DESC', 'Stash: save text and code snippets for reuse throughout your templates.'); diff --git a/system/expressionengine/third_party/stash/ext.stash.php b/system/expressionengine/third_party/stash/ext.stash.php index 5d7e440..48ec308 100755 --- a/system/expressionengine/third_party/stash/ext.stash.php +++ b/system/expressionengine/third_party/stash/ext.stash.php @@ -7,7 +7,7 @@ * * @package Stash * @author Mark Croxton (mcroxton@hallmark-design.co.uk) - * @copyright Copyright (c) 2012 Hallmark Design + * @copyright Copyright (c) 2014 Hallmark Design * @license http://creativecommons.org/licenses/by-nc-sa/3.0/ * @link http://hallmark-design.co.uk */ @@ -121,7 +121,7 @@ private function _add_hook($name) * @return array */ public function template_fetch_template($row) - { + { // get the latest version of $row if (isset($this->EE->extensions->last_call) && $this->EE->extensions->last_call) { @@ -292,9 +292,10 @@ public function template_fetch_template($row) * @param string Parsed template string * @param bool Whether an embed or not * @param integer Site ID + * @param bool Has the extension been called by Stash rather than EE? * @return string Template string */ - public function template_post_parse($template, $sub, $site_id) + public function template_post_parse($template, $sub, $site_id, $from_stash = FALSE) { // play nice with other extensions on this hook if (isset($this->EE->extensions->last_call) && $this->EE->extensions->last_call) @@ -348,10 +349,10 @@ public function template_post_parse($template, $sub, $site_id) // loop through, prep the Stash instance, call the postponed tag and replace output into the placeholder foreach($cache as $placeholder => $tag) { - // make sure there is a placeholder in the template - // it may have been removed by advanced conditional processing if ( strpos( $template, $placeholder ) !== FALSE) { + // make sure there is a placeholder in the template + // it may have been removed by advanced conditional processing $this->EE->TMPL->log_item("Stash: post-processing tag: " . $tag['tagproper'] . " will be replaced into " . LD . $placeholder . RD); $this->EE->TMPL->tagparams = $tag['tagparams']; @@ -410,6 +411,31 @@ public function template_post_parse($template, $sub, $site_id) // cleanup unset($cache); + + // just before the template is sent to output + if (FALSE == $from_stash) + { + /* + // remove Stash from the extensions array to avoid infinite recursion + $other_ext = array(); + + foreach($this->EE->extensions->extensions['template_post_parse'] as $priority => $ext) + { + foreach($ext as $class => $config) + { + if ($class != 'Stash_ext') + { + $other_ext[$priority][$class] = $config; + } + } + } + */ + + // batch processing of cached variables + $this->EE->TMPL->log_item("Stash: batch processing queued queries"); + $this->EE->load->model('stash_model'); + $this->EE->stash_model->process_queue(); + } } return $template; diff --git a/system/expressionengine/third_party/stash/language/english/stash_lang.php b/system/expressionengine/third_party/stash/language/english/stash_lang.php index 7f5a7b8..dab9480 100755 --- a/system/expressionengine/third_party/stash/language/english/stash_lang.php +++ b/system/expressionengine/third_party/stash/language/english/stash_lang.php @@ -5,7 +5,7 @@ * * @package Stash * @author Mark Croxton (mcroxton@hallmark-design.co.uk) - * @copyright Copyright (c) 2012 Hallmark Design + * @copyright Copyright (c) 2014 Hallmark Design * @license http://creativecommons.org/licenses/by-nc-sa/3.0/ * @link http://hallmark-design.co.uk */ diff --git a/system/expressionengine/third_party/stash/mcp.stash.php b/system/expressionengine/third_party/stash/mcp.stash.php index bbc3525..62caa38 100755 --- a/system/expressionengine/third_party/stash/mcp.stash.php +++ b/system/expressionengine/third_party/stash/mcp.stash.php @@ -5,7 +5,7 @@ * * @package Stash * @author Mark Croxton (mcroxton@hallmark-design.co.uk) - * @copyright Copyright (c) 2012 Hallmark Design + * @copyright Copyright (c) 2014 Hallmark Design * @license http://creativecommons.org/licenses/by-nc-sa/3.0/ * @link http://hallmark-design.co.uk */ diff --git a/system/expressionengine/third_party/stash/mod.stash.php b/system/expressionengine/third_party/stash/mod.stash.php index bb86ae8..d785c75 100755 --- a/system/expressionengine/third_party/stash/mod.stash.php +++ b/system/expressionengine/third_party/stash/mod.stash.php @@ -7,7 +7,7 @@ * * @package Stash * @author Mark Croxton (mcroxton@hallmark-design.co.uk) - * @copyright Copyright (c) 2012 Hallmark Design + * @copyright Copyright (c) 2014 Hallmark Design * @license http://creativecommons.org/licenses/by-nc-sa/3.0/ * @link http://hallmark-design.co.uk */ @@ -37,6 +37,7 @@ class Stash { protected $process = 'inline'; protected $priority = 1; protected static $bundles = array(); + protected $check_expired = FALSE; private $_update = FALSE; private $_append = TRUE; @@ -78,10 +79,20 @@ public function __construct($calling_from_hook = FALSE) $this->default_scope = $this->EE->config->item('stash_default_scope') ? $this->EE->config->item('stash_default_scope') : 'user'; $this->limit_bots = $this->EE->config->item('stash_limit_bots') ? $this->EE->config->item('stash_limit_bots') : FALSE; + // cache pruning can cache stampede mitigation defaults + $this->prune = $this->EE->config->item('stash_prune_enabled') === FALSE ? FALSE : TRUE; + $this->prune_probability = $this->EE->config->item('stash_prune_probability') ? $this->EE->config->item('stash_prune_probability') : .4; // percent + $this->regen = $this->EE->config->item('stash_regen_enabled') === FALSE ? FALSE : TRUE; + $this->regen_probability = $this->EE->config->item('stash_regen_probability') ? $this->EE->config->item('stash_regen_probability') : .8; // percent + $this->invalidation_period = $this->EE->config->item('stash_invalidation_period') ? $this->EE->config->item('stash_invalidation_period') : 0; // seconds + // permitted file extensions for Stash embeds $this->file_extensions = $this->EE->config->item('stash_file_extensions') ? (array) $this->EE->config->item('stash_file_extensions') : array('html', 'md', 'css', 'js', 'rss', 'xml'); + + // Support {if var1 IN (var2) }...{/if} style conditionals in Stash templates / tagdata? + $this->parse_if_in = $this->EE->config->item('stash_parse_if_in') ? $this->EE->config->item('stash_parse_if_in') : FALSE; // initialise tag parameters if (FALSE === $calling_from_hook) @@ -97,15 +108,23 @@ public function __construct($calling_from_hook = FALSE) { // YES - restore session $this->EE->session->cache['stash']['_session_id'] = $cookie_data['id']; - $last_activity = $cookie_data['dt']; - - if ( $last_activity + 300 < $this->EE->localize->now) - { - // refresh cookie - $this->_set_stash_cookie($cookie_data['id']); - - // prune variables with expiry date older than right now - $this->EE->stash_model->prune_keys(); + + // shall we prune expired variables? + if ($this->prune) + { + // probability that pruning occurs + $prune_chance = 100/$this->prune_probability; + + // trigger pruning every 1 chance out of $prune_chance + if (mt_rand(0, ($prune_chance-1)) === 0) + { + // prune variables with expiry date older than right now + $this->EE->stash_model->prune_keys(); + + // uncomment for http load testing (e.g. with Siege) + #header('HTTP/1.0 404 Not Found'); + #die(); + } } } else @@ -129,7 +148,7 @@ public function __construct($calling_from_hook = FALSE) } // create a reference to the session id - $this->_session_id =& $this->EE->session->cache['stash']['_session_id']; + $this->_session_id =& $this->EE->session->cache['stash']['_session_id']; } // --------------------------------------------------------- @@ -609,18 +628,35 @@ public function set($params=array(), $value='', $type='variable', $scope='user') $session_filter =& $this->_session_id; } - // let's check if there is an existing record, and that that it matches the new one exactly + // let's check if there is an existing record $result = $this->EE->stash_model->get_key($stash_key, $this->bundle_id, $session_filter, $this->site_id); if ( $result !== FALSE) { - // record exists, but is it identical? + // yes record exists, but do we want to update it? + $update_key = FALSE; + + // has it expired? + if ($this->check_expired) + { + // Yes, let's regenerate it now rather than letting it get pruned and recreated. + // Since this is triggered at random, it makes it more likely that only one request + // at a time will trigger a regen (i.e. reduces chance of a cache stampede on the cached variable) + $update_key = TRUE; + } + + // is the new variable value identical to the value in the cache? // allow append/prepend if the stash key has been created *in this page load* $cache_key = $stash_key. '_'. $this->bundle_id .'_' .$this->site_id . '_' . $session_filter; - if ( $result !== $parameters && ($this->replace || ($this->_update && $this->EE->stash_model->is_inserted_key($cache_key)) ) ) + if ( $result !== $parameters && ($this->replace || ($this->_update && $this->EE->stash_model->is_inserted_key($cache_key)) )) { - // nope - update + $update_key = TRUE; + } + + if ($update_key) + { + // update $this->EE->stash_model->update_key( $stash_key, $this->bundle_id, @@ -629,6 +665,10 @@ public function set($params=array(), $value='', $type='variable', $scope='user') $refresh, $parameters ); + + // uncomment for http load testing (e.g. with Siege) + #header('HTTP/1.0 500 Not Found'); + #die(); } } else @@ -836,7 +876,7 @@ public function get($params='', $type='variable', $scope='user') { $value = $this->_stash[$name] = self::$bundles[$bundle][$name]; } - elseif ( ! $this->_update && ! ($dynamic && ! $save) && $scope !== 'local') + elseif ( ! $this->_update && ! ($dynamic && ! $save) && $scope !== 'local') { // let's look in the database table cache, but only if if we're not // appending/prepending or trying to register a global without saving it @@ -846,13 +886,29 @@ public function get($params='', $type='variable', $scope='user') // replace '@' placeholders with the current context $stash_key = $this->_parse_context($name); + + // for random requests (frequency determined by prune_probability) we should check if + // the variable has passed expiry date, and *update* it with latest value if it has + if ($this->regen) + { + // chance that we should regenerate - should be double the chance of pruning + $regen_chance = 100/$this->regen_probability; + + // trigger regeneration of the variable every 1 chance out of $regen_chance + if (mt_rand(0, ($regen_chance-1)) === 0) + { + $this->check_expired = TRUE; + } + } // look for our key if ( $parameters = $this->EE->stash_model->get_key( $stash_key, $this->bundle_id, $session_id, - $this->site_id + $this->site_id, + 'parameters', + $this->check_expired )) { // save to session @@ -994,7 +1050,6 @@ public function get($params='', $type='variable', $scope='user') // note: don't save if we're updating a variable (to avoid recursion) if ( $set && ! $this->_update) { - $this->EE->TMPL->tagparams['name'] = $name; $this->EE->TMPL->tagparams['output'] = 'yes'; $this->EE->TMPL->tagdata = $value; @@ -1378,6 +1433,12 @@ public function set_list() { $this->_prep_no_results($prefix); } + + // Unprefix common variables in wrapped tags + if($unprefix = $this->EE->TMPL->fetch_param('unprefix')) + { + $this->EE->TMPL->tagdata = $this->_un_prefix($unprefix, $this->EE->TMPL->tagdata); + } // do we want to replace an existing list variable? $set = TRUE; @@ -1419,6 +1480,7 @@ public function set_list() { // do any parsing and string transforms before making the list $this->EE->TMPL->tagdata = $this->_parse_output($this->EE->TMPL->tagdata); + $this->parse_complete = TRUE; // make sure we don't run parsing again, if we're saving the list // regenerate tag variable pairs array using the parsed tagdata $tag_vars = $this->EE->functions->assign_variables($this->EE->TMPL->tagdata); @@ -1894,6 +1956,19 @@ public function get_list($params=array(), $value='', $type='variable', $scope='u // because it can potentially break unparsed conditionals / tags etc in the list $backspace = $this->EE->TMPL->fetch_param('backspace', FALSE); $this->EE->TMPL->tagparams['backspace'] = FALSE; + + // prep {if IN ()}...{/if} conditionals + if ($this->parse_if_in) + { + // prefixed ifs? We have to hide them in EE 2.9+ if this tagdata is in the root template + if ( ! is_null($prefix)) + { + $this->EE->TMPL->tagdata = str_replace(LD.$prefix.':if', LD.'if', $this->EE->TMPL->tagdata); + $this->EE->TMPL->tagdata = str_replace(LD.'/'.$prefix.':if'.RD, LD.'/if'.RD, $this->EE->TMPL->tagdata); + } + + $this->EE->TMPL->tagdata = $this->_prep_in_conditionals($this->EE->TMPL->tagdata); + } // Replace into template. // @@ -1908,9 +1983,6 @@ public function get_list($params=array(), $value='', $type='variable', $scope='u // variables inside the get_list tag pair which have names that could collide. $list_html = $this->EE->TMPL->parse_variables($this->EE->TMPL->tagdata, $list); - - // prep {if IN ()}...{/if} conditionals - #$list_html = $this->_prep_in_conditionals($list_html); // restore original backspace parameter $this->EE->TMPL->tagparams['backspace'] = $backspace; @@ -2063,7 +2135,10 @@ public function get_bundle($set=TRUE) } // prep 'IN' conditionals if the retreived var is a delimited string - $out = $this->_prep_in_conditionals($out); + if ($this->parse_if_in) + { + $out = $this->_prep_in_conditionals($out); + } } $this->EE->TMPL->log_item("Stash: RETRIEVED bundle ".$bundle); @@ -2763,7 +2838,8 @@ public function destroy($params=array(), $type='variable', $scope='user') $bundle_id, $session_id, $this->site_id, - trim($name, '#') + trim($name, '#'), + $this->invalidation_period ); } } @@ -2794,7 +2870,8 @@ public function destroy($params=array(), $type='variable', $scope='user') $stash_key, $bundle_id, $session_id, - $this->site_id + $this->site_id, + $this->invalidation_period ); } } @@ -2810,7 +2887,9 @@ public function destroy($params=array(), $type='variable', $scope='user') $this->EE->stash_model->delete_matching_keys( $bundle_id, $session_id, - $this->site_id + $this->site_id, + NULL, + $this->invalidation_period ); } } @@ -3552,6 +3631,12 @@ private function _parse_context($name) $uri = $ee_uri->uri_string(); $uri = empty($uri) ? $this->EE->stash_model->get_index_key() : $uri; + // append query string? + if ($query_str = ee()->input->server('QUERY_STRING')) + { + $uri = $uri . '?' . $query_str; + } + // replace '@URI:' with the current URI if (strncmp($name, '@URI:', 5) == 0) { @@ -3678,11 +3763,27 @@ private function _parse_sub_template($tags = TRUE, $vars = TRUE, $conditionals = } } - // parse simple conditionals + // parse conditionals if ($conditionals) - { - $TMPL2->tagdata = $TMPL2->parse_simple_segment_conditionals($TMPL2->tagdata); - $TMPL2->tagdata = $TMPL2->simple_conditionals($TMPL2->tagdata, $this->EE->config->_global_vars); + { + // *prep* {If var1 IN (var2)}../if] style conditionals + if ($this->parse_if_in) + { + $TMPL2->tagdata = $this->_prep_in_conditionals($TMPL2->tagdata); + } + + // *parse* simple conditionals + if (version_compare(APP_VER, '2.9', '>=')) + { + $this->EE->TMPL = $TMPL2; + $TMPL2->tagdata = $TMPL2->simple_conditionals($TMPL2->tagdata, $this->EE->config->_global_vars); + unset($this->EE->TMPL); + } + else + { + $TMPL2->tagdata = $TMPL2->parse_simple_segment_conditionals($TMPL2->tagdata); + $TMPL2->tagdata = $TMPL2->simple_conditionals($TMPL2->tagdata, $this->EE->config->_global_vars); + } } // Remove any EE comments that might have been exposed before parsing tags @@ -3725,7 +3826,6 @@ private function _parse_sub_template($tags = TRUE, $vars = TRUE, $conditionals = // parse tags $this->EE->TMPL->parse_tags(); $this->EE->TMPL->process_tags(); - $this->EE->TMPL->loop_count = 0; $TMPL2->tagdata = $this->EE->TMPL->template; @@ -3785,7 +3885,8 @@ private function _parse_sub_template($tags = TRUE, $vars = TRUE, $conditionals = 'template_post_parse', $this->EE->TMPL->tagdata, FALSE, - $this->site_id + $this->site_id, + TRUE ); // restore original extensions on the 'template_fetch_template' hook @@ -4121,7 +4222,7 @@ private function _placeholders($matches) // --------------------------------------------------------- /** - * process processing our method until template_post_parse hook + * Delay processing a tag until template_post_parse hook * * @access private * @param String Method name (e.g. display, link or embed) @@ -4132,7 +4233,7 @@ private function _post_parse($method) // base our needle off the calling tag // add a random number to prevent EE caching the tag, if it is used more than once $placeholder = md5($this->EE->TMPL->tagproper) . rand(); - + if ( ! isset($this->EE->session->cache['stash']['__template_post_parse__'])) { $this->EE->session->cache['stash']['__template_post_parse__'] = array(); diff --git a/system/expressionengine/third_party/stash/models/stash_model.php b/system/expressionengine/third_party/stash/models/stash_model.php index 5bc00c6..e3040fb 100755 --- a/system/expressionengine/third_party/stash/models/stash_model.php +++ b/system/expressionengine/third_party/stash/models/stash_model.php @@ -5,7 +5,7 @@ * * @package Stash * @author Mark Croxton (mcroxton@hallmark-design.co.uk) - * @copyright Copyright (c) 2012 Hallmark Design + * @copyright Copyright (c) 2014 Hallmark Design * @license http://creativecommons.org/licenses/by-nc-sa/3.0/ * @link http://hallmark-design.co.uk */ @@ -16,6 +16,7 @@ class Stash_model extends CI_Model { protected static $keys = array(); protected static $inserted_keys = array(); + protected static $queue; // default bundle types protected static $bundle_ids = array( @@ -34,6 +35,11 @@ function __construct() { parent::__construct(); $this->EE = get_instance(); + + // batch processing of queued queries + self::$queue = new stdClass(); + self::$queue->inserts = array(); + self::$queue->updates = array(); } /** @@ -46,10 +52,12 @@ function __construct() * @param string $parameters * @param string $label * @param integer $bundle_id - * @return integer + * @return boolean */ function insert_key($key, $bundle_id = 1, $session_id, $site_id = 1, $expire = 0, $parameters = '', $label = '') { + $cache_key = $key . '_'. $bundle_id .'_' .$site_id . '_' . $session_id; + $data = array( 'key_name' => $key, 'bundle_id' => $bundle_id, @@ -60,11 +68,10 @@ function insert_key($key, $bundle_id = 1, $session_id, $site_id = 1, $expire = 0 'parameters' => $parameters, 'key_label' => $label ); - - if ( $result = $this->db->insert('stash', $data) ) + + if ($result = $this->queue_insert('stash', $cache_key, $data)) { - // store a record of the newly created key - $cache_key = $key . '_'. $bundle_id .'_' .$site_id . '_' . $session_id; + // store a record of the key self::$inserted_keys[] = $cache_key; // cache result to eliminate need for a query in future gets @@ -73,8 +80,7 @@ function insert_key($key, $bundle_id = 1, $session_id, $site_id = 1, $expire = 0 // write to static file cache? $this->write_static_cache($key, $bundle_id, $site_id, $parameters); - // return insert id - return $this->db->insert_id(); + return TRUE; } else { @@ -105,38 +111,38 @@ function is_inserted_key($cache_key) */ function update_key($key, $bundle_id = 1, $session_id = '', $site_id = 1, $expire = 0, $parameters = NULL) { + $cache_key = $key . '_'. $bundle_id .'_' .$site_id . '_' . $session_id; + $data = array( - 'created' => $this->EE->localize->now, - 'expire' => $expire + 'created' => $this->EE->localize->now, + 'expire' => $expire ); if ($parameters !== NULL) { $data += array('parameters' => $parameters); } - - $this->db->where('key_name', $key) - ->where('bundle_id', $bundle_id) - ->where('site_id', $site_id); + + $where = array( + 'key_name' => $key, + 'bundle_id' => $bundle_id, + 'site_id' => $site_id + ); if ( ! empty($session_id)) { - $this->db->where('session_id', $session_id); + $where += array('session_id' => $session_id); } - if ($result = $this->db->update('stash', $data)) + if ($result = $this->queue_update('stash', $cache_key, $data, $where)) { - if ( (bool) $this->db->affected_rows()) - { - // success - update cache - $cache_key = $key . '_'. $bundle_id .'_' .$site_id . '_' . $session_id; - self::$keys[$cache_key] = $parameters; + // update cache + self::$keys[$cache_key] = $parameters; - // write to static file cache? - $this->write_static_cache($key, $bundle_id, $site_id, $parameters); + // write to static file cache? + $this->write_static_cache($key, $bundle_id, $site_id, $parameters); - return TRUE; - } + return TRUE; } else { @@ -168,9 +174,10 @@ function refresh_key($key, $bundle_id = 1, $session_id = '', $site_id = 1, $refr * @param string $col * @return string */ - function get_key($key, $bundle_id = 1, $session_id = '', $site_id = 1, $col = 'parameters') + function get_key($key, $bundle_id = 1, $session_id = '', $site_id = 1, $col = 'parameters', $check_expired = FALSE) { $cache_key = $key . '_'. $bundle_id .'_' .$site_id . '_' . $session_id; + $now = $this->EE->localize->now; if ( ! isset(self::$keys[$cache_key])) { @@ -189,22 +196,32 @@ function get_key($key, $bundle_id = 1, $session_id = '', $site_id = 1, $col = 'p if ($result->num_rows() == 1) { + $key_created = $result->row('created'); + $key_expire = $result->row('expire'); + // if this key expires soon and is scoped to user session, refresh it - if ($result->row('expire') > 0 && $session_id != '_global' && ! empty($session_id)) + if ($key_expire > 0 && $session_id != '_global' && ! empty($session_id)) { - $refresh = $result->row('expire') - $result->row('created'); // refresh period (seconds) - $expire = $result->row('expire') - $this->EE->localize->now; // time to expiry (seconds) + $refresh = $key_expire - $key_created; // refresh period (seconds) + $expire = $key_expire - $this->EE->localize->now; // time to expiry (seconds) if ( ($refresh / $expire) > 2 ) { // more than half the refresh time has passed since the last time key was accessed // so let's refresh the key expiry $this->refresh_key($key, $bundle_id, $session_id, $site_id, $refresh); + + // update key dates + $key_expire = $this->EE->localize->now + $refresh; } } // cache result - self::$keys[$cache_key] = $result->row($col); + self::$keys[$cache_key] = array( + $col => $result->row($col), + 'expire' => $key_expire, + 'created' => $key_created + ); } else { @@ -219,8 +236,38 @@ function get_key($key, $bundle_id = 1, $session_id = '', $site_id = 1, $col = 'p } else { - return self::$keys[$cache_key]; + if ($check_expired && self::$keys[$cache_key]['expire'] != 0) + { + if (self::$keys[$cache_key]['expire'] < $this->EE->localize->now) + { + // variable has expired + return FALSE; + } + } + + return self::$keys[$cache_key][$col]; + } + } + + /** + * Get key expiry date + * + * @param string $key + * @param string $session_id + * @param integer $site_id + * @param string $col + * @return timestamp / boolean + */ + function get_key_expiry($key, $bundle_id = 1, $session_id = '', $site_id = 1) + { + $cache_key = $key . '_'. $bundle_id .'_' .$site_id . '_' . $session_id; + + if ( isset(self::$keys[$cache_key])) + { + return self::$keys[$cache_key]['expire']; } + + return FALSE; } /** @@ -230,9 +277,10 @@ function get_key($key, $bundle_id = 1, $session_id = '', $site_id = 1, $col = 'p * @param integer/boolean $bundle_id * @param string $session_id * @param integer $site_id + * @param integer $invalidate Delay until cached item expires (seconds) * @return boolean */ - function delete_key($key, $bundle_id = FALSE, $session_id = NULL, $site_id = 1) + function delete_key($key, $bundle_id = FALSE, $session_id = NULL, $site_id = 1, $invalidate=0) { $this->db->where('key_name', $key) ->where('site_id', $site_id); @@ -259,7 +307,7 @@ function delete_key($key, $bundle_id = FALSE, $session_id = NULL, $site_id = 1) if ($query->num_rows() > 0) { - if ( $this->delete_cache($query->result(), $site_id)) + if ( $this->delete_cache($query->result(), $site_id, FALSE, $invalidate)) { // deleted, now remove the key from the internal key cache $cache_key = $key . '_'. $bundle_id .'_' .$site_id . '_' . $session_id; @@ -283,9 +331,10 @@ function delete_key($key, $bundle_id = FALSE, $session_id = NULL, $site_id = 1) * @param string $session_id * @param integer $site_id * @param string $regex a regular expression + * @param integer $invalidate Delay until cached item expires (seconds) * @return boolean */ - function delete_matching_keys($bundle_id = FALSE, $session_id=NULL, $site_id = 1, $regex=NULL) + function delete_matching_keys($bundle_id = FALSE, $session_id=NULL, $site_id = 1, $regex=NULL, $invalidate=0) { $deleted = FALSE; @@ -322,30 +371,15 @@ function delete_matching_keys($bundle_id = FALSE, $session_id=NULL, $site_id = 1 if ( ! is_null($regex)) { $this->db->where('key_name RLIKE ', $this->db->escape($regex), FALSE); - - // get matching keys - $query = $this->db->select('id, key_name, key_label, bundle_id, session_id')->get('stash'); - - if ($query->num_rows() > 0) - { - $deleted = $this->delete_cache($query->result(), $site_id); - } } - elseif ($this->db->delete('stash')) + + // get matching keys + $query = $this->db->select('id, key_name, key_label, bundle_id, session_id')->get('stash'); + + if ($query->num_rows() > 0) { - // ------------------------------------- - // 'stash_delete' hook - // ------------------------------------- - if ($this->EE->extensions->active_hook('stash_delete') === TRUE) - { - $this->EE->extensions->call('stash_delete', array( - 'key_name' => FALSE, - 'key_label' => FALSE, - 'bundle_id' => $bundle_id, - 'session_id' => $session_id, - 'site_id' => $site_id - )); - } + // clear the entire static cache dir for the selected site? + $clear_static_dir = FALSE; // delete entire static cache for this site if bundle is 'static' or not specified // and scope is 'site', 'all' or not specified @@ -353,10 +387,10 @@ function delete_matching_keys($bundle_id = FALSE, $session_id=NULL, $site_id = 1 { if ( is_null($session_id) || $session_id === 'site' || $session_id === 'all') { - $this->_delete_dir('/', $site_id); + $clear_static_dir = TRUE; } } - $deleted = TRUE; + $deleted = $this->delete_cache($query->result(), $site_id, $clear_static_dir, $invalidate); } if ($deleted) @@ -368,15 +402,16 @@ function delete_matching_keys($bundle_id = FALSE, $session_id=NULL, $site_id = 1 return $deleted; } - /** * Delete an array of variables in a given site * * @param array $vars An array of objects * @param integer $site_id + * @param boolean $clear_static_dir + * @param integer $invalidate Delay until cached item expires (seconds) * @return boolean */ - protected function delete_cache($vars, $site_id = 1) + protected function delete_cache($vars, $site_id = 1, $clear_static_dir = FALSE, $invalidate=0) { $ids = array(); @@ -398,28 +433,73 @@ protected function delete_cache($vars, $site_id = 1) )); } - // delete any corresponding static cache files, individually - $this->delete_static_cache($row->key_name, $row->bundle_id, $site_id); + if ($clear_static_dir) + { + // delete the entire static cache directory for the given site (fast) + $this->_delete_dir('/', $site_id); + } + else + { + // delete any corresponding static cache files, individually + $this->delete_static_cache($row->key_name, $row->bundle_id, $site_id); + } } // delete any db records - if ($this->EE->db->where_in('id', $ids)->delete('stash')) + $result = FALSE; + + if ($invalidate > 0) { - return TRUE; + // soft delete - update variables to expire at random intervals within + // the invalidation period and so help prevent cache stampedes + $result = $this->_invalidate($ids, $invalidate); } + else + { + // delete immediately + $result = $this->EE->db->where_in('id', $ids)->delete('stash'); + } + + return $result; + } - return FALSE; + /** + * Update variables to expire at random intervals within the + * invalidation period and so help prevent cache stampedes + * + * @param array $ids An array of variable ids + * @param integer $period the invalidation period in seconds + * @return boolean + */ + private function _invalidate($ids, $period=0) + { + $now = $this->EE->localize->now; + + // sort low to high + sort($ids); + + // get the last id value and the count + $id_end = end($ids); + $id_count = count($ids) - 1; + + // what we're doing here is approximately dividing the expiry delay across the target ids, + // increasing the delay according the original id value, so that variables + // generated later in the original template, get regenereated later too + $this->EE->db->where_in('id', $ids); + $this->db->set('expire', 'FLOOR (' . $this->EE->localize->now . ' + '.$period.' - ( ('.$id_end.' - id) / '.$id_count.' * ' . $period . ' ))', false); + return $this->db->update('stash'); } /** * Prune expired keys * + * @param integer $buffer time to wait past expiry before pruning * @return boolean */ - function prune_keys() + function prune_keys($buffer=15) { if ($result = $this->db->delete('stash', array( - 'expire <' => $this->EE->localize->now, + 'expire <' => $this->EE->localize->now - $buffer, 'expire !=' => '0' ))) { @@ -742,6 +822,118 @@ public function get_index_key() { return $this->_index_key; } + + /** + * Prepare INSERT IGNORE BATCH SQL query + * + * @param string $table The table to insert into + * @param Array $data Array in form of "Column" => "Value", ... + * @return Null + */ + protected function insert_ignore_batch($table, array $data) + { + $_keys = array(); + $_prepared = array(); + + foreach ($data as $row) + { + $_values = array(); + + foreach ($row as $col => $val) + { + // add key + if ( ! in_array($col, $_keys) ) + { + $_keys[] = $col; + } + + // add values + $_values[] = $this->db->escape($val); + } + $_prepared[] = '(' . implode(',', $_values) . ')'; + } + + $this->db->query('INSERT IGNORE INTO '.$this->db->dbprefix.$table.' ('.implode(',',$_keys).') VALUES '.implode(',', array_values($_prepared)).';'); + } + + /** + * Queue an insert for batch processing later + * + * @param string $table The table to insert into + * @param string $cache_key Unique key identifying this variable + * @param Array $data Array in form of "Column" => "Value", ... + * @return boolean + */ + protected function queue_insert($table, $cache_key, $data) + { + if ( ! isset(self::$queue->inserts[$table])) + { + self::$queue->inserts[$table] = array(); + } + elseif( isset(self::$queue->inserts[$table][$cache_key])) + { + // insert already queued + return FALSE; + } + + self::$queue->inserts[$table][$cache_key] = $data; + return TRUE; + } + + /** + * Queue an update for batch processing later + * + * @param string $table The table to insert into + * @param string $cache_key Unique key identifying this variable + * @param Array $data Array in form of "Column" => "Value", ... + * @param Array $where Array in form of "Column" => "Value", ... + * @return boolean + */ + protected function queue_update($table, $cache_key, $data, $where) + { + if ( ! isset(self::$queue->updates[$table])) + { + self::$queue->updates[$table] = array(); + } + + // overwrite any existing key, so that only the last update to same cached item actually runs + self::$queue->updates[$table][$cache_key] = array( + 'data' => $data, + 'where' => $where + ); + return TRUE; + } + + /** + * Process queued queries + * + * @return boolean + */ + public function process_queue() + { + // batch inserts - must run first + foreach(self::$queue->inserts as $table => $data) + { + $this->insert_ignore_batch($table, $data); + } + + // run each queued update in order + if (count(self::$queue->updates) > 0) + { + // using innodb, so we can wrap with a transaction + // to save a tiny bit of overhead + $this->db->trans_start(); + foreach(self::$queue->updates as $table => $updates) + { + foreach($updates as $query) + { + $this->db->where($query['where']); + $this->db->update($table, $query['data']); + } + } + $this->db->trans_complete(); + } + } } diff --git a/system/expressionengine/third_party/stash/upd.stash.php b/system/expressionengine/third_party/stash/upd.stash.php index 21487f3..1b8b35e 100755 --- a/system/expressionengine/third_party/stash/upd.stash.php +++ b/system/expressionengine/third_party/stash/upd.stash.php @@ -7,7 +7,7 @@ * * @package Stash * @author Mark Croxton (mcroxton@hallmark-design.co.uk) - * @copyright Copyright (c) 2012 Hallmark Design + * @copyright Copyright (c) 2014 Hallmark Design * @license http://creativecommons.org/licenses/by-nc-sa/3.0/ * @link http://hallmark-design.co.uk */ @@ -61,11 +61,11 @@ public function install() `created` int(10) unsigned NOT NULL, `expire` int(10) unsigned NOT NULL default '0', `parameters` MEDIUMTEXT, - PRIMARY KEY (`id`), + PRIMARY KEY (`id`), + UNIQUE KEY `cache_key` (`key_name`,`bundle_id`,`site_id`,`session_id`), KEY `bundle_id` (`bundle_id`), - KEY `key_session` (`key_name`,`session_id`), - KEY `key_name` (`key_name`), - KEY `site_id` (`site_id`) + KEY `site_id` (`site_id`), + KEY `expire` (`expire`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; "; @@ -140,11 +140,11 @@ public function update($current = '') return FALSE; } + $sql = array(); + // Update to 2.3.7 if (version_compare($current, '2.3.7', '<')) { - $sql = array(); - // increase variable max key and parameter sizes $sql[] = "ALTER TABLE `{$this->EE->db->dbprefix}stash` CHANGE `key_name` `key_name` VARCHAR(255) NOT NULL"; $sql[] = "ALTER TABLE `{$this->EE->db->dbprefix}stash` CHANGE `key_label` `key_label` VARCHAR(255) NOT NULL"; @@ -170,6 +170,22 @@ public function update($current = '') } } + // Update to 2.5.4 + if (version_compare($current, '2.5.4', '<')) + { + // change indexes + $sql[] = "ALTER TABLE `{$this->EE->db->dbprefix}stash` + DROP INDEX `key_session`, + DROP INDEX `key_name`, + ADD UNIQUE `cache_key` (`key_name`, `bundle_id`, `site_id`, `session_id`), + ADD INDEX `expire` (`expire`)"; + } + + foreach ($sql as $query) + { + $this->EE->db->query($query); + } + // update version number return TRUE;