Skip to content

Commit

Permalink
Make migrate command isolated (#44743)
Browse files Browse the repository at this point in the history
* Add Isolated interface to `migrate` command

* Add CommandMutex with cache implementation

* Remove typehints in favor of docblocks

* Apply StyleCI

* Add support for releasing lock again

* fix db migrate command tests

* cleanup

* Add `--isolated` flag to command

* rename file. formatting

* allow exit code

* fix option

* fix order

Co-authored-by: Taylor Otwell <taylor@laravel.com>
  • Loading branch information
Oliver Nybroe and taylorotwell authored Oct 31, 2022
1 parent 4d126e8 commit c2798fd
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 3 deletions.
98 changes: 98 additions & 0 deletions src/Illuminate/Console/CacheCommandMutex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

namespace Illuminate\Console;

use Carbon\CarbonInterval;
use Illuminate\Contracts\Cache\Factory as Cache;

class CacheCommandMutex implements CommandMutex
{
/**
* The cache factory implementation.
*
* @var \Illuminate\Contracts\Cache\Factory
*/
public $cache;

/**
* The cache store that should be used.
*
* @var string|null
*/
public $store = null;

/**
* Create a new command mutex.
*
* @param \Illuminate\Contracts\Cache\Factory $cache
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}

/**
* Attempt to obtain a command mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function create($command)
{
return $this->cache->store($this->store)->add(
$this->commandMutexName($command),
true,
method_exists($command, 'isolationExpiresAt')
? $command->isolationExpiresAt()
: CarbonInterval::hour(),
);
}

/**
* Determine if a command mutex exists for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function exists($command)
{
return $this->cache->store($this->store)->has(
$this->commandMutexName($command)
);
}

/**
* Release the mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function forget($command)
{
return $this->cache->store($this->store)->forget(
$this->commandMutexName($command)
);
}

/**
* @param \Illuminate\Console\Command $command
* @return string
*/
protected function commandMutexName($command)
{
return 'framework'.DIRECTORY_SEPARATOR.'command-'.$command->getName();
}

/**
* Specify the cache store that should be used.
*
* @param string|null $store
* @return $this
*/
public function useStore($store)
{
$this->store = $store;

return $this;
}
}
53 changes: 52 additions & 1 deletion src/Illuminate/Console/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
namespace Illuminate\Console;

use Illuminate\Console\View\Components\Factory;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class Command extends SymfonyCommand
Expand Down Expand Up @@ -86,6 +88,10 @@ public function __construct()
if (! isset($this->signature)) {
$this->specifyParameters();
}

if ($this instanceof Isolatable) {
$this->configureIsolation();
}
}

/**
Expand All @@ -106,6 +112,22 @@ protected function configureUsingFluentDefinition()
$this->getDefinition()->addOptions($options);
}

/**
* Configure the console command for isolation.
*
* @return void
*/
protected function configureIsolation()
{
$this->getDefinition()->addOption(new InputOption(
'isolated',
null,
InputOption::VALUE_OPTIONAL,
'Do not run the command if another instance of the command is already running',
false
));
}

/**
* Run the console command.
*
Expand Down Expand Up @@ -139,9 +161,38 @@ public function run(InputInterface $input, OutputInterface $output): int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($this instanceof Isolatable && $this->option('isolated') !== false &&
! $this->commandIsolationMutex()->create($this)) {
$this->comment(sprintf(
'The [%s] command is already running.', $this->getName()
));

return (int) (is_numeric($this->option('isolated'))
? $this->option('isolated')
: self::SUCCESS);
}

$method = method_exists($this, 'handle') ? 'handle' : '__invoke';

return (int) $this->laravel->call([$this, $method]);
try {
return (int) $this->laravel->call([$this, $method]);
} finally {
if ($this instanceof Isolatable && $this->option('isolated') !== false) {
$this->commandIsolationMutex()->forget($this);
}
}
}

/**
* Get a command isolation mutex instance for the command.
*
* @return \Illuminate\Console\CommandMutex
*/
protected function commandIsolationMutex()
{
return $this->laravel->bound(CommandMutex::class)
? $this->laravel->make(CommandMutex::class)
: $this->laravel->make(CacheCommandMutex::class);
}

/**
Expand Down
30 changes: 30 additions & 0 deletions src/Illuminate/Console/CommandMutex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Illuminate\Console;

interface CommandMutex
{
/**
* Attempt to obtain a command mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function create($command);

/**
* Determine if a command mutex exists for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function exists($command);

/**
* Release the mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function forget($command);
}
8 changes: 8 additions & 0 deletions src/Illuminate/Contracts/Console/Isolatable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Illuminate\Contracts\Console;

interface Isolatable
{
//
}
4 changes: 2 additions & 2 deletions src/Illuminate/Database/Console/Migrations/MigrateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Illuminate\Database\Console\Migrations;

use Illuminate\Console\ConfirmableTrait;
use Illuminate\Console\View\Components\Task;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Events\SchemaLoaded;
use Illuminate\Database\Migrations\Migrator;
Expand All @@ -12,7 +12,7 @@
use PDOException;
use Throwable;

class MigrateCommand extends BaseCommand
class MigrateCommand extends BaseCommand implements Isolatable
{
use ConfirmableTrait;

Expand Down
78 changes: 78 additions & 0 deletions tests/Console/CacheCommandMutexTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace Illuminate\Tests\Console;

use Illuminate\Console\CacheCommandMutex;
use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Factory;
use Illuminate\Contracts\Cache\Repository;
use Mockery as m;
use PHPUnit\Framework\TestCase;

class CacheCommandMutexTest extends TestCase
{
/**
* @var \Illuminate\Console\CacheCommandMutex
*/
protected $mutex;

/**
* @var \Illuminate\Console\Command
*/
protected $command;

/**
* @var \Illuminate\Contracts\Cache\Factory
*/
protected $cacheFactory;

/**
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cacheRepository;

protected function setUp(): void
{
$this->cacheFactory = m::mock(Factory::class);
$this->cacheRepository = m::mock(Repository::class);
$this->cacheFactory->shouldReceive('store')->andReturn($this->cacheRepository);
$this->mutex = new CacheCommandMutex($this->cacheFactory);
$this->command = new class extends Command
{
protected $name = 'command-name';
};
}

public function testCanCreateMutex()
{
$this->cacheRepository->shouldReceive('add')
->andReturn(true)
->once();
$actual = $this->mutex->create($this->command);

$this->assertTrue($actual);
}

public function testCannotCreateMutexIfAlreadyExist()
{
$this->cacheRepository->shouldReceive('add')
->andReturn(false)
->once();
$actual = $this->mutex->create($this->command);

$this->assertFalse($actual);
}

public function testCanCreateMutexWithCustomConnection()
{
$this->cacheRepository->shouldReceive('getStore')
->with('test')
->andReturn($this->cacheRepository);
$this->cacheRepository->shouldReceive('add')
->andReturn(false)
->once();
$this->mutex->useStore('test');

$this->mutex->create($this->command);
}
}
Loading

0 comments on commit c2798fd

Please sign in to comment.