Skip to content
Snippets Groups Projects
Unverified Commit 276716b8 authored by Nikita Malyshev's avatar Nikita Malyshev
Browse files

Initial commit

parents
Branches
Tags 1.0.0-alpha1
No related merge requests found
Showing
with 575 additions and 0 deletions
# Cache Pilot
The module provides easy-to-use configuration, service, and command-line tools
for clearing the APCu and/or Zend OPcache caches. The cache is cleared through
FastCGI using TCP or Unix Sockets. This is necessary when clearing the cache
from the command line during deployment because it uses a dedicated instance of
PHP-FPM.
## Table of contents
- Requirements
- Installation
- Configuration
- Usage
- FAQ
## Requirements
- Drupal 10.3+
- PHP 8.3+
- (optional) APCu
- (optional) Zend OPcache
## Installation
Install as you would normally install a contributed Drupal module. For further
information, see [Installing Drupal Modules][1].
## Configuration
To configure a module, go to: **Configuration** > **Performance** > **Cache
Pilot Settings**.
There are several settings available:
* **Connection type:** This setting defines how the module connects to FastCGI.
You have three options to choose from:
* **None:** This option disables the connection. It is useful for development
environments where you don't need this functionality.
* **TCP:** Connects using the '[host]:[port]' information, for example:
`127.0.0.1:9000`.
* **Unix domain socket:** Connects using a unix socket to which you provide a
path, for example: `/var/run/php/php-fpm.sock`.
## Usage
The main purpose of this module is to clear the Zend OPcache and/or APCu caches
during deployment using the command-line interface (CLI). To make it easier to
use, the module provides two commands:
* `drush cache-pilot:apcu:clear`: Clears APCu caches.
* `drush cache-pilot:opcache:clear`: Clears Zend OPcache caches.
You can clear these caches from the user interface (UI) or directly from the
code using a dedicated service for that purpose.
## FAQ
### How to disable it for a specific environment?
To disable the functionality of the module in a particular environment, it is
recommended to use settings.php to override the configuration:
```php
$config['cache_pilot.settings']['connection']['type'] = NULL;
```
[1]: https://www.drupal.org/docs/extending-drupal/installing-drupal-modules
<?php
/**
* @file
* Provides responses from FastCGI.
*
* @internal
*/
if (!array_key_exists('cache_pilot', $_SERVER) || $_SERVER['cache_pilot'] !== '1') {
echo 'Request is not from the cache_pilot module.';
exit;
}
/**
* Returns whether APCu is enabled.
*/
function is_apcu_enabled(): bool {
return extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOL);
}
/**
* Returns whether Zend OPcache is enabled.
*/
function is_opcache_enabled(): bool {
return extension_loaded('Zend OPcache') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOL);
}
// phpcs:ignore DrupalPractice.Variables.GetRequestData.SuperglobalAccessedWithVar
switch ($_POST['command'] ?? NULL) {
case 'echo':
echo 'Ok';
break;
case 'apcu-status':
echo is_apcu_enabled() ? 'Ok' : 'APCu is not enabled';
break;
case 'apcu-clear':
if (is_apcu_enabled()) {
apcu_clear_cache();
echo 'Ok';
}
else {
echo 'APCu clear failed because APCu is not enabled';
}
break;
case 'apcu-statistic':
if (is_apcu_enabled()) {
$statistics = [
'cache_info' => apcu_cache_info(TRUE),
'memory_info' => apcu_sma_info(),
];
}
else {
$statistics = [];
}
echo json_encode($statistics);
break;
case 'opcache-status':
echo is_opcache_enabled() ? 'Ok' : 'Zend Opcache is not enabled';
break;
case 'opcache-clear':
if (is_opcache_enabled()) {
opcache_reset();
echo 'Ok';
}
else {
echo 'Zend Opcache clear failed because Zend Opcache is not enabled';
}
break;
case 'opcache-statistic':
if (is_opcache_enabled()) {
$statistics = opcache_get_status(FALSE);
}
else {
$statistics = [];
}
echo json_encode($statistics);
break;
default:
echo 'Unexpected command';
break;
}
name: Cache Pilot
type: module
description: Simple tools to manage the APCu and OPcache caches.
core_version_requirement: ^10.3 || ^11
<?php
declare(strict_types=1);
use Drupal\cache_pilot\Cache\ApcuCache;
use Drupal\cache_pilot\Client\Client;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Implements hook_requirements().
*/
function cache_pilot_requirements(string $phase): array {
$requirements = [];
if ($phase === 'runtime') {
$fast_cgi_client = \Drupal::service(Client::class);
$is_connected = $fast_cgi_client->isConnected();
$requirements['cache_pilot_connection'] = [
'title' => new TranslatableMarkup('Cache Pilot connection'),
'description' => $is_connected ? new TranslatableMarkup('Connected') : new TranslatableMarkup('Not connected'),
'severity' => $is_connected ? REQUIREMENT_OK : REQUIREMENT_WARNING,
];
}
return $requirements;
}
fragmentation-bar:
css:
component:
css/cache-pilot-fragmentation-bar.css: { }
cache_pilot.dashboard:
title: 'Cache Pilot dashboard'
description: 'Get statistics for APCu and Zend Opcache caches.'
route_name: cache_pilot.dashboard
parent: system.admin_reports
cache_pilot.settings:
title: 'Cache Pilot settings'
route_name: cache_pilot.settings
base_route: system.performance_settings
<?php
declare(strict_types=1);
use Drupal\cache_pilot\Cache\ApcuCache;
use Drupal\cache_pilot\Hook\Theme\PreprocessCachePilotApcuStatistics;
use Drupal\cache_pilot\Hook\Theme\PreprocessCachePilotOpcacheStatistics;
/**
* Implements hook_cache_flush().
*/
function cache_pilot_cache_flush(): void {
$apcu = \Drupal::service(ApcuCache::class);
$apcu->clear();
}
/**
* Implements hook_theme().
*/
function cache_pilot_theme(): array {
return [
'cache_pilot_dashboard' => [
'variables' => [
'apcu' => [],
'opcache' => [],
],
],
'cache_pilot_apcu_statistics' => [
'variables' => [
'statistics' => [],
],
],
'cache_pilot_opcache_statistics' => [
'variables' => [
'statistics' => [],
],
],
'cache_pilot_fragmentation_bar' => [
'variables' => [
'fragments' => [],
],
],
];
}
/**
* Implements template_preprocess_HOOK().
*/
function template_preprocess_cache_pilot_apcu_statistics(array &$variables): void {
\Drupal::classResolver(PreprocessCachePilotApcuStatistics::class)($variables);
}
/**
* Implements template_preprocess_HOOK().
*/
function template_preprocess_cache_pilot_opcache_statistics(array &$variables): void {
\Drupal::classResolver(PreprocessCachePilotOpcacheStatistics::class)($variables);
}
cache_pilot.administer:
title: 'Administer Cache Pilot settings'
restrict access: true
cache_pilot.settings:
path: '/admin/config/development/performance/cache-pilot'
methods: ['GET', 'POST']
defaults:
_title: 'Cache Pilot settings'
_form: \Drupal\cache_pilot\Form\SettingsForm
requirements:
_permission: 'cache_pilot.administer'
cache_pilot.dashboard:
path: '/admin/reports/cache-pilot'
methods: ['GET']
defaults:
_controller: '\Drupal\cache_pilot\Controller\DashboardController'
_title: 'Cache Pilot dashboard'
requirements:
_permission: 'cache_pilot.administer'
services:
_defaults:
autowire: true
logger.channel.cache_pilot:
class: Drupal\Core\Logger\LoggerChannel
factory: logger.factory:get
arguments: [ 'cache_pilot' ]
Drupal\cache_pilot\Client\Client: {}
Drupal\cache_pilot\Connection\SocketConnectionBuilder: {}
Drupal\cache_pilot\Cache\ApcuCache: {}
Drupal\cache_pilot\Cache\OpcacheCache: {}
{
"name": "drupal/cache_pilot",
"type": "drupal-module",
"version": "1.0.0-dev",
"description": "Simple tools to manage the APCu and OPcache caches.",
"keywords": [
"Drupal",
"cache",
"APCu",
"Zend OPcache"
],
"license": "GPL-2.0+",
"homepage": "https://www.drupal.org/project/apcu",
"support": {
"issues": "https://www.drupal.org/project/issues/apcu",
"source": "https://cgit.drupalcode.org/apcu"
},
"require": {
"php": ">=8.3",
"drupal/core": "^10.3 || ^11.0",
"hollodotme/fast-cgi-client": "^3.1"
}
}
connection:
type: ''
host: ''
uds_path: ''
cache_pilot.settings:
type: config_object
label: Cache Pilot settings
mapping:
connection:
type: mapping
label: Cache Pilot connection
mapping:
type:
type: string
label: Type
constraints:
Choice:
- ''
- 'tcp'
- 'uds'
host:
type: string
label: Host
uds_path:
type: string
label: Socket path
.cache-pilot-fragmentation-bar {
display: flex;
flex-direction: column;
gap: var(--space-s, 0.5rem);
}
.cache-pilot-fragmentation-bar__progress {
border-radius: var(--progress-bar-default-size-radius, 1.5rem);
overflow: hidden;
display: flex;
}
.cache-pilot-fragmentation-bar__fragment {
height: var(--progress-bar-default-size, 1.5rem);
width: var(--fragment-percentage, 0%);
background: var(--fragment-background-color);
}
.cache-pilot-fragmentation-bar__fragment:not(:first-child) {
margin-left: 0.125rem;
}
.cache-pilot-fragmentation-bar__color {
width: 1rem;
height: 1rem;
background: var(--fragment-background-color);
border-radius: var(--progress-bar-default-size-radius, 1.5rem);
}
.cache-pilot-fragmentation-bar__label {
display: flex;
align-items: center;
gap: var(--space-xs, 0.25rem);
font-size: var(--font-size-label, 0.889rem);
}
services:
cache_pilot.command.opcache_clear:
class: Drupal\cache_pilot\Command\OpcacheClear
arguments:
- '@Drupal\cache_pilot\Cache\OpcacheCache'
tags:
- { name: console.command }
cache_pilot.command.apcu_clear:
class: Drupal\cache_pilot\Command\ApcuClear
arguments:
- '@Drupal\cache_pilot\Cache\ApcuCache'
tags:
- { name: console.command }
parameters:
level: 9
ignoreErrors:
- identifier: missingType.iterableValue
- '#Drush\\Style\\DrushStyle#'
\ No newline at end of file
<?php
declare(strict_types=1);
namespace Drupal\cache_pilot\Cache;
use Drupal\Component\Serialization\Json;
use Drupal\cache_pilot\Client\Client;
use Drupal\cache_pilot\Contract\CacheInterface;
use Drupal\cache_pilot\Data\ClientCommand;
/**
* Provides an APCu cache integration.
*/
final readonly class ApcuCache implements CacheInterface {
public function __construct(
private Client $client,
) {}
/**
* {@inheritdoc}
*/
public function clear(): bool {
return $this->client->sendCommand(ClientCommand::ApcuClear)->getBody() === 'Ok';
}
/**
* {@inheritdoc}
*/
public function isEnabled(): bool {
return $this->client->sendCommand(ClientCommand::ApcuStatus)->getBody() === 'Ok';
}
/**
* {@inheritdoc}
*/
public function statistics(): array {
$statistics = Json::decode($this->client->sendCommand(ClientCommand::ApcuStatistic)->getBody());
return is_array($statistics) ? $statistics : [];
}
}
<?php
declare(strict_types=1);
namespace Drupal\cache_pilot\Cache;
use Drupal\Component\Serialization\Json;
use Drupal\cache_pilot\Client\Client;
use Drupal\cache_pilot\Contract\CacheInterface;
use Drupal\cache_pilot\Data\ClientCommand;
/**
* Provides a Zend OPcache cache integration.
*/
final class OpcacheCache implements CacheInterface {
public function __construct(
private Client $client,
) {}
/**
* {@inheritdoc}
*/
public function clear(): bool {
return $this->client->sendCommand(ClientCommand::OpcacheClear)->getBody() === 'Ok';
}
/**
* {@inheritdoc}
*/
public function isEnabled(): bool {
return $this->client->sendCommand(ClientCommand::OpcacheStatus)->getBody() === 'Ok';
}
/**
* {@inheritdoc}
*/
public function statistics(): array {
$statistics = Json::decode($this->client->sendCommand(ClientCommand::OpcacheStatistic)->getBody());
return is_array($statistics) ? $statistics : [];
}
}
<?php
declare(strict_types=1);
namespace Drupal\cache_pilot\Client;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\cache_pilot\Connection\SocketConnectionBuilder;
use Drupal\cache_pilot\Data\ClientCommand;
use Drupal\cache_pilot\Exception\MissingConnectionTypeException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use hollodotme\FastCGI\Client as FastCgiClient;
use hollodotme\FastCGI\Interfaces\ProvidesResponseData;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\Responses\Response;
/**
* Provides a FastCGI client.
*/
final readonly class Client {
public function __construct(
#[Autowire(service: 'logger.channel.cache_pilot')]
private LoggerChannelInterface $logger,
private SocketConnectionBuilder $connection,
private ModuleExtensionList $moduleExtensionList,
) {}
/**
* Checks if the connection to the FastCGI is established.
*
* @return bool
* TRUE if the connection is established, FALSE otherwise.
*/
public function isConnected(): bool {
return $this->sendCommand(ClientCommand::Echo)->getBody() === 'Ok';
}
/**
* Sends the command to the FastCGI server.
*
* @param \Drupal\cache_pilot\Data\ClientCommand $command
* The command to send.
*
* @return \hollodotme\FastCGI\Interfaces\ProvidesResponseData
* The response from the FastCGI server.
*/
public function sendCommand(ClientCommand $command): ProvidesResponseData {
return $this->sendSocketRequest($command->value);
}
/**
* Sends the command to the FastCGI server via socket.
*
* @param string $command
* The command to send.
*
* @return \hollodotme\FastCGI\Interfaces\ProvidesResponseData
* The response from the FastCGI server.
*/
protected function sendSocketRequest(string $command): ProvidesResponseData {
try {
$connection = $this->connection->build();
}
catch (MissingConnectionTypeException) {
return new Response('', 'Missing connection type', 0);
}
$module_path = $this->moduleExtensionList->getPath('cache_pilot');
$script_path = \DRUPAL_ROOT . '/' . $module_path . '/cache-pilot.php';
$client = new FastCgiClient();
$request = new PostRequest(
scriptFilename: $script_path,
content: http_build_query(['command' => $command]),
);
$request->setCustomVar('cache_pilot', '1');
try {
$response = $client->sendRequest($connection, $request);
}
catch (\Throwable $e) {
$this->logger->error(
message: 'The request failed: @message',
context: ['@message' => $e->getMessage()],
);
return new Response('', 'Request failed', 0);
}
return $response;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment