PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.17
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.17
2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 All 139 releases
metasync / includes / class-metasync-cpu-monitor.php

class-metasync-cpu-monitor.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.17, at includes/class-metasync-cpu-monitor.php

306 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * CPU Load Monitoring and Deferral Logic
4 *
5 * Provides utility methods to detect CPU load and defer batch processing
6 * when the server is under high load, improving performance during peak traffic.
7 *
8 * @package Metasync
9 */
10
11 class Metasync_CPU_Monitor {
12 /**
13 * Option key for persistent statistics
14 */
15 const STATS_OPTION_KEY = 'metasync_cpu_stats';
16
17 /**
18 * Transient key for deferral notices
19 */
20 const DEFER_NOTICE_TRANSIENT = 'metasync_cpu_deferral_notice';
21
22 /**
23 * Default CPU load threshold (per core)
24 */
25 const DEFAULT_PER_CORE = 2.0;
26
27 /**
28 * Check if sys_getloadavg() function is available
29 *
30 * @return bool True if function exists, false on Windows or if unavailable
31 */
32 public static function is_available() {
33 return function_exists( 'sys_getloadavg' );
34 }
35
36 /**
37 * Get the current 1-minute load average
38 *
39 * Wraps sys_getloadavg() with a filter hook for testing purposes.
40 *
41 * @return float|false Load average or false on error
42 */
43 public static function get_load_average() {
44 if ( ! self::is_available() ) {
45 return false;
46 }
47
48 $raw = sys_getloadavg();
49 if ( ! is_array( $raw ) || empty( $raw ) ) {
50 return false;
51 }
52
53 // Apply filter for testing purposes (allows mocking high load)
54 $load = apply_filters( 'metasync_cpu_load_average', $raw[0] );
55
56 return is_numeric( $load ) ? (float) $load : false;
57 }
58
59 /**
60 * Cached core-detection result for the current request.
61 *
62 * int = detected core count
63 * false = every detection method failed (restricted host)
64 * null = not probed yet
65 *
66 * @var int|false|null
67 */
68 private static $detected_cores = null;
69
70 /**
71 * Probe the system for its CPU core count
72 *
73 * Detection methods, in order:
74 * 1. Linux: Parse /proc/cpuinfo
75 * 2. Linux/some Unix: shell_exec('nproc')
76 * 3. macOS: shell_exec('sysctl -n hw.ncpu')
77 *
78 * Returns false when every method fails — common on managed/shared
79 * hosting where open_basedir blocks /proc/cpuinfo and shell_exec is
80 * disabled. Callers must treat false as "unknown", NOT as 1 core:
81 * sys_getloadavg() still reports the WHOLE machine's load on such
82 * hosts, so pairing it with an assumed 1-core threshold wrongly flags
83 * large, healthy shared servers as overloaded.
84 *
85 * @return int|false Core count, or false when detection failed
86 */
87 public static function detect_cpu_core_count() {
88 if ( self::$detected_cores !== null ) {
89 return self::$detected_cores;
90 }
91
92 $detected = false;
93
94 // Try Linux: count processor lines in /proc/cpuinfo
95 // Use @ to suppress open_basedir warnings on restricted shared hosting
96 if ( @is_readable( '/proc/cpuinfo' ) ) {
97 $cpuinfo = @file_get_contents( '/proc/cpuinfo' );
98 if ( $cpuinfo !== false ) {
99 $count = substr_count( $cpuinfo, 'processor' );
100 if ( $count > 0 ) {
101 $detected = (int) $count;
102 }
103 }
104 }
105
106 // Try shell_exec: nproc (Linux, some Unix), then sysctl (macOS)
107 if ( $detected === false && function_exists( 'shell_exec' ) && ! in_array( 'shell_exec', explode( ',', ini_get( 'disable_functions' ) ), true ) ) {
108 $nproc = intval( @shell_exec( 'nproc 2>/dev/null' ) );
109 if ( $nproc > 0 ) {
110 $detected = $nproc;
111 } else {
112 $sysctl = intval( @shell_exec( 'sysctl -n hw.ncpu 2>/dev/null' ) );
113 if ( $sysctl > 0 ) {
114 $detected = $sysctl;
115 }
116 }
117 }
118
119 /**
120 * Filter the detected CPU core count.
121 *
122 * Lets hosts and tests override detection: return a positive int to
123 * force a core count, or false to mark detection as failed.
124 *
125 * @param int|false $detected Detected core count, or false when unknown.
126 */
127 $filtered = apply_filters( 'metasync_cpu_detected_cores', $detected );
128
129 // Defend the int|false contract: anything that is not a positive
130 // number counts as failed detection. This keeps the cache from being
131 // reset to null (which would defeat it and re-run shell_exec probes
132 // on every call) and stops a bogus 0/negative/garbage filter return
133 // from silently recreating the 1-core/2.0-threshold gate this class
134 // must avoid while reporting detection as "reliable".
135 // Round through float so numeric strings like "1e3" parse by value
136 // (a direct (int) cast would stop at the "e" and yield 1).
137 $normalized = is_numeric( $filtered ) ? (int) round( (float) $filtered ) : 0;
138 self::$detected_cores = $normalized > 0 ? $normalized : false;
139
140 return self::$detected_cores;
141 }
142
143 /**
144 * Reset the cached core-detection result (used by unit tests)
145 */
146 public static function reset_core_detection_cache() {
147 self::$detected_cores = null;
148 }
149
150 /**
151 * Whether the core count came from a real probe rather than the fallback
152 *
153 * @return bool True when a detection method succeeded
154 */
155 public static function is_core_detection_reliable() {
156 return self::detect_cpu_core_count() !== false;
157 }
158
159 /**
160 * Get the number of CPU cores, falling back to 1 when detection fails
161 *
162 * Note: when is_core_detection_reliable() is false this returns the
163 * 1-core fallback, which is NOT a trustworthy basis for a load
164 * threshold — is_load_safe() fails open in that case.
165 *
166 * @return int Number of CPU cores (minimum 1)
167 */
168 public static function get_cpu_core_count() {
169 $detected = self::detect_cpu_core_count();
170 return $detected === false ? 1 : max( 1, (int) $detected );
171 }
172
173 /**
174 * Get the configured per-core load threshold
175 *
176 * @return float Per-core threshold (default 2.0)
177 */
178 public static function get_per_core_threshold() {
179 return (float) ( Metasync::get_option( 'performance' )['cpu_load_per_core_threshold'] ?? self::DEFAULT_PER_CORE );
180 }
181
182 /**
183 * Get the effective load threshold for this system
184 *
185 * Calculated as: number_of_cores × per_core_threshold
186 *
187 * @return float Effective threshold
188 */
189 public static function get_effective_threshold() {
190 return (float) self::get_cpu_core_count() * self::get_per_core_threshold();
191 }
192
193 /**
194 * Check if it's safe to process (CPU load is below threshold)
195 *
196 * Main guard method called before batch processing operations.
197 * Returns true to proceed, false to defer. On failure, calls record_deferral().
198 *
199 * @return bool True if safe to process, false if deferred
200 */
201 public static function is_load_safe() {
202 // Skip check if sys_getloadavg() is unavailable (Windows)
203 if ( ! self::is_available() ) {
204 return true;
205 }
206
207 // without a real core count the effective threshold
208 // (assumed 1 core × per-core limit) is meaningless. On managed
209 // hosts that block /proc/cpuinfo and shell_exec, sys_getloadavg()
210 // reports the whole shared machine's load — which normally sits
211 // far above a 1-core threshold — so the gate would block every
212 // job forever. A gate that cannot measure must not drop work:
213 // fail open.
214 if ( ! self::is_core_detection_reliable() ) {
215 return true;
216 }
217
218 $load = self::get_load_average();
219 if ( $load === false ) {
220 return true; // Error: assume safe
221 }
222
223 $threshold = self::get_effective_threshold();
224 if ( $load > $threshold ) {
225 self::record_deferral( $load );
226 return false;
227 }
228
229 return true;
230 }
231
232 /**
233 * Record a deferral event and update statistics
234 *
235 * Updates persistent stats (deferrals count, max load, running average).
236 * Sets a transient notice for display to admins (5-minute TTL).
237 *
238 * @param float $load The current load average
239 */
240 public static function record_deferral( $load ) {
241 $stats = get_option( self::STATS_OPTION_KEY, array(
242 'deferrals' => 0,
243 'max_load' => 0.0,
244 'total_load' => 0.0,
245 'sample_count' => 0,
246 ) );
247
248 // Increment deferral counter
249 $stats['deferrals']++;
250
251 // Track maximum load observed
252 $stats['max_load'] = max( (float) $stats['max_load'], $load );
253
254 // Update running sum for average calculation
255 $stats['total_load'] += $load;
256 $stats['sample_count']++;
257
258 // Timestamp of the most recent deferral, so consumers can tell an
259 // active load problem from a historic spike (the counter is lifetime).
260 $stats['last_deferral'] = time();
261
262 // Store updated stats (autoload=false to reduce options table bloat)
263 update_option( self::STATS_OPTION_KEY, $stats, false );
264
265 // Set admin notice transient (5-minute TTL per requirements)
266 set_transient( self::DEFER_NOTICE_TRANSIENT, array(
267 'load' => round( $load, 2 ),
268 'threshold' => round( self::get_effective_threshold(), 2 ),
269 'cores' => self::get_cpu_core_count(),
270 'time' => time(),
271 ), 300 );
272 }
273
274 /**
275 * Get current CPU statistics
276 *
277 * @return array Statistics including deferrals, max_load, avg_load, sample_count
278 */
279 public static function get_stats() {
280 $stats = get_option( self::STATS_OPTION_KEY, array(
281 'deferrals' => 0,
282 'max_load' => 0.0,
283 'total_load' => 0.0,
284 'sample_count' => 0,
285 ) );
286
287 // Calculate average load
288 $avg_load = $stats['sample_count'] > 0 ? $stats['total_load'] / $stats['sample_count'] : 0.0;
289
290 return array(
291 'deferrals' => (int) $stats['deferrals'],
292 'max_load' => (float) $stats['max_load'],
293 'avg_load' => round( $avg_load, 2 ),
294 'sample_count' => (int) $stats['sample_count'],
295 'last_deferral' => (int) ( $stats['last_deferral'] ?? 0 ),
296 );
297 }
298
299 /**
300 * Reset CPU statistics to default values
301 */
302 public static function reset_stats() {
303 delete_option( self::STATS_OPTION_KEY );
304 }
305 }
306