PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.21
ووسلام – همگام سازی ووکامرس و باسلام v1.10.21
1.10.21 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 All 54 releases
← All changes | includes/Services/Api/CircuitBreaker.php +78 -23 1.8.51.10.21 View file →
@@ -7,11 +7,11 @@
7 7 /**
8 8 * Circuit Breaker for Basalam API requests.
9 9 *
10 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.
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 14 *
15 15 * State is persisted in a single WordPress option so it survives across requests/cron jobs.
16 16 */
17 17 class CircuitBreaker
@@ -36,16 +36,24 @@
36 36
37 37 /** Seconds of inactivity in CLOSED state after which failure_count resets to 0. */
38 38 private int $closedResetInterval;
39 39
40 + /** Seconds to wait before considering a HALF_OPEN probe stale. */
41 + private int $halfOpenProbeTimeout;
42 +
40 43 private array $state;
41 44
42 - public function __construct(int $failureThreshold = 10, int $recoveryTimeout = 60, int $closedResetInterval = 300)
43 - {
44 - $this->failureThreshold = $failureThreshold;
45 - $this->recoveryTimeout = $recoveryTimeout;
46 - $this->closedResetInterval = $closedResetInterval;
47 - $this->state = $this->loadState();
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();
48 56 }
49 57
50 58 /**
51 59 * Returns true when a request should be allowed through.
@@ -59,16 +67,26 @@
59 67
60 68 if ($currentState === self::STATE_OPEN) {
61 69 if ($this->recoveryTimeoutElapsed()) {
62 70 $this->transitionTo(self::STATE_HALF_OPEN);
63 - return true;
71 + $currentState = self::STATE_HALF_OPEN;
72 + } else {
73 + throw new CircuitBreakerOpenException('سرویس باسلام موقتاً در دسترس نیست. لطفاً چند دقیقه دیگر تلاش کنید.', 503);
64 74 }
75 + }
65 76
66 - throw new CircuitBreakerOpenException('سرویس باسلام موقتاً در دسترس نیست. لطفاً چند دقیقه دیگر تلاش کنید.', 503);
77 + if ($currentState === self::STATE_HALF_OPEN) {
78 + if ($this->hasHalfOpenProbeInFlight() && !$this->halfOpenProbeExpired()) {
79 + throw new CircuitBreakerOpenException('در حال بررسی اتصال مجدد به باسلام هستیم. لطفاً چند لحظه دیگر دوباره تلاش کنید.', 503);
80 + }
81 +
82 + $this->state['probe_started_at'] = time();
83 + $this->saveState();
84 +
85 + return true;
67 86 }
68 87
69 - // HALF_OPEN: allow the single probe request through.
70 - return true;
88 + return false;
71 89 }
72 90
73 91 /**
74 92 * Records a successful request and resets the failure counter.
@@ -85,9 +103,10 @@
85 103 */
86 104 public function recordFailure(): void
87 105 {
88 106 $this->state['failure_count']++;
89 - $this->state['last_failure'] = time();
107 + $this->state['last_failure'] = time();
108 + $this->state['probe_started_at'] = null;
90 109
91 110 if ($this->state['state'] === self::STATE_HALF_OPEN) {
92 111 $this->transitionTo(self::STATE_OPEN);
93 112 return;
@@ -94,8 +113,9 @@
94 113 }
95 114
96 115 if ($this->state['failure_count'] >= $this->failureThreshold) {
97 116 $this->transitionTo(self::STATE_OPEN);
117 + return;
98 118 }
99 119
100 120 $this->saveState();
101 121 }
@@ -109,8 +129,18 @@
109 129 {
110 130 return $this->state['failure_count'];
111 131 }
112 132
133 + public function getLastFailure(): ?int
134 + {
135 + return empty($this->state['last_failure']) ? null : (int) $this->state['last_failure'];
136 + }
137 +
138 + public function getSnapshot(): array
139 + {
140 + return $this->state;
141 + }
142 +
113 143 public function reset(): void
114 144 {
115 145 $this->state = $this->defaultState();
116 146 $this->saveState();
@@ -124,16 +154,29 @@
124 154
125 155 return (time() - $this->state['last_failure']) >= $this->recoveryTimeout;
126 156 }
127 157
158 + private function hasHalfOpenProbeInFlight(): bool
159 + {
160 + return !empty($this->state['probe_started_at']);
161 + }
162 +
163 + private function halfOpenProbeExpired(): bool
164 + {
165 + if (empty($this->state['probe_started_at'])) {
166 + return true;
167 + }
168 +
169 + return (time() - $this->state['probe_started_at']) >= $this->halfOpenProbeTimeout;
170 + }
171 +
128 172 private function transitionTo(string $newState): void
129 173 {
130 - $previous = $this->state['state'];
131 -
132 174 if ($newState === self::STATE_CLOSED) {
133 175 $this->state = $this->defaultState();
134 176 } else {
135 - $this->state['state'] = $newState;
177 + $this->state['state'] = $newState;
178 + $this->state['probe_started_at'] = null;
136 179 }
137 180
138 181 $this->saveState();
139 182 }
@@ -143,8 +186,9 @@
143 186 if (is_array(self::$requestStateCache)) {
144 187 $state = self::$requestStateCache;
145 188 } else {
146 189 $stored = get_option(self::OPTION_KEY, null);
190 +
147 191 if (!is_array($stored)) {
148 192 $state = $this->defaultState();
149 193 } else {
150 194 $state = array_merge($this->defaultState(), $stored);
@@ -152,9 +196,8 @@
152 196
153 197 self::$requestStateCache = $state;
154 198 }
155 199
156 - // Reset failure_count every 30 minutes while the circuit stays CLOSED.
157 200 if (
158 201 $state['state'] === self::STATE_CLOSED &&
159 202 $state['failure_count'] > 0 &&
160 203 !empty($state['last_failure']) &&
@@ -159,14 +202,25 @@
159 202 $state['failure_count'] > 0 &&
160 203 !empty($state['last_failure']) &&
161 204 (time() - $state['last_failure']) >= $this->closedResetInterval
162 205 ) {
163 - $state['failure_count'] = 0;
164 - $state['last_failure'] = null;
206 + $state['failure_count'] = 0;
207 + $state['last_failure'] = null;
208 + $state['probe_started_at'] = null;
165 209 update_option(self::OPTION_KEY, $state, false);
166 210 self::$requestStateCache = $state;
167 211 }
168 212
213 + if (
214 + $state['state'] === self::STATE_HALF_OPEN &&
215 + !empty($state['probe_started_at']) &&
216 + $this->halfOpenProbeExpired()
217 + ) {
218 + $state['probe_started_at'] = null;
219 + update_option(self::OPTION_KEY, $state, false);
220 + self::$requestStateCache = $state;
221 + }
222 +
169 223 return $state;
170 224 }
171 225
172 226 private function saveState(): void
@@ -177,10 +231,11 @@
177 231
178 232 private function defaultState(): array
179 233 {
180 234 return [
181 - 'state' => self::STATE_CLOSED,
182 - 'failure_count' => 0,
183 - 'last_failure' => null,
235 + 'state' => self::STATE_CLOSED,
236 + 'failure_count' => 0,
237 + 'last_failure' => null,
238 + 'probe_started_at' => null,
184 239 ];
185 240 }
186 241 }