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;
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 Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Lock\LockFactory;
@@ -30,8 +30,8 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand
$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 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\Style\SymfonyStyle;
use Symfony\Component\Lock\LockFactory;
use function sprintf;
class CommandUtils
{
@@ -25,4 +28,31 @@ class CommandUtils
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;
final class LockedCommandConfig
final readonly class LockConfig
{
public const float DEFAULT_TTL = 600.0; // 10 minutes
private function __construct(
public readonly string $lockName,
public readonly bool $isBlocking,
public readonly float $ttl = self::DEFAULT_TTL,
public string $lockName,
public bool $isBlocking,
public float $ttl = self::DEFAULT_TTL,
) {
}
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
{
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;
use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
use Shlinkio\Shlink\CLI\Command\Util\CommandUtils;
use Shlinkio\Shlink\CLI\Command\Util\LockConfig;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
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\Model\UnlocatableIpType;
use Shlinkio\Shlink\IpGeolocation\Model\Location;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputInterface;
@@ -26,7 +27,7 @@ use Throwable;
use function sprintf;
class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocationHelperInterface
class LocateVisitsCommand extends Command implements VisitGeolocationHelperInterface
{
public const string NAME = 'visit:locate';
@@ -35,9 +36,9 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
public function __construct(
private readonly VisitLocatorInterface $visitLocator,
private readonly VisitToLocationHelperInterface $visitToLocation,
LockFactory $locker,
private readonly LockFactory $locker,
) {
parent::__construct($locker);
parent::__construct();
}
protected function configure(): void
@@ -97,7 +98,17 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
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');
$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.');
}
}
protected function getLockConfig(): LockedCommandConfig
{
return LockedCommandConfig::nonBlocking(self::NAME);
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Util;
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\ProcessHelper;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
@@ -24,7 +24,7 @@ class ProcessRunner implements ProcessRunnerInterface
{
$this->createProcess = $createProcess !== null
? $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