Skip to content

Instantly share code, notes, and snippets.

@hbackman
Created June 12, 2020 18:38
Show Gist options
  • Save hbackman/82fc0e86fd484001dd354f64e668b28b to your computer and use it in GitHub Desktop.
Save hbackman/82fc0e86fd484001dd354f64e668b28b to your computer and use it in GitHub Desktop.
Laravel 7.x Database Cache Locking for 6.x
<?php
namespace App\Providers;
use App\Cache\DatabaseStore;
use Illuminate\Cache\CacheManager;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
class CacheServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*/
public function boot()
{
$this->registerDatabaseDriver();
}
// This driver is copied from the laravel/framework github repo.
// This should be removed if we upgrade to laravel 7, because
// it's already built in.
private function registerDatabaseDriver()
{
$cacheManager = $this->app->make(CacheManager::class);
$cacheManager->forgetDriver('database');
$registerDriver =
function (Application $app, array $config) {
/** @var CacheManager $self */
$self = $this;
// Get the current database connection and prefix for
// the given configured connection.
$connection = $app['db']
->connection($config['connection'] ?? null);
// Build and return the reposistory
return $self->repository(
new DatabaseStore(
$connection,
$config['table'],
$self->getPrefix($config)
)
);
};
$cacheManager->extend('database', $registerDriver);
}
}
<?php
namespace App\Cache;
use Illuminate\Database\QueryException;
use Illuminate\Database\Connection;
use Illuminate\Cache\Lock;
class DatabaseLock extends Lock
{
/**
* The database connection instance.
*
* @var Connection
*/
protected $connection;
/**
* The database table name.
*
* @var string
*/
protected $table;
/**
* The prune probability odds.
*
* @var array
*/
protected $lottery;
/**
* Create a new lock instance.
*
* @param Connection $connection
* @param string $table
* @param string $name
* @param int $seconds
* @param string|null $owner
* @param array $lottery
* @return void
*/
public function __construct(Connection $connection, $table, $name, $seconds, $owner = null, $lottery = [2, 100])
{
parent::__construct($name, $seconds, $owner);
$this->connection = $connection;
$this->table = $table;
$this->lottery = $lottery;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
$acquired = false;
try {
$this->connection->table($this->table)->insert([
'key' => $this->name,
'owner' => $this->owner,
'expiration' => $this->expiresAt(),
]);
$acquired = true;
} catch (QueryException $e) {
$updated = $this->connection->table($this->table)
->where('key', $this->name)
->where(function ($query) {
return $query->where('owner', $this->owner)->orWhere('expiration', '<=', time());
})->update([
'owner' => $this->owner,
'expiration' => $this->expiresAt(),
]);
$acquired = $updated >= 1;
}
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
$this->connection->table($this->table)->where('expiration', '<=', time())->delete();
}
return $acquired;
}
/**
* Get the UNIX timestamp indicating when the lock should expire.
*
* @return int
*/
protected function expiresAt()
{
return $this->seconds > 0 ? time() + $this->seconds : now()->addDays(1)->getTimestamp();
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
if ($this->isOwnedByCurrentProcess()) {
$this->connection->table($this->table)
->where('key', $this->name)
->where('owner', $this->owner)
->delete();
return true;
}
return false;
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->connection->table($this->table)
->where('key', $this->name)
->delete();
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return string
*/
protected function getCurrentOwner()
{
return optional($this->connection->table($this->table)->where('key', $this->name)->first())->owner;
}
}
<?php
namespace App\Cache;
use Illuminate\Cache\DatabaseStore as LaravelDatabaseStore;
use Illuminate\Database\ConnectionInterface;
class DatabaseStore extends LaravelDatabaseStore implements LockGenerator
{
/**
* The name of the cache locks table.
*
* @var string
*/
protected $lockTable;
/**
* A array representation of the lock lottery odds.
*
* @var array
*/
protected $lockLottery;
/**
* Create a new database store
*
* @param ConnectionInterface $connection
* @param $table
* @param string $prefix
* @param string $lockTable
* @param array $lockLottery
*/
public function __construct(
ConnectionInterface $connection,
$table,
$prefix = '',
$lockTable = 'cache_locks',
$lockLottery = [2, 100])
{
parent::__construct($connection, $table, $prefix);
$this->lockLottery = $lockLottery;
$this->lockTable = $lockTable;
}
/**
* Get a lock instance
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new DatabaseLock(
$this->connection,
$this->lockTable,
$this->prefix.$name,
$seconds,
$owner,
$this->lockLottery
);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
}
<?php
namespace App\Cache;
interface LockGenerator
{
/**
* Get a lock instance
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null);
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment