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

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

227 lines 8.0 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 * Owns the plugin's database notice and runtime-flag state.
9 *
10 * Extracted from DatabaseCore (design-audit M202 "separate state holder"
11 * callout). This class is the single home for:
12 * - Runtime flags persisted as transients (with option fallback): cooldown
13 * timestamps, write-block markers, and the admin-notice payload.
14 * - The abj404_plugin_db_notice admin-notice payload lifecycle (set, clear,
15 * clear-if-type).
16 * - Write-block / skip-non-essential-write cooldown detection.
17 * - The per-request server-side-issue tracking booleans
18 * (serverSideIssueNoted / serverSideIssueChecked) that drive notice
19 * auto-clear once the server recovers.
20 *
21 * This class holds no SQL query pipeline and builds no queries. It depends on
22 * a clock (for cooldown comparisons and notice timestamps) and on a
23 * quota-cooldown checker callable (supplied by DatabaseCore as a bound closure
24 * over its error classifier) so that it needs no DatabaseCore reference and
25 * introduces no cyclic coupling. The callable signature is:
26 * function(): bool
27 */
28 class ABJ_404_Solution_DatabaseNoticeStateHolder {
29
30 /** @var int Cooldown when DB is read-only or storage is full. */
31 const DB_WRITE_BLOCK_COOLDOWN_SECONDS = 900;
32
33 /** @var ABJ_404_Solution_Clock|null */
34 private $clock = null;
35
36 /** @var callable(): bool */
37 private $quotaCooldownChecker;
38
39 /** @var bool Whether a server-side DB issue was noted this request (for auto-clear). */
40 private $serverSideIssueNoted = false;
41
42 /** @var bool Whether we already checked for a stale notice transient this request. */
43 private $serverSideIssueChecked = false;
44
45 /**
46 * @param callable(): bool $quotaCooldownChecker Returns true when a DB quota
47 * cooldown is currently active. Supplied by DatabaseCore as a bound closure
48 * over its error classifier so this class needs no DatabaseCore reference.
49 * @param ABJ_404_Solution_Clock|null $clock Optional clock; resolved lazily
50 * from the service container (or a SystemClock fallback) when omitted.
51 */
52 public function __construct(callable $quotaCooldownChecker, $clock = null) {
53 $this->quotaCooldownChecker = $quotaCooldownChecker;
54 $this->clock = $clock;
55 }
56
57 /**
58 * Inject the clock instance for testability.
59 *
60 * @param ABJ_404_Solution_Clock $clock
61 * @return void
62 */
63 public function setClock(ABJ_404_Solution_Clock $clock): void {
64 $this->clock = $clock;
65 }
66
67 /**
68 * Resolve the clock instance (injected, container, or SystemClock fallback).
69 *
70 * @return ABJ_404_Solution_Clock
71 */
72 public function clock(): ABJ_404_Solution_Clock {
73 if ($this->clock !== null) { return $this->clock; }
74 if (class_exists('ABJ_404_Solution_ServiceContainer')) {
75 $resolved = ABJ_404_Solution_ServiceContainer::safeGet('clock');
76 if ($resolved instanceof ABJ_404_Solution_Clock) {
77 $this->clock = $resolved;
78 return $this->clock;
79 }
80 }
81 $this->clock = new ABJ_404_Solution_SystemClock();
82 return $this->clock;
83 }
84
85 /**
86 * Persist a runtime flag as a transient (option fallback when transients
87 * are unavailable).
88 *
89 * @param string $key Flag name.
90 * @param mixed $value Value to store (admin-notice payload, cooldown
91 * timestamp, or lock-state marker).
92 * @param int $ttlSeconds Transient lifetime in seconds.
93 * @return void
94 */
95 public function setRuntimeFlag(string $key, $value, int $ttlSeconds): void {
96 if (function_exists('set_transient')) {
97 // allow-cache-empty: passthrough helper. Callers store admin-notice payloads, cooldown timestamps, and lock-state markers, not query results.
98 set_transient($key, $value, $ttlSeconds);
99 return;
100 }
101 if (function_exists('update_option')) {
102 update_option($key, $value, false);
103 }
104 }
105
106 /**
107 * Read a runtime flag (transient, with option fallback).
108 *
109 * @param string $key Flag name.
110 * @return mixed The stored value, or false when unset/unavailable.
111 */
112 public function getRuntimeFlag(string $key) {
113 if (function_exists('get_transient')) {
114 return get_transient($key);
115 }
116 if (function_exists('get_option')) {
117 return get_option($key, false);
118 }
119 return false;
120 }
121
122 /**
123 * Store the plugin DB admin-notice payload as a runtime flag.
124 *
125 * @param string $type Notice type discriminator (e.g. 'lock_timeout').
126 * @param string $message Already translated admin notice message.
127 * @param string $guidance Already translated optional remediation guidance.
128 * @param string $errorString Underlying MySQL error string (diagnostic).
129 * @return void
130 */
131 public function setPluginDbNotice(string $type, string $message, string $guidance, string $errorString = ''): void {
132 $payload = array(
133 'type' => $type,
134 'message' => $message,
135 'guidance' => $guidance,
136 'timestamp' => $this->clock()->now(),
137 'error_string' => $errorString,
138 );
139 $this->setRuntimeFlag('abj404_plugin_db_notice', $payload, self::DB_WRITE_BLOCK_COOLDOWN_SECONDS);
140 }
141
142 /**
143 * Clear the plugin DB notice only when its current type matches.
144 *
145 * @param string $type Notice type to match before clearing.
146 * @return void
147 */
148 public function clearPluginDbNoticeIfType(string $type): void {
149 $existing = $this->getRuntimeFlag('abj404_plugin_db_notice');
150 if (!is_array($existing)) {
151 return;
152 }
153 $currentType = isset($existing['type']) && is_string($existing['type']) ? $existing['type'] : '';
154 if ($currentType !== $type) {
155 return;
156 }
157 $this->clearServerSideDbNotice();
158 }
159
160 /**
161 * Delete the plugin DB notice and reset the server-side-issue flag.
162 *
163 * @return void
164 */
165 public function clearServerSideDbNotice(): void {
166 if (function_exists('delete_transient')) {
167 delete_transient('abj404_plugin_db_notice');
168 } elseif (function_exists('delete_option')) {
169 delete_option('abj404_plugin_db_notice');
170 }
171 $this->serverSideIssueNoted = false;
172 }
173
174 /**
175 * @return bool True when a write-block cooldown is active (disk full or read-only).
176 */
177 public function isWriteBlockActive(): bool {
178 $rawDiskFlag = $this->getRuntimeFlag('abj404_db_disk_full_until');
179 $diskUntil = is_scalar($rawDiskFlag) ? (int)$rawDiskFlag : 0;
180 $rawReadOnlyFlag = $this->getRuntimeFlag('abj404_db_read_only_until');
181 $readOnlyUntil = is_scalar($rawReadOnlyFlag) ? (int)$rawReadOnlyFlag : 0;
182 $now = $this->clock()->now();
183 return ($diskUntil > $now || $readOnlyUntil > $now);
184 }
185
186 /**
187 * @return bool True when non-essential DB writes should be skipped (quota
188 * cooldown or write-block active).
189 */
190 public function shouldSkipNonEssentialDbWrites(): bool {
191 return (($this->quotaCooldownChecker)() || $this->isWriteBlockActive());
192 }
193
194 /**
195 * Mark that a server-side DB issue was noted this request, so the notice
196 * can be auto-cleared once a later query succeeds.
197 *
198 * @return void
199 */
200 public function markServerSideIssueNoted(): void {
201 $this->serverSideIssueNoted = true;
202 }
203
204 /**
205 * @return bool Whether a server-side DB issue was noted this request.
206 */
207 public function isServerSideIssueNoted(): bool {
208 return $this->serverSideIssueNoted;
209 }
210
211 /**
212 * @return bool Whether the stale-notice transient was already checked this request.
213 */
214 public function isServerSideIssueChecked(): bool {
215 return $this->serverSideIssueChecked;
216 }
217
218 /**
219 * Mark the stale-notice transient as checked for this request.
220 *
221 * @return void
222 */
223 public function markServerSideIssueChecked(): void {
224 $this->serverSideIssueChecked = true;
225 }
226 }
227