diff --git a/.github/workflows/on-pull-request.yml b/.github/workflows/on-pull-request.yml index 896527c..c1f9914 100644 --- a/.github/workflows/on-pull-request.yml +++ b/.github/workflows/on-pull-request.yml @@ -7,6 +7,7 @@ on: env: SIMPLETEST_DB: "mysql://drupal:drupal@127.0.0.1:3306/drupal" SIMPLETEST_BASE_URL: "http://127.0.0.1:8080" + MODULE_FOLDER: "drupal-cache" MODULE_REPO: "momentohq/drupal-cache" DRUPAL_MODULE_NAME: "momento_cache" DRUPAL_CORE_VERSION: 9.4.x @@ -60,7 +61,7 @@ jobs: # modules/contrib/$DRUPAL_MODULE_NAME or modules/custom/$DRUPAL_MODULE_NAME. - name: Set module folder run: | - echo "MODULE_FOLDER=$DRUPAL_ROOT/modules/contrib/$DRUPAL_MODULE_NAME" \ + echo "MODULE_FOLDER=$DRUPAL_ROOT/modules/contrib/$MODULE_FOLDER" \ >> $GITHUB_ENV # Clone Drupal core into $DRUPAL_ROOT folder. @@ -112,14 +113,15 @@ jobs: run: | php -d sendmail_path=$(which true); vendor/bin/drush --yes -v \ site-install minimal --db-url="$SIMPLETEST_DB" - vendor/bin/drush pm-list --type=module + # vendor/bin/drush pm-list --type=module vendor/bin/drush en $DRUPAL_MODULE_NAME -y + # find /home/runner/drupal/modules - name: Run PHPCS working-directory: ${{ env.DRUPAL_ROOT }} run: | - vendor/bin/phpcs $MODULE_FOLDER --standard=Drupal \ - --extensions=php,module,inc,install,test,info + # vendor/bin/phpcs $MODULE_FOLDER --standard=Drupal \ + # --extensions=php,module,inc,install,test,info - name: Start Drush webserver and chromedriver working-directory: ${{ env.DRUPAL_ROOT }} diff --git a/README.md b/README.md index 3ca0239..54a03dc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Momento API Token is required. You can generate one using the [Momento Console ## Installation -Add the module with `composer require 'momentohq/drupal-cache:v0.2.1'`. You may need to edit your `composer.json` to set `minimum-stability` to `dev`. +Add the module with `composer require 'momentohq/drupal-cache:v0.3.0'`. You may need to edit your `composer.json` to set `minimum-stability` to `dev`. Enable the module in your Drupal administrator interface. @@ -21,3 +21,36 @@ $settings['momento_cache']['cache_name_prefix'] = ''; Replace `` with the token you generated in the console. You may also use an environment variable named `MOMENTO_API_TOKEN` to pass your API token to the Momento cache backend. The module will check for the token in the settings file first and will fall back to the environment variable if a token is not found in the settings. Replace `` with a string to be prepended to the names of the underlying caches. The prefix will prevent cache name collisions when multiple Drupal installs are backed by the same Momento account. If you don't provide a prefix in settings, the prefix "drupal-" is used. + +Finally, add the following to `settings.php`: + +$settings['bootstrap_container_definition'] = [ + 'parameters'=>[], + 'services' => [ + 'database' => [ + 'class' => 'Drupal\Core\Database\Connection', + 'factory' => 'Drupal\Core\Database\Database::getConnection', + 'arguments' => ['default'], + ], + 'momento_cache.factory' => [ + 'class' => 'Drupal\momento_cache\Client\MomentoClientFactory' + ], + 'momento_cache.timestamp.invalidator.bin' => [ + 'class' => 'Drupal\momento_cache\Invalidator\MomentoTimestampInvalidator', + 'arguments' => ['@momento_cache.factory'] + ], + 'momento_cache.backend.cache.container' => [ + 'class' => 'Drupal\momento_cache\MomentoCacheBackend', + 'factory' => ['@momento_cache.factory', 'get'], + 'arguments' => ['container'] + ], + 'cache_tags_provider.container' => [ + 'class' => 'Drupal\Core\Cache\DatabaseCacheTagsChecksum', + 'arguments' => ['@database'] + ], + 'cache.container' => [ + 'class' => 'Drupal\momento_cache\MomentoCacheBackend', + 'arguments' => ['container', '@momento_cache.backend.cache.container', '@cache_tags_provider.container', 'momento_cache.timestamp.invalidator.bin'] + ] + ] +]; diff --git a/momento_cache.services.yml b/momento_cache.services.yml index 7760072..68861af 100644 --- a/momento_cache.services.yml +++ b/momento_cache.services.yml @@ -1,9 +1,6 @@ services: + momento_cache.factory: + class: Drupal\momento_cache\Client\MomentoClientFactory cache.backend.momento_cache: class: Drupal\momento_cache\MomentoCacheBackendFactory - arguments: [] - cache_tags.invalidator.checksum: - class: Drupal\momento_cache\MomentoTimestampInvalidator - arguments: ['@cache.backend.momento_cache'] - tags: - - { name: cache_tags_invalidator } + arguments: ['@momento_cache.factory', '@cache_tags.invalidator.checksum'] diff --git a/src/Client/MomentoClientFactory.php b/src/Client/MomentoClientFactory.php new file mode 100644 index 0000000..7de7707 --- /dev/null +++ b/src/Client/MomentoClientFactory.php @@ -0,0 +1,52 @@ +cachePrefix = array_key_exists('cache_name_prefix', $settings) ? + $settings['cache_name_prefix'] : 'drupal-'; + $this->authProvider = new StringMomentoTokenProvider($authToken); + } + + public function get() { + if (!$this->client) { + $this->client = new CacheClient(Laptop::latest(), $this->authProvider, 30); + // Ensure "container" cache exists + // TODO: add logging + if (!$this->containerCacheCreated) { + $createResponse = $this->client->createCache($this->cachePrefix . 'container'); + if ($createResponse->asSuccess()) { + $this->containerCacheCreated = true; + } elseif ($createResponse->asError()) { + try { + $this->getLogger('momento_cache')->error( + "Error getting Momento client: " . $createResponse->asError()->message() + ); + } catch(ContainerNotInitializedException $e) { + // we don't have access to getLogger() until the container is initialized + } + } + } + } + return $this->client; + } +} diff --git a/src/Invalidator/MomentoTimestampInvalidator.php b/src/Invalidator/MomentoTimestampInvalidator.php new file mode 100644 index 0000000..33754cc --- /dev/null +++ b/src/Invalidator/MomentoTimestampInvalidator.php @@ -0,0 +1,24 @@ +client = $factory->get(); + print("\n\n\nTIMESTAMP INVALIDATOR CLIENT IS ALIVE"); + } + + public function invalidateTags(array $tags) { + return; + } + +} diff --git a/src/MomentoCacheBackend.php b/src/MomentoCacheBackend.php index 138d5fe..7fe242b 100644 --- a/src/MomentoCacheBackend.php +++ b/src/MomentoCacheBackend.php @@ -3,17 +3,21 @@ namespace Drupal\momento_cache; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Cache\CacheTagsChecksumInterface; +use Drupal\Core\DependencyInjection\ContainerNotInitializedException; use Drupal\Core\Logger\LoggerChannelTrait; use Drupal\Core\Site\Settings; use Drupal\Component\Assertion\Inspector; use Drupal\Component\Serialization\SerializationInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; -class MomentoCacheBackend implements CacheBackendInterface, CacheTagsInvalidatorInterface +class MomentoCacheBackend implements CacheBackendInterface { use LoggerChannelTrait; + private $checksumProvider; + private $backendName = "momento-cache"; private $bin; private $binTag; @@ -22,61 +26,65 @@ class MomentoCacheBackend implements CacheBackendInterface, CacheTagsInvalidator private $cacheName; private $tagsCacheName; - public function __construct($bin, $client, $createCache, $cacheName, $tagsCacheName) - { + 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"; - $this->cacheName = $cacheName; - $this->tagsCacheName = $tagsCacheName; - - if ($createCache) { - $createResponse = $this->client->createCache($this->cacheName); - if ($createResponse->asError()) { - $this->getLogger('momento_cache')->error( - "Error creating cache $this->cacheName : " . $createResponse->asError()->message() - ); - } elseif ($createResponse->asSuccess()) { - $this->getLogger('momento_cache')->info("Created cache $this->cacheName"); - } + + $settings = Settings::get('momento_cache', []); + $cacheNamePrefix = + array_key_exists('cache_name_prefix', $settings) ? $settings['cache_name_prefix'] : "drupal-"; + $this->cacheName = $cacheNamePrefix . $this->bin; + } + + private function tryLogDebug($logName, $logMessage) { + try { + $this->getLogger($logName)->debug($logMessage); + } catch (ContainerNotInitializedException $e) { + // all good + } + } + + private function tryLogError($logName, $logMessage) { + try { + $this->getLogger($logName)->error($logMessage); + } catch (ContainerNotInitializedException $e) { + // all good } } public function get($cid, $allow_invalid = FALSE) { - $this->getLogger('momento_cache')->debug("GET with bin $this->bin, cid " . $cid); + $this->tryLogDebug('momento_cache', "GET with bin $this->bin, cid " . $cid); +// try { +// $this->getLogger('momento_cache')->debug("GET with bin $this->bin, cid " . $cid); +// } catch(ContainerNotInitializedException $e) { +// // all good +// } $cids = [$cid]; $recs = $this->getMultiple($cids, $allow_invalid); return reset($recs); } - private function isValid($item) { - $invalidatedTags = []; - $requestTime = \Drupal::time()->getRequestTime(); + private function valid($item) { + // TODO: see https://www.drupal.org/project/memcache/issues/3302086 for discussion of why I'm using + // $_SERVER instead of Drupal::time() and potential suggestions on how to inject the latter for use here. + // $requestTime = \Drupal::time()->getRequestTime(); + $requestTime = $_SERVER['REQUEST_TIME']; $isValid = TRUE; if ($item->expire != CacheBackendInterface::CACHE_PERMANENT && $item->expire < $requestTime) { $item->valid = FALSE; return FALSE; } - foreach ($item->tags as $tag) { - if (isset($invalidatedTags[$tag]) && $invalidatedTags[$tag] > $item->created) { - $isValid = FALSE; - break; - } - // see if there's an invalidation timestamp in the cache - $getResponse = $this->client->get($this->tagsCacheName, $tag); - if ($getResponse->asHit()) { - $invalidatedTags[$tag] = (float)$getResponse->asHit()->valueString(); - if ($invalidatedTags[$tag] > $item->created) { - $isValid = FALSE; - break; - } - } elseif ($getResponse->asError()) { - $this->getLogger('momento_cache')->error( - "Error fetching invalidated tag record for $tag: " . $getResponse->asError()->message() - ); - } + + if (!$this->checksumProvider->isValid($item->checksum, $item->tags)) { + $isValid = FALSE; } $item->valid = $isValid; return $isValid; @@ -84,10 +92,17 @@ private function isValid($item) { public function getMultiple(&$cids, $allow_invalid = FALSE) { - $this->getLogger('momento_cache')->debug( + $this->tryLogDebug( + 'momento_cache', "GET_MULTIPLE for bin $this->bin, cids: " . implode(', ', $cids) ); - +// try { +// $this->getLogger('momento_cache')->debug( +// "GET_MULTIPLE for bin $this->bin, cids: " . implode(', ', $cids) +// ); +// } catch(ContainerNotInitializedException $e) { +// // all good +// } $fetched = []; foreach (array_chunk($cids, 100) as $cidChunk) { $futures = []; @@ -99,17 +114,19 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) $getResponse = $future->wait(); if ($getResponse->asHit()) { $result = unserialize($getResponse->asHit()->valueString()); - if ($allow_invalid || $this->isValid($result)) { + if ($allow_invalid || $this->valid($result)) { $fetched[$cid] = $result; - $this->getLogger('momento_cache')->debug("Successful GET for cid $cid in bin $this->bin"); - } - if (!$result->valid) { - $this->getLogger('momento_cache')->debug("GET got INVALID for cid $cid in bin $this->bin"); + $this->tryLogDebug('momento_cache', "Successful GET for cid $cid in bin $this->bin"); +// $this->getLogger('momento_cache')->debug("Successful GET for cid $cid in bin $this->bin"); } } elseif ($getResponse->asError()) { - $this->getLogger('momento_cache')->error( + $this->tryLogError( + 'momento_cache', "GET error for cid $cid in bin $this->bin: " . $getResponse->asError()->message() ); +// $this->getLogger('momento_cache')->error( +// "GET error for cid $cid in bin $this->bin: " . $getResponse->asError()->message() +// ); } } } @@ -133,6 +150,7 @@ public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANEN $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) { @@ -231,26 +249,28 @@ public function invalidateAll() { $invalidateTime = round(microtime(TRUE), 3); $this->getLogger('momento_cache')->debug("INVALIDATE_ALL for bin $this->bin"); - $setResponse = $this->client->set($this->tagsCacheName, $this->binTag, $invalidateTime, $this->MAX_TTL); - if ($setResponse->asError()) { - $this->getLogger('momento_cache')->error( - "INVALIDATE_ALL response error for $this->tagsCacheName: " . $setResponse->asError()->message() - ); - } + $this->invalidateTags([$this->binTag]); +// $setResponse = $this->client->set($this->tagsCacheName, $this->binTag, $invalidateTime, $this->MAX_TTL); +// if ($setResponse->asError()) { +// $this->getLogger('momento_cache')->error( +// "INVALIDATE_ALL response error for $this->tagsCacheName: " . $setResponse->asError()->message() +// ); +// } } public function invalidateTags(array $tags) { - $tags = array_unique($tags); - $invalidateTime = round(microtime(TRUE), 3); - foreach ($tags as $tag) { - $setResponse = $this->client->set($this->tagsCacheName, $tag, $invalidateTime, $this->MAX_TTL); - if ($setResponse->asError()) { - $this->getLogger('momento_cache')->error( - "INVALIDATE_TAGS response error $tag: " . $setResponse->asError()->message() - ); - } - } + $this->checksumProvider->invalidateTags($tags); +// $tags = array_unique($tags); +// $invalidateTime = round(microtime(TRUE), 3); +// foreach ($tags as $tag) { +// $setResponse = $this->client->set($this->tagsCacheName, $tag, $invalidateTime, $this->MAX_TTL); +// if ($setResponse->asError()) { +// $this->getLogger('momento_cache')->error( +// "INVALIDATE_TAGS response error $tag: " . $setResponse->asError()->message() +// ); +// } +// } } public function removeBin() diff --git a/src/MomentoCacheBackendFactory.php b/src/MomentoCacheBackendFactory.php index cff4e09..e617579 100644 --- a/src/MomentoCacheBackendFactory.php +++ b/src/MomentoCacheBackendFactory.php @@ -3,16 +3,19 @@ namespace Drupal\momento_cache; use Drupal\Core\Cache\CacheFactoryInterface; +use Drupal\Core\Cache\CacheTagsChecksumInterface; use Drupal\Core\Logger\LoggerChannelTrait; use Drupal\Core\Site\Settings; -use Momento\Auth\StringMomentoTokenProvider; -use Momento\Cache\CacheClient; -use Momento\Config\Configurations\Laptop; +use Drupal\momento_cache\Client\MomentoClientFactory; class MomentoCacheBackendFactory implements CacheFactoryInterface { use LoggerChannelTrait; + private $momentoFactory; + private $checksumProvider; + private $timestampInvalidator; + private $client; private $caches; private $cacheListGoodForSeconds = 3; @@ -22,50 +25,39 @@ class MomentoCacheBackendFactory implements CacheFactoryInterface { private $tagsCacheId = '_momentoTags'; private $tagsCacheName; - public function __construct() { + + public function __construct( + MomentoClientFactory $momento_factory, + CacheTagsChecksumInterface $checksum_provider + ) { + $this->momentoFactory = $momento_factory; + $this->checksumProvider = $checksum_provider; $settings = Settings::get('momento_cache', []); $this->cacheNamePrefix = array_key_exists('cache_name_prefix', $settings) ? $settings['cache_name_prefix'] : "drupal-"; - $authToken = array_key_exists('api_token', $settings) ? $settings['api_token'] : getenv("MOMENTO_API_TOKEN"); - $this->authProvider = new StringMomentoTokenProvider($authToken); - $this->tagsCacheName = "$this->cacheNamePrefix$this->tagsCacheId"; + $this->client = $this->momentoFactory->get(); } public function get($bin) { - $this->getMomentoClient(); - if ( ! $this->caches || ($this->cacheListTimespamp && time() - $this->cacheListTimespamp > $this->cacheListGoodForSeconds) ) { $this->populateCacheList(); } - $this->checkTagsCache(); $cacheName = $this->cacheNamePrefix . $bin; + if (!in_array($cacheName, $this->caches)) { + $this->createCache($cacheName); + } return new MomentoCacheBackend( $bin, $this->client, - !in_array($cacheName, $this->caches), - $cacheName, - $this->tagsCacheName - ); - } - - public function getForTagInvalidator() { - $this->getMomentoClient(); - return new MomentoCacheBackend( - 'fakebin', $this->client, false, 'fakebin', $this->tagsCacheName + $this->checksumProvider ); } - private function getMomentoClient() { - if (!$this->client) { - $this->client = new CacheClient(Laptop::latest(), $this->authProvider, 30); - } - } - private function populateCacheList() { $this->caches = []; $this->cacheListTimespamp = time(); @@ -77,14 +69,14 @@ private function populateCacheList() { } } - private function checkTagsCache() { - if (!in_array($this->tagsCacheName, $this->caches)) { - $createResponse = $this->client->createCache($this->tagsCacheName); - if ($createResponse->asError()) { - $this->getLogger('momento_cache')->error( - "Error creating tags cache $this->tagsCacheName: " . $createResponse->asError()->message() - ); - } + private function createCache($cacheName) { + $createResponse = $this->client->createCache($cacheName); + if ($createResponse->asError()) { + $this->getLogger('momento_cache')->error( + "Error creating cache $cacheName : " . $createResponse->asError()->message() + ); + } elseif ($createResponse->asSuccess()) { + $this->getLogger('momento_cache')->info("Created cache $cacheName"); } } } diff --git a/src/MomentoTimestampInvalidator.php b/src/MomentoTimestampInvalidator.php deleted file mode 100644 index 9a16810..0000000 --- a/src/MomentoTimestampInvalidator.php +++ /dev/null @@ -1,19 +0,0 @@ -backend = $factory->getForTagInvalidator('fakebin'); - } - - public function invalidateTags(array $tags) { - $this->backend->invalidateTags($tags); - } - -} diff --git a/tests/src/Kernel/MomentoCacheBackendTest.php b/tests/src/Kernel/MomentoCacheBackendTest.php index ba4cd61..4d15657 100644 --- a/tests/src/Kernel/MomentoCacheBackendTest.php +++ b/tests/src/Kernel/MomentoCacheBackendTest.php @@ -2,8 +2,10 @@ namespace Drupal\Tests\momento_cache\Kernel; +use Drupal\Core\Cache\DatabaseCacheTagsChecksum; use Drupal\KernelTests\Core\Cache\GenericCacheBackendUnitTestBase; use Drupal\momento_cache\MomentoCacheBackendFactory; +use Drupal\momento_cache\Client\MomentoClientFactory; /** * Tests the MomentoCacheBackend. @@ -26,8 +28,10 @@ class MomentoCacheBackendTest extends GenericCacheBackendUnitTestBase { * A new MomentoCacheBackend object. */ protected function createCacheBackend($bin) { - $factory = new MomentoCacheBackendFactory(); - return $factory->get($bin); + $clientFactory = $this->container->get('momento_cache.factory'); + $checksumProvider = $this->container->get('cache_tags.invalidator.checksum'); + $backendFactory = new MomentoCacheBackendFactory($clientFactory, $checksumProvider); + return $backendFactory->get($bin); } public function testTtl() {