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;
|
||
|
}
|
||
|
}
|