PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.14
ووسلام – همگام سازی ووکامرس و باسلام v1.10.14
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
sync-basalam / includes / Services / Api / CircuitBreaker.php

CircuitBreaker.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.14, at includes/Services/Api/CircuitBreaker.php

247 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Services\Api;
4
5 defined('ABSPATH') || exit;
6
7 /**
8 * Circuit Breaker for Basalam API requests.
9 *
10 * States:
11 * CLOSED - normal operation, requests pass through.
12 * OPEN - requests are blocked after too many consecutive failures.
13 * HALF_OPEN - one probe request is allowed after the cooldown period to test recovery.
14 *
15 * State is persisted in a single WordPress option so it survives across requests/cron jobs.
16 */
17 class CircuitBreaker
18 {
19 const STATE_CLOSED = 'closed';
20 const STATE_OPEN = 'open';
21 const STATE_HALF_OPEN = 'half_open';
22
23 const OPTION_KEY = 'sync_basalam_circuit_breaker';
24
25 /**
26 * Request-local cache to avoid repeated DB reads when multiple API service
27 * instances are created in the same request lifecycle.
28 */
29 private static ?array $requestStateCache = null;
30
31 /** Number of consecutive failures before the circuit opens. */
32 private int $failureThreshold;
33
34 /** Seconds to wait in OPEN state before moving to HALF_OPEN. */
35 private int $recoveryTimeout;
36
37 /** Seconds of inactivity in CLOSED state after which failure_count resets to 0. */
38 private int $closedResetInterval;
39
40 /** Seconds to wait before considering a HALF_OPEN probe stale. */
41 private int $halfOpenProbeTimeout;
42
43 private array $state;
44
45 public function __construct(
46 int $failureThreshold = 10,
47 int $recoveryTimeout = 60,
48 int $closedResetInterval = 300,
49 ?int $halfOpenProbeTimeout = null
50 ) {
51 $this->failureThreshold = $failureThreshold;
52 $this->recoveryTimeout = $recoveryTimeout;
53 $this->closedResetInterval = $closedResetInterval;
54 $this->halfOpenProbeTimeout = $halfOpenProbeTimeout ?? $recoveryTimeout;
55 $this->state = $this->loadState();
56 }
57
58 /**
59 * Returns true when a request should be allowed through.
60 * Throws a CircuitBreakerOpenException when the circuit is OPEN.
61 */
62 public function isAllowed(): bool
63 {
64 $currentState = $this->state['state'];
65
66 if ($currentState === self::STATE_CLOSED) return true;
67
68 if ($currentState === self::STATE_OPEN) {
69 if ($this->recoveryTimeoutElapsed()) {
70 $this->transitionTo(self::STATE_HALF_OPEN);
71 $currentState = self::STATE_HALF_OPEN;
72 } else {
73 throw new CircuitBreakerOpenException('سرویس باسلا�
74
75 وقتاً در دسترس نیست. لطفاً چند دقیقه دیگر تلاش کنید.', 503);
76 }
77 }
78
79 if ($currentState === self::STATE_HALF_OPEN) {
80 if ($this->hasHalfOpenProbeInFlight() && !$this->halfOpenProbeExpired()) {
81 throw new CircuitBreakerOpenException('در حال بررسی اتصال �
82 جدد به باسلا�
83 هستی�
84 . لطفاً چند لحظه دیگر دوباره تلاش کنید.', 503);
85 }
86
87 $this->state['probe_started_at'] = time();
88 $this->saveState();
89
90 return true;
91 }
92
93 return false;
94 }
95
96 /**
97 * Records a successful request and resets the failure counter.
98 */
99 public function recordSuccess(): void
100 {
101 $this->state['failure_count'] = 0;
102 $this->state['last_failure'] = null;
103 $this->transitionTo(self::STATE_CLOSED);
104 }
105
106 /**
107 * Records a failed request and opens the circuit when the threshold is reached.
108 */
109 public function recordFailure(): void
110 {
111 $this->state['failure_count']++;
112 $this->state['last_failure'] = time();
113 $this->state['probe_started_at'] = null;
114
115 if ($this->state['state'] === self::STATE_HALF_OPEN) {
116 $this->transitionTo(self::STATE_OPEN);
117 return;
118 }
119
120 if ($this->state['failure_count'] >= $this->failureThreshold) {
121 $this->transitionTo(self::STATE_OPEN);
122 return;
123 }
124
125 $this->saveState();
126 }
127
128 public function getState(): string
129 {
130 return $this->state['state'];
131 }
132
133 public function getFailureCount(): int
134 {
135 return $this->state['failure_count'];
136 }
137
138 public function getLastFailure(): ?int
139 {
140 return empty($this->state['last_failure']) ? null : (int) $this->state['last_failure'];
141 }
142
143 public function getSnapshot(): array
144 {
145 return $this->state;
146 }
147
148 public function reset(): void
149 {
150 $this->state = $this->defaultState();
151 $this->saveState();
152 }
153
154 private function recoveryTimeoutElapsed(): bool
155 {
156 if (empty($this->state['last_failure'])) {
157 return true;
158 }
159
160 return (time() - $this->state['last_failure']) >= $this->recoveryTimeout;
161 }
162
163 private function hasHalfOpenProbeInFlight(): bool
164 {
165 return !empty($this->state['probe_started_at']);
166 }
167
168 private function halfOpenProbeExpired(): bool
169 {
170 if (empty($this->state['probe_started_at'])) {
171 return true;
172 }
173
174 return (time() - $this->state['probe_started_at']) >= $this->halfOpenProbeTimeout;
175 }
176
177 private function transitionTo(string $newState): void
178 {
179 if ($newState === self::STATE_CLOSED) {
180 $this->state = $this->defaultState();
181 } else {
182 $this->state['state'] = $newState;
183 $this->state['probe_started_at'] = null;
184 }
185
186 $this->saveState();
187 }
188
189 private function loadState(): array
190 {
191 if (is_array(self::$requestStateCache)) {
192 $state = self::$requestStateCache;
193 } else {
194 $stored = get_option(self::OPTION_KEY, null);
195
196 if (!is_array($stored)) {
197 $state = $this->defaultState();
198 } else {
199 $state = array_merge($this->defaultState(), $stored);
200 }
201
202 self::$requestStateCache = $state;
203 }
204
205 if (
206 $state['state'] === self::STATE_CLOSED &&
207 $state['failure_count'] > 0 &&
208 !empty($state['last_failure']) &&
209 (time() - $state['last_failure']) >= $this->closedResetInterval
210 ) {
211 $state['failure_count'] = 0;
212 $state['last_failure'] = null;
213 $state['probe_started_at'] = null;
214 update_option(self::OPTION_KEY, $state, false);
215 self::$requestStateCache = $state;
216 }
217
218 if (
219 $state['state'] === self::STATE_HALF_OPEN &&
220 !empty($state['probe_started_at']) &&
221 $this->halfOpenProbeExpired()
222 ) {
223 $state['probe_started_at'] = null;
224 update_option(self::OPTION_KEY, $state, false);
225 self::$requestStateCache = $state;
226 }
227
228 return $state;
229 }
230
231 private function saveState(): void
232 {
233 self::$requestStateCache = $this->state;
234 update_option(self::OPTION_KEY, $this->state, false);
235 }
236
237 private function defaultState(): array
238 {
239 return [
240 'state' => self::STATE_CLOSED,
241 'failure_count' => 0,
242 'last_failure' => null,
243 'probe_started_at' => null,
244 ];
245 }
246 }
247