/
githubmirror
/
framework
Обзор
Документация
Войти
/
githubmirror
/
framework
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
13.x
tests/Integration/Cache/RepositoryTest.php
409 строк
16 KB
Amirhf
Fix flexible() lock and defer label collisions in TaggedCache (#60626)
30 июн 2026, 15:50
Не верифицирован
30 июн 2026, 15:50
1f95687
Код
Авторство
О чём код?
<?php namespace Illuminate\Tests\Integration\Cache; use Illuminate\Cache\Events\KeyWritten; use Illuminate\Cache\Repository; use Illuminate\Foundation\Testing\LazilyRefreshDatabase; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Event; use Orchestra\Testbench\Attributes\WithMigration; use Orchestra\Testbench\TestCase; #[WithMigration('cache')] class RepositoryTest extends TestCase { use LazilyRefreshDatabase; public function testStaleWhileRevalidate(): void { Carbon::setTestNow('2000-01-01 00:00:00'); $cache = Cache::driver('array'); $count = 0; // Cache is empty. The value should be populated... $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(1, $value); $this->assertCount(0, defer()); $this->assertSame(1, $cache->get('foo')); $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Cache is fresh. The value should be retrieved from the cache and used... $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(1, $value); $this->assertCount(0, defer()); $this->assertSame(1, $cache->get('foo')); $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); Carbon::setTestNow(Carbon::now()->addSeconds(11)); // Cache is now "stale". The stored value should be used and a deferred // callback should be registered to refresh the cache. $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(1, $value); $this->assertCount(1, defer()); $this->assertSame(1, $cache->get('foo')); $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // We will hit it again within the same request. This should not queue // up an additional deferred callback as only one can be registered at // a time for each key. $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(1, $value); $this->assertCount(1, defer()); $this->assertSame(1, $cache->get('foo')); $this->assertSame(946684800, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // We will now simulate the end of the request lifecycle by executing the // deferred callback. This should refresh the cache. defer()->invoke(); $this->assertCount(0, defer()); $this->assertSame(2, $cache->get('foo')); // this has been updated! $this->assertSame(946684811, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // this has been updated! // Now the cache is fresh again... $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(2, $value); $this->assertCount(0, defer()); $this->assertSame(2, $cache->get('foo')); $this->assertSame(946684811, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Let's now progress time beyond the stale TTL... Carbon::setTestNow(Carbon::now()->addSeconds(21)); // Now the values should have left the cache. We should refresh. $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(3, $value); $this->assertCount(0, defer()); $this->assertSame(3, $cache->get('foo')); $this->assertSame(946684832, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Now lets see what happens when another request, job, or command is // also trying to refresh the same key at the same time. Will push past // the "fresh" TTL and register a deferred callback. Carbon::setTestNow(Carbon::now()->addSeconds(11)); $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(3, $value); $this->assertCount(1, defer()); $this->assertSame(3, $cache->get('foo')); $this->assertSame(946684832, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // Now we will execute the deferred callback but we will first acquire // our own lock. This means that the value should not be refreshed by // deferred callback. /** @var Lock */ $lock = $cache->lock('illuminate:cache:flexible:lock:foo'); $this->assertTrue($lock->acquire()); defer()->first()(); $this->assertSame(3, $value); $this->assertCount(1, defer()); $this->assertSame(3, $cache->get('foo')); $this->assertSame(946684832, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); $this->assertTrue($lock->release()); // Now we have cleared the lock we will, one last time, confirm that // the deferred callback does refresh the value when the lock is not active. defer()->invoke(); $this->assertCount(0, defer()); $this->assertSame(4, $cache->get('foo')); $this->assertSame(946684843, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // The last thing is to check that we don't refresh the cache in the // deferred callback if another thread has already done the work for us. // We will make the cache stale... Carbon::setTestNow(Carbon::now()->addSeconds(11)); $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(4, $value); $this->assertCount(1, defer()); $this->assertSame(4, $cache->get('foo')); $this->assertSame(946684843, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); // There is now a deferred callback ready to refresh the cache. We will // simulate another thread updating the value. $cache->putMany([ 'foo' => 99, Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo' => 946684863, ]); // then we will run the refresh callback defer()->invoke(); $value = $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }); $this->assertSame(99, $value); $this->assertCount(0, defer()); $this->assertSame(99, $cache->get('foo')); $this->assertSame(946684863, $cache->get(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'foo')); } public function testItHandlesStrayTtlKeyAfterMainKeyIsForgotten() { $cache = Cache::driver('array'); $count = 0; $value = $cache->flexible('count', [5, 10], function () use (&$count) { $count = 1; return $count; }); $this->assertSame(1, $value); $this->assertSame(1, $count); $cache->forget('count'); $value = $cache->flexible('count', [5, 10], function () use (&$count) { $count = 2; return $count; }); $this->assertSame(2, $value); $this->assertSame(2, $count); } public function testItImplicitlyClearsTtlKeysFromDatabaseCache() { $this->freezeTime(); $cache = Cache::driver('database'); $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->forget('count'); $this->assertEmpty($cache->getConnection()->table('cache')->get()); $this->assertTrue($cache->missing('count')); $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $this->travel(20)->seconds(); $cache->forgetIfExpired('count'); $this->assertEmpty($cache->getConnection()->table('cache')->get()); $this->assertTrue($cache->missing('count')); $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); } public function testItImplicitlyClearsTtlKeysFromFileDriver() { $this->freezeTime(); $cache = Cache::driver('file'); $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->forget('count'); $this->assertFalse($cache->getFilesystem()->exists($cache->path('count'))); $this->assertFalse($cache->getFilesystem()->exists($cache->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count'))); $this->assertTrue($cache->missing('count')); $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $cache->flexible('count', [5, 10], fn () => 1); $this->assertTrue($cache->has('count')); $this->assertTrue($cache->has(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); $this->travel(20)->seconds(); $this->assertTrue($cache->missing('count')); $this->assertFalse($cache->getFilesystem()->exists($cache->path('count'))); $this->assertFalse($cache->getFilesystem()->exists($cache->path(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count'))); $this->assertTrue($cache->missing(Repository::FLEXIBLE_CREATED_KEY_PREFIX.'count')); } public function testItCanAlwaysDefer() { $this->freezeTime(); $cache = Cache::driver('array'); $count = 0; // Cache is empty. The value should be populated... $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }, alwaysDefer: true); // First call to flexible() should not defer $this->assertCount(0, defer()); Carbon::setTestNow(Carbon::now()->addSeconds(11)); // Second callback should defer with always now true $cache->flexible('foo', [10, 20], function () use (&$count) { return ++$count; }, alwaysDefer: true); $this->assertCount(1, defer()); $this->assertTrue(defer()->first()->always); } public function testItRoundsDateTimeValuesToAccountForTimePassedDuringScriptExecution() { // do not freeze time as this test depends on time progressing duration execution. $cache = Cache::driver('array'); $events = []; Event::listen(function (KeyWritten $event) use (&$events) { $events[] = $event; }); $result = $cache->put('foo', 'bar', Carbon::now()->addSecond()); $this->assertTrue($result); $this->assertCount(1, $events); $this->assertSame('foo', $events[0]->key); $this->assertSame(1, $events[0]->seconds); } public function testWorksWithEnumKey() { $cache = Cache::driver('array'); // put / get / has / missing $cache->put(TestCacheKey::FOO, 'value'); $this->assertSame('value', $cache->get(TestCacheKey::FOO)); $this->assertSame(['foo' => 'value', 'bar' => null], $cache->get([TestCacheKey::FOO, TestCacheKey::BAR])); $this->assertTrue($cache->has(TestCacheKey::FOO)); $this->assertFalse($cache->missing(TestCacheKey::FOO)); // pull $this->assertSame('value', $cache->pull(TestCacheKey::FOO)); $this->assertNull($cache->get(TestCacheKey::FOO)); // add $this->assertTrue($cache->add(TestCacheKey::FOO, 'added', 3600)); $this->assertFalse($cache->add(TestCacheKey::FOO, 'duplicate', 3600)); $this->assertSame('added', $cache->get(TestCacheKey::FOO)); // forever $cache->forever(TestCacheKey::BAR, 'forever'); $this->assertSame('forever', $cache->get(TestCacheKey::BAR)); // remember / rememberForever / sear $this->assertSame('remember', $cache->remember(TestCacheKey::BAZ, 3600, fn () => 'remember')); $this->assertSame('forever', $cache->rememberForever(TestCacheKey::QUX, fn () => 'forever')); $this->assertSame('forever', $cache->sear(TestCacheKey::QUX, fn () => 'ignored')); // increment / decrement $cache->put(TestCacheKey::FOO, 5); $this->assertSame(6, $cache->increment(TestCacheKey::FOO)); $this->assertSame(5, $cache->decrement(TestCacheKey::FOO)); // forget $cache->put(TestCacheKey::FOO, 'x'); $this->assertTrue($cache->forget(TestCacheKey::FOO)); $this->assertNull($cache->get(TestCacheKey::FOO)); // flexible / withoutOverlapping $this->assertSame('flexible', $cache->flexible(TestCacheKey::FOO, [5, 10], fn () => 'flexible')); $this->assertSame('overlapping', $cache->withoutOverlapping(TestCacheKey::FOO, fn () => 'overlapping')); // many / getMultiple $cache->clear(); $this->assertSame(['foo' => null, 'bar' => null, 'baz' => null], $cache->many([TestCacheKey::FOO, TestCacheKey::BAR, TestCacheKey::BAZ])); $this->assertSame(['foo' => 'default', 'qux' => 'default'], $cache->getMultiple([TestCacheKey::FOO, TestCacheKey::QUX], 'default')); } public function testFlexibleDeferLabelIsScopedToTagGroup(): void { Carbon::setTestNow('2000-01-01 00:00:00'); $users = Cache::driver('array')->tags(['users']); $posts = Cache::driver('array')->tags(['posts']); // Populate both tagged caches with the same raw key. $users->flexible('profile', [10, 20], fn () => 'users'); $posts->flexible('profile', [10, 20], fn () => 'posts'); $this->assertCount(0, defer()); // Advance past the "fresh" TTL — both entries are now stale. Carbon::setTestNow(Carbon::now()->addSeconds(11)); $users->flexible('profile', [10, 20], fn () => 'users-refreshed'); $posts->flexible('profile', [10, 20], fn () => 'posts-refreshed'); // Each tagged cache must register its own deferred callback. Before the // fix, both calls produced the same defer label so one would be silently // dropped by DeferredCallbackCollection::forgetDuplicates(), leaving only // one refresh in the queue instead of two. $this->assertCount(2, defer()); defer()->invoke(); $this->assertSame('users-refreshed', $users->get('profile')); $this->assertSame('posts-refreshed', $posts->get('profile')); } public function testFlexibleLockIsScopedToTagGroup(): void { Carbon::setTestNow('2000-01-01 00:00:00'); $store = Cache::driver('array'); $users = $store->tags(['users']); $posts = $store->tags(['posts']); // Populate both tagged caches with the same raw key. $users->flexible('profile', [10, 20], fn () => 'users'); $posts->flexible('profile', [10, 20], fn () => 'posts'); // Make both stale. Carbon::setTestNow(Carbon::now()->addSeconds(11)); $users->flexible('profile', [10, 20], fn () => 'users-refreshed'); $posts->flexible('profile', [10, 20], fn () => 'posts-refreshed'); $this->assertCount(2, defer()); // Acquire the scoped lock that the users-tag refresh closure would use. // Before the fix, both tagged caches shared the same lock key so this // lock would also block the posts refresh — they are now independent. $usersLockKey = 'illuminate:cache:flexible:lock:'.$users->taggedItemKey('profile'); $usersLock = $store->lock($usersLockKey); $this->assertTrue($usersLock->acquire()); defer()->invoke(); // The users refresh was skipped because its lock was held. $this->assertSame('users', $users->get('profile')); // The posts refresh ran independently because its lock key is different. $this->assertSame('posts-refreshed', $posts->get('profile')); $usersLock->release(); } } enum TestCacheKey: string { case FOO = 'foo'; case BAR = 'bar'; case BAZ = 'baz'; case QUX = 'qux'; }