Skip to content

[2.3] [WIP][HttpCache] StoreHouseKeepingInteface #6855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 136 additions & 2 deletions src/Symfony/Component/HttpKernel/HttpCache/Store.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Finder\Finder;

/**
* Store implements all the logic for storing cache metadata (Request and Response headers).
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Store implements StoreInterface
class Store implements StoreInterface, StoreHousekeepingInterface
{
protected $root;
private $keyCache;
private $locks;
private $responseContentList;

/**
* Constructor.
Expand All @@ -41,6 +43,107 @@ public function __construct($root)
}
$this->keyCache = new \SplObjectStorage();
$this->locks = array();
$this->responseContentList = array();
}

/**
* Deletes from disk all the stale response-header files and the response-content files.
*
* @return integer The number of the cleared entries
*/
public function clear()
{
$finder = new Finder();
$finder->depth('< 5')->files()->notName('*.lck')->in($this->root . DIRECTORY_SEPARATOR . 'md');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the finder looks problematic to me, it'd become very complicated to switch to the cache component or implement this in a custom store, the input data comes from a trick only possible with filesystem AFAIK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree, what do you suggest?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually here it'd be overkill, as it's an implementation relying on filesystem, there is no reason not to leverage an existing tool helping out.

I was just pointing out the fact that if the plan is to make HttpCache use the yet to come Cache component for 2.3, most of this PR would probably have to be rewritten anyway.

$counter = 0;
foreach ($finder as $file) {
$counter += $this->purgeKey($this->getKeyByPath($file->getPathname()));
}
foreach ($this->getResponseContentList() as $key => $isNeeded) {
if (!$isNeeded) {
$file = $this->getPath($key);
if ($this->deleteFile($file)) {
$counter++;
}
}
}

return $counter;
}

/**
* When the Response is stale it deletes the header file and add its response content file into the black list.
*
* @param string $key The cached key
*
* @return integer the number of file deleted
*/
protected function purgeKey($key)
{
$metadata = $this->getMetadata($key);
$expiredResponse = 0;
$contentDeleted = 0;
foreach ($metadata as $entry) {
$response = $this->restoreResponse($entry[1]);
$ResponseContentKey = $response->headers->get('x-content-digest');
if (!$response->isFresh()) {
$this->addToResponseContentList($ResponseContentKey, false);
$expiredResponse++;
} else {
$this->addToResponseContentList($ResponseContentKey, true);
}
}
// removes the file, only if all entries are expired
if (count($metadata) == $expiredResponse) {
$file = $this->getPath($key);
if ($this->deleteFile($file, false)) {
$contentDeleted++;
}
// deletes locks
$this->deleteFile($this->getPath($key . '.lck'), false);
}

return $contentDeleted;
}

/**
* Add a key of a response in the ResponseContentList.
*
* @param string $key
* @param Boolean $need
*
* @return Store
*/
private function addToResponseContentList($key, $need = true)
{
if (array_key_exists($key, $this->responseContentList)) {
$this->responseContentList[$key] = $this->responseContentList[$key] || $need;
} else {
$this->responseContentList[$key] = $need;
}

return $this;
}

/**
* Deletes a file.
*
* @param string $file
* @param Boolean $ignoreError
*
* @return Boolean
*/
protected function deleteFile($file, $ignoreError = true)
{
if (is_file($file)) {
if ($ignoreError) {
return @unlink($file);
}

return unlink($file);
}

return false;
}

/**
Expand Down Expand Up @@ -99,7 +202,7 @@ public function unlock(Request $request)
{
$file = $this->getPath($this->getCacheKey($request).'.lck');

return is_file($file) ? @unlink($file) : false;
return $this->deleteFile($file);
}

public function isLocked(Request $request)
Expand Down Expand Up @@ -368,11 +471,42 @@ private function save($key, $data)
@chmod($path, 0666 & ~umask());
}

/**
* Get the Path given the cache-key.
*
* @param string $key
*
* @return string
*/
public function getPath($key)
{
return $this->root.DIRECTORY_SEPARATOR.substr($key, 0, 2).DIRECTORY_SEPARATOR.substr($key, 2, 2).DIRECTORY_SEPARATOR.substr($key, 4, 2).DIRECTORY_SEPARATOR.substr($key, 6);
}

/**
* Get the cache key given the path.
*
* @param string $path The absolute path
*
* @return string
*/
public function getKeyByPath($path)
{
$path = substr($path, strlen($this->root.DIRECTORY_SEPARATOR));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should probably normalize the path (D_S) as the method is public

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that if we use realpath, when the given path doesn't exist we cannot calculate the relative key.


return str_replace(DIRECTORY_SEPARATOR, '', $path);
}

/**
* Get the responseContentList.
*
* @return array
*/
public function getResponseContentList()
{
return $this->responseContentList;
}

/**
* Returns a cache key for the given Request.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\HttpCache;

/**
* Interface implemented by HTTP cache stores, the stores should provide a cleaner
*
* @author Giulio De Donato <liuggio@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
*/
interface StoreHousekeepingInterface
{
/**
* clear all the stale entries
*
* @return int the number of the cleared entries
*/
public function clear();

}
117 changes: 117 additions & 0 deletions src/Symfony/Component/HttpKernel/Tests/HttpCache/StoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,123 @@ public function testLocking()
$this->assertFalse($this->store->isLocked($req));
}


public function testClearSimpleEntryNotExpired()
{
$key = $this->storeSimpleEntry();
$deletedItems = $this->store->clear();

$this->assertEquals($deletedItems, 0);
$this->assertFileExists($this->store->getPath($key));
}


public function testClearSimpleEntryExpired()
{
$key = $this->storeSimpleEntry();

$this->store->invalidate($this->request);
$deletedItems = $this->store->clear();

$this->assertEquals($deletedItems, 2);
$this->assertFileNotExists($this->store->getPath($key));
// the content file shouldn't exist
$r = new \ReflectionObject($this->store);
$m = $r->getMethod('generateContentDigest');
$m->setAccessible(true);
$contentDigest = $m->invoke($this->store, $this->response);
$this->assertFileNotExists($this->store->getPath($contentDigest));
}

public function testClearWithOneVaryExpired()
{
$req1 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Foo', 'HTTP_BAR' => 'Bar'));
$res1 = new Response('test 1', 200, array('Vary' => 'Foo Bar'));
$res1->setTtl(100);
$key = $this->store->write($req1, $res1);

$req2 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Bling', 'HTTP_BAR' => 'Bam'));
$res2 = new Response('test 2', 200, array('Vary' => 'Foo Bar'));
$res2->setTtl(0);
$key2 = $this->store->write($req2, $res2);

$req3 = Request::create('/test', 'get', array(), array(), array(), array('HTTP_FOO' => 'Baz', 'HTTP_BAR' => 'Boom'));
$res3 = new Response('test 3', 200, array('Vary' => 'Foo Bar'));
$res3->setTtl(100);
$key3 = $this->store->write($req3, $res3);

$deletedItems = $this->store->clear();

$this->assertEquals($deletedItems, 1);
$this->assertNull($this->store->lookup($req2));
$this->assertNotNull($this->store->lookup($req1));
$this->assertNotNull($this->store->lookup($req3));

$this->assertFileExists($this->store->getPath($key2));
}


public function testClearSameResponseDifferentRequest()
{
$req1 = Request::create('/test', 'get', array(), array(), array(), array());
$req2 = Request::create('/test.number-2', 'get', array(), array(), array(), array());
$res1 = new Response('test 3', 200, array('Vary' => 'Foo Bar'));
$res1->setTtl(100);

$key1 = $this->store->write($req1, $res1);
$key2 = $this->store->write($req2, $res1);

$this->store->invalidate($req1);

$deletedItems = $this->store->clear();

$this->assertEquals($deletedItems, 1);
$this->assertFileNotExists($this->store->getPath($key1));
$this->assertFileExists($this->store->getPath($key2));
}

public function testPurgeKey()
{
$cacheKey = $this->storeSimpleEntry();
$r = new \ReflectionObject($this->store);
$m = $r->getMethod('purgeKey');
$m->setAccessible(true);

$this->store->invalidate($this->request);
$this->assertEquals($m->invoke($this->store, $cacheKey), 1);
$this->assertFileNotExists($this->store->getPath($cacheKey));
}

public function testAddToResponseContentList()
{
$r = new \ReflectionObject($this->store);
$m = $r->getMethod('addToResponseContentList');
$m->setAccessible(true);
$key = 'key-start-true';
$result = $m->invoke($this->store, $key, true);
$responseList = $result->getResponseContentList();
$this->assertTrue($responseList[$key]);
$result = $m->invoke($result, $key, false);
$responseList = $result->getResponseContentList();
$this->assertTrue($responseList[$key]);

$key = 'key-start-false';
$result = $m->invoke($this->store, $key, false);
$responseList = $result->getResponseContentList();
$this->assertFalse($responseList[$key]);

$result = $m->invoke($this->store, $key, true);
$responseList = $result->getResponseContentList();
$this->assertTrue($responseList[$key]);
}

public function testGetKeyByPath()
{
$key = 'en5ed68c8380c7fd0ba2db6242f9ec06ac1dd2acf9';
$path = $this->store->getPath($key);
$this->assertEquals($key, $this->store->getKeyByPath($path));
}

protected function storeSimpleEntry($path = null, $headers = array())
{
if (null === $path) {
Expand Down