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

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