cache_storage = Dispatcher::get_usage_statistics_cache(); $c = Dispatcher::config(); $this->slot_interval_seconds = $c->get_integer( 'stats.slot_seconds' ); $this->keep_history_interval_seconds = $c->get_integer( 'stats.slots_count' ) * $this->slot_interval_seconds; $this->slots_count = $c->get_integer( 'stats.slots_count' ); } public function reset() { if ( !is_null( $this->cache_storage ) ) { $this->cache_storage->set( 'hotspot_endtime', array( 'content' => 0 ) ); } update_site_option( 'w3tc_stats_hotspot_start', time() ); update_site_option( 'w3tc_stats_history', '' ); } public function counter_add( $metric, $value ) { if ( !is_null( $this->cache_storage ) ) { $this->cache_storage->counter_add( $metric, $value ); } } public function get_hotspot_end() { if ( is_null( $this->hotspot_endtime ) ) { $v = $this->cache_storage->get( 'hotspot_endtime' ); $this->hotspot_endtime = ( isset( $v['content'] ) ? $v['content'] : 0 ); } return $this->hotspot_endtime; } private function get_option_storage() { if ( is_multisite() ) return new _OptionStorageWpmu(); else return new _OptionStorageSingleSite(); } public function maybe_flush_hotspot_data() { $result = $this->begin_flush_hotspot_data(); if ( $result == 'not_needed' ) return; $this->finish_flush_hotspot_data(); } /** * Returns if finish_* should be called. * It tries to pass as litte processes as possible to * flushing_begin if multiple processes come here * at the same time when hotspot time ended. */ public function begin_flush_hotspot_data() { $hotspot_endtime = $this->get_hotspot_end(); if ( is_null( $hotspot_endtime ) ) { // if cache not recognized - means nothing is cached at all // so stats not collected return 'not_needed'; } $hotspot_endtime_int = (int)$hotspot_endtime; $this->now = time(); if ( $hotspot_endtime_int <= 0 ) { $this->flush_state = 'require_db'; } elseif ( $this->now < $hotspot_endtime_int ) { $this->flush_state = 'not_needed'; } else { // rand value makes value unique for each process, // so as a result next replace works as a lock // passing only single process further $this->new_hotspot_endtime = $this->now + $this->slot_interval_seconds + ( rand( 1, 9999 ) / 10000.0 ); $succeeded = $this->cache_storage->set_if_maybe_equals( 'hotspot_endtime', array( 'content' => $hotspot_endtime ), array( 'content' => $this->new_hotspot_endtime ) ); $this->flush_state = ( $succeeded ? 'flushing_began_by_cache' : 'not_needed' ); } return $this->flush_state; } public function finish_flush_hotspot_data() { $option_storage = $this->get_option_storage(); if ( $this->flush_state == 'not_needed' ) return; if ( $this->flush_state != 'require_db' && $this->flush_state != 'flushing_began_by_cache' ) throw new Exception( 'unknown usage stats state ' . $this->flush_state ); // check whats there in db $this->hotspot_endtime = $option_storage->get_hotspot_end(); $hotspot_endtime_int = (int)$this->hotspot_endtime; if ( $this->now < $hotspot_endtime_int ) { // update cache, since there is something old/missing in cache $this->cache_storage->set( 'hotspot_endtime', array( 'content' => $this->hotspot_endtime ) ); return; // not neeeded really, db state after } if ( $this->new_hotspot_endtime <= 0 ) $this->new_hotspot_endtime = $this->now + $this->slot_interval_seconds + ( rand( 1, 9999 ) / 10000.0 ); if ( $hotspot_endtime_int <= 0 ) { // no data in options, initialization $this->cache_storage->set( 'hotspot_endtime', array( 'content' => $this->new_hotspot_endtime ) ); update_site_option( 'w3tc_stats_hotspot_start', time() ); $option_storage->set_hotspot_end( $this->new_hotspot_endtime ); return; } // try to become the process who makes flushing by // performing atomic database update // rand value makes value unique for each process, // so as a result next replace works as a lock // passing only single process further $succeeded = $option_storage->prolong_hotspot_end( $this->hotspot_endtime, $this->new_hotspot_endtime ); if ( !$succeeded ) return; $this->cache_storage->set( 'hotspot_endtime', array( 'content' => $this->new_hotspot_endtime ) ); // flush data $metrics = array(); $metrics = apply_filters( 'w3tc_usage_statistics_metrics', $metrics ); $metric_values = array(); $metric_values['timestamp_start'] = get_site_option( 'w3tc_stats_hotspot_start' ); $metric_values['timestamp_end'] = $hotspot_endtime_int; // try to limit time between get and reset of counter value // to loose as small as posssible foreach ( $metrics as $metric ) { $metric_values[$metric] = $this->cache_storage->counter_get( $metric ); $this->cache_storage->counter_set( $metric, 0 ); } $metric_values = apply_filters( 'w3tc_usage_statistics_metric_values', $metric_values ); $history_encoded = get_site_option( 'w3tc_stats_history' ); $history = null; if ( !empty( $history_encoded ) ) $history = json_decode( $history_encoded, true ); if ( !is_array( $history ) ) $history = array(); $time_keep_border = time() - $this->keep_history_interval_seconds; if ( $hotspot_endtime_int < $time_keep_border ) $history = array( array( 'timestamp_start' => $time_keep_border, 'timestamp_end' => (int)$this->new_hotspot_endtime - $this->slot_interval_seconds - 1 ) ); // this was started too much time from now else { // add collected $history[] = $metric_values; // if we empty place later - fill it for ( ;; ) { $metric_values = array( 'timestamp_start' => $metric_values['timestamp_end'] ); $metric_values['timestamp_end'] = $metric_values['timestamp_start'] + $this->slot_interval_seconds; if ( $metric_values['timestamp_end'] < $this->now ) $history[] = $metric_values; else break; } // make sure we have at least one value in history for ( ;count( $history ) > $this->slots_count; ) { if ( !isset( $history[0]['timestamp_end'] ) || $history[0]['timestamp_end'] < $time_keep_border ) array_shift( $history ); else break; } } $history = apply_filters( 'w3tc_usage_statistics_history_set', $history ); update_site_option( 'w3tc_stats_hotspot_start', $this->now ); update_site_option( 'w3tc_stats_history', json_encode( $history ) ); } } /** * Can update option by directly incrementing current value, * not via get+set operation */ class _OptionStorageSingleSite { private $option_hotspot_end = 'w3tc_stats_hotspot_end'; public function get_hotspot_end() { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT option_value ' . 'FROM ' . $wpdb->options . ' ' . 'WHERE option_name = %s LIMIT 1', $this->option_hotspot_end ) ); if ( !is_object( $row ) ) return false; $v = $row->option_value; return $v; } public function set_hotspot_end( $new_value ) { update_site_option( $this->option_hotspot_end, $new_value ); } /** * Performs atomic update of option value * from old to new value. Makes sure that only single process updates it. * Only single process gets true return value when multiple tries to do that. */ public function prolong_hotspot_end( $old_value, $new_value ) { global $wpdb; $q = $wpdb->prepare( 'UPDATE ' . $wpdb->options . ' ' . 'SET option_value = %s ' . 'WHERE option_name = %s AND option_value = %s', $new_value, $this->option_hotspot_end, $old_value ); $result = $wpdb->query( $q ); $succeeded = ( $result > 0 ); return $succeeded; } } /** * Can update option by directly incrementing current value, * not via get+set operation */ class _OptionStorageWpmu { private $option_hotspot_end = 'w3tc_stats_hotspot_end'; public function get_hotspot_end() { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT meta_value ' . 'FROM ' . $wpdb->sitemeta . ' ' . 'WHERE site_id = %d AND meta_key = %s', $wpdb->siteid, $this->option_hotspot_end ) ); if ( !is_object( $row ) ) return false; $v = $row->meta_value; return $v; } /** * Performs atomic update of option value * from old to new value. Makes sure that only single process updates it. * Only single process gets true return value when multiple tries to do that. */ public function set_hotspot_end( $new_value ) { update_site_option( $this->option_hotspot_end, $new_value ); } /** * Performs atomic update of option value * from old to new value. Makes sure that only single process updates it. * Only single process gets true return value when multiple tries to do that. */ public function prolong_hotspot_end( $old_value, $new_value ) { global $wpdb; $result = $wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->sitemeta . ' ' . 'SET meta_value = %s ' . 'WHERE site_id = %d AND meta_key = %s AND meta_value = %s', $new_value, $wpdb->siteid, $this->option_hotspot_end, $old_value ) ); $succeeded = ( $result > 0 ); return $succeeded; } }