Skip to content

Commit

Permalink
Merge pull request #25 from mathroc/feature/postgresql
Browse files Browse the repository at this point in the history
Add PostgreSQLRetryStrategy
  • Loading branch information
mathroc committed Aug 31, 2015
2 parents 4f9ebdc + c4b52f7 commit c8f03f2
Show file tree
Hide file tree
Showing 18 changed files with 367 additions and 154 deletions.
13 changes: 11 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ php:
- "5.6"
- "7.0"

addons:
postgresql: "9.4"

before_script:
- composer install
- psql -c "create role admin login createdb password 'adminpassword' superuser ;" -U postgres
- psql -c "create role behat login createdb password 'behatpassword';" -U postgres
- psql -c 'create database behat OWNER=behat;' -U postgres
- psql -c 'create database dblinker_test OWNER=behat;' -U postgres
- psql -c 'create database forbidden_db;'
- mysql -e 'create database dblinker_test;'
- mysql -e 'create database forbidden_db;'
- mysql -e 'CREATE USER "behat"@"localhost" IDENTIFIED BY "behatpassword";'
Expand All @@ -18,8 +26,9 @@ script:
- vendor/bin/behat -s $BEHAT_DB_DRIVER --tags ~@skip-travis

env:
- BEHAT_DB_DRIVER=mysqli DBLINKER_DB_1_ENV_MYSQL_DATABASE=dblinker_test DBLINKER_DB_1_PORT_3306_TCP_ADDR=127.0.0.1 DBLINKER_DB_1_ENV_MYSQL_USER=behat DBLINKER_DB_1_ENV_MYSQL_PASSWORD=behatpassword DBLINKER_DB_1_ENV_MYSQL_ROOT_PASSWORD=
- BEHAT_DB_DRIVER=pdo_mysql DBLINKER_DB_1_ENV_MYSQL_DATABASE=dblinker_test DBLINKER_DB_1_PORT_3306_TCP_ADDR=127.0.0.1 DBLINKER_DB_1_ENV_MYSQL_USER=behat DBLINKER_DB_1_ENV_MYSQL_PASSWORD=behatpassword DBLINKER_DB_1_ENV_MYSQL_ROOT_PASSWORD=
- BEHAT_DB_DRIVER=mysqli DBLINKER_MYSQL_1_ENV_MYSQL_DATABASE=dblinker_test DBLINKER_MYSQL_1_PORT_3306_TCP_ADDR=127.0.0.1 DBLINKER_MYSQL_1_ENV_MYSQL_USER=behat DBLINKER_MYSQL_1_ENV_MYSQL_PASSWORD=behatpassword DBLINKER_MYSQL_1_ENV_MYSQL_ROOT_PASSWORD=
- BEHAT_DB_DRIVER=pdo_mysql DBLINKER_MYSQL_1_ENV_MYSQL_DATABASE=dblinker_test DBLINKER_MYSQL_1_PORT_3306_TCP_ADDR=127.0.0.1 DBLINKER_MYSQL_1_ENV_MYSQL_USER=behat DBLINKER_MYSQL_1_ENV_MYSQL_PASSWORD=behatpassword DBLINKER_MYSQL_1_ENV_MYSQL_ROOT_PASSWORD=
- BEHAT_DB_DRIVER=pdo_pgsql DBLINKER_POSTGRESQL_1_ENV_POSTGRES_DATABASE=dblinker_test DBLINKER_POSTGRESQL_1_PORT_5432_TCP_ADDR=127.0.0.1 DBLINKER_POSTGRESQL_1_ENV_POSTGRES_USER=behat DBLINKER_POSTGRESQL_1_ENV_POSTGRES_PASSWORD=behatpassword DBLINKER_POSTGRES_1_ENV_POSTGRES_ROOT_USER=admin DBLINKER_POSTGRES_1_ENV_POSTGRES_ROOT_PASSWORD=adminpassword

matrix:
allow_failures:
Expand Down
4 changes: 4 additions & 0 deletions behat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ default:
contexts: [ PdoMysqlContext ]
filters:
tags: ~@skip-pdo-mysql
pdo_pgsql:
contexts: [ PdoPgsqlContext ]
filters:
tags: ~@skip-pdo-pgsql
4 changes: 2 additions & 2 deletions config/docker/php/behat/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
FROM php:5.6

RUN apt-get update
RUN apt-get install -y git file
RUN apt-get install -y git file libpq-dev
RUN cd /tmp/ && git clone git://github.com/xdebug/xdebug.git
RUN cd /tmp/xdebug && git checkout XDEBUG_2_3_2 && phpize && ./configure --enable-xdebug && make && cp modules/xdebug.so /usr/local/lib/php/extensions/

RUN docker-php-ext-install mbstring
RUN docker-php-ext-install mysql pdo_mysql mysqli
RUN docker-php-ext-install pdo_mysql pdo_pgsql mysqli
RUN docker-php-ext-install pcntl

ENTRYPOINT ["php", "/scripts/vendor/bin/behat"]
Expand Down
12 changes: 10 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,19 @@ behat:
volumes_from:
- vendor
links:
- db
db:
- mysql
- postgresql
mysql:
image: mysql:5.7
environment:
MYSQL_DATABASE: db
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: rootpassword
postgresql:
image: postgres:9.4
environment:
POSTGRES_DATABASE: db
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_ROOT_PASSWORD: password
153 changes: 38 additions & 115 deletions features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php

use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\DriverManager;

trait FeatureContext
Expand All @@ -10,22 +11,7 @@ trait FeatureContext
/**
* @BeforeScenario
*/
public function clearConnections()
{
$this->connections = [
'@master' => [
'params' => $this->masterParams(),
'instance' => null,
'last-result' => null,
'last-error' => null,
]
];
$connection = $this->rootConnection();
$connection->exec("SET GLOBAL MAX_CONNECTIONS = 50");
$connection->close();
$connection = null;
gc_collect_cycles();
}
abstract public function clearConnections();

/**
* @BeforeScenario
Expand All @@ -50,16 +36,7 @@ public function assertNoActiveConnection()
assert($n === 0, "There is $n active connection(s) on the test server");
}

private function activeConnectionsCount()
{
$connection = $this->rootConnection();
gc_collect_cycles();
$n = (int)$connection->fetchAll("show status like 'Threads_connected'")[0]['Value'];
$connection->close();
$connection = null;
gc_collect_cycles();
return $n - 1;
}
abstract protected function activeConnectionsCount();

private function rootConnection()
{
Expand Down Expand Up @@ -87,15 +64,7 @@ public function closeConnections()
* @Given the server accept :n more connection
* @Given the server accept :n more connections
*/
public function theServerAcceptMoreConnections($n)
{
$n += $this->activeConnectionsCount();
$connection = $this->rootConnection();
$connection->exec("SET GLOBAL MAX_CONNECTIONS = $n");
$connection->close();
$connection = null;
gc_collect_cycles();
}
abstract public function theServerAcceptMoreConnections($n);

/**
* @Given a master\/slaves connection :connectionName with :slaveCount slaves
Expand Down Expand Up @@ -148,7 +117,7 @@ public function aRetryMasterSlavesConnectionWithSlavesLimitedToRetriesWithuserna
'slaves' => $slaves,
'driverClass' => 'Ez\DbLinker\Driver\MysqlMasterSlavesDriver',
],
'retryStrategy' => new MysqlRetryStrategy($n),
'retryStrategy' => $this->retryStrategy($n),
];
$this->connections[$connectionName] = [
'params' => $params,
Expand Down Expand Up @@ -266,7 +235,7 @@ public function aRetryConnectionLimitedToRetryWithusername($connectionName, $n,
$params = [
'driverClass' => 'Ez\DbLinker\Driver\MysqlRetryDriver',
'connectionParams' => $this->masterParams($username),
'retryStrategy' => new MysqlRetryStrategy($n),
'retryStrategy' => $this->retryStrategy($n),
];
$this->connections[$connectionName] = [
'params' => $params,
Expand Down Expand Up @@ -299,7 +268,7 @@ public function aRetryMasterSlavesConnectionWithSlavesLimitedToRetryWithDbAndUse
'slaves' => $slaves,
'driverClass' => 'Ez\DbLinker\Driver\MysqlMasterSlavesDriver',
],
'retryStrategy' => new MysqlRetryStrategy($n),
'retryStrategy' => $this->retryStrategy($n),
];
$this->connections[$connectionName] = [
'params' => $params,
Expand All @@ -319,7 +288,7 @@ public function aRetryConnectionLimitedToRetryWithDbAndUsername($connectionName,
$params = [
'driverClass' => 'Ez\DbLinker\Driver\MysqlRetryDriver',
'connectionParams' => $master,
'retryStrategy' => new MysqlRetryStrategy($n),
'retryStrategy' => $this->retryStrategy($n),
];
$params['connectionParams']['dbname'] = $db;
$this->connections[$connectionName] = [
Expand All @@ -332,9 +301,9 @@ public function aRetryConnectionLimitedToRetryWithDbAndUsername($connectionName,


/**
* @Given MySQL has Gone Away on :connectionName
* @Given database has Gone Away on :connectionName
*/
public function mysqlHasGoneAwayOn($connectionName)
public function databaseHasGoneAwayOn($connectionName)
{
$this->getConnection($connectionName)->exec('SET SESSION WAIT_TIMEOUT=1');
usleep(1100000);
Expand Down Expand Up @@ -398,27 +367,6 @@ public function theLastQuerySucceededOn($connectionName)
}
}

/**
* @Then the last error code should be :expectedErrorCode on :connectionName
*/
public function theLastExpectedErrorCodeShouldBeOn($expectedErrorCode, $connectionName)
{
$errorCodeAssertFailureMessage = "No error found, error code $expectedErrorCode expected";
$exception = $this->connections[$connectionName]['last-error'];
if ($exception === null) {
$exception = $this->getWrappedConnection($connectionName)->retryStrategy()->lastError();
}
$errorCode = null;
while ($exception !== null && !($exception instanceof \Doctrine\DBAL\Exception\DriverException)) {
$exception = $exception->getPrevious();
}
if ($exception !== null) {
$errorCode = $exception->getErrorCode();
$errorCodeAssertFailureMessage = "Error code is $errorCode, error code $expectedErrorCode expected";
}
assert($errorCode === (int) $expectedErrorCode, $errorCodeAssertFailureMessage);
}

/**
* @Then the last query failed on :connectionName
*/
Expand Down Expand Up @@ -503,21 +451,34 @@ public function theLastStatementSucceeded()
public function theLastStatementExpectedErrorCodeShouldBe($expectedErrorCode)
{
$errorCodeAssertFailureMessage = "No error found, error code $expectedErrorCode expected";
$exception = $this->lastStatementError;
if ($exception === null) {
$exception = $this->statement->connection->getWrappedConnection()->retryStrategy()->lastError();
}
$errorCode = null;
while ($exception !== null && !($exception instanceof \Doctrine\DBAL\Exception\DriverException)) {
$exception = $exception->getPrevious();
$errorCode = $this->errorCode(
$this->lastStatementError ?:
$this->statement->connection->getWrappedConnection()->retryStrategy()->lastError()
);
if ($errorCode !== null) {
$errorCodeAssertFailureMessage = "Error code is $errorCode, error code $expectedErrorCode expected";
}
if ($exception !== null) {
$errorCode = $exception->getErrorCode();
assert($errorCode === (int) $expectedErrorCode, $errorCodeAssertFailureMessage);
}

/**
* @Then the last error code should be :expectedErrorCode on :connectionName
*/
public function theLastExpectedErrorCodeShouldBeOn($expectedErrorCode, $connectionName)
{
$errorCodeAssertFailureMessage = "No error found, error code $expectedErrorCode expected";
$errorCode = $this->errorCode(
$this->connections[$connectionName]['last-error'] ?:
$this->getWrappedConnection($connectionName)->retryStrategy()->lastError()
);
if ($errorCode !== null) {
$errorCodeAssertFailureMessage = "Error code is $errorCode, error code $expectedErrorCode expected";
}
assert($errorCode === (int) $expectedErrorCode, $errorCodeAssertFailureMessage);
}

abstract protected function errorCode($exception);

private function getWrappedConnection($connectionName)
{
return $this->getConnection($connectionName)->getWrappedConnection();
Expand All @@ -535,22 +496,7 @@ private function getConnection($connectionName)

protected abstract function params(Array $params);

private function masterParams($username = null, $password = '') {
$params = [
'host' => getenv('DBLINKER_DB_1_PORT_3306_TCP_ADDR'),
'user' => getenv('DBLINKER_DB_1_ENV_MYSQL_USER'),
'password' => getenv('DBLINKER_DB_1_ENV_MYSQL_PASSWORD'),
'dbname' => getenv('DBLINKER_DB_1_ENV_MYSQL_DATABASE'),
];
if ($username !== null) {
$params['user'] = $username;
if ($username === 'root') {
$password = getenv('DBLINKER_DB_1_ENV_MYSQL_ROOT_PASSWORD');
}
$params['password'] = $password;
}
return $this->params($params);
}
abstract protected function masterParams($username = null, $password = '');

/**
* @Given table :tableName can be created automatically on :connectionName
Expand All @@ -562,38 +508,15 @@ public function tableCanBeCreatedAutomaticallyOn($tableName, $connectionName)
Doctrine\DBAL\DBALException $exception,
Ez\DbLinker\Driver\Connection\RetryConnection $connection
) use ($tableName) {
if (strpos($exception->getMessage(), "An exception occurred while executing 'SELECT * FROM {$tableName}':") === 0) {
if (
$exception instanceof TableNotFoundException &&
strpos($exception->getMessage(), $tableName) !== false
) {
$connection->exec("CREATE TABLE {$tableName} (id INT)");
return true;
}
});
}
}

class MysqlRetryStrategy extends Ez\DbLinker\RetryStrategy\MysqlRetryStrategy
{
private $lastError = null;
private $handlers = [];

public function shouldRetry(
Doctrine\DBAL\DBALException $exception,
Ez\DbLinker\Driver\Connection\RetryConnection $connection,
$method,
Array $arguments
) {
$this->lastError = $exception;
return array_reduce($this->handlers, function($retry, Closure $handler) use ($exception, $connection) {
return $retry || $handler($exception, $connection);
}, false) || parent::shouldRetry($exception, $connection, $method, $arguments);
}

public function lastError()
{
return $this->lastError;
}

public function addHandler(Closure $handler)
{
$this->handlers[] = $handler;
}
abstract protected function retryStrategy($n);
}
Loading

0 comments on commit c8f03f2

Please sign in to comment.