Skip to content

Commit

Permalink
IBX-1784: Introduced translation-aware HTTP cache invalidation on con…
Browse files Browse the repository at this point in the history
…tent publication (#166)

* Introduced translation-aware HTTP cache invalidation on content publication

* Added Behat tests

Co-authored-by: Marek Nocoń <[email protected]>
  • Loading branch information
mateuszbieniek and mnocon authored Jan 24, 2022
1 parent 18e07b7 commit 6b3a1da
Show file tree
Hide file tree
Showing 14 changed files with 301 additions and 5 deletions.
12 changes: 12 additions & 0 deletions .github/workflows/browser-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ jobs:
test-setup-phase-1: '--mode=standard --profile=httpCache --suite=setup'
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
varnish-translation-aware:
name: "Varnish integration tests (translation-aware)"
uses: ibexa/gh-workflows/.github/workflows/browser-tests.yml@main
with:
project-edition: 'oss'
project-version: '^3.3.x-dev'
setup: "doc/docker/base-dev.yml:doc/docker/varnish.yml:doc/docker/selenium.yml"
test-suite: '--mode=standard --profile=httpCache --suite=varnish-translation-aware'
test-setup-phase-1: '--mode=standard --profile=httpCache --suite=setup-translation-aware'
test-setup-phase-2: '--mode=standard --profile=httpCache --suite=setup'
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
varnish-token:
name: "Varnish integration tests with invalidate token"
uses: ibexa/gh-workflows/.github/workflows/browser-tests.yml@main
Expand Down
23 changes: 22 additions & 1 deletion behat_suites.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,22 @@ httpCache:
paths:
- '%paths.base%/vendor/ezsystems/ezplatform-http-cache/features/varnish'
filters:
tags: '@varnish'
tags: '@varnish&&~@translationAware'
contexts:
- EzSystems\Behat\API\Context\TestContext
- EzSystems\Behat\API\Context\ContentTypeContext
- EzSystems\Behat\Core\Context\TimeContext
- EzSystems\Behat\Core\Context\ConfigurationContext
- EzSystems\Behat\API\Context\ContentContext
- Ibexa\Behat\Browser\Context\BrowserContext
- Ibexa\Behat\Browser\Context\AuthenticationContext
- Behat\MinkExtension\Context\MinkContext
- Ibexa\Behat\Browser\Context\ContentPreviewContext
varnish-translation-aware:
paths:
- '%paths.base%/vendor/ezsystems/ezplatform-http-cache/features/varnish'
filters:
tags: '@varnish&&~@translationNotAware'
contexts:
- EzSystems\Behat\API\Context\TestContext
- EzSystems\Behat\API\Context\ContentTypeContext
Expand All @@ -38,6 +53,7 @@ httpCache:
- EzSystems\Behat\API\Context\ContentTypeContext
- EzSystems\Behat\Core\Context\ConfigurationContext
- EzSystems\Behat\API\Context\ContentContext
- EzSystems\Behat\API\Context\LanguageContext
setup-token:
paths:
- '%paths.base%/vendor/ezsystems/ezplatform-http-cache/features/setup/invalidateToken.feature'
Expand All @@ -48,3 +64,8 @@ httpCache:
- '%paths.base%/vendor/ezsystems/ezplatform-http-cache/features/setup/symfonyCache.feature'
contexts:
- EzSystems\Behat\Core\Context\FileContext
setup-translation-aware:
paths:
- '%paths.base%/vendor/ezsystems/ezplatform-http-cache/features/setup/translationAware.feature'
contexts:
- EzSystems\Behat\Core\Context\ConfigurationContext
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"autoload": {
"psr-4": {
"EzSystems\\PlatformHttpCacheBundle\\": "src",
"EzSystems\\PlatformHttpCacheBundle\\Tests\\": "tests"
"EzSystems\\PlatformHttpCacheBundle\\Tests\\": "tests",
"Ibexa\\HttpCache\\": "src"
}
},
"scripts": {
Expand Down
6 changes: 6 additions & 0 deletions docs/varnish/vcl/varnish5.vcl
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ sub vcl_backend_response {
) {
set beresp.do_gzip = true;
}

// Modify xkey header to add translation suffix
if (beresp.http.xkey && beresp.http.x-lang) {
set beresp.http.xkey = beresp.http.xkey + " " + regsuball(beresp.http.xkey, "(\S+)", "\1" + beresp.http.x-lang);
}
}

// Handle purge
Expand Down Expand Up @@ -323,6 +328,7 @@ sub vcl_deliver {
} else {
// Remove tag headers when delivering to non debug client
unset resp.http.xkey;
unset resp.http.x-lang;
// Sanity check to prevent ever exposing the hash to a non debug client.
unset resp.http.x-user-context-hash;
}
Expand Down
13 changes: 12 additions & 1 deletion features/setup/setup.feature
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
@setup
Feature: Set system to desired state before tests

@admin
@APIUser:admin
Scenario: Set up the system to test translations
Given Language "Polish" with code "pol-PL" exists
And Language "French" with code "fre-FR" exists
And I set configuration to "admin_group" siteaccess
| key | value |
| languages | eng-GB,pol-PL,fre-FR |
And I set configuration to "site" siteaccess
| key | value |
| languages | eng-GB,pol-PL,fre-FR |

@APIUser:admin
Scenario: Set up the system to test caching of subrequests
Given I create a "embeddedContentType" Content Type in "Content" with "embeddedContentType" identifier
| Field Type | Name | Identifier | Required | Searchable | Translatable |
Expand Down
2 changes: 1 addition & 1 deletion features/setup/symfonyCache.feature
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@setup
@setup @symfonyCache
Feature: Set up the system to use Symfony Proxy

Scenario: Set up the system to use Symfony Proxy
Expand Down
8 changes: 8 additions & 0 deletions features/setup/translationAware.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@setup @translationAware
Feature: Set system to use translation-aware cache invalidation

Scenario: Set up the system to use translation-aware cache invalidation
Given I append configuration to "parameters"
"""
ibexa.http_cache.translation_aware.enabled: true
"""
101 changes: 101 additions & 0 deletions features/varnish/translations.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
@varnish
Feature: As an site administrator I want my pages to be cached using Varnish

@APIUser:admin
Scenario Outline: Correct translation is displayed when a new translation is published
Given I create "Folder" Content items in root in "pol-PL"
| name | short_name |
| TestFolder | <itemName> |
And I am viewing the pages on siteaccess "site" as "<user>" "<password>"
And I visit "<itemName>" on siteaccess "site"
And I reload the page
And I see correct preview data for "Folder" Content Type
| field | value |
| title | <itemName> |
And response headers contain
| Header | Value |
| X-Cache | HIT |
When I edit "<itemName>" Content item in "eng-GB"
| short_name |
| <itemNameAfterEdit> |
And I reload the page
# Give Varnish time to fetch the backend response
And I wait 5 seconds
# Second reload is needed because of soft purging
And I reload the page
And I reload the page
Then I see correct preview data for "Folder" Content Type
| field | value |
| title | <itemNameAfterEdit> |
And response headers contain
| Header | Value |
| X-Cache | HIT |

Examples:
| user | password | itemName | itemNameAfterEdit |
| admin | publish | ItemPolskiAdmin | ItemEnglishAdmin |
| anonymous | | ItemPolskiAnon | ItemEnglishAnon |

@APIUser:admin @javascript @translationNotAware
Scenario: Main translation cache is purged when a fallback translation is edited
Given I am viewing the pages on siteaccess "site" as "admin" with password "publish"
And I create "embeddedContentType" Content items in root in "eng-GB"
| name |
| EmbeddedTranslationEnglish |
And I create "embeddingContentType_no_esi" Content items in root in "eng-GB"
| name | relation |
| EmbeddingTranslationEnglish | /EmbeddedTranslationEnglish |
And I edit "EmbeddedTranslationEnglish" Content item in "fre-FR"
| name |
| EmbeddedTranslationFrench |
And I start measuring time
And I visit "/EmbeddingTranslationEnglish" on siteaccess "site"
And the action took longer than 5 seconds
And I should see "EmbeddingTranslationEnglish"
And I should see "EmbeddedTranslationEnglish"
And I start measuring time
And I reload the page
And the action took no longer than 1 seconds
When I edit "EmbeddedTranslationEnglish" Content item in "fre-FR"
| name |
| EmbeddedTranslationFrenchEdited |
# Give Varnish time to get purged
And I wait 1 seconds
And I start measuring time
And I reload the page
And I reload the page
Then I should see "EmbeddingTranslationEnglish"
And I should see "EmbeddedTranslationEnglish"
And the action took longer than 5 seconds

@APIUser:admin @javascript @translationAware
Scenario: Main translation cache is not purged when a fallback translation is edited
Given I am viewing the pages on siteaccess "site" as "admin" with password "publish"
And I create "embeddedContentType" Content items in root in "eng-GB"
| name |
| EmbeddedTranslationEnglish |
And I create "embeddingContentType_no_esi" Content items in root in "eng-GB"
| name | relation |
| EmbeddingTranslationEnglish | /EmbeddedTranslationEnglish |
And I edit "EmbeddedTranslationEnglish" Content item in "fre-FR"
| name |
| EmbeddedTranslationFrench |
And I start measuring time
And I visit "/EmbeddingTranslationEnglish" on siteaccess "site"
And the action took longer than 5 seconds
And I should see "EmbeddingTranslationEnglish"
And I should see "EmbeddedTranslationEnglish"
And I start measuring time
And I reload the page
And the action took no longer than 1 seconds
When I edit "EmbeddedTranslationEnglish" Content item in "fre-FR"
| name |
| EmbeddedTranslationFrenchEdited |
# Give Varnish time to get purged
And I wait 1 seconds
And I start measuring time
And I reload the page
And I reload the page
Then I should see "EmbeddingTranslationEnglish"
And I should see "EmbeddedTranslationEnglish"
And the action took no longer than 1 seconds
57 changes: 57 additions & 0 deletions src/EventSubscriber/AddContentLanguageHeaderSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

/**
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\HttpCache\EventSubscriber;

use eZ\Publish\API\Repository\Values\Content\Content;
use eZ\Publish\Core\MVC\Symfony\View\CachableView;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;

final class AddContentLanguageHeaderSubscriber implements EventSubscriberInterface
{
public const CONTENT_LANGUAGE_HEADER = 'x-lang';

/** @var bool */
private $isTranslationAware;

public function __construct(bool $isTranslationAware)
{
$this->isTranslationAware = $isTranslationAware;
}

public function onKernelResponse(ResponseEvent $event)
{
if (!$this->isTranslationAware || HttpKernelInterface::MASTER_REQUEST != $event->getRequestType()) {
return;
}

$request = $event->getRequest();
$view = $request->attributes->get('view');
if (!$view instanceof CachableView || !$view->isCacheEnabled()) {
return;
}

$content = $request->attributes->get('content');
if ($content instanceof Content) {
$event->getResponse()->headers->add([self::CONTENT_LANGUAGE_HEADER => $content->getDefaultLanguageCode()]);
}
}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return [
KernelEvents::RESPONSE => 'onKernelResponse',
];
}
}
63 changes: 62 additions & 1 deletion src/EventSubscriber/CachePurge/ContentEventsSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,36 @@
use eZ\Publish\API\Repository\Events\Content\RevealContentEvent;
use eZ\Publish\API\Repository\Events\Content\UpdateContentEvent;
use eZ\Publish\API\Repository\Events\Content\UpdateContentMetadataEvent;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use eZ\Publish\API\Repository\Values\ContentType\ContentType;
use eZ\Publish\API\Repository\Values\ContentType\FieldDefinition;
use eZ\Publish\SPI\Persistence\Content\Handler as ContentHandler;
use eZ\Publish\SPI\Persistence\Content\Location\Handler as LocationHandler;
use eZ\Publish\SPI\Persistence\URL\Handler as UrlHandler;
use EzSystems\PlatformHttpCacheBundle\Handler\ContentTagInterface;
use EzSystems\PlatformHttpCacheBundle\PurgeClient\PurgeClientInterface;

final class ContentEventsSubscriber extends AbstractSubscriber
{
/** @var \eZ\Publish\SPI\Persistence\Content\Handler */
private $contentHandler;

/** @var bool */
private $isTranslationAware;

public function __construct(
PurgeClientInterface $purgeClient,
LocationHandler $locationHandler,
UrlHandler $urlHandler,
ContentHandler $contentHandler,
bool $isTranslationAware
) {
parent::__construct($purgeClient, $locationHandler, $urlHandler);

$this->isTranslationAware = $isTranslationAware;
$this->contentHandler = $contentHandler;
}

public static function getSubscribedEvents(): array
{
return [
Expand Down Expand Up @@ -93,13 +119,41 @@ public function onHideContentEvent(HideContentEvent $event): void

public function onPublishVersionEvent(PublishVersionEvent $event): void
{
$contentId = $event->getContent()->getVersionInfo()->getContentInfo()->id;
$content = $event->getContent();
$versionInfo = $content->getVersionInfo();
$contentType = $content->getContentType();
$contentId = $versionInfo->getContentInfo()->id;

$tags = array_merge(
$this->getContentTags($contentId),
$this->getContentLocationsTags($contentId)
);

$initialLanguageCode = $versionInfo->getInitialLanguage()->languageCode;
$mainLanguageCode = $versionInfo->getContentInfo()->mainLanguageCode;

$isNewTranslation = true;
try {
$prevVersionInfo = $this->contentHandler->loadVersionInfo($contentId, $event->getVersionInfo()->getContentInfo()->currentVersionNo);
$isNewTranslation = !in_array($initialLanguageCode, $prevVersionInfo->languageCodes);
} catch (NotFoundException $e) {
}

if (
!$this->isTranslationAware ||
$isNewTranslation ||
($initialLanguageCode === $mainLanguageCode && !$this->isContentTypeFullyTranslatable($contentType))
) {
$this->purgeClient->purge($tags);

return;
}

$tags = array_map(static function (string $tag) use ($initialLanguageCode): string {
return $tag . $initialLanguageCode;
}, $tags);
$tags[] = ContentTagInterface::CONTENT_ALL_TRANSLATIONS_PREFIX . $contentId;

$this->purgeClient->purge($tags);
}

Expand Down Expand Up @@ -132,4 +186,11 @@ public function onUpdateContentMetadataEvent(UpdateContentMetadataEvent $event):
$this->getContentTags($contentId)
);
}

private function isContentTypeFullyTranslatable(ContentType $contentType): bool
{
return !$contentType->getFieldDefinitions()->any(static function (FieldDefinition $fieldDefinition): bool {
return !$fieldDefinition->isTranslatable;
});
}
}
1 change: 1 addition & 0 deletions src/Handler/ContentTagInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
interface ContentTagInterface
{
public const CONTENT_PREFIX = 'c';
public const CONTENT_ALL_TRANSLATIONS_PREFIX = 'ca';
public const LOCATION_PREFIX = 'l';
public const PARENT_LOCATION_PREFIX = 'pl';
public const PATH_PREFIX = 'p';
Expand Down
2 changes: 2 additions & 0 deletions src/Resources/config/default_settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ parameters:
ezplatform.http_cache.store.root: "%kernel.cache_dir%/http_cache"
ezplatform.http_cache.invalidate_token.ttl: 86400
ezplatform.http_cache.no_vary.routes: ['ezplatform.httpcache.invalidatetoken']
ibexa.http_cache.translation_aware.enabled.default_value: false
ibexa.http_cache.translation_aware.enabled: "%env(default:ibexa.http_cache.translation_aware.enabled.default_value:bool:HTTPCACHE_TRANSLATION_AWARE_ENABLED)%"
Loading

0 comments on commit 6b3a1da

Please sign in to comment.