Skip to content

Commit

Permalink
Merge pull request #43 from City-of-Helsinki/revert-42-UHF-8526
Browse files Browse the repository at this point in the history
Revert "UHF-8526: Use PubSub to invalidate caches"
  • Loading branch information
dire authored Sep 21, 2023
2 parents 668049d + b9786aa commit 5c0f648
Show file tree
Hide file tree
Showing 15 changed files with 275 additions and 114 deletions.
8 changes: 1 addition & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,7 @@ Only main-navigation has syncing option. Other navigations are created in Etusiv
- run `drush upwd helfi-admin 123` to update admin password. it is used as api key in other instances.
- Setup any other instance with helfi_navigation module enabled.
- Add following line to local.settings.php. Otherwise syncing global navigation won't work
```php
$config['helfi_api_base.api_accounts']['vault'][] = [
'id' => 'helfi_navigation',
'plugin' => 'authorization_token',
'data' => base64_encode('helfi-admin:123'),
];
```
- `$config['helfi_navigation.api']['key'] = base64_encode('helfi-admin:123');`

### Steps after both instances are up and running.
1. Edit and save menu on any instance with helfi_navigation module enabled.
Expand Down
42 changes: 28 additions & 14 deletions helfi_navigation.module
Original file line number Diff line number Diff line change
Expand Up @@ -93,50 +93,64 @@ function helfi_navigation_preprocess_menu__external_menu__fallback(&$variables)
* Implements hook_ENTITY_TYPE_update().
*/
function helfi_navigation_menu_update(MenuInterface $entity) : void {
_helfi_navigation_queue_item($entity->id(), $entity->language()->getId(), 'update');
if ($entity->id() === 'main') {
_helfi_navigation_queue_item();
}
}

/**
* Implements hook_ENTITY_TYPE_update().
*/
function helfi_navigation_menu_link_content_update(MenuLinkContentInterface $entity) : void {
_helfi_navigation_queue_item($entity->getMenuName(), $entity->language()->getId(), 'update');
if ($entity->getMenuName() === 'main') {
_helfi_navigation_queue_item();
}
}

/**
* Implements hook_ENTITY_TYPE_insert().
*/
function helfi_navigation_menu_link_content_insert(MenuLinkContentInterface $entity) : void {
_helfi_navigation_queue_item($entity->getMenuName(), $entity->language()->getId(), 'insert');
if ($entity->getMenuName() === 'main') {
_helfi_navigation_queue_item();
}
}

/**
* Implements hook_ENTITY_TYPE_delete().
*/
function helfi_navigation_menu_link_content_delete(MenuLinkContentInterface $entity) : void {
_helfi_navigation_queue_item($entity->getMenuName(), $entity->language()->getId(), 'delete');
if ($entity->getMenuName() === 'main') {
_helfi_navigation_queue_item();
}
}

/**
* Create menu update queue item.
*/
function _helfi_navigation_queue_item(string $menuName, string $langcode, string $action) : void {
if (!\Drupal::service('helfi_navigation.api_authorization')->getAuthorization()) {
function _helfi_navigation_queue_item() : void {
if (!\Drupal::config('helfi_navigation.api')->get('key')) {
return;
}
$queue = Drupal::queue('helfi_navigation_menu_queue');

static $items = [];

$key = sprintf('%s:%s:%s', $menuName, $langcode, $action);

// Queue item once per request.
if (!isset($items[$key])) {
$queue->createItem(['menu' => $menuName, 'language' => $langcode]);
$items[$key] = $key;
// Queue items only when queue is empty.
if ($queue->numberOfItems() === 0) {
foreach (\Drupal::languageManager()->getLanguages() as $language) {
$queue->createItem($language->getId());
}
}
}

/**
* Implements hook_cron().
*/
function helfi_navigation_cron() : void {
/** @var \Drupal\helfi_navigation\CacheWarmer $warmer */
$warmer = Drupal::service('helfi_navigation.cache_warmer');
$warmer->warm();
}

/**
* Implements hook_page_attachments().
*/
Expand Down
11 changes: 8 additions & 3 deletions helfi_navigation.services.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
parameters:
helfi_navigation.request_timeout: 15
services:
logger.channel.helfi_navigation:
parent: logger.channel_base
Expand All @@ -18,7 +16,6 @@ services:
- '@helfi_api_base.environment_resolver'
- '@logger.channel.helfi_navigation'
- '@helfi_navigation.api_authorization'
- '%helfi_navigation.request_timeout%'
helfi_navigation.menu_manager:
class: Drupal\helfi_navigation\MainMenuManager
arguments:
Expand All @@ -35,6 +32,14 @@ services:
- '@menu.link_tree'
- '@plugin.manager.menu.link'
- '@event_dispatcher'
helfi_navigation.cache_warmer:
class: Drupal\helfi_navigation\CacheWarmer
arguments:
- '@tempstore.shared'
- '@language_manager'
- '@cache_tags.invalidator'
- '@helfi_navigation.api_manager'

helfi_navigation.api_authorization:
class: Drupal\helfi_navigation\ApiAuthorization
arguments:
Expand Down
39 changes: 19 additions & 20 deletions src/ApiManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use Psr\Log\LoggerInterface;

/**
* Service class for global navigation-related functions.
* Service class for global navigation related functions.
*/
class ApiManager {

Expand Down Expand Up @@ -58,17 +58,14 @@ class ApiManager {
* Logger channel.
* @param \Drupal\helfi_navigation\ApiAuthorization $apiAuthorization
* The API authorization service.
* @param int $requestTimeout
* The request timeout.
*/
public function __construct(
private readonly TimeInterface $time,
private readonly CacheBackendInterface $cache,
private readonly ClientInterface $httpClient,
private readonly EnvironmentResolverInterface $environmentResolver,
private readonly LoggerInterface $logger,
private readonly ApiAuthorization $apiAuthorization,
private readonly int $requestTimeout,
private readonly ApiAuthorization $apiAuthorization
) {
}

Expand Down Expand Up @@ -102,7 +99,7 @@ private function cache(string $key, callable $callback) : ? CacheValue {
$value = ($cache = $this->cache->get($key)) ? $cache->data : NULL;

// Attempt to re-fetch the data in case cache does not exist, cache has
// expired, or bypass cache is set to true.
// expired or bypass cache is set to true.
if (
($value instanceof CacheValue && $value->hasExpired($this->time->getRequestTime())) ||
$this->bypassCache ||
Expand Down Expand Up @@ -131,7 +128,7 @@ private function cache(string $key, callable $callback) : ? CacheValue {
}

/**
* Makes a request to fetch an external menu from Etusivu instance.
* Makes a request to fetch external menu from Etusivu instance.
*
* @param string $langcode
* The langcode.
Expand All @@ -141,7 +138,7 @@ private function cache(string $key, callable $callback) : ? CacheValue {
* The request options.
*
* @return \Drupal\helfi_navigation\ApiResponse
* The JSON object representing the external menu.
* The JSON object representing external menu.
*
* @throws \GuzzleHttp\Exception\GuzzleException
*/
Expand All @@ -162,13 +159,13 @@ public function get(
new CacheValue(
$this->makeRequest('GET', $endpoint, $langcode, $options),
$this->time->getRequestTime(),
[sprintf('external_menu:%s:%s', $menuId, $langcode)],
['external_menu:%s:%s', $menuId, $langcode],
)
)->value;
}

/**
* Updates the main menu for the currently active project.
* Updates the main menu for currently active project.
*
* @param string $langcode
* The langcode.
Expand Down Expand Up @@ -197,23 +194,25 @@ public function update(string $langcode, array $data) : ApiResponse {
*
* @param string $environmentName
* Environment name.
* @param array $options
* The optional options.
*
* @return array
* The request options.
*/
private function getRequestOptions(string $environmentName, array $options = []) : array {
$default = [
'timeout' => $this->requestTimeout,
'curl' => [CURLOPT_TCP_KEEPALIVE => TRUE],
];
private function getDefaultRequestOptions(string $environmentName) : array {
$options = ['timeout' => 15];
$options['curl'] = [CURLOPT_TCP_KEEPALIVE => TRUE];

if (drupal_valid_test_ua()) {
// Speed up mock tests by using very low request timeout value when
// running tests.
$options['timeout'] = 1;
}

if ($environmentName === 'local') {
// Disable SSL verification in local environment.
$default['verify'] = FALSE;
$options['verify'] = FALSE;
}
return array_merge_recursive($options, $default);
return $options;
}

/**
Expand Down Expand Up @@ -297,7 +296,7 @@ private function makeRequest(

$url = $this->getUrl('api', $langcode, ['endpoint' => $endpoint]);

$options = $this->getRequestOptions($activeEnvironmentName, $options);
$options = array_merge_recursive($options, $this->getDefaultRequestOptions($activeEnvironmentName));

try {
if ($this->previousException instanceof \Exception) {
Expand Down
90 changes: 90 additions & 0 deletions src/CacheWarmer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types = 1);

namespace Drupal\helfi_navigation;

use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\helfi_navigation\Plugin\Derivative\ExternalMenuBlock;

/**
* A service to prefetch all menu links.
*/
final class CacheWarmer {

/**
* The TempStore storage key.
*/
public const STORAGE_KEY = 'external_menu_hashes';

/**
* Constructs a new instance.
*
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $tempStoreFactory
* The temp store factory.
* @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
* The language manager.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cacheTagsInvalidator
* The cache tags invalidator service.
* @param \Drupal\helfi_navigation\ApiManager $apiManager
* The api manager.
*/
public function __construct(
private SharedTempStoreFactory $tempStoreFactory,
private LanguageManagerInterface $languageManager,
private CacheTagsInvalidatorInterface $cacheTagsInvalidator,
private ApiManager $apiManager,
) {
}

/**
* Invalidate cache tags for given menu and language.
*
* @param mixed $data
* The data.
* @param string $language
* The language.
* @param string $menuName
* The menu name.
*/
private function invalidateTags(mixed $data, string $language, string $menuName) : void {
$key = sprintf('%s:%s', $language, $menuName);
$storage = $this->tempStoreFactory->get(self::STORAGE_KEY);

$currentHash = $storage->get($key);
$hash = hash('sha256', serialize($data));

// Only invalidate tags if content has actually changed.
if ($currentHash === $hash) {
return;
}
$storage->set($key, $hash);
// Invalidate menu block instances.
$this->cacheTagsInvalidator->invalidateTags(['config:system.menu.' . $menuName]);
}

/**
* Warm caches for all available external menus.
*/
public function warm() : void {
$plugin = new ExternalMenuBlock();
$derives = array_keys($plugin->getDerivativeDefinitions([]));
$derives[] = 'main';

foreach ($this->languageManager->getLanguages() as $language) {
foreach ($derives as $name) {
try {
$response = $this->apiManager
->withBypassCache()
->get($language->getId(), $name);
$this->invalidateTags($response, $language->getId(), $name);
}
catch (\Exception) {
}
}
}
}

}
6 changes: 5 additions & 1 deletion src/ExternalMenuTreeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ private function createLink(

// Parse the URL.
$item->url = !empty($item->url) ? UrlHelper::parse($item->url) : new Url('<nolink>');

if (!isset($item->parentId)) {
$item->parentId = NULL;
}
$item->external = $this->domainResolver->isExternal($item->url);

if (isset($item->weight)) {
Expand All @@ -158,7 +162,7 @@ private function createLink(
'attributes' => new Attribute($item->attributes ?? []),
'title' => $item->name,
'id' => $item->id,
'parent_id' => $item->parentId ?? NULL,
'parent_id' => $item->parentId,
'is_expanded' => $expand_all_items || !empty($item->expanded),
'in_active_trail' => $inActiveTrail,
'is_currentPage' => $inActiveTrail,
Expand Down
4 changes: 2 additions & 2 deletions src/MainMenuManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function __construct(
}

/**
* Sends the main menu tree to Etusivu instance.
* Sends main menu tree to frontpage instance.
*
* @param string $langcode
* The langcode.
Expand All @@ -49,7 +49,7 @@ public function __construct(
* @throws \InvalidArgumentException
*/
public function sync(string $langcode): bool {
// Sync the menu as an anonymous user to make sure no sensitive
// Sync menu as an anonymous user to make sure no sensitive
// links are synced.
$this->accountSwitcher->switchTo(new AnonymousUserSession());
$response = $this->apiManager->update(
Expand Down
10 changes: 5 additions & 5 deletions src/Menu/MenuTreeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class MenuTreeBuilder {
* @param \Drupal\helfi_api_base\Link\InternalDomainResolver $domainResolver
* The internal domain resolver.
* @param \Drupal\Core\Menu\MenuLinkTreeInterface $menuTree
* The 'menu link tree builder' service.
* The menu link tree builder service.
* @param \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager
* The menu link manager.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
Expand Down Expand Up @@ -157,7 +157,7 @@ private function transform(array $menuItems, string $langcode, string $rootId =

$urlObject = $link->getUrlObject();

// Make sure the url object retains the language information.
// Make sure url object retains the language information.
if (!$urlObject->getOption('language')) {
$urlObject->setOptions(['language' => $link->language()]);
}
Expand Down Expand Up @@ -269,9 +269,9 @@ private function getEntity(MenuLinkInterface $link, string $langcode): ? MenuLin
* The element to check entity access for.
*/
private function evaluateEntityAccess(MenuLinkTreeElement $element) : void {
// Attempt to fetch the entity type, and id from link's route parameters.
// The route parameters should be an array containing an entity type => id
// key pairs, like: ['node' => '1'].
// Attempt to fetch the entity type and id from link's route parameters.
// The route parameters should be an array containing entity type => id
// like: ['node' => '1'].
$routeParameters = $element->link->getRouteParameters();
$entityType = key($routeParameters);

Expand Down
Loading

0 comments on commit 5c0f648

Please sign in to comment.