Daily RSS Cache: invalidate cache base on the date

Currently the cache is only invalidated when the datastore changes, while it should rely on selected period of time.

Fixes #1659
This commit is contained in:
ArthurHoaro 2020-12-17 15:43:33 +01:00
parent ab4c170672
commit f00600a283
6 changed files with 213 additions and 68 deletions

View file

@ -1,34 +1,43 @@
<?php
declare(strict_types=1);
namespace Shaarli\Feed;
use DatePeriod;
/**
* Simple cache system, mainly for the RSS/ATOM feeds
*/
class CachedPage
{
// Directory containing page caches
private $cacheDir;
/** Directory containing page caches */
protected $cacheDir;
// Should this URL be cached (boolean)?
private $shouldBeCached;
/** Should this URL be cached (boolean)? */
protected $shouldBeCached;
// Name of the cache file for this URL
private $filename;
/** Name of the cache file for this URL */
protected $filename;
/** @var DatePeriod|null Optionally specify a period of time for cache validity */
protected $validityPeriod;
/**
* Creates a new CachedPage
*
* @param string $cacheDir page cache directory
* @param string $url page URL
* @param bool $shouldBeCached whether this page needs to be cached
* @param string $cacheDir page cache directory
* @param string $url page URL
* @param bool $shouldBeCached whether this page needs to be cached
* @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
*/
public function __construct($cacheDir, $url, $shouldBeCached)
public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod)
{
// TODO: check write access to the cache directory
$this->cacheDir = $cacheDir;
$this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
$this->shouldBeCached = $shouldBeCached;
$this->validityPeriod = $validityPeriod;
}
/**
@ -41,10 +50,20 @@ public function cachedVersion()
if (!$this->shouldBeCached) {
return null;
}
if (is_file($this->filename)) {
return file_get_contents($this->filename);
if (!is_file($this->filename)) {
return null;
}
return null;
if ($this->validityPeriod !== null) {
$cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename));
if (
$cacheDate < $this->validityPeriod->getStartDate()
|| $cacheDate > $this->validityPeriod->getEndDate()
) {
return null;
}
}
return file_get_contents($this->filename);
}
/**

View file

@ -86,9 +86,11 @@ public function index(Request $request, Response $response): Response
public function rss(Request $request, Response $response): Response
{
$response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
$type = DailyPageHelper::extractRequestedType($request);
$cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
$pageUrl = page_url($this->container->environment);
$cache = $this->container->pageCacheManager->getCachePage($pageUrl);
$cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
@ -96,7 +98,6 @@ public function rss(Request $request, Response $response): Response
}
$days = [];
$type = DailyPageHelper::extractRequestedType($request);
$format = DailyPageHelper::getFormatByType($type);
$length = DailyPageHelper::getRssLengthByType($type);
foreach ($this->container->bookmarkService->search() as $bookmark) {

View file

@ -4,6 +4,9 @@
namespace Shaarli\Helper;
use DatePeriod;
use DateTimeImmutable;
use Exception;
use Shaarli\Bookmark\Bookmark;
use Slim\Http\Request;
@ -40,31 +43,31 @@ public static function extractRequestedType(Request $request): string
* @param string|null $requestedDate Input string extracted from the request
* @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
*
* @return \DateTimeImmutable from input or latest bookmark.
* @return DateTimeImmutable from input or latest bookmark.
*
* @throws \Exception Type not supported.
* @throws Exception Type not supported.
*/
public static function extractRequestedDateTime(
string $type,
?string $requestedDate,
Bookmark $latestBookmark = null
): \DateTimeImmutable {
): DateTimeImmutable {
$format = static::getFormatByType($type);
if (empty($requestedDate)) {
return $latestBookmark instanceof Bookmark
? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
: new \DateTimeImmutable()
? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
: new DateTimeImmutable()
;
}
// W is not supported by createFromFormat...
if ($type === static::WEEK) {
return (new \DateTimeImmutable())
return (new DateTimeImmutable())
->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
;
}
return \DateTimeImmutable::createFromFormat($format, $requestedDate);
return DateTimeImmutable::createFromFormat($format, $requestedDate);
}
/**
@ -80,7 +83,7 @@ public static function extractRequestedDateTime(
*
* @see https://www.php.net/manual/en/datetime.format.php
*
* @throws \Exception Type not supported.
* @throws Exception Type not supported.
*/
public static function getFormatByType(string $type): string
{
@ -92,7 +95,7 @@ public static function getFormatByType(string $type): string
case static::DAY:
return 'Ymd';
default:
throw new \Exception('Unsupported daily format type');
throw new Exception('Unsupported daily format type');
}
}
@ -102,14 +105,14 @@ public static function getFormatByType(string $type): string
* and we don't want to alter original datetime.
*
* @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input
* @param DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime)
*
* @return \DateTimeInterface First DateTime of the time period
*
* @throws \Exception Type not supported.
* @throws Exception Type not supported.
*/
public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
{
switch ($type) {
case static::MONTH:
@ -119,7 +122,7 @@ public static function getStartDateTimeByType(string $type, \DateTimeImmutable $
case static::DAY:
return $requested->modify('Today midnight');
default:
throw new \Exception('Unsupported daily format type');
throw new Exception('Unsupported daily format type');
}
}
@ -129,14 +132,14 @@ public static function getStartDateTimeByType(string $type, \DateTimeImmutable $
* and we don't want to alter original datetime.
*
* @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input
* @param DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime)
*
* @return \DateTimeInterface Last DateTime of the time period
*
* @throws \Exception Type not supported.
* @throws Exception Type not supported.
*/
public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface
public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
{
switch ($type) {
case static::MONTH:
@ -146,7 +149,7 @@ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $re
case static::DAY:
return $requested->modify('Today 23:59:59');
default:
throw new \Exception('Unsupported daily format type');
throw new Exception('Unsupported daily format type');
}
}
@ -161,7 +164,7 @@ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $re
*
* @return string Localized time period description
*
* @throws \Exception Type not supported.
* @throws Exception Type not supported.
*/
public static function getDescriptionByType(
string $type,
@ -183,7 +186,7 @@ public static function getDescriptionByType(
}
return $out . format_date($requested, false);
default:
throw new \Exception('Unsupported daily format type');
throw new Exception('Unsupported daily format type');
}
}
@ -194,7 +197,7 @@ public static function getDescriptionByType(
*
* @return int number of elements
*
* @throws \Exception Type not supported.
* @throws Exception Type not supported.
*/
public static function getRssLengthByType(string $type): int
{
@ -206,7 +209,28 @@ public static function getRssLengthByType(string $type): int
case static::DAY:
return 30; // ~1 month
default:
throw new \Exception('Unsupported daily format type');
throw new Exception('Unsupported daily format type');
}
}
/**
* Get the number of items to display in the RSS feed depending on the given type.
*
* @param string $type month/week/day
* @param ?DateTimeImmutable $requested Currently only used for UT
*
* @return DatePeriod number of elements
*
* @throws Exception Type not supported.
*/
public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod
{
$requested = $requested ?? new DateTimeImmutable();
return new DatePeriod(
static::getStartDateTimeByType($type, $requested),
new \DateInterval('P1D'),
static::getEndDateTimeByType($type, $requested)
);
}
}

View file

@ -2,6 +2,7 @@
namespace Shaarli\Render;
use DatePeriod;
use Shaarli\Feed\CachedPage;
/**
@ -49,12 +50,21 @@ public function invalidateCaches(): void
$this->purgeCachedPages();
}
public function getCachePage(string $pageUrl): CachedPage
/**
* Get CachedPage instance for provided URL.
*
* @param string $pageUrl
* @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
*
* @return CachedPage
*/
public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage
{
return new CachedPage(
$this->pageCacheDir,
$pageUrl,
false === $this->isLoggedIn
false === $this->isLoggedIn,
$validityPeriod
);
}
}

View file

@ -40,10 +40,10 @@ protected function setUp(): void
*/
public function testConstruct()
{
new CachedPage(self::$testCacheDir, '', true);
new CachedPage(self::$testCacheDir, '', false);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false);
new CachedPage(self::$testCacheDir, '', true, null);
new CachedPage(self::$testCacheDir, '', false, null);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true, null);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false, null);
$this->addToAssertionCount(1);
}
@ -52,7 +52,7 @@ public function testConstruct()
*/
public function testCache()
{
$page = new CachedPage(self::$testCacheDir, self::$url, true);
$page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
@ -68,7 +68,7 @@ public function testCache()
*/
public function testShouldNotCache()
{
$page = new CachedPage(self::$testCacheDir, self::$url, false);
$page = new CachedPage(self::$testCacheDir, self::$url, false, null);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
@ -80,7 +80,7 @@ public function testShouldNotCache()
*/
public function testCachedVersion()
{
$page = new CachedPage(self::$testCacheDir, self::$url, true);
$page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
@ -96,7 +96,7 @@ public function testCachedVersion()
*/
public function testCachedVersionNoFile()
{
$page = new CachedPage(self::$testCacheDir, self::$url, true);
$page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename);
$this->assertEquals(
@ -110,7 +110,7 @@ public function testCachedVersionNoFile()
*/
public function testNoCachedVersion()
{
$page = new CachedPage(self::$testCacheDir, self::$url, false);
$page = new CachedPage(self::$testCacheDir, self::$url, false, null);
$this->assertFileNotExists(self::$filename);
$this->assertEquals(
@ -118,4 +118,43 @@ public function testNoCachedVersion()
$page->cachedVersion()
);
}
/**
* Return a page's cached content within date period
*/
public function testCachedVersionInDatePeriod()
{
$period = new \DatePeriod(
new \DateTime('yesterday'),
new \DateInterval('P1D'),
new \DateTime('tomorrow')
);
$page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
$this->assertFileExists(self::$filename);
$this->assertEquals(
'<p>Some content</p>',
$page->cachedVersion()
);
}
/**
* Return a page's cached content outside of date period
*/
public function testCachedVersionNotInDatePeriod()
{
$period = new \DatePeriod(
new \DateTime('yesterday noon'),
new \DateInterval('P1D'),
new \DateTime('yesterday midnight')
);
$page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
$this->assertFileExists(self::$filename);
$this->assertNull($page->cachedVersion());
}
}

View file

@ -4,6 +4,8 @@
namespace Shaarli\Helper;
use DateTimeImmutable;
use DateTimeInterface;
use Shaarli\Bookmark\Bookmark;
use Shaarli\TestCase;
use Slim\Http\Request;
@ -32,7 +34,7 @@ public function testExtractRequestedDateTime(
string $type,
string $input,
?Bookmark $bookmark,
\DateTimeInterface $expectedDateTime,
DateTimeInterface $expectedDateTime,
string $compareFormat = 'Ymd'
): void {
$dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
@ -71,8 +73,8 @@ public function testGetFormatByTypeExceptionUnknownType(): void
*/
public function testGetStartDatesByType(
string $type,
\DateTimeImmutable $dateTime,
\DateTimeInterface $expectedDateTime
DateTimeImmutable $dateTime,
DateTimeInterface $expectedDateTime
): void {
$startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
@ -84,7 +86,7 @@ public function testGetStartDatesByTypeExceptionUnknownType(): void
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable());
DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable());
}
/**
@ -92,8 +94,8 @@ public function testGetStartDatesByTypeExceptionUnknownType(): void
*/
public function testGetEndDatesByType(
string $type,
\DateTimeImmutable $dateTime,
\DateTimeInterface $expectedDateTime
DateTimeImmutable $dateTime,
DateTimeInterface $expectedDateTime
): void {
$endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
@ -105,7 +107,7 @@ public function testGetEndDatesByTypeExceptionUnknownType(): void
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable());
DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable());
}
/**
@ -113,7 +115,7 @@ public function testGetEndDatesByTypeExceptionUnknownType(): void
*/
public function testGeDescriptionsByType(
string $type,
\DateTimeImmutable $dateTime,
DateTimeImmutable $dateTime,
string $expectedDescription
): void {
$description = DailyPageHelper::getDescriptionByType($type, $dateTime);
@ -139,7 +141,7 @@ public function getDescriptionByTypeExceptionUnknownType(): void
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable());
DailyPageHelper::getDescriptionByType('nope', new DateTimeImmutable());
}
/**
@ -159,6 +161,29 @@ public function testGeRssLengthsByTypeExceptionUnknownType(): void
DailyPageHelper::getRssLengthByType('nope');
}
/**
* @dataProvider getCacheDatePeriodByType
*/
public function testGetCacheDatePeriodByType(
string $type,
DateTimeImmutable $requested,
DateTimeInterface $start,
DateTimeInterface $end
): void {
$period = DailyPageHelper::getCacheDatePeriodByType($type, $requested);
static::assertEquals($start, $period->getStartDate());
static::assertEquals($end, $period->getEndDate());
}
public function testGetCacheDatePeriodByTypeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getCacheDatePeriodByType('nope');
}
/**
* Data provider for testExtractRequestedType() test method.
*/
@ -229,9 +254,9 @@ public function getFormatsByType(): array
public function getStartDatesByType(): array
{
return [
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
[DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
[DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
[DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
];
}
@ -241,9 +266,9 @@ public function getStartDatesByType(): array
public function getEndDatesByType(): array
{
return [
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
[DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
[DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
[DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
];
}
@ -253,11 +278,11 @@ public function getEndDatesByType(): array
public function getDescriptionsByType(): array
{
return [
[DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
[DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
[DailyPageHelper::DAY, $date = new DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
[DailyPageHelper::DAY, $date = new DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
[DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
[DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
[DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
];
}
@ -276,7 +301,7 @@ public function getDescriptionsByTypeNotIncludeRelative(): array
}
/**
* Data provider for testGetDescriptionsByType() test method.
* Data provider for testGetRssLengthsByType() test method.
*/
public function getRssLengthsByType(): array
{
@ -286,4 +311,31 @@ public function getRssLengthsByType(): array
[DailyPageHelper::MONTH],
];
}
/**
* Data provider for testGetCacheDatePeriodByType() test method.
*/
public function getCacheDatePeriodByType(): array
{
return [
[
DailyPageHelper::DAY,
new DateTimeImmutable('2020-10-09 04:05:06'),
new \DateTime('2020-10-09 00:00:00'),
new \DateTime('2020-10-09 23:59:59'),
],
[
DailyPageHelper::WEEK,
new DateTimeImmutable('2020-10-09 04:05:06'),
new \DateTime('2020-10-05 00:00:00'),
new \DateTime('2020-10-11 23:59:59'),
],
[
DailyPageHelper::MONTH,
new DateTimeImmutable('2020-10-09 04:05:06'),
new \DateTime('2020-10-01 00:00:00'),
new \DateTime('2020-10-31 23:59:59'),
],
];
}
}