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

Add command to ping the database #3697

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 82 additions & 0 deletions lib/Doctrine/DBAL/Tools/Console/Command/PingCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace Doctrine\DBAL\Tools\Console\Command;

use Doctrine\DBAL\Connection;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function is_numeric;
use function sleep;
use function sprintf;

class PingCommand extends Command
greg0ire marked this conversation as resolved.
Show resolved Hide resolved
{
protected function configure()
{
$this
->setName('dbal:ping')
->setDescription('Check db is available')
mcfedr marked this conversation as resolved.
Show resolved Hide resolved
->addOption('limit', null, InputOption::VALUE_REQUIRED, 'Max number of pings to try', '1')
->addOption('sleep', null, InputOption::VALUE_REQUIRED, 'Length of time (seconds) to sleep between pings', '1')
->setHelp(<<<EOT
Connects to the database to check if it is accessible.

The exit code will be non-zero when the connection fails.
EOT
);
}

protected function execute(InputInterface $input, OutputInterface $output) : int
{
$limit = $input->getOption('limit');
if (! is_numeric($limit) || $limit < 0) {
throw new RuntimeException('Option "limit" must contain a positive integer value');
}
$sleep = $input->getOption('sleep');
if (! is_numeric($sleep) || $sleep < 0) {
throw new RuntimeException('Option "sleep" must contain a positive integer value');
}

return $this->waitForPing($this->getHelper('db')->getConnection(), (int) $limit, (int) $sleep, $output);
mcfedr marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @return int > 0 for error
*/
private function waitForPing(Connection $conn, int $limit, int $sleep, OutputInterface $output) : int
{
while (true) {
$last = $this->ping($conn, $output);
if ($last === 0 || --$limit <= 0) {
break;
}
sleep($sleep);
}

return $last;
}

/**
* @return int > 0 for error
*/
private function ping(Connection $conn, OutputInterface $output) : int
{
try {
if ($conn->ping()) {
return 0;
}

$output->writeln('Ping failed');

return 1;
} catch (Throwable $e) {
$output->writeln(sprintf('Ping failed: <error>%s</error>', $e->getMessage()));

return 2;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What difference does it make for the caller? The ping either succeeded or didn't. Note that in DBAL v3, ping will throw an exception on failure, so it's not clear how this would have to be reimplemented w/o breaking the existing behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably makes little difference to the caller, but as there are two different failures it doesnt do any hard to be more informative - note that this becomes the exit code from the process

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've changed the text to just be 'ping failed', so it won't be misleading when DBAL 3 throws on different failure types - anyway the caught exception will say its a connection error or something else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't understand the meaning of the two different return codes and how they will be supported in 3.0.

}
}
}
2 changes: 2 additions & 0 deletions lib/Doctrine/DBAL/Tools/Console/ConsoleRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Tools\Console\Command\ImportCommand;
use Doctrine\DBAL\Tools\Console\Command\PingCommand;
use Doctrine\DBAL\Tools\Console\Command\ReservedWordsCommand;
use Doctrine\DBAL\Tools\Console\Command\RunSqlCommand;
use Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper;
Expand Down Expand Up @@ -58,6 +59,7 @@ public static function addCommands(Application $cli)
new RunSqlCommand(),
new ImportCommand(),
new ReservedWordsCommand(),
new PingCommand(),
]);
}

Expand Down
147 changes: 147 additions & 0 deletions tests/Doctrine/Tests/DBAL/Tools/Console/Command/PingCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php

namespace Doctrine\Tests\DBAL\Tools\Console\Command;

use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Tools\Console\Command\PingCommand;
use Doctrine\DBAL\Tools\Console\ConsoleRunner;
use PDOException;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;

class PingCommandTest extends TestCase
{
/** @var CommandTester */
private $commandTester;
/** @var PingCommand */
private $command;

/** @var Connection */
private $connectionMock;

protected function setUp() : void
{
$application = new Application();
$application->add(new PingCommand());

$this->command = $application->find('dbal:ping');
$this->commandTester = new CommandTester($this->command);

$this->connectionMock = $this->createMock(Connection::class);

$helperSet = ConsoleRunner::createHelperSet($this->connectionMock);
$this->command->setHelperSet($helperSet);
}

public function testConnectionWorking() : void
{
$this->connectionMock
->expects($this->once())
->method('ping')
->willReturn(true);

$this->commandTester->execute([]);

self::assertSame(0, $this->commandTester->getStatusCode());
}

public function testConnectionNotWorking() : void
{
$this->connectionMock
->expects($this->once())
->method('ping')
->willReturn(false);

$this->commandTester->execute([]);

self::assertSame(1, $this->commandTester->getStatusCode());
self::assertSame("Ping failed\n", $this->commandTester->getDisplay(true));
}

public function testConnectionErrors() : void
{
$this->connectionMock
->expects($this->once())
->method('ping')
->willThrowException(new PDOException('Connection failed'));

$this->commandTester->execute([]);

self::assertSame(2, $this->commandTester->getStatusCode());
self::assertSame("Ping failed: Connection failed\n", $this->commandTester->getDisplay(true));
}

public function testConnectionNotWorkingLoop() : void
{
$this->connectionMock
->expects($this->exactly(3))
->method('ping')
->willReturn(false);

$this->commandTester->execute([
'--limit' => '3',
'--sleep' => '0',
]);

self::assertSame(1, $this->commandTester->getStatusCode());
self::assertSame("Ping failed\nPing failed\nPing failed\n", $this->commandTester->getDisplay(true));
}

public function testConnectionStartsWorking() : void
{
$this->connectionMock
->expects($this->exactly(3))
->method('ping')
->willReturnOnConsecutiveCalls(false, false, true);

$this->commandTester->execute([
'--limit' => '5',
'--sleep' => '0',
]);

self::assertSame(0, $this->commandTester->getStatusCode());
self::assertSame("Ping failed\nPing failed\n", $this->commandTester->getDisplay(true));
}

public function testInvalidLimit() : void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Option "limit" must contain a positive integer value');

$this->commandTester->execute(['--limit' => '-1']);

self::assertSame(1, $this->commandTester->getStatusCode());
}

public function testInvalidLimitNum() : void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Option "limit" must contain a positive integer value');

$this->commandTester->execute(['--limit' => 'foo']);

self::assertSame(1, $this->commandTester->getStatusCode());
}

public function testInvalidSleep() : void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Option "sleep" must contain a positive integer value');

$this->commandTester->execute(['--sleep' => '-1']);

self::assertSame(1, $this->commandTester->getStatusCode());
}

public function testInvalidSleepNum() : void
{
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Option "sleep" must contain a positive integer value');

$this->commandTester->execute(['--sleep' => 'foo']);

self::assertSame(1, $this->commandTester->getStatusCode());
}
}