PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
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
← All changes | includes/class-metasync-heartbeat-manager.php +62 -2 2.6.212.7.0 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,