PluginProbe
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript / 4.0.29
BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript v4.0.29
4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 4.1.10 4.0.30 4.0.29 4.0.28 4.0.27 4.0.26 4.0.24 4.0.25 4.0.23 4.0.22 4.0.21 4.0.19 4.0.18 4.0.17 4.0.16 1.9.3 1.9.4 1.9.5 1.9.6 All 170 releases
searchpro / inc / queue / class-berqHeartbeat.php

class-berqHeartbeat.php in BerqWP – All-In-One Optimization for Core Web Vitals, Cache, CDN, Images, CSS & JavaScript 4.0.29, at inc/queue/class-berqHeartbeat.php

500 lines 14.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 class berqHeartbeat
4 {
5
6 private string $queue_key = 'berqwp_optimize_queue';
7 private string $stats_key = 'berqwp_optimize_stats';
8 private $timeout_limit = 120;
9 private string $lock_file;
10
11 public function __construct()
12 {
13 $this->lock_file = WP_CONTENT_DIR . '/cache/berqwp/.lock';
14 add_action('wp_footer', [$this, 'inject_heartbeat'], 999);
15 add_action('admin_footer', [$this, 'inject_heartbeat'], 999);
16 add_action('wp_ajax_berqwp_heartbeat', [$this, 'handle_heartbeat']);
17 add_action('wp_ajax_nopriv_berqwp_heartbeat', [$this, 'handle_heartbeat']);
18 }
19
20 public static function add_queue($page_url)
21 {
22 $queue = get_option('berqwp_optimize_queue', []);
23 $key = md5($page_url);
24
25 // If already in queue, increase priority
26 if (isset($queue[$key])) {
27 global $berq_log;
28 $berq_log->info('Increasing page priority ' . $page_url);
29
30 $queue[$key]['priority'] = max($queue[$key]['priority'] - 1, 1);
31
32 update_option('berqwp_optimize_queue', $queue, false);
33 return;
34 }
35
36 global $berq_log;
37 $berq_log->info('Adding ' . $page_url);
38 $queue[$key] = [
39 'url' => $page_url,
40 'added' => time(),
41 'priority' => $page_url == home_url('/') ? 1 : 5,
42 'attempts' => 0
43 ];
44
45 update_option('berqwp_optimize_queue', $queue, false);
46 }
47
48 /**
49 * Inject heartbeat script
50 */
51 public function inject_heartbeat(): void
52 {
53
54 if (empty(get_option($this->queue_key, [])) && empty(get_option('berqwp_server_queue', []))) {
55 return;
56 }
57
58 $url = $this->get_current_url();
59 $cache_key = 'bwph_' . md5($url);
60 $is_cached = (bool) get_transient($cache_key);
61
62 $config = [
63 'ajaxUrl' => admin_url('admin-ajax.php'),
64 'nonce' => wp_create_nonce('berqwp_heartbeat'),
65 'interval' => $this->get_dynamic_interval(),
66 'currentUrl' => $url,
67 'isCached' => $is_cached
68 ];
69
70 ?>
71 <script id="bwp-heartbeat">
72 (function() {
73 var config = <?php echo wp_json_encode($config); ?>;
74 var active = true;
75 var failures = 0;
76 var maxFailures = 3;
77 var beatCount = 0;
78 var maxBeats = 50; // Stop after 50 beats per page view
79 let making_request = false;
80 const controllers = [];
81
82 // Visibility tracking - pause when tab hidden
83 document.addEventListener('visibilitychange', function() {
84 active = !document.hidden;
85
86 });
87
88 window.addEventListener('beforeunload', () => {
89 controllers.forEach(c => c.abort());
90 });
91
92 // Start heartbeat after page load
93 if (document.readyState === 'complete') {
94 init();
95 } else {
96 document.addEventListener('DOMContentLoaded', init);
97 }
98
99 function init() {
100 beat();
101 // Delay first beat
102 // setTimeout(beat, 3000 + Math.random() * 2000);
103 }
104
105 function beat() {
106
107 if (!active) {
108 scheduleNext();
109 return;
110 }
111
112 if (failures >= maxFailures || beatCount >= maxBeats) {
113 return;
114 }
115
116 if (making_request) return;
117 making_request = true
118
119 const controller = new AbortController();
120 controllers.push(controller);
121
122 beatCount++;
123
124 var data = new FormData();
125 data.append('action', 'berqwp_heartbeat');
126 data.append('nonce', config.nonce);
127 data.append('url', config.currentUrl);
128 data.append('cached', config.isCached ? '1' : '0');
129
130 fetch(config.ajaxUrl, {
131 signal: controller.signal,
132 method: 'POST',
133 body: data,
134 credentials: 'same-origin',
135 keepalive: true
136 })
137 .then(function(r) {
138 return r.json();
139 })
140 .then(function(response) {
141 failures = 0;
142
143 if (response.success && response.data) {
144 // Update interval based on server load
145 if (response.data.interval) {
146 config.interval = response.data.interval;
147 }
148
149 // Stop if server says so
150 if (response.data.stop) {
151 return;
152 }
153
154 // Update cache status
155 if (response.data.cached) {
156 config.isCached = true;
157 }
158 }
159
160 making_request = false;
161
162 // Schedule next beat
163 scheduleNext();
164 })
165 .catch(function() {
166 failures++;
167 making_request = false;
168 scheduleNext();
169 });
170 }
171
172 function scheduleNext() {
173 if (failures < maxFailures && beatCount < maxBeats) {
174 // Add jitter to prevent thundering herd
175 var jitter = Math.random() * 5000;
176 setTimeout(beat, config.interval + jitter);
177 }
178 }
179 })();
180 </script>
181 <?php
182 }
183
184 /**
185 * Handle heartbeat request
186 */
187 public function handle_heartbeat(): void
188 {
189 // Verify nonce
190 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'berqwp_heartbeat')) {
191 wp_send_json_error('Invalid nonce');
192 }
193
194 $url = esc_url_raw($_POST['url'] ?? '');
195 $is_cached = (bool) ($_POST['cached'] ?? false);
196
197 // Update stats
198 $this->record_beat();
199
200 // Get current load
201 $load = $this->get_current_load();
202
203 // Calculate response
204 $response = [
205 'interval' => $this->get_dynamic_interval(),
206 'stop' => false,
207 'cached' => $is_cached,
208 'load' => $load
209 ];
210
211 if (empty(get_option($this->queue_key, [])) && empty(get_option('berqwp_server_queue', []))) {
212 $response['stop'] = true;
213 wp_send_json_success($response);
214 return;
215 }
216
217 // Under heavy load, tell some clients to stop
218 if ($load > 80 && mt_rand(1, 100) <= 50) {
219 $response['stop'] = true;
220 wp_send_json_success($response);
221 return;
222 }
223
224 // Try to process queue if not locked
225 if ($this->acquire_lock()) {
226
227 $response['lock_acquired'] = true;
228
229 try {
230 $this->process_queue();
231 } finally {
232 $this->release_lock();
233 }
234
235 // Send response first
236 wp_send_json_success($response);
237
238 // Close connection
239 $this->close_connection();
240 } else {
241
242 $response['lock_acquired'] = false;
243 wp_send_json_success($response);
244 }
245 }
246
247 /**
248 * Add URL to optimization queue
249 */
250 private function add_to_queue(string $url): void
251 {
252 $queue = get_option($this->queue_key, []);
253 $key = md5($url);
254
255 if (!isset($queue[$key])) {
256 $queue[$key] = [
257 'url' => $url,
258 'added' => time(),
259 'priority' => 5,
260 'attempts' => 0
261 ];
262
263 // Keep queue size manageable
264 if (count($queue) > 100) {
265 // Remove oldest items
266 uasort($queue, fn($a, $b) => $a['added'] - $b['added']);
267 $queue = array_slice($queue, -100, null, true);
268 }
269
270 update_option($this->queue_key, $queue, false);
271 }
272 }
273
274 /**
275 * Process optimization queue
276 */
277 private function process_queue(): void
278 {
279 $queue = get_option($this->queue_key, []);
280
281 berqUpload::request_pending_cache();
282
283 if (empty($queue)) {
284 return;
285 }
286
287 $queue = array_filter($queue, fn($item) => !empty($item['url']) && strpos($item['url'], '?') === false);
288
289 // // Check load
290 // if ($this->get_current_load() > 90) {
291 // return;
292 // }
293
294 set_time_limit($this->timeout_limit);
295
296 // $server_queue = get_option('berqwp_server_queue', []);
297 // $queue = array_filter($queue, function ($item) use ($server_queue) {
298 // return !empty($item) && !in_array($item['url'], $server_queue);
299 // });
300
301 // Sort by priority
302 uasort($queue, fn($a, $b) => $a['priority'] - $b['priority']);
303
304 // remove active pages
305 $pending_queue = array_filter($queue, function ($item) {
306 return empty($item['status']) || $item['status'] !== 'active';
307 });
308
309 if (!empty($pending_queue)) {
310
311 // Process one item
312 $key = array_key_first($pending_queue);
313 $item = $queue[$key];
314
315 if (!empty($item)) {
316 try {
317
318 global $berq_log;
319 $berq_log->info("Doing heartbeat request");
320
321 // Delegate to optimize.php for processing and upload
322 $result = berqUpload::process_page($item['url']);
323
324 if (!isset($result['success'])) {
325 throw new Exception($result['error'] ?? 'Processing failed');
326 }
327
328 // Success - remove from queue
329 unset($queue[$key]);
330
331 } catch (Exception $e) {
332
333 global $berq_log;
334 $berq_log->info("Heartbeat page {$item['url']} failed: {$e->getMessage()}");
335
336 $queue[$key]['url'] = $item['url'];
337 $queue[$key]['status'] = 'pending';
338 $queue[$key]['attempts']++;
339
340 } catch (Throwable $e) {
341
342 global $berq_log;
343 $berq_log->info("Heartbeat page {$item['url']} failed: {$e->getMessage()}");
344
345 $queue[$key]['url'] = $item['url'];
346 $queue[$key]['status'] = 'pending';
347 $queue[$key]['attempts']++;
348 }
349 }
350 }
351
352 // Retry timeout items
353 $queue = array_map(function ($item) {
354
355 if (!empty($item) && !empty($item['status']) && $item['status'] == 'active' && time() > ($item['added'] + $this->timeout_limit)) {
356 $item['status'] = 'pending';
357 $item['added'] = time();
358 }
359
360 return $item;
361 }, $queue);
362
363 // Remove items with too many attempts
364 $queue = array_filter($queue, fn($item) => $item['attempts'] < 3);
365
366 update_option($this->queue_key, $queue, false);
367 }
368
369 /**
370 * Get dynamic interval based on server load
371 */
372 private function get_dynamic_interval(): int
373 {
374 $load = $this->get_current_load();
375
376 // Base interval: 15 seconds
377 // Scale up to 60 seconds under heavy load
378 if ($load > 90) {
379 return 60000;
380 } elseif ($load > 70) {
381 return 45000;
382 } elseif ($load > 50) {
383 return 30000;
384 } elseif ($load > 30) {
385 return 20000;
386 }
387
388 return 15000;
389 }
390
391 /**
392 * Record heartbeat for stats
393 */
394 private function record_beat(): void
395 {
396 $stats = get_transient($this->stats_key) ?: [
397 'beats' => [],
398 'minute_count' => 0
399 ];
400
401 $current_minute = floor(time() / 60);
402
403 // Clean old data (keep last 5 minutes)
404 $stats['beats'] = array_filter(
405 $stats['beats'],
406 fn($timestamp) => $timestamp > time() - 300
407 );
408
409 $stats['beats'][] = time();
410 $stats['minute_count'] = count(array_filter(
411 $stats['beats'],
412 fn($t) => $t > time() - 60
413 ));
414
415 set_transient($this->stats_key, $stats, 600);
416 }
417
418 /**
419 * Get current load (0-100)
420 */
421 private function get_current_load(): int
422 {
423 $stats = get_transient($this->stats_key);
424
425 if (!$stats) {
426 return 0;
427 }
428
429 $beats_per_minute = $stats['minute_count'] ?? 0;
430
431 // Define thresholds
432 $max_beats = (int) get_option('bwph_max_beats_per_minute', 100);
433
434 return min(100, (int) (($beats_per_minute / $max_beats) * 100));
435 }
436
437 /**
438 * Acquire processing lock (file-based)
439 */
440 private function acquire_lock(): bool
441 {
442 if (file_exists($this->lock_file)) {
443 if (filemtime($this->lock_file) > (time() - $this->timeout_limit)) {
444 return false;
445 }
446 @unlink($this->lock_file);
447 }
448
449 $dir = dirname($this->lock_file);
450 if (!is_dir($dir)) {
451 wp_mkdir_p($dir);
452 }
453
454 return (bool) @file_put_contents($this->lock_file, time(), LOCK_EX);
455 }
456
457 /**
458 * Release processing lock
459 */
460 private function release_lock(): void
461 {
462 @unlink($this->lock_file);
463 }
464
465 /**
466 * Close connection and continue processing
467 */
468 private function close_connection(): void
469 {
470 if (function_exists('fastcgi_finish_request')) {
471 fastcgi_finish_request();
472 return;
473 }
474
475 // Fallback
476 ignore_user_abort(true);
477
478 if (ob_get_level() > 0) {
479 ob_end_clean();
480 }
481
482 header('Connection: close');
483 header('Content-Encoding: none');
484 header('Content-Length: 0');
485
486 ob_start();
487 echo ' ';
488 ob_end_flush();
489 flush();
490 }
491
492 private function get_current_url(): string
493 {
494 $protocol = is_ssl() ? 'https://' : 'http://';
495 return $protocol . sanitize_text_field(wp_unslash($_SERVER['HTTP_HOST'] ?? '')) . strtok(sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'] ?? '/')), '?');
496 }
497 }
498
499 // add_action('init', fn() => new berqHeartbeat());
500