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 / DatabaseQueryRecoveryPolicy.php

DatabaseQueryRecoveryPolicy.php in 404 Solution 4.3.0, at includes/database/DatabaseQueryRecoveryPolicy.php

243 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * Post-execution recovery policy for DatabaseCore queryAndGetResults().
9 *
10 * DatabaseQueryExecutor owns normalization, preparation, raw execution, and
11 * result harvesting. This policy owns the ordered recovery decisions that run
12 * after the first wpdb call has produced an error: timeout-wrapper fallback,
13 * transient reconnect retry, missing-table repair, invalid-data retry,
14 * deadlock retry and notice, collation recovery, timeout fallback rows, and
15 * server-side issue state.
16 */
17 class ABJ_404_Solution_DatabaseQueryRecoveryPolicy {
18
19 /** @var ABJ_404_Solution_DatabaseCore */
20 private $core;
21
22 /** @var ABJ_404_Solution_Logging */
23 private $logger;
24
25 /** @var ABJ_404_Solution_DatabaseWpdbResultHarvester */
26 private $resultHarvester;
27
28 /** @var ABJ_404_Solution_DatabaseQueryDiagnostics */
29 private $queryDiagnostics;
30
31 /**
32 * @param ABJ_404_Solution_DatabaseCore $core
33 * @param ABJ_404_Solution_Logging $logger
34 * @param ABJ_404_Solution_DatabaseWpdbResultHarvester $resultHarvester
35 * @param ABJ_404_Solution_DatabaseQueryDiagnostics $queryDiagnostics
36 */
37 public function __construct(
38 ABJ_404_Solution_DatabaseCore $core,
39 $logger,
40 ABJ_404_Solution_DatabaseWpdbResultHarvester $resultHarvester,
41 ABJ_404_Solution_DatabaseQueryDiagnostics $queryDiagnostics
42 ) {
43 $this->core = $core;
44 $this->logger = $logger;
45 $this->resultHarvester = $resultHarvester;
46 $this->queryDiagnostics = $queryDiagnostics;
47 }
48
49 /**
50 * Run all post-execution recovery branches in their legacy order.
51 *
52 * @param string $query
53 * @param array<string, mixed> $result
54 * @param array<string, mixed> $options
55 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType
56 * @param bool $producesRows
57 * @param int $timeoutSeconds
58 * @return bool Updated produces-rows decision after timeout-wrapper fallback.
59 */
60 public function recoverQueryResult(
61 string &$query,
62 array &$result,
63 array $options,
64 string $resultType,
65 bool $producesRows,
66 int $timeoutSeconds
67 ): bool {
68 $producesRows = $this->retryWithoutSetStatementIfNeeded($query, $result, $resultType, $producesRows);
69 $this->retryTransientConnectionIfNeeded($query, $result, $resultType, $producesRows);
70 $this->repairMissingTableIfNeeded($query, $result, $options);
71 $this->retryInvalidDataIfNeeded($query, $result);
72 $this->retryDeadlockIfNeeded($query, $result, $resultType, $producesRows);
73 $this->recoverCollationIfNeeded($query, $result, $resultType, $producesRows);
74 $this->handleTimeoutIfNeeded($query, $result, $timeoutSeconds);
75 $this->noteDatabaseIssueIfNeeded($result);
76 return $producesRows;
77 }
78
79 /**
80 * @param string $query
81 * @param array<string, mixed> $result
82 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType
83 * @param bool $producesRows
84 * @return bool
85 */
86 private function retryWithoutSetStatementIfNeeded(string &$query, array &$result, string $resultType, bool $producesRows): bool {
87 $lastError = $this->lastErrorFromResult($result);
88 if ($lastError === ''
89 || !$this->core->errorClassifier()->taxonomy()->hostState()->classifySetStatementFailure($lastError)
90 || !$this->core->queryTimeoutManager()->queryHasSetStatementWrapper($query)) {
91 return $producesRows;
92 }
93
94 $this->core->queryTimeoutManager()->retryWithoutSetStatementWrapper($query, $result, $resultType);
95 return $this->core->queryTimeoutManager()->queryProducesResultRows($query);
96 }
97
98 /**
99 * @param string $query
100 * @param array<string, mixed> $result
101 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType
102 * @param bool $producesRows
103 * @return void
104 */
105 private function retryTransientConnectionIfNeeded(string $query, array &$result, string $resultType, bool $producesRows): void {
106 $lastError = $this->lastErrorFromResult($result);
107 if ($lastError === '' || !$this->core->errorClassifier()->taxonomy()->connectivity()->isTransientConnectionError($lastError)) {
108 return;
109 }
110
111 global $wpdb;
112 $this->core->connectionManager()->ensureConnection();
113 $wpdb->flush();
114 $result = array_merge($result, $this->executeWpdbQuery($query, $resultType, $producesRows));
115 $this->resultHarvester->harvestWpdbResult($result);
116 }
117
118 /**
119 * @param string $query
120 * @param array<string, mixed> $result
121 * @param array<string, mixed> $options
122 * @return void
123 */
124 private function repairMissingTableIfNeeded(string $query, array &$result, array $options): void {
125 $lastError = $this->lastErrorFromResult($result);
126 if ($options['skip_repair'] || $lastError === '' || !$this->core->errorClassifier()->taxonomy()->schema()->isMissingPluginTableError($lastError)) {
127 return;
128 }
129 $this->core->repairPolicy()->attemptMissingTableRepairAndRetry($query, $result);
130 }
131
132 /**
133 * @param string $query
134 * @param array<string, mixed> $result
135 * @return void
136 */
137 private function retryInvalidDataIfNeeded(string $query, array &$result): void {
138 $lastError = $this->lastErrorFromResult($result);
139 if ($lastError !== '' && $this->core->errorClassifier()->taxonomy()->schema()->isInvalidDataError($lastError)) {
140 $this->core->tableRepairer()->attemptInvalidDataRetry($query, $result);
141 }
142 }
143
144 /**
145 * @param string $query
146 * @param array<string, mixed> $result
147 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType
148 * @param bool $producesRows
149 * @return void
150 */
151 private function retryDeadlockIfNeeded(string $query, array &$result, string $resultType, bool $producesRows): void {
152 $lastError = $this->lastErrorFromResult($result);
153 if ($lastError === '' || !$this->core->errorClassifier()->taxonomy()->connectivity()->isDeadlockOrLockTimeoutError($lastError)) {
154 return;
155 }
156
157 usleep(50000);
158 $result = array_merge($result, $this->executeWpdbQuery($query, $resultType, $producesRows));
159 $this->resultHarvester->harvestWpdbResult($result);
160 $lastError = $this->lastErrorFromResult($result);
161 if ($lastError !== '' && $this->core->errorClassifier()->taxonomy()->connectivity()->isDeadlockOrLockTimeoutError($lastError)) {
162 $this->core->noticeState()->setPluginDbNotice(
163 'lock_timeout',
164 function_exists('__') ? __('A database lock wait timeout occurred. If this persists, contact your host - another process may be holding a long-running lock.', '404-solution') : 'A database lock wait timeout occurred. If this persists, contact your host - another process may be holding a long-running lock.',
165 function_exists('__') ? __('A database lock wait timeout occurred. This is usually caused by another process holding a table lock on your database. It may resolve itself automatically, or contact your hosting provider if it persists.', '404-solution') : 'A database lock wait timeout occurred. This is usually caused by another process holding a table lock on your database. It may resolve itself automatically, or contact your hosting provider if it persists.',
166 $lastError
167 );
168 }
169 }
170
171 /**
172 * @param string $query
173 * @param array<string, mixed> $result
174 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType
175 * @param bool $producesRows
176 * @return void
177 */
178 private function recoverCollationIfNeeded(string $query, array &$result, string $resultType, bool $producesRows): void {
179 $lastError = $this->lastErrorFromResult($result);
180 if ($lastError !== '' && $this->core->errorClassifier()->taxonomy()->schema()->isCollationError($lastError)) {
181 $this->core->collationHelper()->recoverFromCollationMismatchAndRetry($query, $result, $producesRows, $resultType);
182 }
183 }
184
185 /**
186 * @param string $query
187 * @param array<string, mixed> $result
188 * @param int $timeoutSeconds
189 * @return void
190 */
191 private function handleTimeoutIfNeeded(string $query, array &$result, int $timeoutSeconds): void {
192 $lastError = $this->lastErrorFromResult($result);
193 if ($lastError === '' || !$this->core->errorClassifier()->taxonomy()->connectivity()->isQueryTimeoutError($lastError)) {
194 return;
195 }
196
197 $sqlInfo = (defined('WP_DEBUG') && WP_DEBUG) ? $query : $this->queryDiagnostics->extractSqlFilename($query);
198 $this->logger->warn(
199 'Query timed out after ' . $timeoutSeconds . 's. ' .
200 'Query: ' . substr(preg_replace('/\s+/', ' ', trim($sqlInfo)) ?? $sqlInfo, 0, 500)
201 );
202 $result['rows'] = array();
203 $result['timed_out'] = true;
204 }
205
206 /**
207 * @param array<string, mixed> $result
208 * @return void
209 */
210 private function noteDatabaseIssueIfNeeded(array $result): void {
211 $lastError = $this->lastErrorFromResult($result);
212 if ($lastError !== '') {
213 $this->core->errorClassifier()->noteDatabaseIssueFromError($lastError);
214 }
215 }
216
217 /**
218 * @param string $query
219 * @param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $resultType
220 * @param bool $producesRows
221 * @return array<string, mixed>
222 */
223 private function executeWpdbQuery(string $query, string $resultType, bool $producesRows): array {
224 global $wpdb;
225 if ($producesRows) {
226 // DAO-bypass-approved: Recovery retry executes the original SQL outside DAO routing to avoid recursion.
227 return array('rows' => $wpdb->get_results($query, $resultType));
228 }
229
230 // DAO-bypass-approved: Recovery retry executes the original SQL outside DAO routing to avoid recursion.
231 $wpdb->query($query);
232 return array('rows' => array());
233 }
234
235 /**
236 * @param array<string, mixed> $result
237 * @return string
238 */
239 private function lastErrorFromResult(array $result): string {
240 return isset($result['last_error']) && is_scalar($result['last_error']) ? (string)$result['last_error'] : '';
241 }
242 }
243