From 457e056b6016f7ed2b5d84b20065abdc3611d41d Mon Sep 17 00:00:00 2001 From: Jason Varga Date: Wed, 4 Nov 2020 15:22:36 -0500 Subject: [PATCH] Stache Locks (#2794) --- composer.json | 1 + config/stache.php | 18 +++++++++++++ src/Http/Middleware/StacheLock.php | 40 ++++++++++++++++++++++++++++ src/Providers/AppServiceProvider.php | 1 + src/Stache/NullLockStore.php | 34 +++++++++++++++++++++++ src/Stache/ServiceProvider.php | 23 +++++++++++++++- src/Stache/Stache.php | 24 +++++++++++++++++ 7 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 src/Http/Middleware/StacheLock.php create mode 100644 src/Stache/NullLockStore.php diff --git a/composer.json b/composer.json index 8b087953f5..f334cc5c56 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "pixelfear/composer-dist-plugin": "^0.1.4", "spatie/blink": "^1.1.2", "statamic/stringy": "^3.1", + "symfony/lock": "^5.1", "symfony/var-exporter": "^4.3", "symfony/yaml": "^4.1 || ^5.1", "ueberdosis/html-to-prosemirror": "^1.0", diff --git a/config/stache.php b/config/stache.php index e8f11cc4f9..76d23ae01f 100644 --- a/config/stache.php +++ b/config/stache.php @@ -85,4 +85,22 @@ // ], + /* + |-------------------------------------------------------------------------- + | Locking + |-------------------------------------------------------------------------- + | + | In order to prevent concurrent requests from updating the Stache at + | the same and wasting resources, it will be "locked" so subsequent + | requests will have to wait until the first has been completed. + | + | https://statamic.dev/stache#locks + | + */ + + 'lock' => [ + 'enabled' => true, + 'timeout' => 30, + ], + ]; diff --git a/src/Http/Middleware/StacheLock.php b/src/Http/Middleware/StacheLock.php new file mode 100644 index 0000000000..4408f576fc --- /dev/null +++ b/src/Http/Middleware/StacheLock.php @@ -0,0 +1,40 @@ +acquire()) { + if (time() - $start >= config('statamic.stache.lock.timeout', 30)) { + return $this->outputRefreshResponse($request); + } + + sleep(1); + } + + $lock->release(); + + return $next($request); + } + + private function outputRefreshResponse($request) + { + $html = $request->ajax() || $request->wantsJson() + ? __('Service Unavailable') + : sprintf('', $request->getUri()); + + return response($html, 503, ['Retry-After' => 1]); + } +} diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index f296ea9525..4a130d32b1 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -125,6 +125,7 @@ public function register() protected function registerMiddlewareGroup() { $this->app->make(Router::class)->middlewareGroup('statamic.web', [ + \Statamic\Http\Middleware\StacheLock::class, \Statamic\Http\Middleware\Localize::class, \Statamic\StaticCaching\Middleware\Cache::class, ]); diff --git a/src/Stache/NullLockStore.php b/src/Stache/NullLockStore.php new file mode 100644 index 0000000000..e8122e9254 --- /dev/null +++ b/src/Stache/NullLockStore.php @@ -0,0 +1,34 @@ +app->singleton(Stache::class, function () { - return new Stache; + return (new Stache)->setLockFactory($this->locks()); }); $this->app->alias(Stache::class, 'stache'); @@ -35,4 +38,22 @@ public function boot() return app($config['class'])->directory($config['directory']); })->all()); } + + private function locks() + { + if (config('statamic.stache.lock.enabled', true)) { + $store = $this->createFileLockStore(); + } else { + $store = new NullLockStore; + } + + return new LockFactory($store); + } + + private function createFileLockStore() + { + File::makeDirectory($dir = storage_path('statamic/stache-locks')); + + return new FlockStore($dir); + } } diff --git a/src/Stache/Stache.php b/src/Stache/Stache.php index a8af21cadb..c0df7f0a05 100644 --- a/src/Stache/Stache.php +++ b/src/Stache/Stache.php @@ -8,6 +8,8 @@ use Statamic\Facades\File; use Statamic\Stache\Stores\Store; use Statamic\Support\Str; +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\LockInterface; use Wilderborn\Partyline\Facade as Partyline; class Stache @@ -16,6 +18,8 @@ class Stache protected $stores; protected $startTime; protected $updateIndexes = true; + protected $lockFactory; + protected $locks = []; public function __construct() { @@ -95,11 +99,15 @@ public function warm() { Partyline::comment('Warming Stache...'); + $lock = tap($this->lock('stache-warming'))->acquire(true); + $this->startTimer(); $this->stores()->each->warm(); $this->stopTimer(); + + $lock->release(); } public function instance() @@ -173,4 +181,20 @@ public function shouldUpdateIndexes() { return $this->updateIndexes; } + + public function setLockFactory(LockFactory $lockFactory) + { + $this->lockFactory = $lockFactory; + + return $this; + } + + public function lock($name): LockInterface + { + if (isset($this->locks[$name])) { + return $this->locks[$name]; + } + + return $this->locks[$name] = $this->lockFactory->createLock($name); + } }