Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Model/Config/Source/StorageDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace SamJUK\CacheDebounce\Model\Config\Source;

use Magento\Framework\Data\OptionSourceInterface;

class StorageDriver implements OptionSourceInterface
{
/**
* @inheritDoc
*/
public function toOptionArray(): array
{
return [
['value' => 'db', 'label' => __('Database (default)')],
['value' => 'redis', 'label' => __('Redis')],
];
}
}
95 changes: 95 additions & 0 deletions Model/Storage/Pool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

declare(strict_types=1);

namespace SamJUK\CacheDebounce\Model\Storage;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\ObjectManagerInterface;

class Pool implements QueueStorageInterface
{
private const XML_PATH_STORAGE_DRIVER = 'samjuk_cache_debounce_advanced/general/storage_driver';
private const DEFAULT_DRIVER = 'db';

/** @var ObjectManagerInterface $objectManager */
private $objectManager;

/** @var ScopeConfigInterface $scopeConfig */
private $scopeConfig;

/** @var string[] $drivers */
private $drivers;

/** @var QueueStorageInterface|null $resolved */
private $resolved;

public function __construct(
ObjectManagerInterface $objectManager,
ScopeConfigInterface $scopeConfig,
array $drivers = []
) {
$this->objectManager = $objectManager;
$this->scopeConfig = $scopeConfig;
$this->drivers = $drivers;
}

/**
* @inheritDoc
*/
public function add(array $tags): void
{
$this->driver()->add($tags);
}

/**
* @inheritDoc
*/
public function claim(): string
{
return $this->driver()->claim();
}

/**
* @inheritDoc
*/
public function tags(string $batchId): array
{
return $this->driver()->tags($batchId);
}

/**
* @inheritDoc
*/
public function clear(string $batchId): void
{
$this->driver()->clear($batchId);
}

/**
* @inheritDoc
*/
public function release(string $batchId): void
{
$this->driver()->release($batchId);
}

/**
* @inheritDoc
*/
public function activeBatch(): string
{
return $this->driver()->activeBatch();
}

private function driver(): QueueStorageInterface
{
if ($this->resolved === null) {
$key = (string)$this->scopeConfig->getValue(self::XML_PATH_STORAGE_DRIVER) ?: self::DEFAULT_DRIVER;
$class = $this->drivers[$key] ?? $this->drivers[self::DEFAULT_DRIVER];
$this->resolved = $this->objectManager->create($class);
}

return $this->resolved;
}
}
99 changes: 99 additions & 0 deletions Model/Storage/Redis.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace SamJUK\CacheDebounce\Model\Storage;

use SamJUK\CacheDebounce\Model\Storage\Redis\ConnectionResolver;

class Redis implements QueueStorageInterface
{
private const KEY_LIVE = 'samjuk_cache_debounce:queue:live';
private const KEY_BATCH_PREFIX = 'samjuk_cache_debounce:queue:batch:';
private const ACTIVE_BATCH_ID = 'active';

/** @var ConnectionResolver $connectionResolver */
private $connectionResolver;

public function __construct(
ConnectionResolver $connectionResolver
) {
$this->connectionResolver = $connectionResolver;
}

/**
* @inheritDoc
*/
public function add(array $tags): void
{
if (!$tags) {
return;
}

$this->connectionResolver->getClient()->sAdd(self::KEY_LIVE, ...$tags);
}

/**
* @inheritDoc
*/
public function claim(): string
{
if ($this->activeBatch() !== '') {
return '';
}

$client = $this->connectionResolver->getClient();

try {
$client->rename(self::KEY_LIVE, $this->batchKey(self::ACTIVE_BATCH_ID));
} catch (\CredisException $e) {
if (stripos($e->getMessage(), 'no such key') !== false) {
return '';
}
throw $e;
}

return self::ACTIVE_BATCH_ID;
}

/**
* @inheritDoc
*/
public function tags(string $batchId): array
{
return $this->connectionResolver->getClient()->sMembers($this->batchKey($batchId));
}

/**
* @inheritDoc
*/
public function clear(string $batchId): void
{
$this->connectionResolver->getClient()->del($this->batchKey($batchId));
}

/**
* @inheritDoc
*/
public function release(string $batchId): void
{
$client = $this->connectionResolver->getClient();
$client->sUnionStore(self::KEY_LIVE, self::KEY_LIVE, $this->batchKey($batchId));
$client->del($this->batchKey($batchId));
}

/**
* @inheritDoc
*/
public function activeBatch(): string
{
return $this->connectionResolver->getClient()->sCard($this->batchKey(self::ACTIVE_BATCH_ID)) > 0
? self::ACTIVE_BATCH_ID
: '';
}

private function batchKey(string $batchId): string
{
return self::KEY_BATCH_PREFIX . $batchId;
}
}
88 changes: 88 additions & 0 deletions Model/Storage/Redis/ConnectionResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace SamJUK\CacheDebounce\Model\Storage\Redis;

use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\Exception\LocalizedException;

class ConnectionResolver
{
private const CONFIG_PATH_DEDICATED = 'cache_debounce/redis';
private const CONFIG_PATH_SHARED_FALLBACK = 'cache/frontend/page_cache/backend_options';

/** @var DeploymentConfig $deploymentConfig */
private $deploymentConfig;

/** @var \Credis_Client|null $client */
private $client;

public function __construct(
DeploymentConfig $deploymentConfig
) {
$this->deploymentConfig = $deploymentConfig;
}

/**
* @throws LocalizedException when no usable connection can be resolved.
*/
public function getClient(): \Credis_Client
{
if ($this->client !== null) {
return $this->client;
}

$dedicated = $this->deploymentConfig->get(self::CONFIG_PATH_DEDICATED);
$config = $dedicated ? $this->normalize($dedicated) : $this->resolveFallback();

if (!$config) {
throw new LocalizedException(__(
'SamJUK_CacheDebounce: no Redis connection could be resolved. Configure the '
. '"cache_debounce.redis" block in app/etc/env.php, or configure a Redis backend '
. 'for the "page_cache" cache frontend.'
));
}

$this->client = new \Credis_Client(
$config['host'],
(int)$config['port'],
null,
'',
(int)$config['database'],
$config['password'] ?: null
);

return $this->client;
}

private function resolveFallback(): ?array
{
$backendOptions = $this->deploymentConfig->get(self::CONFIG_PATH_SHARED_FALLBACK);

if (!$backendOptions) {
return null;
}

return [
'host' => $backendOptions['server'] ?? '127.0.0.1',
'port' => $backendOptions['port'] ?? 6379,
'password' => $backendOptions['password'] ?? '',
'database' => $backendOptions['database'] ?? 0,
];
}

/**
* Fill in defaults for whichever of host/port/database/password the
* "cache_debounce.redis" env.php block leaves out.
*/
private function normalize(array $config): array
{
return [
'host' => $config['host'] ?? '127.0.0.1',
'port' => $config['port'] ?? 6379,
'password' => $config['password'] ?? '',
'database' => $config['database'] ?? 0,
];
}
}
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@ Option | Config Path | Default | Description
--- | --- | --- | ---
Enabled | `samjuk_cache_debounce/general/enabled` | `0` | Feature flag to toggle functionality of the module
Flush Schedule | `samjuk_cache_debounce/cron/flush_schedule` | `*/5 0 0 0 0` | Cron schedule to run the scheduled flush
Storage Driver | `samjuk_cache_debounce_advanced/general/storage_driver` | `db` | Queue storage backend — `db` or `redis`. A deploy-time infra decision, not a store setting — see below

This one is a deliberate exception to "Configuration can be handled via System configuration" above: it's hidden from Stores > Configuration entirely (`showInDefault`/`showInWebsite`/`showInStore` all `0`) and gated behind its own ACL resource (`SamJUK_CacheDebounce::storage_driver`) that isn't granted to any role by default. It exists only so `config:set` has a registered path to write to. Set it with:
```sh
php bin/magento config:set samjuk_cache_debounce_advanced/general/storage_driver redis
```
Add `-e` to lock it into `app/etc/env.php` instead of `core_config_data`, removing Admin overridability entirely.

### Redis storage driver

The `redis` driver connects via a dedicated `app/etc/env.php` block:
```php
'cache_debounce' => [
'redis' => [
'host' => '127.0.0.1',
'port' => '6379',
'password' => '',
'database' => '2',
],
],
```

If that block is absent, the module falls back to whatever Redis backend is already configured for the `page_cache` cache frontend, so it works zero-config once the driver is switched on.

> **Note:** sharing the page cache's Redis instance means pending purge tags are subject to that instance's `maxmemory-policy`. If it's `allkeys-lru`/`allkeys-lfu` (common for FPC), tags can be silently evicted under memory pressure. Configure a dedicated block above to avoid this.

### Adding your own storage driver

Drivers are resolved from an array bound in `etc/di.xml`, keyed by the value of `storage_driver`. Any module can add an entry to extend it, without touching this module's `di.xml`:
```xml
<type name="SamJUK\CacheDebounce\Model\Storage\Pool">
<arguments>
<argument name="drivers" xsi:type="array">
<item name="my_driver" xsi:type="string">Vendor\Module\Model\Storage\MyDriver</item>
</argument>
</arguments>
</type>
```
`MyDriver` just needs to implement `QueueStorageInterface`. Then `bin/magento config:set samjuk_cache_debounce_advanced/general/storage_driver my_driver`.

## Will this help my store?

Expand Down
Loading