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 / upgrades / DatabaseUpgradeNGram.php

DatabaseUpgradeNGram.php in 404 Solution 4.3.0, at includes/database/upgrades/DatabaseUpgradeNGram.php

378 lines 13.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 require_once __DIR__ . '/../../ngram/NGramNetworkOptionStore.php';
8 require_once __DIR__ . '/../../ngram/NGramCacheRebuildScheduler.php';
9 require_once __DIR__ . '/../../ngram/NGramCacheSyncRebuilder.php';
10 require_once __DIR__ . '/../../ngram/NGramCacheReconciler.php';
11 require_once __DIR__ . '/../../ngram/NGramLastUpdatedEpochMigration.php';
12
13 /**
14 * DatabaseUpgradesEtc delegate that owns the n-gram cache lifecycle.
15 *
16 * Acts as a thin orchestrator around five single-responsibility
17 * collaborators:
18 *
19 * - NGramNetworkOptionStore: network-aware option storage + multisite
20 * detection (also reached from DatabaseUpgradeBootstrap via the
21 * cross-component dispatcher).
22 * - NGramCacheRebuildScheduler: WP-Cron driven async rebuild loop.
23 * - NGramCacheSyncRebuilder: synchronous TRUNCATE+rebuild used by
24 * manual rebuild tools and the all-content composer.
25 * - NGramCacheReconciler: incremental sync-missing + cleanup-orphaned
26 * (posts, categories, and tags).
27 *
28 * Lock ownership lives here on the public entry points: the three
29 * write paths (rebuildNGramCache, rebuildNGramCacheAsync,
30 * syncMissingNGrams) acquire the 'ngram_rebuild' SyncUtils lock and
31 * scheduleNGramCacheRebuild acquires 'ngram_schedule', then delegate
32 * the actual work. This keeps the lock contract on the public
33 * surface while the collaborators stay pure.
34 */
35 class ABJ_404_Solution_DatabaseUpgradeNGram extends ABJ_404_Solution_DatabaseUpgradeComponent {
36
37 /**
38 * Documented cron hook for the n-gram rebuild loop. Defined here
39 * so the literal appears in this file for the Pattern 8
40 * coordination-key audit (AsyncWorkerCoordinationAuditTest).
41 * The scheduler collaborator owns the runtime use.
42 */
43 const REBUILD_CRON_HOOK = 'abj404_rebuild_ngram_cache_hook';
44
45 /**
46 * Convert legacy n-gram cache `last_updated` datetime storage to bigint
47 * Unix epoch seconds before the generic schema diff runs.
48 *
49 * Cross-component contract: DatabaseUpgradeBootstrap reaches this via the
50 * upgrade dispatcher. The conversion itself lives in
51 * {@see ABJ_404_Solution_NGramLastUpdatedEpochMigration}.
52 *
53 * @param string $tableName Physical n-gram cache table name.
54 * @return bool True when the table is already safe for generic schema
55 * verification, or after conversion succeeds.
56 */
57 function ensureLastUpdatedEpochColumn($tableName): bool {
58 return $this->newLastUpdatedEpochMigration()->ensureEpochColumn($tableName);
59 }
60
61 private function newLastUpdatedEpochMigration(): ABJ_404_Solution_NGramLastUpdatedEpochMigration {
62 return new ABJ_404_Solution_NGramLastUpdatedEpochMigration($this->dbCore, $this->logger);
63 }
64
65 /**
66 * Acquire the 'ngram_schedule' SyncUtils lock and delegate to the
67 * scheduler. Multiple admin clicks during a click storm collapse
68 * into one scheduled cron event.
69 *
70 * @return bool
71 */
72 function scheduleNGramCacheRebuild() {
73 $lockKey = 'ngram_schedule';
74 $uniqueID = $this->syncUtils->synchronizerAcquireLockTry($lockKey);
75
76 if (empty($uniqueID)) {
77 $this->logger->debugMessage("N-gram rebuild scheduling: Another process holds the lock. Skipping.");
78 return true;
79 }
80
81 try {
82 return $this->newScheduler()->scheduleRebuild();
83 } finally {
84 $this->syncUtils->synchronizerReleaseLock($uniqueID, $lockKey);
85 }
86 }
87
88 /**
89 * WP-Cron callback. Acquires the shared 'ngram_rebuild' lock so
90 * its INSERTs cannot race with a concurrent TRUNCATE from the
91 * sync rebuilder, then delegates to the scheduler's batch driver.
92 *
93 * @param int $offset legacy parameter retained for cron payload
94 * compatibility; the scheduler reads the
95 * authoritative offset from the network option
96 * store.
97 * @return void
98 */
99 function rebuildNGramCacheAsync($offset = 0) {
100 $lockKey = 'ngram_rebuild';
101 $uniqueID = $this->syncUtils->synchronizerAcquireLockTry($lockKey);
102 if (empty($uniqueID)) {
103 $this->logger->debugMessage("N-gram async rebuild batch already processing (another process holds lock). Skipping.");
104 return;
105 }
106
107 try {
108 $this->newScheduler()->runAsyncBatch();
109 } finally {
110 $this->syncUtils->synchronizerReleaseLock($uniqueID, $lockKey);
111 }
112 }
113
114 /**
115 * Synchronous rebuild entry point. Same lock as the async path so
116 * its TRUNCATE cannot race batch INSERTs.
117 *
118 * @param int $batchSize
119 * @param bool $forceRebuild
120 * @return array<string, mixed>
121 */
122 function rebuildNGramCache($batchSize = 100, $forceRebuild = false) {
123 $lockKey = 'ngram_rebuild';
124 $uniqueID = $this->syncUtils->synchronizerAcquireLockTry($lockKey);
125 if (empty($uniqueID)) {
126 $this->logger->infoMessage("N-gram rebuild already in progress (locked). Skipping.");
127 return [
128 'total_pages' => 0,
129 'processed' => 0,
130 'success' => 0,
131 'failed' => 0,
132 'locked' => true,
133 ];
134 }
135
136 try {
137 return $this->newSyncRebuilder()->rebuild($batchSize, $forceRebuild);
138 } finally {
139 $this->syncUtils->synchronizerReleaseLock($uniqueID, $lockKey);
140 }
141 }
142
143 /**
144 * Sync entries that exist in the source but are missing from the
145 * cache. Same lock as rebuild to keep mutations serialized.
146 *
147 * @param int $batchSize
148 * @return array<string, mixed>
149 */
150 function syncMissingNGrams($batchSize = 50) {
151 $lockKey = 'ngram_rebuild';
152 $uniqueID = $this->syncUtils->synchronizerAcquireLockTry($lockKey);
153 if (empty($uniqueID)) {
154 $this->logger->debugMessage("Ngram sync skipped - rebuild/sync already in progress.");
155 return ['posts_added' => 0, 'posts_failed' => 0, 'categories_added' => 0, 'categories_failed' => 0, 'locked' => true];
156 }
157
158 try {
159 return $this->newReconciler()->syncMissing($batchSize);
160 } finally {
161 $this->syncUtils->synchronizerReleaseLock($uniqueID, $lockKey);
162 }
163 }
164
165 /**
166 * Delete cache rows whose source no longer exists. Runs without
167 * the rebuild lock — it only deletes by primary key.
168 *
169 * @return array<string, mixed>
170 */
171 function cleanupOrphanedNGrams() {
172 return $this->newReconciler()->cleanupOrphaned();
173 }
174
175 /**
176 * Cross-component contract: DatabaseUpgradeBootstrap and others
177 * reach this via the upgrade dispatcher to learn whether the
178 * plugin is network-activated.
179 *
180 * @return bool
181 */
182 function isNetworkActivated() {
183 return $this->newOptionStore()->isNetworkActivated();
184 }
185
186 /**
187 * Cross-component contract: network-aware option getter.
188 *
189 * @param string $option_name
190 * @param mixed $default
191 * @return mixed
192 */
193 function getNetworkAwareOption($option_name, $default = false) {
194 return $this->newOptionStore()->getOption($option_name, $default);
195 }
196
197 /**
198 * Cross-component contract: network-aware option setter.
199 *
200 * @param string $option_name
201 * @param mixed $value
202 * @return bool
203 */
204 function updateNetworkAwareOption($option_name, $value) {
205 return $this->newOptionStore()->updateOption($option_name, $value);
206 }
207
208 /**
209 * Exposed for the multisite race-condition test (calls through
210 * the upgrade dispatcher) and as part of the schedule
211 * pre-condition. Sums permalink_cache rows across every site when
212 * network-activated, otherwise returns the current site count.
213 *
214 * @return int
215 */
216 function countTotalPagesForNGramRebuild() {
217 return $this->newScheduler()->countTotalPagesForRebuild();
218 }
219
220 private function newOptionStore(): ABJ_404_Solution_NGramNetworkOptionStore {
221 return new ABJ_404_Solution_NGramNetworkOptionStore();
222 }
223
224 private function newScheduler(): ABJ_404_Solution_NGramCacheRebuildScheduler {
225 return new ABJ_404_Solution_NGramCacheRebuildScheduler(
226 $this->dbCore,
227 $this->resolveNGramRebuilder(),
228 $this->logger,
229 $this->newOptionStore(),
230 $this->cronScheduler instanceof ABJ_404_Solution_CronScheduler ? $this->cronScheduler : null
231 );
232 }
233
234 private function newSyncRebuilder(): ABJ_404_Solution_NGramCacheSyncRebuilder {
235 return new ABJ_404_Solution_NGramCacheSyncRebuilder(
236 $this->dbCore,
237 $this->resolveNGramRebuilder(),
238 $this->resolveNGramCoveragePolicy(),
239 $this->logger
240 );
241 }
242
243 private function newReconciler(): ABJ_404_Solution_NGramCacheReconciler {
244 return new ABJ_404_Solution_NGramCacheReconciler(
245 $this->dbCore,
246 $this->resolveNGramRebuilder(),
247 $this->resolveNGramExtractor(),
248 $this->resolveNGramCacheRepository(),
249 $this->resolveNGramCoveragePolicy(),
250 $this->contentRepo,
251 $this->f,
252 $this->logger
253 );
254 }
255
256 /** @return object */
257 private function resolveNGramExtractor() {
258 if ($this->ngramExtractor instanceof ABJ_404_Solution_NGramExtractor) {
259 return $this->ngramExtractor;
260 }
261 if (is_object($this->ngramExtractor) && method_exists($this->ngramExtractor, 'extractNGrams')) {
262 return $this->ngramExtractor;
263 }
264 $legacy = $this->legacyNGramFacade('extractNGrams');
265 if ($legacy !== null) {
266 return $legacy;
267 }
268 return new ABJ_404_Solution_NGramExtractor($this->f, $this->logger);
269 }
270
271 /** @return object */
272 private function resolveNGramCacheRepository() {
273 if ($this->ngramCacheRepository instanceof ABJ_404_Solution_NGramCacheRepository) {
274 return $this->ngramCacheRepository;
275 }
276 if (is_object($this->ngramCacheRepository) && method_exists($this->ngramCacheRepository, 'storeNGrams')) {
277 return $this->ngramCacheRepository;
278 }
279 $legacy = $this->legacyNGramFacade('storeNGrams');
280 if ($legacy !== null) {
281 return $legacy;
282 }
283 return new ABJ_404_Solution_NGramCacheRepository(
284 $this->typedDbCoreOrNull(),
285 $this->logger,
286 new ABJ_404_Solution_NGramSimilarity(),
287 function() {
288 return $this->resolveConcreteNGramCoveragePolicy();
289 }
290 );
291 }
292
293 /** @return object */
294 private function resolveNGramCoveragePolicy() {
295 if ($this->ngramCoveragePolicy instanceof ABJ_404_Solution_NGramCoveragePolicy) {
296 return $this->ngramCoveragePolicy;
297 }
298 if (is_object($this->ngramCoveragePolicy) && method_exists($this->ngramCoveragePolicy, 'invalidateCoverageCaches')) {
299 return $this->ngramCoveragePolicy;
300 }
301 $legacy = $this->legacyNGramFacade('invalidateCoverageCaches');
302 if ($legacy !== null) {
303 return $legacy;
304 }
305 return new ABJ_404_Solution_NGramCoveragePolicy($this->typedDbCoreOrNull());
306 }
307
308 /** @return object */
309 private function resolveNGramRebuilder() {
310 if ($this->ngramRebuilder instanceof ABJ_404_Solution_NGramRebuilder) {
311 return $this->ngramRebuilder;
312 }
313 if (is_object($this->ngramRebuilder) && method_exists($this->ngramRebuilder, 'rebuildCache')) {
314 return $this->ngramRebuilder;
315 }
316 $legacy = $this->legacyNGramFacade('rebuildCache');
317 if ($legacy !== null) {
318 return $legacy;
319 }
320 return new ABJ_404_Solution_NGramRebuilder(
321 new ABJ_404_Solution_NGramRebuilderDependencies(
322 $this->typedDbCoreOrNull(),
323 $this->logger,
324 $this->f,
325 $this->resolveConcreteNGramExtractor(),
326 $this->resolveConcreteNGramCacheRepository(),
327 $this->resolveConcreteNGramCoveragePolicy()
328 )
329 );
330 }
331
332 /**
333 * @param string $requiredMethod
334 * @return object|null
335 */
336 private function legacyNGramFacade(string $requiredMethod) {
337 return is_object($this->ngramFilter) && method_exists($this->ngramFilter, $requiredMethod)
338 ? $this->ngramFilter
339 : null;
340 }
341
342 /** @return ABJ_404_Solution_DatabaseCore */
343 private function typedDbCoreOrNull() {
344 return $this->dbCore;
345 }
346
347 /** @return ABJ_404_Solution_NGramExtractor */
348 private function resolveConcreteNGramExtractor() {
349 if ($this->ngramExtractor instanceof ABJ_404_Solution_NGramExtractor) {
350 return $this->ngramExtractor;
351 }
352 return new ABJ_404_Solution_NGramExtractor($this->f, $this->logger);
353 }
354
355 /** @return ABJ_404_Solution_NGramCacheRepository */
356 private function resolveConcreteNGramCacheRepository() {
357 if ($this->ngramCacheRepository instanceof ABJ_404_Solution_NGramCacheRepository) {
358 return $this->ngramCacheRepository;
359 }
360 return new ABJ_404_Solution_NGramCacheRepository(
361 $this->typedDbCoreOrNull(),
362 $this->logger,
363 new ABJ_404_Solution_NGramSimilarity(),
364 function() {
365 return $this->resolveConcreteNGramCoveragePolicy();
366 }
367 );
368 }
369
370 /** @return ABJ_404_Solution_NGramCoveragePolicy */
371 private function resolveConcreteNGramCoveragePolicy() {
372 if ($this->ngramCoveragePolicy instanceof ABJ_404_Solution_NGramCoveragePolicy) {
373 return $this->ngramCoveragePolicy;
374 }
375 return new ABJ_404_Solution_NGramCoveragePolicy($this->typedDbCoreOrNull());
376 }
377 }
378