Skip to content

Commit

Permalink
Better caching with "tags~ish" (#2882)
Browse files Browse the repository at this point in the history
  • Loading branch information
ildyria committed Jan 10, 2025
1 parent 3ad3d6c commit ab43e2e
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 232 deletions.
20 changes: 20 additions & 0 deletions app/Assets/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Exceptions\Internal\ZeroModuloException;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use function Safe\ini_get;

Expand Down Expand Up @@ -281,4 +282,23 @@ public function exceptionTraceToText(\Exception $e): string
'line' => $err['line'] ?? '?',
'function' => $err['function']])->all());
}

/**
* Given a request return the uri WITH the query paramters.
* This makes sure that we handle the case where the query parameters are empty or contains an album id or pagination.
*
* @param Request $request
*
* @return string
*/
public function getUriWithQueryString(Request $request): string
{
/** @var array<string,mixed>|null $query */
$query = $request->query();
if ($query === null || $query === []) {
return $request->path();
}

return $request->path() . '?' . http_build_query($query);
}
}
10 changes: 9 additions & 1 deletion app/Events/AlbumRouteCacheUpdated.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ class AlbumRouteCacheUpdated
use SerializesModels;

/**
* Create a new event instance.
* This event is fired when the gallery is updated.
* Note that:
* - if $album_id is null, then all routes are to be cleared.
* - if $album_id is '', then only the root is updated.
* - if $album_id is an id, then only that id is updated.
*
* @param string|null $album_id
*
* @return void
*/
public function __construct(public ?string $album_id = null)
{
Expand Down
1 change: 1 addition & 0 deletions app/Facades/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* @method static int convertSize(string $size)
* @method static string decimalToDegreeMinutesSeconds(float $decimal, bool $type)
* @method static string censor(string $string, float $percentOfClear = 0.5)
* @method static string getUriWithQueryString(\Illuminate\Http\Request $request): string
*/
class Helpers extends Facade
{
Expand Down
58 changes: 12 additions & 46 deletions app/Http/Middleware/Caching/ResponseCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@

namespace App\Http\Middleware\Caching;

use App\Metadata\Cache\RouteCacheConfig;
use App\Metadata\Cache\RouteCacheManager;
use App\Metadata\Cache\RouteCacher;
use App\Models\Configs;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;

/**
* Response caching, this allows to speed up the reponse time of Lychee by hopefully a lot.
*/
class ResponseCache
{
private RouteCacheManager $route_cache_manager;

public function __construct(RouteCacheManager $route_cache_manager)
{
$this->route_cache_manager = $route_cache_manager;
public function __construct(
private RouteCacheManager $route_cache_manager,
private RouteCacher $route_cacher,
) {
}

/**
Expand All @@ -43,52 +40,21 @@ public function handle(Request $request, \Closure $next): mixed
return $next($request);
}

$config = $this->route_cache_manager->get_config($request->route()->uri);
$uri = $request->route()->uri;
$config = $this->route_cache_manager->get_config($uri);

// Check with the route manager if we can cache this route.
if ($config === false) {
return $next($request);
}

if (Cache::supportsTags()) {
return $this->cacheWithTags($request, $next, $config);
}

return $this->chacheWithoutTags($request, $next, $config);
}

/**
* This is the light version of caching: we cache only if the user is not logged in.
*
* @param Request $request
* @param \Closure $next
*
* @return mixed
*/
private function chacheWithoutTags(Request $request, \Closure $next, RouteCacheConfig $config): mixed
{
// We do not cache this.
if ($config->user_dependant && Auth::user() !== null) {
return $next($request);
}

$key = $this->route_cache_manager->get_key($request, $config);

return Cache::remember($key, Configs::getValueAsInt('cache_ttl'), fn () => $next($request));
}

/**
* This is the stronger version of caching.
*
* @param Request $request
* @param \Closure $next
*
* @return mixed
*/
private function cacheWithTags(Request $request, \Closure $next, RouteCacheConfig $config): mixed
{
$key = $this->route_cache_manager->get_key($request, $config);
$extras = [];
foreach ($config->extra as $extra) {
$extras[] = $request->input($extra) ?? '';
}

return Cache::tags([$config->tag->value])->remember($key, Configs::getValueAsInt('cache_ttl'), fn () => $next($request));
return $this->route_cacher->remember($key, $uri, Configs::getValueAsInt('cache_ttl'), fn () => $next($request), $extras);
}
}
102 changes: 18 additions & 84 deletions app/Listeners/AlbumCacheCleaner.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
use App\Enum\SmartAlbumType;
use App\Events\AlbumRouteCacheUpdated;
use App\Metadata\Cache\RouteCacheManager;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Metadata\Cache\RouteCacher;

/**
* We react to AlbumRouteCacheUpdated events and clear the associated cache.
*/
class AlbumCacheCleaner
{
/**
* Create the event listener.
*/
public function __construct(
private RouteCacheManager $route_cache_manager,
private RouteCacher $route_cacher,
) {
}

Expand All @@ -24,97 +27,28 @@ public function __construct(
*/
public function handle(AlbumRouteCacheUpdated $event): void
{
// The quick way.
if (Cache::supportsTags()) {
Cache::tags(CacheTag::GALLERY->value)->flush();

return;
}

$this->dropCachedRoutesWithoutExtra();

// By default we refresh all the smart albums.
$this->handleSmartAlbums();

if ($event->album_id === null) {
$this->handleAllAlbums();

return;
}
// this is a clear all.
$routes = $this->route_cache_manager->retrieve_routes_for_tag(CacheTag::GALLERY);
foreach ($routes as $route) {
$this->route_cacher->forgetRoute($route);
}

// Root album => already taken care of with the route without extra.
if ($event->album_id === '') {
return;
}

$this->handleAlbumId($event->album_id);
}

/**
* Drop cache for all routes without extra (meaning which do not depend on album_id).
*
* @return void
*/
private function dropCachedRoutesWithoutExtra(): void
{
$cached_routes_without_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, without_extra: true);
foreach ($cached_routes_without_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route);
Cache::forget($cache_key);
$routes = $this->route_cache_manager->retrieve_routes_for_tag(CacheTag::GALLERY, with_extra: false, without_extra: true);
foreach ($routes as $route) {
$this->route_cacher->forgetRoute($route);
}
}

/**
* Drop cache for all routes related to albums.
*
* @return void
*/
private function handleAllAlbums(): void
{
// The long way.
$cached_routes_with_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, with_extra: true);
DB::table('base_albums')->select('id')->pluck('id')->each(function ($id) use ($cached_routes_with_extra) {
$extra = ['album_id' => $id];
foreach ($cached_routes_with_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route, extras: $extra);
Cache::forget($cache_key);
}
// Clear smart albums. Simple.
collect(SmartAlbumType::cases())->each(function (SmartAlbumType $type) {
$this->route_cacher->forgetTag($type->value);
});
}

/**
* Drop cache fro all routes related to an album.
*
* @param string $album_id
*
* @return void
*/
private function handleAlbumId(string $album_id): void
{
$cached_routes_with_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, with_extra: true);
$extra = ['album_id' => $album_id];

foreach ($cached_routes_with_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route, extras: $extra);
Cache::forget($cache_key);
if ($event->album_id !== '') {
$this->route_cacher->forgetTag($event->album_id);
}
}

/**
* Drop cache for all smart albums too.
*
* @return void
*/
private function handleSmartAlbums(): void
{
$cached_routes_with_extra = $this->route_cache_manager->retrieve_keys_for_tag(CacheTag::GALLERY, with_extra: true);
// Also reset smart albums ;)
collect(SmartAlbumType::cases())->each(function (SmartAlbumType $type) use ($cached_routes_with_extra) {
$extra = ['album_id' => $type->value];
foreach ($cached_routes_with_extra as $route) {
$cache_key = $this->route_cache_manager->gen_key(uri: $route, extras: $extra);
Cache::forget($cache_key);
}
});
}
}
22 changes: 19 additions & 3 deletions app/Listeners/CacheListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Support\Facades\Log;

/**
* Just logging of Cache events.
*/
class CacheListener
{
/**
* Handle the event.
*/
public function handle(CacheHit|CacheMissed|KeyForgotten $event): void
public function handle(CacheHit|CacheMissed|KeyForgotten|KeyWritten $event): void
{
if (str_contains($event->key, 'lv:dev-lycheeOrg')) {
return;
Expand All @@ -24,10 +28,22 @@ public function handle(CacheHit|CacheMissed|KeyForgotten $event): void
}

match (get_class($event)) {
CacheMissed::class => Log::info('CacheListener: Miss for ' . $event->key),
CacheHit::class => Log::info('CacheListener: Hit for ' . $event->key),
CacheMissed::class => Log::debug('CacheListener: Miss for ' . $event->key),
CacheHit::class => Log::debug('CacheListener: Hit for ' . $event->key),
KeyForgotten::class => Log::info('CacheListener: Forgetting key ' . $event->key),
KeyWritten::class => $this->keyWritten($event),
default => '',
};
}

private function keyWritten(KeyWritten $event): void
{
if (!str_starts_with($event->key, 'api/')) {
Log::info('CacheListener: Writing key ' . $event->key);

return;
}

Log::debug('CacheListener: Writing key ' . $event->key . ' with value: ' . var_export($event->value, true));
}
}
13 changes: 4 additions & 9 deletions app/Listeners/TaggedRouteCacheCleaner.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use App\Events\TaggedRouteCacheUpdated;
use App\Metadata\Cache\RouteCacheManager;
use Illuminate\Support\Facades\Cache;
use App\Metadata\Cache\RouteCacher;

class TaggedRouteCacheCleaner
{
Expand All @@ -13,6 +13,7 @@ class TaggedRouteCacheCleaner
*/
public function __construct(
private RouteCacheManager $route_cache_manager,
private RouteCacher $route_cacher,
) {
}

Expand All @@ -21,15 +22,9 @@ public function __construct(
*/
public function handle(TaggedRouteCacheUpdated $event): void
{
$cached_routes = $this->route_cache_manager->retrieve_keys_for_tag($event->tag);

$cached_routes = $this->route_cache_manager->retrieve_routes_for_tag($event->tag);
foreach ($cached_routes as $route) {
$cache_key = $this->route_cache_manager->gen_key($route);
Cache::forget($cache_key);
}

if (Cache::supportsTags()) {
Cache::tags($event->tag->value)->flush();
$this->route_cacher->forgetRoute($route);
}
}
}
Loading

0 comments on commit ab43e2e

Please sign in to comment.