Skip to content
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

[9.x] Make migrate command isolated #44743

Merged
merged 12 commits into from
Oct 31, 2022
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
olivernybroe marked this conversation as resolved.
Show resolved Hide resolved
*/
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)
olivernybroe marked this conversation as resolved.
Show resolved Hide resolved
{
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