core = $core; $this->logger = $logger; $this->taxonomy = new ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy($functions); $this->tableInspector = new ABJ_404_Solution_DatabaseErrorTableInspector($logger); $this->prefixDiagnostics = new ABJ_404_Solution_DatabasePrefixDiagnostics($core, $logger); } /** @return ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy */ public function taxonomy(): ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy { return $this->taxonomy; } /** @return ABJ_404_Solution_DatabaseErrorTableInspector */ public function tableInspector(): ABJ_404_Solution_DatabaseErrorTableInspector { return $this->tableInspector; } /** @return ABJ_404_Solution_DatabasePrefixDiagnostics */ public function prefixDiagnostics(): ABJ_404_Solution_DatabasePrefixDiagnostics { return $this->prefixDiagnostics; } /** * Classify and handle a host-side database issue from direct wpdb call * sites that bypass queryAndGetResults(). * * @param string $errorText * @return bool True when the text matched an infrastructure error and * notice-state side effects were applied. */ public function classifyAndHandleInfrastructureError(string $errorText): bool { if ($errorText === '') { return false; } if ($this->taxonomy->isInfrastructureSqlError($errorText)) { $this->logger->warn("Server-side DB issue (handled): " . $errorText); $this->noteDatabaseIssueFromError($errorText); return true; } return false; } /** * Apply notice and runtime-flag side effects for a recognized * infrastructure-error string. Sets write-block / quota-cooldown runtime * flags so subsequent write attempts short-circuit, and registers a * plugin-admin notice describing the situation. * * @param string $errorText * @return void */ public function noteDatabaseIssueFromError(string $errorText): void { if (trim($errorText) === '') { return; } if ($this->taxonomy->hostState()->isDiskFullError($errorText)) { $this->core->noticeState()->markServerSideIssueNoted(); $this->core->noticeState()->setRuntimeFlag('abj404_db_disk_full_until', $this->core->clock()->now() + self::DB_WRITE_BLOCK_COOLDOWN_SECONDS, self::DB_WRITE_BLOCK_COOLDOWN_SECONDS); $tableFull = stripos($errorText, 'table') !== false && stripos($errorText, 'is full') !== false; if ($tableFull) { $tableName = $this->tableInspector->extractTableNameFromFullError($errorText); if ($tableName !== null && $this->tableInspector->isInnoDBTable($tableName)) { $this->core->noticeState()->setPluginDbNotice( 'disk_full', function_exists('__') ? __('The InnoDB tablespace appears to be exhausted. Deleting plugin data will NOT free this space. Contact your hosting provider to expand the InnoDB tablespace (ibdata1).', '404-solution') : 'The InnoDB tablespace appears to be exhausted. Deleting plugin data will NOT free this space. Contact your hosting provider to expand the InnoDB tablespace (ibdata1).', function_exists('__') ? __('Contact your hosting provider. This is usually caused by a database quota, tablespace limit, or full /tmp partition - not necessarily a full disk.', '404-solution') : 'Contact your hosting provider. This is usually caused by a database quota, tablespace limit, or full /tmp partition - not necessarily a full disk.', $errorText ); return; } } $this->core->noticeState()->setPluginDbNotice( 'disk_full', function_exists('__') ? __('Database storage appears full (disk/engine space). Plugin write-heavy tasks are temporarily paused.', '404-solution') : 'Database storage appears full (disk/engine space). Plugin write-heavy tasks are temporarily paused.', function_exists('__') ? __('Contact your hosting provider. This is usually caused by a database quota, tablespace limit, or full /tmp partition - not necessarily a full disk.', '404-solution') : 'Contact your hosting provider. This is usually caused by a database quota, tablespace limit, or full /tmp partition - not necessarily a full disk.', $errorText ); return; } if ($this->taxonomy->hostState()->isQuotaLimitError($errorText)) { $this->core->noticeState()->markServerSideIssueNoted(); $this->core->noticeState()->setRuntimeFlag('abj404_db_quota_cooldown_until', $this->core->clock()->now() + self::DB_QUOTA_COOLDOWN_SECONDS, self::DB_QUOTA_COOLDOWN_SECONDS); $this->core->noticeState()->setPluginDbNotice( 'query_quota', function_exists('__') ? __('Database query quota was exceeded (for example max_questions). Non-essential plugin background tasks are temporarily paused.', '404-solution') : 'Database query quota was exceeded (for example max_questions). Non-essential plugin background tasks are temporarily paused.', function_exists('__') ? __('Your database query quota was exceeded. This usually resets automatically.', '404-solution') : 'Your database query quota was exceeded. This usually resets automatically.', $errorText ); return; } if ($this->taxonomy->hostState()->isReadOnlyError($errorText)) { $this->core->noticeState()->markServerSideIssueNoted(); $this->core->noticeState()->setRuntimeFlag('abj404_db_read_only_until', $this->core->clock()->now() + self::DB_WRITE_BLOCK_COOLDOWN_SECONDS, self::DB_WRITE_BLOCK_COOLDOWN_SECONDS); $this->core->noticeState()->setPluginDbNotice( 'read_only', function_exists('__') ? __('Database appears to be in read-only mode. Plugin write operations are temporarily paused.', '404-solution') : 'Database appears to be in read-only mode. Plugin write operations are temporarily paused.', function_exists('__') ? __('Your database is currently in read-only mode. Contact your hosting provider.', '404-solution') : 'Your database is currently in read-only mode. Contact your hosting provider.', $errorText ); return; } if ($this->taxonomy->schema()->isCollationError($errorText)) { $this->logger->debugMessage("Collation mismatch detected (background repair will be scheduled): " . $errorText); } } /** * True when a prior quota-exceeded error is still inside its cooldown * window, so callers should skip non-essential queries. * * @return bool */ public function isQuotaCooldownActive(): bool { $rawQuotaFlag = $this->core->noticeState()->getRuntimeFlag('abj404_db_quota_cooldown_until'); $until = is_scalar($rawQuotaFlag) ? (int)$rawQuotaFlag : 0; return ($until > $this->core->clock()->now()); } }