-
-
Save Elendev/55069e424d7d8829bb6ddfb294a35717 to your computer and use it in GitHub Desktop.
Drupal Readonly
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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]; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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()); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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