diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1cb260ff..c198d562 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -173,7 +173,7 @@ jobs: composer update ${{ matrix.dependencies == 'lowest' && '--prefer-lowest' || '' }} ${{ env.COMPOSER_FLAGS }} ${{ matrix.php-version == '8.5' && '--ignore-platform-req=ext-redis --ignore-platform-req=ext-memcached' || '' }} - name: Run PHPUnit tests - run: ./vendor/bin/phpunit --coverage-clover=coverage.xml + run: ./vendor/bin/phpunit ${{ (matrix.os == 'ubuntu-latest' && matrix.php-version == '8.4' && matrix.dependencies == 'highest') && '--coverage-clover=coverage.xml' || '' }} - name: Upload coverage report if: matrix.os == 'ubuntu-latest' && matrix.php-version == '8.4' && matrix.dependencies == 'highest' diff --git a/src/ResourceStorage.php b/src/ResourceStorage.php index a3d3b1da..0768eb06 100644 --- a/src/ResourceStorage.php +++ b/src/ResourceStorage.php @@ -207,8 +207,12 @@ public function saveDonutView(ResourceObject $ro, int|null $ttl): bool /** @return list */ private function getTags(ResourceObject $ro): array { - $etag = $ro->headers['ETag']; - $tags = [$etag, ($this->uriTag)($ro->uri)]; + // ETag is intentionally NOT used as an invalidation tag. The cache entry is + // purged by its URI tag (deleteEtag/invalidateTags) and surrogate keys; no code + // path ever invalidates by ETag. Because ETag is content-versioned, registering it + // as a tag produced one non-volatile tag Set per content version that, under a + // volatile-* eviction policy, is never reclaimed - leaking memory without being read. + $tags = [($this->uriTag)($ro->uri)]; if (isset($ro->headers[Header::SURROGATE_KEY])) { $tags = array_merge($tags, explode(' ', $ro->headers[Header::SURROGATE_KEY])); } diff --git a/tests/ResourceStorageTest.php b/tests/ResourceStorageTest.php index 71854af0..3f6516b3 100644 --- a/tests/ResourceStorageTest.php +++ b/tests/ResourceStorageTest.php @@ -56,4 +56,18 @@ public function testSaveGetStatic(): void $donut = $this->storage->getDonut($this->ro->uri); $this->assertInstanceOf(ResourceDonut::class, $donut); } + + public function testEtagIsNotRegisteredAsInvalidationTag(): void + { + $this->ro->headers['ETag'] = 'test-etag-value'; + $this->storage->saveValue($this->ro, 0); + + // ETag is not an invalidation tag: invalidating by the ETag value must NOT purge the entry. + $this->storage->invalidateTags(['test-etag-value']); + $this->assertNotNull($this->storage->get($this->ro->uri)); + + // The URI tag still invalidates the same entry. + $this->storage->invalidateTags([(new UriTag())($this->ro->uri)]); + $this->assertNull($this->storage->get($this->ro->uri)); + } }