Changeset 3396626
- Timestamp:
- 11/16/2025 03:40:01 PM (4 months ago)
- Location:
- dynamic-front-end-heartbeat-control
- Files:
-
- 35 added
- 17 edited
-
tags/1.2.995 (added)
-
tags/1.2.995/LICENSE (added)
-
tags/1.2.995/admin (added)
-
tags/1.2.995/admin/affix.php (added)
-
tags/1.2.995/admin/ajax-handler.php (added)
-
tags/1.2.995/admin/asset-manager.php (added)
-
tags/1.2.995/admin/heartbeat-config.php (added)
-
tags/1.2.995/admin/unclogger-menu.php (added)
-
tags/1.2.995/css (added)
-
tags/1.2.995/css/dfhcsl-admin.css (added)
-
tags/1.2.995/defibrillator (added)
-
tags/1.2.995/defibrillator/cli-helper.php (added)
-
tags/1.2.995/defibrillator/db-health.php (added)
-
tags/1.2.995/defibrillator/load-estimator.php (added)
-
tags/1.2.995/defibrillator/rest-api.php (added)
-
tags/1.2.995/defibrillator/unclogger-db.php (added)
-
tags/1.2.995/defibrillator/unclogger.php (added)
-
tags/1.2.995/engine (added)
-
tags/1.2.995/engine/interval-helper.php (added)
-
tags/1.2.995/engine/server-load.php (added)
-
tags/1.2.995/engine/server-response.php (added)
-
tags/1.2.995/engine/system-load-fallback.php (added)
-
tags/1.2.995/heartbeat-async.php (added)
-
tags/1.2.995/heartbeat-controller.php (added)
-
tags/1.2.995/js (added)
-
tags/1.2.995/js/chart.js (added)
-
tags/1.2.995/js/dfhcsl-admin.js (added)
-
tags/1.2.995/js/heartbeat.js (added)
-
tags/1.2.995/js/heartbeat.min.js (added)
-
tags/1.2.995/readme.txt (added)
-
tags/1.2.995/settings.php (added)
-
tags/1.2.995/visitor (added)
-
tags/1.2.995/visitor/cookie-helper.php (added)
-
tags/1.2.995/visitor/manager.php (added)
-
tags/1.2.995/widget.php (added)
-
trunk/defibrillator/cli-helper.php (modified) (1 diff)
-
trunk/defibrillator/db-health.php (modified) (2 diffs)
-
trunk/defibrillator/load-estimator.php (modified) (4 diffs)
-
trunk/defibrillator/rest-api.php (modified) (1 diff)
-
trunk/defibrillator/unclogger-db.php (modified) (1 diff)
-
trunk/defibrillator/unclogger.php (modified) (13 diffs)
-
trunk/engine/interval-helper.php (modified) (1 diff)
-
trunk/engine/server-load.php (modified) (15 diffs)
-
trunk/engine/server-response.php (modified) (7 diffs)
-
trunk/engine/system-load-fallback.php (modified) (3 diffs)
-
trunk/heartbeat-async.php (modified) (19 diffs)
-
trunk/heartbeat-controller.php (modified) (12 diffs)
-
trunk/js/heartbeat.js (modified) (2 diffs)
-
trunk/js/heartbeat.min.js (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/visitor/cookie-helper.php (modified) (5 diffs)
-
trunk/visitor/manager.php (modified) (14 diffs)
Legend:
- Unmodified
- Added
- Removed
-
dynamic-front-end-heartbeat-control/trunk/defibrillator/cli-helper.php
r3310561 r3396626 2 2 namespace DynamicHeartbeat; 3 3 4 defined('ABSPATH') or die(); 5 6 class dfehcUncloggerCli extends dfehcUnclogger { 7 public function __construct() { 8 if (!class_exists('WP_CLI')) return; 9 10 $this->db = new dfehcUncloggerDb(); 11 $this->tweaks = new dfehcUncloggerTweaks(); 12 13 load_plugin_textdomain('dynamic-front-end-heartbeat-control', false, dirname(plugin_basename(__FILE__)) . '/languages'); 14 15 WP_CLI::add_command('dfehc-unclogger', [$this, 'dfehc_unclogger_command']); 16 } 17 18 public function dfehc_unclogger_command($args, $assoc_args) { 19 if (empty($args)) { 20 WP_CLI::line('commands:'); 21 WP_CLI::line(' - wp dfehc-unclogger db <command>'); 22 return; 23 } 24 25 if (!isset($args[1]) || $args[0] !== 'db') { 26 WP_CLI::line('Invalid command.'); 27 return; 28 } 29 30 if ($args[1] === 'optimize_all') { 31 $before = $this->db->get_database_size(); 32 $this->db->optimize_all(); 33 $after = $this->db->get_database_size(); 34 WP_CLI::success('Before: ' . $before . ', after: ' . $after); 35 } 36 } 37 } 38 39 if (defined('WP_CLI') && WP_CLI) { 40 class DFEHC_CLI_Command { 41 public function recalc_interval() { 42 $load = dfehc_get_server_load(); 43 if ($load === null) { 44 WP_CLI::error('Unable to fetch server load.'); 45 return; 46 } 47 $interval = dfehc_calculate_recommended_interval_user_activity($load); 48 set_transient('dfehc_recommended_interval', $interval, 5 * MINUTE_IN_SECONDS); 49 WP_CLI::success("Heartbeat interval recalculated and cached: {$interval}s"); 50 } 51 52 public function process_users() { 53 dfehc_process_user_activity(); 54 WP_CLI::success('User activity queue processed.'); 55 } 56 57 public function clear_cache() { 58 global $wpdb; 59 $transients = array_merge( 60 $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_transient_%dfehc_%'"), 61 $wpdb->get_col("SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '_site_transient_%dfehc_%'") 62 ); 63 foreach ($transients as $transient) { 64 $key = preg_replace('/^_site_transient_|^_transient_/', '', $transient); 65 delete_transient($key); 66 delete_site_transient($key); 67 } 68 delete_option('dfehc_server_load_logs'); 69 delete_option('dfehc_stale_total_visitors'); 70 WP_CLI::success('DFEHC transients and options cleared.'); 71 } 72 73 public function enable_heartbeat() { 74 update_option('dfehc_disable_heartbeat', 0); 75 WP_CLI::success('Heartbeat has been enabled.'); 76 } 77 78 public function disable_heartbeat() { 79 update_option('dfehc_disable_heartbeat', 1); 80 WP_CLI::success('Heartbeat has been disabled.'); 81 } 82 83 public function status() { 84 $load = dfehc_get_server_load(); 85 $interval = get_transient('dfehc_recommended_interval'); 86 $heartbeat_enabled = !get_option('dfehc_disable_heartbeat'); 87 88 WP_CLI::log("Server Load: " . ($load !== null ? round($load, 2) : 'N/A')); 89 WP_CLI::log("Heartbeat Interval: " . ($interval !== false ? $interval . 's' : 'Not set')); 90 WP_CLI::log("Heartbeat Enabled: " . ($heartbeat_enabled ? 'Yes' : 'No')); 91 } 92 93 public function calibrate_baseline() { 94 $hostname = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_hostname_key(); 95 $blog_id = is_multisite() ? get_current_blog_id() : 1; 96 $transient = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::BASELINE_TRANSIENT_PREFIX . $hostname . '_' . $blog_id; 97 98 $baseline = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::calibrate_baseline(); 99 set_transient($transient, $baseline, 7 * DAY_IN_SECONDS); 100 101 WP_CLI::success("Baseline calibrated: $baseline"); 102 } 103 public function load() { 104 $load = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load(); 105 $spike_score = (float) get_transient(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::LOAD_SPIKE_TRANSIENT); 106 107 if ($load === false) { 108 WP_CLI::error('Unable to estimate server load.'); 109 return; 110 } 111 112 WP_CLI::log("Current Load: {$load}%"); 113 WP_CLI::log("Spike Score: " . round($spike_score, 2)); 114 } 115 116 } 117 118 WP_CLI::add_command('dfehc', 'DFEHC_CLI_Command'); 119 } 4 if (!\defined('WP_CLI') || !WP_CLI) { 5 return; 6 } 7 8 if (!\function_exists(__NAMESPACE__ . '\\dfehc_blog_id')) { 9 function dfehc_blog_id(): int { 10 return \function_exists('\get_current_blog_id') ? (int) \get_current_blog_id() : 0; 11 } 12 } 13 14 if (!\function_exists(__NAMESPACE__ . '\\dfehc_scope_key')) { 15 function dfehc_scope_key(string $base): string { 16 if (\function_exists('\dfehc_scoped_key')) { 17 return \dfehc_scoped_key($base); 18 } 19 $hostToken = \function_exists('\dfehc_host_token') 20 ? \dfehc_host_token() 21 : \substr(\md5((string) ((\php_uname('n') ?: 'unknown') . (\defined('DB_NAME') ? DB_NAME : ''))), 0, 10); 22 return "{$base}_" . dfehc_blog_id() . "_{$hostToken}"; 23 } 24 } 25 26 if (\class_exists(__NAMESPACE__ . '\\dfehcUnclogger') && \class_exists('WP_CLI')) { 27 class DfehcUncloggerCli extends dfehcUnclogger { 28 public function __construct() { 29 if (!\class_exists('\WP_CLI')) return; 30 31 if (\class_exists(__NAMESPACE__ . '\\dfehcUncloggerDb')) { 32 $this->db = new dfehcUncloggerDb(); 33 } 34 if (\class_exists(__NAMESPACE__ . '\\dfehcUncloggerTweaks')) { 35 $this->tweaks = new dfehcUncloggerTweaks(); 36 } 37 38 if (\function_exists('\load_plugin_textdomain') && \function_exists('\plugin_basename')) { 39 \load_plugin_textdomain( 40 'dynamic-front-end-heartbeat-control', 41 false, 42 \dirname(\plugin_basename(__FILE__)) . '/languages' 43 ); 44 } 45 46 if (isset($this->db)) { 47 \WP_CLI::add_command('dfehc-unclogger', [$this, 'dfehc_unclogger_command']); 48 } 49 } 50 51 public function dfehc_unclogger_command(array $args, array $assoc_args) { 52 if (empty($args)) { 53 \WP_CLI::line('commands:'); 54 \WP_CLI::line(' - wp dfehc-unclogger db optimize_all'); 55 return 0; 56 } 57 58 if (!isset($args[1]) || $args[0] !== 'db') { 59 \WP_CLI::error('Invalid command. Usage: wp dfehc-unclogger db optimize_all'); 60 return 1; 61 } 62 63 if ($args[1] === 'optimize_all') { 64 if (!isset($this->db)) { 65 \WP_CLI::error('Database unclogger is not available.'); 66 return 1; 67 } 68 $before = \method_exists($this->db, 'get_database_size') ? $this->db->get_database_size() : 'unknown'; 69 if (\method_exists($this->db, 'optimize_all')) { 70 $this->db->optimize_all(); 71 } 72 $after = \method_exists($this->db, 'get_database_size') ? $this->db->get_database_size() : 'unknown'; 73 \WP_CLI::success('Before: ' . $before . ', after: ' . $after); 74 return 0; 75 } 76 77 \WP_CLI::error('Unknown db subcommand.'); 78 return 1; 79 } 80 } 81 new DfehcUncloggerCli(); 82 } 83 84 class DFEHC_CLI_Command { 85 86 public function recalc_interval(array $args = [], array $assoc_args = []) { 87 if (!\function_exists('\dfehc_get_server_load')) { 88 \WP_CLI::error('dfehc_get_server_load() is unavailable.'); 89 return; 90 } 91 $load = \dfehc_get_server_load(); 92 if ($load === false || $load === null) { 93 \WP_CLI::error('Unable to fetch server load.'); 94 return; 95 } 96 97 $interval = null; 98 if (\function_exists('\dfehc_calculate_recommended_interval_user_activity')) { 99 $interval = (int) \dfehc_calculate_recommended_interval_user_activity((float) $load); 100 } elseif (\function_exists('\dfehc_calculate_recommended_interval')) { 101 $interval = (int) \dfehc_calculate_recommended_interval(60.0, (float) $load, 0.0); 102 } else { 103 \WP_CLI::error('No interval calculator function available.'); 104 return; 105 } 106 107 $baseKey = \defined('DFEHC_RECOMMENDED_INTERVAL') ? \DFEHC_RECOMMENDED_INTERVAL : 'dfehc_recommended_interval'; 108 $key = dfehc_scope_key($baseKey); 109 110 if (\function_exists('\dfehc_set_transient_noautoload')) { 111 \dfehc_set_transient_noautoload($key, $interval, 5 * MINUTE_IN_SECONDS); 112 } else { 113 \set_transient($key, $interval, 5 * MINUTE_IN_SECONDS); 114 } 115 116 \WP_CLI::success("Heartbeat interval recalculated and cached: {$interval}s"); 117 } 118 119 public function process_users(array $args = [], array $assoc_args = []) { 120 if (!\function_exists('\dfehc_process_user_activity')) { 121 \WP_CLI::error('dfehc_process_user_activity() is unavailable.'); 122 return; 123 } 124 \dfehc_process_user_activity(); 125 \WP_CLI::success('User activity queue processed.'); 126 } 127 128 public function clear_cache(array $args = [], array $assoc_args = []) { 129 $network = !empty($assoc_args['network']) && \is_multisite(); 130 $flush_object_cache = !empty($assoc_args['flush-object-cache']); 131 $group = !empty($assoc_args['group']) ? (string) $assoc_args['group'] : (\defined('DFEHC_CACHE_GROUP') ? \DFEHC_CACHE_GROUP : 'dfehc'); 132 133 if (empty($assoc_args['yes'])) { 134 \WP_CLI::confirm('This will clear DFEHC caches. Continue?'); 135 } 136 137 $bases = [ 138 'dfehc_db_metrics', 139 'dfehc_db_size_mb', 140 'dfehc_db_size_fail', 141 'dfehc_server_load', 142 'dfehc_server_load_payload', 143 'dfehc:server_load', 144 'dfehc_recommended_interval', 145 'dfehc_load_averages', 146 'dfehc_ema_' . dfehc_blog_id(), 147 'dfehc_prev_int_' . dfehc_blog_id(), 148 'dfehc_total_visitors', 149 'dfehc_regenerating_cache', 150 'dfehc_system_load_avg', 151 ]; 152 153 if ($network && \function_exists('\get_sites')) { 154 $siteIds = \array_map('intval', (array) \get_sites(['fields' => 'ids'])); 155 } else { 156 $siteIds = [ dfehc_blog_id() ]; 157 } 158 159 foreach ($siteIds as $sid) { 160 $switched = false; 161 if ($network) { \switch_to_blog($sid); $switched = true; } 162 try { 163 foreach ($bases as $base) { 164 $key = dfehc_scope_key($base); 165 \delete_transient($key); 166 \delete_site_transient($key); 167 } 168 \delete_option('dfehc_stale_total_visitors'); 169 } finally { 170 if ($switched) { \restore_current_blog(); } 171 } 172 } 173 174 if ($flush_object_cache) { 175 if (\function_exists('\wp_cache_flush_group')) { 176 @\wp_cache_flush_group($group); 177 \WP_CLI::success("Flushed object cache group: {$group}"); 178 } elseif (\function_exists('\wp_cache_flush')) { 179 @\wp_cache_flush(); 180 \WP_CLI::warning('Global object cache flushed (group-specific flush not supported).'); 181 } else { 182 \WP_CLI::warning('Object cache flush not supported by this installation.'); 183 } 184 } 185 186 \WP_CLI::success('DFEHC caches cleared.'); 187 } 188 189 public function enable_heartbeat(array $args = [], array $assoc_args = []) { 190 \update_option('dfehc_disable_heartbeat', 0, false); 191 \WP_CLI::success('Heartbeat has been enabled.'); 192 } 193 194 public function disable_heartbeat(array $args = [], array $assoc_args = []) { 195 \update_option('dfehc_disable_heartbeat', 1, false); 196 \WP_CLI::success('Heartbeat has been disabled.'); 197 } 198 199 public function status(array $args = [], array $assoc_args = []) { 200 $load = \function_exists('\dfehc_get_server_load') ? \dfehc_get_server_load() : null; 201 202 $baseKey = \defined('DFEHC_RECOMMENDED_INTERVAL') ? \DFEHC_RECOMMENDED_INTERVAL : 'dfehc_recommended_interval'; 203 $key = dfehc_scope_key($baseKey); 204 $interval = \get_transient($key); 205 206 $heartbeat_enabled = !\get_option('dfehc_disable_heartbeat'); 207 208 \WP_CLI::log('Server Load: ' . ($load !== null && $load !== false ? \round((float) $load, 2) : 'N/A')); 209 \WP_CLI::log('Heartbeat Interval: ' . ($interval !== false && $interval !== null ? (int) $interval . 's' : 'Not set')); 210 \WP_CLI::log('Heartbeat Enabled: ' . ($heartbeat_enabled ? 'Yes' : 'No')); 211 } 212 213 public function calibrate_baseline(array $args = [], array $assoc_args = []) { 214 if (!\class_exists(__NAMESPACE__ . '\\Dfehc_ServerLoadEstimator')) { 215 \WP_CLI::error('Server load estimator is unavailable.'); 216 return; 217 } 218 $baseline = Dfehc_ServerLoadEstimator::calibrate_baseline(); 219 \WP_CLI::success("Baseline calibrated: {$baseline}"); 220 } 221 222 public function load(array $args = [], array $assoc_args = []) { 223 if (!\class_exists(__NAMESPACE__ . '\\Dfehc_ServerLoadEstimator')) { 224 \WP_CLI::error('Server load estimator is unavailable.'); 225 return; 226 } 227 $load = Dfehc_ServerLoadEstimator::get_server_load(); 228 if ($load === false || $load === null) { 229 \WP_CLI::error('Unable to estimate server load.'); 230 return; 231 } 232 233 \WP_CLI::log('Current Load: ' . \round((float) $load, 2) . '%'); 234 235 $spikeShown = false; 236 $candidates = [ 237 dfehc_scope_key('dfehc_spike_score'), 238 'dfehc_spike_score', 239 ]; 240 foreach ($candidates as $k) { 241 $v = \get_transient($k); 242 if (\is_numeric($v)) { 243 \WP_CLI::log('Spike Score: ' . \round((float) $v, 2)); 244 $spikeShown = true; 245 break; 246 } 247 } 248 if (!$spikeShown) { 249 \WP_CLI::log('Spike Score: N/A'); 250 } 251 } 252 } 253 254 \WP_CLI::add_command('dfehc', __NAMESPACE__ . '\\DFEHC_CLI_Command', [ 255 'shortdesc' => 'Dynamic Front-End Heartbeat Control utilities.', 256 'synopsis' => [], 257 ]); -
dynamic-front-end-heartbeat-control/trunk/defibrillator/db-health.php
r3310561 r3396626 4 4 namespace DynamicHeartbeat; 5 5 6 function dfehc_gather_database_metrics(bool $force = false): array 7 { 6 if (!\function_exists(__NAMESPACE__ . '\\dfehc_blog_id')) { 7 function dfehc_blog_id(): int { 8 return \function_exists('\get_current_blog_id') ? (int) \get_current_blog_id() : 0; 9 } 10 } 11 12 function dfehc_scoped_key(string $base): string { 13 return $base . '_' . dfehc_blog_id(); 14 } 15 16 function dfehc_cache_group(): string { 17 return \defined('DFEHC_CACHE_GROUP') ? (string) \DFEHC_CACHE_GROUP : 'dfehc'; 18 } 19 20 function dfehc_cache_get(string $key) { 21 if (\function_exists('\wp_using_ext_object_cache') && \wp_using_ext_object_cache()) { 22 return \wp_cache_get($key, dfehc_cache_group()); 23 } 24 return \get_site_transient($key); 25 } 26 27 function dfehc_cache_set(string $key, $value, int $ttl): void { 28 if (\function_exists('\wp_using_ext_object_cache') && \wp_using_ext_object_cache()) { 29 \wp_cache_set($key, $value, dfehc_cache_group(), $ttl); 30 return; 31 } 32 \set_site_transient($key, $value, $ttl); 33 } 34 35 function dfehc_gather_database_metrics(bool $force = false): array { 8 36 static $cached = null; 9 if ($cached !== null && !$force) { 37 $persist_ttl = (int) \apply_filters('dfehc_db_metrics_ttl', 600); 38 $metrics_key = dfehc_scoped_key('dfehc_db_metrics'); 39 40 if (!$force) { 41 $persisted = dfehc_cache_get($metrics_key); 42 if (\is_array($persisted)) { 43 $cached = $persisted; 44 return $persisted; 45 } 46 } elseif ($cached !== null) { 10 47 return $cached; 11 48 } 12 49 13 $unclogger = new DfehcUncloggerDb(); 14 $revision_count = $unclogger->count_revisions(); 15 $trashed_posts_count = $unclogger->count_trashed_posts(); 16 $expired_transients_count = $unclogger->count_expired_transients(); 17 18 $order_count = wp_count_posts('shop_order')->publish ?? 0; 19 $product_count = wp_count_posts('product')->publish ?? 0; 20 $page_count = wp_count_posts('page')->publish ?? 0; 21 22 $customer_query = new \WP_User_Query([ 23 'role' => 'customer', 24 'number' => 1, 25 'fields' => 'ID', 26 'count_total' => true, 50 $toggles = \wp_parse_args(\apply_filters('dfehc_db_metrics_toggles', []), [ 51 'revisions' => true, 52 'trashed_posts' => true, 53 'expired_transients' => true, 54 'orders' => true, 55 'products' => true, 56 'pages' => true, 57 'customers' => true, 58 'users' => true, 59 'db_size' => true, 60 'disk' => true, 27 61 ]); 28 $customer_count = (int) $customer_query->get_total(); 29 30 $user_query = new \WP_User_Query([ 31 'number' => 1, 32 'fields' => 'ID', 33 'count_total' => true, 34 ]); 35 $user_count = (int) $user_query->get_total(); 62 63 $budget_ms = (int) \apply_filters('dfehc_db_metrics_budget_ms', 250); 64 $budget_ms = $budget_ms > 0 ? $budget_ms : 250; 65 $t0 = \microtime(true); 66 $within_budget = static function () use ($t0, $budget_ms): bool { 67 return ((\microtime(true) - $t0) * 1000) < $budget_ms; 68 }; 69 70 $sources = []; 71 $metrics = [ 72 'revision_count' => 0, 73 'trashed_posts_count' => 0, 74 'expired_transients_count'=> 0, 75 'order_count' => 0, 76 'product_count' => 0, 77 'page_count' => 0, 78 'customer_count' => 0, 79 'user_count' => 0, 80 'db_size_mb' => 0.0, 81 'disk_free_space_mb' => 0.0, 82 'collected_at' => \current_time('mysql'), 83 'cache_backend' => (\function_exists('\wp_using_ext_object_cache') && \wp_using_ext_object_cache()) ? 'object' : 'transient', 84 ]; 85 86 $unclogger = null; 87 if (\class_exists(__NAMESPACE__ . '\\DfehcUncloggerDb')) { 88 $unclogger = new DfehcUncloggerDb(); 89 } elseif (\class_exists('\\DfehcUncloggerDb')) { 90 $unclogger = new \DfehcUncloggerDb(); 91 } 92 93 if ($toggles['revisions'] && $within_budget()) { 94 $metrics['revision_count'] = $unclogger ? (int) $unclogger->count_revisions() : 0; 95 $sources['revision_count'] = $unclogger ? 'sql' : 'none'; 96 } else { 97 $sources['revision_count'] = 'skipped'; 98 } 99 100 if ($toggles['trashed_posts'] && $within_budget()) { 101 $metrics['trashed_posts_count'] = $unclogger ? (int) $unclogger->count_trashed_posts() : 0; 102 $sources['trashed_posts_count'] = $unclogger ? 'sql' : 'none'; 103 } else { 104 $sources['trashed_posts_count'] = 'skipped'; 105 } 106 107 if ($toggles['expired_transients'] && $within_budget()) { 108 $metrics['expired_transients_count'] = $unclogger ? (int) $unclogger->count_expired_transients() : 0; 109 $sources['expired_transients_count'] = $unclogger ? 'sql' : 'none'; 110 } else { 111 $sources['expired_transients_count'] = 'skipped'; 112 } 113 114 if ($toggles['orders'] && $within_budget() && \function_exists('\post_type_exists') && \post_type_exists('shop_order')) { 115 $o = \wp_count_posts('shop_order'); 116 $metrics['order_count'] = (int) ((\is_object($o) ? (array) $o : [])['publish'] ?? 0); 117 $sources['order_count'] = 'wp_count_posts'; 118 } else { 119 $sources['order_count'] = $toggles['orders'] ? 'skipped' : 'disabled'; 120 } 121 122 if ($toggles['products'] && $within_budget() && \function_exists('\post_type_exists') && \post_type_exists('product')) { 123 $p = \wp_count_posts('product'); 124 $metrics['product_count'] = (int) ((\is_object($p) ? (array) $p : [])['publish'] ?? 0); 125 $sources['product_count'] = 'wp_count_posts'; 126 } else { 127 $sources['product_count'] = $toggles['products'] ? 'skipped' : 'disabled'; 128 } 129 130 if ($toggles['pages'] && $within_budget() && \function_exists('\post_type_exists') && \post_type_exists('page')) { 131 $pg = \wp_count_posts('page'); 132 $metrics['page_count'] = (int) ((\is_object($pg) ? (array) $pg : [])['publish'] ?? 0); 133 $sources['page_count'] = 'wp_count_posts'; 134 } else { 135 $sources['page_count'] = $toggles['pages'] ? 'skipped' : 'disabled'; 136 } 137 138 if ($toggles['users'] && $within_budget()) { 139 $cu = \function_exists('\count_users') ? \count_users() : null; 140 $metrics['user_count'] = \is_array($cu) && isset($cu['total_users']) ? (int) $cu['total_users'] : 0; 141 $sources['user_count'] = $cu ? 'count_users' : 'none'; 142 } else { 143 $sources['user_count'] = $toggles['users'] ? 'skipped' : 'disabled'; 144 } 145 146 if ($toggles['customers'] && $within_budget()) { 147 $cu = \function_exists('\count_users') ? \count_users() : null; 148 $avail = \is_array($cu) && isset($cu['avail_roles']) ? (array) $cu['avail_roles'] : []; 149 $metrics['customer_count'] = (int) ($avail['customer'] ?? 0); 150 $sources['customer_count'] = $cu ? 'count_users' : 'none'; 151 } else { 152 $sources['customer_count'] = $toggles['customers'] ? 'skipped' : 'disabled'; 153 } 36 154 37 155 global $wpdb; 38 $db_size_mb = get_transient('dfehc_db_size_mb'); 39 if ($db_size_mb === false) { 40 $db_size_mb = $wpdb->get_var( 41 $wpdb->prepare( 156 157 $db_size_key = dfehc_scoped_key('dfehc_db_size_mb'); 158 $db_size_fail_key = dfehc_scoped_key('dfehc_db_size_fail'); 159 $db_size_mb = null; 160 161 if ($toggles['db_size'] && $within_budget()) { 162 $db_size_mb = dfehc_cache_get($db_size_key); 163 $db_size_fail = dfehc_cache_get($db_size_fail_key); 164 if ($db_size_mb === false && $db_size_fail === false) { 165 $prev = $wpdb->suppress_errors(); 166 $wpdb->suppress_errors(true); 167 168 $qry = $wpdb->prepare( 42 169 "SELECT SUM(data_length + index_length) / 1024 / 1024 43 170 FROM information_schema.tables 44 171 WHERE table_schema = %s", 45 172 $wpdb->dbname 46 ) 47 ); 48 if ($db_size_mb !== null) { 49 set_transient('dfehc_db_size_mb', (float) $db_size_mb, 6 * HOUR_IN_SECONDS); 50 } 173 ); 174 $db_size_mb = $wpdb->get_var($qry); 175 176 $wpdb->suppress_errors($prev); 177 178 if (\is_numeric($db_size_mb)) { 179 $db_size_mb = (float) $db_size_mb; 180 if ($db_size_mb <= 0 || $db_size_mb > (float) \apply_filters('dfehc_db_size_upper_bound_mb', 10 * 1024 * 1024)) { 181 $db_size_mb = null; 182 } else { 183 dfehc_cache_set($db_size_key, (float) $db_size_mb, 6 * HOUR_IN_SECONDS); 184 $sources['db_size_mb'] = 'information_schema'; 185 } 186 } else { 187 $db_size_mb = null; 188 } 189 190 if ($db_size_mb === null && $within_budget()) { 191 $prev = $wpdb->suppress_errors(); 192 $wpdb->suppress_errors(true); 193 194 $prefix_like = $wpdb->esc_like($wpdb->base_prefix) . '%'; 195 $rows = $wpdb->get_results( 196 $wpdb->prepare("SHOW TABLE STATUS WHERE Name LIKE %s", $prefix_like), 197 ARRAY_A 198 ); 199 200 $wpdb->suppress_errors($prev); 201 202 if (\is_array($rows) && $rows) { 203 $sum = 0.0; 204 foreach ($rows as $r) { 205 $sum += (isset($r['Data_length']) ? (float) $r['Data_length'] : 0.0) 206 + (isset($r['Index_length']) ? (float) $r['Index_length'] : 0.0); 207 } 208 if ($sum > 0) { 209 $db_size_mb = \round($sum / 1024 / 1024, 2); 210 dfehc_cache_set($db_size_key, (float) $db_size_mb, 6 * HOUR_IN_SECONDS); 211 $sources['db_size_mb'] = 'show_table_status'; 212 } 213 } 214 } 215 216 if ($db_size_mb === null) { 217 dfehc_cache_set($db_size_fail_key, 1, (int) \apply_filters('dfehc_db_size_fail_ttl', 900)); 218 } 219 } elseif (\is_numeric($db_size_mb)) { 220 $db_size_mb = (float) $db_size_mb; 221 $sources['db_size_mb'] = 'cache'; 222 } 223 } else { 224 $sources['db_size_mb'] = $toggles['db_size'] ? 'skipped' : 'disabled'; 51 225 } 52 226 53 227 if ($db_size_mb === null || $db_size_mb === false) { 54 $multipliers = apply_filters('dfehc_db_size_multipliers', [55 'user' => 0.03,56 'order' => 0.05,57 'revision' => 0.05,58 'trash' => 0.05,59 'page' => 0.10,228 $multipliers = \apply_filters('dfehc_db_size_multipliers', [ 229 'user' => 0.03, 230 'order' => 0.05, 231 'revision' => 0.05, 232 'trash' => 0.05, 233 'page' => 0.10, 60 234 'transient' => 0.01, 61 'default' => 500,235 'default' => 500.0, 62 236 ]); 63 $estimate = 0 ;64 $estimate += $ user_count * $multipliers['user'];65 $estimate += $ order_count * $multipliers['order'];66 $estimate += $ revision_count * $multipliers['revision'];67 $estimate += $ trashed_posts_count * $multipliers['trash'];68 $estimate += $ page_count * $multipliers['page'];69 $estimate += $ expired_transients_count * $multipliers['transient'];237 $estimate = 0.0; 238 $estimate += $metrics['user_count'] * (float) ($multipliers['user'] ?? 0.03); 239 $estimate += $metrics['order_count'] * (float) ($multipliers['order'] ?? 0.05); 240 $estimate += $metrics['revision_count'] * (float) ($multipliers['revision'] ?? 0.05); 241 $estimate += $metrics['trashed_posts_count'] * (float) ($multipliers['trash'] ?? 0.05); 242 $estimate += $metrics['page_count'] * (float) ($multipliers['page'] ?? 0.10); 243 $estimate += $metrics['expired_transients_count']* (float) ($multipliers['transient'] ?? 0.01); 70 244 if ($estimate <= 0) { 71 $estimate = $multipliers['default']; 72 } 73 $db_size_mb = $estimate; 74 } 75 76 $disk_paths = apply_filters('dfehc_disk_paths', [ 77 WP_CONTENT_DIR, 78 defined('ABSPATH') ? ABSPATH : null, 79 ]); 80 $disk_free_space_mb = null; 81 foreach ($disk_paths as $p) { 82 if ($p && is_dir($p) && is_readable($p)) { 83 $space = disk_free_space($p); 84 if ($space !== false) { 85 $disk_free_space_mb = round($space / 1024 / 1024, 2); 86 break; 87 } 88 } 89 } 90 if ($disk_free_space_mb === null) { 91 $disk_free_space_mb = round((50 * 1024 * 1024 * 1024) / 1024 / 1024, 2); 92 } 93 94 $metrics = [ 95 'revision_count' => $revision_count, 96 'trashed_posts_count' => $trashed_posts_count, 97 'expired_transients_count' => $expired_transients_count, 98 'order_count' => $order_count, 99 'product_count' => $product_count, 100 'page_count' => $page_count, 101 'customer_count' => $customer_count, 102 'user_count' => $user_count, 103 'db_size' => (float) $db_size_mb, 104 'disk_free_space_mb' => $disk_free_space_mb, 105 ]; 245 $estimate = (float) ($multipliers['default'] ?? 500.0); 246 } 247 $db_size_mb = \round($estimate, 2); 248 $sources['db_size_mb'] = 'estimated'; 249 } 250 251 $metrics['db_size_mb'] = (float) $db_size_mb; 252 253 if ($toggles['disk'] && $within_budget()) { 254 $disk_paths = \apply_filters('dfehc_disk_paths', (function (): array { 255 $paths = []; 256 if (\defined('WP_CONTENT_DIR')) { $paths[] = WP_CONTENT_DIR; } 257 if (\function_exists('\wp_get_upload_dir')) { 258 $u = \wp_get_upload_dir(); 259 if (\is_array($u) && !empty($u['basedir'])) { 260 $paths[] = (string) $u['basedir']; 261 } 262 } 263 if (\defined('ABSPATH')) { $paths[] = ABSPATH; } 264 return $paths; 265 })()); 266 267 $disk_free_space_mb = null; 268 foreach ($disk_paths as $p) { 269 if ($p && \is_dir($p) && \is_readable($p)) { 270 $space = @\disk_free_space($p); 271 if ($space !== false) { 272 $disk_free_space_mb = \round($space / 1024 / 1024, 2); 273 $sources['disk_free_space_mb'] = 'disk_free_space'; 274 break; 275 } 276 } 277 } 278 if ($disk_free_space_mb === null) { 279 $fallback = (float) \apply_filters('dfehc_disk_free_default_mb', \round((50 * 1024 * 1024 * 1024) / 1024 / 1024, 2)); 280 $disk_free_space_mb = $fallback; 281 $sources['disk_free_space_mb'] = 'default'; 282 } 283 $metrics['disk_free_space_mb'] = (float) $disk_free_space_mb; 284 } else { 285 $metrics['disk_free_space_mb'] = (float) \apply_filters('dfehc_disk_free_default_mb', \round((50 * 1024 * 1024 * 1024) / 1024 / 1024, 2)); 286 $sources['disk_free_space_mb'] = $toggles['disk'] ? 'skipped' : 'disabled'; 287 } 288 289 $metrics['sources'] = $sources; 106 290 107 291 $cached = $metrics; 292 dfehc_cache_set($metrics_key, $metrics, $persist_ttl); 293 108 294 return $metrics; 109 295 } 110 296 111 function dfehc_evaluate_database_health(array $metrics): array 112 { 297 function dfehc_evaluate_database_health(array $metrics): array { 113 298 $severity = 'ok'; 114 299 $conditions_met = 0; 115 300 116 301 $thresholds = [ 117 'revision' => 1000,118 'trash' => 1000,119 'transients' => 5000,120 'db_size '=> 500,302 'revision' => 1000, 303 'trash' => 1000, 304 'transients' => 5000, 305 'db_size_mb' => 500, 121 306 'disk_free_mb' => 10240, 122 307 ]; 123 308 124 if ( $metrics['order_count']> 10000) {125 $thresholds = array_merge($thresholds, [126 'revision' => 5000,127 'trash' => 5000,309 if (((int) ($metrics['order_count'] ?? 0)) > 10000) { 310 $thresholds = \array_merge($thresholds, [ 311 'revision' => 5000, 312 'trash' => 5000, 128 313 'transients' => 20000, 129 'db_size ' => 3000,314 'db_size_mb' => 3000, 130 315 ]); 131 316 } 132 317 133 if ( $metrics['product_count']> 3000) {134 $thresholds = array_merge($thresholds, [135 'revision' => 6000,136 'trash' => 6000,318 if (((int) ($metrics['product_count'] ?? 0)) > 3000) { 319 $thresholds = \array_merge($thresholds, [ 320 'revision' => 6000, 321 'trash' => 6000, 137 322 'transients' => 30000, 138 'db_size ' => 3000,323 'db_size_mb' => 3000, 139 324 ]); 140 325 } 141 326 142 if ( $metrics['user_count']> 10000) {143 $thresholds = array_merge($thresholds, [144 'revision' => 3000,145 'trash' => 3000,327 if (((int) ($metrics['user_count'] ?? 0)) > 10000) { 328 $thresholds = \array_merge($thresholds, [ 329 'revision' => 3000, 330 'trash' => 3000, 146 331 'transients' => 10000, 147 'db_size ' => 3000,332 'db_size_mb' => 3000, 148 333 ]); 149 334 } 150 335 151 $thresholds = apply_filters('dfehc_database_health_thresholds', $thresholds, $metrics);152 153 if ( $metrics['revision_count'] > $thresholds['revision']) $conditions_met++;154 if ( $metrics['trashed_posts_count'] > $thresholds['trash']) $conditions_met++;155 if ( $metrics['expired_transients_count'] > $thresholds['transients']) $conditions_met++;156 if ( $metrics['db_size'] > $thresholds['db_size']) $conditions_met++;157 if ( $metrics['disk_free_space_mb'] < $thresholds['disk_free_mb']) $conditions_met++;336 $thresholds = \apply_filters('dfehc_database_health_thresholds', $thresholds, $metrics); 337 338 if (((int) ($metrics['revision_count'] ?? 0)) > (int) ($thresholds['revision'] ?? 0)) $conditions_met++; 339 if (((int) ($metrics['trashed_posts_count'] ?? 0)) > (int) ($thresholds['trash'] ?? 0)) $conditions_met++; 340 if (((int) ($metrics['expired_transients_count'] ?? 0)) > (int) ($thresholds['transients'] ?? 0)) $conditions_met++; 341 if (((float) ($metrics['db_size_mb'] ?? 0)) > (float) ($thresholds['db_size_mb'] ?? 0)) $conditions_met++; 342 if (((float) ($metrics['disk_free_space_mb'] ?? \INF)) < (float) ($thresholds['disk_free_mb'] ?? 0)) $conditions_met++; 158 343 159 344 if ($conditions_met >= 3) { … … 163 348 } 164 349 165 $color_map = apply_filters('dfehc_database_health_colors', [166 'ok' => '#00ff00',167 'warn' => '#ffff00',168 'critical' => '# ff0000',350 $color_map = \apply_filters('dfehc_database_health_colors', [ 351 'ok' => '#1a7f37', 352 'warn' => '#9a6700', 353 'critical' => '#d1242f', 169 354 ]); 170 355 171 356 return [ 172 'severity' => $severity,173 'status_color' => $color_map[$severity] ?? '#00ff00',174 'conditions_met' => $conditions_met,175 'thresholds' => $thresholds,176 'metrics' => $metrics,357 'severity' => $severity, 358 'status_color' => $color_map[$severity] ?? '#1a7f37', 359 'conditions_met' => $conditions_met, 360 'thresholds' => $thresholds, 361 'metrics' => $metrics, 177 362 ]; 178 363 } 179 364 180 function dfehc_get_database_health_status(): array 181 { 365 function dfehc_get_database_health_status(): array { 182 366 $metrics = dfehc_gather_database_metrics(); 183 367 return dfehc_evaluate_database_health($metrics); 184 368 } 185 369 186 function check_database_health_on_admin_page(): void 187 { 188 if (is_admin() && isset($_GET['page']) && $_GET['page'] === 'dfehc_plugin') { 189 dfehc_get_database_health_status(); 190 } 191 } 192 add_action('admin_init', __NAMESPACE__ . '\\check_database_health_on_admin_page'); 370 function check_database_health_on_admin_page(): void { 371 if (!\is_admin()) return; 372 if (!\current_user_can('manage_options')) return; 373 $page = isset($_GET['page']) ? \sanitize_text_field((string) $_GET['page']) : ''; 374 if ($page !== 'dfehc_plugin') return; 375 dfehc_get_database_health_status(); 376 } 377 \add_action('admin_init', __NAMESPACE__ . '\\check_database_health_on_admin_page'); -
dynamic-front-end-heartbeat-control/trunk/defibrillator/load-estimator.php
r3320647 r3396626 3 3 4 4 defined('ABSPATH') || exit; 5 6 if (!defined('DFEHC_CACHE_GROUP')) { 7 define('DFEHC_CACHE_GROUP', 'dfehc'); 8 } 5 9 6 10 class Dfehc_ServerLoadEstimator { … … 8 12 const LOAD_CACHE_TRANSIENT = 'dfehc_last_known_load'; 9 13 const LOAD_SPIKE_TRANSIENT = 'dfehc_load_spike_score'; 14 const BASELINE_RESET_CD_PREFIX = 'dfehc_baseline_reset_cd_'; 10 15 11 16 public static function get_server_load(float $duration = 0.025) { 12 if ( !function_exists('microtime') || (defined('DFEHC_DISABLE_LOAD_ESTIMATION') && DFEHC_DISABLE_LOAD_ESTIMATION)) {17 if (\apply_filters('dfehc_disable_loop_estimator', false)) { 13 18 return false; 14 19 } 15 16 $hostname = self::get_hostname_key(); 17 $baseline_t = self::get_baseline_transient_name($hostname); 18 $cache_ttl = (int) apply_filters('dfehc_load_cache_ttl', 90); 19 $cached = get_transient(self::LOAD_CACHE_TRANSIENT); 20 21 if ($cached !== false) { 20 if (!\function_exists('microtime') || (\defined('DFEHC_DISABLE_LOAD_ESTIMATION') && DFEHC_DISABLE_LOAD_ESTIMATION)) { 21 return false; 22 } 23 $duration = (float) \apply_filters('dfehc_loop_duration', $duration); 24 if (!\is_finite($duration) || $duration <= 0.0) { 25 $duration = 0.025; 26 } 27 if ($duration < 0.01) { 28 $duration = 0.01; 29 } 30 if ($duration > 0.5) { 31 $duration = 0.5; 32 } 33 $suffix = self::scope_suffix(); 34 $baselineT = self::get_baseline_transient_name($suffix); 35 $cacheTtl = (int) \apply_filters('dfehc_load_cache_ttl', 90); 36 $cacheKey = self::get_cache_key($suffix); 37 $cached = self::get_scoped_transient($cacheKey); 38 if ($cached !== false && $cached !== null) { 22 39 return $cached; 23 40 } 24 25 $sys_avg = self::try_sys_getloadavg(); 26 if ($sys_avg !== null) { 27 set_transient(self::LOAD_CACHE_TRANSIENT, $sys_avg, $cache_ttl); 28 return $sys_avg; 29 } 30 31 $baseline = self::get_baseline_value($baseline_t); 32 if (!$baseline) { 33 $baseline = self::maybe_calibrate($baseline_t, $duration); 34 } 35 36 $loops_per_sec = self::run_loop($duration); 37 if ($loops_per_sec <= 0) { 41 $sysAvg = self::try_sys_getloadavg(); 42 if ($sysAvg !== null) { 43 self::set_scoped_transient_noautoload($cacheKey, $sysAvg, $cacheTtl); 44 return $sysAvg; 45 } 46 $baseline = self::get_baseline_value($baselineT); 47 if ($baseline === false || $baseline === null || !\is_numeric($baseline) || $baseline <= 0) { 48 $baseline = self::maybe_calibrate($baselineT, $duration); 49 } 50 $loopsPerSec = self::run_loop_avg($duration); 51 if ($loopsPerSec <= 0) { 38 52 return false; 39 53 } 40 41 $load_ratio = ($baseline * 0.125) / max($loops_per_sec, 1); 42 $load_percent = round(min(100, max(0, $load_ratio * 100)), 2); 43 44 self::update_spike_score($load_percent); 45 set_transient(self::LOAD_CACHE_TRANSIENT, $load_percent, $cache_ttl); 46 47 return $load_percent; 54 $loadRatio = ($baseline * 0.125) / \max($loopsPerSec, 1); 55 $loadPercent = \round(\min(100.0, \max(0.0, $loadRatio * 100.0)), 2); 56 $loadPercent = (float) \apply_filters('dfehc_computed_load_percent', $loadPercent, $baseline, $loopsPerSec); 57 self::update_spike_score($loadPercent, $suffix); 58 self::set_scoped_transient_noautoload($cacheKey, $loadPercent, $cacheTtl); 59 return $loadPercent; 48 60 } 49 61 50 62 public static function calibrate_baseline(float $duration = 0.025): float { 51 return self::run_loop($duration); 63 $duration = (float) \apply_filters('dfehc_loop_duration', $duration); 64 if (!\is_finite($duration) || $duration <= 0.0) { 65 $duration = 0.025; 66 } 67 if ($duration < 0.01) { 68 $duration = 0.01; 69 } 70 if ($duration > 0.5) { 71 $duration = 0.5; 72 } 73 return self::run_loop_avg($duration); 74 } 75 76 public static function maybe_calibrate_if_idle(): void { 77 if (\is_admin() || \is_user_logged_in()) { 78 return; 79 } 80 if ((\function_exists('wp_doing_cron') && \wp_doing_cron()) || 81 (\function_exists('wp_doing_ajax') && \wp_doing_ajax()) || 82 (\function_exists('wp_is_json_request') && \wp_is_json_request()) || 83 (\defined('WP_CLI') && WP_CLI)) { 84 return; 85 } 86 $suffix = self::scope_suffix(); 87 $seenKey = 'dfehc_seen_recently_' . $suffix; 88 $ttl = (int) \apply_filters('dfehc_seen_recently_ttl', 60); 89 if (\get_transient($seenKey) !== false) { 90 return; 91 } 92 self::set_transient_noautoload($seenKey, 1, $ttl); 93 self::ensure_baseline(); 52 94 } 53 95 54 96 public static function maybe_calibrate_during_cron(): void { 55 if (!defined('DOING_CRON') || !DOING_CRON) { 56 return; 57 } 97 if (!\function_exists('wp_doing_cron') || !\wp_doing_cron()) { 98 return; 99 } 100 $suffix = self::scope_suffix(); 101 $cdKey = 'dfehc_cron_cal_cd_' . $suffix; 102 $cdTtl = (int) \apply_filters('dfehc_cron_calibration_cooldown', 300); 103 if (\get_transient($cdKey) !== false) { 104 return; 105 } 106 self::set_transient_noautoload($cdKey, 1, $cdTtl); 58 107 self::ensure_baseline(); 59 108 } 60 109 61 public static function maybe_calibrate_if_idle(): void {62 if (is_admin() || is_user_logged_in()) {63 return;64 }65 self::ensure_baseline();66 }67 68 110 private static function try_sys_getloadavg(): ?float { 69 if (! function_exists('sys_getloadavg')) {111 if (!\function_exists('sys_getloadavg')) { 70 112 return null; 71 113 } 72 $avg = sys_getloadavg();73 if (! is_array($avg) || !isset($avg[0])) {114 $avg = \sys_getloadavg(); 115 if (!\is_array($avg) || !isset($avg[0])) { 74 116 return null; 75 117 } 76 $cores = function_exists('dfehc_get_cpu_cores') ? dfehc_get_cpu_cores() : 1; 118 $raw = (float) $avg[0]; 119 $raw = (float) \apply_filters('dfehc_raw_sys_load', $raw); 120 $cores = 0; 121 if (\defined('DFEHC_CPU_CORES')) { 122 $cores = (int) DFEHC_CPU_CORES; 123 } elseif (\function_exists('\dfehc_get_cpu_cores')) { 124 $cores = (int) \dfehc_get_cpu_cores(); 125 } elseif (\function_exists('dfehc_get_cpu_cores')) { 126 $cores = (int) \dfehc_get_cpu_cores(); 127 } 128 $cores = (int) \apply_filters('dfehc_cpu_cores', $cores); 77 129 if ($cores <= 0) { 78 130 $cores = 1; 79 131 } 80 return min(100, round(($avg[0] / $cores) * 100, 2)); 132 return \min(100.0, \round(($raw / $cores) * 100.0, 2)); 133 } 134 135 private static function now(): float { 136 return \function_exists('hrtime') ? (hrtime(true) / 1e9) : \microtime(true); 81 137 } 82 138 83 139 private static function run_loop(float $duration): float { 84 $start = microtime(true); 140 if ($duration <= 0.0) { 141 return 0.0; 142 } 143 if ($duration < 0.01) { 144 $duration = 0.01; 145 } 146 if ($duration > 0.5) { 147 $duration = 0.5; 148 } 149 $duration += \mt_rand(0, 2) * 0.001; 150 $warm = self::now(); 151 for ($i = 0; $i < 1000; $i++) { $warm += 0; } 152 $start = self::now(); 85 153 $end = $start + $duration; 86 154 $cnt = 0; 155 $cap = (int) \apply_filters('dfehc_loop_iteration_cap', 10000000); 87 156 $now = $start; 88 while ($now < $end ) {157 while ($now < $end && $cnt < $cap) { 89 158 ++$cnt; 90 $now = microtime(true);159 $now = self::now(); 91 160 } 92 161 $elapsed = $now - $start; … … 94 163 } 95 164 96 private static function maybe_calibrate(string $baseline_t, float $duration): float { 97 $hostname = self::get_hostname_key(); 98 $lock_key = 'dfehc_calibrating_' . $hostname; 99 $lock = self::acquire_lock($lock_key, 30); 100 101 $baseline = self::run_loop($duration); 165 private static function run_loop_avg(float $duration): float { 166 $a = self::run_loop($duration); 167 $b = self::run_loop(\min(0.5, $duration * 1.5)); 168 if ($a <= 0.0 && $b <= 0.0) { 169 return 0.0; 170 } 171 if ($a <= 0.0) return $b; 172 if ($b <= 0.0) return $a; 173 return ($a + $b) / 2.0; 174 } 175 176 private static function maybe_calibrate(string $baselineT, float $duration): float { 177 $suffix = self::scope_suffix(); 178 $lockKey = 'dfehc_calibrating_' . $suffix; 179 $lock = self::acquire_lock($lockKey, 30); 180 $baseline = self::run_loop_avg($duration); 181 if ($baseline <= 0.0) { 182 $baseline = 1.0; 183 } 102 184 if ($lock) { 103 $exp = (int) apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS);104 self::set_baseline_value($baseline _t, $baseline, $exp);105 self::release_lock($lock, $lock _key);185 $exp = (int) \apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS); 186 self::set_baseline_value($baselineT, $baseline, $exp); 187 self::release_lock($lock, $lockKey); 106 188 } 107 189 return $baseline; 108 190 } 109 191 110 private static function update_spike_score(float $load_percent): void { 111 $score = (float) get_transient(self::LOAD_SPIKE_TRANSIENT); 112 $decay = (float) apply_filters('dfehc_spike_decay', 0.5); 113 $increment = (float) apply_filters('dfehc_spike_increment', 1.0); 114 $threshold = (float) apply_filters('dfehc_spike_threshold', 3.0); 115 $hostname = self::get_hostname_key(); 116 $baseline_name = self::get_baseline_transient_name($hostname); 117 118 if ($load_percent > 90) { 119 $score += $increment; 120 } else { 121 $score = max(0.0, $score - $decay); 122 } 123 192 private static function update_spike_score(float $loadPercent, string $suffix): void { 193 $spikeKey = self::get_spike_key($suffix); 194 $score = (float) self::get_scoped_transient($spikeKey); 195 $decay = (float) \apply_filters('dfehc_spike_decay', 0.5); 196 $increment = (float) \apply_filters('dfehc_spike_increment', 1.0); 197 $threshold = (float) \apply_filters('dfehc_spike_threshold', 3.0); 198 $trigger = (float) \apply_filters('dfehc_spike_trigger', 90.0); 199 $resetCdTtl = (int) \apply_filters('dfehc_baseline_reset_cooldown', 3600); 200 $resetCdKey = self::BASELINE_RESET_CD_PREFIX . $suffix; 201 $scoreMax = (float) \apply_filters('dfehc_spike_score_max', 20.0); 202 if ($loadPercent > $trigger) { 203 $score += $increment * (1 + (($loadPercent - $trigger) / 20.0)); 204 } else { 205 $score = \max(0.0, $score - $decay); 206 } 207 $score = \min($scoreMax, \max(0.0, $score)); 124 208 if ($score >= $threshold) { 125 self::delete_baseline_value($baseline_name); 126 delete_transient(self::LOAD_SPIKE_TRANSIENT); 127 } else { 128 set_transient(self::LOAD_SPIKE_TRANSIENT, $score, HOUR_IN_SECONDS); 129 } 209 if (\get_transient($resetCdKey) === false) { 210 $baselineName = self::get_baseline_transient_name($suffix); 211 self::delete_baseline_value($baselineName); 212 self::set_transient_noautoload($resetCdKey, 1, $resetCdTtl); 213 self::delete_scoped_transient($spikeKey); 214 return; 215 } 216 } 217 self::set_scoped_transient_noautoload($spikeKey, $score, (int) \apply_filters('dfehc_spike_score_ttl', HOUR_IN_SECONDS)); 130 218 } 131 219 132 220 private static function ensure_baseline(): void { 133 $hostname = self::get_hostname_key(); 134 $baseline_t = self::get_baseline_transient_name($hostname); 135 if (self::get_baseline_value($baseline_t)) { 136 return; 137 } 138 $lock_key = 'dfehc_calibrating_' . $hostname; 139 $lock = self::acquire_lock($lock_key, 30); 221 $suffix = self::scope_suffix(); 222 $baselineT = self::get_baseline_transient_name($suffix); 223 $existing = self::get_baseline_value($baselineT); 224 if ($existing !== false && $existing !== null && \is_numeric($existing) && $existing > 0) { 225 return; 226 } 227 $lockKey = 'dfehc_calibrating_' . $suffix; 228 $lock = self::acquire_lock($lockKey, 30); 140 229 if (!$lock) { 141 230 return; 142 231 } 143 $baseline = self::run_loop(0.025); 144 $exp = (int) apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS); 145 self::set_baseline_value($baseline_t, $baseline, $exp); 146 self::release_lock($lock, $lock_key); 232 $duration = (float) \apply_filters('dfehc_loop_duration', 0.025); 233 if (!\is_finite($duration) || $duration <= 0.0) { 234 $duration = 0.025; 235 } 236 if ($duration < 0.01) { 237 $duration = 0.01; 238 } 239 if ($duration > 0.5) { 240 $duration = 0.5; 241 } 242 $baseline = self::run_loop_avg($duration); 243 if ($baseline <= 0.0) { 244 $baseline = 1.0; 245 } 246 $exp = (int) \apply_filters('dfehc_baseline_expiration', 7 * DAY_IN_SECONDS); 247 self::set_baseline_value($baselineT, $baseline, $exp); 248 self::release_lock($lock, $lockKey); 147 249 } 148 250 149 251 private static function acquire_lock(string $key, int $ttl) { 150 if ( class_exists('WP_Lock')) {252 if (\class_exists('\WP_Lock')) { 151 253 $lock = new \WP_Lock($key, $ttl); 152 254 return $lock->acquire() ? $lock : null; 153 255 } 154 return wp_cache_add($key, 1, $ttl) ? (object) ['key' => $key] : null; 256 if (\function_exists('wp_cache_add') && \wp_cache_add($key, 1, DFEHC_CACHE_GROUP, $ttl)) { 257 return (object) ['type' => 'cache', 'key' => $key]; 258 } 259 if (\get_transient($key) !== false) { 260 return null; 261 } 262 if (\set_transient($key, 1, $ttl)) { 263 return (object) ['type' => 'transient', 'key' => $key]; 264 } 265 return null; 155 266 } 156 267 … … 158 269 if ($lock instanceof \WP_Lock) { 159 270 $lock->release(); 160 } else { 161 wp_cache_delete($key); 162 } 163 } 164 165 private static function get_baseline_transient_name(string $hostname): string { 166 return self::BASELINE_TRANSIENT_PREFIX . $hostname; 271 return; 272 } 273 if (\is_object($lock) && isset($lock->type, $lock->key)) { 274 if ($lock->type === 'cache') { 275 \wp_cache_delete($key, DFEHC_CACHE_GROUP); 276 return; 277 } 278 if ($lock->type === 'transient') { 279 \delete_transient($key); 280 return; 281 } 282 } 283 } 284 285 private static function get_baseline_transient_name(string $suffix): string { 286 return self::BASELINE_TRANSIENT_PREFIX . $suffix; 167 287 } 168 288 169 289 private static function get_hostname_key(): string { 170 return substr(md5(php_uname('n')), 0, 10); 290 $host = @\php_uname('n'); 291 if (!$host) { 292 $url = \defined('WP_HOME') && WP_HOME ? WP_HOME : (\function_exists('home_url') ? \home_url() : ''); 293 $parts = \parse_url((string) $url); 294 $host = $parts['host'] ?? ($url ?: 'unknown'); 295 } 296 $salt = \defined('DB_NAME') ? (string) DB_NAME : ''; 297 return \substr(\md5((string) $host . $salt), 0, 10); 298 } 299 300 private static function get_blog_id(): int { 301 return \function_exists('get_current_blog_id') ? (int) \get_current_blog_id() : 0; 302 } 303 304 private static function scope_suffix(): string { 305 $sapi = \php_sapi_name(); 306 $sapiTag = $sapi ? \substr(\preg_replace('/[^a-z0-9]/i', '', strtolower($sapi)), 0, 6) : 'web'; 307 $suffix = self::get_hostname_key() . '_' . self::get_blog_id() . '_' . $sapiTag; 308 $override = \apply_filters('dfehc_baseline_scope_suffix', null, $suffix); 309 if (\is_string($override) && $override !== '') { 310 return $override; 311 } 312 return $suffix; 313 } 314 315 private static function get_cache_key(string $suffix): string { 316 return self::LOAD_CACHE_TRANSIENT . '_' . $suffix; 317 } 318 319 private static function get_spike_key(string $suffix): string { 320 return self::LOAD_SPIKE_TRANSIENT . '_' . $suffix; 171 321 } 172 322 173 323 private static function get_baseline_value(string $name) { 174 return is_multisite() ? get_site_transient($name) :get_transient($name);324 return \is_multisite() ? \get_site_transient($name) : \get_transient($name); 175 325 } 176 326 177 327 private static function set_baseline_value(string $name, $value, int $exp): void { 178 if ( is_multisite()) {179 se t_site_transient($name, $value, $exp);180 } else { 181 se t_transient($name, $value, $exp);328 if (\is_multisite()) { 329 self::set_site_transient_noautoload($name, $value, $exp); 330 } else { 331 self::set_transient_noautoload($name, $value, $exp); 182 332 } 183 333 } 184 334 185 335 private static function delete_baseline_value(string $name): void { 186 if (is_multisite()) { 187 delete_site_transient($name); 188 } else { 189 delete_transient($name); 190 } 336 if (\is_multisite()) { 337 \delete_site_transient($name); 338 } else { 339 \delete_transient($name); 340 } 341 } 342 343 private static function get_scoped_transient(string $key) { 344 return \is_multisite() ? \get_site_transient($key) : \get_transient($key); 345 } 346 347 private static function set_scoped_transient_noautoload(string $key, $value, int $ttl): void { 348 if (\is_multisite()) { 349 self::set_site_transient_noautoload($key, $value, $ttl); 350 } else { 351 self::set_transient_noautoload($key, $value, $ttl); 352 } 353 } 354 355 private static function delete_scoped_transient(string $key): void { 356 if (\is_multisite()) { 357 \delete_site_transient($key); 358 } else { 359 \delete_transient($key); 360 } 361 } 362 363 private static function set_transient_noautoload(string $key, $value, int $ttl): void { 364 $jitter = (\function_exists('random_int') ? \random_int(0, 5) : 0); 365 $ttl = \max(1, $ttl + $jitter); 366 if (\function_exists('wp_using_ext_object_cache') && \wp_using_ext_object_cache()) { 367 \wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $ttl); 368 return; 369 } 370 \set_transient($key, $value, $ttl); 371 global $wpdb; 372 if (!isset($wpdb->options)) { 373 return; 374 } 375 $opt_key = "_transient_$key"; 376 $opt_key_to = "_transient_timeout_$key"; 377 $wpdb->suppress_errors(true); 378 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key)); 379 if ($autoload === 'yes') { 380 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 381 } 382 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to)); 383 if ($autoload_to === 'yes') { 384 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 385 } 386 $wpdb->suppress_errors(false); 387 } 388 389 private static function set_site_transient_noautoload(string $key, $value, int $ttl): void { 390 $jitter = (\function_exists('random_int') ? \random_int(0, 5) : 0); 391 $ttl = \max(1, $ttl + $jitter); 392 if (\function_exists('wp_using_ext_object_cache') && \wp_using_ext_object_cache()) { 393 \wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $ttl); 394 return; 395 } 396 \set_site_transient($key, $value, $ttl); 191 397 } 192 398 } 193 399 194 add_action('init', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_during_cron']);195 add_action('template_redirect', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_if_idle']);196 197 add_filter('heartbeat_settings', function ($settings) {198 if (! class_exists(Dfehc_ServerLoadEstimator::class)) {400 \add_action('init', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_during_cron']); 401 \add_action('template_redirect', [Dfehc_ServerLoadEstimator::class, 'maybe_calibrate_if_idle']); 402 403 \add_filter('heartbeat_settings', function ($settings) { 404 if (!\class_exists(Dfehc_ServerLoadEstimator::class)) { 199 405 return $settings; 200 406 } 201 202 407 $load = Dfehc_ServerLoadEstimator::get_server_load(); 203 408 if ($load === false) { 204 409 return $settings; 205 410 } 206 207 $ths = apply_filters('dfehc_heartbeat_thresholds', [ 411 $ths = \wp_parse_args(\apply_filters('dfehc_heartbeat_thresholds', []), [ 208 412 'low' => 20, 209 413 'medium' => 50, 210 414 'high' => 75, 211 415 ]); 212 213 if (!is_admin() && !current_user_can('edit_posts')) { 214 $settings['interval'] = $load < $ths['low'] ? 50 : ($load < $ths['medium'] ? 60 : ($load < $ths['high'] ? 120 : 180)); 215 } elseif (current_user_can('editor')) { 216 $settings['interval'] = $load < $ths['high'] ? 30 : 60; 217 } elseif (current_user_can('administrator')) { 218 $settings['interval'] = $load < $ths['high'] ? 20 : 40; 219 } 220 416 $suggested = null; 417 if (!\is_admin() && !\current_user_can('edit_posts')) { 418 $suggested = $load < $ths['low'] ? 50 : ($load < $ths['medium'] ? 60 : ($load < $ths['high'] ? 120 : 180)); 419 } elseif (\current_user_can('manage_options')) { 420 $suggested = $load < $ths['high'] ? 20 : 40; 421 } elseif (\current_user_can('edit_others_posts')) { 422 $suggested = $load < $ths['high'] ? 30 : 60; 423 } 424 if ($suggested !== null) { 425 $suggested = (int) \min(\max($suggested, 15), 300); 426 if (isset($settings['interval']) && \is_numeric($settings['interval'])) { 427 $current = (int) $settings['interval']; 428 $settings['interval'] = (int) \min(\max(\max($current, $suggested), 15), 300); 429 } else { 430 $settings['interval'] = $suggested; 431 } 432 } 221 433 return $settings; 222 } );434 }, 5); -
dynamic-front-end-heartbeat-control/trunk/defibrillator/rest-api.php
r3287827 r3396626 2 2 namespace DynamicHeartbeat; 3 3 4 defined('ABSPATH') or die(); 5 6 class DfehcUncloggerRestApi { 7 public function __construct() { 8 add_action('rest_api_init', [$this, 'register_routes']); 9 } 10 11 public function register_routes() { 12 register_rest_route('dfehc-unclogger/v1', '/woocommerce-transients/count/', [ 13 'methods' => 'GET', 14 'callback' => [$this, 'count_woocommerce_transients'], 15 'permission_callback' => [$this, 'permission_check'], 4 if (!\defined('ABSPATH')) { 5 exit; 6 } 7 8 class DfehcUncloggerRestApi 9 { 10 public function __construct() 11 { 12 \add_action('rest_api_init', [$this, 'register_routes']); 13 } 14 15 public function register_routes(): void 16 { 17 \register_rest_route( 18 'dfehc-unclogger/v1', 19 '/woocommerce-transients/count', 20 [ 21 'methods' => \WP_REST_Server::READABLE, 22 'callback' => [$this, 'count_woocommerce_transients'], 23 'permission_callback' => [$this, 'permission_check'], 24 'args' => [ 25 'network' => [ 26 'type' => 'boolean', 27 'required' => false, 28 'description' => 'When true on multisite, count across sites (optionally filtered by "sites").', 29 ], 30 'sites' => [ 31 'type' => 'array', 32 'items' => ['type' => 'integer'], 33 'required' => false, 34 'description' => 'Optional list of site IDs to include when network=true. Also accepts comma-separated string.', 35 ], 36 ], 37 ] 38 ); 39 40 \register_rest_route( 41 'dfehc-unclogger/v1', 42 '/woocommerce-transients/delete', 43 [ 44 'methods' => \WP_REST_Server::CREATABLE, 45 'callback' => [$this, 'delete_woocommerce_transients'], 46 'permission_callback' => [$this, 'permission_check'], 47 'args' => [ 48 'network' => [ 49 'type' => 'boolean', 50 'required' => false, 51 'description' => 'Run across all sites (multisite only, super admins only).', 52 ], 53 'sites' => [ 54 'type' => 'array', 55 'items' => ['type' => 'integer'], 56 'required' => false, 57 'description' => 'Optional list of site IDs to include when network=true. Also accepts comma-separated string.', 58 ], 59 'allow_sql' => [ 60 'type' => 'boolean', 61 'required' => false, 62 'description' => 'Opt-in to SQL fallback (still requires dfehc_unclogger_allow_sql_delete filter to allow).', 63 ], 64 'dry_run' => [ 65 'type' => 'boolean', 66 'required' => false, 67 'description' => 'If true, estimates what would be deleted without performing deletion.', 68 ], 69 'limit' => [ 70 'type' => 'integer', 71 'required' => false, 72 'description' => 'Per-chunk DELETE limit for SQL fallback.', 73 ], 74 'time_budget' => [ 75 'type' => 'number', 76 'required' => false, 77 'description' => 'Maximum seconds to spend in SQL fallback loop.', 78 ], 79 ], 80 ] 81 ); 82 83 \register_rest_route( 84 'dfehc-unclogger/v1', 85 '/woocommerce-cache/clear', 86 [ 87 'methods' => \WP_REST_Server::CREATABLE, 88 'callback' => [$this, 'clear_woocommerce_cache'], 89 'permission_callback' => [$this, 'permission_check'], 90 'args' => [ 91 'network' => [ 92 'type' => 'boolean', 93 'required' => false, 94 'description' => 'Run across all sites (multisite only, super admins only).', 95 ], 96 'sites' => [ 97 'type' => 'array', 98 'items' => ['type' => 'integer'], 99 'required' => false, 100 'description' => 'Optional list of site IDs to include when network=true. Also accepts comma-separated string.', 101 ], 102 'allow_sql' => [ 103 'type' => 'boolean', 104 'required' => false, 105 'description' => 'Opt-in to SQL fallback (still requires dfehc_unclogger_allow_sql_delete filter to allow).', 106 ], 107 'dry_run' => [ 108 'type' => 'boolean', 109 'required' => false, 110 'description' => 'If true, estimates what would be cleared without performing deletion.', 111 ], 112 'limit' => [ 113 'type' => 'integer', 114 'required' => false, 115 'description' => 'Per-chunk DELETE limit for SQL fallback.', 116 ], 117 'time_budget' => [ 118 'type' => 'number', 119 'required' => false, 120 'description' => 'Maximum seconds to spend in SQL fallback loop.', 121 ], 122 ], 123 ] 124 ); 125 } 126 127 public function permission_check(\WP_REST_Request $request) 128 { 129 $allowed = (bool) \apply_filters('dfehc_unclogger_allow_rest', true, $request); 130 if (!$allowed) { 131 return new \WP_Error('dfehc_rest_disabled', 'DFEHC REST endpoints are disabled.', ['status' => 403]); 132 } 133 134 $required_caps = (array) \apply_filters('dfehc_unclogger_required_capabilities', ['manage_options', 'manage_woocommerce'], $request); 135 $can_manage = false; 136 foreach ($required_caps as $cap) { 137 if (\current_user_can($cap)) { 138 $can_manage = true; 139 break; 140 } 141 } 142 if (!$can_manage) { 143 return new \WP_Error('dfehc_forbidden', 'You are not allowed to access this endpoint.', ['status' => 403]); 144 } 145 146 $network = (bool) $this->get_bool_param($request, 'network'); 147 if ($network) { 148 if (!\is_multisite() || !\is_super_admin()) { 149 return new \WP_Error('dfehc_network_forbidden', 'Network-wide action requires super admin on multisite.', ['status' => 403]); 150 } 151 } 152 153 if ((bool) \apply_filters('dfehc_unclogger_rest_rate_limit_enable', true, $request)) { 154 $limit = (int) \apply_filters('dfehc_unclogger_rest_rate_limit', 60, $request); 155 $window = (int) \apply_filters('dfehc_unclogger_rest_rate_window', 60, $request); 156 $user_id = (int) \get_current_user_id(); 157 $ip = (string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); 158 $key = $this->scoped_key('dfehc_unclogger_rl_' . ($user_id ? 'u' . $user_id : 'ip' . \md5($ip))); 159 $cnt = (int) \get_transient($key); 160 if ($cnt >= $limit) { 161 return new \WP_Error('dfehc_rate_limited', 'Rate limited.', ['status' => 429]); 162 } 163 \set_transient($key, $cnt + 1, $window); 164 } 165 166 return true; 167 } 168 169 public function count_woocommerce_transients(\WP_REST_Request $request): \WP_REST_Response 170 { 171 if (!$this->has_woocommerce()) { 172 return \rest_ensure_response([ 173 'available' => false, 174 'reason' => 'woocommerce_not_active', 175 'can_estimate' => null, 176 'count' => null, 177 ]); 178 } 179 180 if (\wp_using_ext_object_cache()) { 181 return \rest_ensure_response([ 182 'available' => true, 183 184 'reason' => 'persistent_object_cache', 185 'can_estimate' => false, 186 'count' => null, 187 ]); 188 } 189 190 $network = (bool) $this->get_bool_param($request, 'network'); 191 $sites = $this->parse_sites_param($request); 192 193 if ($network && \is_multisite()) { 194 $ids = $sites ?: \get_sites(['fields' => 'ids']); 195 $total = 0; 196 foreach ($ids as $blog_id) { 197 $switched = false; 198 \switch_to_blog((int) $blog_id); 199 $switched = true; 200 try { 201 $total += $this->count_wc_transients_for_site(); 202 } finally { 203 if ($switched) { \restore_current_blog(); } 204 } 205 } 206 return \rest_ensure_response([ 207 'available' => true, 208 'reason' => 'ok', 209 'can_estimate' => true, 210 'count' => (int) $total, 211 'network' => true, 212 'sites_counted' => \count($ids), 213 ]); 214 } 215 216 $count = $this->count_wc_transients_for_site(); 217 return \rest_ensure_response([ 218 'available' => true, 219 'reason' => 'ok', 220 'can_estimate' => true, 221 'count' => (int) $count, 222 'network' => false, 16 223 ]); 17 18 register_rest_route('dfehc-unclogger/v1', '/woocommerce-transients/delete/', [ 19 'methods' => 'POST', 20 'callback' => [$this, 'delete_woocommerce_transients'], 21 'permission_callback' => [$this, 'permission_check'], 224 } 225 226 public function delete_woocommerce_transients(\WP_REST_Request $request): \WP_REST_Response 227 { 228 $network = (bool) $this->get_bool_param($request, 'network'); 229 $sites = $this->parse_sites_param($request); 230 231 $allow_sql_req = (bool) $this->get_bool_param($request, 'allow_sql'); 232 $allow_sql_filter = (bool) \apply_filters('dfehc_unclogger_allow_sql_delete', false, $request); 233 $allow_sql = $allow_sql_req && $allow_sql_filter; 234 235 $dry_run = (bool) $this->get_bool_param($request, 'dry_run'); 236 $limit_default = (int) \apply_filters('dfehc_unclogger_sql_delete_limit', 500); 237 $time_default = (float) \apply_filters('dfehc_unclogger_sql_delete_time_budget', 2.5); 238 $limit = (int) ($request->get_param('limit') ?? $limit_default); 239 if ($limit <= 0) { $limit = $limit_default; } 240 $time_budget = (float) ($request->get_param('time_budget') ?? $time_default); 241 if ($time_budget <= 0) { $time_budget = $time_default; } 242 243 $results = $this->run_per_site($network, $sites, function (int $site_id) use ($allow_sql, $limit, $time_budget, $dry_run): array { 244 return $this->delete_wc_transients_for_site($allow_sql, $limit, $time_budget, $dry_run); 245 }); 246 247 return \rest_ensure_response($results + [ 248 'network' => (bool) $network, 249 'action' => 'delete_transients', 250 'allow_sql' => (bool) $allow_sql, 251 'dry_run' => (bool) $dry_run, 252 'limit' => (int) $limit, 253 'time_budget' => (float) $time_budget, 22 254 ]); 23 24 register_rest_route('dfehc-unclogger/v1', '/woocommerce-cache/clear/', [ 25 'methods' => 'POST', 26 'callback' => [$this, 'clear_woocommerce_cache'], 27 'permission_callback' => [$this, 'permission_check'], 255 } 256 257 public function clear_woocommerce_cache(\WP_REST_Request $request): \WP_REST_Response 258 { 259 $network = (bool) $this->get_bool_param($request, 'network'); 260 $sites = $this->parse_sites_param($request); 261 262 $allow_sql_req = (bool) $this->get_bool_param($request, 'allow_sql'); 263 $allow_sql_filter = (bool) \apply_filters('dfehc_unclogger_allow_sql_delete', false, $request); 264 $allow_sql = $allow_sql_req && $allow_sql_filter; 265 266 $dry_run = (bool) $this->get_bool_param($request, 'dry_run'); 267 $limit_default = (int) \apply_filters('dfehc_unclogger_sql_delete_limit', 500); 268 $time_default = (float) \apply_filters('dfehc_unclogger_sql_delete_time_budget', 2.5); 269 $limit = (int) ($request->get_param('limit') ?? $limit_default); 270 if ($limit <= 0) { $limit = $limit_default; } 271 $time_budget = (float) ($request->get_param('time_budget') ?? $time_default); 272 if ($time_budget <= 0) { $time_budget = $time_default; } 273 274 $results = $this->run_per_site($network, $sites, function (int $site_id) use ($allow_sql, $limit, $time_budget, $dry_run): array { 275 return $this->clear_wc_cache_for_site($allow_sql, $limit, $time_budget, $dry_run); 276 }); 277 278 return \rest_ensure_response($results + [ 279 'network' => (bool) $network, 280 'action' => 'clear_cache', 281 'allow_sql' => (bool) $allow_sql, 282 'dry_run' => (bool) $dry_run, 283 'limit' => (int) $limit, 284 'time_budget' => (float) $time_budget, 28 285 ]); 29 286 } 30 287 31 public function permission_check() { 32 return current_user_can('manage_options'); 33 } 34 35 public function count_woocommerce_transients() { 288 protected function has_woocommerce(): bool 289 { 290 return \class_exists('\WC_Cache_Helper') || \function_exists('\wc'); 291 } 292 293 protected function run_per_site(bool $network, array $sites, callable $work): array 294 { 295 $sites_processed = 0; 296 $cleared_via_all = []; 297 $fallback_used = false; 298 $deleted_transients_total = 0; 299 $deleted_timeouts_total = 0; 300 $errors_all = []; 301 302 if ($network && \is_multisite()) { 303 $ids = $sites ?: \get_sites(['fields' => 'ids']); 304 foreach ($ids as $blog_id) { 305 $switched = false; 306 \switch_to_blog((int) $blog_id); 307 $switched = true; 308 try { 309 $res = (array) $work((int) $blog_id); 310 $sites_processed++; 311 $cleared_via_all = \array_merge($cleared_via_all, (array) ($res['cleared_via'] ?? [])); 312 $fallback_used = $fallback_used || !empty($res['fallback_used']); 313 $deleted_transients_total += (int) ($res['deleted_transients'] ?? 0); 314 $deleted_timeouts_total += (int) ($res['deleted_timeouts'] ?? 0); 315 if (!empty($res['errors']) && \is_array($res['errors'])) { 316 $errors_all = \array_merge($errors_all, $res['errors']); 317 } 318 } finally { 319 if ($switched) { \restore_current_blog(); } 320 } 321 } 322 } else { 323 $res = (array) $work(\get_current_blog_id() ? (int) \get_current_blog_id() : 0); 324 $sites_processed = 1; 325 $cleared_via_all = \array_merge($cleared_via_all, (array) ($res['cleared_via'] ?? [])); 326 $fallback_used = !empty($res['fallback_used']); 327 $deleted_transients_total += (int) ($res['deleted_transients'] ?? 0); 328 $deleted_timeouts_total += (int) ($res['deleted_timeouts'] ?? 0); 329 if (!empty($res['errors']) && \is_array($res['errors'])) { 330 $errors_all = \array_merge($errors_all, $res['errors']); 331 } 332 } 333 334 return [ 335 'sites_processed' => (int) $sites_processed, 336 'cleared_via' => \array_values(\array_unique(\array_filter($cleared_via_all, 'strlen'))), 337 'fallback_used' => (bool) $fallback_used, 338 'deleted_transients' => (int) $deleted_transients_total, 339 'deleted_timeouts' => (int) $deleted_timeouts_total, 340 'errors' => \array_values(\array_unique(\array_filter($errors_all, 'strlen'))), 341 ]; 342 } 343 344 protected function delete_wc_transients_for_site(bool $allow_sql, int $limit, float $time_budget, bool $dry_run): array 345 { 346 if (!$this->has_woocommerce()) { 347 return ['cleared_via' => ['no_woocommerce'], 'fallback_used' => false, 'deleted_transients' => 0, 'deleted_timeouts' => 0, 'errors' => []]; 348 } 349 350 $cleared_via = []; 351 $errors = []; 352 $deleted_transients = 0; 353 $deleted_timeouts = 0; 354 $used_fallback = false; 355 356 if (\function_exists('\wc_delete_product_transients')) { 357 if ($dry_run) { 358 $cleared_via[] = 'wc_delete_product_transients(dry_run)'; 359 } else { 360 try { \wc_delete_product_transients(); $cleared_via[] = 'wc_delete_product_transients'; } catch (\Throwable $e) { $errors[] = 'wc_delete_product_transients: ' . $e->getMessage(); } 361 } 362 } 363 364 if (\function_exists('\wc_delete_expired_transients')) { 365 if ($dry_run) { 366 $cleared_via[] = 'wc_delete_expired_transients(dry_run)'; 367 } else { 368 try { \wc_delete_expired_transients(); $cleared_via[] = 'wc_delete_expired_transients'; } catch (\Throwable $e) { $errors[] = 'wc_delete_expired_transients: ' . $e->getMessage(); } 369 } 370 } 371 372 if (\class_exists('\WC_Cache_Helper')) { 373 foreach (['product','shipping','orders'] as $group) { 374 if ($dry_run) { 375 $cleared_via[] = "bump:{$group}(dry_run)"; 376 } else { 377 try { \WC_Cache_Helper::get_transient_version($group, true); $cleared_via[] = "bump:{$group}"; } catch (\Throwable $e) { $errors[] = "bump:{$group}: " . $e->getMessage(); } 378 } 379 } 380 } 381 382 if (!$cleared_via || $allow_sql) { 383 if ($allow_sql && !$dry_run) { 384 $used_fallback = true; 385 $res = $this->chunked_sql_delete($limit, $time_budget, false); 386 $deleted_transients += (int) $res['deleted_transients']; 387 $deleted_timeouts += (int) $res['deleted_timeouts']; 388 $cleared_via[] = 'sql_chunk_delete'; 389 } elseif ($allow_sql && $dry_run) { 390 $used_fallback = true; 391 $res = $this->chunked_sql_delete($limit, $time_budget, true); 392 $deleted_transients += (int) $res['deleted_transients']; 393 $deleted_timeouts += (int) $res['deleted_timeouts']; 394 $cleared_via[] = 'sql_chunk_delete(dry_run)'; 395 } elseif (!$cleared_via) { 396 $cleared_via[] = 'noop'; 397 } 398 } 399 400 return [ 401 'cleared_via' => \array_values(\array_unique($cleared_via)), 402 'fallback_used' => (bool) $used_fallback, 403 'deleted_transients' => (int) $deleted_transients, 404 'deleted_timeouts' => (int) $deleted_timeouts, 405 'errors' => \array_values(\array_unique($errors)), 406 ]; 407 } 408 409 protected function clear_wc_cache_for_site(bool $allow_sql, int $limit, float $time_budget, bool $dry_run): array 410 { 411 if (!$this->has_woocommerce()) { 412 return ['cleared_via' => ['no_woocommerce'], 'fallback_used' => false, 'deleted_transients' => 0, 'deleted_timeouts' => 0, 'errors' => []]; 413 } 414 415 $cleared_via = []; 416 $errors = []; 417 $deleted_transients = 0; 418 $deleted_timeouts = 0; 419 $used_fallback = false; 420 421 if (\class_exists('\WC_Cache_Helper')) { 422 foreach (['product','shipping','orders'] as $group) { 423 if ($dry_run) { 424 $cleared_via[] = "bump:{$group}(dry_run)"; 425 } else { 426 try { \WC_Cache_Helper::get_transient_version($group, true); $cleared_via[] = "bump:{$group}"; } catch (\Throwable $e) { $errors[] = "bump:{$group}: " . $e->getMessage(); } 427 } 428 } 429 } 430 431 if (\function_exists('\wc_delete_product_transients')) { 432 if ($dry_run) { 433 $cleared_via[] = 'wc_delete_product_transients(dry_run)'; 434 } else { 435 try { \wc_delete_product_transients(); $cleared_via[] = 'wc_delete_product_transients'; } catch (\Throwable $e) { $errors[] = 'wc_delete_product_transients: ' . $e->getMessage(); } 436 } 437 } 438 439 if (\function_exists('\wc_delete_expired_transients')) { 440 if ($dry_run) { 441 $cleared_via[] = 'wc_delete_expired_transients(dry_run)'; 442 } else { 443 try { \wc_delete_expired_transients(); $cleared_via[] = 'wc_delete_expired_transients'; } catch (\Throwable $e) { $errors[] = 'wc_delete_expired_transients: ' . $e->getMessage(); } 444 } 445 } 446 447 if ($allow_sql) { 448 if ($dry_run) { 449 $used_fallback = true; 450 $res = $this->chunked_sql_delete($limit, $time_budget, true); 451 $deleted_transients += (int) $res['deleted_transients']; 452 $deleted_timeouts += (int) $res['deleted_timeouts']; 453 $cleared_via[] = 'sql_chunk_delete(dry_run)'; 454 } else { 455 $used_fallback = true; 456 $res = $this->chunked_sql_delete($limit, $time_budget, false); 457 $deleted_transients += (int) $res['deleted_transients']; 458 $deleted_timeouts += (int) $res['deleted_timeouts']; 459 $cleared_via[] = 'sql_chunk_delete'; 460 } 461 } 462 463 if (!$cleared_via) { 464 $cleared_via[] = 'noop'; 465 } 466 467 return [ 468 'cleared_via' => \array_values(\array_unique($cleared_via)), 469 'fallback_used' => (bool) $used_fallback, 470 'deleted_transients' => (int) $deleted_transients, 471 'deleted_timeouts' => (int) $deleted_timeouts, 472 'errors' => \array_values(\array_unique($errors)), 473 ]; 474 } 475 476 protected function chunked_sql_delete(int $limit, float $time_budget, bool $dry_run): array 477 { 478 $deleted_transients = 0; 479 $deleted_timeouts = 0; 480 481 if (\wp_using_ext_object_cache()) { 482 return ['deleted_transients' => 0, 'deleted_timeouts' => 0]; 483 } 484 36 485 global $wpdb; 37 $count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_woocommerce_%'"); 38 return rest_ensure_response(['count' => (int) $count]); 39 } 40 41 public function delete_woocommerce_transients() { 486 487 $limit = max(1, (int) $limit); 488 $time_budget = max(0.1, (float) $time_budget); 489 490 $like_to = $wpdb->esc_like('_transient_timeout_woocommerce_') . '%'; 491 $like = $wpdb->esc_like('_transient_woocommerce_') . '%'; 492 493 if ($dry_run) { 494 $timeouts = (int) $wpdb->get_var($wpdb->prepare( 495 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s", 496 $like_to 497 )); 498 $values = (int) $wpdb->get_var($wpdb->prepare( 499 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s", 500 $like 501 )); 502 return ['deleted_transients' => $values, 'deleted_timeouts' => $timeouts]; 503 } 504 505 $start = \microtime(true); 506 507 do { 508 $count_to = (int) $wpdb->query( 509 $wpdb->prepare( 510 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT %d", 511 $like_to, 512 $limit 513 ) 514 ); 515 $deleted_timeouts += $count_to; 516 517 $count_val = (int) $wpdb->query( 518 $wpdb->prepare( 519 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT %d", 520 $like, 521 $limit 522 ) 523 ); 524 $deleted_transients += $count_val; 525 526 $elapsed = \microtime(true) - $start; 527 $more = ($count_to + $count_val) >= (2 * $limit); 528 } while ($more && $elapsed < $time_budget); 529 530 return ['deleted_transients' => $deleted_transients, 'deleted_timeouts' => $deleted_timeouts]; 531 } 532 533 protected function count_wc_transients_for_site(): int 534 { 42 535 global $wpdb; 43 $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_woocommerce_%'"); 44 return rest_ensure_response(['deleted' => true]); 45 } 46 47 public function clear_woocommerce_cache() { 48 if (function_exists('wc_delete_product_transients')) { 49 wc_delete_product_transients(); 50 } 51 return rest_ensure_response(['cleared' => true]); 536 $like = $wpdb->esc_like('_transient_woocommerce_') . '%'; 537 $count = (int) $wpdb->get_var( 538 $wpdb->prepare( 539 "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s", 540 $like 541 ) 542 ); 543 return $count; 544 } 545 546 protected function parse_sites_param(\WP_REST_Request $request): array 547 { 548 $sites = $request->get_param('sites'); 549 if (is_array($sites)) { 550 return array_values(array_filter(array_map('intval', $sites), fn($n) => $n > 0)); 551 } 552 if (is_string($sites) && $sites !== '') { 553 $parts = array_map('trim', explode(',', $sites)); 554 return array_values(array_filter(array_map('intval', $parts), fn($n) => $n > 0)); 555 } 556 return []; 557 } 558 559 protected function get_bool_param(\WP_REST_Request $request, string $name): bool 560 { 561 $v = $request->get_param($name); 562 if (is_bool($v)) return $v; 563 if (is_numeric($v)) return ((int) $v) !== 0; 564 if (is_string($v)) { 565 $l = strtolower($v); 566 return in_array($l, ['1','true','yes','on'], true); 567 } 568 return false; 569 } 570 571 protected function scoped_key(string $base): string 572 { 573 if (\function_exists('\dfehc_scoped_key')) { 574 return \dfehc_scoped_key($base); 575 } 576 $bid = \function_exists('\get_current_blog_id') ? (string) \get_current_blog_id() : '0'; 577 $host = @\php_uname('n') ?: (\defined('WP_HOME') ? WP_HOME : (\function_exists('\home_url') ? \home_url() : 'unknown')); 578 $salt = \defined('DB_NAME') ? (string) DB_NAME : ''; 579 $tok = \substr(\md5((string) $host . $salt), 0, 10); 580 return "{$base}_{$bid}_{$tok}"; 52 581 } 53 582 } -
dynamic-front-end-heartbeat-control/trunk/defibrillator/unclogger-db.php
r3310561 r3396626 5 5 6 6 if (!class_exists(__NAMESPACE__ . '\DfehcUncloggerDb')) { 7 class DfehcUncloggerDb { 8 9 public function get_database_size() { 10 global $wpdb; 11 return (float) $wpdb->get_var("SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) FROM information_schema.tables WHERE TABLE_SCHEMA = '{$wpdb->dbname}' GROUP BY table_schema"); 7 class DfehcUncloggerDb 8 { 9 private function sanitize_identifier(string $name): string 10 { 11 $name = str_replace('`', '``', $name); 12 return '`' . $name . '`'; 13 } 14 15 private function time_budget_exceeded(float $start, float $budget): bool 16 { 17 return (microtime(true) - $start) >= $budget; 18 } 19 20 public function get_database_size() 21 { 22 global $wpdb; 23 $sql = "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) 24 FROM information_schema.tables 25 WHERE table_schema = %s 26 GROUP BY table_schema"; 27 return (float) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname)); 28 } 29 30 public function count_trashed_posts() 31 { 32 global $wpdb; 33 $sql = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = %s"; 34 return (int) $wpdb->get_var($wpdb->prepare($sql, 'trash')); 35 } 36 37 public function delete_trashed_posts() 38 { 39 global $wpdb; 40 $limit = (int) apply_filters('dfehc_unclogger_delete_limit', 1000); 41 $budget = (float) apply_filters('dfehc_unclogger_time_budget', 3.0); 42 $start = microtime(true); 43 $total = 0; 44 do { 45 $deleted = (int) $wpdb->query( 46 $wpdb->prepare( 47 "DELETE FROM {$wpdb->posts} WHERE post_status = %s ORDER BY ID ASC LIMIT %d", 48 'trash', 49 $limit 50 ) 51 ); 52 $total += $deleted; 53 if ($deleted < $limit) break; 54 } while (!$this->time_budget_exceeded($start, $budget)); 55 return $total; 56 } 57 58 public function count_revisions() 59 { 60 global $wpdb; 61 $sql = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s"; 62 return (int) $wpdb->get_var($wpdb->prepare($sql, 'revision')); 63 } 64 65 public function delete_revisions() 66 { 67 global $wpdb; 68 $limit = (int) apply_filters('dfehc_unclogger_delete_limit', 1000); 69 $budget = (float) apply_filters('dfehc_unclogger_time_budget', 3.0); 70 $start = microtime(true); 71 $total = 0; 72 do { 73 $deleted = (int) $wpdb->query( 74 $wpdb->prepare( 75 "DELETE FROM {$wpdb->posts} WHERE post_type = %s ORDER BY ID ASC LIMIT %d", 76 'revision', 77 $limit 78 ) 79 ); 80 $total += $deleted; 81 if ($deleted < $limit) break; 82 } while (!$this->time_budget_exceeded($start, $budget)); 83 return $total; 84 } 85 86 public function count_auto_drafts() 87 { 88 global $wpdb; 89 $sql = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = %s"; 90 return (int) $wpdb->get_var($wpdb->prepare($sql, 'auto-draft')); 91 } 92 93 public function delete_auto_drafts() 94 { 95 global $wpdb; 96 $limit = (int) apply_filters('dfehc_unclogger_delete_limit', 1000); 97 $budget = (float) apply_filters('dfehc_unclogger_time_budget', 3.0); 98 $start = microtime(true); 99 $total = 0; 100 do { 101 $deleted = (int) $wpdb->query( 102 $wpdb->prepare( 103 "DELETE FROM {$wpdb->posts} WHERE post_status = %s ORDER BY ID ASC LIMIT %d", 104 'auto-draft', 105 $limit 106 ) 107 ); 108 $total += $deleted; 109 if ($deleted < $limit) break; 110 } while (!$this->time_budget_exceeded($start, $budget)); 111 return $total; 112 } 113 114 public function count_orphaned_postmeta() 115 { 116 global $wpdb; 117 $sql = "SELECT COUNT(*) 118 FROM {$wpdb->postmeta} pm 119 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id 120 WHERE p.ID IS NULL"; 121 return (int) $wpdb->get_var($sql); 122 } 123 124 public function delete_orphaned_postmeta() 125 { 126 global $wpdb; 127 $limit = (int) apply_filters('dfehc_unclogger_delete_limit', 2000); 128 $budget = (float) apply_filters('dfehc_unclogger_time_budget', 3.0); 129 $start = microtime(true); 130 $total = 0; 131 do { 132 $deleted = (int) $wpdb->query( 133 $wpdb->prepare( 134 "DELETE pm 135 FROM {$wpdb->postmeta} pm 136 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id 137 WHERE p.ID IS NULL 138 LIMIT %d", 139 $limit 140 ) 141 ); 142 $total += $deleted; 143 if ($deleted < $limit) break; 144 } while (!$this->time_budget_exceeded($start, $budget)); 145 return $total; 146 } 147 148 public function count_woocommerce_transients() 149 { 150 global $wpdb; 151 $like1 = $wpdb->esc_like('_transient_woocommerce_') . '%'; 152 $like2 = $wpdb->esc_like('_transient_timeout_woocommerce_') . '%'; 153 $sql = "SELECT 154 (SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s) + 155 (SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s)"; 156 return (int) $wpdb->get_var($wpdb->prepare($sql, $like1, $like2)); 157 } 158 159 public function delete_woocommerce_transients() 160 { 161 global $wpdb; 162 $limit = (int) apply_filters('dfehc_unclogger_delete_limit', 1000); 163 $budget = (float) apply_filters('dfehc_unclogger_time_budget', 3.0); 164 $start = microtime(true); 165 $like1 = $wpdb->esc_like('_transient_woocommerce_') . '%'; 166 $like2 = $wpdb->esc_like('_transient_timeout_woocommerce_') . '%'; 167 $total = 0; 168 do { 169 $d1 = (int) $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT %d", $like1, $limit)); 170 $d2 = (int) $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT %d", $like2, $limit)); 171 $total += ($d1 + $d2); 172 if ($d1 + $d2 < (2 * $limit)) break; 173 } while (!$this->time_budget_exceeded($start, $budget)); 174 return $total; 175 } 176 177 public function clear_woocommerce_cache() 178 { 179 $cleared = false; 180 if (class_exists('\WC_Cache_Helper')) { 181 try { \WC_Cache_Helper::get_transient_version('product', true); $cleared = true; } catch (\Throwable $e) {} 182 try { \WC_Cache_Helper::get_transient_version('shipping', true); $cleared = true; } catch (\Throwable $e) {} 183 try { \WC_Cache_Helper::get_transient_version('orders', true); $cleared = true; } catch (\Throwable $e) {} 184 } 185 if (function_exists('\wc_delete_product_transients')) { 186 try { \wc_delete_product_transients(); $cleared = true; } catch (\Throwable $e) {} 187 } 188 if (function_exists('\wc_delete_expired_transients')) { 189 try { \wc_delete_expired_transients(); $cleared = true; } catch (\Throwable $e) {} 190 } 191 return (bool) $cleared; 192 } 193 194 public function count_expired_transients() 195 { 196 global $wpdb; 197 $like = $wpdb->esc_like('_transient_timeout_') . '%'; 198 $sql = "SELECT COUNT(*) 199 FROM {$wpdb->options} 200 WHERE option_name LIKE %s 201 AND CAST(option_value AS UNSIGNED) < %d"; 202 return (int) $wpdb->get_var($wpdb->prepare($sql, $like, time())); 203 } 204 205 public function delete_expired_transients() 206 { 207 global $wpdb; 208 $batch = (int) apply_filters('dfehc_delete_transients_batch', 1000); 209 $budget = (float) apply_filters('dfehc_unclogger_time_budget', 3.0); 210 $start = microtime(true); 211 $likeTO = $wpdb->esc_like('_transient_timeout_') . '%'; 212 $count = 0; 213 214 do { 215 $rows = (array) $wpdb->get_col( 216 $wpdb->prepare( 217 "SELECT REPLACE(option_name, %s, '') AS tname 218 FROM {$wpdb->options} 219 WHERE option_name LIKE %s 220 AND CAST(option_value AS UNSIGNED) < %d 221 LIMIT %d", 222 '_transient_timeout_', 223 $likeTO, 224 time(), 225 $batch 226 ) 227 ); 228 if (!$rows) break; 229 foreach ($rows as $name) { 230 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s", "_transient_{$name}")); 231 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s", "_transient_timeout_{$name}")); 232 $count++; 233 } 234 if (count($rows) < $batch) break; 235 } while (!$this->time_budget_exceeded($start, $budget)); 236 237 return $count; 238 } 239 240 public function count_tables_with_different_prefix() 241 { 242 global $wpdb; 243 $sql = "SELECT COUNT(TABLE_NAME) 244 FROM information_schema.TABLES 245 WHERE TABLE_SCHEMA = %s 246 AND TABLE_NAME NOT LIKE %s"; 247 return (int) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 248 } 249 250 public function list_tables_with_different_prefix() 251 { 252 global $wpdb; 253 $sql = "SELECT TABLE_NAME 254 FROM information_schema.TABLES 255 WHERE TABLE_SCHEMA = %s 256 AND TABLE_NAME NOT LIKE %s"; 257 $results = (array) $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 258 return implode(', ', array_map('esc_html', $results)); 259 } 260 261 public function drop_tables_with_different_prefix() 262 { 263 global $wpdb; 264 $allowed_prefixes = (array) apply_filters('dfehc_unclogger_allowed_drop_prefixes', []); 265 if (empty($allowed_prefixes)) { 266 return 0; 267 } 268 $clauses = []; 269 $params = [$wpdb->dbname]; 270 foreach ($allowed_prefixes as $p) { 271 $clauses[] = "TABLE_NAME LIKE %s"; 272 $params[] = $wpdb->esc_like($p) . '%'; 273 } 274 $sql = "SELECT TABLE_NAME 275 FROM information_schema.TABLES 276 WHERE TABLE_SCHEMA = %s 277 AND (" . implode(' OR ', $clauses) . ")"; 278 $tables = (array) $wpdb->get_col($wpdb->prepare($sql, $params)); 279 280 $count = 0; 281 foreach ($tables as $t) { 282 $safe = $this->sanitize_identifier($t); 283 $wpdb->query("DROP TABLE IF EXISTS {$safe}"); 284 $count++; 285 } 286 return $count; 287 } 288 289 public function count_myisam_tables() 290 { 291 global $wpdb; 292 $sql = "SELECT COUNT(*) FROM information_schema.TABLES 293 WHERE TABLE_SCHEMA = %s AND ENGINE = 'MyISAM'"; 294 return (int) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname)); 295 } 296 297 public function list_myisam_tables() 298 { 299 global $wpdb; 300 $sql = "SELECT TABLE_NAME FROM information_schema.TABLES 301 WHERE TABLE_SCHEMA = %s AND ENGINE = 'MyISAM'"; 302 $results = (array) $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname)); 303 return implode(', ', array_map('esc_html', $results)); 304 } 305 306 public function convert_to_innodb() 307 { 308 global $wpdb; 309 $sql = "SELECT TABLE_NAME FROM information_schema.TABLES 310 WHERE TABLE_SCHEMA = %s AND ENGINE = 'MyISAM' AND TABLE_NAME LIKE %s"; 311 $tables = (array) $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 312 313 $budget = (float) apply_filters('dfehc_unclogger_schema_time_budget', 5.0); 314 $start = microtime(true); 315 $count = 0; 316 317 foreach ($tables as $t) { 318 if ($this->time_budget_exceeded($start, $budget)) break; 319 $safe = $this->sanitize_identifier($t); 320 $wpdb->query("ALTER TABLE {$safe} ENGINE=InnoDB"); 321 $count++; 322 } 323 return $count; 324 } 325 326 public function optimize_tables() 327 { 328 global $wpdb; 329 $sql = "SELECT TABLE_NAME FROM information_schema.TABLES 330 WHERE TABLE_SCHEMA = %s AND TABLE_NAME LIKE %s"; 331 $tables = (array) $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 332 333 $budget = (float) apply_filters('dfehc_unclogger_schema_time_budget', 5.0); 334 $start = microtime(true); 335 $count = 0; 336 337 foreach ($tables as $t) { 338 if ($this->time_budget_exceeded($start, $budget)) break; 339 $safe = $this->sanitize_identifier($t); 340 $wpdb->query("OPTIMIZE TABLE {$safe}"); 341 $count++; 342 } 343 return $count; 344 } 345 346 public function count_tables() 347 { 348 global $wpdb; 349 $sql = "SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s"; 350 return (int) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname)); 351 } 352 353 public function optimize_all() 354 { 355 $this->delete_trashed_posts(); 356 $this->delete_revisions(); 357 $this->delete_auto_drafts(); 358 $this->delete_orphaned_postmeta(); 359 $this->delete_expired_transients(); 360 $this->convert_to_innodb(); 361 $this->optimize_tables(); 362 } 363 364 public function set_wp_post_revisions($value) 365 { 366 if (!isset($this->config)) { 367 return new \WP_Error('config_missing', 'Config instance not set.'); 368 } 369 if ($value === 'default') { 370 return $this->config->remove('constant', 'WP_POST_REVISIONS'); 371 } 372 return $this->config->update('constant', 'WP_POST_REVISIONS', $value); 373 } 12 374 } 13 14 public function count_trashed_posts() {15 global $wpdb;16 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}posts WHERE post_status = 'trash'");17 }18 19 public function delete_trashed_posts() {20 global $wpdb;21 return (int) $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE post_status = 'trash'");22 }23 24 public function count_revisions() {25 global $wpdb;26 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}posts WHERE post_type = 'revision'");27 }28 29 public function delete_revisions() {30 global $wpdb;31 return (int) $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE post_type = 'revision'");32 }33 34 public function count_auto_drafts() {35 global $wpdb;36 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}posts WHERE post_status = 'auto-draft'");37 }38 39 public function delete_auto_drafts() {40 global $wpdb;41 return (int) $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE post_status = 'auto-draft'");42 }43 44 public function count_orphaned_postmeta() {45 global $wpdb;46 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}postmeta pm LEFT JOIN {$wpdb->prefix}posts wp ON wp.ID = pm.post_id WHERE wp.ID IS NULL");47 }48 49 public function delete_orphaned_postmeta() {50 global $wpdb;51 return (int) $wpdb->query("DELETE pm FROM {$wpdb->prefix}postmeta pm LEFT JOIN {$wpdb->prefix}posts wp ON wp.ID = pm.post_id WHERE wp.ID IS NULL");52 }53 54 public function count_woocommerce_transients() {55 global $wpdb;56 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '%woocommerce_%transient%'");57 }58 59 public function delete_woocommerce_transients() {60 global $wpdb;61 return (int) $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '%woocommerce_%transient%'");62 }63 64 public function clear_woocommerce_cache() {65 if (function_exists('wc_delete_product_transients')) {66 wc_delete_product_transients();67 }68 69 if (function_exists('wc_cache_helper')) {70 wc_cache_helper()->clear_cache();71 }72 73 if (function_exists('wp_cache_flush')) {74 wp_cache_flush();75 }76 77 return true;78 }79 80 public function count_expired_transients() {81 global $wpdb;82 $query = $wpdb->get_results("SELECT option_value FROM {$wpdb->prefix}options WHERE option_name LIKE '_transient_timeout_%'", ARRAY_A);83 return count(array_filter($query, fn($t) => (int) $t['option_value'] < time()));84 }85 86 public function delete_expired_transients() {87 global $wpdb;88 $query = $wpdb->get_results("SELECT option_name, option_value FROM {$wpdb->prefix}options WHERE option_name LIKE '_transient_timeout_%'", ARRAY_A);89 90 $count = 0;91 foreach ($query as $transient) {92 if ((int) $transient['option_value'] < time()) {93 $name = str_replace('_transient_timeout_', '', $transient['option_name']);94 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}options WHERE option_name = %s", "_transient_{$name}"));95 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}options WHERE option_name = %s", "_transient_timeout_{$name}"));96 $count++;97 }98 }99 return $count;100 }101 102 public function count_tables_with_different_prefix() {103 global $wpdb;104 return (int) $wpdb->get_var("SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}' AND TABLE_NAME NOT LIKE '{$wpdb->base_prefix}%'");105 }106 107 public function list_tables_with_different_prefix() {108 global $wpdb;109 $results = $wpdb->get_results("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}' AND TABLE_NAME NOT LIKE '{$wpdb->base_prefix}%'", ARRAY_A);110 return implode(', ', wp_list_pluck($results, 'TABLE_NAME'));111 }112 113 public function drop_tables_with_different_prefix() {114 global $wpdb;115 $results = $wpdb->get_results("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}' AND TABLE_NAME NOT LIKE '{$wpdb->base_prefix}%'", ARRAY_A);116 117 $count = 0;118 foreach ($results as $row) {119 $table = esc_sql($row['TABLE_NAME']);120 $wpdb->query("DROP TABLE IF EXISTS `{$table}`");121 $count++;122 }123 return $count;124 }125 126 public function count_myisam_tables() {127 global $wpdb;128 $results = $wpdb->get_results("SHOW TABLE STATUS WHERE Engine = 'MyISAM'", ARRAY_A);129 return is_array($results) ? count($results) : 0;130 }131 132 public function list_myisam_tables() {133 global $wpdb;134 $results = $wpdb->get_results("SHOW TABLE STATUS WHERE Engine = 'MyISAM'", ARRAY_A);135 return implode(', ', wp_list_pluck($results, 'Name'));136 }137 138 public function convert_to_innodb() {139 global $wpdb;140 $results = $wpdb->get_results("SHOW TABLE STATUS WHERE Engine = 'MyISAM'", ARRAY_A);141 $count = 0;142 foreach ($results as $row) {143 $table = esc_sql($row['Name']);144 $wpdb->query("ALTER TABLE `{$table}` ENGINE=InnoDB");145 $count++;146 }147 return $count;148 }149 150 public function optimize_tables() {151 global $wpdb;152 $results = $wpdb->get_results("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}'", ARRAY_A);153 $count = 0;154 foreach ($results as $row) {155 $table = esc_sql($row['TABLE_NAME']);156 $wpdb->query("OPTIMIZE TABLE `{$table}`");157 $count++;158 }159 return $count;160 }161 162 public function count_tables() {163 global $wpdb;164 return (int) $wpdb->get_var("SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}'");165 }166 167 public function optimize_all() {168 $this->delete_trashed_posts();169 $this->delete_revisions();170 $this->delete_auto_drafts();171 $this->delete_orphaned_postmeta();172 $this->drop_tables_with_different_prefix();173 $this->delete_expired_transients();174 $this->convert_to_innodb();175 $this->optimize_tables();176 }177 178 public function set_wp_post_revisions($value) {179 if (!isset($this->config)) {180 return new \WP_Error('config_missing', 'Config instance not set.');181 }182 if ($value === 'default') {183 return $this->config->remove('constant', 'WP_POST_REVISIONS');184 }185 return $this->config->update('constant', 'WP_POST_REVISIONS', $value);186 }187 }188 375 } -
dynamic-front-end-heartbeat-control/trunk/defibrillator/unclogger.php
r3347790 r3396626 17 17 ]; 18 18 19 protected array $allowed_tools = [ 20 'delete_trashed_posts', 21 'delete_revisions', 22 'delete_auto_drafts', 23 'delete_orphaned_postmeta', 24 'delete_expired_transients', 25 'count_expired_transients', 26 'convert_to_innodb', 27 'optimize_tables', 28 'optimize_all', 29 'clear_woocommerce_cache', 30 ]; 31 19 32 public function __construct() 20 33 { … … 22 35 $base = rtrim(self::get_plugin_path(), '/\\'); 23 36 $cli = $base . '/cli-helper.php'; 24 25 37 if (!is_file($cli)) { 26 38 $cli = $base . '/defibrillator/cli-helper.php'; 27 39 } 28 29 40 if (is_file($cli)) { 30 41 require_once $cli; … … 33 44 34 45 $this->db = new DfehcUncloggerDb(); 35 36 46 $this->set_default_settings(); 37 47 … … 56 66 } 57 67 } 58 public function get_settings() { 59 return get_option('dfehc_unclogger_settings', $this->default_settings); 60 } 61 62 public function set_setting($req) { 68 69 public function get_settings() 70 { 71 $settings = get_option('dfehc_unclogger_settings', $this->default_settings); 72 return [ 73 'success' => true, 74 'data' => $settings, 75 ]; 76 } 77 78 public function set_setting($req) 79 { 63 80 $data = $req->get_json_params(); 64 65 $setting = sanitize_text_field($data['setting']); 66 $value = sanitize_text_field($data['value']); 67 68 $method = 'set_' . strtolower($setting); 69 if (method_exists($this->db, $method)) { 70 return $this->db->{$method}($value); 71 } 72 73 if (isset($this->tweaks) && method_exists($this->tweaks, $method)) { 74 return $this->tweaks->{$method}($value); 75 } 76 77 return new \WP_Error('invalid_method', 'Setting method not found.'); 78 } 79 80 public function optimize_db($req) { 81 $tool = sanitize_text_field($req['tool'] ?? null); 81 if (!is_array($data)) { 82 return new \WP_Error('invalid_body', 'Invalid JSON payload.'); 83 } 84 85 $setting = sanitize_text_field($data['setting'] ?? ''); 86 $value = $data['value'] ?? null; 87 88 $known = ['auto_cleanup', 'post_revision_limit']; 89 if (!in_array($setting, $known, true)) { 90 return new \WP_Error('invalid_setting', 'Unknown setting key.'); 91 } 92 93 if ($setting === 'auto_cleanup') { 94 $value = (bool) $value; 95 } elseif ($setting === 'post_revision_limit') { 96 $value = max(0, (int) $value); 97 } 98 99 $settings = get_option('dfehc_unclogger_settings', $this->default_settings); 100 $settings[$setting] = $value; 101 update_option('dfehc_unclogger_settings', $settings, true); 102 103 if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { 104 error_log('[DFEHC] Setting updated: ' . $setting . ' => ' . wp_json_encode($value)); 105 } 106 107 return [ 108 'success' => true, 109 'data' => $settings, 110 ]; 111 } 112 113 protected function get_max_load(string $context = 'interactive'): float 114 { 115 $interactive = (float) apply_filters('dfehc_optimize_max_load', 45.0); 116 $background = (float) apply_filters('dfehc_optimize_max_load_background', 5.0); 117 return $context === 'background' ? $background : $interactive; 118 } 119 120 public function optimize_db($req) 121 { 122 $tool = sanitize_key($req->get_param('tool')); 123 124 $uid = get_current_user_id(); 125 $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; 126 $rl_key = 'dfehc_optimize_rl_' . ($uid ? 'u' . $uid : 'ip' . md5($ip)); 127 $rl_limit = (int) apply_filters('dfehc_optimize_rl_limit', 10); 128 $rl_win = (int) apply_filters('dfehc_optimize_rl_window', 60); 129 $rl_cnt = (int) get_transient($rl_key); 130 if ($rl_cnt >= $rl_limit) { 131 return new \WP_Error('rate_limited', 'Too many requests. Please try again shortly.', ['status' => 429]); 132 } 133 set_transient($rl_key, $rl_cnt + 1, $rl_win); 82 134 83 135 if (get_transient('dfehc_optimizing')) { … … 85 137 } 86 138 87 $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : 0; 88 if ($load > 45) { 139 if (!$tool || !in_array($tool, $this->allowed_tools, true) || !method_exists($this->db, $tool)) { 140 return new \WP_Error('invalid_tool', 'No valid optimization tool specified.'); 141 } 142 143 $load = function_exists('dfehc_get_server_load') ? (float) dfehc_get_server_load() : 0.0; 144 if ($load > $this->get_max_load('interactive')) { 89 145 return new \WP_Error('server_busy', 'Server load is too high to run optimization safely.'); 90 }91 92 if (!$tool || !method_exists($this->db, $tool)) {93 return new \WP_Error('invalid_tool', 'No valid optimization tool specified.');94 146 } 95 147 … … 98 150 if ($tool === 'optimize_all') { 99 151 wp_schedule_single_event(time() + 10, 'dfehc_async_optimize_all'); 100 return ['scheduled' => true]; 101 } 102 152 delete_transient('dfehc_optimizing'); 153 return [ 154 'success' => true, 155 'data' => ['scheduled' => true, 'tool' => $tool], 156 ]; 157 } 158 159 $result = null; 103 160 try { 104 $result = call_user_func([$this->db, $tool]); 105 } finally { 161 $result = call_user_func([$this, 'guarded_run_tool'], $tool); 162 if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { 163 error_log('[DFEHC] Ran tool: ' . $tool . ' -> ' . wp_json_encode($result)); 164 } 165 } catch (\Throwable $e) { 106 166 delete_transient('dfehc_optimizing'); 107 } 167 return new \WP_Error('optimize_failed', 'Optimization failed: ' . $e->getMessage()); 168 } 169 170 delete_transient('dfehc_optimizing'); 108 171 109 172 return [ 110 'result' => $result, 111 'tool' => $tool, 112 'settings' => $this->get_settings(), 173 'success' => true, 174 'data' => [ 175 'result' => $result, 176 'tool' => $tool, 177 'settings' => get_option('dfehc_unclogger_settings', $this->default_settings), 178 ], 113 179 ]; 114 180 } 115 181 116 public function get_option($option_name) { 117 $settings = get_option('dfehc_unclogger_settings', []); 118 return $settings[$option_name] ?? false; 119 } 120 121 public function update_option($option_name, $value) { 122 $settings = get_option('dfehc_unclogger_settings', []); 123 $settings[$option_name] = $value; 124 update_option('dfehc_unclogger_settings', $settings, true); 125 } 126 127 public function register_rest_routes() { 182 protected function guarded_run_tool(string $tool) 183 { 184 if (!method_exists($this->db, $tool)) { 185 throw new \RuntimeException('Unknown tool: ' . $tool); 186 } 187 if (in_array($tool, ['convert_to_innodb', 'optimize_tables', 'optimize_all'], true)) { 188 $load = function_exists('dfehc_get_server_load') ? (float) dfehc_get_server_load() : 0.0; 189 if ($load > $this->get_max_load('interactive')) { 190 throw new \RuntimeException('Server load spiked during operation.'); 191 } 192 } 193 return call_user_func([$this->db, $tool]); 194 } 195 196 public function register_rest_routes() 197 { 128 198 register_rest_route('dfehc-unclogger/v1', '/get/', [ 129 'methods' => 'GET',130 'permission_callback' => 'dfehc_permission_check',131 'callback' => [$this, 'get_settings'],199 'methods' => \WP_REST_Server::READABLE, 200 'permission_callback' => __NAMESPACE__ . '\\dfehc_permission_check', 201 'callback' => [$this, 'get_settings'], 132 202 ]); 133 203 134 204 register_rest_route('dfehc-unclogger/v1', '/optimize-db/(?P<tool>[^/]+)', [ 135 'methods' => 'GET', 136 'permission_callback' => 'dfehc_permission_check', 137 'callback' => [$this, 'optimize_db'], 205 'methods' => \WP_REST_Server::CREATABLE, 206 'permission_callback' => __NAMESPACE__ . '\\dfehc_permission_check', 207 'callback' => [$this, 'optimize_db'], 208 'args' => [ 209 'tool' => [ 210 'required' => true, 211 'sanitize_callback' => 'sanitize_key', 212 ], 213 ], 138 214 ]); 139 215 140 216 register_rest_route('dfehc-unclogger/v1', '/set/', [ 141 'methods' => 'POST',142 'permission_callback' => 'dfehc_permission_check',143 'callback' => [$this, 'set_setting'],217 'methods' => \WP_REST_Server::CREATABLE, 218 'permission_callback' => __NAMESPACE__ . '\\dfehc_permission_check', 219 'callback' => [$this, 'set_setting'], 144 220 ]); 145 221 } 146 222 147 public function __call($method, $args) { 223 public function __call($method, $args) 224 { 148 225 if (method_exists($this->db, $method)) { 149 226 return call_user_func_array([$this->db, $method], $args); 150 227 } 151 152 228 throw new \BadMethodCallException("Method {$method} does not exist."); 153 229 } 154 230 } 155 231 156 function dfehc_permission_check() { 157 return apply_filters('dfehc_unclogger_permission_check', current_user_can('manage_options')); 158 } 159 160 function dfehc_async_optimize_all() { 232 function dfehc_permission_check() 233 { 234 return (bool) apply_filters('dfehc_unclogger_permission_check', current_user_can('manage_options')); 235 } 236 237 function dfehc_async_optimize_all() 238 { 161 239 if (get_transient('dfehc_optimizing')) { 162 240 return; 163 241 } 164 242 165 $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : 0; 166 if ($load > 5) { 243 $load = function_exists('dfehc_get_server_load') ? (float) dfehc_get_server_load() : 0.0; 244 $max = (float) apply_filters('dfehc_optimize_max_load_background', 5.0); 245 if ($load > $max) { 167 246 return; 168 247 } … … 170 249 set_transient('dfehc_optimizing', 1, 300); 171 250 251 $prev = ignore_user_abort(true); 252 if (function_exists('set_time_limit')) { 253 @set_time_limit(60); 254 } 255 172 256 try { 173 $db = new \DynamicHeartbeat\DfehcUncloggerDb();257 $db = class_exists(__NAMESPACE__ . '\\DfehcUncloggerDb') ? new DfehcUncloggerDb() : new \DynamicHeartbeat\DfehcUncloggerDb(); 174 258 $db->optimize_all(); 259 if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { 260 error_log('[DFEHC] optimize_all completed.'); 261 } 175 262 } finally { 263 ignore_user_abort($prev); 176 264 delete_transient('dfehc_optimizing'); 177 265 } 178 266 } 179 267 268 add_action('dfehc_async_optimize_all', __NAMESPACE__ . '\\dfehc_async_optimize_all'); 269 180 270 if (defined('WP_CLI') && WP_CLI) { 181 \WP_CLI::add_command('dfehc optimize', function () {271 \WP_CLI::add_command('dfehc optimize', function () { 182 272 if (get_transient('dfehc_optimizing')) { 183 273 \WP_CLI::error('Optimization already in progress.'); 184 274 } 185 186 $ load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : 0;187 if ($load > 5) {275 $load = function_exists('dfehc_get_server_load') ? (float) dfehc_get_server_load() : 0.0; 276 $max = (float) apply_filters('dfehc_optimize_max_load_background', 5.0); 277 if ($load > $max) { 188 278 \WP_CLI::error('Server load too high to proceed.'); 189 279 } 190 191 280 set_transient('dfehc_optimizing', 1, 300); 192 281 $prev = ignore_user_abort(true); 282 if (function_exists('set_time_limit')) { 283 @set_time_limit(60); 284 } 193 285 try { 194 $db = new \DynamicHeartbeat\DfehcUncloggerDb();286 $db = class_exists(__NAMESPACE__ . '\\DfehcUncloggerDb') ? new DfehcUncloggerDb() : new \DynamicHeartbeat\DfehcUncloggerDb(); 195 287 $db->optimize_all(); 196 288 \WP_CLI::success('Database optimized successfully.'); 197 289 } finally { 290 ignore_user_abort($prev); 198 291 delete_transient('dfehc_optimizing'); 199 292 } … … 203 296 $dfehc_unclogger = new \DynamicHeartbeat\DfehcUnclogger(); 204 297 205 class DfehcUncloggerDb { 206 207 public function get_database_size() { 208 global $wpdb; 209 return (float) $wpdb->get_var("SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) FROM information_schema.tables WHERE TABLE_SCHEMA = '{$wpdb->dbname}' GROUP BY table_schema"); 210 } 211 212 public function count_trashed_posts() { 213 global $wpdb; 214 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}posts WHERE post_status = 'trash'"); 215 } 216 217 public function delete_trashed_posts() { 218 global $wpdb; 219 return (int) $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE post_status = 'trash'"); 220 } 221 222 public function count_revisions() { 223 global $wpdb; 224 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}posts WHERE post_type = 'revision'"); 225 } 226 227 public function delete_revisions() { 228 global $wpdb; 229 return (int) $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE post_type = 'revision'"); 230 } 231 232 public function count_auto_drafts() { 233 global $wpdb; 234 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}posts WHERE post_status = 'auto-draft'"); 235 } 236 237 public function delete_auto_drafts() { 238 global $wpdb; 239 return (int) $wpdb->query("DELETE FROM {$wpdb->prefix}posts WHERE post_status = 'auto-draft'"); 240 } 241 242 public function count_orphaned_postmeta() { 243 global $wpdb; 244 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}postmeta pm LEFT JOIN {$wpdb->prefix}posts wp ON wp.ID = pm.post_id WHERE wp.ID IS NULL"); 245 } 246 247 public function delete_orphaned_postmeta() { 248 global $wpdb; 249 return (int) $wpdb->query("DELETE pm FROM {$wpdb->prefix}postmeta pm LEFT JOIN {$wpdb->prefix}posts wp ON wp.ID = pm.post_id WHERE wp.ID IS NULL"); 250 } 251 252 public function count_woocommerce_transients() { 253 global $wpdb; 254 return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '%woocommerce_%transient%'"); 255 } 256 257 public function delete_woocommerce_transients() { 258 global $wpdb; 259 return (int) $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '%woocommerce_%transient%'"); 260 } 261 262 public function clear_woocommerce_cache() { 263 if (function_exists('wc_delete_product_transients')) { 264 wc_delete_product_transients(); 265 } 266 267 if (function_exists('wc_cache_helper')) { 268 wc_cache_helper()->clear_cache(); 269 } 270 271 if (function_exists('wp_cache_flush')) { 272 wp_cache_flush(); 273 } 274 275 return true; 276 } 277 278 public function count_expired_transients() { 279 global $wpdb; 280 $query = $wpdb->get_results("SELECT option_value FROM {$wpdb->prefix}options WHERE option_name LIKE '_transient_timeout_%'", ARRAY_A); 281 return count(array_filter($query, fn($t) => (int) $t['option_value'] < time())); 282 } 283 284 public function delete_expired_transients() { 285 global $wpdb; 286 $query = $wpdb->get_results("SELECT option_name, option_value FROM {$wpdb->prefix}options WHERE option_name LIKE '_transient_timeout_%'", ARRAY_A); 298 if (!class_exists(__NAMESPACE__ . '\\DfehcUncloggerDb')) { 299 class DfehcUncloggerDb 300 { 301 private function sanitize_ident(string $name): string 302 { 303 return preg_replace('/[^A-Za-z0-9$_]/', '', $name); 304 } 305 306 public function get_database_size() 307 { 308 global $wpdb; 309 $sql = "SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) 310 FROM information_schema.tables 311 WHERE table_schema = %s 312 GROUP BY table_schema"; 313 return (float) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname)); 314 } 315 316 public function count_trashed_posts() 317 { 318 global $wpdb; 319 $sql = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = %s"; 320 return (int) $wpdb->get_var($wpdb->prepare($sql, 'trash')); 321 } 322 323 public function delete_trashed_posts() 324 { 325 global $wpdb; 326 $sql = "DELETE FROM {$wpdb->posts} WHERE post_status = %s"; 327 return (int) $wpdb->query($wpdb->prepare($sql, 'trash')); 328 } 329 330 public function count_revisions() 331 { 332 global $wpdb; 333 $sql = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = %s"; 334 return (int) $wpdb->get_var($wpdb->prepare($sql, 'revision')); 335 } 336 337 public function delete_revisions() 338 { 339 global $wpdb; 340 $sql = "DELETE FROM {$wpdb->posts} WHERE post_type = %s"; 341 return (int) $wpdb->query($wpdb->prepare($sql, 'revision')); 342 } 343 344 public function count_auto_drafts() 345 { 346 global $wpdb; 347 $sql = "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = %s"; 348 return (int) $wpdb->get_var($wpdb->prepare($sql, 'auto-draft')); 349 } 350 351 public function delete_auto_drafts() 352 { 353 global $wpdb; 354 $sql = "DELETE FROM {$wpdb->posts} WHERE post_status = %s"; 355 return (int) $wpdb->query($wpdb->prepare($sql, 'auto-draft')); 356 } 357 358 public function count_orphaned_postmeta() 359 { 360 global $wpdb; 361 $sql = "SELECT COUNT(*) 362 FROM {$wpdb->postmeta} pm 363 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id 364 WHERE p.ID IS NULL"; 365 return (int) $wpdb->get_var($sql); 366 } 367 368 public function delete_orphaned_postmeta() 369 { 370 global $wpdb; 371 $sql = "DELETE pm 372 FROM {$wpdb->postmeta} pm 373 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id 374 WHERE p.ID IS NULL"; 375 return (int) $wpdb->query($sql); 376 } 377 378 public function count_woocommerce_transients() 379 { 380 global $wpdb; 381 $like_val = $wpdb->esc_like('_transient_woocommerce_') . '%'; 382 $like_to = $wpdb->esc_like('_transient_timeout_woocommerce_') . '%'; 383 $sql = "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s"; 384 return (int) $wpdb->get_var($wpdb->prepare($sql, $like_val, $like_to)); 385 } 386 387 public function delete_woocommerce_transients() 388 { 389 global $wpdb; 390 $like_val = $wpdb->esc_like('_transient_woocommerce_') . '%'; 391 $like_to = $wpdb->esc_like('_transient_timeout_woocommerce_') . '%'; 392 $sql1 = $wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like_val); 393 $sql2 = $wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $like_to); 394 $c1 = (int) $wpdb->query($sql1); 395 $c2 = (int) $wpdb->query($sql2); 396 return $c1 + $c2; 397 } 398 399 public function clear_woocommerce_cache() 400 { 401 if (class_exists('\WC_Cache_Helper')) { 402 try { \WC_Cache_Helper::get_transient_version('product', true); } catch (\Throwable $e) {} 403 try { \WC_Cache_Helper::get_transient_version('shipping', true); } catch (\Throwable $e) {} 404 try { \WC_Cache_Helper::get_transient_version('orders', true); } catch (\Throwable $e) {} 405 } 406 if (function_exists('\wc_delete_product_transients')) { 407 \wc_delete_product_transients(); 408 } 409 return true; 410 } 411 412 413 public function count_expired_transients() 414 { 415 global $wpdb; 416 $like_to = $wpdb->esc_like('_transient_timeout_') . '%'; 417 $sql = "SELECT COUNT(*) 418 FROM {$wpdb->options} 419 WHERE option_name LIKE %s 420 AND CAST(option_value AS UNSIGNED) < %d"; 421 return (int) $wpdb->get_var($wpdb->prepare($sql, $like_to, time())); 422 } 423 424 public function delete_expired_transients() 425 { 426 global $wpdb; 427 $like_to = $wpdb->esc_like('_transient_timeout_') . '%'; 428 $names_sql = "SELECT REPLACE(option_name, %s, '') AS tname 429 FROM {$wpdb->options} 430 WHERE option_name LIKE %s 431 AND CAST(option_value AS UNSIGNED) < %d 432 LIMIT %d"; 433 $batch = (int) apply_filters('dfehc_delete_transients_batch', 1000); 434 $time_budget = (float) apply_filters('dfehc_delete_transients_time_budget', 2.5); 435 $start = microtime(true); 287 436 288 437 $count = 0; 289 foreach ($query as $transient) { 290 if ((int) $transient['option_value'] < time()) { 291 $name = str_replace('_transient_timeout_', '', $transient['option_name']); 292 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}options WHERE option_name = %s", "_transient_{$name}")); 293 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->prefix}options WHERE option_name = %s", "_transient_timeout_{$name}")); 438 do { 439 $rows = $wpdb->get_col($wpdb->prepare($names_sql, '_transient_timeout_', $like_to, time(), $batch)); 440 if (!$rows) { 441 break; 442 } 443 foreach ($rows as $name) { 444 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s", "_transient_{$name}")); 445 $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name = %s", "_transient_timeout_{$name}")); 294 446 $count++; 295 } 296 } 447 if ((microtime(true) - $start) > $time_budget) { 448 break 2; 449 } 450 } 451 } while (true); 452 297 453 return $count; 298 454 } 299 455 300 public function count_tables_with_different_prefix() { 301 global $wpdb; 302 return (int) $wpdb->get_var("SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}' AND TABLE_NAME NOT LIKE '{$wpdb->base_prefix}%'"); 303 } 304 305 public function list_tables_with_different_prefix() { 306 global $wpdb; 307 $results = $wpdb->get_results("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}' AND TABLE_NAME NOT LIKE '{$wpdb->base_prefix}%'", ARRAY_A); 308 return implode(', ', wp_list_pluck($results, 'TABLE_NAME')); 309 } 310 311 public function drop_tables_with_different_prefix() { 312 global $wpdb; 313 $results = $wpdb->get_results("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}' AND TABLE_NAME NOT LIKE '{$wpdb->base_prefix}%'", ARRAY_A); 456 public function count_tables_with_different_prefix() 457 { 458 global $wpdb; 459 $sql = "SELECT COUNT(TABLE_NAME) 460 FROM information_schema.TABLES 461 WHERE TABLE_SCHEMA = %s 462 AND TABLE_NAME NOT LIKE %s"; 463 return (int) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 464 } 465 466 public function list_tables_with_different_prefix() 467 { 468 global $wpdb; 469 $sql = "SELECT TABLE_NAME 470 FROM information_schema.TABLES 471 WHERE TABLE_SCHEMA = %s 472 AND TABLE_NAME NOT LIKE %s"; 473 $results = $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 474 return implode(', ', array_map('esc_html', (array) $results)); 475 } 476 477 public function drop_tables_with_different_prefix() 478 { 479 global $wpdb; 480 $allowed_prefixes = (array) apply_filters('dfehc_unclogger_allowed_drop_prefixes', []); 481 if (empty($allowed_prefixes)) { 482 return 0; 483 } 484 $sql = "SELECT TABLE_NAME 485 FROM information_schema.TABLES 486 WHERE TABLE_SCHEMA = %s 487 AND (" . implode(' OR ', array_fill(0, count($allowed_prefixes), "TABLE_NAME LIKE %s")) . ")"; 488 $params = [$wpdb->dbname]; 489 foreach ($allowed_prefixes as $p) { 490 $params[] = $wpdb->esc_like($p) . '%'; 491 } 492 $prepared = $wpdb->prepare($sql, $params); 493 $results = $wpdb->get_col($prepared); 314 494 315 495 $count = 0; 316 foreach ($results as $row) { 317 $table = esc_sql($row['TABLE_NAME']); 318 $wpdb->query("DROP TABLE IF EXISTS `{$table}`"); 496 foreach ((array) $results as $table) { 497 $safe = $this->sanitize_ident($table); 498 if ($safe === '') { 499 continue; 500 } 501 $wpdb->query("DROP TABLE IF EXISTS `{$safe}`"); 319 502 $count++; 320 503 } … … 322 505 } 323 506 324 public function count_myisam_tables() { 325 global $wpdb; 326 $results = $wpdb->get_results("SHOW TABLE STATUS WHERE Engine = 'MyISAM'", ARRAY_A); 327 return is_array($results) ? count($results) : 0; 328 } 329 330 public function list_myisam_tables() { 331 global $wpdb; 332 $results = $wpdb->get_results("SHOW TABLE STATUS WHERE Engine = 'MyISAM'", ARRAY_A); 333 return implode(', ', wp_list_pluck($results, 'Name')); 334 } 335 336 public function convert_to_innodb() { 337 global $wpdb; 338 $results = $wpdb->get_results("SHOW TABLE STATUS WHERE Engine = 'MyISAM'", ARRAY_A); 507 public function count_myisam_tables() 508 { 509 global $wpdb; 510 $sql = "SELECT COUNT(*) FROM information_schema.TABLES 511 WHERE TABLE_SCHEMA = %s AND ENGINE = 'MyISAM'"; 512 return (int) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname)); 513 } 514 515 public function list_myisam_tables() 516 { 517 global $wpdb; 518 $sql = "SELECT TABLE_NAME FROM information_schema.TABLES 519 WHERE TABLE_SCHEMA = %s AND ENGINE = 'MyISAM'"; 520 $results = $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname)); 521 return implode(', ', array_map('esc_html', (array) $results)); 522 } 523 524 public function convert_to_innodb() 525 { 526 global $wpdb; 527 $sql = "SELECT TABLE_NAME FROM information_schema.TABLES 528 WHERE TABLE_SCHEMA = %s AND ENGINE = 'MyISAM' AND TABLE_NAME LIKE %s"; 529 $tables = $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 530 339 531 $count = 0; 340 foreach ($results as $row) { 341 $table = esc_sql($row['Name']); 342 $wpdb->query("ALTER TABLE `{$table}` ENGINE=InnoDB"); 532 foreach ((array) $tables as $t) { 533 $safe = $this->sanitize_ident($t); 534 if ($safe === '') { 535 continue; 536 } 537 $wpdb->query("ALTER TABLE `{$safe}` ENGINE=InnoDB"); 343 538 $count++; 344 539 } … … 346 541 } 347 542 348 public function optimize_tables() { 349 global $wpdb; 350 $results = $wpdb->get_results("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}'", ARRAY_A); 543 public function optimize_tables() 544 { 545 global $wpdb; 546 $sql = "SELECT TABLE_NAME FROM information_schema.TABLES 547 WHERE TABLE_SCHEMA = %s AND TABLE_NAME LIKE %s"; 548 $tables = $wpdb->get_col($wpdb->prepare($sql, $wpdb->dbname, $wpdb->base_prefix . '%')); 549 351 550 $count = 0; 352 foreach ($results as $row) { 353 $table = esc_sql($row['TABLE_NAME']); 354 $wpdb->query("OPTIMIZE TABLE `{$table}`"); 551 foreach ((array) $tables as $t) { 552 $safe = $this->sanitize_ident($t); 553 if ($safe === '') { 554 continue; 555 } 556 $wpdb->query("OPTIMIZE TABLE `{$safe}`"); 355 557 $count++; 356 558 } … … 358 560 } 359 561 360 public function count_tables() { 361 global $wpdb; 362 return (int) $wpdb->get_var("SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA = '{$wpdb->dbname}'"); 363 } 364 365 public function optimize_all() { 562 public function count_tables() 563 { 564 global $wpdb; 565 $sql = "SELECT COUNT(TABLE_NAME) FROM information_schema.TABLES WHERE TABLE_SCHEMA = %s"; 566 return (int) $wpdb->get_var($wpdb->prepare($sql, $wpdb->dbname)); 567 } 568 569 public function optimize_all() 570 { 366 571 $this->delete_trashed_posts(); 367 572 $this->delete_revisions(); 368 573 $this->delete_auto_drafts(); 369 574 $this->delete_orphaned_postmeta(); 370 $this->drop_tables_with_different_prefix();371 575 $this->delete_expired_transients(); 372 576 $this->convert_to_innodb(); … … 374 578 } 375 579 376 public function set_wp_post_revisions($value) { 580 public function set_wp_post_revisions($value) 581 { 377 582 if (!isset($this->config)) { 378 583 return new \WP_Error('config_missing', 'Config instance not set.'); … … 384 589 } 385 590 } 591 } -
dynamic-front-end-heartbeat-control/trunk/engine/interval-helper.php
r3320647 r3396626 2 2 declare(strict_types=1); 3 3 4 define('DFEHC_OPTIONS_PREFIX', 'dfehc_'); 5 define('DFEHC_OPTION_MIN_INTERVAL', DFEHC_OPTIONS_PREFIX . 'min_interval'); 6 define('DFEHC_OPTION_MAX_INTERVAL', DFEHC_OPTIONS_PREFIX . 'max_interval'); 7 define('DFEHC_OPTION_PRIORITY_SLIDER', DFEHC_OPTIONS_PREFIX . 'priority_slider'); 8 define('DFEHC_OPTION_EMA_ALPHA', DFEHC_OPTIONS_PREFIX . 'ema_alpha'); 9 define('DFEHC_OPTION_MAX_SERVER_LOAD', DFEHC_OPTIONS_PREFIX . 'max_server_load'); 10 define('DFEHC_OPTION_MAX_RESPONSE_TIME', DFEHC_OPTIONS_PREFIX . 'max_response_time'); 11 define('DFEHC_OPTION_SMA_WINDOW', DFEHC_OPTIONS_PREFIX . 'sma_window'); 12 define('DFEHC_OPTION_MAX_DECREASE_RATE', DFEHC_OPTIONS_PREFIX . 'max_decrease_rate'); 13 14 define('DFEHC_DEFAULT_MIN_INTERVAL', 15); 15 define('DFEHC_DEFAULT_MAX_INTERVAL', 300); 16 define('DFEHC_DEFAULT_MAX_SERVER_LOAD', 85); 17 define('DFEHC_DEFAULT_MAX_RESPONSE_TIME', 5.0); 18 define('DFEHC_DEFAULT_EMA_ALPHA', 0.4); 19 define('DFEHC_DEFAULT_SMA_WINDOW', 5); 20 define('DFEHC_DEFAULT_MAX_DECREASE_RATE', 0.25); 21 define('DFEHC_DEFAULT_EMA_TTL', 600); 22 23 function dfehc_store_lockfree(string $key, $value, int $ttl): bool 24 { 25 if (function_exists('wp_cache_add') && wp_cache_add($key, $value, '', $ttl)) { 26 return true; 27 } 28 return set_transient($key, $value, $ttl); 29 } 30 31 function dfehc_set_transient(string $key, float $value, float $interval): void 32 { 33 $ttl = (int) apply_filters( 34 'dfehc_transient_ttl', 35 max(60, (int) ceil($interval) * 2), 36 $key, 37 $value, 38 $interval 39 ); 40 dfehc_store_lockfree($key, $value, $ttl); 41 } 42 43 function dfehc_weighted_sum(array $factors, array $weights): float 44 { 45 $sum = 0.0; 46 foreach ($factors as $k => $v) { 47 $sum += ($weights[$k] ?? 0.0) * $v; 48 } 49 return $sum; 50 } 51 52 function dfehc_normalize_weights(array $weights): array 53 { 54 $total = array_sum($weights); 55 if ($total <= 0) { 56 return array_fill_keys(array_keys($weights), 1 / max(1, count($weights))); 57 } 58 foreach ($weights as $k => $w) { 59 $weights[$k] = $w / $total; 60 } 61 return $weights; 62 } 63 64 function dfehc_apply_exponential_moving_average(float $current): float 65 { 66 $alpha = max( 67 0.01, 68 min(1.0, (float) get_option(DFEHC_OPTION_EMA_ALPHA, DFEHC_DEFAULT_EMA_ALPHA)) 69 ); 70 $key = 'dfehc_ema_' . get_current_blog_id(); 71 $prev = get_transient($key); 72 $ema = ($prev === false) ? $current : $alpha * $current + (1 - $alpha) * (float) $prev; 73 $ttl = (int) apply_filters('dfehc_ema_ttl', DFEHC_DEFAULT_EMA_TTL, $current, $ema); 74 dfehc_store_lockfree($key, $ema, $ttl); 75 return $ema; 76 } 77 78 function dfehc_calculate_recommended_interval( 79 float $time_elapsed, 80 float $load_average, 81 float $server_response_time 82 ): float { 83 $min_interval = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL)); 84 $max_interval = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL)); 85 $max_server_load = max(0.1, (float) get_option(DFEHC_OPTION_MAX_SERVER_LOAD, DFEHC_DEFAULT_MAX_SERVER_LOAD)); 86 $max_response_time = max(0.1, (float) get_option(DFEHC_OPTION_MAX_RESPONSE_TIME, DFEHC_DEFAULT_MAX_RESPONSE_TIME)); 87 88 $factors = [ 89 'user_activity' => max(0.0, min(1.0, $time_elapsed / $max_interval)), 90 'server_load' => max(0.0, min(1.0, $load_average / $max_server_load)), 91 'response_time' => $server_response_time > 0 92 ? max(0.0, min(1.0, $server_response_time / $max_response_time)) 93 : 0.0, 94 ]; 95 96 $slider = max(-1.0, min(1.0, (float) get_option(DFEHC_OPTION_PRIORITY_SLIDER, 0.0))); 97 $weights = [ 98 'user_activity' => 0.4 - 0.2 * $slider, 99 'server_load' => (0.6 + 0.2 * $slider) / 2, 100 'response_time' => (0.6 + 0.2 * $slider) / 2, 101 ]; 102 $weights = dfehc_normalize_weights($weights); 103 104 $raw = $min_interval + dfehc_weighted_sum($factors, $weights) * ($max_interval - $min_interval); 105 $smoothed = dfehc_apply_exponential_moving_average($raw); 106 $lagged = dfehc_defensive_stance($smoothed); 107 108 return max($min_interval, min($max_interval, $lagged)); 109 } 110 111 function dfehc_calculate_interval_based_on_duration( 112 float $avg_duration, 113 float $load_average 114 ): float { 115 $min_interval = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL)); 116 $max_interval = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL)); 117 if ($avg_duration <= $min_interval) { 118 return (float) $min_interval; 119 } 120 if ($avg_duration >= $max_interval) { 121 return (float) $max_interval; 122 } 123 return dfehc_calculate_recommended_interval($avg_duration, $load_average, 0.0); 124 } 125 126 function dfehc_smooth_moving(array $values): float 127 { 128 if (!$values) { 129 return 0.0; 130 } 131 $window = max(1, (int) get_option(DFEHC_OPTION_SMA_WINDOW, DFEHC_DEFAULT_SMA_WINDOW)); 132 $subset = array_slice($values, -$window); 133 if (!$subset) { 134 return 0.0; 135 } 136 return array_sum($subset) / count($subset); 137 } 138 139 function dfehc_defensive_stance(float $proposed): float 140 { 141 $key = 'dfehc_prev_int_' . get_current_blog_id(); 142 $previous = get_transient($key); 143 if ($previous === false) { 144 dfehc_store_lockfree($key, $proposed, (int) ceil($proposed)); 145 return $proposed; 146 } 147 148 $previous = (float) $previous; 149 $max_drop = max( 150 0.0, 151 min(1.0, (float) get_option(DFEHC_OPTION_MAX_DECREASE_RATE, DFEHC_DEFAULT_MAX_DECREASE_RATE)) 152 ); 153 $max_rise = (float) apply_filters('dfehc_max_increase_rate', 0.5); 154 155 $lower = $previous * (1 - $max_drop); 156 $upper = $previous * (1 + $max_rise); 157 $final = max($lower, min($upper, $proposed)); 158 159 dfehc_store_lockfree($key, $final, (int) ceil($final)); 160 return $final; 161 } 4 if (!defined('DFEHC_OPTIONS_PREFIX')) define('DFEHC_OPTIONS_PREFIX', 'dfehc_'); 5 if (!defined('DFEHC_OPTION_MIN_INTERVAL')) define('DFEHC_OPTION_MIN_INTERVAL', DFEHC_OPTIONS_PREFIX . 'min_interval'); 6 if (!defined('DFEHC_OPTION_MAX_INTERVAL')) define('DFEHC_OPTION_MAX_INTERVAL', DFEHC_OPTIONS_PREFIX . 'max_interval'); 7 if (!defined('DFEHC_OPTION_PRIORITY_SLIDER')) define('DFEHC_OPTION_PRIORITY_SLIDER', DFEHC_OPTIONS_PREFIX . 'priority_slider'); 8 if (!defined('DFEHC_OPTION_EMA_ALPHA')) define('DFEHC_OPTION_EMA_ALPHA', DFEHC_OPTIONS_PREFIX . 'ema_alpha'); 9 if (!defined('DFEHC_OPTION_MAX_SERVER_LOAD')) define('DFEHC_OPTION_MAX_SERVER_LOAD', DFEHC_OPTIONS_PREFIX . 'max_server_load'); 10 if (!defined('DFEHC_OPTION_MAX_RESPONSE_TIME')) define('DFEHC_OPTION_MAX_RESPONSE_TIME', DFEHC_OPTIONS_PREFIX . 'max_response_time'); 11 if (!defined('DFEHC_OPTION_SMA_WINDOW')) define('DFEHC_OPTION_SMA_WINDOW', DFEHC_OPTIONS_PREFIX . 'sma_window'); 12 if (!defined('DFEHC_OPTION_MAX_DECREASE_RATE')) define('DFEHC_OPTION_MAX_DECREASE_RATE', DFEHC_OPTIONS_PREFIX . 'max_decrease_rate'); 13 14 if (!defined('DFEHC_DEFAULT_MIN_INTERVAL')) define('DFEHC_DEFAULT_MIN_INTERVAL', 15); 15 if (!defined('DFEHC_DEFAULT_MAX_INTERVAL')) define('DFEHC_DEFAULT_MAX_INTERVAL', 300); 16 if (!defined('DFEHC_DEFAULT_MAX_SERVER_LOAD')) define('DFEHC_DEFAULT_MAX_SERVER_LOAD', 85); 17 if (!defined('DFEHC_DEFAULT_MAX_RESPONSE_TIME')) define('DFEHC_DEFAULT_MAX_RESPONSE_TIME', 5.0); 18 if (!defined('DFEHC_DEFAULT_EMA_ALPHA')) define('DFEHC_DEFAULT_EMA_ALPHA', 0.4); 19 if (!defined('DFEHC_DEFAULT_SMA_WINDOW')) define('DFEHC_DEFAULT_SMA_WINDOW', 5); 20 if (!defined('DFEHC_DEFAULT_MAX_DECREASE_RATE')) define('DFEHC_DEFAULT_MAX_DECREASE_RATE', 0.25); 21 if (!defined('DFEHC_DEFAULT_EMA_TTL')) define('DFEHC_DEFAULT_EMA_TTL', 600); 22 23 if (!function_exists('dfehc_host_token')) { 24 function dfehc_host_token(): string { 25 static $t = ''; 26 if ($t !== '') return $t; 27 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 28 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 29 return $t = substr(md5((string) $host . $salt), 0, 10); 30 } 31 } 32 33 if (!function_exists('dfehc_blog_id')) { 34 function dfehc_blog_id(): int { 35 return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0; 36 } 37 } 38 39 if (!function_exists('dfehc_scoped_key')) { 40 function dfehc_scoped_key(string $base): string { 41 return "{$base}_" . dfehc_blog_id() . '_' . dfehc_host_token(); 42 } 43 } 44 45 if (!function_exists('dfehc_store_lockfree')) { 46 function dfehc_store_lockfree(string $key, $value, int $ttl): bool { 47 $group = defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'; 48 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) { 49 wp_cache_set($key, $value, $group, $ttl); 50 return true; 51 } 52 return set_transient($key, $value, $ttl); 53 } 54 } 55 56 if (!function_exists('dfehc_set_transient_noautoload')) { 57 function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void { 58 $group = defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'; 59 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) { 60 if (function_exists('wp_cache_add')) { 61 if (!wp_cache_add($key, $value, $group, $expiration)) { 62 wp_cache_set($key, $value, $group, $expiration); 63 } 64 } else { 65 wp_cache_set($key, $value, $group, $expiration); 66 } 67 return; 68 } 69 set_transient($key, $value, $expiration); 70 global $wpdb; 71 if (!isset($wpdb->options)) return; 72 $opt_key = "_transient_$key"; 73 $opt_key_to = "_transient_timeout_$key"; 74 $wpdb->suppress_errors(true); 75 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key)); 76 if ($autoload === 'yes') { 77 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 78 } 79 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to)); 80 if ($autoload_to === 'yes') { 81 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 82 } 83 $wpdb->suppress_errors(false); 84 } 85 } 86 87 if (!function_exists('dfehc_set_transient')) { 88 function dfehc_set_transient(string $key, float $value, float $interval): void { 89 $ttl = (int) apply_filters('dfehc_transient_ttl', max(60, (int) ceil($interval) * 2), $key, $value, $interval); 90 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 91 dfehc_set_transient_noautoload($key, $value, $ttl); 92 } 93 } 94 95 if (!function_exists('dfehc_clamp')) { 96 function dfehc_clamp(float $v, float $lo, float $hi): float { 97 return max($lo, min($hi, $v)); 98 } 99 } 100 101 if (!function_exists('dfehc_weighted_sum')) { 102 function dfehc_weighted_sum(array $factors, array $weights): float { 103 $sum = 0.0; 104 foreach ($factors as $k => $v) { 105 $sum += ((float) ($weights[$k] ?? 0.0)) * (float) $v; 106 } 107 return $sum; 108 } 109 } 110 111 if (!function_exists('dfehc_normalize_weights')) { 112 function dfehc_normalize_weights(array $weights): array { 113 $total = array_sum($weights); 114 if ($total <= 0) { 115 $n = max(1, count($weights)); 116 $equal = 1 / $n; 117 foreach ($weights as $k => $_) { 118 $weights[$k] = $equal; 119 } 120 return $weights; 121 } 122 foreach ($weights as $k => $w) { 123 $weights[$k] = $w / $total; 124 } 125 return $weights; 126 } 127 } 128 129 if (!function_exists('dfehc_apply_exponential_moving_average')) { 130 function dfehc_apply_exponential_moving_average(float $current): float { 131 $alpha = dfehc_clamp((float) get_option(DFEHC_OPTION_EMA_ALPHA, DFEHC_DEFAULT_EMA_ALPHA), 0.01, 1.0); 132 $key = dfehc_scoped_key('dfehc_ema'); 133 $prev = get_transient($key); 134 $ema = ($prev === false) ? $current : $alpha * $current + (1 - $alpha) * (float) $prev; 135 $ttl_default = max(DFEHC_DEFAULT_EMA_TTL, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL) * 2); 136 $ttl_default = (int) dfehc_clamp($ttl_default, 60, 86400); 137 $ttl = (int) apply_filters('dfehc_ema_ttl', $ttl_default, $current, $ema); 138 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 139 dfehc_set_transient_noautoload($key, $ema, $ttl); 140 return $ema; 141 } 142 } 143 144 if (!function_exists('dfehc_calculate_recommended_interval')) { 145 function dfehc_calculate_recommended_interval(float $time_elapsed, float $load_average, float $server_response_time): float { 146 $min_interval = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL)); 147 $max_interval = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL)); 148 $max_server_load = max(0.1, (float) get_option(DFEHC_OPTION_MAX_SERVER_LOAD, DFEHC_DEFAULT_MAX_SERVER_LOAD)); 149 $max_response_time = max(0.1, (float) get_option(DFEHC_OPTION_MAX_RESPONSE_TIME, DFEHC_DEFAULT_MAX_RESPONSE_TIME)); 150 151 $custom_norm = apply_filters('dfehc_normalize_load', null, $load_average); 152 if (is_numeric($custom_norm)) { 153 $la = dfehc_clamp((float) $custom_norm, 0.0, 1.0); 154 } else { 155 if ($load_average <= 1.0) { 156 $la = dfehc_clamp($load_average, 0.0, 1.0); 157 } elseif ($load_average <= 100.0) { 158 $la = dfehc_clamp($load_average / 100.0, 0.0, 1.0); 159 } else { 160 $assumed_cores = (float) apply_filters('dfehc_assumed_cores_for_normalization', 8.0); 161 $la = dfehc_clamp($load_average / max(1.0, $assumed_cores), 0.0, 1.0); 162 } 163 } 164 165 $msl_ratio = $max_server_load > 1.0 ? ($max_server_load / 100.0) : $max_server_load; 166 if ($msl_ratio <= 0) $msl_ratio = 1.0; 167 $server_load_factor = dfehc_clamp($la / $msl_ratio, 0.0, 1.0); 168 169 $rt_units = apply_filters('dfehc_response_time_is_ms', null, $server_response_time); 170 $rt = (float) $server_response_time; 171 if ($rt_units === true) { 172 $rt = $rt / 1000.0; 173 } elseif ($rt_units === null) { 174 if ($rt > ($max_response_time * 3) && $rt <= 60000.0) { 175 $rt = $rt / 1000.0; 176 } 177 } 178 $rt = max(0.0, $rt); 179 $response_time_factor = $rt > 0 ? dfehc_clamp($rt / $max_response_time, 0.0, 1.0) : 0.0; 180 181 $factors = [ 182 'user_activity' => dfehc_clamp($time_elapsed / $max_interval, 0.0, 1.0), 183 'server_load' => $server_load_factor, 184 'response_time' => $response_time_factor, 185 ]; 186 $factors = (array) apply_filters('dfehc_interval_factors', $factors, $time_elapsed, $load_average, $server_response_time); 187 188 $slider = dfehc_clamp((float) get_option(DFEHC_OPTION_PRIORITY_SLIDER, 0.0), -1.0, 1.0); 189 $weights = [ 190 'user_activity' => 0.4 - 0.2 * $slider, 191 'server_load' => (0.6 + 0.2 * $slider) / 2, 192 'response_time' => (0.6 + 0.2 * $slider) / 2, 193 ]; 194 $weights = (array) apply_filters('dfehc_interval_weights', $weights, $slider); 195 $weights = dfehc_normalize_weights($weights); 196 197 $raw = $min_interval + dfehc_weighted_sum($factors, $weights) * ($max_interval - $min_interval); 198 $smoothed = dfehc_apply_exponential_moving_average($raw); 199 $lagged = dfehc_defensive_stance($smoothed); 200 201 $final = (float) apply_filters('dfehc_interval_snap', $lagged, $min_interval, $max_interval); 202 $final = dfehc_clamp($final, (float) $min_interval, (float) $max_interval); 203 204 return $final; 205 } 206 } 207 208 if (!function_exists('dfehc_calculate_interval_based_on_duration')) { 209 function dfehc_calculate_interval_based_on_duration(float $avg_duration, float $load_average): float { 210 $min_interval = max(1, (int) get_option(DFEHC_OPTION_MIN_INTERVAL, DFEHC_DEFAULT_MIN_INTERVAL)); 211 $max_interval = max($min_interval, (int) get_option(DFEHC_OPTION_MAX_INTERVAL, DFEHC_DEFAULT_MAX_INTERVAL)); 212 if ($avg_duration <= $min_interval) return (float) $min_interval; 213 if ($avg_duration >= $max_interval) return (float) $max_interval; 214 $proposed = dfehc_calculate_recommended_interval($avg_duration, $load_average, 0.0); 215 return dfehc_defensive_stance($proposed); 216 } 217 } 218 219 if (!function_exists('dfehc_smooth_moving')) { 220 function dfehc_smooth_moving(array $values): float { 221 if (!$values) return 0.0; 222 $window = max(1, (int) get_option(DFEHC_OPTION_SMA_WINDOW, DFEHC_DEFAULT_SMA_WINDOW)); 223 $subset = array_slice($values, -$window); 224 if (!$subset) return 0.0; 225 return array_sum($subset) / count($subset); 226 } 227 } 228 229 if (!function_exists('dfehc_defensive_stance')) { 230 function dfehc_defensive_stance(float $proposed): float { 231 $key = dfehc_scoped_key('dfehc_prev_int'); 232 $previous = get_transient($key); 233 if ($previous === false) { 234 $ttl = (int) apply_filters('dfehc_prev_interval_ttl', 1800); 235 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 236 dfehc_set_transient_noautoload($key, $proposed, $ttl); 237 return $proposed; 238 } 239 $previous = (float) $previous; 240 $max_drop = dfehc_clamp((float) get_option(DFEHC_OPTION_MAX_DECREASE_RATE, DFEHC_DEFAULT_MAX_DECREASE_RATE), 0.0, 0.95); 241 $max_rise = dfehc_clamp((float) apply_filters('dfehc_max_increase_rate', 0.5), 0.0, 5.0); 242 $lower = $previous * (1 - $max_drop); 243 $upper = $previous * (1 + $max_rise); 244 $final = dfehc_clamp($proposed, $lower, $upper); 245 $ttl = (int) apply_filters('dfehc_prev_interval_ttl', 1800); 246 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 247 dfehc_set_transient_noautoload($key, $final, $ttl); 248 return $final; 249 } 250 } -
dynamic-front-end-heartbeat-control/trunk/engine/server-load.php
r3347790 r3396626 9 9 } 10 10 11 define('DFEHC_SERVER_LOAD_TTL', (int) apply_filters('dfehc_server_load_ttl', 180)); 12 define('DFEHC_UNKNOWN_LOAD', 0.404); 13 define('DFEHC_SERVER_LOAD_CACHE_KEY', 'dfehc:server_load'); 14 define('DFEHC_SERVER_LOAD_PAYLOAD_KEY', 'dfehc_server_load_payload'); 15 define('DFEHC_LOAD_LOCK', 'dfehc_load_lock'); 11 if (!defined('DFEHC_CACHE_GROUP')) { 12 define('DFEHC_CACHE_GROUP', 'dfehc'); 13 } 14 if (!defined('DFEHC_SERVER_LOAD_TTL')) { 15 define('DFEHC_SERVER_LOAD_TTL', 180); 16 } 17 if (!defined('DFEHC_SERVER_LOAD_CACHE_KEY')) { 18 define('DFEHC_SERVER_LOAD_CACHE_KEY', 'dfehc:server_load'); 19 } 20 if (!defined('DFEHC_SERVER_LOAD_PAYLOAD_KEY')) { 21 define('DFEHC_SERVER_LOAD_PAYLOAD_KEY', 'dfehc_server_load_payload'); 22 } 23 24 if (!function_exists('dfehc_server_load_ttl')) { 25 function dfehc_server_load_ttl(): int 26 { 27 return (int) apply_filters('dfehc_server_load_ttl', (int) DFEHC_SERVER_LOAD_TTL); 28 } 29 } 30 31 if (!function_exists('dfehc_unknown_load')) { 32 function dfehc_unknown_load(): float 33 { 34 static $v; 35 if ($v === null) { 36 $v = (float) apply_filters('dfehc_unknown_load', 0.404); 37 } 38 return $v; 39 } 40 } 41 42 if (!function_exists('dfehc_host_token')) { 43 function dfehc_host_token(): string 44 { 45 static $t = ''; 46 if ($t !== '') return $t; 47 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 48 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 49 return $t = substr(md5((string) $host . $salt), 0, 10); 50 } 51 } 52 53 if (!function_exists('dfehc_blog_id')) { 54 function dfehc_blog_id(): int 55 { 56 return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0; 57 } 58 } 59 60 if (!function_exists('dfehc_key')) { 61 function dfehc_key(string $base): string 62 { 63 return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token(); 64 } 65 } 66 67 if (!function_exists('dfehc_client_ip')) { 68 function dfehc_client_ip(): string 69 { 70 $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); 71 $trusted = (array) apply_filters('dfehc_trusted_proxies', []); 72 if ($trusted && in_array($ip, $trusted, true)) { 73 $headers = (array) apply_filters('dfehc_proxy_ip_headers', ['HTTP_X_FORWARDED_FOR', 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP']); 74 foreach ($headers as $h) { 75 if (!empty($_SERVER[$h])) { 76 $raw = (string) $_SERVER[$h]; 77 $parts = array_map('trim', explode(',', $raw)); 78 $cand = end($parts); 79 if ($cand) { 80 return (string) apply_filters('dfehc_client_ip', $cand); 81 } 82 } 83 } 84 } 85 return (string) apply_filters('dfehc_client_ip', $ip); 86 } 87 } 88 89 if (!function_exists('dfehc_get_redis_server')) { 90 function dfehc_get_redis_server(): string 91 { 92 $h = getenv('REDIS_HOST'); 93 return $h ? (string) $h : '127.0.0.1'; 94 } 95 } 96 if (!function_exists('dfehc_get_redis_port')) { 97 function dfehc_get_redis_port(): int 98 { 99 $p = getenv('REDIS_PORT'); 100 return $p && ctype_digit((string) $p) ? (int) $p : 6379; 101 } 102 } 103 if (!function_exists('dfehc_get_memcached_server')) { 104 function dfehc_get_memcached_server(): string 105 { 106 $h = getenv('MEMCACHED_HOST'); 107 return $h ? (string) $h : '127.0.0.1'; 108 } 109 } 110 if (!function_exists('dfehc_get_memcached_port')) { 111 function dfehc_get_memcached_port(): int 112 { 113 $p = getenv('MEMCACHED_PORT'); 114 return $p && ctype_digit((string) $p) ? (int) $p : 11211; 115 } 116 } 16 117 17 118 function _dfehc_get_cache_client(): array … … 35 136 $redis = new Redis(); 36 137 if ($redis->connect(dfehc_get_redis_server(), dfehc_get_redis_port(), 1.0)) { 37 $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD')); 38 if ($pass) { 138 $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD') ?: null); 139 $user = apply_filters('dfehc_redis_user', getenv('REDIS_USERNAME') ?: null); 140 if ($user && $pass && method_exists($redis, 'auth')) { 141 $redis->auth([$user, $pass]); 142 } elseif ($pass && method_exists($redis, 'auth')) { 39 143 $redis->auth($pass); 40 144 } … … 42 146 } 43 147 } catch (Throwable $e) { 44 trigger_error('DFEHC Redis connect error: ' . $e->getMessage(), E_USER_WARNING); 148 if (defined('WP_DEBUG') && WP_DEBUG) { 149 error_log('DFEHC Redis connect error: ' . $e->getMessage()); 150 } 45 151 } 46 152 } 47 153 if (class_exists('Memcached')) { 48 154 try { 49 $mc = new Memcached(); 50 $mc->addServer(dfehc_get_memcached_server(), dfehc_get_memcached_port()); 155 $mc = new Memcached('dfehc'); 156 if (!$mc->getServerList()) { 157 $mc->addServer(dfehc_get_memcached_server(), dfehc_get_memcached_port()); 158 } 51 159 $user = getenv('MEMCACHED_USERNAME'); 52 160 $pass = getenv('MEMCACHED_PASSWORD'); … … 56 164 } 57 165 $versions = $mc->getVersion(); 58 $ok = $versions && is_array($versions) && reset($versions) && reset($versions) !== '0.0.0'; 166 $first = is_array($versions) ? reset($versions) : false; 167 $ok = $first && $first !== '0.0.0'; 59 168 if ($ok) { 60 169 return $cached = ['client' => $mc, 'type' => 'memcached']; 61 170 } 62 171 } catch (Throwable $e) { 63 trigger_error('DFEHC Memcached connect error: ' . $e->getMessage(), E_USER_WARNING); 172 if (defined('WP_DEBUG') && WP_DEBUG) { 173 error_log('DFEHC Memcached connect error: ' . $e->getMessage()); 174 } 64 175 } 65 176 } … … 73 184 return; 74 185 } 186 $key = dfehc_key(DFEHC_SERVER_LOAD_CACHE_KEY); 187 $ttl = dfehc_server_load_ttl(); 75 188 try { 189 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 76 190 if ($type === 'redis') { 77 $client->setex( DFEHC_SERVER_LOAD_CACHE_KEY, DFEHC_SERVER_LOAD_TTL, $value);191 $client->setex($key, $ttl, $value); 78 192 } elseif ($type === 'memcached') { 79 $client->set( DFEHC_SERVER_LOAD_CACHE_KEY, $value, DFEHC_SERVER_LOAD_TTL);193 $client->set($key, $value, $ttl); 80 194 } 81 195 } catch (Throwable $e) { 82 trigger_error('DFEHC cache write error: ' . $e->getMessage(), E_USER_WARNING); 196 if (defined('WP_DEBUG') && WP_DEBUG) { 197 error_log('DFEHC cache write error: ' . $e->getMessage()); 198 } 83 199 } 84 200 } … … 86 202 function dfehc_get_server_load(): float 87 203 { 88 $payload = get_transient(DFEHC_SERVER_LOAD_PAYLOAD_KEY); 204 $payloadKey = dfehc_key(DFEHC_SERVER_LOAD_PAYLOAD_KEY); 205 $payload = null; 206 if (wp_using_ext_object_cache()) { 207 $payload = wp_cache_get($payloadKey, DFEHC_CACHE_GROUP); 208 if ($payload === false) { 209 $payload = null; 210 } 211 } else { 212 $payload = get_transient($payloadKey); 213 } 89 214 if (!(is_array($payload) && isset($payload['raw'], $payload['cores'], $payload['source']))) { 90 $lock = dfehc_acquire_lock(); 91 if (!$lock) { 92 return DFEHC_UNKNOWN_LOAD; 93 } 94 $data = dfehc_detect_load_raw_with_source(); 95 $payload = [ 96 'raw' => (float) $data['load'], 97 'cores' => dfehc_get_cpu_cores(), 98 'source' => (string) $data['source'], 99 ]; 100 set_transient(DFEHC_SERVER_LOAD_PAYLOAD_KEY, $payload, DFEHC_SERVER_LOAD_TTL); 101 dfehc_release_lock($lock); 102 } 103 $raw = (float) $payload['raw']; 104 $cores = (int) ($payload['cores'] ?: dfehc_get_cpu_cores()); 215 if (!dfehc_load_acquire_lock()) { 216 return dfehc_unknown_load(); 217 } 218 try { 219 $data = dfehc_detect_load_raw_with_source(); 220 $payload = [ 221 'raw' => (float) $data['load'], 222 'cores' => dfehc_get_cpu_cores(), 223 'source' => (string) $data['source'], 224 ]; 225 $ttl = dfehc_server_load_ttl(); 226 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 227 if (wp_using_ext_object_cache()) { 228 wp_cache_set($payloadKey, $payload, DFEHC_CACHE_GROUP, $ttl); 229 } else { 230 set_transient($payloadKey, $payload, $ttl); 231 } 232 } finally { 233 dfehc_load_release_lock(); 234 } 235 } 236 $raw = (float) $payload['raw']; 237 $cores = (int) ($payload['cores'] ?: dfehc_get_cpu_cores()); 105 238 $source = (string) $payload['source']; 106 239 $divide = (bool) apply_filters('dfehc_divide_cpu_load', true, $raw, $cores, $source); 107 $load = ($source === 'cpu_load' && $divide && $cores > 0) ? $raw / $cores : $raw; 240 $load = ($source === 'cpu_load' && $divide && $cores > 0) ? $raw / $cores : $raw; 241 $load = max(0.0, (float) $load); 108 242 return (float) apply_filters('dfehc_contextual_load_value', $load, $source); 109 243 } … … 129 263 if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) { 130 264 $out = shell_exec('LANG=C uptime 2>&1'); 131 if ($out && preg_match('/load average :([0-9.]+)/', $out, $m)) {265 if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) { 132 266 return ['load' => (float) $m[1], 'source' => 'cpu_load']; 133 267 } 134 268 } 135 269 if (defined('DFEHC_PLUGIN_PATH')) { 136 $est = DFEHC_PLUGIN_PATH . 'defibrillator/load-estimator.php';270 $est = rtrim((string) DFEHC_PLUGIN_PATH, "/\\") . '/defibrillator/load-estimator.php'; 137 271 if (file_exists($est)) { 138 272 require_once $est; 139 273 if (class_exists('DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) { 140 return [ 141 'load' => (float) DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load(), 142 'source' => 'fallback', 143 ]; 144 } 145 } 146 } 147 return ['load' => DFEHC_UNKNOWN_LOAD, 'source' => 'fallback']; 274 $pct = DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load(); 275 if (is_numeric($pct)) { 276 $cores = dfehc_get_cpu_cores(); 277 $cpuLoad = ((float) $pct / 100.0) * max(1, $cores); 278 return ['load' => $cpuLoad, 'source' => 'cpu_load']; 279 } 280 } 281 } 282 } 283 if (function_exists('do_action')) { 284 do_action('dfehc_load_detection_fell_back'); 285 } 286 return ['load' => dfehc_unknown_load(), 'source' => 'fallback']; 148 287 } 149 288 150 289 function dfehc_get_cpu_cores(): int 151 290 { 291 static $cores = null; 292 if ($cores !== null) { 293 return (int) $cores; 294 } 295 $override = getenv('DFEHC_CPU_CORES'); 296 if ($override && ctype_digit((string) $override) && (int) $override > 0) { 297 $cores = (int) $override; 298 return (int) $cores; 299 } 152 300 if (is_readable('/sys/fs/cgroup/cpu.max')) { 153 [$quota, $period] = explode(' ', trim(file_get_contents('/sys/fs/cgroup/cpu.max'))); 154 if ($quota !== 'max') { 155 $quota = (int) $quota; 156 $period = (int) $period; 157 if ($quota > 0 && $period > 0) { 158 return max(1, (int) ceil($quota / $period)); 301 $line = file_get_contents('/sys/fs/cgroup/cpu.max'); 302 if ($line !== false) { 303 [$quota, $period] = explode(' ', trim($line)); 304 if ($quota !== 'max') { 305 $q = (int) $quota; 306 $p = (int) $period; 307 if ($q > 0 && $p > 0) { 308 $cores = max(1, (int) ceil($q / $p)); 309 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores); 310 return (int) $cores; 311 } 159 312 } 160 313 } … … 163 316 $content = file_get_contents('/proc/self/cgroup'); 164 317 if ($content !== false && preg_match('/^[0-9]+:[^:]*cpu[^:]*:(.+)$/m', $content, $m)) { 165 $path = '/' . ltrim(trim($m[1]), '/');166 $base = '/sys/fs/cgroup' . $path;167 $quotaFile = "$base/cpu.cfs_quota_us";318 $path = '/' . ltrim(trim($m[1]), '/'); 319 $base = '/sys/fs/cgroup' . $path; 320 $quotaFile = "$base/cpu.cfs_quota_us"; 168 321 $periodFile = "$base/cpu.cfs_period_us"; 169 322 if (is_readable($quotaFile) && is_readable($periodFile)) { 170 $quota = (int) file_get_contents($quotaFile);323 $quota = (int) file_get_contents($quotaFile); 171 324 $period = (int) file_get_contents($periodFile); 172 325 if ($quota > 0 && $period > 0) { 173 return max(1, (int) ceil($quota / $period)); 326 $cores = max(1, (int) ceil($quota / $period)); 327 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores); 328 return (int) $cores; 174 329 } 175 330 } … … 177 332 } 178 333 if (is_readable('/sys/fs/cgroup/cpu/cpu.cfs_quota_us') && is_readable('/sys/fs/cgroup/cpu/cpu.cfs_period_us')) { 179 $quota = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_quota_us');334 $quota = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_quota_us'); 180 335 $period = (int) file_get_contents('/sys/fs/cgroup/cpu/cpu.cfs_period_us'); 181 336 if ($quota > 0 && $period > 0) { 182 return max(1, (int) ceil($quota / $period)); 337 $cores = max(1, (int) ceil($quota / $period)); 338 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores); 339 return (int) $cores; 183 340 } 184 341 } … … 186 343 if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) { 187 344 $n = shell_exec('nproc 2>/dev/null'); 188 if ($n && ctype_digit(trim($n))) { 189 return max(1, (int) trim($n)); 345 if ($n && ctype_digit(trim((string) $n))) { 346 $cores = max(1, (int) trim((string) $n)); 347 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores); 348 return (int) $cores; 190 349 } 191 350 } … … 195 354 $cnt = preg_match_all('/^processor/m', $info); 196 355 if ($cnt) { 197 return $cnt; 198 } 199 } 200 } 201 return 1; 356 $cores = (int) $cnt; 357 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores); 358 return (int) $cores; 359 } 360 } 361 } 362 $cores = 1; 363 $cores = (int) apply_filters('dfehc_cpu_cores', (int) $cores); 364 return (int) $cores; 202 365 } 203 366 204 367 function dfehc_log_server_load(): void 205 368 { 206 $load = dfehc_get_server_load();207 $optKey = 'dfehc_server_load_logs ';208 $logs = get_site_option($optKey, []);369 $load = dfehc_get_server_load(); 370 $optKey = 'dfehc_server_load_logs_' . dfehc_blog_id() . '_' . dfehc_host_token(); 371 $logs = get_option($optKey, []); 209 372 if (!is_array($logs)) { 210 373 $logs = []; 211 374 } 212 $now = time();375 $now = time(); 213 376 $cutoff = $now - DAY_IN_SECONDS; 214 $logs = array_filter(377 $logs = array_filter( 215 378 $logs, 216 static fn(array $row): bool => isset($row['timestamp']) && $row['timestamp'] >= $cutoff 379 static function (array $row) use ($cutoff): bool { 380 return isset($row['timestamp']) && $row['timestamp'] >= $cutoff; 381 } 217 382 ); 383 if (count($logs) > 2000) { 384 $logs = array_slice($logs, -2000); 385 } 218 386 $logs[] = ['timestamp' => $now, 'load' => $load]; 219 update_ site_option($optKey, array_values($logs), false);387 update_option($optKey, array_values($logs), false); 220 388 } 221 389 add_action('dfehc_log_server_load_hook', 'dfehc_log_server_load'); … … 223 391 function dfehc_get_server_load_ajax_handler(): void 224 392 { 225 $nonce = $_POST['nonce'] ?? $_GET['nonce'] ?? ''; 226 if (!wp_verify_nonce((string) $nonce, 'dfehc-ajax-nonce')) { 227 wp_send_json_error('Invalid nonce.'); 228 } 229 if (!is_user_logged_in() && !apply_filters('dfehc_allow_public_server_load', false)) { 230 wp_send_json_error('Not authorised.'); 231 } 393 $allow_public = apply_filters('dfehc_allow_public_server_load', false); 394 if (!$allow_public) { 395 $action = 'get_server_load'; 396 $nonce_action = 'dfehc-' . $action; 397 $valid = function_exists('check_ajax_referer') 398 ? check_ajax_referer($nonce_action, 'nonce', false) 399 : wp_verify_nonce((string)($_POST['nonce'] ?? $_GET['nonce'] ?? ''), $nonce_action); 400 if (!$valid) { 401 wp_send_json_error(['message' => 'Invalid nonce.'], 403); 402 } 403 $cap = apply_filters('dfehc_required_capability', 'read'); 404 if (!current_user_can($cap)) { 405 wp_send_json_error(['message' => 'Not authorised.'], 403); 406 } 407 } else { 408 $ip = dfehc_client_ip(); 409 $rk = dfehc_key('dfehc_rl_' . md5($ip)); 410 $cnt = (int) get_transient($rk); 411 $limit = (int) apply_filters('dfehc_public_rate_limit', 60); 412 $win = (int) apply_filters('dfehc_public_rate_window', 60); 413 if ($cnt >= $limit) { 414 wp_send_json_error(['message' => 'rate_limited'], 429); 415 } 416 set_transient($rk, $cnt + 1, $win); 417 } 418 nocache_headers(); 232 419 wp_send_json_success(dfehc_get_server_load_persistent()); 233 420 } 234 421 add_action('wp_ajax_get_server_load', 'dfehc_get_server_load_ajax_handler'); 235 add_action('wp_ajax_nopriv_get_server_load', 'dfehc_get_server_load_ajax_handler'); 236 237 function dfehc_get_server_load_persistent(): float { 422 423 add_action('init', static function (): void { 424 if (apply_filters('dfehc_allow_public_server_load', false)) { 425 add_action('wp_ajax_nopriv_get_server_load', 'dfehc_get_server_load_ajax_handler'); 426 } 427 }, 0); 428 429 function dfehc_get_server_load_persistent(): float 430 { 238 431 static $cached = null; 239 432 if ($cached !== null) { 240 return $cached; 241 } 242 243 ['client' => $client, 'type' => $type] = _dfehc_get_cache_client(); 433 return (float) $cached; 434 } 435 ['client' => $client] = _dfehc_get_cache_client(); 244 436 $val = false; 245 437 $key = dfehc_key(DFEHC_SERVER_LOAD_CACHE_KEY); 246 438 if ($client) { 247 439 try { 248 $val = $client->get( DFEHC_SERVER_LOAD_CACHE_KEY);440 $val = $client->get($key); 249 441 } catch (Throwable $e) { 250 442 if (defined('WP_DEBUG') && WP_DEBUG) { 251 trigger_error('DFEHC cache read error: ' . $e->getMessage(), E_USER_WARNING); 252 } 253 } 254 } 255 256 if ($val !== false) { 257 return $cached = (float) $val; 258 } 259 443 error_log('DFEHC cache read error: ' . $e->getMessage()); 444 } 445 } 446 } 447 if ($val !== false && $val !== '') { 448 $cached = max(0.0, (float) $val); 449 return (float) $cached; 450 } 260 451 $fresh = dfehc_get_server_load(); 452 $fresh = max(0.0, (float) $fresh); 261 453 dfehc_cache_server_load($fresh); 262 return $cached = $fresh; 454 if (wp_using_ext_object_cache()) { 455 $ttl = dfehc_server_load_ttl(); 456 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 457 wp_cache_set($key, $fresh, DFEHC_CACHE_GROUP, $ttl); 458 } 459 $cached = $fresh; 460 return (float) $cached; 263 461 } 264 462 … … 267 465 $schedules['dfehc_minute'] = [ 268 466 'interval' => 60, 269 'display' => __('Server load (DFEHC)', 'dfehc'),467 'display' => __('Server load (DFEHC)', 'dfehc'), 270 468 ]; 271 469 } … … 279 477 if (!wp_next_scheduled('dfehc_log_server_load_hook')) { 280 478 try { 281 $start = time() - (time() % 60) + 60; 479 $now = time(); 480 $start = $now - ($now % 60) + 60; 282 481 wp_schedule_event($start, 'dfehc_minute', 'dfehc_log_server_load_hook'); 283 482 } catch (Throwable $e) { 284 483 if (defined('WP_DEBUG') && WP_DEBUG) { 285 trigger_error('DFEHC scheduling error: ' . $e->getMessage(), E_USER_WARNING);484 error_log('DFEHC scheduling error: ' . $e->getMessage()); 286 485 } 287 486 } … … 293 492 add_action('init', static function () use ($__dfehc_schedule): void { 294 493 $__dfehc_schedule(); 295 if (wp_next_scheduled('dfehc_log_server_load_hook')) {296 remove_action('init', __FUNCTION__, 1);297 }298 494 }, 1); 495 496 function dfehc_load_acquire_lock(): bool 497 { 498 $key = dfehc_key('dfehc_load_lock'); 499 if (class_exists('WP_Lock')) { 500 $lock = new WP_Lock($key, 30); 501 if ($lock->acquire()) { 502 $GLOBALS['dfehc_load_lock'] = $lock; 503 return true; 504 } 505 return false; 506 } 507 if (function_exists('wp_cache_add') && wp_cache_add($key, 1, DFEHC_CACHE_GROUP, 30)) { 508 $GLOBALS['dfehc_load_lock_cache_key'] = $key; 509 return true; 510 } 511 if (false !== get_transient($key)) { 512 return false; 513 } 514 if (set_transient($key, 1, 30)) { 515 $GLOBALS['dfehc_load_lock_transient_key'] = $key; 516 return true; 517 } 518 return false; 519 } 520 521 function dfehc_load_release_lock(): void 522 { 523 if (isset($GLOBALS['dfehc_load_lock']) && $GLOBALS['dfehc_load_lock'] instanceof WP_Lock) { 524 $GLOBALS['dfehc_load_lock']->release(); 525 unset($GLOBALS['dfehc_load_lock']); 526 return; 527 } 528 if (isset($GLOBALS['dfehc_load_lock_cache_key'])) { 529 wp_cache_delete($GLOBALS['dfehc_load_lock_cache_key'], DFEHC_CACHE_GROUP); 530 unset($GLOBALS['dfehc_load_lock_cache_key']); 531 return; 532 } 533 if (isset($GLOBALS['dfehc_load_lock_transient_key'])) { 534 delete_transient($GLOBALS['dfehc_load_lock_transient_key']); 535 unset($GLOBALS['dfehc_load_lock_transient_key']); 536 } 537 } -
dynamic-front-end-heartbeat-control/trunk/engine/server-response.php
r3320647 r3396626 3 3 4 4 defined('DFEHC_DEFAULT_RESPONSE_TIME') || define('DFEHC_DEFAULT_RESPONSE_TIME', 50.0); 5 defined('DFEHC_HEAD_NEG_TTL') || define('DFEHC_HEAD_NEG_TTL', 600); 6 defined('DFEHC_HEAD_POS_TTL') || define('DFEHC_HEAD_POS_TTL', WEEK_IN_SECONDS); 7 defined('DFEHC_SPIKE_OPT_EPS') || define('DFEHC_SPIKE_OPT_EPS', 0.1); 8 defined('DFEHC_BASELINE_EXP') || define('DFEHC_BASELINE_EXP', 7 * DAY_IN_SECONDS); 5 defined('DFEHC_HEAD_NEG_TTL') || define('DFEHC_HEAD_NEG_TTL', 600); 6 defined('DFEHC_HEAD_POS_TTL') || define('DFEHC_HEAD_POS_TTL', WEEK_IN_SECONDS); 7 defined('DFEHC_SPIKE_OPT_EPS') || define('DFEHC_SPIKE_OPT_EPS', 0.1); 8 defined('DFEHC_BASELINE_EXP') || define('DFEHC_BASELINE_EXP', 7 * DAY_IN_SECONDS); 9 defined('DFEHC_CACHE_GROUP') || define('DFEHC_CACHE_GROUP', 'dfehc'); 10 11 if (!function_exists('dfehc_host_token')) { 12 function dfehc_host_token(): string { 13 static $t = ''; 14 if ($t !== '') return $t; 15 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 16 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 17 return $t = substr(md5((string) $host . $salt), 0, 10); 18 } 19 } 20 21 if (!function_exists('dfehc_blog_id')) { 22 function dfehc_blog_id(): int { 23 return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0; 24 } 25 } 26 27 if (!function_exists('dfehc_key')) { 28 function dfehc_key(string $base): string { 29 return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token(); 30 } 31 } 32 33 if (!function_exists('dfehc_store_lockfree')) { 34 function dfehc_store_lockfree(string $key, $value, int $ttl): bool { 35 if (function_exists('wp_cache_add') && wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $ttl)) { 36 return true; 37 } 38 return set_transient($key, $value, $ttl); 39 } 40 } 41 42 if (!function_exists('dfehc_set_transient_noautoload')) { 43 function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void { 44 if (wp_using_ext_object_cache()) { 45 if (function_exists('wp_cache_add')) { 46 if (!wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $expiration)) { 47 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration); 48 } 49 } else { 50 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration); 51 } 52 return; 53 } 54 set_transient($key, $value, $expiration); 55 global $wpdb; 56 $opt_key = "_transient_$key"; 57 $opt_key_to = "_transient_timeout_$key"; 58 $wpdb->suppress_errors(true); 59 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key)); 60 if ($autoload === 'yes') { 61 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 62 } 63 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to)); 64 if ($autoload_to === 'yes') { 65 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 66 } 67 $wpdb->suppress_errors(false); 68 } 69 } 70 71 if (!function_exists('dfehc_client_ip')) { 72 function dfehc_client_ip(): string { 73 $ip = (string)($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); 74 $trusted = (array) apply_filters('dfehc_trusted_proxies', []); 75 if ($trusted && in_array($ip, $trusted, true)) { 76 $headers = (array) apply_filters('dfehc_proxy_ip_headers', ['HTTP_X_FORWARDED_FOR', 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP']); 77 foreach ($headers as $h) { 78 if (!empty($_SERVER[$h])) { 79 $raw = (string) $_SERVER[$h]; 80 $parts = array_map('trim', explode(',', $raw)); 81 $cand = end($parts); 82 if ($cand) { 83 return (string) apply_filters('dfehc_client_ip', $cand); 84 } 85 } 86 } 87 } 88 return (string) apply_filters('dfehc_client_ip', $ip); 89 } 90 } 9 91 10 92 function dfehc_get_server_response_time(): array 11 93 { 94 $now = time(); 12 95 $default_ms = (float) apply_filters('dfehc_default_response_time', DFEHC_DEFAULT_RESPONSE_TIME); 13 96 14 97 $defaults = [ 15 98 'main_response_ms' => null, 16 'db_response_ms' => null, 17 'method' => '', 18 'measurements' => [], 19 'recalibrated' => false, 20 'timestamp' => current_time('mysql'), 21 'baseline_used' => null, 22 'spike_score' => 0.0, 99 'db_response_ms' => null, 100 'method' => '', 101 'measurements' => [], 102 'recalibrated' => false, 103 'timestamp' => current_time('mysql'), 104 'baseline_used' => null, 105 'spike_score' => 0.0, 106 'ts_unix' => $now, 23 107 ]; 24 108 25 $cached = get_transient('dfehc_cached_response_data'); 109 $cacheKey = dfehc_key('dfehc_cached_response_data'); 110 $cached = get_transient($cacheKey); 26 111 if ($cached !== false && is_array($cached)) { 27 112 return array_merge($defaults, $cached); 28 113 } 29 114 30 if ( dfehc_is_high_traffic()) {115 if (function_exists('dfehc_is_high_traffic') && dfehc_is_high_traffic()) { 31 116 $high = [ 32 117 'main_response_ms' => $default_ms, 33 'db_response_ms' => null, 34 'method' => 'throttled', 35 'measurements' => [], 36 'recalibrated' => false, 37 'timestamp' => current_time('mysql'), 38 'baseline_used' => null, 39 'spike_score' => 0.0, 118 'db_response_ms' => null, 119 'method' => 'throttled', 120 'measurements' => [], 121 'recalibrated' => false, 122 'timestamp' => current_time('mysql'), 123 'baseline_used' => null, 124 'spike_score' => 0.0, 125 'ts_unix' => $now, 40 126 ]; 41 set_transient('dfehc_cached_response_data', $high, (int) apply_filters('dfehc_high_traffic_cache_expiration', 300)); 127 $ttl = (int) apply_filters('dfehc_high_traffic_cache_expiration', 300); 128 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 129 dfehc_set_transient_noautoload($cacheKey, $high, $ttl); 42 130 return $high; 43 131 } … … 47 135 } 48 136 49 $results = dfehc_perform_response_measurements($default_ms); 50 $baseline = get_transient('dfehc_baseline_response_data'); 51 $spike = (float) get_transient('dfehc_spike_score'); 52 $now = time(); 53 $max_age = (int) apply_filters('dfehc_max_baseline_age', DFEHC_BASELINE_EXP); 54 55 if (is_array($baseline)) { 56 $ts = strtotime($baseline['timestamp'] ?? 'now'); 57 if ($now - $ts > $max_age) { 58 delete_transient('dfehc_baseline_response_data'); 59 $baseline = false; 60 } 61 } 62 63 if ($baseline === false && $results['method'] === 'http_loopback' && $results['main_response_ms'] !== null) { 64 $exp = (int) apply_filters('dfehc_baseline_expiration', DFEHC_BASELINE_EXP); 65 $results['timestamp'] = current_time('mysql'); 66 set_transient('dfehc_baseline_response_data', $results, $exp); 67 $baseline = $results; 68 $spike = 0.0; 69 } 70 71 $results['baseline_used'] = $baseline; 72 73 if (is_array($baseline) && $results['method'] === 'http_loopback' && isset($results['main_response_ms'], $baseline['main_response_ms'])) { 74 $base_ms = max(1.0, (float) $baseline['main_response_ms']); 75 $curr_ms = (float) $results['main_response_ms']; 76 $factor = (float) apply_filters('dfehc_spike_threshold_factor', 2.0); 77 $increment = (float) apply_filters('dfehc_spike_increment', max(1.0, $curr_ms / $base_ms - $factor)); 78 $decay = (float) apply_filters('dfehc_spike_decay', 0.25); 79 $threshold = (float) apply_filters('dfehc_recalibrate_threshold', 5.0); 80 81 if ($curr_ms > $base_ms * $factor) { 82 $spike += $increment; 83 } else { 84 $spike = max(0.0, $spike - $decay); 85 } 86 87 if ($spike >= $threshold) { 88 set_transient('dfehc_baseline_response_data', $results, (int) apply_filters('dfehc_baseline_expiration', DFEHC_BASELINE_EXP)); 89 $spike = 0.0; 90 $results['recalibrated'] = true; 91 } 92 } 93 94 $results['spike_score'] = $spike; 95 $prev_spike = (float) get_transient('dfehc_spike_score'); 96 97 if (abs($spike - $prev_spike) >= DFEHC_SPIKE_OPT_EPS) { 98 set_transient('dfehc_spike_score', $spike, DFEHC_BASELINE_EXP); 99 } 100 101 set_transient('dfehc_cached_response_data', $results, (int) apply_filters('dfehc_cache_expiration', 3 * MINUTE_IN_SECONDS)); 102 103 dfehc_release_lock(); 104 105 return array_merge($defaults, $results); 137 try { 138 $results = dfehc_perform_response_measurements($default_ms); 139 $baselineKey = dfehc_key('dfehc_baseline_response_data'); 140 $spikeKey = dfehc_key('dfehc_spike_score'); 141 $baseline = get_transient($baselineKey); 142 $spike = (float) get_transient($spikeKey); 143 $max_age = (int) apply_filters('dfehc_max_baseline_age', DFEHC_BASELINE_EXP); 144 145 if (is_array($baseline)) { 146 $ts = isset($baseline['ts_unix']) && is_numeric($baseline['ts_unix']) ? (int) $baseline['ts_unix'] : strtotime($baseline['timestamp'] ?? 'now'); 147 if ($now - (int) $ts > $max_age) { 148 delete_transient($baselineKey); 149 $baseline = false; 150 } 151 } 152 153 if ($baseline === false && $results['method'] === 'http_loopback' && $results['main_response_ms'] !== null && count((array) $results['measurements']) >= (int) apply_filters('dfehc_baseline_min_samples', 2)) { 154 $exp = (int) apply_filters('dfehc_baseline_expiration', DFEHC_BASELINE_EXP); 155 $results['timestamp'] = current_time('mysql'); 156 $results['ts_unix'] = $now; 157 $exp += function_exists('random_int') ? random_int(0, 5) : 0; 158 dfehc_set_transient_noautoload($baselineKey, $results, $exp); 159 $baseline = $results; 160 $spike = 0.0; 161 } 162 163 $results['baseline_used'] = $baseline; 164 165 if (is_array($baseline) && $results['method'] === 'http_loopback' && isset($results['main_response_ms'], $baseline['main_response_ms'])) { 166 $base_ms = max(1.0, (float) $baseline['main_response_ms']); 167 $curr_ms = (float) $results['main_response_ms']; 168 $factor = (float) apply_filters('dfehc_spike_threshold_factor', 2.0); 169 $raw_inc = max(0.0, $curr_ms / $base_ms - $factor); 170 $floor = (float) apply_filters('dfehc_spike_increment_floor', 0.25); 171 $cap = (float) apply_filters('dfehc_spike_increment_cap', 3.0); 172 $increment = min($cap, max($floor, $raw_inc)); 173 $decay = (float) apply_filters('dfehc_spike_decay', 0.25); 174 $threshold = (float) apply_filters('dfehc_recalibrate_threshold', 5.0); 175 176 if ($curr_ms > $base_ms * $factor) { 177 $spike += $increment; 178 } else { 179 $spike = max(0.0, $spike - $decay); 180 } 181 182 if ($spike >= $threshold) { 183 $results['timestamp'] = current_time('mysql'); 184 $results['ts_unix'] = $now; 185 $exp = (int) apply_filters('dfehc_baseline_expiration', DFEHC_BASELINE_EXP); 186 $exp += function_exists('random_int') ? random_int(0, 5) : 0; 187 dfehc_set_transient_noautoload($baselineKey, $results, $exp); 188 $spike = 0.0; 189 $results['recalibrated'] = true; 190 } 191 } 192 193 $results['spike_score'] = $spike; 194 $prev_spike = (float) get_transient($spikeKey); 195 196 if (abs($spike - $prev_spike) >= DFEHC_SPIKE_OPT_EPS) { 197 $ttl = DFEHC_BASELINE_EXP + (function_exists('random_int') ? random_int(0, 5) : 0); 198 dfehc_set_transient_noautoload($spikeKey, $spike, $ttl); 199 } 200 201 $exp = (int) apply_filters('dfehc_cache_expiration', 3 * MINUTE_IN_SECONDS); 202 $exp += function_exists('random_int') ? random_int(0, 5) : 0; 203 dfehc_set_transient_noautoload($cacheKey, $results, $exp); 204 205 return array_merge($defaults, $results); 206 } finally { 207 dfehc_release_lock(); 208 } 106 209 } 107 210 108 211 function dfehc_perform_response_measurements(float $default_ms): array 109 212 { 213 $now = time(); 110 214 $r = [ 111 215 'main_response_ms' => null, 112 'db_response_ms' => null, 113 'method' => 'http_loopback', 114 'measurements' => [], 115 'recalibrated' => false, 116 'timestamp' => current_time('mysql'), 216 'db_response_ms' => null, 217 'method' => 'http_loopback', 218 'measurements' => [], 219 'recalibrated' => false, 220 'timestamp' => current_time('mysql'), 221 'ts_unix' => $now, 117 222 ]; 118 223 224 if (apply_filters('dfehc_disable_loopback', false) || (function_exists('wp_is_recovery_mode') && wp_is_recovery_mode())) { 225 $r['method'] = 'throttled'; 226 $r['main_response_ms'] = $default_ms; 227 $r['db_response_ms'] = $default_ms; 228 return $r; 229 } 230 119 231 global $wpdb; 120 $db_start = microtime(true); 121 $wpdb->get_var("SELECT option_value FROM {$wpdb->options} LIMIT 1"); 122 $r['db_response_ms'] = (microtime(true) - $db_start) * 1000; 123 124 $url = get_rest_url() ?: home_url('/wp-json/'); 125 if (!get_option('permalink_structure')) { 232 try { 233 $db_start = microtime(true); 234 $wpdb->query('SELECT 1'); 235 $r['db_response_ms'] = (microtime(true) - $db_start) * 1000; 236 } catch (\Throwable $e) { 237 $r['db_response_ms'] = $default_ms; 238 } 239 240 $rest = function_exists('get_rest_url') ? get_rest_url() : ''; 241 $url = $rest ?: (function_exists('home_url') ? home_url('/wp-json/') : '/wp-json/'); 242 if (function_exists('get_option') && !get_option('permalink_structure')) { 126 243 $url = add_query_arg('rest_route', '/', home_url('/index.php')); 127 244 } 128 245 129 $n = max(1, min((int) apply_filters('dfehc_num_requests', 2), 5)); 130 $sleep_us = (int) apply_filters('dfehc_request_pause_us', 50000); 131 $timeout = (int) apply_filters('dfehc_request_timeout', 10); 132 $sslverify = (bool) apply_filters('dfehc_ssl_verify', true); 246 $ajax_fallback = function_exists('admin_url') ? admin_url('admin-ajax.php?action=dfehc_ping') : '/wp-admin/admin-ajax.php?action=dfehc_ping'; 247 $use_ajax_fallback = false; 248 249 $home_host = function_exists('home_url') ? (string) parse_url(home_url(), PHP_URL_HOST) : ''; 250 $headers = (array) apply_filters('dfehc_probe_headers', [ 251 'Host' => $home_host ?: '', 252 'Cache-Control' => 'max-age=0, must-revalidate', 253 'X-DFEHC-Probe' => '1', 254 ]); 255 $hard_deadline = microtime(true) + (float) apply_filters('dfehc_total_timeout', (int) apply_filters('dfehc_request_timeout', 10) + 2); 256 257 if (defined('WP_HTTP_BLOCK_EXTERNAL') && WP_HTTP_BLOCK_EXTERNAL) { 258 $probe_host = parse_url($url, PHP_URL_HOST); 259 $accessible = getenv('WP_ACCESSIBLE_HOSTS') ?: (defined('WP_ACCESSIBLE_HOSTS') ? WP_ACCESSIBLE_HOSTS : ''); 260 $allowed_hosts = array_filter(array_map('trim', explode(',', (string) $accessible))); 261 $is_same_host = $home_host && $probe_host && strcasecmp((string) $home_host, (string) $probe_host) === 0; 262 if (!$is_same_host && !in_array((string) $probe_host, $allowed_hosts, true)) { 263 $use_ajax_fallback = true; 264 } 265 } 266 267 if ($use_ajax_fallback || empty($url)) { 268 $url = $ajax_fallback; 269 $r['method'] = 'ajax_loopback'; 270 } 271 272 $n = max(1, min((int) apply_filters('dfehc_num_requests', 3), 5)); 273 $sleep_us = (int) apply_filters('dfehc_request_pause_us', 50000); 274 $timeout = (int) apply_filters('dfehc_request_timeout', 10); 275 $sslverify = (bool) apply_filters('dfehc_ssl_verify', true); 133 276 $get_fallback = (bool) apply_filters('dfehc_use_get_fallback', true); 134 $use_head = (bool) apply_filters('dfehc_use_head_method', true); 135 136 $head_key = 'dfehc_head_supported_' . md5($url); 277 $use_head = (bool) apply_filters('dfehc_use_head_method', true); 278 $redirection = (int) apply_filters('dfehc_redirection', 1); 279 280 $scheme = (string) parse_url($url, PHP_URL_SCHEME); 281 $head_key = dfehc_key('dfehc_head_supported_' . md5($scheme . '|' . $url)); 137 282 $head_supported = get_transient($head_key); 138 283 if ($head_supported === false) { … … 140 285 } 141 286 142 $times = []; 143 $hard_deadline = microtime(true) + (float) apply_filters('dfehc_total_timeout', $timeout + 2); 287 $negKey = dfehc_key('dfehc_probe_fail'); 288 if (get_transient($negKey)) { 289 $r['method'] = 'failed'; 290 $r['main_response_ms'] = $default_ms; 291 if ($r['db_response_ms'] === null) { 292 $r['db_response_ms'] = $default_ms; 293 } 294 return $r; 295 } 296 297 $times = []; 144 298 145 299 for ($i = 0; $i < $n; $i++) { … … 152 306 153 307 $args = [ 154 'timeout' => (int) ceil($remaining),308 'timeout' => max(1, min((int) ceil($remaining), $timeout)), 155 309 'sslverify' => $sslverify, 156 'headers' => ['Cache-Control' => 'no-cache'], 310 'headers' => $headers, 311 'redirection' => min($redirection, 3), 312 'reject_unsafe_urls' => true, 313 'blocking' => true, 314 'decompress' => false, 315 'limit_response_size' => (int) apply_filters('dfehc_limit_response_size', 512), 157 316 ]; 158 317 159 318 $start = microtime(true); 160 $resp = null;161 162 if ($use_head && $head_supported !== false) {319 $resp = null; 320 321 if ($use_head && $head_supported !== 0) { 163 322 $resp = wp_remote_head($probe_url, $args); 164 323 if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) >= 400) { 165 324 if ($head_supported === null) { 166 set_transient($head_key, 0, (int) apply_filters('dfehc_head_negative_ttl', DFEHC_HEAD_NEG_TTL)); 325 $ttl = (int) apply_filters('dfehc_head_negative_ttl', DFEHC_HEAD_NEG_TTL); 326 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 327 dfehc_set_transient_noautoload($head_key, 0, $ttl); 167 328 } 168 329 $resp = null; 169 330 } else { 170 331 if ($head_supported === null) { 171 set_transient($head_key, 1, DFEHC_HEAD_POS_TTL); 332 $ttl = (int) apply_filters('dfehc_head_positive_ttl', DFEHC_HEAD_POS_TTL); 333 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 334 dfehc_set_transient_noautoload($head_key, 1, $ttl); 172 335 } 173 336 } … … 179 342 180 343 $code = is_wp_error($resp) ? 0 : wp_remote_retrieve_response_code($resp); 181 if ( $code >= 200 && $code < 300) {344 if (($code >= 200 && $code < 300) || $code === 304) { 182 345 $times[] = (microtime(true) - $start) * 1000; 183 346 } … … 193 356 194 357 if ($times) { 195 sort($times); 196 $cnt = count($times); 197 $r['measurements'] = $times; 198 $r['main_response_ms'] = $cnt % 2 ? $times[intdiv($cnt, 2)] : ($times[$cnt / 2 - 1] + $times[$cnt / 2]) / 2; 358 sort($times, SORT_NUMERIC); 359 if (count($times) >= 3 && (bool) apply_filters('dfehc_trim_extremes', true)) { 360 array_shift($times); 361 array_pop($times); 362 } 363 $cnt = count($times); 364 $r['measurements'] = $times; 365 $r['main_response_ms'] = $cnt % 2 ? (float) $times[intdiv($cnt, 2)] : (float) (($times[$cnt / 2 - 1] + $times[$cnt / 2]) / 2); 199 366 } else { 367 $ttl = (int) apply_filters('dfehc_probe_fail_ttl', 60); 368 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 369 dfehc_set_transient_noautoload($negKey, 1, $ttl); 200 370 $r['method'] = 'failed'; 201 371 } … … 211 381 } 212 382 213 function dfehc_is_high_traffic(): bool 214 { 215 $flag_key = 'dfehc_high_traffic_flag'; 216 $flag = get_transient($flag_key); 217 if ($flag !== false) { 218 return (bool) $flag; 219 } 220 221 $threshold = (int) apply_filters('dfehc_high_traffic_threshold', 100); 222 $cnt_key = 'dfehc_cached_visitor_cnt'; 223 $count = get_transient($cnt_key); 224 if ($count === false) { 225 $count = function_exists('dfehc_get_website_visitors') && is_numeric(dfehc_get_website_visitors()) ? (int) dfehc_get_website_visitors() : 0; 226 set_transient($cnt_key, $count, 60); 227 } 228 229 $high = $count >= $threshold; 230 set_transient($flag_key, $high ? 1 : 0, 60); 231 232 return $high; 233 } 234 235 function dfehc_acquire_lock(): bool 236 { 237 $key = 'dfehc_measure_lock_' . get_current_blog_id(); 238 239 if (class_exists('WP_Lock')) { 240 $lock = new WP_Lock($key, 60); 241 if ($lock->acquire()) { 242 $GLOBALS['dfehc_rt_lock'] = $lock; 383 function dfehc_ping_handler(): void { 384 $ip = dfehc_client_ip(); 385 $k = dfehc_key('dfehc_ping_rl_' . md5($ip)); 386 $window = (int) apply_filters('dfehc_ping_rl_ttl', 2); 387 $limit = (int) apply_filters('dfehc_ping_rl_limit', 2); 388 $cnt = (int) get_transient($k); 389 if ($cnt >= $limit) { 390 status_header(429); 391 nocache_headers(); 392 wp_send_json_error('rate_limited', 429); 393 } 394 dfehc_set_transient_noautoload($k, $cnt + 1, $window); 395 nocache_headers(); 396 wp_send_json_success('ok'); 397 } 398 399 add_action('wp_ajax_dfehc_ping', 'dfehc_ping_handler'); 400 if (apply_filters('dfehc_enable_public_ping', true)) { 401 add_action('wp_ajax_nopriv_dfehc_ping', 'dfehc_ping_handler'); 402 } 403 404 if (!function_exists('dfehc_acquire_lock')) { 405 function dfehc_acquire_lock(): bool 406 { 407 $key = dfehc_key('dfehc_measure_lock'); 408 409 if (class_exists('WP_Lock')) { 410 $lock = new WP_Lock($key, 60); 411 if ($lock->acquire()) { 412 $GLOBALS['dfehc_rt_lock'] = $lock; 413 return true; 414 } 415 return false; 416 } 417 418 if (function_exists('wp_cache_add') && wp_cache_add($key, 1, DFEHC_CACHE_GROUP, 60)) { 419 $GLOBALS['dfehc_rt_lock_cache_key'] = $key; 243 420 return true; 244 421 } 422 423 if (false !== get_transient($key)) { 424 return false; 425 } 426 427 if (set_transient($key, 1, 60)) { 428 $GLOBALS['dfehc_rt_lock_transient_key'] = $key; 429 return true; 430 } 431 245 432 return false; 246 433 } 247 248 if (function_exists('wp_cache_add') && wp_cache_add($key, 1, '', 60)) { 249 $GLOBALS['dfehc_rt_lock_cache_key'] = $key; 250 return true; 251 } 252 253 if (false !== get_transient($key)) { 254 return false; 255 } 256 257 if (set_transient($key, 1, 60)) { 258 $GLOBALS['dfehc_rt_lock_transient_key'] = $key; 259 return true; 260 } 261 262 return false; 263 } 264 265 function dfehc_release_lock(): void 266 { 267 if (isset($GLOBALS['dfehc_rt_lock']) && $GLOBALS['dfehc_rt_lock'] instanceof WP_Lock) { 268 $GLOBALS['dfehc_rt_lock']->release(); 269 unset($GLOBALS['dfehc_rt_lock']); 270 return; 271 } 272 273 if (isset($GLOBALS['dfehc_rt_lock_cache_key'])) { 274 wp_cache_delete($GLOBALS['dfehc_rt_lock_cache_key']); 275 unset($GLOBALS['dfehc_rt_lock_cache_key']); 276 return; 277 } 278 279 if (isset($GLOBALS['dfehc_rt_lock_transient_key'])) { 280 delete_transient($GLOBALS['dfehc_rt_lock_transient_key']); 281 unset($GLOBALS['dfehc_rt_lock_transient_key']); 282 } 283 } 434 } 435 436 if (!function_exists('dfehc_release_lock')) { 437 function dfehc_release_lock(): void 438 { 439 if (isset($GLOBALS['dfehc_rt_lock']) && $GLOBALS['dfehc_rt_lock'] instanceof WP_Lock) { 440 $GLOBALS['dfehc_rt_lock']->release(); 441 unset($GLOBALS['dfehc_rt_lock']); 442 return; 443 } 444 445 if (isset($GLOBALS['dfehc_rt_lock_cache_key'])) { 446 wp_cache_delete($GLOBALS['dfehc_rt_lock_cache_key'], DFEHC_CACHE_GROUP); 447 unset($GLOBALS['dfehc_rt_lock_cache_key']); 448 return; 449 } 450 451 if (isset($GLOBALS['dfehc_rt_lock_transient_key'])) { 452 delete_transient($GLOBALS['dfehc_rt_lock_transient_key']); 453 unset($GLOBALS['dfehc_rt_lock_transient_key']); 454 } 455 } 456 } 457 458 if (!function_exists('dfehc_is_high_traffic')) { 459 function dfehc_is_high_traffic(): bool 460 { 461 $flag_key = dfehc_key('dfehc_high_traffic_flag'); 462 $flag = get_transient($flag_key); 463 if ($flag !== false) { 464 return (bool) $flag; 465 } 466 467 $threshold = (int) apply_filters('dfehc_high_traffic_threshold', 100); 468 $cnt_key = dfehc_key('dfehc_cached_visitor_cnt'); 469 $count = get_transient($cnt_key); 470 if ($count === false) { 471 $count = (int) apply_filters('dfehc_website_visitors', 0); 472 dfehc_set_transient_noautoload($cnt_key, (int) $count, 60); 473 } 474 475 $load = apply_filters('dfehc_current_server_load', null); 476 if (is_numeric($load)) { 477 $max_load = (float) apply_filters('dfehc_high_traffic_load_threshold', 85.0); 478 if ((float) $load >= $max_load) { 479 dfehc_set_transient_noautoload($flag_key, 1, 60); 480 return true; 481 } 482 } 483 484 $high = $count >= $threshold; 485 dfehc_set_transient_noautoload($flag_key, $high ? 1 : 0, 60); 486 487 return $high; 488 } 489 } -
dynamic-front-end-heartbeat-control/trunk/engine/system-load-fallback.php
r3320647 r3396626 2 2 declare(strict_types=1); 3 3 4 defined('DFEHC_SERVER_LOAD_TTL') || define('DFEHC_SERVER_LOAD_TTL', 60); 5 defined('DFEHC_SENTINEL_NO_LOAD') || define('DFEHC_SENTINEL_NO_LOAD', 0.404); 6 defined('DFEHC_SYSTEM_LOAD_KEY') || define('DFEHC_SYSTEM_LOAD_KEY', 'dfehc_system_load_avg'); 4 defined('DFEHC_SERVER_LOAD_TTL') || define('DFEHC_SERVER_LOAD_TTL', 60); 5 defined('DFEHC_SENTINEL_NO_LOAD') || define('DFEHC_SENTINEL_NO_LOAD', 0.404); 6 defined('DFEHC_SYSTEM_LOAD_KEY') || define('DFEHC_SYSTEM_LOAD_KEY', 'dfehc_system_load_avg'); 7 defined('DFEHC_CACHE_GROUP') || define('DFEHC_CACHE_GROUP', 'dfehc'); 8 9 if (!function_exists('dfehc_host_token')) { 10 function dfehc_host_token(): string { 11 static $t = ''; 12 if ($t !== '') return $t; 13 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 14 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 15 return $t = substr(md5((string) $host . $salt), 0, 10); 16 } 17 } 18 19 if (!function_exists('dfehc_blog_id')) { 20 function dfehc_blog_id(): int { 21 return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0; 22 } 23 } 24 25 if (!function_exists('dfehc_scoped_key')) { 26 function dfehc_scoped_key(string $base): string { 27 return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token(); 28 } 29 } 30 31 if (!function_exists('dfehc_set_transient_noautoload')) { 32 function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void { 33 $jitter = function_exists('random_int') ? random_int(0, 5) : 0; 34 $expiration = max(1, $expiration + $jitter); 35 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) { 36 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration); 37 return; 38 } 39 set_transient($key, $value, $expiration); 40 if (isset($GLOBALS['wpdb'])) { 41 global $wpdb; 42 if (!isset($wpdb->options)) { 43 return; 44 } 45 $wpdb->suppress_errors(true); 46 $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", "_transient_$key")); 47 $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", "_transient_timeout_$key")); 48 $wpdb->suppress_errors(false); 49 } 50 } 51 } 52 53 if (!function_exists('dfehc_exec_with_timeout')) { 54 function dfehc_exec_with_timeout(string $cmd, float $timeoutSec = 1.0): string { 55 $timeoutSec = max(0.1, min(5.0, $timeoutSec)); 56 $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions'))); 57 if (ini_get('open_basedir')) { 58 return ''; 59 } 60 $can_proc = function_exists('proc_open') && !in_array('proc_open', $disabled, true); 61 if ($can_proc) { 62 $descriptorspec = [['pipe','r'],['pipe','w'],['pipe','w']]; 63 $process = @proc_open($cmd, $descriptorspec, $pipes); 64 if (!is_resource($process)) return ''; 65 foreach ($pipes as $p) { @stream_set_blocking($p, false); @stream_set_timeout($p, (int)ceil($timeoutSec)); } 66 $out = ''; $err = ''; 67 $start = microtime(true); 68 $spins = 0; 69 while (true) { 70 $out .= @stream_get_contents($pipes[1]) ?: ''; 71 $err .= @stream_get_contents($pipes[2]) ?: ''; 72 $status = @proc_get_status($process); 73 if (!$status || !$status['running']) break; 74 if ((microtime(true) - $start) > $timeoutSec) { @proc_terminate($process); break; } 75 $spins++; 76 usleep($spins > 10 ? 25000 : 10000); 77 } 78 foreach ($pipes as $p) { @fclose($p); } 79 @proc_close($process); 80 return trim($out); 81 } 82 $can_shell = function_exists('shell_exec') && !in_array('shell_exec', $disabled, true); 83 if ($can_shell) { 84 return trim((string) @shell_exec($cmd)); 85 } 86 return ''; 87 } 88 } 7 89 8 90 if (!function_exists('dfehc_get_cpu_cores')) { 9 function dfehc_get_cpu_cores(): int 10 { 91 function dfehc_get_cpu_cores(): int { 11 92 static $cached = null; 12 if ($cached !== null) { 13 return $cached; 14 } 15 93 if ($cached !== null) return $cached; 94 $tkey = dfehc_scoped_key('dfehc_cpu_cores'); 95 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) { 96 $oc = wp_cache_get($tkey, DFEHC_CACHE_GROUP); 97 if ($oc !== false && (int) $oc > 0) { 98 return $cached = (int) $oc; 99 } 100 } 101 $tc = get_transient($tkey); 102 if ($tc !== false && (int) $tc > 0) { 103 return $cached = (int) $tc; 104 } 16 105 $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions'))); 17 18 if (PHP_OS_FAMILY !== 'Windows' 19 && function_exists('shell_exec') 20 && !in_array('shell_exec', $disabled, true) 21 && !ini_get('open_basedir') 22 ) { 23 $val = trim((string) shell_exec('getconf _NPROCESSORS_ONLN 2>NUL')); 106 if (PHP_OS_FAMILY !== 'Windows' && function_exists('shell_exec') && !in_array('shell_exec', $disabled, true) && !ini_get('open_basedir')) { 107 $val = trim((string) @shell_exec('getconf _NPROCESSORS_ONLN 2>/dev/null')); 24 108 if (ctype_digit($val) && (int) $val > 0) { 25 return $cached = (int) $val; 26 } 27 } 28 29 if (PHP_OS_FAMILY === 'Windows' 30 && function_exists('shell_exec') 31 && !in_array('shell_exec', $disabled, true) 32 ) { 33 $out = shell_exec('wmic cpu get NumberOfLogicalProcessors /value 2>NUL'); 34 if ($out && preg_match('/NumberOfLogicalProcessors=(\d+)/i', $out, $m) && (int) $m[1] > 0) { 35 return $cached = (int) $m[1]; 36 } 37 } 38 109 $cores = (int) $val; 110 dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS)); 111 return $cached = $cores; 112 } 113 } 114 if (PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir')) { 115 $csv = dfehc_exec_with_timeout('typeperf -sc 1 "\Processor(_Total)\% Processor Time" 2>NUL', 1.5); 116 if ($csv !== '') { 117 $wval = dfehc_exec_with_timeout('wmic cpu get NumberOfLogicalProcessors /value 2>NUL', 1.0); 118 if ($wval && preg_match('/NumberOfLogicalProcessors=(\d+)/i', $wval, $m) && (int) $m[1] > 0) { 119 $cores = (int) $m[1]; 120 dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS)); 121 return $cached = $cores; 122 } 123 } 124 $wout = dfehc_exec_with_timeout('wmic cpu get NumberOfLogicalProcessors /value 2>NUL', 1.0); 125 if ($wout && preg_match('/NumberOfLogicalProcessors=(\d+)/i', $wout, $m2) && (int) $m2[1] > 0) { 126 $cores = (int) $m2[1]; 127 dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS)); 128 return $cached = $cores; 129 } 130 } 39 131 if (is_readable('/proc/cpuinfo')) { 40 $cnt = preg_match_all('/^processor/m', (string) file_get_contents('/proc/cpuinfo'));132 $cnt = preg_match_all('/^processor/m', (string) @file_get_contents('/proc/cpuinfo')); 41 133 if ($cnt > 0) { 42 return $cached = $cnt; 43 } 44 } 45 134 $cores = (int) $cnt; 135 dfehc_set_transient_noautoload($tkey, $cores, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS)); 136 return $cached = $cores; 137 } 138 } 139 dfehc_set_transient_noautoload($tkey, 1, (int) apply_filters('dfehc_cpu_cores_ttl', DAY_IN_SECONDS)); 46 140 return $cached = 1; 47 141 } … … 49 143 50 144 if (!function_exists('dfehc_get_system_load_average')) { 51 function dfehc_get_system_load_average(): float 52 { 53 $ttl = (int) apply_filters('dfehc_system_load_ttl', DFEHC_SERVER_LOAD_TTL); 54 $key = DFEHC_SYSTEM_LOAD_KEY; 145 function dfehc_get_system_load_average(): float { 146 $ttl = (int) apply_filters('dfehc_system_load_ttl', DFEHC_SERVER_LOAD_TTL); 147 $key = dfehc_scoped_key(DFEHC_SYSTEM_LOAD_KEY); 148 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) { 149 $vc = wp_cache_get($key, DFEHC_CACHE_GROUP); 150 if ($vc !== false && $vc !== null) { 151 $ratio = (float) $vc; 152 $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio); 153 return $as_percent ? (float) round($ratio * 100, 2) : $ratio; 154 } 155 } 55 156 $cache = get_transient($key); 56 157 if ($cache !== false) { 57 return (float) $cache; 158 $ratio = (float) $cache; 159 $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio); 160 return $as_percent ? (float) round($ratio * 100, 2) : $ratio; 58 161 } 59 162 60 163 $raw = null; 164 $source = ''; 165 $normalized_ratio = false; 61 166 62 167 if (function_exists('dfehc_get_server_load')) { 63 168 $val = dfehc_get_server_load(); 64 169 if ($val !== DFEHC_SENTINEL_NO_LOAD) { 65 dfehc_set_transient_noautoload($key, $val, $ttl); 66 return (float) $val; 170 dfehc_set_transient_noautoload($key, (float) $val, $ttl); 171 $ratio = (float) $val; 172 $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio); 173 return $as_percent ? (float) round($ratio * 100, 2) : $ratio; 67 174 } 68 175 } 69 176 70 177 if (function_exists('sys_getloadavg')) { 71 $arr = sys_getloadavg();178 $arr = @sys_getloadavg(); 72 179 if ($arr && isset($arr[0])) { 73 180 $raw = (float) $arr[0]; 181 $source = 'sys_getloadavg'; 74 182 } 75 183 } 76 184 77 185 if ($raw === null && PHP_OS_FAMILY !== 'Windows' && is_readable('/proc/loadavg')) { 78 $parts = explode(' ', (string) file_get_contents('/proc/loadavg'));186 $parts = explode(' ', (string) @file_get_contents('/proc/loadavg')); 79 187 if (isset($parts[0])) { 80 188 $raw = (float) $parts[0]; 81 } 82 } 83 84 if ($raw === null) { 85 $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions'))); 86 if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) { 87 if (PHP_OS_FAMILY === 'Windows') { 88 $out = shell_exec('wmic cpu get loadpercentage /value 2>NUL'); 89 if ($out && preg_match('/loadpercentage=(\d+)/i', $out, $m)) { 90 $raw = ((float) $m[1]) / 100.0 * dfehc_get_cpu_cores(); 91 } 189 $source = 'proc_loadavg'; 190 } 191 } 192 193 if ($raw === null && PHP_OS_FAMILY !== 'Windows') { 194 $out = dfehc_exec_with_timeout('LANG=C uptime 2>/dev/null', 1.0); 195 if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) { 196 $raw = (float) $m[1]; 197 $source = 'uptime'; 198 } 199 } 200 201 if ($raw === null && PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir')) { 202 $csv = dfehc_exec_with_timeout('typeperf -sc 1 "\Processor(_Total)\% Processor Time" 2>NUL', 1.5); 203 if ($csv) { 204 $lines = array_values(array_filter(array_map('trim', explode("\n", $csv)))); 205 $last = end($lines); 206 if ($last && preg_match('/,"?([0-9.]+)"?$/', $last, $m)) { 207 $pct = (float) $m[1]; 208 $raw = ($pct / 100.0) * dfehc_get_cpu_cores(); 209 $source = 'typeperf'; 210 } 211 } 212 } 213 214 if ($raw === null && PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir')) { 215 $psCmd = "powershell -NoProfile -NonInteractive -Command \"\\\$v=(Get-Counter '\\Processor(_Total)\\% Processor Time').CounterSamples[0].CookedValue; [Console]::Out.WriteLine([Math]::Round(\\\$v,2))\" 2>NUL"; 216 $ps = dfehc_exec_with_timeout($psCmd, 1.5); 217 if ($ps !== '' && is_numeric(trim($ps))) { 218 $pct = (float) trim($ps); 219 $raw = ($pct / 100.0) * dfehc_get_cpu_cores(); 220 $source = 'powershell'; 221 } 222 } 223 224 if ($raw === null && PHP_OS_FAMILY === 'Windows' && !ini_get('open_basedir')) { 225 $out = dfehc_exec_with_timeout('wmic cpu get loadpercentage /value 2>NUL', 1.0); 226 if ($out && preg_match('/loadpercentage=(\d+)/i', $out, $m)) { 227 $pct = (float) $m[1]; 228 $raw = ($pct / 100.0) * dfehc_get_cpu_cores(); 229 $source = 'wmic'; 230 } 231 } 232 233 if ($raw === null && class_exists('\\DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) { 234 $est = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load(); 235 if (is_numeric($est)) { 236 $pct = (float) $est; 237 if ($pct >= 0.0 && $pct <= 100.0) { 238 $raw = $pct / 100.0; 239 $normalized_ratio = true; 240 $source = 'estimator_percent'; 92 241 } else { 93 $out = shell_exec('LANG=C uptime 2>/dev/null'); 94 if ($out && preg_match('/load average[s]?:\s*([0-9.]+)/', $out, $m)) { 95 $raw = (float) $m[1]; 96 } 242 $raw = (float) $pct; 243 $source = 'estimator_raw'; 97 244 } 98 245 } 99 }100 101 if ($raw === null && class_exists('\\DynamicHeartbeat\\Dfehc_ServerLoadEstimator')) {102 $raw = (float) \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load();103 246 } 104 247 … … 106 249 $sentinel_ttl = (int) apply_filters('dfehc_sentinel_ttl', 5); 107 250 dfehc_set_transient_noautoload($key, DFEHC_SENTINEL_NO_LOAD, $sentinel_ttl); 108 return DFEHC_SENTINEL_NO_LOAD; 109 } 110 111 $divide = (bool) apply_filters('dfehc_divide_load_by_cores', true, $raw); 112 if ($divide) { 113 $cores = dfehc_get_cpu_cores(); 114 if ($cores > 0) { 115 $raw = $raw / $cores; 116 } 117 } 118 119 dfehc_set_transient_noautoload($key, $raw, $ttl); 120 return (float) $raw; 121 } 122 } 251 $ratio = (float) DFEHC_SENTINEL_NO_LOAD; 252 $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio); 253 return $as_percent ? (float) round($ratio * 100, 2) : $ratio; 254 } 255 256 if ($normalized_ratio) { 257 $ratio = (float) $raw; 258 } else { 259 $divide = (bool) apply_filters('dfehc_divide_load_by_cores', true, $raw, $source); 260 if ($divide) { 261 $cores = dfehc_get_cpu_cores(); 262 $ratio = $cores > 0 ? ((float) $raw) / $cores : (float) $raw; 263 } else { 264 $ratio = (float) $raw; 265 } 266 } 267 268 dfehc_set_transient_noautoload($key, $ratio, $ttl); 269 $as_percent = (bool) apply_filters('dfehc_system_load_return_percent', false, $ratio); 270 return $as_percent ? (float) round($ratio * 100, 2) : (float) $ratio; 271 } 272 } -
dynamic-front-end-heartbeat-control/trunk/heartbeat-async.php
r3320647 r3396626 2 2 declare(strict_types=1); 3 3 4 define('DFEHC_MAX_SERVER_LOAD', 85);5 define('DFEHC_MIN_INTERVAL', 15);6 define('DFEHC_MAX_INTERVAL', 300);7 define('DFEHC_FALLBACK_INTERVAL', 60);8 4 define('DFEHC_LOAD_AVERAGES', 'dfehc_load_averages'); 9 5 define('DFEHC_SERVER_LOAD', 'dfehc_server_load'); … … 11 7 define('DFEHC_CAPABILITY', 'read'); 12 8 define('DFEHC_LOAD_LOCK_BASE', 'dfehc_compute_load_lock'); 9 define('DFEHC_CACHE_GROUP', 'dfehc'); 10 11 function dfehc_max_server_load(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_max_server_load', 85); } return $v; } 12 function dfehc_min_interval(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_min_interval', 15); } return $v; } 13 function dfehc_max_interval(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_max_interval', 300); } return $v; } 14 function dfehc_fallback_interval(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_fallback_interval', 60); } return $v; } 15 function dfehc_server_load_ttl(): int { static $v; if ($v === null) { $v = (int) apply_filters('dfehc_server_load_ttl', 180); } return $v; } 16 17 function dfehc_host_token(): string 18 { 19 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 20 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 21 return substr(md5((string) $host . $salt), 0, 10); 22 } 23 24 function dfehc_scoped_key(string $base): string 25 { 26 $blog = function_exists('get_current_blog_id') ? (string) get_current_blog_id() : '0'; 27 return "{$base}_{$blog}_" . dfehc_host_token(); 28 } 13 29 14 30 function dfehc_store_lockfree(string $key, $value, int $ttl): bool 15 31 { 16 if (function_exists('wp_cache_add') && wp_cache_add($key, $value, '', $ttl)) {32 if (function_exists('wp_cache_add') && wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $ttl)) { 17 33 return true; 18 34 } … … 20 36 } 21 37 38 function dfehc_client_ip(): string 39 { 40 $ip = isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; 41 $trusted = (array) apply_filters('dfehc_trusted_proxies', []); 42 if ($trusted && in_array($ip, $trusted, true)) { 43 $headers = (array) apply_filters('dfehc_proxy_ip_headers', ['HTTP_X_FORWARDED_FOR', 'HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP']); 44 foreach ($headers as $h) { 45 if (!empty($_SERVER[$h])) { 46 $raw = (string) $_SERVER[$h]; 47 $parts = array_map('trim', explode(',', $raw)); 48 $cand = end($parts); 49 if ($cand) { 50 return $cand; 51 } 52 } 53 } 54 } 55 return $ip; 56 } 57 22 58 function dfehc_register_ajax(string $action, callable $callback): void 23 59 { 24 60 add_action("wp_ajax_$action", $callback); 25 if (apply_filters('dfehc_allow_public_server_load', false)) { 61 $allow_public = false; 62 if ($action === 'get_server_load') { 63 $allow_public = (bool) apply_filters('dfehc_allow_public_server_load', false); 64 } elseif ($action === 'dfehc_async_heartbeat') { 65 $allow_public = (bool) apply_filters('dfehc_allow_public_async', false); 66 } else { 67 $allow_public = (bool) apply_filters("{$action}_allow_public", false); 68 } 69 if ($allow_public) { 26 70 add_action("wp_ajax_nopriv_$action", $callback); 27 71 } … … 31 75 { 32 76 if (wp_using_ext_object_cache()) { 33 wp_cache_set($key, $value, '', $expiration); 77 if (function_exists('wp_cache_add')) { 78 if (!wp_cache_add($key, $value, DFEHC_CACHE_GROUP, $expiration)) { 79 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration); 80 } 81 } else { 82 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration); 83 } 34 84 return; 35 85 } 36 86 dfehc_store_lockfree($key, $value, $expiration); 37 87 global $wpdb; 38 $wpdb->query( 39 $wpdb->prepare( 40 "UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", 41 "_transient_$key" 42 ) 43 ); 88 $opt_key = "_transient_$key"; 89 $opt_key_to = "_transient_timeout_$key"; 90 $wpdb->suppress_errors(true); 91 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key)); 92 if ($autoload === 'yes') { 93 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 94 } 95 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to)); 96 if ($autoload_to === 'yes') { 97 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 98 } 99 $wpdb->suppress_errors(false); 44 100 } 45 101 … … 49 105 static $cached = null; 50 106 if ($cached !== null) { 51 return $cached; 52 } 107 return (int) $cached; 108 } 109 $detected = 1; 53 110 if (is_readable('/proc/cpuinfo')) { 54 111 $cnt = preg_match_all('/^processor/m', (string) file_get_contents('/proc/cpuinfo')); 55 112 if ($cnt > 0) { 56 return $cached = $cnt; 57 } 58 } 59 $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions'))); 60 if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) { 61 $out = (string) (shell_exec('nproc 2>/dev/null') ?? ''); 62 if (ctype_digit(trim($out)) && (int) $out > 0) { 63 return $cached = (int) trim($out); 64 } 65 } 66 return $cached = 1; 113 $detected = $cnt; 114 } 115 } else { 116 $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions'))); 117 if (function_exists('shell_exec') && !in_array('shell_exec', $disabled, true)) { 118 $out = (string) (shell_exec('nproc 2>/dev/null') ?? ''); 119 if (ctype_digit(trim($out)) && (int) $out > 0) { 120 $detected = (int) trim($out); 121 } 122 } 123 } 124 $env_override = getenv('DFEHC_CPU_CORES'); 125 if ($env_override !== false && ctype_digit((string) $env_override) && (int) $env_override > 0) { 126 $detected = (int) $env_override; 127 } 128 $detected = (int) apply_filters('dfehc_cpu_cores', $detected); 129 if ($detected <= 0) { 130 $detected = 1; 131 } 132 $cached = $detected; 133 return (int) $cached; 67 134 } 68 135 } … … 70 137 function dfehc_acquire_lock(string $base, int $ttl) 71 138 { 72 $key = $base . '_' . get_current_blog_id();139 $key = dfehc_scoped_key($base); 73 140 if (class_exists('WP_Lock')) { 74 141 $lock = new WP_Lock($key, $ttl); … … 78 145 return null; 79 146 } 80 if (function_exists('wp_cache_add') && wp_cache_add($key, 1, '', $ttl)) {147 if (function_exists('wp_cache_add') && wp_cache_add($key, 1, DFEHC_CACHE_GROUP, $ttl)) { 81 148 return (object) ['cache_key' => $key]; 82 149 } … … 97 164 } 98 165 if (is_object($lock) && isset($lock->cache_key)) { 99 wp_cache_delete($lock->cache_key );166 wp_cache_delete($lock->cache_key, DFEHC_CACHE_GROUP); 100 167 return; 101 168 } … … 105 172 } 106 173 107 function dfehc_get_or_calculate_server_load(): float|false 108 { 109 $load = get_transient(DFEHC_SERVER_LOAD); 174 function dfehc_get_or_calculate_server_load() 175 { 176 $key = dfehc_scoped_key(DFEHC_SERVER_LOAD); 177 $load = get_transient($key); 110 178 if ($load !== false) { 111 179 return (float) $load; 112 180 } 113 $ttl = (int) apply_filters('dfehc_server_load_ttl', 180);181 $ttl = dfehc_server_load_ttl(); 114 182 $lock = dfehc_acquire_lock(DFEHC_LOAD_LOCK_BASE, $ttl + 5); 115 183 if (!$lock) { … … 123 191 $cores = max(1, dfehc_get_cpu_cores()); 124 192 $load_pct = min(100.0, round($raw / $cores * 100, 2)); 125 dfehc_set_transient_noautoload( DFEHC_SERVER_LOAD, $load_pct, $ttl);193 dfehc_set_transient_noautoload($key, $load_pct, $ttl); 126 194 return $load_pct; 127 195 } … … 129 197 function dfehc_get_server_load_ajax_handler(): void 130 198 { 131 $nonce = $_REQUEST['nonce'] ?? '';132 if (!wp_verify_nonce((string) $nonce, 'dfehc-ajax-nonce')) {133 wp_send_json_error('Heartbeat: Invalid nonce.');134 }135 199 $cap = apply_filters('dfehc_required_capability', DFEHC_CAPABILITY); 136 $allow_public = apply_filters('dfehc_allow_public_server_load', false); 137 if (!current_user_can($cap) && !$allow_public) { 138 wp_send_json_error('Heartbeat: Not authorised.'); 200 $allow_public = (bool) apply_filters('dfehc_allow_public_server_load', false); 201 if (!$allow_public) { 202 $nonce_action = 'dfehc-get_server_load'; 203 $valid = function_exists('check_ajax_referer') 204 ? check_ajax_referer($nonce_action, 'nonce', false) 205 : wp_verify_nonce(isset($_REQUEST['nonce']) ? (string) $_REQUEST['nonce'] : '', $nonce_action); 206 if (!$valid) { 207 wp_send_json_error(['message' => 'dfehc/get_server_load: invalid nonce'], 403); 208 } 209 if (!current_user_can($cap)) { 210 wp_send_json_error(['message' => 'dfehc/get_server_load: not authorized'], 403); 211 } 212 } else { 213 $limit = absint(apply_filters('dfehc_public_rate_limit', 30)); 214 $window = absint(apply_filters('dfehc_public_rate_window', 60)); 215 $ip = dfehc_client_ip(); 216 $rl_key = dfehc_scoped_key('dfehc_rl_' . md5($ip)); 217 $cnt = (int) get_transient($rl_key); 218 if ($cnt >= $limit) { 219 wp_send_json_error(['message' => 'dfehc/get_server_load: rate limited'], 429); 220 } 221 dfehc_set_transient_noautoload($rl_key, $cnt + 1, $window); 139 222 } 140 223 $load = dfehc_get_or_calculate_server_load(); 141 224 if ($load === false) { 142 wp_send_json_success( DFEHC_FALLBACK_INTERVAL);143 } 144 $interval = dfehc_calculate_recommended_interval_user_activity( $load);225 wp_send_json_success(dfehc_fallback_interval()); 226 } 227 $interval = dfehc_calculate_recommended_interval_user_activity((float) $load); 145 228 if ($interval <= 0) { 146 $interval = DFEHC_FALLBACK_INTERVAL;229 $interval = dfehc_fallback_interval(); 147 230 } 148 231 wp_send_json_success($interval); … … 150 233 dfehc_register_ajax('get_server_load', 'dfehc_get_server_load_ajax_handler'); 151 234 152 function dfehc_calculate_server_load() : float|false235 function dfehc_calculate_server_load() 153 236 { 154 237 if (function_exists('sys_getloadavg')) { … … 171 254 } 172 255 } 173 if (defined('DFEHC_PLUGIN_PATH') && file_exists(DFEHC_PLUGIN_PATH . 'defibrillator/load-estimator.php')) { 174 require_once DFEHC_PLUGIN_PATH . 'defibrillator/load-estimator.php'; 175 if (class_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class)) { 176 return \DynamicHeartbeat\Dfehc_ServerLoadEstimator::estimate(); 256 if (defined('DFEHC_PLUGIN_PATH')) { 257 $estimator = rtrim((string) DFEHC_PLUGIN_PATH, "/\\") . '/defibrillator/load-estimator.php'; 258 if (file_exists($estimator)) { 259 require_once $estimator; 260 if (class_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class)) { 261 if (method_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class, 'get_server_load')) { 262 $pct = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::get_server_load(); 263 if ($pct !== false && is_numeric($pct)) { 264 $cores = max(1, dfehc_get_cpu_cores()); 265 return (float) ($cores * ((float) $pct) / 100.0); 266 } 267 } elseif (method_exists(\DynamicHeartbeat\Dfehc_ServerLoadEstimator::class, 'estimate')) { 268 $raw = \DynamicHeartbeat\Dfehc_ServerLoadEstimator::estimate(); 269 if (is_numeric($raw)) { 270 return (float) $raw; 271 } 272 } 273 } 177 274 } 178 275 } … … 182 279 class Heartbeat_Async 183 280 { 184 protected string $action = 'dfehc_async_heartbeat'; 185 protected bool $scheduled = false; 281 protected $action = 'dfehc_async_heartbeat'; 282 protected $scheduled = false; 283 186 284 public function __construct() 187 285 { 188 286 add_action('init', [$this, 'maybe_schedule']); 287 add_action($this->action, [$this, 'run_action']); 189 288 dfehc_register_ajax($this->action, [$this, 'handle_async_request']); 190 289 } 290 191 291 public function maybe_schedule(): void 192 292 { 193 if ($this->scheduled ) {293 if ($this->scheduled === true) { 194 294 return; 195 295 } 196 296 $this->scheduled = true; 197 $interval = 300; 198 $aligned = time() - (time() % $interval) + $interval; 297 $interval = absint(300); 298 $now = time(); 299 $aligned = $now - ($now % $interval) + $interval; 300 $schedules = function_exists('wp_get_schedules') ? wp_get_schedules() : []; 301 if (!isset($schedules['dfehc_5_minutes'])) { 302 return; 303 } 199 304 if (!wp_next_scheduled($this->action)) { 200 305 wp_schedule_event($aligned, 'dfehc_5_minutes', $this->action); 201 306 } else { 202 307 $next = wp_next_scheduled($this->action); 203 if ($next === false || $next - time() > $interval * 2) { 204 wp_schedule_single_event(time() + $interval, $this->action); 205 } 206 } 207 } 308 if ($next === false || ($next - $now) > ($interval * 2)) { 309 wp_schedule_single_event($now + $interval, $this->action); 310 } 311 } 312 } 313 208 314 public function handle_async_request(): void 209 315 { 316 $cap = apply_filters('dfehc_required_capability', DFEHC_CAPABILITY); 317 $allow_public = (bool) apply_filters('dfehc_allow_public_async', false); 318 if (!$allow_public) { 319 $nonce_action = 'dfehc-' . $this->action; 320 $valid = function_exists('check_ajax_referer') 321 ? check_ajax_referer($nonce_action, 'nonce', false) 322 : wp_verify_nonce(isset($_REQUEST['nonce']) ? (string) $_REQUEST['nonce'] : '', $nonce_action); 323 if (!$valid) { 324 wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: invalid nonce'], 403); 325 } 326 if (!current_user_can($cap)) { 327 wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: not authorized'], 403); 328 } 329 } else { 330 $limit = absint(apply_filters('dfehc_public_rate_limit', 30)); 331 $window = absint(apply_filters('dfehc_public_rate_window', 60)); 332 $ip = dfehc_client_ip(); 333 $rl_key = dfehc_scoped_key('dfehc_rl_' . md5($ip)); 334 $cnt = (int) get_transient($rl_key); 335 if ($cnt >= $limit) { 336 wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: rate limited'], 429); 337 } 338 dfehc_set_transient_noautoload($rl_key, $cnt + 1, $window); 339 } 210 340 try { 211 341 $this->run_action(); 342 wp_send_json_success(true); 212 343 } catch (\Throwable $e) { 213 344 if (defined('WP_DEBUG') && WP_DEBUG) { 214 error_log('[DFEHC Async] ' . $e->getMessage()); 215 } 345 error_log('[dfehc] async error: ' . $e->getMessage()); 346 } 347 wp_send_json_error(['message' => 'dfehc/dfehc_async_heartbeat: internal error'], 500); 216 348 } 217 349 wp_die(); 218 350 } 351 219 352 protected function run_action(): void 220 353 { 221 $l ast_activity = get_transient('dfehc_last_user_activity');222 if ( $last_activity === false) {354 $lock = dfehc_acquire_lock('dfehc_async_run', 30); 355 if (!$lock) { 223 356 return; 224 357 } 225 $elapsed = time() - (int) $last_activity; 226 $load_raw = dfehc_calculate_server_load(); 227 if ($load_raw === false) { 228 return; 229 } 230 $cores = max(1, dfehc_get_cpu_cores()); 231 $load_pct = min(100.0, round($load_raw / $cores * 100, 2)); 232 dfehc_set_transient_noautoload(DFEHC_SERVER_LOAD, $load_pct, (int) apply_filters('dfehc_server_load_ttl', 300)); 233 $samples = get_transient(DFEHC_LOAD_AVERAGES) ?: []; 234 $samples[] = $load_pct; 235 if (count($samples) > 5) { 236 array_shift($samples); 237 } 238 dfehc_set_transient_noautoload(DFEHC_LOAD_AVERAGES, $samples, 1800); 239 $weights_raw = apply_filters('dfehc_load_weights', [5, 4, 3, 2, 1]); 240 $weights = array_slice(array_pad(array_values((array) $weights_raw), count($samples), 1), 0, count($samples)); 241 $avg_load = dfehc_weighted_average($samples, $weights); 242 $interval = $this->calculate_interval($elapsed, $avg_load); 243 dfehc_set_transient_noautoload(DFEHC_RECOMMENDED_INTERVAL, $interval, 300); 244 } 358 try { 359 $last_activity = get_transient(dfehc_scoped_key('dfehc_last_user_activity')); 360 if ($last_activity === false) { 361 dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), dfehc_fallback_interval(), 300); 362 return; 363 } 364 $now = time(); 365 $elapsed = $now - (int) $last_activity; 366 if ($elapsed < 0) { 367 $elapsed = 0; 368 } 369 $load_raw = dfehc_calculate_server_load(); 370 if ($load_raw === false) { 371 dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), dfehc_fallback_interval(), 300); 372 return; 373 } 374 $cores = max(1, dfehc_get_cpu_cores()); 375 $load_pct = min(100.0, round($load_raw / $cores * 100, 2)); 376 dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_SERVER_LOAD), $load_pct, dfehc_server_load_ttl()); 377 $samples = get_transient(dfehc_scoped_key(DFEHC_LOAD_AVERAGES)); 378 if (!is_array($samples)) { 379 $samples = []; 380 } 381 $samples[] = $load_pct; 382 if (count($samples) > 5) { 383 array_shift($samples); 384 } 385 dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_LOAD_AVERAGES), $samples, 1800); 386 $weights_raw = apply_filters('dfehc_load_weights', [5, 4, 3, 2, 1]); 387 $wr = array_values((array) $weights_raw); 388 if (!$wr) { 389 $wr = [1]; 390 } 391 $wr_count = count($wr); 392 $weights = []; 393 $n = count($samples); 394 for ($i = 0; $i < $n; $i++) { 395 $j = min($n - 1 - $i, $wr_count - 1); 396 $w = isset($wr[$j]) ? $wr[$j] : 1; 397 if (!is_numeric($w) || $w <= 0) { 398 $w = 1; 399 } 400 $weights[] = (float) $w; 401 } 402 $avg_load = dfehc_weighted_average($samples, $weights); 403 $interval = $this->calculate_interval($elapsed, $avg_load); 404 dfehc_set_transient_noautoload(dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL), $interval, 300); 405 } finally { 406 dfehc_release_lock($lock); 407 } 408 } 409 245 410 protected function calculate_interval(int $elapsed, float $load_pct): int 246 411 { 247 if ($elapsed <= DFEHC_MIN_INTERVAL && $load_pct < DFEHC_MAX_SERVER_LOAD) {248 return DFEHC_MIN_INTERVAL;249 } 250 if ($elapsed >= DFEHC_MAX_INTERVAL) {251 return DFEHC_MAX_INTERVAL;252 } 253 $load_factor = min(1.0, $load_pct / DFEHC_MAX_SERVER_LOAD);254 $activity_factor = ($elapsed - DFEHC_MIN_INTERVAL) / (DFEHC_MAX_INTERVAL - DFEHC_MIN_INTERVAL);412 if ($elapsed <= dfehc_min_interval() && $load_pct < dfehc_max_server_load()) { 413 return dfehc_min_interval(); 414 } 415 if ($elapsed >= dfehc_max_interval()) { 416 return dfehc_max_interval(); 417 } 418 $load_factor = min(1.0, $load_pct / dfehc_max_server_load()); 419 $activity_factor = ($elapsed - dfehc_min_interval()) / (dfehc_max_interval() - dfehc_min_interval()); 255 420 $activity_factor = max(0.0, min(1.0, $activity_factor)); 256 421 $dominant = max($load_factor, $activity_factor); 257 return (int) round( DFEHC_MIN_INTERVAL + $dominant * (DFEHC_MAX_INTERVAL - DFEHC_MIN_INTERVAL));422 return (int) round(dfehc_min_interval() + $dominant * (dfehc_max_interval() - dfehc_min_interval())); 258 423 } 259 424 } … … 261 426 function dfehc_weighted_average(array $values, array $weights): float 262 427 { 428 if ($values === []) { 429 return 0.0; 430 } 263 431 $tv = 0.0; 264 432 $tw = 0.0; 265 433 foreach ($values as $i => $v) { 266 $w = $weights[$i] ?? 1; 267 $tv += $v * $w; 268 $tw += $w; 434 $w = isset($weights[$i]) ? $weights[$i] : 1; 435 if (!is_numeric($w) || $w <= 0) { 436 $w = 1; 437 } 438 $tv += (float) $v * (float) $w; 439 $tw += (float) $w; 269 440 } 270 441 return $tw > 0 ? round($tv / $tw, 2) : 0.0; … … 273 444 function dfehc_calculate_recommended_interval_user_activity(float $current_load): int 274 445 { 275 $interval = get_transient(DFEHC_RECOMMENDED_INTERVAL); 446 $key = dfehc_scoped_key(DFEHC_RECOMMENDED_INTERVAL); 447 $interval = get_transient($key); 276 448 if ($interval !== false) { 277 449 return (int) $interval; 278 450 } 279 return $current_load >= DFEHC_MAX_SERVER_LOAD ? DFEHC_MAX_INTERVAL : DFEHC_MIN_INTERVAL;451 return $current_load >= dfehc_max_server_load() ? dfehc_max_interval() : dfehc_min_interval(); 280 452 } 281 453 … … 290 462 return $s; 291 463 } 292 add_filter('cron_schedules', 'dfehc_register_schedules' );464 add_filter('cron_schedules', 'dfehc_register_schedules', 1); 293 465 294 466 function dfehc_prune_server_load_logs(): void 295 467 { 296 $max_age = (int) apply_filters('dfehc_log_retention_seconds', DAY_IN_SECONDS);297 $max_cnt = (int) apply_filters('dfehc_log_retention_max', 1440);468 $max_age = absint(apply_filters('dfehc_log_retention_seconds', DAY_IN_SECONDS)); 469 $max_cnt = absint(apply_filters('dfehc_log_retention_max', 1440)); 298 470 $now = time(); 299 471 $all_ids = function_exists('get_sites') 300 ? array_map( static fn($s) => (int) $s->blog_id, get_sites(['fields' => 'ids']))472 ? array_map('intval', get_sites(['fields' => 'ids'])) 301 473 : [get_current_blog_id()]; 302 $chunk_size = (int) apply_filters('dfehc_prune_chunk_size', 50);474 $chunk_size = absint(apply_filters('dfehc_prune_chunk_size', 50)); 303 475 $offset_key = 'dfehc_prune_offset'; 304 476 $offset = (int) get_site_option($offset_key, 0); … … 309 481 } 310 482 foreach ($chunk as $id) { 483 $did_switch = false; 311 484 if (is_multisite()) { 312 485 switch_to_blog($id); 313 } 314 $option = 'dfehc_server_load_logs_' . get_current_blog_id(); 315 $logs = get_option($option, []); 316 if ($logs) { 317 $cutoff = $now - $max_age; 318 $logs = array_filter( 319 $logs, 320 static fn($row) => isset($row['timestamp']) && $row['timestamp'] >= $cutoff 321 ); 322 if (count($logs) > $max_cnt) { 323 $logs = array_slice($logs, -$max_cnt); 324 } 325 update_option($option, array_values($logs), false); 326 } 327 if (is_multisite()) { 328 restore_current_blog(); 486 $did_switch = true; 487 } 488 try { 489 $option = 'dfehc_server_load_logs_' . get_current_blog_id(); 490 $logs = get_option($option, []); 491 if ($logs) { 492 $cutoff = $now - $max_age; 493 $logs = array_filter( 494 $logs, 495 static function ($row) use ($cutoff) { 496 return isset($row['timestamp']) && (int) $row['timestamp'] >= $cutoff; 497 } 498 ); 499 if (count($logs) > $max_cnt) { 500 $logs = array_slice($logs, -$max_cnt); 501 } 502 update_option($option, array_values($logs), false); 503 } 504 } finally { 505 if ($did_switch) { 506 restore_current_blog(); 507 } 329 508 } 330 509 } … … 343 522 } 344 523 345 add_filter('dfehc_required_capability', fn() => 'manage_options'); 346 add_filter('dfehc_server_load_ttl', fn() => 120); 347 add_filter('dfehc_load_weights', fn() => [3, 2, 1]); 348 add_filter('dfehc_async_retry', fn() => 1); 349 add_filter('dfehc_log_retention_seconds', fn() => 2 * DAY_IN_SECONDS); 350 add_filter('dfehc_log_retention_max', fn() => 3000); 524 add_filter('dfehc_required_capability', function () { return 'manage_options'; }); 525 add_filter('dfehc_server_load_ttl', function () { return 120; }); 526 add_filter('dfehc_load_weights', function () { return [3, 2, 1]; }); 527 add_filter('dfehc_async_retry', function () { return 1; }); 528 add_filter('dfehc_log_retention_seconds', function () { return 2 * DAY_IN_SECONDS; }); 529 add_filter('dfehc_log_retention_max', function () { return 3000; }); 530 add_filter('dfehc_allow_public_server_load', '__return_false'); 531 add_filter('dfehc_allow_public_async', '__return_false'); 351 532 352 533 new Heartbeat_Async(); -
dynamic-front-end-heartbeat-control/trunk/heartbeat-controller.php
r3320647 r3396626 4 4 Plugin URI: https://heartbeat.support 5 5 Description: An enhanced solution to optimize the performance of your WordPress website. Stabilize your website's load averages and enhance the browsing experience for visitors during high-traffic fluctuations. 6 Version: 1.2.99 6 Version: 1.2.995 7 7 Author: Codeloghin 8 8 Author URI: https://codeloghin.com … … 15 15 define('DFEHC_PLUGIN_PATH', plugin_dir_path(__FILE__)); 16 16 } 17 18 require_once plugin_dir_path(__FILE__) . 'engine/interval-helper.php'; 19 require_once plugin_dir_path(__FILE__) . 'engine/server-load.php'; 20 require_once plugin_dir_path(__FILE__) . 'engine/server-response.php'; 21 require_once plugin_dir_path(__FILE__) . 'engine/system-load-fallback.php'; 22 require_once plugin_dir_path(__FILE__) . 'visitor/manager.php'; 23 require_once plugin_dir_path(__FILE__) . 'visitor/cookie-helper.php'; 24 require_once plugin_dir_path(__FILE__) . 'defibrillator/unclogger.php'; 25 require_once plugin_dir_path(__FILE__) . 'defibrillator/rest-api.php'; 26 require_once plugin_dir_path(__FILE__) . 'defibrillator/db-health.php'; 27 require_once plugin_dir_path(__FILE__) . 'widget.php'; 28 require_once plugin_dir_path(__FILE__) . 'settings.php'; 29 17 if (!defined('DFEHC_CACHE_GROUP')) { 18 define('DFEHC_CACHE_GROUP', 'dfehc'); 19 } 30 20 if (!defined('DFEHC_MIN_INTERVAL')) { 31 21 define('DFEHC_MIN_INTERVAL', 15); … … 53 43 } 54 44 45 require_once DFEHC_PLUGIN_PATH . 'engine/interval-helper.php'; 46 require_once DFEHC_PLUGIN_PATH . 'engine/server-load.php'; 47 require_once DFEHC_PLUGIN_PATH . 'engine/server-response.php'; 48 require_once DFEHC_PLUGIN_PATH . 'engine/system-load-fallback.php'; 49 require_once DFEHC_PLUGIN_PATH . 'visitor/manager.php'; 50 require_once DFEHC_PLUGIN_PATH . 'visitor/cookie-helper.php'; 51 require_once DFEHC_PLUGIN_PATH . 'defibrillator/unclogger.php'; 52 require_once DFEHC_PLUGIN_PATH . 'defibrillator/rest-api.php'; 53 require_once DFEHC_PLUGIN_PATH . 'defibrillator/db-health.php'; 54 require_once DFEHC_PLUGIN_PATH . 'widget.php'; 55 require_once DFEHC_PLUGIN_PATH . 'settings.php'; 56 57 function dfehc_scoped_tkey(string $base): string 58 { 59 if (function_exists('dfehc_scoped_key')) { 60 return dfehc_scoped_key($base); 61 } 62 $bid = function_exists('get_current_blog_id') ? (string) get_current_blog_id() : '0'; 63 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 64 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 65 $tok = substr(md5((string) $host . $salt), 0, 10); 66 return "{$base}_{$bid}_{$tok}"; 67 } 68 55 69 function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void 56 70 { 57 if (wp_using_ext_object_cache()) { 58 wp_cache_set($key, $value, '', $expiration); 71 $jitter = function_exists('random_int') ? random_int(0, 5) : 0; 72 $expiration = max(1, $expiration + $jitter); 73 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) { 74 wp_cache_set($key, $value, DFEHC_CACHE_GROUP, $expiration); 59 75 return; 60 76 } 61 77 set_transient($key, $value, $expiration); 62 global $wpdb; 63 $wpdb->query( 64 $wpdb->prepare( 65 "UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", 66 '_transient_' . $key 67 ) 68 ); 78 if (isset($GLOBALS['wpdb'])) { 79 global $wpdb; 80 if (!isset($wpdb->options)) { 81 return; 82 } 83 $opt_key_val = '_transient_' . $key; 84 $opt_key_to = '_transient_timeout_' . $key; 85 $wpdb->suppress_errors(true); 86 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_val)); 87 if ($autoload === 'yes') { 88 $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", $opt_key_val)); 89 } 90 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to)); 91 if ($autoload_to === 'yes') { 92 $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET autoload='no' WHERE option_name=%s AND autoload='yes' LIMIT 1", $opt_key_to)); 93 } 94 $wpdb->suppress_errors(false); 95 } 69 96 } 70 97 71 98 function dfehc_enqueue_scripts(): void 72 99 { 73 wp_enqueue_script('heartbeat', plugin_dir_url(__FILE__) . 'js/heartbeat.min.js', ['jquery'], '1.5.2', true); 74 if (function_exists('wp_script_add_data')) { 75 wp_script_add_data('heartbeat', 'async', true); 76 } 77 $load_average = dfehc_get_system_load_average(); 78 $recommended = dfehc_calculate_recommended_interval_user_activity($load_average, DFEHC_BATCH_SIZE); 79 wp_localize_script('heartbeat', 'dfehc_heartbeat_vars', [ 80 'recommendedInterval' => $recommended, 100 wp_register_script('dfehc-heartbeat', plugin_dir_url(__FILE__) . 'js/heartbeat.min.js', ['heartbeat'], '1.6.0', true); 101 wp_enqueue_script('dfehc-heartbeat'); 102 103 $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : null; 104 if ($load === false || $load === null) { 105 $load = (float) DFEHC_MAX_SERVER_LOAD; 106 } 107 108 $recommended = function_exists('dfehc_calculate_recommended_interval_user_activity') 109 ? dfehc_calculate_recommended_interval_user_activity((float) $load, DFEHC_BATCH_SIZE) 110 : 60.0; 111 112 $host = wp_parse_url(home_url(), PHP_URL_HOST) ?: 'site'; 113 $ver = defined('DFEHC_VERSION') ? (string) DFEHC_VERSION : (string) filemtime(__FILE__); 114 115 wp_localize_script('dfehc-heartbeat', 'dfehc_heartbeat_vars', [ 116 'recommendedInterval' => (float) $recommended, 81 117 'heartbeat_control_enabled' => get_option('dfehc_heartbeat_control_enabled', '1'), 82 'cache_duration' => 5 * 60 * 1000,83 118 'nonce' => wp_create_nonce(DFEHC_NONCE_ACTION), 119 'ver' => $ver, 120 'cache_bypass_rate' => 0.05, 84 121 ]); 85 122 } … … 88 125 function dfehc_get_user_activity_summary(int $batch_size = DFEHC_BATCH_SIZE): array 89 126 { 90 $cached = get_transient('dfehc_user_activity_summary'); 91 if ($cached !== false) { 127 $key = dfehc_scoped_tkey('dfehc_user_activity_summary'); 128 $cached = get_transient($key); 129 if ($cached !== false && is_array($cached)) { 92 130 return $cached; 93 131 } 94 132 $summary = dfehc_gather_user_activity_data($batch_size); 95 dfehc_set_transient_noautoload( 'dfehc_user_activity_summary', $summary,DFEHC_USER_ACTIVITY_TTL);133 dfehc_set_transient_noautoload($key, $summary, (int) DFEHC_USER_ACTIVITY_TTL); 96 134 return $summary; 97 135 } … … 99 137 function dfehc_calculate_recommended_interval_user_activity(?float $load_average = null, int $batch_size = DFEHC_BATCH_SIZE): float 100 138 { 101 if (!function_exists('sys_getloadavg')) {102 return 60.0;103 }104 139 if ($load_average === null) { 105 $load_average = dfehc_get_system_load_average(); 140 $load_average = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : null; 141 } 142 if ($load_average === false || $load_average === null) { 143 $load_average = (float) DFEHC_MAX_SERVER_LOAD; 106 144 } 107 145 $user_data = dfehc_get_user_activity_summary($batch_size); 108 if ( $user_data['total_weight'] === 0) {146 if (empty($user_data['total_weight'])) { 109 147 return (float) DFEHC_MIN_INTERVAL; 110 148 } 111 $avg_duration = $user_data['total_duration'] / max(1,$user_data['total_weight']);112 return dfehc_calculate_interval_based_on_duration($avg_duration,$load_average);149 $avg_duration = (float) $user_data['total_duration'] / max(1, (int) $user_data['total_weight']); 150 return (float) dfehc_calculate_interval_based_on_duration($avg_duration, (float) $load_average); 113 151 } 114 152 … … 125 163 foreach ($userBatch as $user) { 126 164 $activity = get_user_meta($user->ID, 'dfehc_user_activity', true); 127 if (empty($activity['durations']) ) {165 if (empty($activity['durations']) || !is_array($activity['durations'])) { 128 166 continue; 129 167 } 130 $weight = count($activity['durations']); 131 $avg = array_sum($activity['durations']) / $weight; 132 $total_weighted_duration += $weight * $avg; 168 $weight = count($activity['durations']); 169 if ($weight <= 0) { 170 continue; 171 } 172 $avg = array_sum($activity['durations']) / $weight; 173 $total_weighted_duration += $weight * (float) $avg; 133 174 $total_weight += $weight; 134 175 } … … 144 185 } 145 186 if (!class_exists('Heartbeat_Async')) { 146 return get_transient('dfehc_recommended_interval'); 187 $fallback = get_transient(dfehc_scoped_tkey('dfehc_recommended_interval')); 188 return $fallback !== false ? (float) $fallback : (float) DFEHC_MIN_INTERVAL; 147 189 } 148 190 if (!class_exists('Dfehc_Get_Recommended_Heartbeat_Interval_Async')) { … … 150 192 { 151 193 protected string $action = 'dfehc_get_recommended_interval_async'; 194 public function dispatch(): void 195 { 196 if (has_action($this->action)) { 197 do_action($this->action); 198 } else { 199 if (!wp_next_scheduled($this->action)) { 200 wp_schedule_single_event(time() + 1, $this->action); 201 } 202 } 203 } 152 204 protected function run_action(): void 153 205 { 154 $lock = dfehc_acquire_lock('dfehc_interval_calculation_lock', DFEHC_LOCK_TTL);155 if (!$lock ) {206 $lock = function_exists('dfehc_acquire_lock') ? dfehc_acquire_lock('dfehc_interval_calculation_lock', (int) DFEHC_LOCK_TTL) : null; 207 if (!$lock && function_exists('dfehc_acquire_lock')) { 156 208 return; 157 209 } 158 $last_activity = (int) get_transient('dfehc_last_user_activity'); 159 $elapsed = time() - $last_activity; 160 $load = dfehc_get_system_load_average(); 161 $interval = dfehc_calculate_recommended_interval($elapsed, $load, 0); 162 dfehc_set_transient_noautoload('dfehc_recommended_interval', $interval, 5 * MINUTE_IN_SECONDS); 163 dfehc_release_lock($lock); 210 $last_key = dfehc_scoped_tkey('dfehc_last_user_activity'); 211 $ri_key = dfehc_scoped_tkey('dfehc_recommended_interval'); 212 $last_activity = (int) get_transient($last_key); 213 $elapsed = max(0, time() - $last_activity); 214 $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : null; 215 if ($load === false || $load === null) { 216 $load = (float) DFEHC_MAX_SERVER_LOAD; 217 } 218 $interval = (float) dfehc_calculate_recommended_interval((float) $elapsed, (float) $load, 0.0); 219 if ($interval <= 0) { 220 $interval = (float) DFEHC_MIN_INTERVAL; 221 } 222 dfehc_set_transient_noautoload($ri_key, $interval, 5 * MINUTE_IN_SECONDS); 223 if ($lock && function_exists('dfehc_release_lock')) { 224 dfehc_release_lock($lock); 225 } 164 226 } 165 227 } 166 228 } 167 $current = dfehc_get_website_visitors(); 168 $prev = get_transient('dfehc_previous_visitor_count'); 229 230 $vis_key = dfehc_scoped_tkey('dfehc_previous_visitor_count'); 231 $ri_key = dfehc_scoped_tkey('dfehc_recommended_interval'); 232 233 $current = function_exists('dfehc_get_website_visitors') ? (int) dfehc_get_website_visitors() : 0; 234 $prev = get_transient($vis_key); 169 235 $ratio = (float) apply_filters('dfehc_visitors_delta_ratio', 0.2); 170 if ($prev === false || abs($current - $prev) > $current * $ratio) { 171 delete_transient('dfehc_recommended_interval'); 172 dfehc_set_transient_noautoload('dfehc_previous_visitor_count', $current, 5 * MINUTE_IN_SECONDS); 173 } 174 $cached = get_transient('dfehc_recommended_interval'); 175 if ($cached !== false) { 236 if ($prev === false || ($current > 0 && abs($current - (int) $prev) > $current * $ratio)) { 237 delete_transient($ri_key); 238 dfehc_set_transient_noautoload($vis_key, $current, 5 * MINUTE_IN_SECONDS); 239 } 240 241 $cached = get_transient($ri_key); 242 if ($cached !== false && is_numeric($cached)) { 176 243 return (float) $cached; 177 244 } 178 $lock = dfehc_acquire_lock('dfehc_interval_calculation_lock', DFEHC_LOCK_TTL); 179 if ($lock) { 245 246 $lock = function_exists('dfehc_acquire_lock') ? dfehc_acquire_lock('dfehc_interval_calculation_lock', (int) DFEHC_LOCK_TTL) : null; 247 if ($lock || !function_exists('dfehc_acquire_lock')) { 180 248 (new Dfehc_Get_Recommended_Heartbeat_Interval_Async())->dispatch(); 181 dfehc_release_lock($lock); 182 } 183 return get_transient('dfehc_recommended_interval'); 249 if ($lock && function_exists('dfehc_release_lock')) { 250 dfehc_release_lock($lock); 251 } 252 } 253 254 $val = get_transient($ri_key); 255 if ($val === false || !is_numeric($val)) { 256 return (float) dfehc_calculate_recommended_interval_user_activity(); 257 } 258 return (float) $val; 184 259 } 185 260 … … 187 262 { 188 263 check_ajax_referer(DFEHC_NONCE_ACTION, 'nonce'); 264 $ip = (string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'); 265 $rlk = dfehc_scoped_tkey('dfehc_rl_' . md5($ip)); 266 $cnt = (int) get_transient($rlk); 267 $limit = (int) apply_filters('dfehc_recommend_rl_limit', 30); 268 $window = (int) apply_filters('dfehc_recommend_rl_window', 60); 269 if ($cnt >= $limit) { 270 wp_send_json_error(['message' => 'rate_limited'], 429); 271 } 272 dfehc_set_transient_noautoload($rlk, $cnt + 1, $window); 189 273 $interval = dfehc_get_recommended_heartbeat_interval_async(); 190 wp_send_json_success(['interval' => $interval]); 274 if (!is_numeric($interval) || $interval <= 0) { 275 $interval = (float) dfehc_calculate_recommended_interval_user_activity(); 276 } 277 wp_send_json_success(['interval' => (float) $interval]); 191 278 } 192 279 add_action('wp_ajax_dfehc_update_heartbeat_interval', 'dfehc_get_recommended_intervals'); … … 195 282 function dfehc_override_heartbeat_interval(array $settings): array 196 283 { 197 $interval = (int) ($settings['interval'] ?? DFEHC_MIN_INTERVAL);198 $interval = min(max($interval, DFEHC_MIN_INTERVAL),DFEHC_MAX_INTERVAL);284 $interval = (int) ($settings['interval'] ?? DFEHC_MIN_INTERVAL); 285 $interval = min(max($interval, (int) DFEHC_MIN_INTERVAL), (int) DFEHC_MAX_INTERVAL); 199 286 $settings['interval'] = $interval; 200 287 return $settings; … … 202 289 add_filter('heartbeat_settings', 'dfehc_override_heartbeat_interval'); 203 290 204 add_filter('dfehc_safe_transient_get', static function ($value, $key) {205 if ($value === false) {206 dfehc_set_transient_noautoload("dfehc_retry_{$key}", true, DFEHC_LOCK_TTL);207 }208 return $value;209 }, 10, 2);210 211 291 function dfehc_get_server_health_status(float $load): string 212 292 { … … 228 308 function dfehc_invalidate_heartbeat_cache(): void 229 309 { 230 $visitors = dfehc_get_website_visitors();310 $visitors = function_exists('dfehc_get_website_visitors') ? (int) dfehc_get_website_visitors() : 0; 231 311 if ($visitors > 100) { 232 delete_transient( 'dfehc_recommended_interval');312 delete_transient(dfehc_scoped_tkey('dfehc_recommended_interval')); 233 313 } 234 314 } -
dynamic-front-end-heartbeat-control/trunk/js/heartbeat.js
r3320647 r3396626 3 3 low: [15, 30, 60, 120, 180, 240, 300], 4 4 medium: [30, 60, 120, 180, 240, 300], 5 high: [60, 120, 180, 240, 300], 6 }; 7 8 const cacheTimeout = dfehc_heartbeat_vars.cache_duration || 5 * 60 * 1000; 9 const memoryCache = Object.create(null); 10 const localCacheKey = 'dfehc_heartbeat_server_load'; 5 high: [60, 120, 180, 240, 300] 6 }; 7 8 const vars = typeof window.dfehc_heartbeat_vars === 'object' ? window.dfehc_heartbeat_vars : {}; 9 10 const clampNumber = (v, lo, hi, d) => { 11 const n = Number(v); 12 if (!Number.isFinite(n)) return d; 13 return Math.min(hi, Math.max(lo, n)); 14 }; 15 16 const DEFAULT_MIN = 15; 17 const DEFAULT_MAX = 300; 18 const ABS_MIN = 1; 19 const ABS_MAX = 3600; 20 21 let MIN = clampNumber(vars.min_interval, ABS_MIN, ABS_MAX, DEFAULT_MIN); 22 let MAX = clampNumber(vars.max_interval, MIN, ABS_MAX, DEFAULT_MAX); 23 if (MIN > MAX) { const t = MIN; MIN = MAX; MAX = t; } 24 25 const LOAD_CAP = Number.isFinite(vars.max_server_load) ? Number(vars.max_server_load) : 85; 26 27 const msOrAuto = (v, d) => { 28 const n = Number(v); 29 if (!Number.isFinite(n)) return d; 30 return n <= 600 ? n * 1000 : n; 31 }; 32 const cacheTimeout = msOrAuto(vars.cache_duration, 5 * 60 * 1000); 33 const cacheBypassRate = Math.min(1, Math.max(0, Number(vars.cache_bypass_rate) || 0.05)); 34 35 const sanitizeKey = (s) => String(s || '').replace(/[^a-z0-9_.-]/gi, '_'); 36 const siteKey = sanitizeKey(vars.site_key || location.host); 37 const localCacheKey = `dfehc_heartbeat_server_load:${siteKey}:${vars.ver || '1'}`; 38 const memoryCache = Object.create(null); 39 40 const SUPPORTS_BC = typeof window.BroadcastChannel === 'function'; 41 const bc = SUPPORTS_BC ? new BroadcastChannel('dfehc-heartbeat') : null; 42 const TAB_ID = Math.random().toString(36).slice(2) + Date.now().toString(36); 43 let isLeader = false; 44 let lastLeaderBeat = 0; 45 const LEADER_TTL = 5000; 46 const leaderKey = `${localCacheKey}:leader`; 47 48 function readLeader() { 49 try { 50 const raw = localStorage.getItem(leaderKey); 51 if (!raw) return null; 52 const obj = JSON.parse(raw); 53 if (!obj || typeof obj !== 'object') return null; 54 const ts = Number(obj.ts); 55 const tab = String(obj.tab || ''); 56 if (!Number.isFinite(ts) || !tab) return null; 57 return { ts, tab }; 58 } catch { 59 return null; 60 } 61 } 62 63 function writeLeader(ts, tab) { 64 try { localStorage.setItem(leaderKey, JSON.stringify({ ts, tab })); } catch {} 65 } 66 67 function clearLeader(tab) { 68 try { 69 const cur = readLeader(); 70 if (!cur || cur.tab === tab) localStorage.removeItem(leaderKey); 71 } catch {} 72 } 73 74 function tryBecomeLeader() { 75 if (document.hidden) return false; 76 const now = Date.now(); 77 const cur = readLeader(); 78 if (!cur || now - cur.ts > LEADER_TTL) { 79 writeLeader(now, TAB_ID); 80 isLeader = true; 81 lastLeaderBeat = now; 82 if (bc) bc.postMessage({ t: 'leader', ts: now, tab: TAB_ID }); 83 return true; 84 } 85 if (cur.tab === TAB_ID) { 86 isLeader = true; 87 lastLeaderBeat = cur.ts; 88 return true; 89 } 90 return false; 91 } 92 93 function renewLeadership() { 94 if (!isLeader) return; 95 const now = Date.now(); 96 if (now - lastLeaderBeat > 2000) { 97 writeLeader(now, TAB_ID); 98 lastLeaderBeat = now; 99 } 100 } 101 102 function relinquishLeadership() { 103 if (!isLeader) return; 104 clearLeader(TAB_ID); 105 isLeader = false; 106 } 11 107 12 108 const getLocalCache = (key) => { 13 109 try { 14 const raw = localStorage.getItem(key);110 const raw = window.localStorage.getItem(key); 15 111 if (!raw) return null; 16 const { timestamp, data } = JSON.parse(raw); 112 const parsed = JSON.parse(raw); 113 if (!parsed || typeof parsed !== 'object') return null; 114 const timestamp = Number(parsed.timestamp); 115 const data = parsed.data; 116 if (!Number.isFinite(timestamp)) return null; 17 117 return Date.now() - timestamp < cacheTimeout ? data : null; 18 118 } catch { 19 localStorage.removeItem(key);119 try { window.localStorage.removeItem(key); } catch {} 20 120 return null; 21 121 } … … 24 124 const setLocalCache = (key, data) => { 25 125 try { 26 localStorage.setItem(key, JSON.stringify({ timestamp: Date.now(), data })); 126 const jitterMs = Math.floor(Math.random() * 5000); 127 window.localStorage.setItem(key, JSON.stringify({ timestamp: Date.now() - jitterMs, data })); 128 } catch {} 129 }; 130 131 const nearestFrom = (value, list) => 132 list.reduce((best, v) => (Math.abs(v - value) < Math.abs(best - value) ? v : best), list[0]); 133 134 const trafficLevel = (load) => (load <= 50 ? 'low' : load <= 75 ? 'medium' : 'high'); 135 136 const calcRecommendedIntervalFromLoad = (load) => { 137 const bucket = Math.max(0, Math.min(100, Math.round(load))); 138 if (Object.prototype.hasOwnProperty.call(memoryCache, bucket)) return memoryCache[bucket]; 139 const level = trafficLevel(load); 140 const opts = intervals[level]; 141 const min = Math.max(MIN, opts[0]); 142 const max = Math.min(MAX, opts[opts.length - 1]); 143 const ratio = Math.max(0, Math.min(1, load / LOAD_CAP)); 144 const raw = min + (max - min) * ratio; 145 const clamped = Math.min(Math.max(raw, MIN), MAX); 146 const snapped = nearestFrom(clamped, opts); 147 memoryCache[bucket] = snapped; 148 return snapped; 149 }; 150 151 const toNum = (v) => { 152 const n = Number(v); 153 return Number.isFinite(n) ? n : null; 154 }; 155 156 const coerceServerPayload = (val) => { 157 if (val && typeof val === 'object') { 158 const interval = toNum(val.interval); 159 const load = toNum(val.load); 160 return { interval, load }; 161 } 162 if (Number.isFinite(Number(val))) { 163 const n = Number(val); 164 const mode = (vars.server_payload || 'auto').toLowerCase(); 165 if (mode === 'interval') return { interval: n, load: null }; 166 if (mode === 'load') return { interval: null, load: n }; 167 if (n >= 0 && n <= 100) return { interval: null, load: n }; 168 if (n > 100) return { interval: n, load: null }; 169 } 170 return { interval: null, load: null }; 171 }; 172 173 const debounce = (fn, wait) => { 174 let t; 175 return (...args) => { 176 clearTimeout(t); 177 t = setTimeout(() => fn(...args), wait); 178 }; 179 }; 180 181 let lastInterval = null; 182 const applyPresetThenExact = (secs) => { 183 const hb = wp && wp.heartbeat; 184 if (!hb || typeof hb.interval !== 'function') return; 185 if (typeof secs !== 'number' || !Number.isFinite(secs)) return; 186 if (lastInterval !== null && Math.round(lastInterval) === Math.round(secs)) return; 187 const preset = secs <= 20 ? 'fast' : secs <= 60 ? 'standard' : 'slow'; 188 try { hb.interval(preset); hb.interval(secs); } catch {} 189 lastInterval = secs; 190 }; 191 192 const setHeartbeatInterval = (secs) => { 193 let s = secs; 194 if (s >= 60) { 195 const jitter = Math.floor(Math.random() * 3) - 1; 196 s = Math.max(MIN, Math.min(MAX, s + jitter)); 197 } 198 applyPresetThenExact(s); 199 }; 200 201 const debouncedSetInterval = debounce(setHeartbeatInterval, 150); 202 203 let failureCount = 0; 204 let nextRetryAt = 0; 205 const backoffMs = () => Math.min(120000, 2000 * Math.pow(2, Math.min(failureCount, 5))); 206 207 const fetchWithTimeout = (url, opts, ms) => { 208 if ('AbortController' in window) { 209 const ctrl = new AbortController(); 210 const t = setTimeout(() => ctrl.abort(), ms); 211 return fetch(url, { ...opts, signal: ctrl.signal }).finally(() => clearTimeout(t)); 212 } 213 return Promise.race([ 214 fetch(url, opts), 215 new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms)) 216 ]); 217 }; 218 219 let reqSeq = 0; 220 221 const fetchServerData = async (nonce, rid) => { 222 if (Date.now() < nextRetryAt) { 223 const base = Number(vars.fallback_interval) || 60; 224 const jitter = Math.floor(Math.random() * 7) - 3; 225 return { interval: Math.max(MIN, Math.min(MAX, base + jitter)), load: null, rid }; 226 } 227 228 if (Math.random() >= cacheBypassRate) { 229 const cached = getLocalCache(localCacheKey); 230 if (cached !== null) return { ...coerceServerPayload(cached), rid }; 231 } 232 233 const I_AM_LEADER = tryBecomeLeader(); 234 if (!I_AM_LEADER) { 235 const wait = Math.min(1500, Math.max(150, Math.floor(Math.random() * 600))); 236 await new Promise(r => setTimeout(r, wait)); 237 const cached = getLocalCache(localCacheKey); 238 if (cached !== null) return { ...coerceServerPayload(cached), rid }; 239 } 240 241 if (navigator.onLine === false) { 242 const base = Number(vars.fallback_interval) || 60; 243 const jitter = Math.floor(Math.random() * 7) - 3; 244 return { interval: Math.max(MIN, Math.min(MAX, base + jitter)), load: null, rid }; 245 } 246 247 const url = 248 vars.ajax_url || 249 (typeof window.ajaxurl !== 'undefined' ? window.ajaxurl : `${location.origin.replace(/\/$/, '')}/wp-admin/admin-ajax.php`); 250 251 try { 252 const body = new URLSearchParams({ action: 'get_server_load' }); 253 if (nonce) body.append('nonce', nonce); 254 const res = await fetchWithTimeout(url, { 255 method: 'POST', 256 body, 257 credentials: 'same-origin', 258 headers: { 259 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 260 'Cache-Control': 'no-store' 261 } 262 }, 6000); 263 if (!res.ok) throw new Error(String(res.status)); 264 const json = await res.json(); 265 if (!json || json.success !== true) throw new Error('bad payload'); 266 setLocalCache(localCacheKey, json.data); 267 if (bc) bc.postMessage({ t: 'payload', data: json.data }); 268 failureCount = 0; 269 nextRetryAt = 0; 270 renewLeadership(); 271 return { ...coerceServerPayload(json.data), rid }; 27 272 } catch { 28 } 29 }; 30 31 const fetchServerLoad = async (nonce) => { 32 const cached = getLocalCache(localCacheKey); 33 if (cached !== null) return cached; 34 35 try { 36 const body = new URLSearchParams({ action: 'get_server_load', nonce }); 37 const res = await fetch(ajaxurl, { 38 method: 'POST', 39 body, 40 credentials: 'same-origin', 41 }); 42 43 if (!res.ok) throw new Error(`HTTP ${res.status}`); 44 const json = await res.json(); 45 if (!json.success || typeof json.data !== 'number') throw new Error('Bad payload'); 46 47 setLocalCache(localCacheKey, json.data); 48 return json.data; 49 } catch (err) { 50 console.error('DFEHC fetch error:', err.message); 51 return 60; 52 } 53 }; 54 55 const smoothMoving = (arr, windowSize = 5) => { 56 const slice = arr.slice(-windowSize); 57 return slice.reduce((s, v) => s + v, 0) / slice.length; 58 }; 59 60 const weightedAverage = (arr) => { 61 const len = arr.length; 62 const denom = (len * (len + 1)) / 2; 63 return arr.reduce((sum, v, i) => sum + v * (i + 1), 0) / denom; 64 }; 65 66 const trafficLevel = (load) => 67 load <= 50 ? 'low' : load <= 75 ? 'medium' : 'high'; 68 69 const calcRecommendedInterval = (load) => { 70 const bucket = Math.round(load / 5) * 5; 71 if (bucket in memoryCache) return memoryCache[bucket]; 72 73 const level = trafficLevel(load); 74 const recent = intervals[level]; 75 const avg = weightedAverage(recent); 76 const smooth = smoothMoving(recent); 77 const loadFactor = 1 - Math.min(load / 85, 1); 78 const rawInterval = Math.round(avg + smooth * loadFactor); 79 const clampedInterval = Math.min(Math.max(rawInterval, 30), 300); 80 81 memoryCache[bucket] = clampedInterval; 82 return clampedInterval; 273 failureCount += 1; 274 nextRetryAt = Date.now() + backoffMs(); 275 const base = Number(vars.fallback_interval) || 60; 276 const jitter = Math.floor(Math.random() * 7) - 3; 277 relinquishLeadership(); 278 return { interval: Math.max(MIN, Math.min(MAX, base + jitter)), load: null, rid }; 279 } 83 280 }; 84 281 85 282 const heartbeat = { 86 update: (interval) => { 87 if (wp && wp.heartbeat && typeof wp.heartbeat.interval === 'function') { 88 wp.heartbeat.interval(interval); 283 update(interval) { debouncedSetInterval(interval); }, 284 updateUI(interval) { 285 const sel = document.querySelector('#dfehc-heartbeat-interval'); 286 if (sel) sel.value = String(interval); 287 debouncedSetInterval(interval); 288 }, 289 async init(nonce) { 290 const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; 291 const et = conn && conn.effectiveType ? String(conn.effectiveType) : ''; 292 if ('deviceMemory' in navigator && navigator.deviceMemory < 2) return; 293 if ((conn && conn.saveData) || et.startsWith('2g') || et.startsWith('slow-2g')) return; 294 const myRid = ++reqSeq; 295 const payload = await fetchServerData(nonce, myRid); 296 if (payload.rid !== reqSeq) return; 297 const interval = payload.interval; 298 const load = payload.load; 299 const snapList = 300 intervals[trafficLevel(typeof load === 'number' ? load : LOAD_CAP)] || intervals.low; 301 const finalInterval = 302 typeof interval === 'number' 303 ? nearestFrom(Math.min(Math.max(interval, MIN), MAX), snapList) 304 : calcRecommendedIntervalFromLoad(typeof load === 'number' ? load : 60); 305 this.updateUI(finalInterval); 306 } 307 }; 308 309 if (bc) { 310 bc.onmessage = (e) => { 311 const m = e.data || {}; 312 if (m.t === 'leader') { 313 const tab = String(m.tab || ''); 314 if (tab && tab !== TAB_ID) isLeader = false; 89 315 } 90 }, 91 updateUI: (interval) => { 92 const sel = document.querySelector('#dfehc-heartbeat-interval'); 93 if (sel) sel.value = interval; 94 this.update(interval); 95 }, 96 init: async function (nonce) { 97 if ('deviceMemory' in navigator && navigator.deviceMemory < 2) return; 98 if (navigator.connection && navigator.connection.effectiveType === '2g') return; 99 100 const load = await fetchServerLoad(nonce); 101 const recommended = calcRecommendedInterval(load); 102 this.updateUI(recommended); 103 }, 104 }; 316 if (m.t === 'payload') { 317 try { setLocalCache(localCacheKey, m.data); } catch {} 318 Object.keys(memoryCache).forEach(k => delete memoryCache[k]); 319 const coerced = coerceServerPayload(m.data); 320 const interval = coerced.interval; 321 const load = coerced.load; 322 const snapList = 323 intervals[trafficLevel(typeof load === 'number' ? load : LOAD_CAP)] || intervals.low; 324 const finalInterval = 325 typeof interval === 'number' 326 ? nearestFrom(Math.min(Math.max(interval, MIN), MAX), snapList) 327 : calcRecommendedIntervalFromLoad(typeof load === 'number' ? load : 60); 328 debouncedSetInterval(finalInterval); 329 } 330 }; 331 } 332 333 window.addEventListener('storage', (e) => { 334 if (e.key !== localCacheKey || !e.newValue) return; 335 try { 336 const v = JSON.parse(e.newValue); 337 if (!v || typeof v !== 'object' || !('data' in v)) return; 338 Object.keys(memoryCache).forEach(k => delete memoryCache[k]); 339 const coerced = coerceServerPayload(v.data); 340 const interval = coerced.interval; 341 const load = coerced.load; 342 const snapList = 343 intervals[trafficLevel(typeof load === 'number' ? load : LOAD_CAP)] || intervals.low; 344 const finalInterval = 345 typeof interval === 'number' 346 ? nearestFrom(Math.min(Math.max(interval, MIN), MAX), snapList) 347 : calcRecommendedIntervalFromLoad(typeof load === 'number' ? load : 60); 348 debouncedSetInterval(finalInterval); 349 } catch {} 350 }); 351 352 let memoVersion = String(vars.ver || '1'); 353 function maybeResetMemo() { 354 const v = String(vars.ver || '1'); 355 if (v !== memoVersion) { 356 memoVersion = v; 357 Object.keys(memoryCache).forEach(k => delete memoryCache[k]); 358 } 359 } 360 361 function cleanup() { 362 relinquishLeadership(); 363 if (bc && typeof bc.close === 'function') { 364 try { bc.close(); } catch {} 365 } 366 } 367 368 document.addEventListener('visibilitychange', () => { 369 if (!document.hidden) nextRetryAt = 0; 370 }); 371 window.addEventListener('online', () => { nextRetryAt = 0; }); 372 window.addEventListener('pageshow', (e) => { if (e.persisted) nextRetryAt = 0; }); 373 window.addEventListener('pagehide', cleanup, { once: true }); 374 window.addEventListener('beforeunload', cleanup, { once: true }); 105 375 106 376 document.addEventListener('DOMContentLoaded', () => { 107 if (dfehc_heartbeat_vars.heartbeat_control_enabled !== '1') return; 108 109 const { nonce } = dfehc_heartbeat_vars; 110 377 maybeResetMemo(); 378 if ((vars.heartbeat_control_enabled || '') !== '1') return; 379 const nonce = vars.nonce; 111 380 if ('requestIdleCallback' in window) { 112 requestIdleCallback(() => heartbeat.init(nonce));381 window.requestIdleCallback(() => heartbeat.init(nonce)); 113 382 } else { 114 383 setTimeout(() => heartbeat.init(nonce), 100); 115 384 } 116 117 385 const sel = document.querySelector('#dfehc-heartbeat-interval'); 118 386 if (sel) { 119 387 sel.addEventListener('change', function () { 120 388 const val = parseInt(this.value, 10); 121 if (! isNaN(val)) heartbeat.update(val);389 if (!Number.isNaN(val)) heartbeat.update(Math.min(Math.max(val, MIN), MAX)); 122 390 }); 123 391 } -
dynamic-front-end-heartbeat-control/trunk/js/heartbeat.min.js
r3320647 r3396626 1 ((wp)=>{const i ={low:[15,30,60,120,180,240,300],medium:[30,60,120,180,240,300],high:[60,120,180,240,300]},c=dfehc_heartbeat_vars.cache_duration||3e5,l=Object.create(null),d="dfehc_heartbeat_server_load",a=e=>{try{const{timestamp:t,data:n}=JSON.parse(localStorage.getItem(e));return Date.now()-t<c?n:null}catch{return localStorage.removeItem(e),null}},s=(e,t)=>{try{localStorage.setItem(e,JSON.stringify({timestamp:Date.now(),data:t}))}catch{}},u=async e=>{const t=a(d);if(t!==null)return t;try{const t=new URLSearchParams({action:"get_server_load",nonce:e}),n=await fetch(ajaxurl,{method:"POST",body:t,credentials:"same-origin"});if(!n.ok)throw new Error(`HTTP ${n.status}`);const r=await n.json();if(!r.success||typeof r.data!=="number")throw new Error("Bad payload");return s(d,r.data),r.data}catch(e){return console.error("DFEHC fetch error:",e.message),60}},f=(e,t=5)=>{const n=e.slice(-t);return n.reduce((e,t)=>e+t,0)/n.length},p=e=>{const t=e.length;return e.reduce((e,n,r)=>e+n*(r+1),0)/(t*(t+1)/2)},m=e=>e<=50?"low":e<=75?"medium":"high",h=e=>{const t=Math.round(e/5)*5;if(t in l)return l[t];const n=m(e),r=i[n],o=p(r),g=f(r),a=1-Math.min(e/85,1),c_=Math.round(o+g*a),s_=Math.min(Math.max(c_,30),300);return l[t]=s_,s_},g={update:e=>{wp&&wp.heartbeat&&typeof wp.heartbeat.interval=="function"&&wp.heartbeat.interval(e)},updateUI(e){const t=document.querySelector("#dfehc-heartbeat-interval");t&&(t.value=e),this.update(e)},async init(e){if("deviceMemory"in navigator&&navigator.deviceMemory<2)return;if(navigator.connection&&navigator.connection.effectiveType==="2g")return;const t=await u(e),n=h(t);this.updateUI(n)}};document.addEventListener("DOMContentLoaded",()=>{if(dfehc_heartbeat_vars.heartbeat_control_enabled!=="1")return;const{nonce:e}=dfehc_heartbeat_vars;"requestIdleCallback"in window?requestIdleCallback(()=>g.init(e)):setTimeout(()=>g.init(e),100);const t=document.querySelector("#dfehc-heartbeat-interval");t&&t.addEventListener("change",function(){const e=parseInt(this.value,10);isNaN(e)||g.update(e)})})})(window.wp||{});1 ((wp)=>{const intervals={low:[15,30,60,120,180,240,300],medium:[30,60,120,180,240,300],high:[60,120,180,240,300]};const vars=typeof window.dfehc_heartbeat_vars==="object"?window.dfehc_heartbeat_vars:{};const clampNumber=(v,lo,hi,d)=>{const n=Number(v);if(!Number.isFinite(n))return d;return Math.min(hi,Math.max(lo,n))};const DEFAULT_MIN=15;const DEFAULT_MAX=300;const ABS_MIN=1;const ABS_MAX=3600;let MIN=clampNumber(vars.min_interval,ABS_MIN,ABS_MAX,DEFAULT_MIN);let MAX=clampNumber(vars.max_interval,MIN,ABS_MAX,DEFAULT_MAX);if(MIN>MAX){const t=MIN;MIN=MAX;MAX=t}const LOAD_CAP=Number.isFinite(vars.max_server_load)?Number(vars.max_server_load):85;const msOrAuto=(v,d)=>{const n=Number(v);if(!Number.isFinite(n))return d;return n<=600?n*1e3:n};const cacheTimeout=msOrAuto(vars.cache_duration,3e5);const cacheBypassRate=Math.min(1,Math.max(0,Number(vars.cache_bypass_rate)||.05));const sanitizeKey=(s)=>String(s||"").replace(/[^a-z0-9_.-]/gi,"_");const siteKey=sanitizeKey(vars.site_key||location.host);const localCacheKey=`dfehc_heartbeat_server_load:${siteKey}:${vars.ver||"1"}`;const memoryCache=Object.create(null);const SUPPORTS_BC=typeof window.BroadcastChannel==="function";const bc=SUPPORTS_BC?new BroadcastChannel("dfehc-heartbeat"):null;const TAB_ID=Math.random().toString(36).slice(2)+Date.now().toString(36);let isLeader=false;let lastLeaderBeat=0;const LEADER_TTL=5e3;const leaderKey=`${localCacheKey}:leader`;function readLeader(){try{const raw=localStorage.getItem(leaderKey);if(!raw)return null;const obj=JSON.parse(raw);if(!obj||typeof obj!=="object")return null;const ts=Number(obj.ts);const tab=String(obj.tab||"");if(!Number.isFinite(ts)||!tab)return null;return{ts,tab}}catch{return null}}function writeLeader(ts,tab){try{localStorage.setItem(leaderKey,JSON.stringify({ts,tab}))}catch{}}function clearLeader(tab){try{const cur=readLeader();if(!cur||cur.tab===tab)localStorage.removeItem(leaderKey)}catch{}}function tryBecomeLeader(){if(document.hidden)return false;const now=Date.now();const cur=readLeader();if(!cur||now-cur.ts>LEADER_TTL){writeLeader(now,TAB_ID);isLeader=true;lastLeaderBeat=now;if(bc)bc.postMessage({t:"leader",ts:now,tab:TAB_ID});return true}if(cur.tab===TAB_ID){isLeader=true;lastLeaderBeat=cur.ts;return true}return false}function renewLeadership(){if(!isLeader)return;const now=Date.now();if(now-lastLeaderBeat>2e3){writeLeader(now,TAB_ID);lastLeaderBeat=now}}function relinquishLeadership(){if(!isLeader)return;clearLeader(TAB_ID);isLeader=false}const getLocalCache=(key)=>{try{const raw=window.localStorage.getItem(key);if(!raw)return null;const parsed=JSON.parse(raw);if(!parsed||typeof parsed!=="object")return null;const timestamp=Number(parsed.timestamp);const data=parsed.data;if(!Number.isFinite(timestamp))return null;return Date.now()-timestamp<cacheTimeout?data:null}catch{try{window.localStorage.removeItem(key)}catch{}return null}};const setLocalCache=(key,data)=>{try{const jitterMs=Math.floor(Math.random()*5e3);window.localStorage.setItem(key,JSON.stringify({timestamp:Date.now()-jitterMs,data}))}catch{}};const nearestFrom=(value,list)=>list.reduce((best,v)=>Math.abs(v-value)<Math.abs(best-value)?v:best,list[0]);const trafficLevel=(load)=>load<=50?"low":load<=75?"medium":"high";const calcRecommendedIntervalFromLoad=(load)=>{const bucket=Math.max(0,Math.min(100,Math.round(load)));if(Object.prototype.hasOwnProperty.call(memoryCache,bucket))return memoryCache[bucket];const level=trafficLevel(load);const opts=intervals[level];const min=Math.max(MIN,opts[0]);const max=Math.min(MAX,opts[opts.length-1]);const ratio=Math.max(0,Math.min(1,load/LOAD_CAP));const raw=min+(max-min)*ratio;const clamped=Math.min(Math.max(raw,MIN),MAX);const snapped=nearestFrom(clamped,opts);memoryCache[bucket]=snapped;return snapped};const toNum=(v)=>{const n=Number(v);return Number.isFinite(n)?n:null};const coerceServerPayload=(val)=>{if(val&&typeof val==="object"){const interval=toNum(val.interval);const load=toNum(val.load);return{interval,load}}if(Number.isFinite(Number(val))){const n=Number(val);const mode=(vars.server_payload||"auto").toLowerCase();if(mode==="interval")return{interval:n,load:null};if(mode==="load")return{interval:null,load:n};if(n>=0&&n<=100)return{interval:null,load:n};if(n>100)return{interval:n,load:null}}return{interval:null,load:null}};const debounce=(fn,wait)=>{let t;return(...args)=>{clearTimeout(t);t=setTimeout(()=>fn(...args),wait)}};let lastInterval=null;const applyPresetThenExact=(secs)=>{const hb=wp&&wp.heartbeat;if(!hb||typeof hb.interval!=="function")return;if(typeof secs!=="number"||!Number.isFinite(secs))return;if(lastInterval!==null&&Math.round(lastInterval)===Math.round(secs))return;const preset=secs<=20?"fast":secs<=60?"standard":"slow";try{hb.interval(preset);hb.interval(secs)}catch{}lastInterval=secs};const setHeartbeatInterval=(secs)=>{let s=secs;if(s>=60){const jitter=Math.floor(Math.random()*3)-1;s=Math.max(MIN,Math.min(MAX,s+jitter))}applyPresetThenExact(s)};const debouncedSetInterval=debounce(setHeartbeatInterval,150);let failureCount=0;let nextRetryAt=0;const backoffMs=()=>Math.min(12e4,2e3*Math.pow(2,Math.min(failureCount,5)));const fetchWithTimeout=(url,opts,ms)=>{if("AbortController"in window){const ctrl=new AbortController;const t=setTimeout(()=>ctrl.abort(),ms);return fetch(url,{...opts,signal:ctrl.signal}).finally(()=>clearTimeout(t))}return Promise.race([fetch(url,opts),new Promise((_,rej)=>setTimeout(()=>rej(new Error("timeout")),ms))])};let reqSeq=0;const fetchServerData=async(nonce,rid)=>{if(Date.now()<nextRetryAt){const base=Number(vars.fallback_interval)||60;const jitter=Math.floor(Math.random()*7)-3;return{interval:Math.max(MIN,Math.min(MAX,base+jitter)),load:null,rid}}if(Math.random()>=cacheBypassRate){const cached=getLocalCache(localCacheKey);if(cached!==null)return{...coerceServerPayload(cached),rid}}const I_AM_LEADER=tryBecomeLeader();if(!I_AM_LEADER){const wait=Math.min(1500,Math.max(150,Math.floor(Math.random()*600)));await new Promise(r=>setTimeout(r,wait));const cached=getLocalCache(localCacheKey);if(cached!==null)return{...coerceServerPayload(cached),rid}}if(navigator.onLine===false){const base=Number(vars.fallback_interval)||60;const jitter=Math.floor(Math.random()*7)-3;return{interval:Math.max(MIN,Math.min(MAX,base+jitter)),load:null,rid}}const url=vars.ajax_url||typeof window.ajaxurl!=="undefined"?window.ajaxurl:`${location.origin.replace(/\/$/,"")}/wp-admin/admin-ajax.php`;try{const body=new URLSearchParams({action:"get_server_load"});if(nonce)body.append("nonce",nonce);const res=await fetchWithTimeout(url,{method:"POST",body,credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8","Cache-Control":"no-store"}},6e3);if(!res.ok)throw new Error(String(res.status));const json=await res.json();if(!json||json.success!==true)throw new Error("bad payload");setLocalCache(localCacheKey,json.data);if(bc)bc.postMessage({t:"payload",data:json.data});failureCount=0;nextRetryAt=0;renewLeadership();return{...coerceServerPayload(json.data),rid}}catch{failureCount+=1;nextRetryAt=Date.now()+backoffMs();const base=Number(vars.fallback_interval)||60;const jitter=Math.floor(Math.random()*7)-3;relinquishLeadership();return{interval:Math.max(MIN,Math.min(MAX,base+jitter)),load:null,rid}}};const heartbeat={update(interval){debouncedSetInterval(interval)},updateUI(interval){const sel=document.querySelector("#dfehc-heartbeat-interval");if(sel)sel.value=String(interval);debouncedSetInterval(interval)},async init(nonce){const conn=navigator.connection||navigator.mozConnection||navigator.webkitConnection;const et=conn&&conn.effectiveType?String(conn.effectiveType):"";if("deviceMemory"in navigator&&navigator.deviceMemory<2)return;if(conn&&conn.saveData||et.startsWith("2g")||et.startsWith("slow-2g"))return;const myRid=++reqSeq;const payload=await fetchServerData(nonce,myRid);if(payload.rid!==reqSeq)return;const interval=payload.interval;const load=payload.load;const snapList=intervals[trafficLevel(typeof load==="number"?load:LOAD_CAP)]||intervals.low;const finalInterval=typeof interval==="number"?nearestFrom(Math.min(Math.max(interval,MIN),MAX),snapList):calcRecommendedIntervalFromLoad(typeof load==="number"?load:60);this.updateUI(finalInterval)}};if(bc){bc.onmessage=(e)=>{const m=e.data||{};if(m.t==="leader"){const tab=String(m.tab||"");if(tab&&tab!==TAB_ID)isLeader=false}if(m.t==="payload"){try{setLocalCache(localCacheKey,m.data)}catch{}Object.keys(memoryCache).forEach(k=>delete memoryCache[k]);const coerced=coerceServerPayload(m.data);const interval=coerced.interval;const load=coerced.load;const snapList=intervals[trafficLevel(typeof load==="number"?load:LOAD_CAP)]||intervals.low;const finalInterval=typeof interval==="number"?nearestFrom(Math.min(Math.max(interval,MIN),MAX),snapList):calcRecommendedIntervalFromLoad(typeof load==="number"?load:60);debouncedSetInterval(finalInterval)}}}window.addEventListener("storage",(e)=>{if(e.key!==localCacheKey||!e.newValue)return;try{const v=JSON.parse(e.newValue);if(!v||typeof v!=="object"||!("data"in v))return;Object.keys(memoryCache).forEach(k=>delete memoryCache[k]);const coerced=coerceServerPayload(v.data);const interval=coerced.interval;const load=coerced.load;const snapList=intervals[trafficLevel(typeof load==="number"?load:LOAD_CAP)]||intervals.low;const finalInterval=typeof interval==="number"?nearestFrom(Math.min(Math.max(interval,MIN),MAX),snapList):calcRecommendedIntervalFromLoad(typeof load==="number"?load:60);debouncedSetInterval(finalInterval)}catch{}});let memoVersion=String(vars.ver||"1");function maybeResetMemo(){const v=String(vars.ver||"1");if(v!==memoVersion){memoVersion=v;Object.keys(memoryCache).forEach(k=>delete memoryCache[k])}}function cleanup(){relinquishLeadership();if(bc&&typeof bc.close==="function"){try{bc.close()}catch{}}}document.addEventListener("visibilitychange",()=>{if(!document.hidden)nextRetryAt=0});window.addEventListener("online",()=>{nextRetryAt=0});window.addEventListener("pageshow",(e)=>{if(e.persisted)nextRetryAt=0});window.addEventListener("pagehide",cleanup,{once:true});window.addEventListener("beforeunload",cleanup,{once:true});document.addEventListener("DOMContentLoaded",()=>{maybeResetMemo();if((vars.heartbeat_control_enabled||"")!=="1")return;const nonce=vars.nonce;if("requestIdleCallback"in window){window.requestIdleCallback(()=>heartbeat.init(nonce))}else{setTimeout(()=>heartbeat.init(nonce),100)}const sel=document.querySelector("#dfehc-heartbeat-interval");if(sel){sel.addEventListener("change",function(){const val=parseInt(this.value,10);if(!Number.isNaN(val))heartbeat.update(Math.min(Math.max(val,MIN),MAX))})}})})(window.wp||{}); -
dynamic-front-end-heartbeat-control/trunk/readme.txt
r3320647 r3396626 3 3 Tested up to: 6.8 4 4 Requires PHP: 7.2 5 Stable tag: 1.2.99 5 Stable tag: 1.2.995 6 6 License: GPLv2 or later 7 7 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 41 41 42 42 == Changelog == 43 44 = 1.2.995 = 45 46 * Improved multisite support. 47 * Increased resilience and reliability under high-traffic loads and limited server resources. 48 * Enhanced overall compatibility and security. 43 49 44 50 = 1.2.99 = -
dynamic-front-end-heartbeat-control/trunk/visitor/cookie-helper.php
r3320647 r3396626 8 8 return $pattern; 9 9 } 10 $sigs = (array) apply_filters('dfehc_bot_signatures', [11 'bot', 'crawl', 'slurp', 'spider', 'mediapartners','bingpreview',12 'yandex', 'duckduckbot', 'baiduspider', 'sogou','exabot',13 'facebot', 'facebookexternalhit','ia_archiver',10 $sigs = (array) apply_filters('dfehc_bot_signatures', [ 11 'bot','crawl','crawler','slurp','spider','mediapartners','bingpreview', 12 'yandex','duckduckbot','baiduspider','sogou','exabot', 13 'facebot','facebookexternalhit','ia_archiver', 14 14 ]); 15 15 $tokens = array_map( 16 static fn(string $s): string => '(?:^|\\b)' . preg_quote($s, '/'), 16 static fn(string $s): string => 17 '(?<![A-Za-z0-9])' . preg_quote($s, '/') . '(?![A-Za-z0-9])', 17 18 $sigs 18 19 ); … … 20 21 } 21 22 23 if (!function_exists('dfehc_blog_id')) { 24 function dfehc_blog_id(): int 25 { 26 return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0; 27 } 28 } 29 30 if (!function_exists('dfehc_host_token')) { 31 function dfehc_host_token(): string 32 { 33 static $t = ''; 34 if ($t !== '') return $t; 35 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 36 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 37 return $t = substr(md5((string) $host . $salt), 0, 10); 38 } 39 } 40 41 if (!function_exists('dfehc_scoped_key')) { 42 function dfehc_scoped_key(string $base): string 43 { 44 return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token(); 45 } 46 } 47 48 if (!function_exists('dfehc_set_transient_noautoload')) { 49 function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void 50 { 51 if (wp_using_ext_object_cache()) { 52 if (function_exists('wp_cache_add')) { 53 if (!wp_cache_add($key, $value, defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc', $expiration)) { 54 wp_cache_set($key, $value, defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc', $expiration); 55 } 56 } else { 57 wp_cache_set($key, $value, defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc', $expiration); 58 } 59 return; 60 } 61 set_transient($key, $value, $expiration); 62 global $wpdb; 63 if (!isset($wpdb->options)) { 64 return; 65 } 66 $opt_key = "_transient_$key"; 67 $opt_key_to = "_transient_timeout_$key"; 68 $wpdb->suppress_errors(true); 69 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key)); 70 if ($autoload === 'yes') { 71 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 72 } 73 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to)); 74 if ($autoload_to === 'yes') { 75 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 76 } 77 $wpdb->suppress_errors(false); 78 } 79 } 80 81 function dfehc_ip_in_cidr(string $ip, string $cidr): bool 82 { 83 if (strpos($cidr, '/') === false) { 84 return (bool) filter_var($ip, FILTER_VALIDATE_IP) && $ip === $cidr; 85 } 86 [$subnet, $mask] = explode('/', $cidr, 2); 87 $mask = (int) $mask; 88 89 $ip_bin = @inet_pton($ip); 90 $sub_bin = @inet_pton($subnet); 91 if ($ip_bin === false || $sub_bin === false) { 92 return false; 93 } 94 if (strlen($ip_bin) !== strlen($sub_bin)) { 95 return false; 96 } 97 98 $len = strlen($ip_bin); 99 $max_bits = $len * 8; 100 if ($mask < 0 || $mask > $max_bits) { 101 return false; 102 } 103 104 $bytes = intdiv($mask, 8); 105 $bits = $mask % 8; 106 107 if ($bytes && substr($ip_bin, 0, $bytes) !== substr($sub_bin, 0, $bytes)) { 108 return false; 109 } 110 if ($bits === 0) { 111 return true; 112 } 113 $ip_byte = ord($ip_bin[$bytes]); 114 $sub_byte = ord($sub_bin[$bytes]); 115 $mask_byte = (0xFF << (8 - $bits)) & 0xFF; 116 return ($ip_byte & $mask_byte) === ($sub_byte & $mask_byte); 117 } 118 119 function dfehc_trusted_proxy_request(): bool 120 { 121 $remote = $_SERVER['REMOTE_ADDR'] ?? ''; 122 if ($remote === '') return false; 123 $trusted = (array) apply_filters('dfehc_trusted_proxies', []); 124 foreach ($trusted as $cidr) { 125 if (is_string($cidr) && dfehc_ip_in_cidr($remote, $cidr)) { 126 return true; 127 } 128 } 129 return false; 130 } 131 132 function dfehc_select_client_ip_from_xff(string $xff, array $trustedCidrs): ?string 133 { 134 $candidates = array_filter(array_map('trim', explode(',', $xff)), 'strlen'); 135 $ipNonTrusted = null; 136 foreach ($candidates as $ip) { 137 $ip = preg_replace('/%[0-9A-Za-z.\-]+$/', '', $ip); 138 if (!filter_var($ip, FILTER_VALIDATE_IP)) { 139 continue; 140 } 141 $isTrustedHop = false; 142 foreach ($trustedCidrs as $cidr) { 143 if (is_string($cidr) && dfehc_ip_in_cidr($ip, $cidr)) { 144 $isTrustedHop = true; 145 break; 146 } 147 } 148 if ($isTrustedHop) { 149 continue; 150 } 151 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { 152 return $ip; 153 } 154 if ($ipNonTrusted === null) { 155 $ipNonTrusted = $ip; 156 } 157 } 158 if ($ipNonTrusted !== null) { 159 return $ipNonTrusted; 160 } 161 $last = trim((string) end($candidates)); 162 return filter_var($last, FILTER_VALIDATE_IP) ? $last : null; 163 } 164 22 165 function dfehc_is_request_bot(): bool 23 166 { 24 167 static $cached = null; 25 168 if ($cached !== null) { 26 return $cached;169 return (bool) apply_filters('dfehc_is_request_bot', $cached, $_SERVER); 27 170 } 28 171 29 172 $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; 30 173 if ($ua === '' || preg_match(dfehc_get_bot_pattern(), $ua)) { 31 return $cached = true; 174 $cached = true; 175 return (bool) apply_filters('dfehc_is_request_bot', $cached, $_SERVER); 32 176 } 33 177 … … 35 179 $sec_ch = $_SERVER['HTTP_SEC_CH_UA'] ?? ''; 36 180 if (($accept === '' || stripos($accept, 'text/html') === false) && $sec_ch === '') { 37 return $cached = true; 38 } 39 40 $ip = $_SERVER['REMOTE_ADDR'] ?? ''; 41 $ipKey = $ip ? 'dfehc_bad_ip_' . md5($ip) : ''; 42 $group = apply_filters('dfehc_cache_group', 'dfehc'); 43 44 if ($ipKey && wp_using_ext_object_cache() && function_exists('wp_cache_get')) { 45 if (wp_cache_get($ipKey, $group)) { 46 return $cached = true; 47 } 48 } 49 return $cached = false; 181 $cached = true; 182 return (bool) apply_filters('dfehc_is_request_bot', $cached, $_SERVER); 183 } 184 185 $ip = dfehc_client_ip(); 186 $ipKeyScoped = dfehc_scoped_key('dfehc_bad_ip_') . md5($ip ?: 'none'); 187 $group = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'); 188 189 if ($ip && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) { 190 if (wp_cache_get($ipKeyScoped, $group)) { 191 $cached = true; 192 return (bool) apply_filters('dfehc_is_request_bot', $cached, $_SERVER); 193 } 194 } else { 195 $tkey = $ipKeyScoped . '_t'; 196 if ($ip && get_transient($tkey)) { 197 $cached = true; 198 return (bool) apply_filters('dfehc_is_request_bot', $cached, $_SERVER); 199 } 200 } 201 202 $cached = false; 203 return (bool) apply_filters('dfehc_is_request_bot', $cached, $_SERVER); 204 } 205 206 function dfehc_should_set_cookie(): bool 207 { 208 if (defined('DOING_CRON') && DOING_CRON) return false; 209 if (defined('WP_CLI') && WP_CLI) return false; 210 if (function_exists('is_admin') && is_admin()) return false; 211 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) return false; 212 if (function_exists('wp_is_json_request') && wp_is_json_request()) return false; 213 if (function_exists('rest_get_url_prefix')) { 214 $p = rest_get_url_prefix(); 215 $uri = $_SERVER['REQUEST_URI'] ?? ''; 216 if ($p && strpos($uri, '/' . trim($p, '/') . '/') === 0) return false; 217 } 218 if (function_exists('is_feed') && is_feed()) return false; 219 if (function_exists('is_robots') && is_robots()) return false; 220 if (!empty($_SERVER['REQUEST_METHOD']) && strtoupper((string) $_SERVER['REQUEST_METHOD']) !== 'GET') return false; 221 return true; 50 222 } 51 223 52 224 function dfehc_set_user_cookie(): void 53 225 { 226 if (!dfehc_should_set_cookie()) { 227 return; 228 } 54 229 if (dfehc_is_request_bot()) { 55 230 return; 56 231 } 57 232 58 $ip = $_SERVER['REMOTE_ADDR'] ?? '';59 $group = apply_filters('dfehc_cache_group', 'dfehc');233 $ip = dfehc_client_ip(); 234 $group = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'); 60 235 $maxRPM = (int) apply_filters('dfehc_max_rpm', 120); 61 62 if ($ip && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) { 63 $rpmKey = 'dfehc_iprpm_' . md5($ip); 64 $rpm = wp_cache_incr($rpmKey, 1, $group); 236 $badIpTtl = (int) apply_filters('dfehc_bad_ip_ttl', HOUR_IN_SECONDS); 237 $scopedVisitorKey = dfehc_scoped_key('dfehc_total_visitors'); 238 239 if ($ip && function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) { 240 $rpmKey = dfehc_scoped_key('dfehc_iprpm_') . md5($ip); 241 $rpmTtl = 60 + (function_exists('random_int') ? random_int(0, 5) : 0); 242 if (false === wp_cache_add($rpmKey, 0, $group, $rpmTtl)) { 243 wp_cache_set($rpmKey, (int) (wp_cache_get($rpmKey, $group) ?: 0), $group, $rpmTtl); 244 } 245 $rpm = wp_cache_incr($rpmKey, 1, $group); 65 246 if ($rpm === false) { 66 wp_cache_set($rpmKey, 1, $group, 60);247 wp_cache_set($rpmKey, 1, $group, $rpmTtl); 67 248 $rpm = 1; 249 } else { 250 wp_cache_set($rpmKey, (int) $rpm, $group, $rpmTtl); 68 251 } 69 252 if ($rpm > $maxRPM) { 70 wp_cache_set('dfehc_bad_ip_' . md5($ip), 1, $group, HOUR_IN_SECONDS); 253 $badKey = dfehc_scoped_key('dfehc_bad_ip_') . md5($ip); 254 wp_cache_set($badKey, 1, $group, $badIpTtl); 255 $tkey = $badKey . '_t'; 256 dfehc_set_transient_noautoload($tkey, 1, $badIpTtl); 257 wp_cache_delete($rpmKey, $group); 71 258 return; 72 259 } 73 260 } 74 261 75 $name = 'dfehc_user'; 76 $lifetime = (int) apply_filters('dfehc_cookie_lifetime', 400); 77 $path = (string) apply_filters('dfehc_cookie_path', '/'); 78 $domain = (string) apply_filters('dfehc_cookie_domain', parse_url(home_url(), PHP_URL_HOST)); 79 $sameSite = (string) apply_filters('dfehc_cookie_samesite', 'Strict'); 80 $secure = is_ssl() || $sameSite === 'None'; 81 $httpOnly = true; 82 83 $val = $_COOKIE[$name] ?? ''; 84 if (!preg_match('/^[A-Za-z0-9]{32,64}$/', $val)) { 85 $val = function_exists('random_bytes') 86 ? bin2hex(random_bytes(16)) 87 : uniqid('v_', true) . mt_rand(); 88 } 89 $expires = time() + $lifetime; 90 91 if (PHP_VERSION_ID >= 70300) { 92 setcookie($name, $val, [ 93 'expires' => $expires, 94 'path' => $path, 95 'domain' => $domain, 96 'secure' => $secure, 97 'httponly' => $httpOnly, 98 'samesite' => $sameSite, 99 ]); 100 } else { 101 header(sprintf( 102 'Set-Cookie: %s=%s; Expires=%s; Path=%s; Domain=%s%s; HttpOnly; SameSite=%s', 103 rawurlencode($name), 104 rawurlencode($val), 105 gmdate('D, d-M-Y H:i:s T', $expires), 106 $path, 107 $domain, 108 $secure ? '; Secure' : '', 109 $sameSite 110 )); 262 $name = (string) apply_filters('dfehc_cookie_name', 'dfehc_user'); 263 $lifetime = (int) apply_filters('dfehc_cookie_lifetime', 400); 264 $path = (string) apply_filters('dfehc_cookie_path', defined('COOKIEPATH') ? COOKIEPATH : '/'); 265 $host = function_exists('home_url') ? parse_url(home_url(), PHP_URL_HOST) : ''; 266 $isIpHost = $host && (filter_var($host, FILTER_VALIDATE_IP) !== false); 267 $domainDefault = $isIpHost ? '' : ($host ?: (defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : '')); 268 $domain = (string) apply_filters('dfehc_cookie_domain', $domainDefault); 269 270 $sameSite = (string) apply_filters('dfehc_cookie_samesite', 'Lax'); 271 $map = ['lax' => 'Lax', 'strict' => 'Strict', 'none' => 'None']; 272 $sameSiteUpper = $map[strtolower($sameSite)] ?? 'Lax'; 273 274 $secure = (function_exists('is_ssl') && is_ssl()) || $sameSiteUpper === 'None'; 275 if ($sameSiteUpper === 'None' && !$secure) { 276 $sameSiteUpper = 'Lax'; 277 } 278 $httpOnly = true; 279 280 $nowTs = (int) ($_SERVER['REQUEST_TIME'] ?? time()); 281 $existing = $_COOKIE[$name] ?? ''; 282 $val = $existing; 283 if (!preg_match('/^[A-Fa-f0-9]{32,64}$/', (string) $val)) { 284 if (function_exists('random_bytes')) { 285 $val = bin2hex(random_bytes(16)); 286 } else { 287 $val = bin2hex(pack('H*', substr(sha1(uniqid((string) mt_rand(), true)), 0, 32))); 288 } 289 } 290 291 $shouldRefresh = !isset($_COOKIE[$name]) || (mt_rand(0, 99) < (int) apply_filters('dfehc_cookie_refresh_percent', 5)); 292 if ($shouldRefresh) { 293 if (!headers_sent()) { 294 $expires = $nowTs + $lifetime; 295 if (PHP_VERSION_ID >= 70300) { 296 setcookie($name, $val, [ 297 'expires' => $expires, 298 'path' => $path, 299 'domain' => $domain ?: null, 300 'secure' => $secure, 301 'httponly' => $httpOnly, 302 'samesite' => $sameSiteUpper, 303 ]); 304 } else { 305 header(sprintf( 306 'Set-Cookie: %s=%s; Expires=%s; Path=%s%s%s; HttpOnly; SameSite=%s', 307 rawurlencode($name), 308 rawurlencode($val), 309 gmdate('D, d-M-Y H:i:s T', $expires), 310 $path, 311 $domain ? '; Domain=' . $domain : '', 312 $secure ? '; Secure' : '', 313 $sameSiteUpper 314 ), false); 315 } 316 } 111 317 } 112 318 … … 115 321 } 116 322 117 $visitorKey = 'dfehc_total_visitors'; 118 if (wp_using_ext_object_cache() && function_exists('wp_cache_incr')) { 119 if (false === wp_cache_incr($visitorKey, 1, $group)) { 120 wp_cache_set($visitorKey, 1, $group, $lifetime); 121 } 122 return; 123 } 124 125 static $client = null; 126 127 if (!$client && extension_loaded('redis') && class_exists('Redis')) { 128 try { 129 $client = new \Redis(); 130 131 $sock = get_option('dfehc_redis_socket', ''); 132 $ok = $sock 133 ? $client->pconnect($sock) 134 : $client->pconnect( 135 function_exists('dfehc_get_redis_server') ? dfehc_get_redis_server() : '127.0.0.1', 136 function_exists('dfehc_get_redis_port') ? dfehc_get_redis_port() : 6379, 137 1.0 323 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) { 324 $vTtl = $lifetime + (function_exists('random_int') ? random_int(0, 5) : 0); 325 if (false === wp_cache_add($scopedVisitorKey, 0, $group, $vTtl)) { 326 wp_cache_set($scopedVisitorKey, (int) (wp_cache_get($scopedVisitorKey, $group) ?: 0), $group, $vTtl); 327 } 328 $valInc = wp_cache_incr($scopedVisitorKey, 1, $group); 329 if ($valInc === false) { 330 wp_cache_set($scopedVisitorKey, 1, $group, $vTtl); 331 } else { 332 wp_cache_set($scopedVisitorKey, (int) $valInc, $group, $vTtl); 333 } 334 return; 335 } 336 337 $allowDirectClients = (bool) apply_filters('dfehc_enable_direct_cache_clients', false); 338 339 static $client = null; 340 if ($allowDirectClients && !$client && extension_loaded('redis') && class_exists('Redis')) { 341 try { 342 $client = new \Redis(); 343 $sock = function_exists('get_option') ? get_option('dfehc_redis_socket', '') : ''; 344 $ok = $sock 345 ? $client->pconnect($sock) 346 : $client->pconnect( 347 function_exists('dfehc_get_redis_server') ? dfehc_get_redis_server() : '127.0.0.1', 348 function_exists('dfehc_get_redis_port') ? dfehc_get_redis_port() : 6379, 349 1.0 350 ); 351 if ($ok) { 352 $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD') ?: null); 353 $user = apply_filters('dfehc_redis_user', getenv('REDIS_USERNAME') ?: null); 354 if ($user && $pass && method_exists($client, 'auth')) { 355 $client->auth([$user, $pass]); 356 } elseif ($pass && method_exists($client, 'auth')) { 357 $client->auth($pass); 358 } 359 $pong = $client->ping(); 360 if (!in_array($pong, ['+PONG','PONG',true], true)) { 361 $client = null; 362 } 363 } else { 364 $client = null; 365 } 366 } catch (\Throwable $e) { 367 $client = null; 368 } 369 } 370 if ($client) { 371 $client->incr($scopedVisitorKey); 372 $client->expire($scopedVisitorKey, $lifetime); 373 return; 374 } 375 376 static $mem = null; 377 if ($allowDirectClients && !$mem && extension_loaded('memcached') && class_exists('Memcached')) { 378 $mem = new \Memcached('dfehc-cookie'); 379 if (!$mem->getServerList()) { 380 $mem->addServer( 381 function_exists('dfehc_get_memcached_server') ? dfehc_get_memcached_server() : '127.0.0.1', 382 function_exists('dfehc_get_memcached_port') ? dfehc_get_memcached_port() : 11211 138 383 ); 139 140 if (!$ok || $client->ping() !== '+PONG') { 141 $client = null; 142 } 143 } catch (\Throwable $e) { 144 $client = null; 145 } 146 } 147 148 if ($client) { 149 $client->incr($visitorKey); 150 $client->expire($visitorKey, $lifetime); 151 return; 152 } 153 154 static $mem = null; 155 if (!$mem && extension_loaded('memcached') && class_exists('Memcached')) { 156 $mem = new \Memcached(); 157 $mem->addServer( 158 function_exists('dfehc_get_memcached_server') ? dfehc_get_memcached_server() : '127.0.0.1', 159 function_exists('dfehc_get_memcached_port') ? dfehc_get_memcached_port() : 11211 160 ); 384 } 161 385 if (empty($mem->getStats())) { 162 386 $mem = null; … … 164 388 } 165 389 if ($mem) { 166 $val = $mem->increment($visitorKey, 1);167 if ($val === false) {168 $mem->set($ visitorKey, 1, $lifetime);390 $valInc = $mem->increment($scopedVisitorKey, 1); 391 if ($valInc === false) { 392 $mem->set($scopedVisitorKey, 1, $lifetime); 169 393 } else { 170 $mem->touch($visitorKey, $lifetime); 171 } 172 return; 173 } 174 175 $cnt = (int) get_transient($visitorKey); 176 set_transient($visitorKey, $cnt + 1, $lifetime); 177 } 178 179 add_action('send_headers', 'dfehc_set_user_cookie'); 394 $mem->touch($scopedVisitorKey, $lifetime); 395 } 396 return; 397 } 398 399 $cnt = (int) get_transient($scopedVisitorKey); 400 $vTtl = $lifetime + (function_exists('random_int') ? random_int(0, 5) : 0); 401 dfehc_set_transient_noautoload($scopedVisitorKey, $cnt + 1, $vTtl); 402 } 403 404 add_action('send_headers', 'dfehc_set_user_cookie', 1); -
dynamic-front-end-heartbeat-control/trunk/visitor/manager.php
r3320647 r3396626 2 2 declare(strict_types=1); 3 3 4 if (!function_exists('dfehc_blog_id')) { 5 function dfehc_blog_id(): int { 6 return function_exists('get_current_blog_id') ? (int) get_current_blog_id() : 0; 7 } 8 } 9 if (!function_exists('dfehc_host_token')) { 10 function dfehc_host_token(): string { 11 static $t = ''; 12 if ($t !== '') return $t; 13 $host = @php_uname('n') ?: (defined('WP_HOME') ? WP_HOME : (function_exists('home_url') ? home_url() : 'unknown')); 14 $salt = defined('DB_NAME') ? (string) DB_NAME : ''; 15 return $t = substr(md5((string) $host . $salt), 0, 10); 16 } 17 } 18 if (!function_exists('dfehc_scoped_key')) { 19 function dfehc_scoped_key(string $base): string { 20 return $base . '_' . dfehc_blog_id() . '_' . dfehc_host_token(); 21 } 22 } 23 if (!function_exists('dfehc_set_transient_noautoload')) { 24 function dfehc_set_transient_noautoload(string $key, $value, int $expiration): void { 25 $group = defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'; 26 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache()) { 27 if (function_exists('wp_cache_add')) { 28 if (!wp_cache_add($key, $value, $group, $expiration)) { 29 wp_cache_set($key, $value, $group, $expiration); 30 } 31 } else { 32 wp_cache_set($key, $value, $group, $expiration); 33 } 34 return; 35 } 36 set_transient($key, $value, $expiration); 37 global $wpdb; 38 if (!isset($wpdb->options)) return; 39 $opt_key = "_transient_$key"; 40 $opt_key_to = "_transient_timeout_$key"; 41 $wpdb->suppress_errors(true); 42 $autoload = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key)); 43 if ($autoload === 'yes') { 44 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 45 } 46 $autoload_to = $wpdb->get_var($wpdb->prepare("SELECT autoload FROM {$wpdb->options} WHERE option_name=%s LIMIT 1", $opt_key_to)); 47 if ($autoload_to === 'yes') { 48 $wpdb->update($wpdb->options, ['autoload' => 'no'], ['option_name' => $opt_key_to, 'autoload' => 'yes'], ['%s'], ['%s','%s']); 49 } 50 $wpdb->suppress_errors(false); 51 } 52 } 53 if (!function_exists('dfehc_get_redis_server')) { 54 function dfehc_get_redis_server(): string { 55 $h = getenv('REDIS_HOST'); 56 return $h ? (string) $h : '127.0.0.1'; 57 } 58 } 59 if (!function_exists('dfehc_get_redis_port')) { 60 function dfehc_get_redis_port(): int { 61 $p = getenv('REDIS_PORT'); 62 return $p && ctype_digit((string) $p) ? (int) $p : 6379; 63 } 64 } 65 if (!function_exists('dfehc_get_memcached_server')) { 66 function dfehc_get_memcached_server(): string { 67 $h = getenv('MEMCACHED_HOST'); 68 return $h ? (string) $h : '127.0.0.1'; 69 } 70 } 71 if (!function_exists('dfehc_get_memcached_port')) { 72 function dfehc_get_memcached_port(): int { 73 $p = getenv('MEMCACHED_PORT'); 74 return $p && ctype_digit((string) $p) ? (int) $p : 11211; 75 } 76 } 77 if (!defined('DFEHC_SENTINEL_NO_LOAD')) { 78 define('DFEHC_SENTINEL_NO_LOAD', -1); 79 } 80 4 81 if (!function_exists('dfehc_acquire_lock')) { 5 82 function dfehc_acquire_lock(string $key, int $ttl = 60) { 83 $group = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'); 84 $scoped = dfehc_scoped_key($key); 6 85 if (class_exists('WP_Lock')) { 7 $lock = new WP_Lock($ key, $ttl);86 $lock = new WP_Lock($scoped, $ttl); 8 87 return $lock->acquire() ? $lock : null; 9 88 } 10 return wp_cache_add($key, 1, '', $ttl) ? (object) ['cache_key' => $key] : null; 89 if (function_exists('wp_cache_add') && wp_cache_add($scoped, 1, $group, $ttl)) { 90 return (object) ['cache_key' => $scoped, 'cache_group' => $group]; 91 } 92 if (false !== get_transient($scoped)) { 93 return null; 94 } 95 if (set_transient($scoped, 1, $ttl)) { 96 return (object) ['transient_key' => $scoped]; 97 } 98 return null; 11 99 } 12 100 function dfehc_release_lock($lock): void { 13 101 if ($lock instanceof WP_Lock) { 14 102 $lock->release(); 15 } elseif (is_object($lock) && isset($lock->cache_key)) { 16 wp_cache_delete($lock->cache_key, ''); 103 return; 104 } 105 if (is_object($lock) && isset($lock->cache_key)) { 106 $group = $lock->cache_group ?? apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'); 107 wp_cache_delete($lock->cache_key, $group); 108 return; 109 } 110 if (is_object($lock) && isset($lock->transient_key)) { 111 delete_transient($lock->transient_key); 17 112 } 18 113 } … … 20 115 21 116 function dfehc_set_default_last_activity_time(int $user_id): void { 22 update_user_meta($user_id, 'last_activity_time', time()); 117 $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time'); 118 update_user_meta($user_id, $meta_key, time()); 23 119 } 24 120 add_action('user_register', 'dfehc_set_default_last_activity_time'); … … 26 122 function dfehc_add_intervals(array $s): array { 27 123 $s['dfehc_5_minutes'] ??= ['interval' => 300, 'display' => __('Every 5 minutes (DFEHC)', 'dfehc')]; 124 $s['dfehc_daily'] ??= ['interval' => DAY_IN_SECONDS, 'display' => __('Daily (DFEHC)', 'dfehc')]; 28 125 return $s; 29 126 } … … 31 128 32 129 function dfehc_schedule_user_activity_processing(): void { 130 $lock = dfehc_acquire_lock('dfehc_cron_sched_lock', 15); 131 $aligned = time() - time() % 300 + 300; 33 132 if (!get_option('dfehc_activity_cron_scheduled') && !wp_next_scheduled('dfehc_process_user_activity')) { 34 wp_schedule_event(time() - time() % 300 + 300, 'dfehc_5_minutes', 'dfehc_process_user_activity'); 35 update_option('dfehc_activity_cron_scheduled', 1, false); 133 $ok = wp_schedule_event($aligned, 'dfehc_5_minutes', 'dfehc_process_user_activity'); 134 if ($ok) { 135 update_option('dfehc_activity_cron_scheduled', 1, false); 136 } else { 137 wp_schedule_single_event($aligned, 'dfehc_process_user_activity'); 138 } 139 } 140 if (!wp_next_scheduled('dfehc_cleanup_user_activity')) { 141 $args = [0, (int) apply_filters('dfehc_cleanup_batch_size', 75)]; 142 $ok2 = wp_schedule_event($aligned + 300, 'dfehc_daily', 'dfehc_cleanup_user_activity', $args); 143 if (!$ok2) { 144 wp_schedule_single_event($aligned + 300, 'dfehc_cleanup_user_activity', $args); 145 } 146 } 147 if ($lock) { 148 dfehc_release_lock($lock); 36 149 } 37 150 } … … 43 156 return; 44 157 } 158 159 $prev = (bool) ignore_user_abort(true); 160 45 161 try { 46 162 dfehc_process_user_activity(); 47 163 } finally { 164 ignore_user_abort((bool) $prev); 48 165 dfehc_release_lock($lock); 49 166 } … … 52 169 53 170 function dfehc_process_user_activity(): void { 54 if (get_transient('dfehc_activity_backfill_complete')) { 171 $flag_opt = dfehc_scoped_key('dfehc_activity_backfill_done'); 172 if (get_option($flag_opt)) { 55 173 return; 56 174 } 57 175 global $wpdb; 58 $batch = (int) apply_filters('dfehc_activity_processing_batch_size', 75); 59 $last_id = (int) get_option('dfehc_activity_last_id', 0); 60 $ids = $wpdb->get_col($wpdb->prepare( 176 $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time'); 177 $batch = (int) apply_filters('dfehc_activity_processing_batch_size', 75); 178 $batch = max(1, min(500, $batch)); 179 $last_id_opt = dfehc_scoped_key('dfehc_activity_last_id'); 180 $last_id = (int) get_option($last_id_opt, 0); 181 $ids = $wpdb->get_col($wpdb->prepare( 61 182 "SELECT ID FROM $wpdb->users WHERE ID > %d ORDER BY ID ASC LIMIT %d", 62 183 $last_id, $batch 63 184 )); 64 185 if (!$ids) { 65 set_transient('dfehc_activity_backfill_complete', true, DAY_IN_SECONDS); 66 delete_option('dfehc_activity_last_id'); 186 update_option($flag_opt, 1, false); 187 delete_option($last_id_opt); 188 update_option(dfehc_scoped_key('dfehc_last_activity_cron'), time(), false); 67 189 return; 68 190 } 69 191 $now = time(); 192 $written = 0; 193 $max_writes = (int) apply_filters('dfehc_activity_max_writes_per_run', 500); 70 194 foreach ($ids as $id) { 71 if (!get_user_meta($id, 'last_activity_time', true)) { 72 update_user_meta($id, 'last_activity_time', $now); 73 } 74 } 75 update_option('dfehc_activity_last_id', end($ids), false); 195 if (!get_user_meta($id, $meta_key, true)) { 196 update_user_meta($id, $meta_key, $now); 197 $written++; 198 if ($written >= $max_writes) { 199 break; 200 } 201 } 202 } 203 update_option($last_id_opt, end($ids), false); 204 update_option(dfehc_scoped_key('dfehc_last_activity_cron'), time(), false); 76 205 } 77 206 78 207 function dfehc_record_user_activity(): void { 79 if (! is_user_logged_in()) {208 if (!function_exists('is_user_logged_in') || !is_user_logged_in()) { 80 209 return; 81 210 } 82 211 static $cache = []; 83 $uid = get_current_user_id(); 84 $now = time(); 212 $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time'); 213 $uid = get_current_user_id(); 214 $now = time(); 85 215 $interval = (int) apply_filters('dfehc_activity_update_interval', 900); 86 $last = $cache[$uid] ?? (int) get_user_meta($uid, 'last_activity_time', true); 216 $interval = max(60, $interval); 217 $last = $cache[$uid] ?? (int) get_user_meta($uid, $meta_key, true); 87 218 if ($now - $last >= $interval) { 88 update_user_meta($uid, 'last_activity_time', $now);219 update_user_meta($uid, $meta_key, $now); 89 220 $cache[$uid] = $now; 90 221 } … … 97 228 return; 98 229 } 230 $prev = ignore_user_abort(true); 231 if (function_exists('set_time_limit')) { 232 @set_time_limit(30); 233 } 99 234 try { 100 235 global $wpdb; 236 $meta_key = (string) apply_filters('dfehc_last_activity_meta_key', 'last_activity_time'); 101 237 $batch_size = (int) apply_filters('dfehc_cleanup_batch_size', $batch_size); 238 $batch_size = max(1, min(500, $batch_size)); 102 239 $ids = $wpdb->get_col($wpdb->prepare( 103 240 "SELECT ID FROM $wpdb->users WHERE ID > %d ORDER BY ID ASC LIMIT %d", … … 105 242 )); 106 243 if (!$ids) { 107 return; 108 } 244 update_option(dfehc_scoped_key('dfehc_last_cleanup_cron'), time(), false); 245 return; 246 } 247 $cutoff = time() - (int) apply_filters('dfehc_activity_expiration', WEEK_IN_SECONDS); 109 248 foreach ($ids as $id) { 110 delete_user_meta($id, 'last_activity_time'); 249 $ts = (int) get_user_meta($id, $meta_key, true); 250 if ($ts && $ts < $cutoff) { 251 delete_user_meta($id, $meta_key); 252 } 111 253 } 112 254 if (count($ids) === $batch_size) { 113 wp_schedule_single_event(time() + 15, 'dfehc_cleanup_user_activity', [end($ids), $batch_size]); 114 } 255 wp_schedule_single_event(time() + 15 + (function_exists('random_int') ? random_int(0, 5) : rand(0, 5)), 'dfehc_cleanup_user_activity', [end($ids), $batch_size]); 256 } 257 update_option(dfehc_scoped_key('dfehc_last_cleanup_cron'), time(), false); 115 258 } finally { 259 ignore_user_abort($prev); 116 260 dfehc_release_lock($lock); 117 261 } … … 120 264 121 265 function dfehc_increment_total_visitors(): void { 122 $key = 'dfehc_total_visitors'; 123 $grp = apply_filters('dfehc_cache_group', 'dfehc'); 124 $ttl = HOUR_IN_SECONDS; 125 126 if (wp_using_ext_object_cache() && function_exists('wp_cache_incr')) { 127 if (false === wp_cache_incr($key, 1, $grp)) { 266 $key = dfehc_scoped_key('dfehc_total_visitors'); 267 $grp = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'); 268 $ttl = (int) apply_filters('dfehc_total_visitors_ttl', HOUR_IN_SECONDS); 269 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 270 271 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_incr')) { 272 if (false === wp_cache_add($key, 0, $grp, $ttl)) { 273 $existing = wp_cache_get($key, $grp); 274 wp_cache_set($key, (int) ($existing ?: 0), $grp, $ttl); 275 } 276 $val = wp_cache_incr($key, 1, $grp); 277 if ($val === false) { 128 278 wp_cache_set($key, 1, $grp, $ttl); 129 279 } … … 131 281 } 132 282 133 static $conn; 134 135 if (!$conn && extension_loaded('redis') && class_exists('Redis')) { 136 try { 137 $conn = new \Redis(); 138 if ( 139 !$conn->pconnect('127.0.0.1', 6379) || 140 $conn->ping() !== '+PONG' 141 ) { 142 $conn = null; 143 } 144 } catch (\Throwable $e) { 145 $conn = null; 146 } 147 } 148 149 if ($conn) { 150 $conn->incr($key); 151 $conn->expire($key, $ttl); 152 return; 153 } 154 283 $allowDirectClients = (bool) apply_filters('dfehc_enable_direct_cache_clients', false); 284 285 static $redis = null; 286 if ($allowDirectClients && !$redis && extension_loaded('redis') && class_exists('Redis')) { 287 try { 288 $redis = new \Redis(); 289 $ok = $redis->pconnect(dfehc_get_redis_server(), dfehc_get_redis_port(), 1.0); 290 if ($ok) { 291 $pass = apply_filters('dfehc_redis_auth', getenv('REDIS_PASSWORD') ?: null); 292 $user = apply_filters('dfehc_redis_user', getenv('REDIS_USERNAME') ?: null); 293 if ($user && $pass && method_exists($redis, 'auth')) { 294 $redis->auth([$user, $pass]); 295 } elseif ($pass && method_exists($redis, 'auth')) { 296 $redis->auth($pass); 297 } 298 $pong = $redis->ping(); 299 if (!in_array($pong, ['+PONG','PONG', true], true)) { 300 $redis = null; 301 } 302 } else { 303 $redis = null; 304 } 305 } catch (\Throwable $e) { 306 $redis = null; 307 } 308 } 309 if ($redis) { 310 $redis->incr($key); 311 $redis->expire($key, $ttl); 312 return; 313 } 314 315 static $mem = null; 316 if ($allowDirectClients && !$mem && extension_loaded('memcached') && class_exists('Memcached')) { 317 $mem = new \Memcached('dfehc-visitors'); 318 if (!$mem->getServerList()) { 319 $mem->addServer(dfehc_get_memcached_server(), dfehc_get_memcached_port()); 320 } 321 if (empty($mem->getStats())) { 322 $mem = null; 323 } 324 } 325 if ($mem) { 326 $inc = $mem->increment($key, 1); 327 if ($inc === false) { 328 $mem->set($key, 1, $ttl); 329 } else { 330 $mem->touch($key, $ttl); 331 } 332 return; 333 } 155 334 156 335 $cnt = (int) get_transient($key); 157 set_transient($key, $cnt + 1, $ttl); 336 if ($cnt === 0) { 337 dfehc_set_transient_noautoload($key, 0, $ttl); 338 $cnt = 0; 339 } 340 dfehc_set_transient_noautoload($key, $cnt + 1, $ttl); 158 341 } 159 342 function dfehc_increment_total_visitors_fallback(): void { … … 162 345 163 346 function dfehc_safe_cache_get(string $key): int { 164 $grp = apply_filters('dfehc_cache_group', 'dfehc'); 165 if (wp_using_ext_object_cache() && function_exists('wp_cache_get')) { 166 $v = wp_cache_get($key, $grp); 167 return $v !== false ? (int) $v : 0; 168 } 169 $v = get_transient($key); 170 return $v !== false ? (int) $v : 0; 347 $scoped = dfehc_scoped_key($key); 348 $grp = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'); 349 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_get')) { 350 $v = wp_cache_get($scoped, $grp); 351 return is_numeric($v) ? (int) $v : 0; 352 } 353 $v = get_transient($scoped); 354 return is_numeric($v) ? (int) $v : 0; 171 355 } 172 356 173 357 function dfehc_safe_cache_delete(string $key): void { 174 $grp = apply_filters('dfehc_cache_group', 'dfehc'); 175 if (wp_using_ext_object_cache() && function_exists('wp_cache_delete')) { 176 wp_cache_delete($key, $grp); 177 } 178 delete_transient($key); 358 $scoped = dfehc_scoped_key($key); 359 $grp = apply_filters('dfehc_cache_group', defined('DFEHC_CACHE_GROUP') ? DFEHC_CACHE_GROUP : 'dfehc'); 360 if (function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache() && function_exists('wp_cache_delete')) { 361 wp_cache_delete($scoped, $grp); 362 } 363 delete_transient($scoped); 179 364 } 180 365 181 366 function dfehc_get_website_visitors(): int { 182 $cache = get_transient('dfehc_total_visitors'); 367 $cache_key = dfehc_scoped_key('dfehc_total_visitors_cache'); 368 $regen_key = dfehc_scoped_key('dfehc_regenerating_cache'); 369 $stale_opt = dfehc_scoped_key('dfehc_stale_total_visitors'); 370 $cache = get_transient($cache_key); 183 371 if ($cache !== false) { 184 372 return (int) $cache; 185 373 } 186 if (get_transient('dfehc_regenerating_cache')) { 187 return (int) get_option('dfehc_stale_total_visitors', 0); 188 } 189 190 set_transient('dfehc_regenerating_cache', true, MINUTE_IN_SECONDS); 374 if (get_transient($regen_key)) { 375 $stale = get_option($stale_opt, 0); 376 return is_numeric($stale) ? (int) $stale : 0; 377 } 378 379 $regen_ttl = MINUTE_IN_SECONDS + (function_exists('random_int') ? random_int(0, 5) : 0); 380 dfehc_set_transient_noautoload($regen_key, true, $regen_ttl); 191 381 $total = dfehc_safe_cache_get('dfehc_total_visitors'); 192 set_transient('dfehc_total_visitors', $total, 4 * MINUTE_IN_SECONDS); 193 update_option('dfehc_stale_total_visitors', $total, false); 194 195 delete_transient('dfehc_regenerating_cache'); 196 return (int) apply_filters('dfehc_get_website_visitors_result', $total); 197 } 198 function dfehc_get_users_in_batches(int $batch_size, int $offset): array 199 { 382 $ttl = (int) apply_filters('dfehc_visitors_cache_ttl', 10 * MINUTE_IN_SECONDS); 383 $ttl += function_exists('random_int') ? random_int(0, 5) : 0; 384 dfehc_set_transient_noautoload($cache_key, (int) $total, $ttl); 385 update_option($stale_opt, (int) $total, false); 386 delete_transient($regen_key); 387 388 return (int) apply_filters('dfehc_get_website_visitors_result', (int) $total); 389 } 390 391 function dfehc_get_users_in_batches(int $batch_size, int $offset): array { 392 $batch_size = max(1, min(1000, $batch_size)); 393 $offset = max(0, $offset); 200 394 $query = new WP_User_Query([ 201 395 'number' => $batch_size, … … 203 397 'fields' => ['ID'], 204 398 ]); 205 return $query->get_results(); 206 } 399 $res = $query->get_results(); 400 return is_array($res) ? $res : []; 401 } 402 207 403 function dfehc_reset_total_visitors(): void { 404 $start = microtime(true); 208 405 $lock = dfehc_acquire_lock('dfehc_resetting_visitors', 60); 209 406 if (!$lock) { … … 212 409 try { 213 410 $threshold = (float) apply_filters('dfehc_reset_load_threshold', 15.0); 214 $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : 0; 215 if ($load === \DFEHC_SENTINEL_NO_LOAD || $load >= $threshold) { 411 $load = function_exists('dfehc_get_server_load') ? dfehc_get_server_load() : DFEHC_SENTINEL_NO_LOAD; 412 if (!is_numeric($load)) { 413 return; 414 } 415 $load = (float) $load; 416 if ($load === (float) DFEHC_SENTINEL_NO_LOAD || $load >= $threshold) { 216 417 return; 217 418 } 218 419 dfehc_safe_cache_delete('dfehc_total_visitors'); 219 delete_option('dfehc_stale_total_visitors'); 220 delete_transient('dfehc_total_visitors'); 420 delete_option(dfehc_scoped_key('dfehc_stale_total_visitors')); 421 delete_transient(dfehc_scoped_key('dfehc_total_visitors')); 422 delete_transient(dfehc_scoped_key('dfehc_regenerating_cache')); 423 if (microtime(true) - $start > 5) { 424 return; 425 } 221 426 } finally { 222 427 dfehc_release_lock($lock); … … 226 431 227 432 function dfehc_on_activate(): void { 433 $aligned = time() - time() % 300 + 300; 228 434 if (!wp_next_scheduled('dfehc_reset_total_visitors_event')) { 229 wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', 'dfehc_reset_total_visitors_event'); 435 $ok = wp_schedule_event($aligned + HOUR_IN_SECONDS, 'hourly', 'dfehc_reset_total_visitors_event'); 436 if (!$ok) { 437 wp_schedule_single_event($aligned + HOUR_IN_SECONDS, 'dfehc_reset_total_visitors_event'); 438 } 230 439 } 231 440 dfehc_process_user_activity(); 232 441 } 233 register_activation_hook(__FILE__, 'dfehc_on_activate'); 442 if (function_exists('register_activation_hook')) { 443 register_activation_hook(__FILE__, 'dfehc_on_activate'); 444 } 234 445 235 446 function dfehc_on_deactivate(): void { 236 447 wp_clear_scheduled_hook('dfehc_process_user_activity'); 237 448 wp_clear_scheduled_hook('dfehc_reset_total_visitors_event'); 449 wp_clear_scheduled_hook('dfehc_cleanup_user_activity'); 238 450 delete_option('dfehc_activity_cron_scheduled'); 239 } 240 register_deactivation_hook(__FILE__, 'dfehc_on_deactivate'); 451 delete_option(dfehc_scoped_key('dfehc_activity_backfill_done')); 452 } 453 if (function_exists('register_deactivation_hook')) { 454 register_deactivation_hook(__FILE__, 'dfehc_on_deactivate'); 455 } 241 456 242 457 if (defined('WP_CLI') && WP_CLI) {
Note: See TracChangeset
for help on using the changeset viewer.