|
2 | 2 |
|
3 | 3 | In the upcoming version of WoltLab Suite, the caching system has been reworked to provide a more flexible and efficient way to cache data. |
4 | 4 |
|
5 | | -To solve the problems: |
| 5 | +The previous implementation had three distinct shortcomings: |
6 | 6 |
|
7 | 7 | - Resetting a cache causes the next request that needs this cache to trigger a synchronous rebuild. |
8 | 8 | - Non-critical caches can sometimes be expensive to generate, causing dips in response times. |
9 | 9 | - The same rebuild can take place simultaneously by concurrent requests. |
10 | 10 |
|
| 11 | +The new caching systems solves this moving the any request for a cache rebuild into the request that triggered the cache invalidation. |
| 12 | + |
| 13 | +This will add the burden to rebuild these caches onto the current request which is usually an (in comparison) expensive request anyway. |
| 14 | +The idea behind this is that adding a few miliseconds to a request that already takes half a second makes no difference to the user. |
| 15 | +On the other hand, accessing a thread and suddenly have a significantly longer loading time is unexpected to the visitor. |
| 16 | + |
| 17 | +Performing a full cache reset will still have the same latency impact as before. |
| 18 | + |
11 | 19 | ## General Guidelines |
12 | 20 |
|
13 | | -- MUST NOT rely on any (runtime) cache |
14 | | -- Return a `CacheData` object instead of an `array` |
15 | | -- Optional, parameterized caches with state |
16 | | - - Use `readonly` properties in the constructor |
17 | | - ```PHP |
18 | | - final class FooCache extends AbstractTolerantCache { |
19 | | - public function __construct( |
20 | | - public readonly int $categoryID |
21 | | - ) {} |
22 | | - // Additional methods … |
23 | | - } |
24 | | - ``` |
| 21 | +- MUST NOT rely on any (runtime) cache. |
| 22 | +- Return a `CacheData` object instead of an `array`. |
| 23 | +- (Optional) For parameterized caches with state: |
| 24 | + - Use `readonly` properties in the constructor. |
| 25 | + ```php |
| 26 | + final class FooCache extends AbstractTolerantCache { |
| 27 | + public function __construct( |
| 28 | + public readonly int $categoryID |
| 29 | + ) {} |
| 30 | + // Additional methods … |
| 31 | + } |
| 32 | + ``` |
25 | 33 |
|
26 | 34 | ## Eager Caches |
27 | 35 |
|
28 | | -The eager cache has no lifetime and must be updated manually by the developer if the data changes. |
29 | | -`(new FooEagerCache())->rebuild()` |
| 36 | +The eager cache has no lifetime and must be rebuild manually by the developer if the data changes. |
| 37 | +An eager cache is guaranteed to be always present, either by fetching the cached data or by rebuilding it synchronously. |
| 38 | + |
| 39 | +```php |
| 40 | +(new FooCache())->rebuild() |
| 41 | +``` |
30 | 42 |
|
31 | 43 | #### Example |
32 | 44 |
|
33 | | -```PHP |
| 45 | +```php |
34 | 46 | /** |
35 | | -* @extends AbstractEagerCache<\stdClass> |
| 47 | +* @extends AbstractEagerCache<FooCacheData> |
36 | 48 | */ |
37 | | -final class FooEagerCache extends AbstractEagerCache |
| 49 | +final class FooCache extends AbstractEagerCache |
38 | 50 | { |
39 | 51 | public function __construct( |
40 | | - public readonly int $categoryID, |
| 52 | + public readonly bool $snafucated, |
41 | 53 | ) {} |
42 | | - |
| 54 | + |
43 | 55 | #[\Override] |
44 | | - protected function getCacheData(): \stdClass |
| 56 | + protected function getCacheData(): FooCacheData |
45 | 57 | { |
46 | | - // Load cache data from database |
47 | | - return new \stdClass(); |
| 58 | + // Load and return the data here … |
48 | 59 | } |
49 | 60 | } |
50 | 61 | ``` |
51 | 62 |
|
52 | | -The parameter `$categoryID` is certainly not required. |
53 | | -It can also be omitted. |
54 | | - |
55 | | -This cache can be used as follows |
| 63 | +Parameters for a stateful cache are passed through the constructor and are required to be marked as `readonly`. |
56 | 64 |
|
57 | | -```PHP |
58 | | -$cache = (new FooEagerCache(1))->getCache(); |
59 | | -// without a state |
60 | | -$cache = (new FooEagerCache())->getCache(); |
| 65 | +```php |
| 66 | +$cache = (new FooCache(true))->getCache(); |
61 | 67 | ``` |
62 | 68 |
|
63 | 69 | ## Tolerant Caches |
64 | 70 |
|
65 | | -The tolerant cache is a special cache that can return outdated content. |
66 | | -This must not cause any problems at runtime. |
| 71 | +A tolerant cache is similar to an eager cache but has a maximum lifetime after which it is queued up for a rebuild. |
| 72 | +Similar to the eager cache, it is also guaranteed to exist when queried, serving either cached data or rebuilding it synchronously. |
| 73 | + |
| 74 | +The main difference is that the tolerant cache is permitted to serve stale content. For example, a cache is set a lifetime of 300 seconds but querying it 307 seconds after its creation may still return the old data. |
| 75 | +The cache content will be refreshed eventually, but callees must not expect the data to be of a precise age. |
67 | 76 |
|
68 | 77 | The cache is updated by a background job when the lifetime expires or by a [probabilistic early expiration](https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration). |
| 78 | +The early expiration will randomly queue a tolerant cache for a rebuild before it exceeds its maximum lifetime. |
| 79 | +The odds of an early rebuild increase significantly the less time is remaining, making it very likely that a tolerant does not become stale. |
69 | 80 |
|
70 | 81 | #### Example |
71 | 82 |
|
72 | | -```PHP |
| 83 | +```php |
73 | 84 | /** |
74 | | -* @extends AbstractTolerantCache<\stdClass> |
| 85 | +* @extends AbstractTolerantCache<BarCacheData> |
75 | 86 | */ |
76 | | -final class FooAsyncCache extends AbstractTolerantCache |
| 87 | +final class BarCache extends AbstractTolerantCache |
77 | 88 | { |
78 | 89 | #[\Override] |
79 | 90 | public function getLifetime(): int |
80 | 91 | { |
81 | | - return 6000; |
| 92 | + // 3,600 seconds = 1 hour |
| 93 | + return 3_600; |
82 | 94 | } |
83 | | - |
| 95 | + |
84 | 96 | #[\Override] |
85 | | - protected function rebuildCacheData(): \stdClass |
| 97 | + protected function rebuildCacheData(): BarCacheData |
86 | 98 | { |
87 | | - // Load cache data from database |
88 | | - return new \stdClass(); |
| 99 | + // Load and return the data here … |
89 | 100 | } |
90 | 101 | } |
91 | 102 | ``` |
92 | 103 |
|
93 | 104 | This cache can be used as follows |
94 | 105 |
|
95 | | -```PHP |
96 | | -$cache = (new FooAsyncCache())->getCache(); |
| 106 | +```php |
| 107 | +$cache = (new BarCache())->getCache(); |
97 | 108 | // with a state |
98 | | -$cache = (new FooAsyncCache($parameterOne, $parameterTwo, …))->getCache(); |
| 109 | +$cache = (new BarCache($parameterOne, $parameterTwo, …))->getCache(); |
99 | 110 | ``` |
0 commit comments