Skip to content

Instantly share code, notes, and snippets.

@Elendev
Last active November 23, 2020 09:36
Show Gist options
  • Save Elendev/55069e424d7d8829bb6ddfb294a35717 to your computer and use it in GitHub Desktop.
Save Elendev/55069e424d7d8829bb6ddfb294a35717 to your computer and use it in GitHub Desktop.
Drupal Readonly
<?php
namespace Drupal\sq_readonly\Controller;
use Drupal\sq_readonly\ReadonlyHelper;
use Drupal;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\Serializer;
/**
* Returns responses for SQ Readonly routes.
*/
class APIController extends ControllerBase {
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
private $config;
/**
* @var \Symfony\Component\Serializer\Serializer
*/
private $serializer;
public static function create(ContainerInterface $container) {
return new static(
$container->get('config.factory')->get('sq_readonly.settings'),
$container->get('serializer')
);
}
public function __construct(ImmutableConfig $config, Serializer $serializer) {
$this->config = $config;
$this->serializer = $serializer;
}
/**
* Builds the response.
*
* @param \Symfony\Component\HttpFoundation\Request $request
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function update(Request $request) {
if (!ReadonlyHelper::isReadonlyEnabled()) {
return new JsonResponse([
'status' => 'not available',
], 404);
}
$secret = $this->config->get('secret') ? $this->config->get('secret') : false;
if ($request->get('secret', false) !== $secret) {
return new JsonResponse([
'status' => 'not authorized',
], 403);
}
$content = $this->serializer->decode($request->getContent(), 'json');
// Keep a track of empty bins to avoid trying to clean keys on them
$emptyBins = [];
// Here the direct access to Drupal is justified since we rely on the naming logic for the caches entries.
// Nevertheless, having a container dependency would be a better practice.
foreach ($content['binsToRemove'] as $bin) {
$emptyBins[] = $bin;
try {
$bin = Drupal::cache($bin);
$bin->removeBin();
} catch (ServiceNotFoundException $e) {
}
}
foreach ($content['binsToFlush'] as $bin) {
if (in_array($bin, $emptyBins)) {
continue;
}
$emptyBins[] = $bin;
try {
$bin = Drupal::cache($bin);
$bin->deleteAll();
} catch (ServiceNotFoundException $e) {
}
}
foreach ($content['binsToGarbageCollect'] as $bin) {
if (in_array($bin, $emptyBins)) {
continue;
}
try {
$bin = Drupal::cache($bin);
$bin->garbageCollection();
} catch (ServiceNotFoundException $e) {
}
}
foreach ($content['keysToClean'] as $bin => $keys) {
if (in_array($bin, $emptyBins)) {
continue;
}
try {
$bin = Drupal::cache($bin);
$bin->deleteMultiple($keys);
} catch (ServiceNotFoundException $e) {
}
}
return new JsonResponse([
'status' => 'done',
], 200);
}
}
<?php
namespace Drupal\sq_readonly\Cache;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
class CacheBackendDecorator implements CacheBackendInterface {
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
private $cacheBackend;
/**
* @var string
*/
private $binName;
/**
* @var \Drupal\sq_readonly\Cache\ReadonlyCacheClearer
*/
private $readonlyCacheClearer;
public function __construct(CacheBackendInterface $cacheBackend, string $binName, ReadonlyCacheClearer $readonlyCacheClearer) {
$this->cacheBackend = $cacheBackend;
$this->binName = $binName;
$this->readonlyCacheClearer = $readonlyCacheClearer;
}
/**
* @inheritDoc
*/
public function get($cid, $allow_invalid = FALSE) {
return $this->cacheBackend->get($cid, $allow_invalid);
}
/**
* @inheritDoc
*/
public function getMultiple(&$cids, $allow_invalid = FALSE) {
return $this->cacheBackend->getMultiple($cids, $allow_invalid);
}
/**
* @inheritDoc
*/
public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) {
$this->cacheBackend->set($cid, $data, $expire, $tags);
}
/**
* @inheritDoc
*/
public function setMultiple(array $items) {
$this->cacheBackend->setMultiple($items);
}
/**
* @inheritDoc
*/
public function delete($cid) {
$this->cacheBackend->delete($cid);
$this->addTagsToCacheClearer($cid);
}
/**
* @inheritDoc
*/
public function deleteMultiple(array $cids) {
$this->cacheBackend->deleteMultiple($cids);
$this->addTagsToCacheClearer($cids);
}
/**
* @inheritDoc
*/
public function deleteAll() {
$this->cacheBackend->deleteAll();
$this->addBinToCacheClearer();
}
/**
* @inheritDoc
*/
public function invalidate($cid) {
$this->cacheBackend->invalidate($cid);
$this->addTagsToCacheClearer($cid);
}
/**
* @inheritDoc
*/
public function invalidateMultiple(array $cids) {
$this->cacheBackend->invalidateMultiple($cids);
$this->addTagsToCacheClearer($cids);
}
/**
* @inheritDoc
*/
public function invalidateAll() {
$this->cacheBackend->invalidateAll();
$this->addBinToCacheClearer();
}
/**
* @inheritDoc
*/
public function garbageCollection() {
$this->cacheBackend->garbageCollection();
$this->readonlyCacheClearer->addBinToGarbageCollection($this->binName);
}
/**
* @inheritDoc
*/
public function removeBin() {
$this->cacheBackend->removeBin();
$this->readonlyCacheClearer->removeBin($this->binName);
}
protected function addTagsToCacheClearer($tags) {
if (!is_array($tags)) {
$tags = [$tags];
}
$this->readonlyCacheClearer->addKeysToClear($this->binName, $tags);
}
/**
* Clear all for the bin
* @param $tags
*/
protected function addBinToCacheClearer() {
$this->readonlyCacheClearer->addBinToClear($this->binName);
}
}
<?php
namespace Drupal\sq_readonly\Cache;
use Drupal\Core\Cache\CacheFactoryInterface;
use Drupal\sq_readonly\ReadonlyHelper;
class CacheBackendDecoratorFactory implements CacheFactoryInterface {
/**
* @var \Drupal\sq_readonly\Cache\ReadonlyCacheClearer
*/
private $readonlyCacheClearer;
/**
* @var \Drupal\Core\Cache\CacheFactoryInterface
*/
private $cacheFactory;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface[]
*/
private $bins;
/**
* CacheBackendDecoratorFactory constructor.
*
* @param \Drupal\Core\Cache\CacheFactoryInterface $cacheFactory
* @param \Drupal\sq_readonly\Cache\ReadonlyCacheClearer $readonlyCacheClearer
*/
public function __construct(CacheFactoryInterface $cacheFactory, ReadonlyCacheClearer $readonlyCacheClearer) {
$this->readonlyCacheClearer = $readonlyCacheClearer;
$this->cacheFactory = $cacheFactory;
}
/**
* @inheritDoc
*/
public function get($bin) {
if (ReadonlyHelper::isReadonlyEnabled()) {
return $this->cacheFactory->get($bin);
}
if (!isset($this->bins[$bin])) {
$this->bins[$bin] = new CacheBackendDecorator($this->cacheFactory->get($bin), $bin, $this->readonlyCacheClearer);
}
return $this->bins[$bin];
}
}
<?php
namespace Drupal\sq_readonly\Cache;
use Drupal;
use Drupal\Core\Config\ImmutableConfig;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Serializer\Serializer;
use Throwable;
use function GuzzleHttp\Promise\unwrap;
/**
* Class ReadonlyCacheClearer
*
* Store every keys that has been cleared in the bins and send the requests to the slaves when asked to.
*
* @package Drupal\sq_readonly\Cache
*/
class ReadonlyCacheClearer {
private $keysToClean = [];
private $binsToFlush = [];
private $binsToGarbageCollect = [];
private $binsToRemove = [];
/**
* @var \Drupal\Core\Config\ImmutableConfig
*/
private $config;
public function setConfig(ImmutableConfig $config) {
$this->config = $config;
}
/**
* Add the given keys to the keys-to-clean array
* @param $bin
* @param array $keys
*/
public function addKeysToClear($bin, array $keys) {
if (!isset($this->keysToClean[$bin])) {
$this->keysToClean[$bin] = $keys;
} else {
$this->keysToClean[$bin] = array_merge($this->keysToClean[$bin], $keys);
}
}
/**
* Add the given bin to be entirely cleared
* @param string $binName
*/
public function addBinToClear(string $binName) {
$this->binsToFlush = array_merge([$binName], $this->binsToFlush);
}
/**
* Launch the garbage collection process to the given bin
* @param string $binName
*/
public function addBinToGarbageCollection(string $binName) {
$this->binsToGarbageCollect = array_merge([$binName], $this->binsToGarbageCollect);
}
/**
* Remove the given bins
* @param string $binName
*/
public function removeBin(string $binName) {
$this->binsToRemove = array_merge([$binName], $this->binsToRemove);
}
/**
* Send clearing requests to slaves
*/
public function sendClearingRequests() {
// If all the bins are empty there is nothing to be send => do nothing
if (empty($this->binsToFlush) && empty($this->keysToClean) && empty($this->binsToGarbageCollect) && empty($this->binsToRemove)) {
return;
}
// We cannot use dependency injection for most of the services here otherwise we have a circular reference issue.
// The solution is to lazily get them here.
/** @var ImmutableConfig $config */
$config = Drupal::service('sq_readonly.config');
/** @var \Psr\Log\LoggerInterface $logger */
$logger = Drupal::service('logger.channel.sq_readonly');
/** @var Serializer $serializer */
$serializer = Drupal::service('serializer');
$currentRequest = Drupal::request();
$secret = $config->get('secret');
$servers = $config->get('readonly_servers');
if (empty($servers)) {
return;
}
$promises = [];
if ($config->get('direct_access')) {
$httpClient = new Client();
} else {
$httpClient = Drupal::httpClient();
}
foreach ($servers as $server) {
$request = new Request(
'POST',
$server . '/api/sq-readonly/v1/update?secret=' . $secret,
[
'Host' => $currentRequest->getHost(),
'Content-type' => 'application/json',
],
$serializer->serialize([
'keysToClean' => $this->keysToClean,
'binsToFlush' => $this->binsToFlush,
'binsToRemove' => $this->binsToRemove,
'binsToGarbageCollect' => $this->binsToGarbageCollect,
], 'json')
);
$promises[] = $httpClient->sendAsync($request, ['timeout' => 500])->then(
function (ResponseInterface $response) use($logger, $server) {
if ($response->getStatusCode() > 200) {
$logger->error('An error occured with ' . $server . ' while trying to cache-clear (status code: ' . $response->getStatusCode() . '): ' . $response->getBody()->getContents());
}
},
function (RequestException $e) use($logger, $server) {
$logger->error('Impossible to send cache-clear request to ' . $server . ': ' . $e->getMessage());
}
);
}
try {
unwrap($promises);
}
catch (Throwable $e) {
$logger->error('An error occured with one of the requests to the servers: ' . $e->getMessage());
}
}
}
<?php
namespace Drupal\sq_readonly\EventSubscriber;
use Drupal\sq_readonly\Cache\ReadonlyCacheClearer;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* SQ Readonly event subscriber.
*/
class SqReadonlySubscriber implements EventSubscriberInterface {
/**
* @var \Drupal\sq_readonly\Cache\ReadonlyCacheClearer
*/
private $readonlyCacheClearer;
/**
* @var \Psr\Log\LoggerInterface
*/
private $logger;
/**
* Constructs event subscriber.
*
* @param \Drupal\sq_readonly\Cache\ReadonlyCacheClearer $readonlyCacheClearer
* @param \Psr\Log\LoggerInterface $logger
*/
public function __construct(ReadonlyCacheClearer $readonlyCacheClearer) {
$this->readonlyCacheClearer = $readonlyCacheClearer;
}
public function onKernelTerminate(PostResponseEvent $event) {
$this->readonlyCacheClearer->sendClearingRequests();
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
KernelEvents::TERMINATE => ['onKernelTerminate'],
];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment