|
8 | 8 | use Tempest\Cache\Config\InMemoryCacheConfig; |
9 | 9 | use Tempest\Cache\GenericCache; |
10 | 10 | use Tempest\Cache\NotNumberException; |
| 11 | +use Tempest\Core\DeferredTasks; |
| 12 | +use Tempest\Core\Kernel\FinishDeferredTasks; |
11 | 13 | use Tempest\DateTime\Duration; |
12 | 14 | use Tests\Tempest\Integration\FrameworkIntegrationTestCase; |
13 | 15 |
|
@@ -226,4 +228,54 @@ public function test_clear(): void |
226 | 228 | $this->assertNull($cache->get('a')); |
227 | 229 | $this->assertNull($cache->get('b')); |
228 | 230 | } |
| 231 | + |
| 232 | + public function test_stale_while_revalidate(): void |
| 233 | + { |
| 234 | + $clock = $this->clock(); |
| 235 | + $cache = new GenericCache( |
| 236 | + cacheConfig: new InMemoryCacheConfig(), |
| 237 | + adapter: $pool = new ArrayAdapter(clock: $clock->toPsrClock()), |
| 238 | + deferredTasks: $tasks = $this->container->get(DeferredTasks::class), |
| 239 | + ); |
| 240 | + |
| 241 | + // Cache value can be stale for 1min, but will be refreshed in the background |
| 242 | + $retrieve = fn (string $value) => $cache->resolve('test', fn () => $value, expiration: Duration::minute(), stale: Duration::minute()); |
| 243 | + |
| 244 | + // We fetch the value within the allowed duration, there is no deferring |
| 245 | + $this->assertSame('update1', $retrieve('update1')); |
| 246 | + $this->assertSame('update1', $pool->getItem('test')->get()); |
| 247 | + $this->assertSame($clock->now()->plus(Duration::minute())->getTimestamp()->getSeconds(), $pool->getItem('tempest.stale-cache.stale-at.test')->get()); |
| 248 | + $this->assertEmpty($tasks->getTasks()); |
| 249 | + |
| 250 | + // After 30 seconds, we should still get the same value, with no plan for refreshing it |
| 251 | + $this->container->invoke(FinishDeferredTasks::class); |
| 252 | + $clock->plus(Duration::seconds(30)); |
| 253 | + $this->assertSame('update1', $retrieve('update2')); |
| 254 | + $this->assertSame($clock->now()->plus(Duration::seconds(30))->getTimestamp()->getSeconds(), $pool->getItem('tempest.stale-cache.stale-at.test')->get()); |
| 255 | + $this->assertEmpty($tasks->getTasks()); |
| 256 | + |
| 257 | + // We fetch it again after one minute, within stale window, so under the hood it gets deferred for refresh |
| 258 | + $this->container->invoke(FinishDeferredTasks::class); |
| 259 | + $clock->plus(Duration::seconds(30)); |
| 260 | + $this->assertSame('update1', $retrieve('update3')); |
| 261 | + $this->assertSame($clock->now()->getTimestamp()->getSeconds(), $pool->getItem('tempest.stale-cache.stale-at.test')->get()); |
| 262 | + $this->assertCount(1, $tasks->getTasks()); |
| 263 | + |
| 264 | + // After 1min30 total, within stale window again, we should get the previous value, |
| 265 | + // since it has been refreshed by the deferred task. However, we start the countdown |
| 266 | + // again, so we are within the fresh window. So, no update task will be deferred. |
| 267 | + $this->container->invoke(FinishDeferredTasks::class); |
| 268 | + $clock->plus(Duration::seconds(30)); |
| 269 | + $this->assertSame('update3', $retrieve('update4')); |
| 270 | + $this->assertSame($clock->now()->plus(Duration::seconds(30))->getTimestamp()->getSeconds(), $pool->getItem('tempest.stale-cache.stale-at.test')->get()); |
| 271 | + $this->assertEmpty($tasks->getTasks()); |
| 272 | + |
| 273 | + // We now try it again 2 minutes after last refresh, the value is |
| 274 | + // totally invalidated, with no plan on refreshing it yet since we just did. |
| 275 | + $this->container->invoke(FinishDeferredTasks::class); |
| 276 | + $clock->plus(Duration::minutes(2)); |
| 277 | + $this->assertSame('update5', $retrieve('update5')); |
| 278 | + $this->assertSame($clock->now()->plus(Duration::seconds(60))->getTimestamp()->getSeconds(), $pool->getItem('tempest.stale-cache.stale-at.test')->get()); |
| 279 | + $this->assertEmpty($tasks->getTasks()); |
| 280 | + } |
229 | 281 | } |
0 commit comments