PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
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 4.3.0, at includes/database/DatabaseErrorClassifier.php

184 lines 9.4 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 /**
7 * Coordinates database error response: applies notice and runtime-flag side
8 * effects on top of focused error-classification collaborators.
9 *
10 * The pure error vocabulary lives in the four collaborators exposed via
11 * accessor methods:
12 * - taxonomy(): string-pattern matchers (is*Error()).
13 * - stagedFailures(): staged-build failure policy (classifyStageFailure() etc).
14 * - tableInspector(): table name extraction + InnoDB engine probe.
15 * - prefixDiagnostics(): prefix-mismatch + multisite cross-prefix detection.
16 *
17 * This class itself owns only the side-effecting coordination: noting an
18 * issue against notice state and runtime flags, gating writes via the quota
19 * cooldown, and dispatching the infrastructure-error entry point that direct
20 * wpdb sites use to bypass queryAndGetResults().
21 *
22 * @since 4.1.0
23 */
24
25 class ABJ_404_Solution_DatabaseErrorClassifier {
26
27 /** @var int Cooldown when DB query quota is exceeded. */
28 const DB_QUOTA_COOLDOWN_SECONDS = ABJ_404_Solution_DatabaseRuntimeState::DB_QUOTA_COOLDOWN_SECONDS;
29 /** @var int Cooldown when DB is read-only or storage is full. */
30 const DB_WRITE_BLOCK_COOLDOWN_SECONDS = ABJ_404_Solution_DatabaseRuntimeState::DB_WRITE_BLOCK_COOLDOWN_SECONDS;
31
32 /** @var ABJ_404_Solution_DatabaseCore */
33 private $core;
34
35 /** @var ABJ_404_Solution_Logging */
36 private $logger;
37
38 /** @var ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy */
39 private $taxonomy;
40
41 /** @var ABJ_404_Solution_DatabaseStagedFailureClassifier */
42 private $stagedFailures;
43
44 /** @var ABJ_404_Solution_DatabaseErrorTableInspector */
45 private $tableInspector;
46
47 /** @var ABJ_404_Solution_DatabasePrefixDiagnostics */
48 private $prefixDiagnostics;
49
50 /**
51 * @param ABJ_404_Solution_DatabaseCore $core
52 * @param ABJ_404_Solution_Functions $functions
53 * @param ABJ_404_Solution_Logging $logger
54 */
55 public function __construct(ABJ_404_Solution_DatabaseCore $core, $functions, $logger) {
56 $this->core = $core;
57 $this->logger = $logger;
58 $this->taxonomy = new ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy($functions);
59 $this->stagedFailures = new ABJ_404_Solution_DatabaseStagedFailureClassifier($this->taxonomy);
60 $this->tableInspector = new ABJ_404_Solution_DatabaseErrorTableInspector($logger);
61 $this->prefixDiagnostics = new ABJ_404_Solution_DatabasePrefixDiagnostics($core, $logger);
62 }
63
64 /** @return ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy */
65 public function taxonomy(): ABJ_404_Solution_DatabaseInfrastructureErrorTaxonomy {
66 return $this->taxonomy;
67 }
68
69 /** @return ABJ_404_Solution_DatabaseStagedFailureClassifier */
70 public function stagedFailures(): ABJ_404_Solution_DatabaseStagedFailureClassifier {
71 return $this->stagedFailures;
72 }
73
74 /** @return ABJ_404_Solution_DatabaseErrorTableInspector */
75 public function tableInspector(): ABJ_404_Solution_DatabaseErrorTableInspector {
76 return $this->tableInspector;
77 }
78
79 /** @return ABJ_404_Solution_DatabasePrefixDiagnostics */
80 public function prefixDiagnostics(): ABJ_404_Solution_DatabasePrefixDiagnostics {
81 return $this->prefixDiagnostics;
82 }
83
84 /**
85 * Classify and handle a host-side database issue from direct wpdb call
86 * sites that bypass queryAndGetResults().
87 *
88 * @param string $errorText
89 * @return bool True when the text matched an infrastructure error and
90 * notice-state side effects were applied.
91 */
92 public function classifyAndHandleInfrastructureError(string $errorText): bool {
93 if ($errorText === '') {
94 return false;
95 }
96
97 if ($this->taxonomy->isInfrastructureSqlError($errorText)) {
98 $this->logger->warn("Server-side DB issue (handled): " . $errorText);
99 $this->noteDatabaseIssueFromError($errorText);
100 return true;
101 }
102
103 return false;
104 }
105
106 /**
107 * Apply notice and runtime-flag side effects for a recognized
108 * infrastructure-error string. Sets write-block / quota-cooldown runtime
109 * flags so subsequent write attempts short-circuit, and registers a
110 * plugin-admin notice describing the situation.
111 *
112 * @param string $errorText
113 * @return void
114 */
115 public function noteDatabaseIssueFromError(string $errorText): void {
116 if (trim($errorText) === '') {
117 return;
118 }
119 if ($this->taxonomy->hostState()->isDiskFullError($errorText)) {
120 $this->core->noticeState()->markServerSideIssueNoted();
121 $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);
122
123 $tableFull = stripos($errorText, 'table') !== false && stripos($errorText, 'is full') !== false;
124 if ($tableFull) {
125 $tableName = $this->tableInspector->extractTableNameFromFullError($errorText);
126 if ($tableName !== null && $this->tableInspector->isInnoDBTable($tableName)) {
127 $this->core->noticeState()->setPluginDbNotice(
128 'disk_full',
129 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).',
130 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.',
131 $errorText
132 );
133 return;
134 }
135 }
136
137 $this->core->noticeState()->setPluginDbNotice(
138 'disk_full',
139 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.',
140 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.',
141 $errorText
142 );
143 return;
144 }
145 if ($this->taxonomy->hostState()->isQuotaLimitError($errorText)) {
146 $this->core->noticeState()->markServerSideIssueNoted();
147 $this->core->noticeState()->setRuntimeFlag('abj404_db_quota_cooldown_until', $this->core->clock()->now() + self::DB_QUOTA_COOLDOWN_SECONDS, self::DB_QUOTA_COOLDOWN_SECONDS);
148 $this->core->noticeState()->setPluginDbNotice(
149 'query_quota',
150 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.',
151 function_exists('__') ? __('Your database query quota was exceeded. This usually resets automatically.', '404-solution') : 'Your database query quota was exceeded. This usually resets automatically.',
152 $errorText
153 );
154 return;
155 }
156 if ($this->taxonomy->hostState()->isReadOnlyError($errorText)) {
157 $this->core->noticeState()->markServerSideIssueNoted();
158 $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);
159 $this->core->noticeState()->setPluginDbNotice(
160 'read_only',
161 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.',
162 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.',
163 $errorText
164 );
165 return;
166 }
167 if ($this->taxonomy->schema()->isCollationError($errorText)) {
168 $this->logger->debugMessage("Collation mismatch detected (auto-recovery will run): " . $errorText);
169 }
170 }
171
172 /**
173 * True when a prior quota-exceeded error is still inside its cooldown
174 * window, so callers should skip non-essential queries.
175 *
176 * @return bool
177 */
178 public function isQuotaCooldownActive(): bool {
179 $rawQuotaFlag = $this->core->noticeState()->getRuntimeFlag('abj404_db_quota_cooldown_until');
180 $until = is_scalar($rawQuotaFlag) ? (int)$rawQuotaFlag : 0;
181 return ($until > $this->core->clock()->now());
182 }
183 }
184