Decouple LocateVisitsCommand from AbstractLockedCommand

This commit is contained in:
Alejandro Celaya
2025-12-15 14:55:06 +01:00
parent 0f3f9d53c9
commit 96d122bcbf
6 changed files with 59 additions and 23 deletions

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db; namespace Shlinkio\Shlink\CLI\Command\Db;
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand; use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig; use Shlinkio\Shlink\CLI\Command\Util\LockConfig;
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface; use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockFactory;
@@ -30,8 +30,8 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand
$this->processRunner->run($output, $command); $this->processRunner->run($output, $command);
} }
protected function getLockConfig(): LockedCommandConfig protected function getLockConfig(): LockConfig
{ {
return LockedCommandConfig::blocking($this->getName() ?? static::class); return LockConfig::blocking($this->getName() ?? static::class);
} }
} }

View File

@@ -39,5 +39,5 @@ abstract class AbstractLockedCommand extends Command
abstract protected function lockedExecute(InputInterface $input, OutputInterface $output): int; abstract protected function lockedExecute(InputInterface $input, OutputInterface $output): int;
abstract protected function getLockConfig(): LockedCommandConfig; abstract protected function getLockConfig(): LockConfig;
} }

View File

@@ -6,6 +6,9 @@ namespace Shlinkio\Shlink\CLI\Command\Util;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\LockFactory;
use function sprintf;
class CommandUtils class CommandUtils
{ {
@@ -25,4 +28,31 @@ class CommandUtils
return $callback(); return $callback();
} }
/**
* Runs a callback with a lock, making sure the lock is released after running the callback, and the callback does
* not run if the lock is already acquired.
*
* @param callable(): int $callback
*/
public static function executeWithLock(
LockFactory $locker,
LockConfig $lockConfig,
SymfonyStyle $io,
callable $callback,
): int {
$lock = $locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking);
if (! $lock->acquire($lockConfig->isBlocking)) {
$io->writeln(
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
);
return Command::INVALID;
}
try {
return $callback();
} finally {
$lock->release();
}
}
} }

View File

@@ -4,24 +4,24 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util; namespace Shlinkio\Shlink\CLI\Command\Util;
final class LockedCommandConfig final readonly class LockConfig
{ {
public const float DEFAULT_TTL = 600.0; // 10 minutes public const float DEFAULT_TTL = 600.0; // 10 minutes
private function __construct( private function __construct(
public readonly string $lockName, public string $lockName,
public readonly bool $isBlocking, public bool $isBlocking,
public readonly float $ttl = self::DEFAULT_TTL, public float $ttl = self::DEFAULT_TTL,
) { ) {
} }
public static function blocking(string $lockName): self public static function blocking(string $lockName): self
{ {
return new self($lockName, true); return new self($lockName, isBlocking: true);
} }
public static function nonBlocking(string $lockName): self public static function nonBlocking(string $lockName): self
{ {
return new self($lockName, false); return new self($lockName, isBlocking: false);
} }
} }

View File

@@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit; namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand; use Shlinkio\Shlink\CLI\Command\Util\CommandUtils;
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig; use Shlinkio\Shlink\CLI\Command\Util\LockConfig;
use Shlinkio\Shlink\Common\Util\IpAddress; use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException; use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
use Shlinkio\Shlink\Core\Visit\Entity\Visit; use Shlinkio\Shlink\Core\Visit\Entity\Visit;
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocatorInterface;
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelperInterface; use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelperInterface;
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType; use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
use Shlinkio\Shlink\IpGeolocation\Model\Location; use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@@ -26,7 +27,7 @@ use Throwable;
use function sprintf; use function sprintf;
class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocationHelperInterface class LocateVisitsCommand extends Command implements VisitGeolocationHelperInterface
{ {
public const string NAME = 'visit:locate'; public const string NAME = 'visit:locate';
@@ -35,9 +36,9 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
public function __construct( public function __construct(
private readonly VisitLocatorInterface $visitLocator, private readonly VisitLocatorInterface $visitLocator,
private readonly VisitToLocationHelperInterface $visitToLocation, private readonly VisitToLocationHelperInterface $visitToLocation,
LockFactory $locker, private readonly LockFactory $locker,
) { ) {
parent::__construct($locker); parent::__construct();
} }
protected function configure(): void protected function configure(): void
@@ -97,7 +98,17 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
return $this->io->confirm('Do you want to proceed?', false); return $this->io->confirm('Do you want to proceed?', false);
} }
protected function lockedExecute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
{
return CommandUtils::executeWithLock(
$this->locker,
LockConfig::nonBlocking(self::NAME),
new SymfonyStyle($input, $output),
fn () => $this->runStuff($input),
);
}
private function runStuff(InputInterface $input): int
{ {
$retry = $input->getOption('retry'); $retry = $input->getOption('retry');
$all = $retry && $input->getOption('all'); $all = $retry && $input->getOption('all');
@@ -174,9 +185,4 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.'); throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
} }
} }
protected function getLockConfig(): LockedCommandConfig
{
return LockedCommandConfig::nonBlocking(self::NAME);
}
} }

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util; namespace Shlinkio\Shlink\CLI\Util;
use Closure; use Closure;
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig; use Shlinkio\Shlink\CLI\Command\Util\LockConfig;
use Symfony\Component\Console\Helper\DebugFormatterHelper; use Symfony\Component\Console\Helper\DebugFormatterHelper;
use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface;
@@ -24,7 +24,7 @@ class ProcessRunner implements ProcessRunnerInterface
{ {
$this->createProcess = $createProcess !== null $this->createProcess = $createProcess !== null
? $createProcess(...) ? $createProcess(...)
: static fn (array $cmd) => new Process($cmd, timeout: LockedCommandConfig::DEFAULT_TTL); : static fn (array $cmd) => new Process($cmd, timeout: LockConfig::DEFAULT_TTL);
} }
public function run(OutputInterface $output, array $cmd): void public function run(OutputInterface $output, array $cmd): void