From bc2546b5b6d7377a68730ba6268fe6ce7058dfc5 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Tue, 28 Nov 2017 11:50:23 +0100 Subject: [PATCH 01/14] use php7.1 from sury --- config/docker/php/behat/Dockerfile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/docker/php/behat/Dockerfile b/config/docker/php/behat/Dockerfile index 4efce33..96414b7 100644 --- a/config/docker/php/behat/Dockerfile +++ b/config/docker/php/behat/Dockerfile @@ -1,13 +1,13 @@ -FROM php:7.1 +FROM isanosyan/php:7.1-cli-base RUN apt-get update -RUN apt-get install -y git file libpq-dev +RUN apt-get install -y git file libpq-dev php-apcu php7.1-dev php7.1-mbstring php7.1-mysql php7.1-pgsql RUN cd /tmp/ && git clone git://github.com/xdebug/xdebug.git -RUN cd /tmp/xdebug && git checkout XDEBUG_2_5_5 && phpize && ./configure --enable-xdebug && make && cp modules/xdebug.so /usr/local/lib/php/extensions/ +RUN cd /tmp/xdebug && git checkout XDEBUG_2_5_5 && phpize && ./configure --enable-xdebug && make && make install + +COPY apc.ini /etc/php/7.1/mods-available/apcu.ini +RUN phpenmod -v ALL -s ALL apcu -RUN docker-php-ext-install mbstring -RUN docker-php-ext-install pdo_mysql pdo_pgsql mysqli -RUN docker-php-ext-install pcntl ENTRYPOINT ["php", "/scripts/vendor/bin/behat"] From 381b572c7f2e21b1bb7e72148925f1571b971d50 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Tue, 28 Nov 2017 11:51:02 +0100 Subject: [PATCH 02/14] by default, use Apcu cache --- composer.json | 5 +++-- src/Driver/MasterSlavesDriver.php | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 7815610..930f343 100644 --- a/composer.json +++ b/composer.json @@ -13,9 +13,10 @@ } }, "require": { - "php": "^7.1", + "php": "^7", "doctrine/dbal": "^2.5.2", - "psr/cache": "^1.0.0" + "psr/cache": "^1.0.0", + "cache/apcu-adapter": "^1.0" }, "require-dev": { "behat/behat": "~3.0" diff --git a/src/Driver/MasterSlavesDriver.php b/src/Driver/MasterSlavesDriver.php index 0c34a1c..61afb83 100644 --- a/src/Driver/MasterSlavesDriver.php +++ b/src/Driver/MasterSlavesDriver.php @@ -3,6 +3,7 @@ namespace Ez\DbLinker\Driver; use Ez\DbLinker\Driver\Connection\MasterSlavesConnection; +use Cache\Adapter\Apcu\ApcuCachePool; trait MasterSlavesDriver { @@ -25,6 +26,13 @@ public function connect(Array $params, $username = null, $password = null, Array $key = "dblinker.master-slave-config.".hash("sha256", serialize($configParams)); $config = null; + + // default cache, disable it with $cache = false + if ($cache !== false && $cache === null) { + $cache = new ApcuCachePool(); + $driverOptions["config_cache"] = $cache; + } + if ($cache !== null) { assert($driverOptions["config_cache"] instanceof \Psr\Cache\CacheItemPoolInterface); $cacheItem = $cache->getItem($key); @@ -37,6 +45,7 @@ public function connect(Array $params, $username = null, $password = null, Array if ($cache !== null) { $cacheItem->set($config); + $cacheItem->expiresAt(time()+60); $cache->save($cacheItem); } From 05828998c5c0c3bb4c3a32464832d7a578bb61b7 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Tue, 28 Nov 2017 17:47:07 +0100 Subject: [PATCH 03/14] apc config --- config/docker/php/behat/apc.ini | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 config/docker/php/behat/apc.ini diff --git a/config/docker/php/behat/apc.ini b/config/docker/php/behat/apc.ini new file mode 100644 index 0000000..ffe1fe5 --- /dev/null +++ b/config/docker/php/behat/apc.ini @@ -0,0 +1,8 @@ +extension=apcu.so +[apc] +apc.enabled=1 +apc.shm_size=4M +apc.ttl=60 +apc.user_ttl=60 +apc.enable_cli=1 +apc.gc_ttl=600 From b6b4972e73088d04ba24c6612f430a0ffaf65216 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Tue, 28 Nov 2017 17:47:47 +0100 Subject: [PATCH 04/14] wip: Ez\DbLinker\Cache wrapper of psr/cache class --- features/bootstrap/FeatureContext.php | 9 +++ features/master-slave.feature | 9 ++- src/Cache.php | 64 +++++++++++++++++++ .../Connection/MasterSlavesConnection.php | 8 ++- src/Driver/MasterSlavesDriver.php | 26 ++++---- 5 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 src/Cache.php diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 0bf28d5..84e85ee 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -518,5 +518,14 @@ public function tableCanBeCreatedAutomaticallyOn($tableName, $connectionName) }); } + /** + * @Given the cache is disable on :connectionName + */ + public function theCacheIsDisable($connectionName) + { + $connection = $this->getWrappedConnection($connectionName); + $connection->disableCache(); + } + abstract protected function retryStrategy($n); } diff --git a/features/master-slave.feature b/features/master-slave.feature index af90bb7..3751549 100644 --- a/features/master-slave.feature +++ b/features/master-slave.feature @@ -44,4 +44,11 @@ Feature: Master / Slaves And "conn" is on master Scenario: Get database - Then I can get the database name on "conn" + Then I can get the database name on "conn" + + Scenario: Disable cache + Given the cache is disable on "conn" + When I query "SELECT 1" on "conn" + Then the last query succeeded on "conn" + And "conn" is on master + diff --git a/src/Cache.php b/src/Cache.php new file mode 100644 index 0000000..29b22cc --- /dev/null +++ b/src/Cache.php @@ -0,0 +1,64 @@ +<?php + +namespace Ez\DbLinker; + +use Exception; +use stdClass; +use Cache\Adapter\Apcu\ApcuCachePool; + +class Cache +{ + private $cache; + private $defaultCache = "Cache\Adapter\Apcu\ApcuCachePool"; + + public function __construct($cache = null) + { + $this->setCache($cache); + } + + // null = set to default cache (Apcu) + // false = disable cache + // \Psr\Cache\CacheItemPoolInterface class + private function setCache($cache = null) { + if ($cache !== false && $cache === null) { + $cache = new $this->defaultCache(); + } + + if ($cache !== null) { + assert($cache instanceof \Psr\Cache\CacheItemPoolInterface); + } + $this->cache = $cache; + } + + public function getCache() { + return $this->cache; + } + + public function hasCache() { + return (bool)$this->cache; + } + + public function getCacheItem($key) { + if ($this->hasCache()) { + $cacheItem = $this->getCache()->getItem($key); + return $cacheItem->get(); + } + return null; + } + + public function setCacheItem($key, $data, $ttl = 0) { + if ($this->hasCache()) { + $cacheItem = $this->getCache()->getItem($key); + $cacheItem->set($data); + if ($ttl > 0) { + $cacheItem->expiresAt(time()+$ttl); + } + return $this->getCache()->save($cacheItem); + } + return null; + } + + public function disableCache() { + return $this->setCache(false); + } +} diff --git a/src/Driver/Connection/MasterSlavesConnection.php b/src/Driver/Connection/MasterSlavesConnection.php index 30b835a..d66cd2e 100644 --- a/src/Driver/Connection/MasterSlavesConnection.php +++ b/src/Driver/Connection/MasterSlavesConnection.php @@ -14,12 +14,18 @@ class MasterSlavesConnection implements Connection, ConnectionWrapper private $slaves; private $currentConnectionParams; private $currentSlave; + private $cache; - public function __construct(array $master, array $slaves) + public function __construct(array $master, array $slaves, $cache = null) { $this->master = $master; $this->checkSlaves($slaves); $this->slaves = $slaves; + $this->cache = $cache; + } + + public function disableCache() { + return $this->cache->disableCache(); } private function checkSlaves(array $slaves) diff --git a/src/Driver/MasterSlavesDriver.php b/src/Driver/MasterSlavesDriver.php index 61afb83..33675d1 100644 --- a/src/Driver/MasterSlavesDriver.php +++ b/src/Driver/MasterSlavesDriver.php @@ -4,11 +4,15 @@ use Ez\DbLinker\Driver\Connection\MasterSlavesConnection; use Cache\Adapter\Apcu\ApcuCachePool; +use Ez\DbLinker\Cache; trait MasterSlavesDriver { use WrapperDriver; + private $cache; + private $cacheDefaultTtl = 60; + /** * Attempts to create a connection with the database. * @@ -20,7 +24,7 @@ trait MasterSlavesDriver * @return \Doctrine\DBAL\Driver\Connection The database connection. */ public function connect(Array $params, $username = null, $password = null, Array $driverOptions = []) { - $cache = array_key_exists("config_cache", $driverOptions) ? $driverOptions["config_cache"] : null; + $cacheDriver = array_key_exists("config_cache", $driverOptions) ? $driverOptions["config_cache"] : null; $configParams = array_intersect_key($params, array_flip(["master", "slaves"])); $key = "dblinker.master-slave-config.".hash("sha256", serialize($configParams)); @@ -28,28 +32,21 @@ public function connect(Array $params, $username = null, $password = null, Array $config = null; // default cache, disable it with $cache = false - if ($cache !== false && $cache === null) { - $cache = new ApcuCachePool(); - $driverOptions["config_cache"] = $cache; - } + $cache = new Cache($cacheDriver); - if ($cache !== null) { - assert($driverOptions["config_cache"] instanceof \Psr\Cache\CacheItemPoolInterface); - $cacheItem = $cache->getItem($key); - $config = $cacheItem->get(); + if ($cache->hasCache()) { + $config = $cache->getCacheItem($key); } if ($config === null) { $config = $this->config($configParams, $driverOptions); } - if ($cache !== null) { - $cacheItem->set($config); - $cacheItem->expiresAt(time()+60); - $cache->save($cacheItem); + if ($cache->hasCache()) { + $cache->setCacheItem($key, $config, 60); } - return new MasterSlavesConnection($config["master"], $config["slaves"]); + return new MasterSlavesConnection($config["master"], $config["slaves"], $cache); } private function config(array $params, array $driverOptions) @@ -76,4 +73,5 @@ private function config(array $params, array $driverOptions) assert(is_callable($driverOptions["config_transform"])); return $driverOptions["config_transform"]($config); } + } From 4df6f65b625c4591345cda9f3aabd45546809cde Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Wed, 29 Nov 2017 10:18:06 +0100 Subject: [PATCH 05/14] enable assertions ... --- composer.json | 6 +++++- config/docker/php/behat/apc.ini | 25 +++++++++++++++++++++++++ features/master-slave.feature | 4 ++-- features/retry.feature | 3 +-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 930f343..c2cc55c 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,10 @@ { "name": "Mathieu Rochette", "email": "mathieu@rochette.cc" + }, + { + "name": "Grégory Duchatelet", + "email": "greg@duchatelet.net" } ], "autoload": { @@ -15,7 +19,7 @@ "require": { "php": "^7", "doctrine/dbal": "^2.5.2", - "psr/cache": "^1.0.0", + "psr/cache": "^1.0", "cache/apcu-adapter": "^1.0" }, "require-dev": { diff --git a/config/docker/php/behat/apc.ini b/config/docker/php/behat/apc.ini index ffe1fe5..0bd647b 100644 --- a/config/docker/php/behat/apc.ini +++ b/config/docker/php/behat/apc.ini @@ -1,4 +1,29 @@ extension=apcu.so +zend.assertions = On + +[Assertion] +; Assert(expr); active by default. +; http://php.net/assert.active +assert.active = On + +; Issue a PHP warning for each failed assertion. +; http://php.net/assert.warning +assert.warning = On +assert.exception = On + +; Don't bail out by default. +; http://php.net/assert.bail +;assert.bail = Off + +; User-function to be called if an assertion fails. +; http://php.net/assert.callback +;assert.callback = 0 + +; Eval the expression with current error_reporting(). Set to true if you want +; error_reporting(0) around the eval(). +; http://php.net/assert.quiet-eval +;assert.quiet_eval = 0 + [apc] apc.enabled=1 apc.shm_size=4M diff --git a/features/master-slave.feature b/features/master-slave.feature index 3751549..126a2af 100644 --- a/features/master-slave.feature +++ b/features/master-slave.feature @@ -18,7 +18,7 @@ Feature: Master / Slaves When I exec "SET @var = 1" on "conn" And I query "SELECT 1" on "conn" Then the last query succeeded on "conn" - And "conn" is on master + And "conn" is on slave Scenario: Connect on master when there is no slaves Given a master/slaves connection "connMaster" with no slaves @@ -50,5 +50,5 @@ Feature: Master / Slaves Given the cache is disable on "conn" When I query "SELECT 1" on "conn" Then the last query succeeded on "conn" - And "conn" is on master + And "conn" is on slave diff --git a/features/retry.feature b/features/retry.feature index e0ef9f5..2893d81 100644 --- a/features/retry.feature +++ b/features/retry.feature @@ -27,8 +27,7 @@ Feature: Retry @skip-travis @skip-mysqli @skip-pdo-pgsql Scenario: Deadlock found when trying to get lock When I create a deadlock on "conn" with "@master" - Then the last query succeeded on "conn" - And the last error should be "DEADLOCK" on "conn" + Then the last error should be "DEADLOCK" on "conn" And "conn" retry limit should be 0 @skip-travis-pdo-pgsql Scenario: ER_DBACCESS_DENIED_ERROR don't restart From 8063726c4f1c5cd42049bc9d8fb4c16100515ac4 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Wed, 29 Nov 2017 11:40:41 +0100 Subject: [PATCH 06/14] fix test has Gone Away --- features/retry-master-slave.feature | 35 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/features/retry-master-slave.feature b/features/retry-master-slave.feature index 6bfefc9..224bc0d 100644 --- a/features/retry-master-slave.feature +++ b/features/retry-master-slave.feature @@ -13,6 +13,24 @@ Feature: Retry Master/Slaves And "conn" retry limit should be 0 And "conn" should have 1 slave + Scenario: ER_BAD_DB_ERROR restart on another slave + Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry with db "unknown_db" and username "root" + When I query "SELECT 1" on "conn" + Then the last query failed on "conn" + And the last error should be "BAD_DB" on "conn" + And "conn" retry limit should be 0 + And "conn" should have 1 slave + @skip-pdo-pgsql + Scenario: database has Gone Away + Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry + And requests are forced on master for "conn" + And database has Gone Away on "conn" + When I query "SELECT 1" on "conn" + Then the last query succeeded on "conn" + And the last error should be "GONE_AWAY" on "conn" + And "conn" retry limit should be 0 + And "conn" should have 2 slaves + Scenario: ACCESS_DENIED_ERROR does not restart on master Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry with username "nobody" And requests are forced on master for "conn" @@ -22,14 +40,6 @@ Feature: Retry Master/Slaves And "conn" retry limit should be 1 And "conn" should have 2 slaves - Scenario: ER_BAD_DB_ERROR restart on another slave - Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry with db "unknown_db" and username "root" - When I query "SELECT 1" on "conn" - Then the last query failed on "conn" - And the last error should be "BAD_DB" on "conn" - And "conn" retry limit should be 0 - And "conn" should have 1 slave - Scenario: ER_BAD_DB_ERROR does not restart on master Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry with db "unknown_db" and username "root" And requests are forced on master for "conn" @@ -38,12 +48,3 @@ Feature: Retry Master/Slaves And the last error should be "BAD_DB" on "conn" And "conn" retry limit should be 1 And "conn" should have 2 slaves - @skip-pdo-pgsql - Scenario: database has Gone Away - Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry - And database has Gone Away on "conn" - When I query "SELECT 1" on "conn" - Then the last query succeeded on "conn" - And the last error should be "GONE_AWAY" on "conn" - And "conn" retry limit should be 0 - And "conn" should have 2 slaves From afb429d76f30e7731dd947637d92a1d1f0f43839 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Wed, 29 Nov 2017 11:41:10 +0100 Subject: [PATCH 07/14] force connection on master on specific cases like a new transaction --- features/bootstrap/FeatureContext.php | 2 +- src/Cache.php | 2 +- .../Connection/MasterSlavesConnection.php | 23 +++++++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 84e85ee..888b14d 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -138,7 +138,7 @@ public function requestsAreForcedOnMasterFor($connectionName) if ($connection instanceof Ez\DbLinker\Driver\Connection\RetryConnection) { $connection = $connection->wrappedConnection(); } - $connection->connectToMaster(); + $connection->connectToMaster(true); } /** diff --git a/src/Cache.php b/src/Cache.php index 29b22cc..33abeb4 100644 --- a/src/Cache.php +++ b/src/Cache.php @@ -24,7 +24,7 @@ private function setCache($cache = null) { $cache = new $this->defaultCache(); } - if ($cache !== null) { + if ($cache) { assert($cache instanceof \Psr\Cache\CacheItemPoolInterface); } $this->cache = $cache; diff --git a/src/Driver/Connection/MasterSlavesConnection.php b/src/Driver/Connection/MasterSlavesConnection.php index d66cd2e..0783846 100644 --- a/src/Driver/Connection/MasterSlavesConnection.php +++ b/src/Driver/Connection/MasterSlavesConnection.php @@ -15,6 +15,7 @@ class MasterSlavesConnection implements Connection, ConnectionWrapper private $currentConnectionParams; private $currentSlave; private $cache; + private $forceMaster; public function __construct(array $master, array $slaves, $cache = null) { @@ -22,6 +23,7 @@ public function __construct(array $master, array $slaves, $cache = null) $this->checkSlaves($slaves); $this->slaves = $slaves; $this->cache = $cache; + $this->forceMaster = false; } public function disableCache() { @@ -37,8 +39,11 @@ private function checkSlaves(array $slaves) } } - public function connectToMaster() + public function connectToMaster($forced = null) { + if ($forced !== null) { + $this->forceMaster = $forced; + } if ($this->currentConnectionParams === $this->master) { return; } @@ -49,6 +54,10 @@ public function connectToMaster() public function connectToSlave() { + $this->forceMaster = false; + if ($this->currentConnectionParams !== null && $this->currentConnectionParams !== $this->master) { + return; + } $this->currentConnectionParams = null; $this->currentSlave = null; $this->wrappedConnection = null; @@ -129,7 +138,7 @@ public function slaves() */ public function prepare($prepareString) { - $this->connectToMaster(); + $this->connectToMaster(true); return $this->wrappedConnection()->prepare($prepareString); } @@ -140,6 +149,9 @@ public function prepare($prepareString) */ public function query() { + if ($this->forceMaster !== true) { + $this->connectToSlave(); + } return call_user_func_array([$this->wrappedConnection(), __FUNCTION__], func_get_args()); } @@ -178,6 +190,7 @@ public function exec($statement) */ public function lastInsertId($name = null) { + $this->forceMaster = true; return $this->wrappedConnection()->lastInsertId($name); } @@ -188,7 +201,7 @@ public function lastInsertId($name = null) */ public function beginTransaction() { - $this->connectToMaster(); + $this->connectToMaster(true); return $this->wrappedConnection()->beginTransaction(); } @@ -199,7 +212,7 @@ public function beginTransaction() */ public function commit() { - $this->connectToMaster(); + $this->connectToMaster(false); return $this->wrappedConnection()->commit(); } @@ -210,7 +223,7 @@ public function commit() */ public function rollBack() { - $this->connectToMaster(); + $this->connectToMaster(false); return $this->wrappedConnection()->rollBack(); } From 8fe4248674d787259f2959b4332e1c4302a0bb3a Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Wed, 29 Nov 2017 14:57:41 +0100 Subject: [PATCH 08/14] close connections --- src/Driver/Connection/MasterSlavesConnection.php | 8 ++++++++ src/Driver/Connection/RetryConnection.php | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/src/Driver/Connection/MasterSlavesConnection.php b/src/Driver/Connection/MasterSlavesConnection.php index 0783846..a8088a5 100644 --- a/src/Driver/Connection/MasterSlavesConnection.php +++ b/src/Driver/Connection/MasterSlavesConnection.php @@ -5,6 +5,7 @@ use Exception; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Driver\Connection; +use Doctrine\DBAL\Driver\PDOConnection; class MasterSlavesConnection implements Connection, ConnectionWrapper { @@ -246,4 +247,11 @@ public function errorInfo() { return $this->wrappedConnection()->errorInfo(); } + + public function close() + { + if (!$this->wrappedConnection() instanceof PDOConnection) { + return $this->wrappedConnection()->getWrappedResourceHandle()->close(); + } + } } diff --git a/src/Driver/Connection/RetryConnection.php b/src/Driver/Connection/RetryConnection.php index 57b9ba4..6979f71 100644 --- a/src/Driver/Connection/RetryConnection.php +++ b/src/Driver/Connection/RetryConnection.php @@ -3,6 +3,7 @@ namespace Ez\DbLinker\Driver\Connection; use Doctrine\DBAL\Driver\Connection; +use Doctrine\DBAL\Driver\PDOConnection; use Doctrine\DBAL\DriverManager; use Ez\DbLinker\RetryStrategy; @@ -130,6 +131,12 @@ public function errorInfo() { public function close() { + if ($this->wrappedConnection instanceof MasterSlavesConnection) { + $this->wrappedConnection->close(); + } elseif ($this->wrappedConnection instanceof PDOConnection) { + } elseif (method_exists($this->wrappedConnection, "getWrappedResourceHandle")) { + $this->wrappedConnection->getWrappedResourceHandle()->close(); + } $this->wrappedConnection = null; } From 269eeceec26e1d6560288632c411f4448c1baf1a Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Thu, 30 Nov 2017 14:24:55 +0100 Subject: [PATCH 09/14] wip: stash commit, I'm pausing this dev --- behat.yml | 4 ++++ features/bootstrap/FeatureContext.php | 13 ++++++++++++ features/retry-master-slave.feature | 12 +++++++++++ .../Connection/MasterSlavesConnection.php | 21 +++++++++++++++++++ src/Driver/Connection/RetryConnection.php | 6 ++++++ 5 files changed, 56 insertions(+) diff --git a/behat.yml b/behat.yml index 234904f..2af0ee8 100644 --- a/behat.yml +++ b/behat.yml @@ -14,3 +14,7 @@ default: contexts: [ PdoPgsqlContext ] filters: tags: ~@skip-pdo-pgsql + mysqlreplic: + contexts: [ MysqlReplicContext ] + filters: + tags: ~@skip-mysql-replic diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 888b14d..eb48333 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -527,5 +527,18 @@ public function theCacheIsDisable($connectionName) $connection->disableCache(); } + /** + * @Given slave replication is stopped on :connectionName + */ + public function slaveReplicationIsStopped($connectionName) + { + $connection = $this->getWrappedConnection($connectionName); + if ($connection instanceof Ez\DbLinker\Driver\Connection\RetryConnection) { + $connection = $connection->wrappedConnection(); + } + $connection->checkReplication(); + } + + abstract protected function retryStrategy($n); } diff --git a/features/retry-master-slave.feature b/features/retry-master-slave.feature index 224bc0d..d29f4d0 100644 --- a/features/retry-master-slave.feature +++ b/features/retry-master-slave.feature @@ -48,3 +48,15 @@ Feature: Retry Master/Slaves And the last error should be "BAD_DB" on "conn" And "conn" retry limit should be 1 And "conn" should have 2 slaves + + @skip-travis @skip-mysqli @skip-pdo-pgsql + Scenario: Replication is stopped on slave and query restart on another slave + Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry + And slave replication is stopped on "conn" + When I query "SELECT 1" on "conn" + Then the last query succeeded on "conn" + And "conn" retry limit should be 1 + And "conn" should have 1 slaves + + + diff --git a/src/Driver/Connection/MasterSlavesConnection.php b/src/Driver/Connection/MasterSlavesConnection.php index a8088a5..60b5be9 100644 --- a/src/Driver/Connection/MasterSlavesConnection.php +++ b/src/Driver/Connection/MasterSlavesConnection.php @@ -17,6 +17,7 @@ class MasterSlavesConnection implements Connection, ConnectionWrapper private $currentSlave; private $cache; private $forceMaster; + private $maxSlaveDelay = 10; public function __construct(array $master, array $slaves, $cache = null) { @@ -254,4 +255,24 @@ public function close() return $this->wrappedConnection()->getWrappedResourceHandle()->close(); } } + + public function checkReplication() { + if ($this->isConnectedToMaster()) { + return null; + } + $sss = $this->query("SHOW SLAVE STATUS")->fetch(); + echo "Slave is ".$sss['Slave_IO_Running']."/".$sss['Slave_SQL_Running']." : ".$sss['Seconds_Behind_Master']."\n"; + if ($sss['Slave_IO_Running'] === 'No' || $sss['Slave_SQL_Running'] === 'No') { + // slave is STOPPED + $this->disableCurrentSlave(); + return false; + } elseif ($sss['Seconds_Behind_Master'] >= $this->maxSlaveDelay) { + // slave has DELAY + $this->disableCurrentSlave(); + return false; + } else { + // slave is OK + return true; + } + } } diff --git a/src/Driver/Connection/RetryConnection.php b/src/Driver/Connection/RetryConnection.php index 6979f71..f0e9788 100644 --- a/src/Driver/Connection/RetryConnection.php +++ b/src/Driver/Connection/RetryConnection.php @@ -160,4 +160,10 @@ protected function wrap() $this->wrappedConnection = $connection->getWrappedConnection(); $this->wrappedDriver = $connection->getDriver(); } + + public function checkReplication() { + if ($this->wrappedConnection instanceof MasterSlavesConnection) { + $this->wrappedConnection->checkReplication(); + } + } } From 455d3d40fc41b171f75d4faee280f25ce18bebbc Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Wed, 13 Dec 2017 17:54:55 +0100 Subject: [PATCH 10/14] check slave status and store it for 10s --- features/bootstrap/FeatureContext.php | 7 +- features/bootstrap/MysqlReplicContext.php | 48 ++++++++++++++ features/retry-master-slave.feature | 6 +- features/retry.feature | 12 ++-- .../Connection/MasterSlavesConnection.php | 64 ++++++++++++++----- src/Driver/Connection/RetryConnection.php | 6 -- 6 files changed, 114 insertions(+), 29 deletions(-) create mode 100644 features/bootstrap/MysqlReplicContext.php diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index eb48333..767af6a 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -106,9 +106,10 @@ public function aRetryMasterSlavesConnectionWithSlavesLimitedToRetriesWithuserna $slaveCount = (int) $slaveCount; $slaves = []; + $master['weight'] = 1; while ($slaveCount--) { - $master['weight'] = 1; $slaves[] = $master; + $master['weight']++; } $params = [ @@ -536,7 +537,9 @@ public function slaveReplicationIsStopped($connectionName) if ($connection instanceof Ez\DbLinker\Driver\Connection\RetryConnection) { $connection = $connection->wrappedConnection(); } - $connection->checkReplication(); + $connection->connectToSlave(); + $connection->setSlaveStatus(false, 120); + $connection->isSlaveOk(); } diff --git a/features/bootstrap/MysqlReplicContext.php b/features/bootstrap/MysqlReplicContext.php new file mode 100644 index 0000000..9cbb420 --- /dev/null +++ b/features/bootstrap/MysqlReplicContext.php @@ -0,0 +1,48 @@ +<?php + +use Behat\Behat\Context\Context; +use Behat\Behat\Context\SnippetAcceptingContext; + +class MysqlReplicContext implements Context, SnippetAcceptingContext +{ + use FeatureContext; + use MySQLContext; + + private function masterParams($username = null, $password = '') { + $params = [ + 'host' => '192.168.0.8', + 'user' => $username === null ? 'mcm' : $username, + 'password' => 'uvieng7c', + 'dbname' => 'mcm', + ]; + return $this->params($params); + } + /** + * @BeforeScenario + */ + public function clearConnections() { + } + + /** + * @BeforeScenario + */ + public function clearDatabase() { + } + + /** + * @BeforeScenario + */ + public function assertNoActiveConnection() { + } + + private function params(Array $params) + { + $params['driver'] = 'mysqli'; + return $params; + } + + private function defaultDatabaseName() + { + return 'mcm'; + } +} diff --git a/features/retry-master-slave.feature b/features/retry-master-slave.feature index d29f4d0..f7e9cb4 100644 --- a/features/retry-master-slave.feature +++ b/features/retry-master-slave.feature @@ -13,11 +13,12 @@ Feature: Retry Master/Slaves And "conn" retry limit should be 0 And "conn" should have 1 slave + @skip-mysql-replic Scenario: ER_BAD_DB_ERROR restart on another slave - Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry with db "unknown_db" and username "root" + Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry with db "unknown_db" When I query "SELECT 1" on "conn" Then the last query failed on "conn" - And the last error should be "BAD_DB" on "conn" + And the last error should be "DBACCESS_DENIED" on "conn" And "conn" retry limit should be 0 And "conn" should have 1 slave @skip-pdo-pgsql @@ -40,6 +41,7 @@ Feature: Retry Master/Slaves And "conn" retry limit should be 1 And "conn" should have 2 slaves + @skip-mysql-replic Scenario: ER_BAD_DB_ERROR does not restart on master Given a retry master/slaves connection "conn" with 2 slaves limited to 1 retry with db "unknown_db" and username "root" And requests are forced on master for "conn" diff --git a/features/retry.feature b/features/retry.feature index 2893d81..a25a83b 100644 --- a/features/retry.feature +++ b/features/retry.feature @@ -10,9 +10,10 @@ Feature: Retry Then the last query succeeded on "conn" And the last error should be "GONE_AWAY" on "conn" And "conn" retry limit should be 0 - @skip-pdo-pgsql + @skip-pdo-pgsql @skip-mysql-replic Scenario: Lock wait timeout exceeded Given a retry connection "once" limited to 1 retry + And a retry connection "@master" limited to 1 retry And I exec "SET SESSION innodb_lock_wait_timeout = 1" on "@master" And I exec "SET SESSION innodb_lock_wait_timeout = 1" on "once" And there is a table "test_lock" on "@master" @@ -24,12 +25,13 @@ Feature: Retry Then the last query failed on "conn" And the last error should be "LOCK_WAIT_TIMEOUT" on "once" And "once" retry limit should be 0 - @skip-travis @skip-mysqli @skip-pdo-pgsql + @skip-travis @skip-mysqli @skip-pdo-pgsql @skip-mysql-replic Scenario: Deadlock found when trying to get lock + Given a retry connection "@master" limited to 1 retry When I create a deadlock on "conn" with "@master" Then the last error should be "DEADLOCK" on "conn" And "conn" retry limit should be 0 - @skip-travis-pdo-pgsql + @skip-travis-pdo-pgsql @skip-mysql-replic Scenario: ER_DBACCESS_DENIED_ERROR don't restart Given a retry connection "conn" limited to 1 retry with db "forbidden_db" When I query "SELECT 1" on "conn" @@ -44,6 +46,7 @@ Feature: Retry And the last error should be "ACCESS_DENIED" on "conn" And "conn" retry limit should be 1 + @skip-mysql-replic Scenario: ER_BAD_DB_ERROR don't restart Given a retry connection "conn" limited to 1 retry with db "unknown_db" and username "root" When I query "SELECT 1" on "conn" @@ -66,7 +69,7 @@ Feature: Retry And I query "SELECT 1" on "conn" Then the last query succeeded on "conn" And "conn" retry limit should be 0 - @skip-pdo-pgsql + @skip-pdo-pgsql @skip-mysql-replic Scenario: Too many connections Given the server accept 1 more connections And a retry connection "conn1" limited to 1 retry @@ -80,6 +83,7 @@ Feature: Retry Scenario: Get database Then I can get the database name on "conn" + @skip-mysql-replic Scenario: No such table Given table "not_here_yet" can be created automatically on "conn" When I prepare a statement "SELECT * FROM not_here_yet" on "conn" diff --git a/src/Driver/Connection/MasterSlavesConnection.php b/src/Driver/Connection/MasterSlavesConnection.php index 60b5be9..9283063 100644 --- a/src/Driver/Connection/MasterSlavesConnection.php +++ b/src/Driver/Connection/MasterSlavesConnection.php @@ -17,7 +17,8 @@ class MasterSlavesConnection implements Connection, ConnectionWrapper private $currentSlave; private $cache; private $forceMaster; - private $maxSlaveDelay = 10; + private $maxSlaveDelay = 30; + private $slaveStatusCacheTtl = 10; public function __construct(array $master, array $slaves, $cache = null) { @@ -63,6 +64,10 @@ public function connectToSlave() $this->currentConnectionParams = null; $this->currentSlave = null; $this->wrappedConnection = null; + $this->wrap(); + while (!$this->isSlaveOk() && $this->currentSlave !== null) { + $this->wrap(); + } } public function isConnectedToMaster() @@ -256,23 +261,52 @@ public function close() } } - public function checkReplication() { - if ($this->isConnectedToMaster()) { - return null; + private function hasCache() { + return $this->cache !== null; + } + + private function getCacheKey() { + return "MasterSlavesConnection_".strtr(serialize($this->currentConnectionParams), '{}()/@:', '______|'); + } + + public function setSlaveStatus(bool $running, int $delay) { + if ($this->hasCache()) { + $this->cache->setCacheItem($this->getCacheKey(), ["running" => $running, "delay" => $delay], $this->slaveStatusCacheTtl); } - $sss = $this->query("SHOW SLAVE STATUS")->fetch(); - echo "Slave is ".$sss['Slave_IO_Running']."/".$sss['Slave_SQL_Running']." : ".$sss['Seconds_Behind_Master']."\n"; - if ($sss['Slave_IO_Running'] === 'No' || $sss['Slave_SQL_Running'] === 'No') { - // slave is STOPPED - $this->disableCurrentSlave(); - return false; - } elseif ($sss['Seconds_Behind_Master'] >= $this->maxSlaveDelay) { - // slave has DELAY + return ['running' => $running, 'delay' => $delay]; + } + + private function getSlaveStatus() { + try { + $sss = $this->wrappedConnection()->query("SHOW SLAVE STATUS")->fetch(); + if ($sss['Slave_IO_Running'] === 'No' || $sss['Slave_SQL_Running'] === 'No') { + // slave is STOPPED + return $this->setSlaveStatus(false, INF); + } else { + return $this->setSlaveStatus(true, $sss['Seconds_Behind_Master']); + } + } catch (\Exception $e) { + return $this->setSlaveStatus(true, 0); + } + } + + public function isSlaveOk($maxdelay = null) { + if ($maxdelay === null) { + $maxdelay = $this->maxSlaveDelay; + } + if ($this->hasCache()) { + $status = $this->cache->getCacheItem($this->getCacheKey()); + if ($status === null) { + $status = $this->getSlaveStatus(); + } + } else { + $status = $this->getSlaveStatus(); + } + if (!$status['running'] || $status['delay'] >= $maxdelay) { $this->disableCurrentSlave(); + $this->wrap(); return false; - } else { - // slave is OK - return true; } + return true; } } diff --git a/src/Driver/Connection/RetryConnection.php b/src/Driver/Connection/RetryConnection.php index f0e9788..6979f71 100644 --- a/src/Driver/Connection/RetryConnection.php +++ b/src/Driver/Connection/RetryConnection.php @@ -160,10 +160,4 @@ protected function wrap() $this->wrappedConnection = $connection->getWrappedConnection(); $this->wrappedDriver = $connection->getDriver(); } - - public function checkReplication() { - if ($this->wrappedConnection instanceof MasterSlavesConnection) { - $this->wrappedConnection->checkReplication(); - } - } } From 593ee780ba6ec62d0f87b07c6b3a706a31213a3a Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Thu, 14 Dec 2017 09:13:09 +0100 Subject: [PATCH 11/14] add apcu to travis --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 074bdb8..87d878f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,10 @@ php: - "7.1" addons: - postgresql: "9.4" + postgresql: "10.0" + apt: + packages: + - php-apcu before_script: - composer install From 7a25f5e495c1bd651f1e9211c2e747854d90a000 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Thu, 14 Dec 2017 09:34:57 +0100 Subject: [PATCH 12/14] fix apcu travis --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 87d878f..f0baa6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,9 @@ php: addons: postgresql: "10.0" - apt: - packages: - - php-apcu before_script: + - pecl install apcu-5.1.8 - 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 From 5a737f9189418795aacedb0183ae5585b9c92b46 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Thu, 14 Dec 2017 09:41:57 +0100 Subject: [PATCH 13/14] fix apcu travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f0baa6d..87187a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ addons: postgresql: "10.0" before_script: - - pecl install apcu-5.1.8 + - phpenv config-add config/docker/php/behat/apc.ini - 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 From e059c516969302a7715ba06df7d259a516e3f937 Mon Sep 17 00:00:00 2001 From: Gregory Duchatelet <greg@easyflirt.com> Date: Thu, 14 Dec 2017 09:46:37 +0100 Subject: [PATCH 14/14] fix travis: postgresql 9.6 instead of 10.0 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 87187a9..b4a9263 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ php: - "7.1" addons: - postgresql: "10.0" + postgresql: "9.6" before_script: - phpenv config-add config/docker/php/behat/apc.ini