PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / database / DatabaseErrorClassifier.php

DatabaseErrorClassifier.php in 404 Solution trunk, at includes/database/DatabaseErrorClassifier.php

217 lines 10.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 require_once __DIR__ . '/DatabaseTableNameResolver.php';
7 require_once __DIR__ . '/DatabaseInfrastructureErrorTaxonomy.php';
8 require_once __DIR__ . '/DatabaseErrorTableInspector.php';
9 require_once __DIR__ . '/DatabasePrefixDiagnostics.php';
10
11 /**
12 * Coordinates database error response: applies notice and runtime-flag side
13 * effects on top of focused error-classification collaborators.
14 *
15 * The pure error vocabulary lives in the three collaborators exposed via
16 * accessor methods:
17 * - taxonomy(): string-pattern matchers (is*Error()).
18 * - tableInspector(): table name extraction + InnoDB engine probe.
19 * - prefixDiagnostics(): prefix-mismatch + multisite cross-prefix detection.
20 *
21 * This class itself owns only the side-effecting coordination: noting an
22 * issue against notice state and runtime flags, gating writes via the quota
23 * cooldown, and dispatching the infrastructure-error entry point that direct
24 * wpdb sites use to bypass queryAndGetResults().
25 *
26 * @since 4.1.0
27 */
28
29 class ABJ_404_Solution_DatabaseErrorClassifier {
30
31 /** @var int Cooldown when DB query quota is exceeded. */
32 const DB_QUOTA_COOLDOWN_SECONDS = ABJ_404_Solution_DatabaseRuntimeState::DB_QUOTA_COOLDOWN_SECONDS;
33 /** @var int Cooldown when DB is read-only or storage is full. */
34 const DB_WRITE_BLOCK_COOLDOWN_SECONDS = ABJ_404_Solution_DatabaseRuntimeState::DB_WRITE_BLOCK_COOLDOWN_SECONDS;
35
36 /** @var ABJ_404_Solution_DatabaseCore */
37 private $core;
38
39 /** @var ABJ_404_Solution_Logging */
40 private $logger;
41
42 /** @var ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy */
43 private $taxonomy;
44
45 /** @var ABJ_404_Solution_DatabaseErrorTableInspector */
46 private $tableInspector;
47
48 /** @var ABJ_404_Solution_DatabasePrefixDiagnostics */
49 private $prefixDiagnostics;
50
51 /**
52 * @param ABJ_404_Solution_DatabaseCore $core
53 * @param ABJ_404_Solution_Functions $functions
54 * @param ABJ_404_Solution_Logging $logger
55 */
56 public function __construct(ABJ_404_Solution_DatabaseCore $core, $functions, $logger) {
57 $this->core = $core;
58 $this->logger = $logger;
59 $this->taxonomy = new ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy($functions);
60 $this->tableInspector = new ABJ_404_Solution_DatabaseErrorTableInspector(
61 $logger,
62 function (string $tableName) use ($core): bool {
63 return $core->tableNameResolver()->tableExistenceStatus($tableName)
64 === ABJ_404_Solution_DatabaseTableNameResolver::TABLE_ABSENT;
65 }
66 );
67 $this->prefixDiagnostics = new ABJ_404_Solution_DatabasePrefixDiagnostics($core, $logger);
68 }
69
70 /** @return ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy */
71 public function taxonomy(): ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy {
72 return $this->taxonomy;
73 }
74
75 /** @return ABJ_404_Solution_DatabaseErrorTableInspector */
76 public function tableInspector(): ABJ_404_Solution_DatabaseErrorTableInspector {
77 return $this->tableInspector;
78 }
79
80 /** @return ABJ_404_Solution_DatabasePrefixDiagnostics */
81 public function prefixDiagnostics(): ABJ_404_Solution_DatabasePrefixDiagnostics {
82 return $this->prefixDiagnostics;
83 }
84
85 /** Whether a failed statement is safe to retry as transient contention. */
86 public function isDeadlockOrLockTimeoutError(string $errorText): bool {
87 return $this->taxonomy->connectivity()->isDeadlockOrLockTimeoutError($errorText);
88 }
89
90 /** Whether the failed DDL asks for schema state that already exists. */
91 public function isRedundantSchemaChangeError(string $errorText): bool {
92 return $this->taxonomy->schema()->isRedundantSchemaChangeError($errorText);
93 }
94
95 /**
96 * Whether the error is a host/infrastructure failure.
97 *
98 * Most families are pure string taxonomy. A missing WordPress core table
99 * needs positive database evidence: it is infrastructure only when the
100 * authoritative table name WordPress exposes is also confirmed absent.
101 */
102 public function isInfrastructureSqlError(string $errorText): bool {
103 return $this->taxonomy->isInfrastructureSqlError($errorText)
104 || $this->tableInspector->isConfirmedMissingWordPressTableError($errorText);
105 }
106
107 /** Whether the server reports a corrupt or unusable key file. */
108 public function isIncorrectKeyFileError(string $errorText): bool {
109 return $this->taxonomy->schema()->isIncorrectKeyFileError($errorText);
110 }
111
112 /** Whether the server rejected data that cannot fit the target schema. */
113 public function isInvalidDataError(string $errorText): bool {
114 return $this->taxonomy->schema()->isInvalidDataError($errorText);
115 }
116
117 /**
118 * Classify and handle a host-side database issue from direct wpdb call
119 * sites that bypass queryAndGetResults().
120 *
121 * @param string $errorText
122 * @return bool True when the text matched an infrastructure error and
123 * notice-state side effects were applied.
124 */
125 public function classifyAndHandleInfrastructureError(string $errorText): bool {
126 if ($errorText === '') {
127 return false;
128 }
129
130 if ($this->isInfrastructureSqlError($errorText)) {
131 $this->logger->warn("Server-side DB issue (handled): " . $errorText);
132 $this->noteDatabaseIssueFromError($errorText);
133 return true;
134 }
135
136 return false;
137 }
138
139 /**
140 * Apply notice and runtime-flag side effects for a recognized
141 * infrastructure-error string. Sets write-block / quota-cooldown runtime
142 * flags so subsequent write attempts short-circuit, and registers a
143 * plugin-admin notice describing the situation.
144 *
145 * @param string $errorText
146 * @return void
147 */
148 public function noteDatabaseIssueFromError(string $errorText): void {
149 if (trim($errorText) === '') {
150 return;
151 }
152 if ($this->taxonomy->hostState()->isDiskFullError($errorText)) {
153 $this->core->noticeState()->markServerSideIssueNoted();
154 $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);
155
156 $tableFull = stripos($errorText, 'table') !== false && stripos($errorText, 'is full') !== false;
157 if ($tableFull) {
158 $tableName = $this->tableInspector->extractTableNameFromFullError($errorText);
159 if ($tableName !== null && $this->tableInspector->isInnoDBTable($tableName)) {
160 $this->core->noticeState()->setPluginDbNotice(
161 'disk_full',
162 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).',
163 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.',
164 $errorText
165 );
166 return;
167 }
168 }
169
170 $this->core->noticeState()->setPluginDbNotice(
171 'disk_full',
172 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.',
173 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.',
174 $errorText
175 );
176 return;
177 }
178 if ($this->taxonomy->hostState()->isQuotaLimitError($errorText)) {
179 $this->core->noticeState()->markServerSideIssueNoted();
180 $this->core->noticeState()->setRuntimeFlag('abj404_db_quota_cooldown_until', $this->core->clock()->now() + self::DB_QUOTA_COOLDOWN_SECONDS, self::DB_QUOTA_COOLDOWN_SECONDS);
181 $this->core->noticeState()->setPluginDbNotice(
182 'query_quota',
183 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.',
184 function_exists('__') ? __('Your database query quota was exceeded. This usually resets automatically.', '404-solution') : 'Your database query quota was exceeded. This usually resets automatically.',
185 $errorText
186 );
187 return;
188 }
189 if ($this->taxonomy->hostState()->isReadOnlyError($errorText)) {
190 $this->core->noticeState()->markServerSideIssueNoted();
191 $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);
192 $this->core->noticeState()->setPluginDbNotice(
193 'read_only',
194 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.',
195 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.',
196 $errorText
197 );
198 return;
199 }
200 if ($this->taxonomy->schema()->isCollationError($errorText)) {
201 $this->logger->debugMessage("Collation mismatch detected (background repair will be scheduled): " . $errorText);
202 }
203 }
204
205 /**
206 * True when a prior quota-exceeded error is still inside its cooldown
207 * window, so callers should skip non-essential queries.
208 *
209 * @return bool
210 */
211 public function isQuotaCooldownActive(): bool {
212 $rawQuotaFlag = $this->core->noticeState()->getRuntimeFlag('abj404_db_quota_cooldown_until');
213 $until = is_scalar($rawQuotaFlag) ? (int)$rawQuotaFlag : 0;
214 return ($until > $this->core->clock()->now());
215 }
216 }
217