PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.8.6
ووسلام – همگام سازی ووکامرس و باسلام v1.8.6
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.8.6, at includes/Services/Api/CircuitBreaker.php

189 lines 5.3 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 private array $state;
41
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();
48 }
49
50 /**
51 * Returns true when a request should be allowed through.
52 * Throws a CircuitBreakerOpenException when the circuit is OPEN.
53 */
54 public function isAllowed(): bool
55 {
56 $currentState = $this->state['state'];
57
58 if ($currentState === self::STATE_CLOSED) return true;
59
60 if ($currentState === self::STATE_OPEN) {
61 if ($this->recoveryTimeoutElapsed()) {
62 $this->transitionTo(self::STATE_HALF_OPEN);
63 return true;
64 }
65
66 throw new CircuitBreakerOpenException('سرویس باسلا�
67
68 وقتاً در دسترس نیست. لطفاً چند دقیقه دیگر تلاش کنید.', 503);
69 }
70
71 // HALF_OPEN: allow the single probe request through.
72 return true;
73 }
74
75 /**
76 * Records a successful request and resets the failure counter.
77 */
78 public function recordSuccess(): void
79 {
80 $this->state['failure_count'] = 0;
81 $this->state['last_failure'] = null;
82 $this->transitionTo(self::STATE_CLOSED);
83 }
84
85 /**
86 * Records a failed request and opens the circuit when the threshold is reached.
87 */
88 public function recordFailure(): void
89 {
90 $this->state['failure_count']++;
91 $this->state['last_failure'] = time();
92
93 if ($this->state['state'] === self::STATE_HALF_OPEN) {
94 $this->transitionTo(self::STATE_OPEN);
95 return;
96 }
97
98 if ($this->state['failure_count'] >= $this->failureThreshold) {
99 $this->transitionTo(self::STATE_OPEN);
100 }
101
102 $this->saveState();
103 }
104
105 public function getState(): string
106 {
107 return $this->state['state'];
108 }
109
110 public function getFailureCount(): int
111 {
112 return $this->state['failure_count'];
113 }
114
115 public function reset(): void
116 {
117 $this->state = $this->defaultState();
118 $this->saveState();
119 }
120
121 private function recoveryTimeoutElapsed(): bool
122 {
123 if (empty($this->state['last_failure'])) {
124 return true;
125 }
126
127 return (time() - $this->state['last_failure']) >= $this->recoveryTimeout;
128 }
129
130 private function transitionTo(string $newState): void
131 {
132 $previous = $this->state['state'];
133
134 if ($newState === self::STATE_CLOSED) {
135 $this->state = $this->defaultState();
136 } else {
137 $this->state['state'] = $newState;
138 }
139
140 $this->saveState();
141 }
142
143 private function loadState(): array
144 {
145 if (is_array(self::$requestStateCache)) {
146 $state = self::$requestStateCache;
147 } else {
148 $stored = get_option(self::OPTION_KEY, null);
149 if (!is_array($stored)) {
150 $state = $this->defaultState();
151 } else {
152 $state = array_merge($this->defaultState(), $stored);
153 }
154
155 self::$requestStateCache = $state;
156 }
157
158 // Reset failure_count every 30 minutes while the circuit stays CLOSED.
159 if (
160 $state['state'] === self::STATE_CLOSED &&
161 $state['failure_count'] > 0 &&
162 !empty($state['last_failure']) &&
163 (time() - $state['last_failure']) >= $this->closedResetInterval
164 ) {
165 $state['failure_count'] = 0;
166 $state['last_failure'] = null;
167 update_option(self::OPTION_KEY, $state, false);
168 self::$requestStateCache = $state;
169 }
170
171 return $state;
172 }
173
174 private function saveState(): void
175 {
176 self::$requestStateCache = $this->state;
177 update_option(self::OPTION_KEY, $this->state, false);
178 }
179
180 private function defaultState(): array
181 {
182 return [
183 'state' => self::STATE_CLOSED,
184 'failure_count' => 0,
185 'last_failure' => null,
186 ];
187 }
188 }
189