Skip to content

Commit e765a5e

Browse files
committed
chore: adds APC as L1 cache at cache middleware
1 parent f5cf533 commit e765a5e

3 files changed

Lines changed: 129 additions & 34 deletions

File tree

app/Http/Middleware/CacheMiddleware.php

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?php namespace App\Http\Middleware;
22

3+
use App\Utils\Cache\APC;
34
use Closure;
45
use Illuminate\Http\JsonResponse;
56
use Illuminate\Support\Facades\Cache;
@@ -87,49 +88,78 @@ public function handle($request, Closure $next, $cache_lifetime, $cache_region =
8788
}
8889
}
8990
$status = 200;
91+
$wasAPCHit = false;
9092
$wasHit = false;
93+
$data = null;
94+
9195
if ($regionTag) {
9296
Log::debug("CacheMiddleware: using region tag {$regionTag} ip {$ip} agent {$agent}");
93-
$wasHit = Cache::tags($regionTag)->has($key);
94-
Log::debug($wasHit ? "CacheMiddleware: cache HIT (tagged)" : "CacheMiddleware: cache MISS (tagged)", [
95-
'tag' => $regionTag,
96-
'ip' => $ip,
97-
'agent' => $agent,
98-
'key' => $key,
99-
]);
100-
101-
$encoded = Cache::tags($regionTag)
102-
->remember($key, $cache_lifetime, function() use ($next, $request, $regionTag, $key, $cache_lifetime, &$status,$ip, $agent) {
97+
// try L1 APC
98+
$encoded = APC::get($key);
99+
$wasAPCHit = $encoded !== null;
100+
101+
if(!$wasAPCHit) {
102+
// then L2 Redis
103+
$wasHit = Cache::tags($regionTag)->has($key);
104+
Log::debug($wasHit ? "CacheMiddleware: cache HIT Redis (tagged)" : "CacheMiddleware: cache MISS (tagged)", [
105+
'tag' => $regionTag,
106+
'ip' => $ip,
107+
'agent' => $agent,
108+
'key' => $key,
109+
]);
110+
111+
$encoded = Cache::tags($regionTag)
112+
->remember($key, $cache_lifetime, function () use ($next, $request, $regionTag, $key, $cache_lifetime, &$status, $ip, $agent) {
113+
$resp = $next($request);
114+
if ($resp instanceof JsonResponse) {
115+
$status = $resp->getStatusCode();
116+
if ($status === 200) {
117+
return $this->encode($resp->getData(true));
118+
}
119+
}
120+
// don’t cache non-200 or non-JSON
121+
return Cache::get($key);
122+
});
123+
124+
125+
// backfill APC only if we actually have a value
126+
if ($encoded !== null) { // avoid null writes
127+
APC::put($key, $encoded, $cache_lifetime, $regionTag);
128+
}
129+
}
130+
$data = $this->decode($encoded);
131+
} else {
132+
// try L1 APC
133+
$encoded = APC::get($key);
134+
$wasAPCHit = !is_null($encoded);
135+
136+
if(!$wasAPCHit) {
137+
// then L2 Redis
138+
139+
$wasHit = Cache::has($key);
140+
141+
Log::debug($wasHit ? "CacheMiddleware: cache HIT" : "CacheMiddleware: cache MISS", [
142+
'ip' => $ip,
143+
'agent' => $agent,
144+
'key' => $key,
145+
]);
146+
147+
$encoded = Cache::remember($key, $cache_lifetime, function () use ($next, $request, $key, &$status, $ip, $agent) {
103148
$resp = $next($request);
104149
if ($resp instanceof JsonResponse) {
105150
$status = $resp->getStatusCode();
106-
if($status === 200) {
151+
if ($status === 200)
107152
return $this->encode($resp->getData(true));
108-
}
109153
}
110-
// don’t cache non-200 or non-JSON
111154
return Cache::get($key);
112155
});
113-
$data = $this->decode($encoded);
114-
} else {
115-
$wasHit = Cache::has($key);
116-
117-
Log::debug($wasHit ? "CacheMiddleware: cache HIT" : "CacheMiddleware: cache MISS", [
118-
'ip' => $ip,
119-
'agent' => $agent,
120-
'key' => $key,
121-
]);
122-
123-
$encoded = Cache::remember($key, $cache_lifetime, function() use ($next, $request, $key, &$status, $ip, $agent) {
124-
$resp = $next($request);
125-
if ($resp instanceof JsonResponse) {
126-
$status = $resp->getStatusCode();
127-
if($status === 200)
128-
return $this->encode($resp->getData(true));
156+
// store at APC
157+
if ($encoded !== null) { // avoid null writes
158+
APC::put($key, $encoded, $cache_lifetime);
129159
}
130-
return Cache::get($key);
131-
});
132-
$data = $this->decode($encoded);
160+
161+
$data = $this->decode($encoded);
162+
}
133163
}
134164
// safe guard
135165
if ($data === null) $data = is_array($encoded) ? $encoded : [];
@@ -143,7 +173,7 @@ public function handle($request, Closure $next, $cache_lifetime, $cache_region =
143173
$response->headers->addCacheControlDirective('must-revalidate', true);
144174
$response->headers->addCacheControlDirective('proxy-revalidate', true);
145175
$response->headers->add([
146-
'X-Cache-Result' => $wasHit ? 'HIT':'MISS',
176+
'X-Cache-Result' => $wasAPCHit ? 'HIT APC' : ($wasHit ? 'HIT REDIS' : 'MISS'),
147177
]);
148178
Log::debug( "CacheMiddleware: returning response", [
149179
'ip' => $ip,

app/Services/Utils/RedisCacheService.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* limitations under the License.
1313
**/
1414

15+
use App\Utils\Cache\APC;
1516
use Illuminate\Support\Facades\Cache;
1617
use Illuminate\Support\Facades\Log;
1718
use libs\utils\ICacheService;
@@ -331,7 +332,7 @@ public function ttl($key)
331332
return (int)$conn->ttl($key);
332333
}, 0);
333334
}
334-
335+
335336
/**
336337
* @param string $cache_region_key
337338
* @return void
@@ -340,6 +341,7 @@ public function clearCacheRegion(string $cache_region_key): void
340341
{
341342
if (!empty($cache_region_key)) {
342343
Cache::tags($cache_region_key)->flush();
344+
APC::apcClearRegion($cache_region_key);
343345
if($this->exists($cache_region_key)){
344346
Log::debug(sprintf("RedisCacheService::clearCacheRegion will clear cache region %s", $cache_region_key));
345347
$region_data = $this->getSingleValue($cache_region_key);

app/Utils/Cache/APC.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php namespace App\Utils\Cache;
2+
3+
use Illuminate\Support\Facades\Cache;
4+
5+
final class APC
6+
{
7+
private const APC_REGION_PREFIX = 'cm:region:';
8+
9+
private static function apc(): \Illuminate\Contracts\Cache\Repository
10+
{
11+
// Will throw if APCu not loaded; wrap if you want soft-disable.
12+
return Cache::store('apc');
13+
}
14+
15+
public static function get(string $key)
16+
{
17+
try {
18+
return self::apc()->get($key, null);
19+
} catch (\Throwable $e) {
20+
return null;
21+
}
22+
}
23+
24+
25+
private static function trackKey(string $regionTag, string $key, int $ttl): void
26+
{
27+
try {
28+
$setKey = self::APC_REGION_PREFIX.$regionTag;
29+
$list = self::apc()->get($setKey, []);
30+
if (!\is_array($list)) $list = [];
31+
$list[$key] = \time() + $ttl;
32+
if (\count($list) > 10000) { array_shift($list); }
33+
self::apc()->put($setKey, $list, $ttl);
34+
} catch (\Throwable $e) { /* ignore */ }
35+
}
36+
37+
public static function put(string $key, $value, int $ttl, ?string $regionTag = null): void
38+
{
39+
try {
40+
self::apc()->put($key, $value, $ttl);
41+
if ($regionTag) self::trackKey($regionTag, $key, $ttl);
42+
} catch (\Throwable $e) { /* ignore if APC not available */
43+
}
44+
}
45+
46+
public static function apcClearRegion(string $regionTag): int
47+
{
48+
try {
49+
$store = self::apc();
50+
$setKey = self::APC_REGION_PREFIX.$regionTag;
51+
$list = $store->get($setKey, []);
52+
$n = 0;
53+
if (\is_array($list)) {
54+
foreach (array_keys($list) as $k) {
55+
if ($store->forget($k)) $n++;
56+
}
57+
}
58+
$store->forget($setKey);
59+
return $n;
60+
} catch (\Throwable $e) { return 0; }
61+
}
62+
63+
}

0 commit comments

Comments
 (0)