-
Notifications
You must be signed in to change notification settings - Fork 1
feature/remove-migration-states #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b663b3c
f37170d
95c06d0
6e64b17
7d477c8
874a6dc
9f51c0d
ce2f2ab
1d2a576
54208f0
5848bd7
ffd0f02
513dd28
31ec1f8
70a6f0a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| name: Tests | ||
|
|
||
| on: [pull_request, workflow_dispatch] | ||
|
|
||
| jobs: | ||
| test: | ||
| # not self-hosted, because it's a public repo | ||
| runs-on: ubuntu-latest | ||
|
|
||
| # we want to run it on combination of PHP 8.1+ and Laravel 9.1+ | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| php: ['8.1'] | ||
| laravel: ['^9.1'] | ||
|
|
||
| steps: | ||
| - name: Checkout the repo | ||
| uses: actions/checkout@v2 | ||
|
|
||
| - name: Setup PHP | ||
| uses: shivammathur/setup-php@v2 | ||
| with: | ||
| php-version: ${{ matrix.php }} | ||
| extensions: zip, gd, sqlite, json, gmp, bcmath | ||
| coverage: none | ||
|
|
||
| - name: Ensure we use specific version of Laravel, and install other dependencies | ||
| env: | ||
| LARAVEL_VERSION: ${{ matrix.laravel }} | ||
| run: composer require laravel/framework $LARAVEL_VERSION --no-interaction --no-scripts --prefer-dist | ||
|
|
||
| - name: Execute tests | ||
| run: vendor/bin/phpunit |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,28 @@ | ||
| Laravel Mutex Migrations | ||
| ======================== | ||
| Mutex Migrations for Laravel | ||
| ============================ | ||
|
|
||
| Run mutually exclusive migrations from more than one server at a time. | ||
|
|
||
| TODO: | ||
| Using Laravel's functionality for [Atomic Locks](https://laravel.com/docs/9.x/cache#atomic-locks), this package extends the built-in `MigrateCommand` class to allow migrations to be run safely when there is the chance that they may be run concurrently against the same database. | ||
|
|
||
| ## Installation | ||
|
|
||
| Install the package with: | ||
|
|
||
| `composer require netsells/laravel-mutex-migrations` | ||
|
|
||
| Optionally publish the package config file: | ||
|
|
||
| `php artisan vendor:publish --tag=mutex-migrations-config` | ||
|
|
||
| ## Usage | ||
|
|
||
| Before a mutex migration can be run using the default `database` store, the store's `cache_locks` table **must** already have been created by running `php artisan cache:table` followed by a standard migration - i.e. `php artisan migrate`. Once this table exists migrations can be run safely as follows: | ||
|
|
||
| `php artisan migrate --mutex` | ||
|
|
||
| If two or more migrations happen to run concurrently, the first to acquire a lock will block the next one from running until it has finished, or until the lock times out - after 60 seconds, by default. | ||
|
|
||
| ## Testing | ||
|
|
||
| `./vendor/bin/phpunit` | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,58 +2,56 @@ | |
|
|
||
| namespace Netsells\LaravelMutexMigrations\Mutex; | ||
|
|
||
| use Illuminate\Contracts\Cache\Lock; | ||
| use Illuminate\Contracts\Cache\LockProvider; | ||
| use Illuminate\Contracts\Cache\Repository; | ||
| use Illuminate\Database\QueryException; | ||
| use Illuminate\Support\Str; | ||
|
|
||
| class MutexRelay implements MutexRelayInterface | ||
| { | ||
| private string $owner; | ||
| public const DEFAULT_LOCK_TABLE = 'cache_locks'; | ||
|
|
||
| public function __construct(private readonly MutexQueue $queue) | ||
| { | ||
| $this->owner = Str::random(); | ||
| public const KEY = 'laravel-mutex-migrations'; | ||
|
|
||
| private ?Lock $lock = null; | ||
|
|
||
| public function __construct( | ||
| private readonly Repository $cache, | ||
| private readonly int $lockDurationSeconds = 60, | ||
| private readonly string $lockTable = self::DEFAULT_LOCK_TABLE, | ||
| ) { | ||
| // | ||
| } | ||
|
|
||
| public function acquireLock(array $meta = [], callable $feedback = null): bool | ||
| public function acquireLock(): bool | ||
| { | ||
| try { | ||
| return $this->queue->push($this->owner, $meta); | ||
| return $this->getLock()->block($this->lockDurationSeconds); | ||
| } catch (\Throwable $th) { | ||
| if ($this->isCacheTableNotFoundException($th)) { | ||
| throw new DatabaseCacheTableNotFoundException(); | ||
| } | ||
|
|
||
| throw $th; | ||
| } finally { | ||
| // after pushing an item to the queue, we'll have a maximum of the | ||
| // configured TTL value - 60 seconds by default - to acquire a lock | ||
| // before the queued items expire | ||
| while (! isset($th) && ! $this->queue->isFirst($this->owner)) { | ||
| if (! $this->queue->contains($this->owner)) { | ||
| throw new MutexRelayTimeoutException($this->owner); | ||
| } | ||
|
|
||
| if (is_callable($feedback)) { | ||
| $feedback(); | ||
| }; | ||
|
|
||
| $wait = $this->backOff($wait ?? 1); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public function releaseLock(): bool | ||
| { | ||
| return $this->queue->pull($this->owner); | ||
| return $this->lock?->release() ?? false; | ||
| } | ||
|
|
||
| public function hasOwnerWithMeta(array $meta): bool | ||
| private function getLock(): Lock | ||
| { | ||
| if ($this->queue->isEmpty()) { | ||
| return false; | ||
| if ($this->lock) { | ||
| return $this->lock; | ||
| } | ||
|
|
||
| return $this->queue->contains(fn ($item) => $item === $meta); | ||
| /** @var LockProvider $provider */ | ||
| $provider = $this->cache->getStore(); | ||
|
|
||
| return $this->lock = $provider->lock(self::KEY . '.lock'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a big deal, because in the current form this scenario won't happen, however normally I think the first thing a method like that should do is check if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 100% - I'd definitely code it like that ordinarily and think I didn't for the precise reason that it seemed like it wouldn't occur. Anyway, I added an early return if the instance already exists. |
||
| } | ||
|
|
||
| private function isCacheTableNotFoundException(\Throwable $th): bool | ||
|
|
@@ -62,13 +60,6 @@ private function isCacheTableNotFoundException(\Throwable $th): bool | |
| return false; | ||
| } | ||
|
|
||
| return Str::contains($th->getMessage(), ['Base table or view new found', 'cache'], true); | ||
| } | ||
|
|
||
| private function backOff(int $wait): int | ||
| { | ||
| sleep($wait++); | ||
|
|
||
| return $wait; | ||
| return $th->getCode() === '42S02' && Str::contains($th->getMessage(), $this->lockTable); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.