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