Skip to content

Commit dd75534

Browse files
committed
Fixed:
- static $successCount in CircuitBreaker → private property $successCount, reset properly in reset() - Raw level string passed to GeocachingHttpLoggerPlugin → normalized in enableHttpLoggingWithLogger() before use - UPGRADE-4.0.md token refresh section → corrected to distinguish token_storage+reference_code (storage-aware auth) from enableTokenRefresh() (actual 401 auto-refresh) - UPGRADE-4.0.md reliability examples → updated to use the real configureRetry() / configureFixedRetry() / enableCircuitBreaker() / enableReliability() array API
1 parent d54c044 commit dd75534

3 files changed

Lines changed: 73 additions & 39 deletions

File tree

UPGRADE-4.0.md

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,24 @@ The sections below describe what 4.0 adds. Nothing else you already use has been
119119

120120
### Automatic token refresh
121121

122-
Pass a `TokenStorageInterface` and a `reference_code` (the user's Geocaching reference code, e.g. `'PR12345'`) to `Options` to enable automatic access-token refresh on `401` responses. The SDK will refresh, store the new token, and retry the request transparently.
122+
Token refresh has two independent layers:
123+
124+
**Layer 1 — storage-aware authentication** (`token_storage` + `reference_code` in `Options`):
125+
Reads the freshest access token from storage on every request so that a token refreshed elsewhere (e.g. another process) is always used. Pass these options to enable it:
126+
127+
```php
128+
$options = new Options([
129+
'environment' => Environment::PRODUCTION,
130+
'access_token' => $storedAccessToken,
131+
'token_storage' => new MyTokenStorage(), // implements TokenStorageInterface
132+
'reference_code' => 'PR12345',
133+
]);
134+
```
135+
136+
**Layer 2 — automatic 401 refresh** (`enableTokenRefresh()`):
137+
Intercepts `401` responses, refreshes the token via OAuth, stores it, and retries the original request. Requires an OAuth provider instance:
123138

124139
```php
125-
use Geocaching\Options;
126-
use Geocaching\Enum\Environment;
127140
use League\OAuth2\Client\Provider\Geocaching as OAuthProvider;
128141

129142
$provider = new OAuthProvider([
@@ -133,17 +146,26 @@ $provider = new OAuthProvider([
133146
'environment' => 'production',
134147
]);
135148

136-
$options = new Options([
137-
'environment' => Environment::PRODUCTION,
138-
'access_token' => $storedAccessToken,
139-
'token_storage' => new MyTokenStorage(), // implements TokenStorageInterface
149+
$options->enableTokenRefresh([
150+
'reference_code' => 'PR12345',
151+
'storage' => new MyTokenStorage(),
152+
'oauth_provider' => $provider,
153+
]);
154+
155+
// Or with credentials (creates the provider for you):
156+
$options->enableTokenRefreshWithCredentials([
140157
'reference_code' => 'PR12345',
158+
'storage' => new MyTokenStorage(),
159+
'client_id' => 'your-client-id',
160+
'client_secret' => 'your-client-secret',
161+
'redirect_uri' => 'https://example.com/callback',
162+
'environment' => 'production',
141163
]);
142164
```
143165

144166
`TokenStorageInterface` and `TokenSet` come from `surfoo/oauth2-geocaching ^3.0`. See that package's `UPGRADE-3.0.md` for implementation details.
145167

146-
When `token_storage` and `reference_code` are omitted, the SDK behaves exactly as in 3.x with a static bearer token.
168+
When neither option is configured, the SDK behaves exactly as in 3.x with a static bearer token.
147169

148170
### HTTP request/response logging
149171

@@ -163,45 +185,51 @@ Each request gets a unique correlation ID so requests and responses can be match
163185

164186
### Reliability plugins
165187

166-
#### Retry
188+
#### Retry with exponential back-off
167189

168190
```php
169-
use Geocaching\Reliability\ExponentialBackoffStrategy;
170-
use Geocaching\Reliability\FixedDelayStrategy;
191+
$options->configureRetry([
192+
'max_attempts' => 3,
193+
'base_delay_ms' => 100, // 100ms, 200ms, 400ms, …
194+
'multiplier' => 2.0,
195+
'max_delay_ms' => 30000,
196+
]);
197+
```
171198

172-
// Exponential back-off: 100ms, 200ms, 400ms, …
173-
$options->enableRetry(
174-
maxRetries: 3,
175-
strategy: new ExponentialBackoffStrategy(initialDelayMs: 100)
176-
);
199+
#### Retry with fixed delay
177200

178-
// Fixed delay
179-
$options->enableRetry(
180-
maxRetries: 3,
181-
strategy: new FixedDelayStrategy(delayMs: 500)
182-
);
201+
```php
202+
$options->configureFixedRetry([
203+
'max_attempts' => 3,
204+
'delay_ms' => 500,
205+
]);
183206
```
184207

185208
#### Circuit breaker
186209

187210
```php
188-
$options->enableCircuitBreaker(
189-
failureThreshold: 5, // open after this many consecutive failures
190-
resetTimeout: 60 // seconds before attempting to close again
191-
);
211+
$options->enableCircuitBreaker([
212+
'failure_threshold' => 5, // open after this many consecutive failures
213+
'recovery_timeout' => 60, // seconds before transitioning to half-open
214+
'success_threshold' => 2, // successes in half-open to close again
215+
]);
192216
```
193217

194218
#### Combined reliability plugin
195219

196-
`ReliabilityPlugin` bundles retry and circuit breaker together:
220+
`enableReliability()` bundles retry and circuit breaker into a single plugin:
197221

198222
```php
199-
$options->enableReliability(
200-
maxRetries: 3,
201-
strategy: new ExponentialBackoffStrategy(100),
202-
failureThreshold: 5,
203-
resetTimeout: 60
204-
);
223+
$options->enableReliability([
224+
'circuit_breaker' => [
225+
'failure_threshold' => 5,
226+
'recovery_timeout' => 60,
227+
],
228+
'retry' => [
229+
'max_attempts' => 3,
230+
'base_delay_ms' => 100,
231+
],
232+
]);
205233
```
206234

207235
### New enum: `Attribute`

src/Options.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,16 @@ public function enableHttpLoggingWithLogger(
172172
bool $maskTokens = true,
173173
int $maxBodyLength = 1000
174174
): void {
175+
$validLevels = [
176+
LogLevel::DEBUG, LogLevel::INFO, LogLevel::NOTICE,
177+
LogLevel::WARNING, LogLevel::ERROR, LogLevel::CRITICAL,
178+
LogLevel::ALERT, LogLevel::EMERGENCY,
179+
];
180+
$normalizedLevel = in_array(strtolower($level), $validLevels, true) ? strtolower($level) : LogLevel::INFO;
181+
175182
$loggingPlugin = new GeocachingHttpLoggerPlugin(
176183
$logger,
177-
$level,
184+
$normalizedLevel,
178185
$logBodies,
179186
$maskTokens,
180187
$maxBodyLength

src/Reliability/CircuitBreaker.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class CircuitBreaker
2323

2424
private string $state = self::STATE_CLOSED;
2525
private int $failureCount = 0;
26+
private int $successCount = 0;
2627
private ?DateTimeImmutable $lastFailureTime = null;
2728
private ?DateTimeImmutable $nextRetryTime = null;
2829

@@ -94,6 +95,7 @@ public function reset(): void
9495
{
9596
$this->state = self::STATE_CLOSED;
9697
$this->failureCount = 0;
98+
$this->successCount = 0;
9799
$this->lastFailureTime = null;
98100
$this->nextRetryTime = null;
99101
}
@@ -130,13 +132,10 @@ private function isOpen(): bool
130132
private function onSuccess(): void
131133
{
132134
if ($this->state === self::STATE_HALF_OPEN) {
133-
// Check if we've had enough successes to close the circuit
134-
static $successCount = 0;
135-
$successCount++;
136-
137-
if ($successCount >= $this->successThreshold) {
135+
$this->successCount++;
136+
137+
if ($this->successCount >= $this->successThreshold) {
138138
$this->reset();
139-
$successCount = 0;
140139
}
141140
} else {
142141
// Reset failure count on success in normal operation

0 commit comments

Comments
 (0)