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

DatabaseTransactionExecutor.php in 404 Solution 4.3.3, at includes/database/DatabaseTransactionExecutor.php

95 lines 3.7 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 * Executes SQL statements inside a transaction with deadlock-aware retry.
9 *
10 * Transaction lifecycle is separate from the single-query pipeline: it owns
11 * BEGIN/COMMIT/ROLLBACK bookkeeping, infrastructure-error classification for
12 * statement failures, and retry delay for deadlock or lock-wait timeouts.
13 */
14 class ABJ_404_Solution_DatabaseTransactionExecutor {
15
16 /** @var ABJ_404_Solution_DatabaseCore */
17 private $core;
18
19 /** @var ABJ_404_Solution_Logging */
20 private $logger;
21
22 /**
23 * @param ABJ_404_Solution_DatabaseCore $core
24 * @param ABJ_404_Solution_Logging $logger
25 */
26 public function __construct(ABJ_404_Solution_DatabaseCore $core, $logger) {
27 $this->core = $core;
28 $this->logger = $logger;
29 }
30
31 /**
32 * @param array<int, string> $statementArray
33 * @return void
34 */
35 public function executeAsTransaction(array $statementArray): void {
36 global $wpdb;
37 $maxAttempts = 3;
38 $lastException = null;
39 $lastError = '';
40
41 for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
42 $allIsWell = true;
43 $lastError = '';
44 $lastException = null;
45 try {
46 // DAO-bypass-approved: transaction boundary must run on the active wpdb connection before grouped statements execute.
47 $wpdb->query('START TRANSACTION');
48 foreach ($statementArray as $statement) {
49 // DAO-bypass-approved: transaction executor must preserve same-connection transaction state and last_error per statement.
50 $wpdb->query($statement);
51 // Null-safe read (matches DataAccess.php/StatsRepository.php/NGramFilter.php):
52 // a partial test double or a future custom wpdb drop-in that omits
53 // last_error must not raise an "undefined property" notice here.
54 $statementError = trim((string)($wpdb->last_error ?? ''));
55 if ($statementError !== '') {
56 $allIsWell = false;
57 $lastError = $statementError;
58 if (!$this->core->errorClassifier()->classifyAndHandleInfrastructureError($lastError)) {
59 $this->logger->errorMessage("Error executing SQL transaction: " . $lastError);
60 $this->logger->errorMessage("SQL causing the transaction error: " . $statement);
61 }
62 break;
63 }
64 }
65 } catch (Throwable $ex) {
66 $allIsWell = false;
67 $lastException = $ex;
68 $lastError = $ex->getMessage();
69 }
70
71 if ($allIsWell && $lastException == null) {
72 // DAO-bypass-approved: transaction boundary must commit the active wpdb connection.
73 $wpdb->query('commit');
74 return;
75 }
76
77 // DAO-bypass-approved: transaction boundary must roll back the active wpdb connection after any grouped statement failure.
78 $wpdb->query('rollback');
79 $retryable = $this->core->errorClassifier()->taxonomy()->connectivity()->isDeadlockOrLockTimeoutError($lastError);
80 if (!$retryable || $attempt >= $maxAttempts) {
81 break;
82 }
83 $sleepMicros = 100000 + random_int(0, 200000);
84 usleep($sleepMicros);
85 }
86
87 if ($lastException != null) {
88 throw $lastException;
89 }
90 if ($lastError !== '') {
91 throw new Exception($lastError); // allow-raw-error: behavior preserved from pre-extraction DatabaseCore::executeAsTransaction
92 }
93 }
94 }
95