momento-1.0.0/src/MomentoCacheBackend.php
src/MomentoCacheBackend.php
<?php
namespace Drupal\momento_cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsChecksumInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Site\Settings;
use Drupal\Component\Assertion\Inspector;
class MomentoCacheBackend implements CacheBackendInterface
{
use LoggerChannelTrait;
private $backendName = "momento-cache";
private $bin;
private $binTag;
private $lastBinDeletionTime;
private $client;
private $checksumProvider;
private $MAX_TTL;
private $cacheName;
private $logFile;
private $batchSize;
public function __construct(
$bin,
$client,
CacheTagsChecksumInterface $checksum_provider
) {
$this->MAX_TTL = intdiv(PHP_INT_MAX, 1000);
$this->client = $client;
$this->bin = $bin;
$this->checksumProvider = $checksum_provider;
$this->binTag = "$this->backendName:$this->bin";
$settings = Settings::get('momento_cache', []);
$this->cacheName = MomentoCacheBackendFactory::getCacheName();
$this->logFile =
array_key_exists('logfile', $settings) ? $settings['logfile'] : null;
$this->batchSize =
array_key_exists('batch_size', $settings) ? $settings['batch_size'] : 50;
$this->ensureLastBinDeletionTimeIsSet();
}
public function get($cid, $allow_invalid = FALSE)
{
$cids = [$cid];
$recs = $this->getMultiple($cids, $allow_invalid);
return reset($recs);
}
private function getCidForBin($cid) {
return "$this->bin:$cid";
}
public function getMultiple(&$cids, $allow_invalid = FALSE)
{
$start = $this->startStopwatch();
$numRequested = count($cids);
$keys = [];
$fetched = [];
foreach ($cids as $cid) {
$keys[$cid] = $this->getCidForBin($cid);
}
$keysArray = array_keys($keys);
$counter = 0;
$response = $this->client->getBatch($this->cacheName, $keys);
if ($response->asError()) {
$this->log(
"GET_MULTIPLE error for bin $this->bin: " . $response->asError()->message(),
true
);
return [];
} else {
$fetched_results = $response->asSuccess()->results();
foreach ($fetched_results as $result) {
$cid = $keysArray[$counter++];
if ($result->asHit()) {
$item = unserialize($result->asHit()->valueString());
if ($item->created <= $this->lastBinDeletionTime) {
continue;
}
if ($allow_invalid || $this->valid($item)) {
$fetched[$cid] = $item;
}
} elseif ($result->asError()) {
$this->log(
"GET_MULTIPLE response error for bin $this->bin: " . $result->asError()->message(),
true
);
}
}
}
$cids = array_diff($cids, array_keys($fetched));
$this->stopStopwatch(
$start,
"GET_MULTIPLE got " . count($fetched_results) . " items of $numRequested requested."
);
return $fetched;
}
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = [])
{
$start = $this->startStopwatch();
$item = $this->processItemForSet($cid, $data, $expire, $tags);
$ttl = $item->ttl;
unset($item->ttl);
$serialized_item = serialize($item);
$setResponse = $this->client->set($this->cacheName, $this->getCidForBin($cid), $serialized_item, $ttl);
if ($setResponse->asError()) {
$this->log("SET response error for bin $this->bin: " . $setResponse->asError()->message(), true);
} else {
$this->stopStopwatch(
$start, "SET cid $cid in bin $this->bin with ttl $ttl and " . strlen($serialized_item) . " bytes"
);
}
}
public function setMultiple(array $items)
{
$start = $this->startStopwatch();
$processed_items = [];
$maxTtl = $this->MAX_TTL;
foreach ($items as $cid => $item) {
$item = $this->processItemForSet(
$cid,
$item['data'] ?? '',
$item['expire'] ?? CacheBackendInterface::CACHE_PERMANENT,
$item['tags'] ?? []
);
$ttl = $item->ttl;
unset($item->ttl);
$serializedItem = serialize($item);
$cacheKey = $this->getCidForBin($cid);
$processed_items[$cacheKey] = $serializedItem;
}
$response = $this->client->setBatch($this->cacheName, $processed_items, $maxTtl);
if ($response->asError()) {
$this->log(
"SET_MULTIPLE response error for bin $this->bin: " . $response->asError()->message(),
true
);
}
$this->stopStopwatch($start, "SET_MULTIPLE in bin $this->bin for " . count($items) . " items");
}
public function delete($cid)
{
$start = $this->startStopwatch();
$deleteResponse = $this->client->delete($this->cacheName, $this->getCidForBin($cid));
if ($deleteResponse->asError()) {
$this->log(
"DELETE response error for $cid in bin $this->bin: " . $deleteResponse->asError()->message(),
true
);
} else {
$this->stopStopwatch($start, "DELETE cid $cid from bin $this->bin");
}
}
public function deleteMultiple(array $cids)
{
$start = $this->startStopwatch();
foreach ($cids as $cid) {
$this->delete($cid);
}
$this->stopStopwatch($start, "DELETE_MULTIPLE in bin $this->bin for " . count($cids) . " items");
}
public function deleteAll()
{
$start = $this->startStopwatch();
$this->setLastBinDeletionTime();
$this->stopStopwatch($start, "DELETE_ALL in bin $this->bin");
}
public function invalidate($cid)
{
$start = $this->startStopwatch();
$this->invalidateMultiple([$cid]);
$this->stopStopwatch($start, "INVALIDATE $cid for bin $this->bin");
}
public function invalidateMultiple(array $cids)
{
$start = $this->startStopwatch();
$requestTime = \Drupal::time()->getRequestTime();
foreach ($cids as $cid) {
if ($item = $this->get($cid)) {
$this->set($item->cid, $item->data, $requestTime - 1, $item->tags);
}
}
$this->stopStopwatch($start, "INVALIDATE_MULTIPLE for " . count($cids) . " items in $this->bin");
}
public function invalidateAll()
{
$start = $this->startStopwatch();
$this->invalidateTags([$this->binTag]);
$this->stopStopwatch($start, "INVALIDATE_ALL for $this->bin");
}
public function invalidateTags(array $tags)
{
$start = $this->startStopwatch();
$this->checksumProvider->invalidateTags($tags);
$this->stopStopwatch($start, "INVALIDATE_TAGS for " . count($tags));
}
public function removeBin()
{
$start = $this->startStopwatch();
$this->setLastBinDeletionTime();
$this->stopStopwatch($start, "REMOVE_BIN $this->bin");
}
public function garbageCollection()
{
// Momento will invalidate items; That item's memory allocation is then
// freed up and reused. So nothing needs to be deleted/cleaned up here.
}
private function processItemForSet($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = [])
{
assert(Inspector::assertAllStrings($tags));
// Add tag for invalidateAll()
$tags[] = $this->binTag;
$tags = array_unique($tags);
sort($tags);
$ttl = $this->MAX_TTL;
$item = new \stdClass();
$item->cid = $cid;
$item->tags = $tags;
$item->data = $data;
$item->created = round(microtime(TRUE), 3);
$item->valid = TRUE;
$item->checksum = $this->checksumProvider->getCurrentChecksum($tags);
$requestTime = \Drupal::time()->getRequestTime();
if ($expire != CacheBackendInterface::CACHE_PERMANENT) {
if ($expire < $requestTime) {
$item->valid = FALSE;
}
}
$item->expire = $expire;
$item->ttl = $ttl;
return $item;
}
private function valid($item) {
// If container isn't initialized yet, use $SERVER's request time value
try {
$requestTime = \Drupal::time()->getRequestTime();
} catch (ContainerNotInitializedException $e) {
$requestTime = $_SERVER['REQUEST_TIME'];
}
$isValid = TRUE;
if ($item->expire != CacheBackendInterface::CACHE_PERMANENT && $item->expire < $requestTime) {
$item->valid = FALSE;
return FALSE;
}
if (!$this->checksumProvider->isValid($item->checksum, $item->tags)) {
$isValid = FALSE;
}
$item->valid = $isValid;
return $isValid;
}
private function log(string $message, bool $logToDblog = false) {
if ($logToDblog) {
$this->getLogger('momento_cache')->error($message);
}
if (!$this->logFile) {
return;
}
if ($message[-1] != "\n") {
$message .= "\n";
}
$mt = microtime(true);
@error_log("[$mt] $message", 3, $this->logFile);
}
private function startStopwatch() {
return hrtime(true);
}
private function stopStopwatch($startTime, $message=null) {
if (!$this->logFile) {
return;
}
$totalTimeMs = (hrtime(true) - $startTime) / 1e+6;
if ($message) {
$this->log("$message [$totalTimeMs ms]\n");
}
}
private function ensureLastBinDeletionTimeIsSet() {
if (!$this->lastBinDeletionTime) {
$getRequest = $this->client->get($this->cacheName, $this->bin);
if ($getRequest->asError()) {
$this->log(
"ERROR getting last deletion time for bin $this->bin: " . $getRequest->asError()->message()
);
$this->setLastBinDeletionTime();
} elseif ($getRequest->asMiss()) {
$this->setLastBinDeletionTime();
} else {
$this->lastBinDeletionTime = intval($getRequest->asHit()->valueString());
}
}
}
private function setLastBinDeletionTime() {
$this->lastBinDeletionTime = round(microtime(TRUE), 3);
$setRequest = $this->client->set($this->cacheName, $this->bin, $this->lastBinDeletionTime);
if ($setRequest->asError()) {
$this->log(
"ERROR getting last deletion time for bin $this->bin: " . $setRequest->asError()->message()
);
}
}
}
