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 / settings / SuggestionWorkerStateStore.php

SuggestionWorkerStateStore.php in 404 Solution trunk, at includes/settings/SuggestionWorkerStateStore.php

139 lines 5.6 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 * Atomic state transitions for one asynchronous suggestion worker.
9 *
10 * The AJAX handler owns request validation and HTTP termination; this store
11 * owns the transient read/decide/write critical sections so those concerns
12 * cannot drift apart across claim, completion, and fatal-shutdown paths.
13 */
14 final class ABJ_404_Solution_SuggestionWorkerStateStore {
15
16 public const OUTCOME_CLAIMED = 'claimed';
17 public const OUTCOME_STALE = 'stale';
18 public const OUTCOME_BUSY = 'busy';
19 public const OUTCOME_WRITE_FAILED = 'write_failed';
20 public const OUTCOME_STORED = 'stored';
21
22 /** @var ABJ_404_Solution_SynchronizationUtils */
23 private $synchronizer;
24
25 /** @var ABJ_404_Solution_Clock */
26 private $clock;
27
28 public function __construct(
29 ABJ_404_Solution_SynchronizationUtils $synchronizer,
30 ABJ_404_Solution_Clock $clock
31 ) {
32 $this->synchronizer = $synchronizer;
33 $this->clock = $clock;
34 }
35
36 /**
37 * @param array{transientKey: string, normalizedURL: string, token: string} $request
38 * @return array{status: string, startedAt: int}
39 */
40 public function claimWorker(array $request): array {
41 $lockKey = ABJ_404_Solution_SuggestionTransient::lockKeyForNormalizedUrl($request['normalizedURL']);
42 $owner = $this->synchronizer->synchronizerAcquireLockTry($lockKey);
43 if ($owner === '') {
44 return array('status' => self::OUTCOME_BUSY, 'startedAt' => 0);
45 }
46
47 try {
48 $current = ABJ_404_Solution_SuggestionTransient::fromRaw(get_transient($request['transientKey']));
49 if ($current === null || $current->isComplete() || $current->getToken() !== $request['token']) {
50 return array('status' => self::OUTCOME_STALE, 'startedAt' => 0);
51 }
52 $now = $this->clock->now();
53 if ($current->isPending() && $current->isClaimed() && !$current->isWorkerStuck($now)) {
54 return array('status' => self::OUTCOME_STALE, 'startedAt' => 0);
55 }
56
57 $createdAt = $current->getCreatedAt() > 0 ? $current->getCreatedAt() : $now;
58 $stored = set_transient(
59 $request['transientKey'],
60 ABJ_404_Solution_SuggestionTransient::pendingArray(
61 $current->getUrl(),
62 $request['token'],
63 $now,
64 $createdAt
65 ),
66 ABJ_404_Solution_SuggestionTransient::PENDING_TTL_SECONDS
67 );
68 return array(
69 'status' => $stored ? self::OUTCOME_CLAIMED : self::OUTCOME_WRITE_FAILED,
70 'startedAt' => $stored ? $now : 0,
71 );
72 } finally {
73 $this->synchronizer->synchronizerReleaseLock($owner, $lockKey);
74 }
75 }
76
77 /**
78 * @param array{transientKey: string, normalizedURL: string, requestedURL: string, token: string, workerStartedAt: int, suggestionsPacket: array<int, mixed>} $request
79 */
80 public function publishCompleted(array $request): string {
81 $lockKey = ABJ_404_Solution_SuggestionTransient::lockKeyForNormalizedUrl($request['normalizedURL']);
82 $owner = $this->synchronizer->synchronizerAcquireLockTry($lockKey);
83 if ($owner === '') {
84 return self::OUTCOME_BUSY;
85 }
86
87 try {
88 $current = ABJ_404_Solution_SuggestionTransient::fromRaw(get_transient($request['transientKey']));
89 if ($current === null || $current->isComplete() || $current->getToken() !== $request['token']
90 || $current->getStartedAt() !== $request['workerStartedAt']
91 ) {
92 return self::OUTCOME_STALE;
93 }
94 $stored = set_transient(
95 $request['transientKey'],
96 ABJ_404_Solution_SuggestionTransient::completeArray(
97 $request['requestedURL'],
98 $request['suggestionsPacket'],
99 $this->clock->now(),
100 $request['token']
101 ),
102 ABJ_404_Solution_SuggestionTransient::COMPLETE_TTL_SECONDS
103 );
104 return $stored ? self::OUTCOME_STORED : self::OUTCOME_WRITE_FAILED;
105 } finally {
106 $this->synchronizer->synchronizerReleaseLock($owner, $lockKey);
107 }
108 }
109
110 /**
111 * @param array{transientKey: string, normalizedURL: string, token: string, workerStartedAt: int|null} $request
112 */
113 public function publishCrashMarker(array $request): string {
114 $lockKey = ABJ_404_Solution_SuggestionTransient::lockKeyForNormalizedUrl($request['normalizedURL']);
115 $owner = $this->synchronizer->synchronizerAcquireLockTry($lockKey);
116 if ($owner === '') {
117 return self::OUTCOME_BUSY;
118 }
119
120 try {
121 $current = ABJ_404_Solution_SuggestionTransient::fromRaw(get_transient($request['transientKey']));
122 if ($current === null || $current->isComplete() || $current->getToken() !== $request['token']
123 || ($request['workerStartedAt'] !== null
124 && $current->getStartedAt() !== $request['workerStartedAt'])
125 ) {
126 return self::OUTCOME_STALE;
127 }
128 $stored = set_transient(
129 $request['transientKey'],
130 ABJ_404_Solution_SuggestionTransient::errorArray($request['token']),
131 ABJ_404_Solution_SuggestionTransient::ERROR_TTL_SECONDS
132 );
133 return $stored ? self::OUTCOME_STORED : self::OUTCOME_WRITE_FAILED;
134 } finally {
135 $this->synchronizer->synchronizerReleaseLock($owner, $lockKey);
136 }
137 }
138 }
139