371 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			371 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace W3TC;
 | |
| 
 | |
| /**
 | |
|  * Manages data statistics.
 | |
|  * Metrics:
 | |
|  *
 | |
|  */
 | |
| class UsageStatistics_StorageWriter {
 | |
| 	private $slot_interval_seconds;
 | |
| 	private $slots_count;
 | |
| 	private $keep_history_interval_seconds;
 | |
| 
 | |
| 	private $cache_storage;
 | |
| 
 | |
| 	/**
 | |
| 	 * Cached values, just keep state between calls
 | |
| 	 */
 | |
| 	private $hotspot_endtime;
 | |
| 	private $new_hotspot_endtime = 0;
 | |
| 	private $now;
 | |
| 
 | |
| 	/**
 | |
| 	 * begin_ sets the state what should it perform later
 | |
| 	 * finish reacts to that state and finishes flushing
 | |
| 	 * values:
 | |
| 	 *   not_needed - no flushinig required now
 | |
| 	 *   require_db - database access has to be available to decide
 | |
| 	 *   flushing_began_by_cache - that process has been selected to
 | |
| 	 *     flush hotspot data based on cache data state, still that has to be
 | |
| 	 *     verified in database
 | |
| 	 */
 | |
| 	private $flush_state;
 | |
| 
 | |
| 
 | |
| 
 | |
| 	public function __construct() {
 | |
| 		$this->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;
 | |
| 	}
 | |
| }
 |