diff --git a/Console/Command/WarmNodeCommand.php b/Console/Command/WarmNodeCommand.php index 2b07cb1..afd5bd3 100644 --- a/Console/Command/WarmNodeCommand.php +++ b/Console/Command/WarmNodeCommand.php @@ -1,35 +1,21 @@ state = $state; - $this->nodeWarmer = $nodeWarmer; + protected \Magento\Framework\App\State $state, + protected \MageOps\NodeWarmer\Service\NodeWarmer $nodeWarmer, + protected \Magento\Framework\Filesystem\DriverInterface $filesystemDriver, + ?string $name = null + ) { + parent::__construct($name); } - /** - * {@inheritdoc} - */ - protected function configure() + protected function configure(): void { $this ->setName('cs:warm-node') @@ -38,18 +24,15 @@ protected function configure() ->addOption('local-url', 'u', \Symfony\Component\Console\Input\InputOption::VALUE_REQUIRED, 'Url of the local app instance', 'http://localhost:80'); } - private function setAreaCode() + private function setAreaCode(): void { $this->state->setAreaCode(\Magento\Framework\App\Area::AREA_FRONTEND); } - /** - * {@inheritdoc} - */ protected function execute( \Symfony\Component\Console\Input\InputInterface $input, \Symfony\Component\Console\Output\OutputInterface $output - ) + ): int { $this->setAreaCode(); @@ -57,13 +40,13 @@ protected function execute( $localUrl = trim($input->getOption('local-url'), '"'); try { - @$this->nodeWarmer->warmNodeUp($localUrl, $force); + @$this->nodeWarmer->warmNodeUp($localUrl, $force); // phpcs:ignore $output->writeln(sprintf('Done, output saved to "%s"', $this->nodeWarmer->getWarmupLogFilePath())); return \Magento\Framework\Console\Cli::RETURN_SUCCESS; } catch (\Exception $exception) { $message = sprintf('Warmup did not complete, generated WARMUP file anyway: %s', (string)$exception); $output->writeln($message); - file_put_contents($this->nodeWarmer->getWarmupLogFilePath(), $message); + $this->filesystemDriver->filePutContents($this->nodeWarmer->getWarmupLogFilePath(), $message); return \Magento\Framework\Console\Cli::RETURN_FAILURE; } } diff --git a/Log/CapturingLoggerDecorator.php b/Log/CapturingLoggerDecorator.php index 05410c1..efb0b7f 100644 --- a/Log/CapturingLoggerDecorator.php +++ b/Log/CapturingLoggerDecorator.php @@ -1,31 +1,21 @@ upstreamLogger = $upstreamLogger; + protected array $buffer = []; + public function __construct( + protected \Psr\Log\LoggerInterface $upstreamLogger + ) { } /** * @return array */ - public function flush() + public function flush(): array { $buffer = $this->buffer; $this->buffer = []; @@ -33,14 +23,9 @@ public function flush() return $buffer; } - /** - * @param mixed $level - * @param string|\Stringable $message - * @param array $context - */ - public function log($level, string|\Stringable $message, array $context = []): void + public function log($level, $message, array $context = []): void //phpcs:ignore { $this->upstreamLogger->log($level, $message, $context); $this->buffer[] = [time(), $level, $message]; } -} \ No newline at end of file +} diff --git a/Log/LogFormatter.php b/Log/LogFormatter.php index a2becbc..30994eb 100644 --- a/Log/LogFormatter.php +++ b/Log/LogFormatter.php @@ -1,10 +1,12 @@ configWriter = $configWriter; - $this->configCollectionFactory = $configCollectionFactory; + protected \Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory $configCollectionFactory, + protected \Magento\Framework\App\Config\Storage\WriterInterface $configWriter + ) { } /** * @return bool */ - public function getCacheCodeVersion() + public function getCacheCodeVersion(): bool { return $this->getUncachedConfigValue(self::CACHE_CODE_VERSION_PATH); } @@ -37,23 +26,17 @@ public function getCacheCodeVersion() /** * @param string $newVersion */ - public function updateCacheCodeVersion($newVersion) + public function updateCacheCodeVersion(string $newVersion): void { $this->configWriter->save(self::CACHE_CODE_VERSION_PATH, $newVersion); } - /** - * @return bool - */ - public function getDeployedStaticContentVersion() + public function getDeployedStaticContentVersion(): ?string { return $this->getUncachedConfigValue(self::DEPLOYED_STATIC_CONTENT_VERSION_PATH); } - /** - * @param string $newVersion - */ - public function updateDeployedStaticContentVersion($newVersion) + public function updateDeployedStaticContentVersion(string $newVersion): void { $this->configWriter->save(self::DEPLOYED_STATIC_CONTENT_VERSION_PATH, $newVersion); } @@ -61,16 +44,14 @@ public function updateDeployedStaticContentVersion($newVersion) /** * Standard ScopeConfig can return value cached in redis * For this module we always need value directly from database - * @param $path - * @return string|null */ - protected function getUncachedConfigValue($path): ?string { + protected function getUncachedConfigValue(string $path): ?string { $configCollection = $this->configCollectionFactory->create(); $configCollection->addFieldToFilter('path', ['eq' => $path]); $config = $configCollection->getFirstItem(); - if($config === null) { + if ($config === null) { return null; } diff --git a/Service/MergedAssetsWarmupUrlsProvider.php b/Service/MergedAssetsWarmupUrlsProvider.php index 1ceef18..1913f1c 100644 --- a/Service/MergedAssetsWarmupUrlsProvider.php +++ b/Service/MergedAssetsWarmupUrlsProvider.php @@ -1,120 +1,66 @@ storeManager = $storeManager; - $this->productCollectionFactory = $productCollectionFactory; - $this->categoryCollectionFactory = $categoryCollectionFactory; - $this->urlFinder = $urlFinder; - $this->url = $url; + protected \Magento\Framework\View\LayoutInterfaceFactory $layoutFactory, + protected \Magento\Framework\View\Page\ConfigFactory $pageConfigFactory, + protected \Magento\Framework\View\DesignInterface $design, + protected \Magento\Framework\View\Asset\MergeService $mergeService, + protected \Magento\Store\Model\StoreManagerInterface $storeManager, + protected \Magento\Store\Model\App\Emulation $emulation, + protected array $layoutHandles + ) { } - public function getUrls() + public function getUrls(): array { - $urls = []; - + $staticStoreUrls = []; foreach ($this->storeManager->getStores() as $store) { - $urls[] = $store->getBaseUrl(); - $urls[] = $store->getUrl('customer/account/login'); - $urls[] = $store->getUrl('customer/account/create'); - $urls[] = $store->getUrl('customer/account/forgotpassword'); - $urls[] = $store->getUrl('checkout/cart'); - $urls[] = $store->getUrl('catalogsearch/result', ['_query' => ['q' => 'test']]); - $urls[] = $this->getProductUrl($store); - $urls[] = $this->getCategoryUrl($store); + $staticStoreUrls[] = $this->getAssetsUrls($store); } - - $urls = array_map([$this, 'extractHostAndPath'], $urls); + $staticStoreUrls = array_unique(array_merge(...$staticStoreUrls)); + $urls = array_map([$this, 'extractHostAndPath'], $staticStoreUrls); return $urls; } - public function getProductUrl($store) + protected function getAssetUrlsByContentType(string $contentType): array { - $productCollection = $this->productCollectionFactory->create(); - - $productCollection - ->addFieldToFilter( - 'status', - \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED - ) - ->addFieldToFilter( - 'visibility', - self::ALLOWED_VISIBILITIES - ) - ->setPageSize(1); + $group = $this->pageConfigFactory->create()->getAssetCollection()->getGroupByContentType($contentType); + $assets = $this->mergeService->getMergedAssets($group->getAll(), $contentType); - $product = $productCollection->getFirstItem(); + $urls = []; + foreach ($assets as $asset) { + $urls[] = $asset->getUrl(); + } - return sprintf( - '%scatalog/product/view/id/%d', - $store->getBaseUrl(), - $product->getId() - ); + return $urls; } - public function getCategoryUrl($store) + protected function getAssetsUrls(\Magento\Store\Api\Data\StoreInterface $store): array { - $categoryCollection = $this->categoryCollectionFactory->create(); - - $categoryCollection - ->addFieldToFilter('is_active', 1) - ->addAttributeToSelect('*') - ->setPageSize(1); - $categoryCollection->getSelect()->orderRand(); - - $category = $categoryCollection->getFirstItem(); - - return sprintf( - '%scatalog/category/view/id/%d', - $store->getBaseUrl(), - $category->getId() + $this->emulation->startEnvironmentEmulation($store->getId()); + $layout = $this->layoutFactory->create(); + $layout->getUpdate()->load($this->layoutHandles); + $layout->generateXml(); + $layout->generateElements(); + $assets = array_merge( + $this->getAssetUrlsByContentType(\Magento\Framework\View\Design\Theme\Customization\File\Js::CONTENT_TYPE), + $this->getAssetUrlsByContentType(\Magento\Framework\View\Design\Theme\Customization\File\Css::CONTENT_TYPE) ); + $this->emulation->stopEnvironmentEmulation(); + + return $assets; } - protected function extractHostAndPath($url) + protected function extractHostAndPath(string $url): array { - $urlParts = parse_url($url); + $urlParts = parse_url($url); // phpcs:ignore Magento2.Functions.DiscouragedFunction $path = $urlParts['path']; diff --git a/Service/NodeWarmer.php b/Service/NodeWarmer.php index 39a5785..d8e7b27 100644 --- a/Service/NodeWarmer.php +++ b/Service/NodeWarmer.php @@ -1,83 +1,33 @@ config = $config; - $this->eventManager = $eventManager; - $this->cacheManager = $cacheManager; - $this->directoryList = $directoryList; - $this->storeManager = $storeManager; - $this->urlGenerator = $urlGenerator; - $this->mergedAssetsWarmupUrlsProvider = $mergedAssetsWarmupUrlsProvider; - $this->logger = new \MageOps\NodeWarmer\Log\CapturingLoggerDecorator($logger); - + protected \MageOps\NodeWarmer\Model\Config $config, + protected \Magento\Framework\Event\ManagerInterface $eventManager, + protected \Magento\Framework\App\Cache\Manager $cacheManager, + protected \Magento\Framework\App\Filesystem\DirectoryList $directoryList, + protected \Magento\Store\Model\StoreManagerInterface $storeManager, + protected \Magento\Framework\UrlInterface $urlGenerator, + protected MergedAssetsWarmupUrlsProvider $mergedAssetsWarmupUrlsProvider, + protected \Psr\Log\LoggerInterface $psrLogger, + protected \Magento\Framework\Filesystem\DriverInterface $filesystemDriver, + protected \Symfony\Component\Stopwatch\StopwatchFactory $stopwatchFactory, + protected \MageOps\NodeWarmer\Log\LogFormatter $logFormatter, + ) { + $this->logger = new \MageOps\NodeWarmer\Log\CapturingLoggerDecorator($psrLogger); $this->http = new \GuzzleHttp\Client([ 'timeout' => self::WARMUP_TIMEOUT, 'allow_redirects' => true, @@ -85,23 +35,19 @@ public function __construct( ]); } - /** - * @param bool $force - * @param string $localUrl - */ - public function warmNodeUp($localUrl, $force = false) + public function warmNodeUp(string $localUrl, bool $force = false): void { $codeVersion = $this->getCurrentCodeVersion(); $deployedStaticContentVersion = $this->getDeployedStaticContentVersion(); $this->logger->info(sprintf('Starting warmup for node "%s"', $this->getNodeId())); - if (file_exists($this->getWarmupLogFilePath()) && !$force) { + if ($this->filesystemDriver->isExists($this->getWarmupLogFilePath()) && !$force) { $this->logger->info('Skipping warmup, already warm...'); return; } - $stopwatch = new \Symfony\Component\Stopwatch\Stopwatch(); + $stopwatch = $this->stopwatchFactory->create(); $stopwatch->start('warmup'); if ($this->config->getCacheCodeVersion() !== $codeVersion) { @@ -119,61 +65,65 @@ public function warmNodeUp($localUrl, $force = false) )); } - if($this->config->getDeployedStaticContentVersion() !== $deployedStaticContentVersion) { - $urls = $this->getUrlsToBeWarmedUp(); - - if (!empty($urls)) { - foreach (array_chunk($urls, $this->warmupRequestBatch) as $urlBatch) { - $asyncOperations = []; - - foreach ($urlBatch as $url) { - $uri = $localUrl . $url['path']; - $asyncOperations[] = [ - 'promise' => $this->http->getAsync( - $uri, - [ - 'headers' => [ - 'Host' => $url['host'], - 'X-Forwarded-Host' => $url['host'], - 'X-Forwarded-Proto' => 'https', - 'User-Agent' => 'Node Warmer' - ] + if ($this->config->getDeployedStaticContentVersion() == $deployedStaticContentVersion) { + $took = $stopwatch->stop('warmup')->getDuration() / 1000.0; + + $this->logger->info(sprintf('All done, took %.2fs', $took)); + $this->saveWarmupLog(); + return; + } + + $urls = $this->getUrlsToBeWarmedUp(); + + if (!empty($urls)) { + foreach (array_chunk($urls, $this->warmupRequestBatch) as $urlBatch) { + $asyncOperations = []; + + foreach ($urlBatch as $url) { + $uri = $localUrl . $url['path']; + $asyncOperations[] = [ + 'promise' => $this->http->getAsync( + $uri, + [ + 'headers' => [ + 'Host' => $url['host'], + 'X-Forwarded-Host' => $url['host'], + 'X-Forwarded-Proto' => 'https', + 'User-Agent' => 'Node Warmer' ] - ), - 'url' => $uri, - 'host' => $url['host'], - 'path' => $url['path'] - ]; - } + ] + ), + 'url' => $uri, + 'host' => $url['host'], + 'path' => $url['path'] + ]; + } - foreach ($asyncOperations as $asyncOperation) { - try { - $this->queryUrl($asyncOperation['promise'], $asyncOperation['url'], $asyncOperation['host']); - }catch(\Exception $exception) { - // Reduce parallel requests if we get a eg. 503 - if ($this->warmupRequestBatch > 1) { - $this->warmupRequestBatch -= 1; - } - // Retry failed requests - $urls[] = [ - 'host' => $asyncOperation['host'], - 'path' => $asyncOperation['path'] - ]; - } + foreach ($asyncOperations as $asyncOperation) { + try { + $this->queryUrl($asyncOperation['promise'], $asyncOperation['url'], $asyncOperation['host']); + }catch(\Exception $exception) { + // Reduce parallel requests if we get a eg. 503 + $this->warmupRequestBatch = max(1, $this->warmupRequestBatch - 1); + // Retry failed requests + $urls[] = [ + 'host' => $asyncOperation['host'], + 'path' => $asyncOperation['path'] + ]; } } } - - $this->config->updateDeployedStaticContentVersion($deployedStaticContentVersion); } + $this->config->updateDeployedStaticContentVersion($deployedStaticContentVersion); + $took = $stopwatch->stop('warmup')->getDuration() / 1000.0; $this->logger->info(sprintf('All done, took %.2fs', $took)); $this->saveWarmupLog(); } - protected function flushCache() + protected function flushCache(): void { $this->eventManager->dispatch('adminhtml_cache_flush_all'); $this->cacheManager->flush($this->cacheManager->getAvailableTypes()); @@ -182,31 +132,28 @@ protected function flushCache() /** * @return string */ - protected function getComposerLockPath() + protected function getComposerLockPath(): string { return $this->directoryList->getRoot() . '/composer.lock'; } - /** - * @return string - */ - public function getWarmupLogFilePath() + public function getWarmupLogFilePath(): string { - return $this->directoryList->getPath('pub') . '/' . self::WARM_LOG_FILENAME; + return $this->directoryList->getPath(\Magento\Framework\App\Filesystem\DirectoryList::PUB) . \DIRECTORY_SEPARATOR . self::WARM_LOG_FILENAME; } - protected function saveWarmupLog() + protected function saveWarmupLog(): void { $path = $this->getWarmupLogFilePath(); - $formatter = new \MageOps\NodeWarmer\Log\LogFormatter(); + $content = $this->logFormatter->formatBatch($this->logger->flush()); - file_put_contents( - $path, - $formatter->formatBatch($this->logger->flush()) - ); + $handle = $this->filesystemDriver->fileOpen($path, 'w'); + $this->filesystemDriver->fileWrite($handle, $content); + $this->filesystemDriver->fileClose($handle); } - protected function getUrlsToBeWarmedUp() { + protected function getUrlsToBeWarmedUp(): array + { $attempt = 1; do { @@ -223,20 +170,12 @@ protected function getUrlsToBeWarmedUp() { } $attempt++; - } - while($attempt < 10); + } while ($attempt < 10); return []; } - /** - * @param \GuzzleHttp\Promise\PromiseInterface $promise - * @param string $url - * @param string $host - * @throws \Exception - * @return void - */ - protected function queryUrl(\GuzzleHttp\Promise\PromiseInterface $promise, string $url, string $host) + protected function queryUrl(\GuzzleHttp\Promise\PromiseInterface $promise, string $url, string $host): void { $this->logger->info(sprintf('Querying url "%s" with host "%s"', $url, $host)); @@ -257,32 +196,32 @@ protected function queryUrl(\GuzzleHttp\Promise\PromiseInterface $promise, strin } } - /** - * @return string - */ - protected function getCurrentCodeVersion() + protected function getCurrentCodeVersion(): string { - return md5(file_get_contents($this->getComposerLockPath())); + try { + $composerLockContent = $this->filesystemDriver->fileGetContents($this->getComposerLockPath()); + return md5($composerLockContent); // phpcs:ignore + } catch (\Magento\Framework\Exception\FileSystemException $e) { + return ''; + } } - /** - * @return string - */ - protected function getDeployedStaticContentVersion() + protected function getDeployedStaticContentVersion(): string { - return file_get_contents($this->getDeployedStaticContentVersionPath()); + try { + return $this->filesystemDriver->fileGetContents($this->getDeployedStaticContentVersionPath()); + } catch (\Magento\Framework\Exception\FileSystemException $e) { + return ''; + } } - /** - * @return string - */ - protected function getDeployedStaticContentVersionPath() + protected function getDeployedStaticContentVersionPath(): string { - return $this->directoryList->getRoot() . '/pub/static/deployed_version.txt'; + return $this->directoryList->getPath(\Magento\Framework\App\Filesystem\DirectoryList::STATIC_VIEW) . \DIRECTORY_SEPARATOR . 'deployed_version.txt'; } - protected function getNodeId() + protected function getNodeId(): string { - return gethostname(); + return (string)gethostname(); } } diff --git a/etc/di.xml b/etc/di.xml index 747464c..90f343d 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,5 +1,6 @@ - + @@ -7,4 +8,20 @@ + + + + default + cms_index_index + checkout_index_index + catalog_product_view + catalog_category_view + checkout_cart_index + customer_account_index + customer_account_login + customer_account_forgotpassword + catalogsearch_result_index + + + diff --git a/registration.php b/registration.php index 9a9576e..16108e1 100644 --- a/registration.php +++ b/registration.php @@ -1,5 +1,7 @@