Skip to content

Commit 900e743

Browse files
committed
New double-tiered cache setup
1 parent fef0474 commit 900e743

File tree

9 files changed

+259
-123
lines changed

9 files changed

+259
-123
lines changed

README.md

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,25 @@ return [
3030
// Cache store to use (null = default store)
3131
'store' => env('CLOUDFLARE_CACHE_STORE', null),
3232

33-
// Cache keys used to store IP ranges
33+
// Primary cache keys ("current" – refreshed list) and fallback ("last_good" – permanent)
3434
'keys' => [
35-
'all' => 'cloudflare:ips',
36-
'v4' => 'cloudflare:ips:v4',
37-
'v6' => 'cloudflare:ips:v6',
35+
'current' => [
36+
'all' => 'cloudflare:ips:current',
37+
'v4' => 'cloudflare:ips:v4:current',
38+
'v6' => 'cloudflare:ips:v6:current',
39+
],
40+
'last_good' => [
41+
'all' => 'cloudflare:ips:last_good',
42+
'v4' => 'cloudflare:ips:v4:last_good',
43+
'v6' => 'cloudflare:ips:v6:last_good',
44+
],
3845
],
3946

40-
// Time to live in seconds (null = forever)
41-
'ttl' => env('CLOUDFLARE_CACHE_TTL', 60 * 60 * 24), // 24 hours
47+
// Time to live in seconds for the "current" list (null = forever). Default: 7 days.
48+
'ttl' => env('CLOUDFLARE_CACHE_TTL', 60 * 60 * 24 * 7),
49+
50+
// Allow falling back to the last known good list when current is missing/expired.
51+
'allow_stale' => env('CLOUDFLARE_ALLOW_STALE', true),
4252
],
4353

4454
// HTTP client settings for fetching IP ranges from Cloudflare
@@ -65,7 +75,7 @@ return [
6575
- Fetches Cloudflare IP ranges from:
6676
- https://www.cloudflare.com/ips-v4
6777
- https://www.cloudflare.com/ips-v6
68-
- Caches the lists for 24h by default
78+
- Caches the lists (default 7 days) and keeps a permanent fallback copy
6979
- Provides a command to keep the list up-to-date: `php artisan cloudflare:refresh`
7080
- Interact with the lists in your code via the `LaravelCloudflare` service:
7181
- `ipv4()`: get IPv4 addresses
@@ -92,21 +102,29 @@ use Illuminate\Support\Facades\Schedule;
92102
Schedule::command('cloudflare:refresh')->twiceDaily(); // or ->daily(), ->hourly(), etc.
93103
```
94104

95-
3. Add the list of IPs to the `TrustProxies` middleware in `bootstrap/app.php`:
105+
3. Trust Cloudflare proxies in `bootstrap/app.php`:
96106

97107
```php
98108
use Sorane\LaravelCloudflare\LaravelCloudflare;
99109

100110
->withMiddleware(function (Middleware $middleware) {
101-
$cloudflareIps = app(LaravelCloudflare::class)->all();
102-
$ipsToTrust = [
103-
// Add other IPs you want to trust here
104-
...$cloudflareIps,
105-
];
106-
$middleware->trustProxies(at: $ipsToTrust);
111+
// Your other middleware interactions here...
112+
113+
app()->booted(function () use ($middleware) {
114+
$cloudflareIps = app(LaravelCloudflare::class)->all();
115+
$ipsToTrust = [
116+
...$cloudflareIps,
117+
// Add any other proxies you want to trust here
118+
];
119+
$middleware->trustProxies(at: $ipsToTrust);
120+
});
107121
})
108122
```
109123

124+
Note: Use `app()->booted()` to ensure the application is fully booted and the cache is accessible.
125+
126+
Note 2: The `all()` method can never return an empty array, if you've at least once successfully run `cloudflare:refresh`. Read more about the caching design below.
127+
110128
4. Use the `cache-info` command to see information about the currently cached IPs.
111129

112130
```bash
@@ -126,6 +144,33 @@ $allIps = $cloudflare->all();
126144
$cacheInfo = $cloudflare->cacheInfo();
127145
```
128146

147+
## Caching design (current + last_good)
148+
149+
To avoid network calls during request handling and still remain resilient if Cloudflare is temporarily unreachable, the package maintains two cache layers:
150+
151+
* current – the actively refreshed list with a configurable TTL (default 7 days).
152+
* last_good – a permanent (forever) copy updated only after a fully successful refresh (both IPv4 and IPv6 lists fetched). It is never cleared on a failed refresh.
153+
154+
Lookup order for `ipv4()`, `ipv6()`, and `all()`:
155+
1. current list
156+
2. last_good list (when `allow_stale` is true)
157+
3. (logs a warning and returns an empty array only if neither exists – typically only before the very first refresh)
158+
159+
Advantages:
160+
* No request latency spikes from on-demand fetching.
161+
* Transient network failures do not drop trusted proxy IPs – the last_good list continues to be served.
162+
* Safe refresh semantics: last_good updates only after a fully successful fetch of both families.
163+
164+
Relevant config options (`config/laravel-cloudflare.php`):
165+
* `cache.ttl` – lifetime for current list (seconds, null = forever).
166+
* `cache.allow_stale` – whether to fall back to last_good when current missing.
167+
* Distinct key sets under `cache.keys.current` and `cache.keys.last_good`.
168+
169+
Operational recommendation:
170+
* Run `cloudflare:refresh` in your deployment pipeline and via the scheduler. A single success seeds both caching layers.
171+
* Keep the TTL of last_good infinite (null) to ensure a fallback is always available.
172+
* Regularly check logs and use `cloudflare:cache-info` to monitor cache status.
173+
129174
## Using with Laravel Octane
130175

131176
When you use this package to trust Cloudflare proxies via the `TrustProxies` middleware, while running behind Laravel Octane, keep the following in mind:

config/laravel-cloudflare.php

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,25 @@
55
// Cache store to use (null = default store)
66
'store' => env('CLOUDFLARE_CACHE_STORE', null),
77

8-
// Cache keys used to store IP ranges
8+
// Primary cache keys ("current" – refreshed list) and fallback ("last_good" – permanent)
99
'keys' => [
10-
'all' => 'cloudflare:ips',
11-
'v4' => 'cloudflare:ips:v4',
12-
'v6' => 'cloudflare:ips:v6',
10+
'current' => [
11+
'all' => 'cloudflare:ips:current',
12+
'v4' => 'cloudflare:ips:v4:current',
13+
'v6' => 'cloudflare:ips:v6:current',
14+
],
15+
'last_good' => [
16+
'all' => 'cloudflare:ips:last_good',
17+
'v4' => 'cloudflare:ips:v4:last_good',
18+
'v6' => 'cloudflare:ips:v6:last_good',
19+
],
1320
],
1421

15-
// Time to live in seconds (null = forever)
16-
'ttl' => env('CLOUDFLARE_CACHE_TTL', 60 * 60 * 24), // 24 hours
22+
// Time to live in seconds for the "current" list (null = forever). Default: 7 days.
23+
'ttl' => env('CLOUDFLARE_CACHE_TTL', 60 * 60 * 24 * 7),
24+
25+
// Allow falling back to the last known good list when current is missing/expired.
26+
'allow_stale' => env('CLOUDFLARE_ALLOW_STALE', true),
1727
],
1828

1929
// HTTP client settings for fetching IP ranges from Cloudflare

src/Commands/CloudflareCacheInfoCommand.php

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,34 @@ public function handle(): int
2323
$this->line('Configured TTL : '.($ttl === null ? 'forever' : ($ttl.'s')));
2424

2525
$this->newLine();
26-
$this->line('Entries:');
27-
28-
foreach (['v4', 'v6', 'all'] as $segment) {
29-
$segmentInfo = $info['keys'][$segment];
30-
$status = $segmentInfo['present'] ? 'cached' : 'missing';
31-
$line = ' '
32-
.str_pad($segment, 3, ' ', STR_PAD_RIGHT)
33-
.' key '
34-
.str_pad($segmentInfo['key'], 25, ' ', STR_PAD_RIGHT)
35-
.' status: '
36-
.str_pad($status, 7, ' ', STR_PAD_RIGHT)
37-
.' count: '
38-
.$segmentInfo['count'];
39-
$this->line($line);
26+
$this->line('Segments:');
27+
28+
foreach (['current', 'last_good'] as $group) {
29+
if (! isset($info['segments'][$group])) {
30+
continue;
31+
}
32+
$this->line(" {$group}:");
33+
foreach (['v4', 'v6', 'all'] as $label) {
34+
$segmentInfo = $info['segments'][$group][$label] ?? null;
35+
if ($segmentInfo === null) {
36+
continue;
37+
}
38+
$status = $segmentInfo['present'] ? 'cached' : 'missing';
39+
$line = ' '
40+
.str_pad($label, 3, ' ', STR_PAD_RIGHT)
41+
.' key '
42+
.str_pad($segmentInfo['key'], 30, ' ', STR_PAD_RIGHT)
43+
.' status: '
44+
.str_pad($status, 7, ' ', STR_PAD_RIGHT)
45+
.' count: '
46+
.$segmentInfo['count'];
47+
$this->line($line);
48+
}
49+
$this->newLine();
4050
}
4151

4252
$this->newLine();
43-
$this->comment('Use cloudflare:refresh to populate or refresh the cache.');
53+
$this->comment('Use cloudflare:refresh to populate or refresh the current cache (last_good updates on successful refresh).');
4454

4555
return self::SUCCESS;
4656
}

src/Commands/LaravelCloudflareCommand.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public function handle(): int
2222
$v6 = $service->ipv6();
2323
$all = $service->all();
2424

25-
$this->line('IPv4: '.count($v4).', IPv6: '.count($v6).', All: '.count($all));
26-
$this->comment('Cloudflare IP ranges refreshed and cached.');
25+
$this->line('IPv4 (current or fallback): '.count($v4).', IPv6 (current or fallback): '.count($v6).', All (current or fallback): '.count($all));
26+
$this->comment('Cloudflare IP ranges refreshed (current + last_good updated on success).');
2727

2828
return self::SUCCESS;
2929
}

0 commit comments

Comments
 (0)