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
55 changes: 0 additions & 55 deletions captainhook.json

This file was deleted.

18 changes: 16 additions & 2 deletions docs/adapters/pdo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ Highlights:
* batched ``multiFetch()`` via single ``IN (...)`` query
* MySQL/MariaDB locking uses bounded connection-scoped named locks
* PostgreSQL locking uses the two-key advisory-lock form
* SQLite and other PDO drivers without advisory locks use an injected
``FileLockProvider`` fallback
* SQLite and other PDO drivers without advisory locks use a fallback-backed
``FileLockProvider`` lock by default
* expired data rows are misses and can be removed in bounded batches with
``PdoCacheAdapter::pruneExpired($limit)``

Expand All @@ -45,6 +45,20 @@ advisory locks are connection-owned. Treat ``leaseSeconds`` as API
compatibility, not automatic expiry. The provider rejects re-entrant
acquisition of the same lock through one provider instance.

For deployments that require native PDO coordination, construct the lock
provider in strict mode and attach it to the cache. Strict mode supports only
MySQL/MariaDB and PostgreSQL and throws ``UnsupportedPdoLockDriver`` at
construction for other PDO drivers; it never falls back to file locking.

.. code-block:: php

use Infocyph\CacheLayer\Cache\Lock\PdoLockProvider;

$cache->setLockProvider(PdoLockProvider::strict($pdo));

The default ``new PdoLockProvider($pdo)`` remains fallback-enabled. It is a
fallback-backed PDO lock on unsupported drivers, not a distributed PDO lock.

Examples:

.. code-block:: php
Expand Down
8 changes: 5 additions & 3 deletions docs/adapters/sqlite.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ Use ``Cache::pdo(...)`` directly if you want to switch to MySQL/MariaDB/PostgreS
without changing the rest of your cache usage pattern.

SQLite does not provide the cross-process advisory-lock contract required by
``LockProviderInterface``. Its ``PdoLockProvider`` therefore delegates locking
to ``FileLockProvider``. All coordinating processes must use the same writable
lock directory and filesystem.
``LockProviderInterface``. Its default fallback-backed ``PdoLockProvider``
therefore delegates locking to ``FileLockProvider``. All coordinating processes
must use the same writable lock directory and filesystem. SQLite cannot use
``PdoLockProvider::strict()`` because strict PDO locks require a native
MySQL/MariaDB or PostgreSQL driver.

Example
-------
Expand Down
24 changes: 21 additions & 3 deletions docs/metrics-and-locking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,27 @@ Provider semantics:
handle's lease duration does not force expiry.
* File locks retain an open ``flock`` until release. Renewal verifies that the
owned file resource is still open.
* SQLite and PDO drivers without native advisory locks use the file provider
fallback. Use the same writable lock directory in every process that must
coordinate.
* ``PdoLockProvider`` uses a native PDO lock only for MySQL/MariaDB and
PostgreSQL. SQLite and other drivers use its fallback provider by default;
this is a fallback-backed PDO lock, not a distributed PDO lock. Use the same
writable lock directory in every process that must coordinate when using
``FileLockProvider``.

Require a native PDO lock with strict mode. It validates the PDO driver during
construction and throws ``UnsupportedPdoLockDriver`` rather than silently
falling back:

.. code-block:: php

use Infocyph\CacheLayer\Cache\Lock\PdoLockProvider;

$locks = PdoLockProvider::strict($pdo);
$cache->setLockProvider($locks);

``PdoLockProvider::supportsNativeDriver($driver)`` reports whether a driver
supports native locking. Constructing ``new PdoLockProvider($pdo)`` remains
fallback-enabled for backward compatibility; pass any
``LockProviderInterface`` explicitly to choose a different fallback.

Release is best effort and ownership guarded. Distributed leases may disappear
after expiry, eviction, backend restart, or connection loss; callers must not
Expand Down
33 changes: 27 additions & 6 deletions src/Cache/Lock/PdoLockProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,31 @@ public function __construct(
private \PDO $pdo,
private string $prefix = 'cachelayer:lock:',
int $retrySleepMicros = 50_000,
private FileLockProvider $fallback = new FileLockProvider(),
private ?LockProviderInterface $fallback = new FileLockProvider(),
) {
$this->retrySleepMicros = max(1_000, $retrySleepMicros);
$driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME);
$this->driver = is_string($driver) ? $driver : '';
$this->driver = is_string($driver) ? strtolower($driver) : '';

if ($this->fallback === null && !self::supportsNativeDriver($this->driver)) {
throw new UnsupportedPdoLockDriver(sprintf(
'PDO driver "%s" does not support native cache locks.',
$this->driver === '' ? 'unknown' : $this->driver,
));
}
}

public static function strict(
\PDO $pdo,
string $prefix = 'cachelayer:lock:',
int $retrySleepMicros = 50_000,
): self {
return new self($pdo, $prefix, $retrySleepMicros, fallback: null);
}

public static function supportsNativeDriver(string $driver): bool
{
return in_array(strtolower($driver), ['mysql', 'mariadb', 'pgsql'], true);
}

public function acquire(string $key, float $waitSeconds, float $leaseSeconds = 30.0): ?LockHandle
Expand All @@ -37,7 +57,7 @@ public function acquire(string $key, float $waitSeconds, float $leaseSeconds = 3
return match ($this->driver) {
'mysql', 'mariadb' => $this->acquireMysql($key, $waitSeconds, $leaseSeconds),
'pgsql' => $this->acquirePgsql($key, $waitSeconds, $leaseSeconds),
default => $this->fallback->acquire($key, $waitSeconds, $leaseSeconds),
default => $this->fallback?->acquire($key, $waitSeconds, $leaseSeconds),
};
}

Expand All @@ -52,7 +72,7 @@ public function refresh(?LockHandle $handle, float $leaseSeconds): bool

return match ($this->driver) {
'mysql', 'mariadb', 'pgsql' => $this->owns($handle) && $this->connectionAlive(),
default => $this->fallback->refresh($handle, $leaseSeconds),
default => $this->fallback?->refresh($handle, $leaseSeconds) ?? false,
};
}

Expand All @@ -62,8 +82,8 @@ public function release(?LockHandle $handle): void
return;
}

if (!in_array($this->driver, ['mysql', 'mariadb', 'pgsql'], true)) {
$this->fallback->release($handle);
if (!self::supportsNativeDriver($this->driver)) {
$this->fallback?->release($handle);

return;
}
Expand All @@ -74,6 +94,7 @@ public function release(?LockHandle $handle): void
$released = match ($this->driver) {
'mysql', 'mariadb' => $this->releaseMysql($handle),
'pgsql' => $this->releasePgsql($handle),
default => false,
};
if ($released) {
unset($this->activeTokens[$handle->key]);
Expand Down
9 changes: 9 additions & 0 deletions src/Cache/Lock/UnsupportedPdoLockDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Infocyph\CacheLayer\Cache\Lock;

use RuntimeException;

final class UnsupportedPdoLockDriver extends RuntimeException {}
Loading