PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 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 All 141 releases
← All changes | includes/class-metasync-heartbeat-manager.php +72 -6 2.6.19 → 2.7.1 View file →
@@ -142,8 +142,37 @@
142 142
143 143 $api_url = $this->build_otto_api_url($otto_pixel_uuid);
144 144 $headers = $this->prepare_api_headers($jwt_token);
145 145
146 + # Bail before the retry loop if the host is already known to be bad.
147 + #
148 + # Backoff case: without this, all three attempts are rejected instantly by the
149 + # pre_http_request filter, but the loop still sleeps 1s + 2s between them,
150 + # holding a PHP worker for 3s to accomplish nothing. Measured at 3.01s.
151 + #
152 + # Breaker case: the retry loop's worst case against a hanging endpoint is
153 + # 3 x 15s timeout + 1s + 2s of sleep = 48s of one worker. The suggestions
154 + # path runs on the same host, so if its breaker has tripped we already know
155 + # this call cannot succeed. This only helps when page traffic has tripped the
156 + # breaker first — an admin loading this page on an idle site still pays the
157 + # full 48s, which is why the retry budget itself is flagged for follow-up.
158 + #
159 + # Both helpers are checked with class_provides() rather than class_exists():
160 + # they live in files separate from this one, so a partially updated install
161 + # can leave a previous copy of either class loaded that predates the method
162 + # being called. See class_provides() for why that matters here.
163 + $endpoint_unhealthy = (self::class_provides('Metasync_API_Backoff_Manager', 'is_endpoint_in_backoff')
164 + && Metasync_API_Backoff_Manager::get_instance()->is_endpoint_in_backoff($api_url))
165 + || (self::class_provides('Metasync_Otto_Transient_Cache', 'is_host_breaker_open')
166 + && Metasync_Otto_Transient_Cache::is_host_breaker_open($api_url));
167 +
168 + if ($endpoint_unhealthy) {
169 + $this->log_fetch_hash_error('error', 'Public hash fetch skipped - endpoint unhealthy', [
170 + 'uuid' => substr($otto_pixel_uuid, 0, 8) . '...'
171 + ]);
172 + return false;
173 + }
174 +
146 175 for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
147 176 $this->log_fetch_hash_error('info', 'Attempting to fetch public hash from API', [
148 177 'attempt' => $attempt,
149 178 'max_retries' => $max_retries,
@@ -185,8 +214,15 @@
185 214 'error_message' => $error_message,
186 215 'will_retry' => $attempt < $max_retries
187 216 ]);
188 217
218 + # A backoff block is not a transient network blip — the
219 + # endpoint is deliberately closed for minutes. Retrying (and
220 + # sleeping between retries) can only burn worker time.
221 + if ($error_code === 'api_backoff_active') {
222 + return false;
223 + }
224 +
189 225 if ($attempt < $max_retries) {
190 226 $this->apply_exponential_backoff($attempt, $base_retry_delay);
191 227 continue;
192 228 }
@@ -224,8 +260,32 @@
224 260 // ------------------------------------------------------------------
225 261 // Public-hash helpers (private)
226 262 // ------------------------------------------------------------------
227 263
264 + /**
265 + * Is $method actually available on $class in this process right now?
266 + *
267 + * A partially updated install can leave a newer copy of one plugin file
268 + * beside an older copy of another — stale opcache bytecode for a single file
269 + * is enough. class_exists() is satisfied by the older copy, and calling a
270 + * method it does not declare is a fatal: that is how the health checks in
271 + * fetch_public_hash() brought down the whole Dashboard admin page with
272 + * "Call to undefined method ...::is_host_breaker_open()".
273 + *
274 + * $class and $method are parameters rather than literals at the call site so
275 + * the check survives static analysis, which would otherwise narrow a literal
276 + * method_exists() on a known class to a constant true — the skew this guards
277 + * against exists only at runtime.
278 + *
279 + * @param string $class Class about to be called.
280 + * @param string $method Method about to be called on it.
281 + * @return bool
282 + */
283 + private static function class_provides($class, $method)
284 + {
285 + return class_exists($class) && method_exists($class, $method);
286 + }
287 +
228 288 private function validate_fetch_hash_inputs($uuid, $token)
229 289 {
230 290 if (empty($uuid) || empty($token)) {
231 291 return false;
@@ -332,9 +392,9 @@
332 392 if (class_exists('Metasync_Error_Logger')) {
333 393 Metasync_Error_Logger::log(
334 394 Metasync_Error_Logger::CATEGORY_API_RATE_LIMIT,
335 395 Metasync_Error_Logger::SEVERITY_WARNING,
336 - 'OTTO API rate limit exceeded',
396 + 'OTTO API throttled upstream',
337 397 [
338 398 'status_code' => $status_code,
339 399 'attempt' => $attempt,
340 400 'will_retry' => $attempt < $max_retries,
@@ -439,9 +499,9 @@
439 499 if (class_exists('Metasync_Error_Logger')) {
440 500 Metasync_Error_Logger::log(
441 501 Metasync_Error_Logger::CATEGORY_API_BACKOFF,
442 502 Metasync_Error_Logger::SEVERITY_INFO,
443 - 'API backoff active - applying exponential retry delay',
503 + 'Retry scheduled - applying backoff delay',
444 504 [
445 505 'attempt' => $attempt,
446 506 'delay_seconds' => $delay,
447 507 'base_delay' => $base_delay,
@@ -492,9 +552,9 @@
492 552 }
493 553 $log_message .= ' | ' . implode(', ', $context_parts);
494 554 }
495 555
496 - error_log($log_message);
556 + error_log($log_message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
497 557 }
498 558
499 559 public function clear_public_hash_cache($otto_pixel_uuid = '')
500 560 {
@@ -566,9 +626,9 @@
566 626 }
567 627 $message .= ' | ' . implode(', ', $details_formatted);
568 628 }
569 629
570 - error_log($message);
630 + error_log($message); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
571 631
572 632 if ($level === 'error' || $level === 'critical') {
573 633 $this->store_heartbeat_error_log($event, $details);
574 634 }
@@ -586,9 +646,9 @@
586 646 if ($last_space !== false && $last_space > $length * 0.75) {
587 647 $truncated = substr($truncated, 0, $last_space);
588 648 }
589 649
590 - $truncated = strip_tags($truncated);
650 + $truncated = wp_strip_all_tags($truncated);
591 651 return $truncated . '... [truncated]';
592 652 }
593 653
594 654 private function should_throttle_log($level, $event, $details = array())
@@ -635,9 +695,9 @@
635 695 'created_at' => current_time('mysql')
636 696 ));
637 697 }
638 698 } catch (Exception $e) {
639 - error_log('Failed to store heartbeat error log: ' . $e->getMessage());
699 + error_log('Failed to store heartbeat error log: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
640 700 }
641 701 }
642 702
643 703 // ------------------------------------------------------------------
@@ -927,32 +987,38 @@
927 987 public function add_heartbeat_cron_schedule($schedules)
928 988 {
929 989 $schedules['metasync_every_2_hours'] = array(
930 990 'interval' => 2 * HOUR_IN_SECONDS,
991 + /* translators: %s: effective plugin name (whitelabel-aware). */
931 992 'display' => esc_html(sprintf(__('Every 2 Hours (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
932 993 );
933 994
934 995 $schedules['metasync_every_2_minutes'] = array(
935 996 'interval' => 2 * MINUTE_IN_SECONDS,
997 + /* translators: %s: effective plugin name (whitelabel-aware). */
936 998 'display' => esc_html(sprintf(__('Every 2 Minutes (%s Burst)', 'metasync'), Metasync::get_effective_plugin_name()))
937 999 );
938 1000 $schedules['metasync_every_5_minutes'] = array(
939 1001 'interval' => 5 * MINUTE_IN_SECONDS,
1002 + /* translators: %s: effective plugin name (whitelabel-aware). */
940 1003 'display' => esc_html(sprintf(__('Every 5 Minutes (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
941 1004 );
942 1005 // 10-minute heartbeat cadence used for UNREGISTERED + KEY_PENDING short-interval behavior
943 1006 $schedules['metasync_every_10_minutes'] = array(
944 1007 'interval' => 10 * MINUTE_IN_SECONDS,
1008 + /* translators: %s: effective plugin name (whitelabel-aware). */
945 1009 'display' => esc_html(sprintf(__('Every 10 Minutes (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
946 1010 );
947 1011
948 1012 $schedules['metasync_daily_cleanup'] = array(
949 1013 'interval' => DAY_IN_SECONDS,
1014 + /* translators: %s: effective plugin name (whitelabel-aware). */
950 1015 'display' => esc_html(sprintf(__('Daily (%s Cleanup)', 'metasync'), Metasync::get_effective_plugin_name()))
951 1016 );
952 1017
953 1018 $schedules['metasync_weekly'] = array(
954 1019 'interval' => 7 * DAY_IN_SECONDS,
1020 + /* translators: %s: effective plugin name (whitelabel-aware). */
955 1021 'display' => esc_html(sprintf(__('Weekly (%s)', 'metasync'), Metasync::get_effective_plugin_name()))
956 1022 );
957 1023
958 1024 return $schedules;