From ce085b46aa97069c538ea2f77f18d4790ac64d0b Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 15 May 2015 11:43:45 +0300 Subject: [PATCH 01/54] #274 Added initial version of the Dockerfile for ProxySQL --- Dockerfile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..6e8d79c85 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# We're using Ubuntu 14:04 because ProxySQL compilation needs one of the latest +# g++ compilers. Also, it's a long term release. +FROM ubuntu:14.04 +MAINTAINER Andrei Ismail +RUN apt-get update +RUN apt-get install -y git +RUN apt-get install -y make +RUN apt-get install -y cmake +RUN apt-get install -y gcc +RUN apt-get install -y g++ +RUN apt-get install -y libssl-dev +RUN apt-get install -y libmysqlclient-dev +# This will enable us to clone the ProxySQL repo without git prompting us for +# the validity of the RSA fingerprint. +RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git +RUN cd /opt/proxysql-0.2; make clean && make +RUN mkdir -p /var/run/proxysql +RUN cp /opt/proxysql-0.2/proxysql.cfg /etc \ No newline at end of file From 30f1aa871272eed75a375bbec0f269cf31ec327e Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 15 May 2015 11:52:49 +0300 Subject: [PATCH 02/54] Remove invalid comment. --- Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6e8d79c85..44bd3d5af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,6 @@ RUN apt-get install -y gcc RUN apt-get install -y g++ RUN apt-get install -y libssl-dev RUN apt-get install -y libmysqlclient-dev -# This will enable us to clone the ProxySQL repo without git prompting us for -# the validity of the RSA fingerprint. RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git RUN cd /opt/proxysql-0.2; make clean && make RUN mkdir -p /var/run/proxysql From 7ea22f93484860402c05c4cc2f30314ceec4b6c7 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 15 May 2015 14:46:57 +0300 Subject: [PATCH 03/54] Making sure our DockerFile takes into account best practices. --- Dockerfile | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 44bd3d5af..0d2935ebb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,14 +2,15 @@ # g++ compilers. Also, it's a long term release. FROM ubuntu:14.04 MAINTAINER Andrei Ismail -RUN apt-get update -RUN apt-get install -y git -RUN apt-get install -y make -RUN apt-get install -y cmake -RUN apt-get install -y gcc -RUN apt-get install -y g++ -RUN apt-get install -y libssl-dev -RUN apt-get install -y libmysqlclient-dev +RUN apt-get update && apt-get install -y\ + cmake\ + make\ + g++\ + gcc\ + git\ + libssl-dev\ + libmysqlclient-dev + RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git RUN cd /opt/proxysql-0.2; make clean && make RUN mkdir -p /var/run/proxysql From 4bef65923097975aa10d950006398aec2221b3d2 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 15 May 2015 14:47:24 +0300 Subject: [PATCH 04/54] Put Dockerfile into an empty dir in order to minimize context transfer. --- Dockerfile => docker/Dockerfile | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Dockerfile => docker/Dockerfile (100%) diff --git a/Dockerfile b/docker/Dockerfile similarity index 100% rename from Dockerfile rename to docker/Dockerfile From 8d805c387793ec067ad11a5e2e954cecbc9e2aaf Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 09:19:56 +0300 Subject: [PATCH 05/54] #285 Add docker compose YML for setting up 1 proxy and 1 MySQL backend --- docker/1backend/docker-compose.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docker/1backend/docker-compose.yml diff --git a/docker/1backend/docker-compose.yml b/docker/1backend/docker-compose.yml new file mode 100644 index 000000000..0f67246c6 --- /dev/null +++ b/docker/1backend/docker-compose.yml @@ -0,0 +1,14 @@ +proxysql: + build: ./proxysql + links: + - backend1hostgroup0 + ports: + - "127.0.0.1:6032:6032" + - "6033:6033" + +backend1hostgroup0: + build: ./mysql + environment: + MYSQL_ROOT_PASSWORD: root + expose: + - "3306" From e45ad15bf32ac4f24b9ad1b494d00fb0753644de Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 09:21:01 +0300 Subject: [PATCH 06/54] #285 Delete Dockerfile -- we will have one per test setup --- docker/Dockerfile | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 docker/Dockerfile diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 0d2935ebb..000000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -# We're using Ubuntu 14:04 because ProxySQL compilation needs one of the latest -# g++ compilers. Also, it's a long term release. -FROM ubuntu:14.04 -MAINTAINER Andrei Ismail -RUN apt-get update && apt-get install -y\ - cmake\ - make\ - g++\ - gcc\ - git\ - libssl-dev\ - libmysqlclient-dev - -RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git -RUN cd /opt/proxysql-0.2; make clean && make -RUN mkdir -p /var/run/proxysql -RUN cp /opt/proxysql-0.2/proxysql.cfg /etc \ No newline at end of file From 13511276c77f1883cecfdc96a0b7adb92868b0fc Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 09:21:34 +0300 Subject: [PATCH 07/54] #285 Add Dockerfile for MySQL instance We are extending the base Dockerfile with a schema dump and a script that is able to install that. --- docker/1backend/mysql/Dockerfile | 7 +++++++ docker/1backend/mysql/import_schema.sh | 1 + docker/1backend/mysql/schema.sql | 5 +++++ 3 files changed, 13 insertions(+) create mode 100644 docker/1backend/mysql/Dockerfile create mode 100644 docker/1backend/mysql/import_schema.sh create mode 100644 docker/1backend/mysql/schema.sql diff --git a/docker/1backend/mysql/Dockerfile b/docker/1backend/mysql/Dockerfile new file mode 100644 index 000000000..7600b1b01 --- /dev/null +++ b/docker/1backend/mysql/Dockerfile @@ -0,0 +1,7 @@ +# We are creating a custom Dockerfile for MySQL as there is no easy way to +# move a file from host into the container. In our case, it's schema.sql +# There is a proposed improvement to "docker cp" but it's still being +# discussed (https://github.com/docker/docker/issues/5846). +FROM mysql:latest +ADD ./schema.sql /tmp/ +ADD ./import_schema.sh /tmp/ \ No newline at end of file diff --git a/docker/1backend/mysql/import_schema.sh b/docker/1backend/mysql/import_schema.sh new file mode 100644 index 000000000..fe993ee7e --- /dev/null +++ b/docker/1backend/mysql/import_schema.sh @@ -0,0 +1 @@ +cat /tmp/schema.sql | mysql -h 127.0.0.1 -u root -proot \ No newline at end of file diff --git a/docker/1backend/mysql/schema.sql b/docker/1backend/mysql/schema.sql new file mode 100644 index 000000000..ff75fee5e --- /dev/null +++ b/docker/1backend/mysql/schema.sql @@ -0,0 +1,5 @@ +CREATE DATABASE test; + +USE test; + +CREATE TABLE strings(value LONGTEXT); \ No newline at end of file From 71c0fb2722224de4870d0c81d1f146df4ea53ab2 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 09:22:17 +0300 Subject: [PATCH 08/54] #285 Add base class for ProxySQL tests Proxy config population is still not working because I'm not able to access the admin port on MacOS (via boot2docker). Moving development into the cloud .. :) --- test/__init__.py | 0 test/proxysql_test.py | 82 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 test/__init__.py create mode 100644 test/proxysql_test.py diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/proxysql_test.py b/test/proxysql_test.py new file mode 100644 index 000000000..ea070d048 --- /dev/null +++ b/test/proxysql_test.py @@ -0,0 +1,82 @@ +import subprocess +import time +from unittest import TestCase + +from docker import Client +from docker.utils import kwargs_from_env +import MySQLdb + +class ProxySQLTest(TestCase): + + DOCKER_COMPOSE_FILE = "./docker/1backend" + + @classmethod + def _startup_docker_services(cls): + # We have to perform docker-compose build + docker-compose up, + # instead of just doing the latter because of a bug which will give a + # 500 internal error for the Docker bug. When this is fixed, we should + # remove this first extra step. + subprocess.call(["docker-compose", "build"], cwd=cls.DOCKER_COMPOSE_FILE) + subprocess.call(["docker-compose", "up", "-d"], cwd=cls.DOCKER_COMPOSE_FILE) + + @classmethod + def _shutdown_docker_services(cls): + subprocess.call(["docker-compose", "stop"], cwd=cls.DOCKER_COMPOSE_FILE) + subprocess.call(["docker-compose", "rm", "--force"], cwd=cls.DOCKER_COMPOSE_FILE) + + @classmethod + def _get_proxysql_container(cls): + containers = Client(**kwargs_from_env()).containers() + for container in containers: + if 'proxysql' in container['image']: + return container + + @classmethod + def _get_mysql_containers(cls): + result = [] + containers = Client(**kwargs_from_env()).containers() + for container in containers: + if 'proxysql' not in container['Image']: + result.append(container) + return result + + @classmethod + def _populate_mysql_containers_with_dump(cls): + mysql_containers = cls._get_mysql_containers() + # We have already added the SQL dump to the container by using + # the ADD mysql command in the Dockerfile for mysql -- check it + # out. The standard agreed location is at /tmp/schema.sql. + # + # Unfortunately we can't do this step at runtime due to limitations + # on how transfer between host and container is supposed to work by + # design. See the Dockerfile for MySQL for more details. + for mysql_container in mysql_containers: + container_id = mysql_container['Names'][0][1:] + subprocess.call(["docker", "exec", container_id, "bash", "/tmp/import_schema.sh"]) + + @classmethod + def _populate_proxy_configuration_with_backends(cls): + pass + + @classmethod + def setUpClass(cls): + cls._shutdown_docker_services() + cls._startup_docker_services() + + # TODO(andrei): figure out a more reliable method to wait for + # MySQL to start up within the container. Otherwise, there will be + # an error when we try to initialize the MySQL instance with the dump. + time.sleep(30) + cls._populate_mysql_containers_with_dump() + + cls._populate_proxy_configuration_with_backends() + + @classmethod + def tearDownClass(cls): + pass + + def test_asd(self): + self.assertEqual(1, 1) + + def test_asdf(self): + self.assertEqual(1, 1) \ No newline at end of file From f9c70f11f8974c5e3da674a5dd645dd0b00130ae Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 09:22:50 +0300 Subject: [PATCH 09/54] #285 Add requirements.txt for running the Python test suite Docker compose and requests version are particularly important. --- requirements.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..592c34ed3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +nose==1.3.6 +docker-compose==1.1.0 +requests==2.4.3 +MySQL-python==1.2.5 \ No newline at end of file From 5db205365d81c3cee09b003babc44ba090099837 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 09:32:19 +0300 Subject: [PATCH 10/54] #285 Add Dockerfile for ProxySQL instance w/ one backend. --- docker/1backend/proxysql/Dockerfile | 20 ++++++++++++++++++++ docker/1backend/proxysql/proxysql.cfg | 18 ++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 docker/1backend/proxysql/Dockerfile create mode 100644 docker/1backend/proxysql/proxysql.cfg diff --git a/docker/1backend/proxysql/Dockerfile b/docker/1backend/proxysql/Dockerfile new file mode 100644 index 000000000..4405f4f78 --- /dev/null +++ b/docker/1backend/proxysql/Dockerfile @@ -0,0 +1,20 @@ +# We're using Ubuntu 14:04 because ProxySQL compilation needs one of the latest +# g++ compilers. Also, it's a long term release. +FROM ubuntu:14.04 +MAINTAINER Andrei Ismail +RUN apt-get update && apt-get install -y\ + cmake\ + make\ + g++\ + gcc\ + git\ + libssl-dev\ + libmysqlclient-dev + +RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git proxysql +RUN cd /opt/proxysql; make clean && make +RUN mkdir -p /var/run/proxysql +ADD ./proxysql.cfg /etc/ + +WORKDIR /opt/proxysql/src +CMD ["/opt/proxysql/src/proxysql", "--initial"] \ No newline at end of file diff --git a/docker/1backend/proxysql/proxysql.cfg b/docker/1backend/proxysql/proxysql.cfg new file mode 100644 index 000000000..85b385f0a --- /dev/null +++ b/docker/1backend/proxysql/proxysql.cfg @@ -0,0 +1,18 @@ +datadir="/tmp" + +admin_variables = +{ + admin_credentials="admin:admin" + mysql_ifaces="127.0.0.1:6032" + refresh_interval=2000 + debug=true +} + +mysql_users = +( + { + username = "root" + password = "root" + default_hostgroup = 0 + } +) \ No newline at end of file From 5f4ae7e00990211d21138b5efc7f413c305f041a Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 17:11:18 +0300 Subject: [PATCH 11/54] #285 Stop mapping ProxySQL admin port 127.0.0.1 interface The reason for which I was trying to do this was that it wasn't being correctly forwarded by Docker. It works correctly if I bind the forwarded port to 0.0.0.0 instead of 127.0.0.1 on the original container. --- docker/1backend/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/1backend/docker-compose.yml b/docker/1backend/docker-compose.yml index 0f67246c6..1d5c2cc55 100644 --- a/docker/1backend/docker-compose.yml +++ b/docker/1backend/docker-compose.yml @@ -3,7 +3,7 @@ proxysql: links: - backend1hostgroup0 ports: - - "127.0.0.1:6032:6032" + - "6032:6032" - "6033:6033" backend1hostgroup0: From d78acb1cbfd8c98873d476cc92ac9c9e139892a7 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 17:11:40 +0300 Subject: [PATCH 12/54] #285 Populate db with 4 strings for playing around with them --- docker/1backend/mysql/schema.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docker/1backend/mysql/schema.sql b/docker/1backend/mysql/schema.sql index ff75fee5e..cc6ea878d 100644 --- a/docker/1backend/mysql/schema.sql +++ b/docker/1backend/mysql/schema.sql @@ -2,4 +2,9 @@ CREATE DATABASE test; USE test; -CREATE TABLE strings(value LONGTEXT); \ No newline at end of file +CREATE TABLE strings(value LONGTEXT); + +INSERT INTO strings(value) VALUES('a'); +INSERT INTO strings(value) VALUES('ab'); +INSERT INTO strings(value) VALUES('abc'); +INSERT INTO strings(value) VALUES('abcd'); \ No newline at end of file From c7d199bd0663c4d02860b9948b71ee37e8dd07e0 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 17:12:49 +0300 Subject: [PATCH 13/54] #285 Fix config file name .cfg wasn't being picked up by ProxySQL. What led me into believing it was .cfg was that the config file in the root of the repo is named like that. --- docker/1backend/proxysql/Dockerfile | 2 +- docker/1backend/proxysql/{proxysql.cfg => proxysql.cnf} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docker/1backend/proxysql/{proxysql.cfg => proxysql.cnf} (86%) diff --git a/docker/1backend/proxysql/Dockerfile b/docker/1backend/proxysql/Dockerfile index 4405f4f78..c4514f6ee 100644 --- a/docker/1backend/proxysql/Dockerfile +++ b/docker/1backend/proxysql/Dockerfile @@ -14,7 +14,7 @@ RUN apt-get update && apt-get install -y\ RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git proxysql RUN cd /opt/proxysql; make clean && make RUN mkdir -p /var/run/proxysql -ADD ./proxysql.cfg /etc/ +ADD ./proxysql.cnf /etc/ WORKDIR /opt/proxysql/src CMD ["/opt/proxysql/src/proxysql", "--initial"] \ No newline at end of file diff --git a/docker/1backend/proxysql/proxysql.cfg b/docker/1backend/proxysql/proxysql.cnf similarity index 86% rename from docker/1backend/proxysql/proxysql.cfg rename to docker/1backend/proxysql/proxysql.cnf index 85b385f0a..b50345d01 100644 --- a/docker/1backend/proxysql/proxysql.cfg +++ b/docker/1backend/proxysql/proxysql.cnf @@ -3,7 +3,7 @@ datadir="/tmp" admin_variables = { admin_credentials="admin:admin" - mysql_ifaces="127.0.0.1:6032" + mysql_ifaces="0.0.0.0:6032" refresh_interval=2000 debug=true } From f4f191c963f37fc0d5c76d8671500b1d2179d0f3 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 17:13:15 +0300 Subject: [PATCH 14/54] #285 Add base class for ProxySQL tests based on a fleet of containers w/ Docker --- ...proxysql_test.py => proxysql_base_test.py} | 68 ++++++++++++++++--- 1 file changed, 58 insertions(+), 10 deletions(-) rename test/{proxysql_test.py => proxysql_base_test.py} (54%) diff --git a/test/proxysql_test.py b/test/proxysql_base_test.py similarity index 54% rename from test/proxysql_test.py rename to test/proxysql_base_test.py index ea070d048..6a5590ad4 100644 --- a/test/proxysql_test.py +++ b/test/proxysql_base_test.py @@ -1,3 +1,4 @@ +import re import subprocess import time from unittest import TestCase @@ -6,9 +7,15 @@ from docker import Client from docker.utils import kwargs_from_env import MySQLdb -class ProxySQLTest(TestCase): +class ProxySQLBaseTest(TestCase): - DOCKER_COMPOSE_FILE = "./docker/1backend" + DOCKER_COMPOSE_FILE = None + PROXYSQL_ADMIN_PORT = 6032 + PROXYSQL_ADMIN_USERNAME = "admin" + PROXYSQL_ADMIN_PASSWORD = "admin" + PROXYSQL_RW_PORT = 6033 + PROXYSQL_RW_USERNAME = "root" + PROXYSQL_RW_PASSWORD = "root" @classmethod def _startup_docker_services(cls): @@ -28,7 +35,7 @@ class ProxySQLTest(TestCase): def _get_proxysql_container(cls): containers = Client(**kwargs_from_env()).containers() for container in containers: - if 'proxysql' in container['image']: + if 'proxysql' in container['Image']: return container @classmethod @@ -54,9 +61,54 @@ class ProxySQLTest(TestCase): container_id = mysql_container['Names'][0][1:] subprocess.call(["docker", "exec", container_id, "bash", "/tmp/import_schema.sh"]) + @classmethod + def _extract_hostgroup_from_container_name(cls, container_name): + service_name = container_name.split('_')[1] + return int(re.search(r'BACKEND(\d+)HOSTGROUP(\d+)', service_name).group(2)) + + @classmethod + def _extract_port_number_from_uri(cls, uri): + return int(uri.split(':')[2]) + + @classmethod + def _get_environment_variables_from_container(cls, container_name): + output = Client(**kwargs_from_env()).execute(container_name, 'env') + result = {} + lines = output.split('\n') + for line in lines: + line = line.strip() + if len(line) == 0: + continue + (k, v) = line.split('=') + result[k] = v + return result + @classmethod def _populate_proxy_configuration_with_backends(cls): - pass + proxysql_container = cls._get_proxysql_container() + mysql_containers = cls._get_mysql_containers() + environment_variables = cls._get_environment_variables_from_container( + proxysql_container['Names'][0][1:]) + + proxy_admin_connection = MySQLdb.connect("127.0.0.1", + cls.PROXYSQL_ADMIN_USERNAME, + cls.PROXYSQL_ADMIN_PASSWORD, + port=cls.PROXYSQL_ADMIN_PORT) + cursor = proxy_admin_connection.cursor() + + for mysql_container in mysql_containers: + container_name = mysql_container['Names'][0][1:].upper() + port_uri = environment_variables['%s_PORT' % container_name] + port_no = cls._extract_port_number_from_uri(port_uri) + ip = environment_variables['%s_PORT_%d_TCP_ADDR' % (container_name, port_no)] + hostgroup = cls._extract_hostgroup_from_container_name(container_name) + cursor.execute("INSERT INTO mysql_servers(hostgroup_id, hostname, port, status) " + "VALUES(%d, '%s', %d, 'ONLINE')" % + (hostgroup, ip, port_no)) + + cursor.execute("LOAD MYSQL SERVERS TO RUNTIME") + cursor.close() + proxy_admin_connection.close() @classmethod def setUpClass(cls): @@ -73,10 +125,6 @@ class ProxySQLTest(TestCase): @classmethod def tearDownClass(cls): - pass - - def test_asd(self): - self.assertEqual(1, 1) + cls._shutdown_docker_services() - def test_asdf(self): - self.assertEqual(1, 1) \ No newline at end of file + \ No newline at end of file From 04fe577032d1bee913c169920065189f33e51cbe Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Fri, 22 May 2015 17:13:40 +0300 Subject: [PATCH 15/54] #285 Implement one simple test that selects strings and checks for all of them being present --- test/one_backend_test.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 test/one_backend_test.py diff --git a/test/one_backend_test.py b/test/one_backend_test.py new file mode 100644 index 000000000..887aaf22f --- /dev/null +++ b/test/one_backend_test.py @@ -0,0 +1,21 @@ +import MySQLdb + +from proxysql_base_test import ProxySQLBaseTest + +class OneBackendTest(ProxySQLBaseTest): + + DOCKER_COMPOSE_FILE = "./docker/1backend" + + def test_select_strings_returns_correct_result(self): + proxy_connection = MySQLdb.connect("127.0.0.1", + ProxySQLBaseTest.PROXYSQL_RW_USERNAME, + ProxySQLBaseTest.PROXYSQL_RW_PASSWORD, + port=ProxySQLBaseTest.PROXYSQL_RW_PORT) + cursor = proxy_connection.cursor() + cursor.execute("USE test") + cursor.execute("SELECT * FROM strings") + rows = cursor.fetchall() + self.assertEqual(set([row[0] for row in rows]), + set(['a', 'ab', 'abc', 'abcd'])) + cursor.close() + proxy_connection.close() \ No newline at end of file From 3edc0ab63f6dc442d2a554e9eb74738b16600aba Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Tue, 2 Jun 2015 14:20:53 +0300 Subject: [PATCH 16/54] #285 Expose the MySQL backend publicly so that the tests too can connect to it. --- docker/1backend/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/1backend/docker-compose.yml b/docker/1backend/docker-compose.yml index 1d5c2cc55..98a8735ac 100644 --- a/docker/1backend/docker-compose.yml +++ b/docker/1backend/docker-compose.yml @@ -12,3 +12,5 @@ backend1hostgroup0: MYSQL_ROOT_PASSWORD: root expose: - "3306" + ports: + - "13306:3306" From e3b03dbaba3d643c5c8e08e639a54c8ff9d208bd Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Tue, 2 Jun 2015 14:21:35 +0300 Subject: [PATCH 17/54] #285 Make sure to always drop the database before populating the schema The reason is that the previous tests might have left the database in an inconsistent state. --- docker/1backend/mysql/schema.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/1backend/mysql/schema.sql b/docker/1backend/mysql/schema.sql index cc6ea878d..19785d9af 100644 --- a/docker/1backend/mysql/schema.sql +++ b/docker/1backend/mysql/schema.sql @@ -1,3 +1,5 @@ +DROP DATABASE IF EXISTS test; + CREATE DATABASE test; USE test; From 0d2667d78bc5b18f6cdab32bdf69e147e95eb43b Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Tue, 2 Jun 2015 14:22:17 +0300 Subject: [PATCH 18/54] #285 Added utility methods for running SQL queries against the proxy or against a MySQL backend --- test/proxysql_base_test.py | 78 +++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 6a5590ad4..6d2768c59 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -1,3 +1,4 @@ +import random import re import subprocess import time @@ -126,5 +127,78 @@ class ProxySQLBaseTest(TestCase): @classmethod def tearDownClass(cls): cls._shutdown_docker_services() - - \ No newline at end of file + + def run_query_proxysql(self, query, db, return_result=True): + """Run a query against the ProxySQL proxy and optionally return its + results as a set of rows.""" + proxy_connection = MySQLdb.connect("127.0.0.1", + ProxySQLBaseTest.PROXYSQL_RW_USERNAME, + ProxySQLBaseTest.PROXYSQL_RW_PASSWORD, + port=ProxySQLBaseTest.PROXYSQL_RW_PORT, + db=db) + cursor = proxy_connection.cursor() + cursor.execute(query) + if return_result: + rows = cursor.fetchall() + cursor.close() + proxy_connection.close() + if return_result: + return rows + + def run_query_mysql(self, query, db, return_result=True, hostgroup=0): + """Run a query against the MySQL backend and optionally return its + results as a set of rows. + + IMPORTANT: since the queries are actually ran against the MySQL backend, + that backend needs to expose its MySQL port to the outside through + docker compose's port mapping mechanism. + + This will actually parse the docker-compose configuration file to + retrieve the available backends and hostgroups and will pick a backend + from the specified hostgroup.""" + + # Figure out which are the containers for the specified hostgroup + mysql_backends = ProxySQLBaseTest._get_mysql_containers() + mysql_backends_in_hostgroup = [] + for backend in mysql_backends: + container_name = backend['Names'][0][1:].upper() + backend_hostgroup = ProxySQLBaseTest._extract_hostgroup_from_container_name(container_name) + + mysql_port_exposed=False + if not backend.get('Ports'): + continue + for exposed_port in backend.get('Ports', []): + if exposed_port['PrivatePort'] == 3306: + mysql_port_exposed = True + + if backend_hostgroup == hostgroup and mysql_port_exposed: + mysql_backends_in_hostgroup.append(backend) + + if len(mysql_backends_in_hostgroup) == 0: + raise Exception('No backends with a publicly exposed port were ' + 'found in hostgroup %d' % hostgroup) + + # Pick a random container, extract its connection details + container = random.choice(mysql_backends_in_hostgroup) + for exposed_port in container.get('Ports', []): + if exposed_port['PrivatePort'] == 3306: + mysql_port = exposed_port['PublicPort'] + + mysql_connection = MySQLdb.connect("127.0.0.1", + # Warning: this assumes that ProxySQL + # and all the backends have the same + # credentials. + # TODO(andrei): revisit this assumption + # in authentication tests. + ProxySQLBaseTest.PROXYSQL_RW_USERNAME, + ProxySQLBaseTest.PROXYSQL_RW_PASSWORD, + port=mysql_port, + db=db) + cursor = mysql_connection.cursor() + cursor.execute(query) + if return_result: + rows = cursor.fetchall() + cursor.close() + mysql_connection.close() + if return_result: + return rows \ No newline at end of file From e0388cd932803700077cfbfad83e7130d7c8a349 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Tue, 2 Jun 2015 14:22:50 +0300 Subject: [PATCH 19/54] #285 Document the methods of the ProxySQL base test class --- test/proxysql_base_test.py | 72 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 6d2768c59..4332d3a72 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -20,6 +20,12 @@ class ProxySQLBaseTest(TestCase): @classmethod def _startup_docker_services(cls): + """Start up all the docker services necessary to start this test. + + They are specified in the docker compose file specified in the variable + cls.DOCKER_COMPOSE_FILE. + """ + # We have to perform docker-compose build + docker-compose up, # instead of just doing the latter because of a bug which will give a # 500 internal error for the Docker bug. When this is fixed, we should @@ -29,11 +35,24 @@ class ProxySQLBaseTest(TestCase): @classmethod def _shutdown_docker_services(cls): + """Shut down all the docker services necessary to start this test. + + They are specified in the docker compose file specified in the variable + cls.DOCKER_COMPOSE_FILE. + """ + subprocess.call(["docker-compose", "stop"], cwd=cls.DOCKER_COMPOSE_FILE) subprocess.call(["docker-compose", "rm", "--force"], cwd=cls.DOCKER_COMPOSE_FILE) @classmethod def _get_proxysql_container(cls): + """Out of all the started docker containers, select the one which + represents the proxy instance. + + Note that this only supports one proxy instance for now. This method + relies on interogating the Docker daemon via its REST API. + """ + containers = Client(**kwargs_from_env()).containers() for container in containers: if 'proxysql' in container['Image']: @@ -41,6 +60,12 @@ class ProxySQLBaseTest(TestCase): @classmethod def _get_mysql_containers(cls): + """Out of all the started docker containers, select the ones which + represent the MySQL backend instances. + + This method relies on interogating the Docker daemon via its REST API. + """ + result = [] containers = Client(**kwargs_from_env()).containers() for container in containers: @@ -50,6 +75,13 @@ class ProxySQLBaseTest(TestCase): @classmethod def _populate_mysql_containers_with_dump(cls): + """Populates the started MySQL backend containers with the specified + SQL dump file. + + The reason for doing this __after__ the containers are started is + because we want to keep them as generic as possible. + """ + mysql_containers = cls._get_mysql_containers() # We have already added the SQL dump to the container by using # the ADD mysql command in the Dockerfile for mysql -- check it @@ -64,15 +96,33 @@ class ProxySQLBaseTest(TestCase): @classmethod def _extract_hostgroup_from_container_name(cls, container_name): + """MySQL backend containers are named using a naming convention: + backendXhostgroupY, where X and Y can be multi-digit numbers. + This extracts the value of the hostgroup from the container name. + + I made this choice because I wasn't able to find another easy way to + associate arbitrary metadata with a Docker container through the + docker compose file. + """ + service_name = container_name.split('_')[1] return int(re.search(r'BACKEND(\d+)HOSTGROUP(\d+)', service_name).group(2)) @classmethod def _extract_port_number_from_uri(cls, uri): + """Given a Docker container URI (exposed as an environment variable by + the host linking mechanism), extract the TCP port number from it.""" return int(uri.split(':')[2]) @classmethod def _get_environment_variables_from_container(cls, container_name): + """Retrieve the environment variables from the given container. + + This is useful because the host linking mechanism will expose + connectivity information to the linked hosts by the use of environment + variables. + """ + output = Client(**kwargs_from_env()).execute(container_name, 'env') result = {} lines = output.split('\n') @@ -86,6 +136,18 @@ class ProxySQLBaseTest(TestCase): @classmethod def _populate_proxy_configuration_with_backends(cls): + """Populate ProxySQL's admin information with the MySQL backends + and their associated hostgroups. + + This is needed because I do not want to hardcode this into the ProxySQL + config file of the test scenario, as it leaves more room for quick + iteration. + + In order to configure ProxySQL with the correct backends, we are using + the MySQL admin interface of ProxySQL, and inserting rows into the + `mysql_servers` table, which contains a list of which servers go into + which hostgroup. + """ proxysql_container = cls._get_proxysql_container() mysql_containers = cls._get_mysql_containers() environment_variables = cls._get_environment_variables_from_container( @@ -113,12 +175,16 @@ class ProxySQLBaseTest(TestCase): @classmethod def setUpClass(cls): + # Always shutdown docker services because the previous test might have + # left them in limbo. cls._shutdown_docker_services() + cls._startup_docker_services() - # TODO(andrei): figure out a more reliable method to wait for - # MySQL to start up within the container. Otherwise, there will be - # an error when we try to initialize the MySQL instance with the dump. + # Sleep for 30 seconds because we want to populate the MySQL containers + # with SQL dumps, but there is a race condition because we do not know + # when the MySQL daemons inside them have actually started or not. + # TODO(andrei): find a better solution time.sleep(30) cls._populate_mysql_containers_with_dump() From 8121f19dafcde86393700e83bc1680a7f37837b7 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Tue, 2 Jun 2015 14:23:15 +0300 Subject: [PATCH 20/54] #285 Simplify the example test to use the utility method that runs queries against the proxy --- test/one_backend_test.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/test/one_backend_test.py b/test/one_backend_test.py index 887aaf22f..3bd6379cd 100644 --- a/test/one_backend_test.py +++ b/test/one_backend_test.py @@ -7,15 +7,7 @@ class OneBackendTest(ProxySQLBaseTest): DOCKER_COMPOSE_FILE = "./docker/1backend" def test_select_strings_returns_correct_result(self): - proxy_connection = MySQLdb.connect("127.0.0.1", - ProxySQLBaseTest.PROXYSQL_RW_USERNAME, - ProxySQLBaseTest.PROXYSQL_RW_PASSWORD, - port=ProxySQLBaseTest.PROXYSQL_RW_PORT) - cursor = proxy_connection.cursor() - cursor.execute("USE test") - cursor.execute("SELECT * FROM strings") - rows = cursor.fetchall() + + rows = self.run_query_proxysql("SELECT * FROM strings", "test") self.assertEqual(set([row[0] for row in rows]), - set(['a', 'ab', 'abc', 'abcd'])) - cursor.close() - proxy_connection.close() \ No newline at end of file + set(['a', 'ab', 'abc', 'abcd'])) \ No newline at end of file From 020dd5b044f14762af9363ba1c593ce2a856603f Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 3 Jun 2015 13:15:42 +0300 Subject: [PATCH 21/54] #285 Move ProxySQL docker container to a "base" folder, from where it can be used "as is" --- docker/1backend/docker-compose.yml | 2 +- docker/{1backend => base}/proxysql/Dockerfile | 0 docker/{1backend => base}/proxysql/proxysql.cnf | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename docker/{1backend => base}/proxysql/Dockerfile (100%) rename docker/{1backend => base}/proxysql/proxysql.cnf (100%) diff --git a/docker/1backend/docker-compose.yml b/docker/1backend/docker-compose.yml index 98a8735ac..7a3bb3607 100644 --- a/docker/1backend/docker-compose.yml +++ b/docker/1backend/docker-compose.yml @@ -1,5 +1,5 @@ proxysql: - build: ./proxysql + build: ../base/proxysql links: - backend1hostgroup0 ports: diff --git a/docker/1backend/proxysql/Dockerfile b/docker/base/proxysql/Dockerfile similarity index 100% rename from docker/1backend/proxysql/Dockerfile rename to docker/base/proxysql/Dockerfile diff --git a/docker/1backend/proxysql/proxysql.cnf b/docker/base/proxysql/proxysql.cnf similarity index 100% rename from docker/1backend/proxysql/proxysql.cnf rename to docker/base/proxysql/proxysql.cnf From dc19cb9ff4b8bfe5353e34324507944554fdf735 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 3 Jun 2015 13:29:59 +0300 Subject: [PATCH 22/54] #285 Rename "docker" top-level folder to scenarios --- {docker => scenarios}/1backend/docker-compose.yml | 0 {docker => scenarios}/1backend/mysql/Dockerfile | 0 {docker => scenarios}/1backend/mysql/import_schema.sh | 0 {docker => scenarios}/1backend/mysql/schema.sql | 0 {docker => scenarios}/base/proxysql/Dockerfile | 0 {docker => scenarios}/base/proxysql/proxysql.cnf | 0 test/one_backend_test.py | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) rename {docker => scenarios}/1backend/docker-compose.yml (100%) rename {docker => scenarios}/1backend/mysql/Dockerfile (100%) rename {docker => scenarios}/1backend/mysql/import_schema.sh (100%) rename {docker => scenarios}/1backend/mysql/schema.sql (100%) rename {docker => scenarios}/base/proxysql/Dockerfile (100%) rename {docker => scenarios}/base/proxysql/proxysql.cnf (100%) diff --git a/docker/1backend/docker-compose.yml b/scenarios/1backend/docker-compose.yml similarity index 100% rename from docker/1backend/docker-compose.yml rename to scenarios/1backend/docker-compose.yml diff --git a/docker/1backend/mysql/Dockerfile b/scenarios/1backend/mysql/Dockerfile similarity index 100% rename from docker/1backend/mysql/Dockerfile rename to scenarios/1backend/mysql/Dockerfile diff --git a/docker/1backend/mysql/import_schema.sh b/scenarios/1backend/mysql/import_schema.sh similarity index 100% rename from docker/1backend/mysql/import_schema.sh rename to scenarios/1backend/mysql/import_schema.sh diff --git a/docker/1backend/mysql/schema.sql b/scenarios/1backend/mysql/schema.sql similarity index 100% rename from docker/1backend/mysql/schema.sql rename to scenarios/1backend/mysql/schema.sql diff --git a/docker/base/proxysql/Dockerfile b/scenarios/base/proxysql/Dockerfile similarity index 100% rename from docker/base/proxysql/Dockerfile rename to scenarios/base/proxysql/Dockerfile diff --git a/docker/base/proxysql/proxysql.cnf b/scenarios/base/proxysql/proxysql.cnf similarity index 100% rename from docker/base/proxysql/proxysql.cnf rename to scenarios/base/proxysql/proxysql.cnf diff --git a/test/one_backend_test.py b/test/one_backend_test.py index 3bd6379cd..1a3cae11e 100644 --- a/test/one_backend_test.py +++ b/test/one_backend_test.py @@ -4,7 +4,7 @@ from proxysql_base_test import ProxySQLBaseTest class OneBackendTest(ProxySQLBaseTest): - DOCKER_COMPOSE_FILE = "./docker/1backend" + DOCKER_COMPOSE_FILE = "./scenarios/1backend" def test_select_strings_returns_correct_result(self): From bd17e0a2fdb016051691bc27850fd126f5fd39c7 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 3 Jun 2015 13:30:10 +0300 Subject: [PATCH 23/54] #285 Add document that describes how to write a test --- test/how_to.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 test/how_to.md diff --git a/test/how_to.md b/test/how_to.md new file mode 100644 index 000000000..1af9899f8 --- /dev/null +++ b/test/how_to.md @@ -0,0 +1,46 @@ +# How are the tests built? + +First off, a few words about how the infrastructure of the tests looks like. + +Tests are written in Python, and the services needed for running a test +(a ProxySQL instance and one or more MySQL instances) are specified in a +docker-compose.yml file and are started by using Docker's docker-compose. + +Tests are ran using nosetests (https://nose.readthedocs.org/en/latest/), +Python's de facto leader in terms of how tests are written and ran. The command +to run the tests is, from the root of the repository: + +```python +nosetests --nocapture +``` + +The "--nocapture" flag is present in order to have detailed output on what is +going on. Otherwise, the output will be suppressed by nosetests to give you only +a high-level report of how many tests passed and failed. + +# Where can I find the tests? + +Tests are grouped in scenarios. A __scenario__ specifies a configuration of +ProxySQL and MySQL backends, together with initial data to populate the MySQL +instances (a text file containing SQL queries). + +The folder "scenarios" found in the root folder of the repository contains +these scenarios. There is a "base" folder with common utilities, and then there +is one folder for each scenario. For example, "1backend" is the name for the +scenario of 1 ProxySQL proxy, and 1 MySQL backend. + +To create such a scenario, the simplest way to go about, is to copy-paste the +"1backend" scenario and modify it. Some of the important things to modify: +- docker-compose.yml. This is where the list of services is described, and + where you actuall specify how many MySQL backends there are, and which ports + they expose and how. Be careful, there is a naming convention +- mysql/schema.sql. This is where the MySQL backends get populated + +# How do I write a test? + +It's pretty simple. Once you have a working scenario, you write a class in +the top-level "test" folder, which inherits from ProxySQLBaseTest. One such +example is one_backend_test.py. The only thing which you should specify is +the docker-compose filename, and then start querying both the proxy and the +MySQL backends and testing assertions by using the `run_query_proxysql` and +`run_query_mysql' class methods. \ No newline at end of file From 5dc21b2acb9ea7926963dfa935399fe92b669c60 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sun, 7 Jun 2015 22:59:47 +0300 Subject: [PATCH 24/54] #287 Add Vagrantfile that is able to provision a Vagrant container in order to run the tests --- .gitignore | 3 ++ test/Vagrantfile | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 test/Vagrantfile diff --git a/.gitignore b/.gitignore index 6ecf39937..cfd9df1cc 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ deps/libconfig/libconfig-1.4.9/ #re2 deps/re2/re2/ + +test/.vagrant +.DS_Store \ No newline at end of file diff --git a/test/Vagrantfile b/test/Vagrantfile new file mode 100644 index 000000000..c7c1c01bf --- /dev/null +++ b/test/Vagrantfile @@ -0,0 +1,85 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure(2) do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. + config.vm.box = "ubuntu-14.04" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + # config.vm.network "forwarded_port", guest: 80, host: 8080 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "../data", "/vagrant_data" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" + # end + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + config.vm.provision "shell", inline: <<-SHELL + sudo apt-get update + sudo apt-get install -y libmysqlclient-dev python python-dev wget + sudo wget -qO- https://bootstrap.pypa.io/ez_setup.py | python + sudo easy_install -U pip + cd /opt + sudo git clone https://github.com/aismail/proxysql-0.2.git proxysql + cd proxysql + sudo git checkout docker-black-box-tests + sudo pip install -r requirements.txt + SHELL + + config.vm.provision "shell", run: "always", inline: <<-SHELL + cd /opt/proxysql + sudo git pull origin docker-black-box-tests + sudo pip install -r requirements.txt + SHELL + +end From cdf463f81b5a8e231ffb28b1ce8d61fab11582c8 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sun, 7 Jun 2015 23:00:20 +0300 Subject: [PATCH 25/54] #287 Add instructions on how to run the tests w/ Vagrant --- test/how_to.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/how_to.md b/test/how_to.md index 1af9899f8..1581e0852 100644 --- a/test/how_to.md +++ b/test/how_to.md @@ -43,4 +43,15 @@ the top-level "test" folder, which inherits from ProxySQLBaseTest. One such example is one_backend_test.py. The only thing which you should specify is the docker-compose filename, and then start querying both the proxy and the MySQL backends and testing assertions by using the `run_query_proxysql` and -`run_query_mysql' class methods. \ No newline at end of file +`run_query_mysql' class methods. + +# How do I run the tests locally? + +1) install vagrant on the machine where you'll be running the tests + +2) vagrant box add ubuntu-14.04 ubuntu-14.04.box +(The ubuntu-14.04.box file is obtained from https://github.com/jose-lpa/packer-ubuntu_14.04/releases/download/v2.0/ubuntu-14.04.box) + +# This will actually install what is needed on the Vagrant box +3) cd proxysql/test; vagrant up; vagrant ssh -c "cd /opt/proxysql; nosetests --nocapture"; vagrant halt + From 045c68cf385f1c7c211c17c6a4c10ed7a69e4c30 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Tue, 9 Jun 2015 16:33:58 +0300 Subject: [PATCH 26/54] #287 Add documentation on how to run the tests without internet connectivity --- test/how_to.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/how_to.md b/test/how_to.md index 1581e0852..aa8089faa 100644 --- a/test/how_to.md +++ b/test/how_to.md @@ -55,3 +55,44 @@ MySQL backends and testing assertions by using the `run_query_proxysql` and # This will actually install what is needed on the Vagrant box 3) cd proxysql/test; vagrant up; vagrant ssh -c "cd /opt/proxysql; nosetests --nocapture"; vagrant halt +# How do I run the tests on a machine without internet connectivity? + +For that you need to prepare a Virtual box .box file with the latest state of +the code and the packages from a machine that has internet connectivity and +copy it over to the machine with no connectivity. + +To prepare the .box file: + +1) clone proxysql test repo locally, let's assume it's in ~/proxysql + +2) cd ~/proxysql/test; vagrant up + +This will update the machine with the latest master code. If you need to be +testing a different branch, you will have to do an extra step (step 3): + +3) vagrant ssh -c "cd /opt/proxysql/test; git checkout my-branch; git pull origin my-branch; sudo pip install -r requirements.txt" + +This will fetch the code for the given branch __and__ install the necessary +packages for running the tests on that branch (if there are any new packages). + +4) Package it all in a .box file + +vagrant package --output proxysql-tests.box + +This will generate a big .box file, approximately 1.1GB as of the writing of +this document. This file can be run without having internet connectivity. + +5) Copy the proxysql-tests.box to the machine where you want to run the tests + +6) vagrant box add proxysql-tests proxysql-tests.box (from the directory where +you copied the .box file and where you are planning to run the tests) + +7) vagrant init proxysql-tests; vagrant up + +8) vagrant up; vagrant ssh -c "cd /opt/proxysql; nosetests --nocapture"; vagrant halt + +to actually run the tests. + +NB: we are assuming that the only useful output from running the tests is +stdout. As we will add more tests to the test suite, this section will be +refined on how to retrieve the results as well. From de9c1f461614ab412c6900dbe1a037fe255abba4 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 10 Jun 2015 12:53:16 +0300 Subject: [PATCH 27/54] #287 Improve run_query_proxysql/run_query_mysql to allow username, password and port customization This was needed for authentication tests. --- test/proxysql_base_test.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 4332d3a72..08a751533 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -194,13 +194,17 @@ class ProxySQLBaseTest(TestCase): def tearDownClass(cls): cls._shutdown_docker_services() - def run_query_proxysql(self, query, db, return_result=True): + def run_query_proxysql(self, query, db, return_result=True, + username=None, password=None, port=None): """Run a query against the ProxySQL proxy and optionally return its results as a set of rows.""" + username = username or ProxySQLBaseTest.PROXYSQL_RW_USERNAME + password = password or ProxySQLBaseTest.PROXYSQL_RW_PASSWORD + port = port or ProxySQLBaseTest.PROXYSQL_RW_PORT proxy_connection = MySQLdb.connect("127.0.0.1", - ProxySQLBaseTest.PROXYSQL_RW_USERNAME, - ProxySQLBaseTest.PROXYSQL_RW_PASSWORD, - port=ProxySQLBaseTest.PROXYSQL_RW_PORT, + username, + password, + port=port, db=db) cursor = proxy_connection.cursor() cursor.execute(query) @@ -211,7 +215,8 @@ class ProxySQLBaseTest(TestCase): if return_result: return rows - def run_query_mysql(self, query, db, return_result=True, hostgroup=0): + def run_query_mysql(self, query, db, return_result=True, hostgroup=0, + username=None, password=None): """Run a query against the MySQL backend and optionally return its results as a set of rows. @@ -250,14 +255,11 @@ class ProxySQLBaseTest(TestCase): if exposed_port['PrivatePort'] == 3306: mysql_port = exposed_port['PublicPort'] + username = username or ProxySQLBaseTest.PROXYSQL_RW_USERNAME + password = password or ProxySQLBaseTest.PROXYSQL_RW_PASSWORD mysql_connection = MySQLdb.connect("127.0.0.1", - # Warning: this assumes that ProxySQL - # and all the backends have the same - # credentials. - # TODO(andrei): revisit this assumption - # in authentication tests. - ProxySQLBaseTest.PROXYSQL_RW_USERNAME, - ProxySQLBaseTest.PROXYSQL_RW_PASSWORD, + username, + password, port=mysql_port, db=db) cursor = mysql_connection.cursor() From 43c81e5fab3ecf6f77c840965b970e50629443c9 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 10 Jun 2015 12:53:39 +0300 Subject: [PATCH 28/54] #287 Add new users to the scenario setup --- scenarios/1backend/mysql/schema.sql | 8 +++++++- scenarios/base/proxysql/proxysql.cnf | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/scenarios/1backend/mysql/schema.sql b/scenarios/1backend/mysql/schema.sql index 19785d9af..419a12708 100644 --- a/scenarios/1backend/mysql/schema.sql +++ b/scenarios/1backend/mysql/schema.sql @@ -1,7 +1,13 @@ DROP DATABASE IF EXISTS test; - CREATE DATABASE test; +CREATE USER john@'%' IDENTIFIED BY 'doe'; +CREATE USER danny@'%' IDENTIFIED BY 'white'; + +GRANT ALL PRIVILEGES ON test.* TO 'john'@'%'; +GRANT ALL PRIVILEGES ON test.* TO 'danny'@'%'; +FLUSH PRIVILEGES; + USE test; CREATE TABLE strings(value LONGTEXT); diff --git a/scenarios/base/proxysql/proxysql.cnf b/scenarios/base/proxysql/proxysql.cnf index b50345d01..75ab20289 100644 --- a/scenarios/base/proxysql/proxysql.cnf +++ b/scenarios/base/proxysql/proxysql.cnf @@ -14,5 +14,11 @@ mysql_users = username = "root" password = "root" default_hostgroup = 0 + }, + + { + username = "john" + password = "doe" + default_hostgroup = 0 } ) \ No newline at end of file From a7695d9cd0a43e93ffecfae4ece8c2ac89932702 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 10 Jun 2015 12:53:54 +0300 Subject: [PATCH 29/54] #287 Add a test that authentication against the proxy works correctly --- test/authentication_test.py | 43 +++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 test/authentication_test.py diff --git a/test/authentication_test.py b/test/authentication_test.py new file mode 100644 index 000000000..53394fbf7 --- /dev/null +++ b/test/authentication_test.py @@ -0,0 +1,43 @@ +import MySQLdb +from MySQLdb import OperationalError +from nose.tools import raises + +from proxysql_base_test import ProxySQLBaseTest + +class AuthenticationTest(ProxySQLBaseTest): + + DOCKER_COMPOSE_FILE = "./scenarios/1backend" + + def test_existing_user_with_correct_password_works(self): + version1 = self.run_query_mysql( + "SELECT @@version_comment LIMIT 1", "test", + return_result=True, + username="john", password="doe") + + version2 = self.run_query_proxysql( + "SELECT @@version_comment LIMIT 1", "test", + return_result=True, + username="john", password="doe") + + self.assertEqual(version1, version2) + + @raises(OperationalError) + def test_existing_user_with_correct_password_but_not_registerd_within_proxysql_does_not_work(self): + version1 = self.run_query_proxysql( + "SELECT @@version_comment LIMIT 1", "test", + return_result=True, + username="danny", password="white") + + @raises(OperationalError) + def test_existing_user_with_incorrect_password_does_not_work(self): + version = self.run_query_proxysql( + "SELECT @@version_comment LIMIT 1", "test", + return_result=True, + username="john", password="doe2") + + @raises(OperationalError) + def test_inexisting_user_with_random_password_does_not_work(self): + version = self.run_query_proxysql( + "SELECT @@version_comment LIMIT 1", "test", + return_result=True, + username="johnny", password="randomdoe") \ No newline at end of file From f684dd35967fc080a4bcca0773d892679f213ebf Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Thu, 11 Jun 2015 13:18:21 +0300 Subject: [PATCH 30/54] #290 Start ignoring Python binaries --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cfd9df1cc..20d0e3261 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.o *.ko *.oo +*.pyc # Libraries *.lib From b99ebd31c2f3c6a9dcc645704ea948e3f8a27fd7 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Thu, 11 Jun 2015 13:20:14 +0300 Subject: [PATCH 31/54] #290 Install sysbench from sources in the Docker image for ProxySQL We need this because we want the .lua files for the test scenarios. Also, I put it on the ProxySQL instance because the alternative would have been to put it in the Vagrant machine that encapsulates the tests. There were 2 disadvantages for that: * you could not easily run the tests locally unless you were also using Vagrant * sometimes there are connectivity problems (due to the way in which Docker forwards ports) which makes certain MySQL clients not work --- scenarios/base/proxysql/Dockerfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scenarios/base/proxysql/Dockerfile b/scenarios/base/proxysql/Dockerfile index c4514f6ee..553eecad2 100644 --- a/scenarios/base/proxysql/Dockerfile +++ b/scenarios/base/proxysql/Dockerfile @@ -3,13 +3,18 @@ FROM ubuntu:14.04 MAINTAINER Andrei Ismail RUN apt-get update && apt-get install -y\ + automake\ cmake\ make\ g++\ gcc\ git\ + libmysqlclient-dev\ libssl-dev\ - libmysqlclient-dev + libtool + +RUN cd /opt; git clone https://github.com/akopytov/sysbench.git +RUN cd /opt/sysbench; ./autogen.sh; ./configure --bindir=/usr/bin; make; make install RUN cd /opt; git clone https://github.com/sysown/proxysql-0.2.git proxysql RUN cd /opt/proxysql; make clean && make From d754221a7ee7498b6f313edbb55ace7602196d63 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Thu, 11 Jun 2015 13:30:18 +0300 Subject: [PATCH 32/54] #290 Add primitive for running a customizable sysbench test Note that it will always "prepare" the test before running it, as the Docker container will not contain the databases. The cleanup phase is not mandatory, but we do it for good practice :) --- test/proxysql_base_test.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 08a751533..fd511b0d0 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -269,4 +269,35 @@ class ProxySQLBaseTest(TestCase): cursor.close() mysql_connection.close() if return_result: - return rows \ No newline at end of file + return rows + + def run_sysbench_proxysql(self, threads=4, time=60, db="test", + username=None, password=None, port=None): + proxysql_container_id = ProxySQLBaseTest._get_proxysql_container()['Id'] + username = username or ProxySQLBaseTest.PROXYSQL_RW_USERNAME + password = password or ProxySQLBaseTest.PROXYSQL_RW_PASSWORD + port = port or ProxySQLBaseTest.PROXYSQL_RW_PORT + + params = [ + "docker", "exec", proxysql_container_id, + "sysbench", + "--test=/opt/sysbench/sysbench/tests/db/oltp.lua", + "--num-threads=%d" % threads, + "--max-requests=0", + "--max-time=%d" % time, + "--mysql-user=%s" % username, + "--mysql-password=%s" % password, + "--mysql-db=%s" % db, + "--db-driver=mysql", + "--oltp-tables-count=4", + "--oltp-read-only=on", + "--oltp-skip-trx=on", + "--report-interval=1", + "--oltp-point-selects=100", + "--oltp-table-size=400000", + "--mysql-host=127.0.0.1", + "--mysql-port=%s" % port + ] + subprocess.call(params + ["prepare"]) + subprocess.call(params + ["run"]) + subprocess.call(params + ["cleanup"]) \ No newline at end of file From aee79149755481a23119b3195650e15c104b45a5 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Thu, 11 Jun 2015 13:46:42 +0300 Subject: [PATCH 33/54] #290 Added example test that uses sysbench --- test/sysbench_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 test/sysbench_test.py diff --git a/test/sysbench_test.py b/test/sysbench_test.py new file mode 100644 index 000000000..a6c883e03 --- /dev/null +++ b/test/sysbench_test.py @@ -0,0 +1,8 @@ +from proxysql_base_test import ProxySQLBaseTest + +class SysBenchTest(ProxySQLBaseTest): + + DOCKER_COMPOSE_FILE = "./scenarios/1backend" + + def test_proxy_doesnt_crash_under_mild_sysbench_load(self): + self.run_sysbench_proxysql() \ No newline at end of file From 10ddc1a8d016a2af8e0c3faa1b0f9ae8a3d80f20 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 17 Jun 2015 13:56:46 +0300 Subject: [PATCH 34/54] #291 Run proxysql container in privileged mode so that apparmor allows gdbserver to run correctly --- scenarios/1backend/docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/scenarios/1backend/docker-compose.yml b/scenarios/1backend/docker-compose.yml index 7a3bb3607..64f037c14 100644 --- a/scenarios/1backend/docker-compose.yml +++ b/scenarios/1backend/docker-compose.yml @@ -5,6 +5,7 @@ proxysql: ports: - "6032:6032" - "6033:6033" + privileged: true backend1hostgroup0: build: ./mysql From 555da2f9178d7b40cb19a9220568e5eb5b5912f1 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Wed, 17 Jun 2015 13:57:28 +0300 Subject: [PATCH 35/54] #291 Expose gdbserver port, in case it runs This is used for remote debugging - connecting to the crashed ProxySQL instaince in order to debug the problem, whenever it crashes --- scenarios/1backend/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scenarios/1backend/docker-compose.yml b/scenarios/1backend/docker-compose.yml index 64f037c14..df1bde6e0 100644 --- a/scenarios/1backend/docker-compose.yml +++ b/scenarios/1backend/docker-compose.yml @@ -5,6 +5,8 @@ proxysql: ports: - "6032:6032" - "6033:6033" + # gdbserver + - "2345:2345" privileged: true backend1hostgroup0: From 8d66e621acc03d8fde91b42da5a73fbf2a08953a Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:01:08 +0200 Subject: [PATCH 36/54] #291 Add descriptions of exposed ports in docker compose file --- scenarios/1backend/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scenarios/1backend/docker-compose.yml b/scenarios/1backend/docker-compose.yml index df1bde6e0..52c3528e6 100644 --- a/scenarios/1backend/docker-compose.yml +++ b/scenarios/1backend/docker-compose.yml @@ -3,7 +3,9 @@ proxysql: links: - backend1hostgroup0 ports: + # ProxySQL admin port for MySQL commands - "6032:6032" + # ProxySQL main port - "6033:6033" # gdbserver - "2345:2345" From 2d60a08b3d88a0ddd8ec21f0f2d5c80fff604ead Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:02:31 +0200 Subject: [PATCH 37/54] #291 Always run ProxySQL with gdbserver The purpose is to drop the user running the tests into an interactive console to debug a crashed ProxySQL test. --- scenarios/base/proxysql/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scenarios/base/proxysql/Dockerfile b/scenarios/base/proxysql/Dockerfile index 553eecad2..748b17b61 100644 --- a/scenarios/base/proxysql/Dockerfile +++ b/scenarios/base/proxysql/Dockerfile @@ -8,6 +8,8 @@ RUN apt-get update && apt-get install -y\ make\ g++\ gcc\ + gdb\ + gdbserver\ git\ libmysqlclient-dev\ libssl-dev\ @@ -22,4 +24,4 @@ RUN mkdir -p /var/run/proxysql ADD ./proxysql.cnf /etc/ WORKDIR /opt/proxysql/src -CMD ["/opt/proxysql/src/proxysql", "--initial"] \ No newline at end of file +CMD ["gdbserver", "0.0.0.0:2345", "/opt/proxysql/src/proxysql", "--initial"] \ No newline at end of file From 4edb0f98675712c09f973534390a5f2c215887fc Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:03:24 +0200 Subject: [PATCH 38/54] #291 Add API for running a ProxySQL admin command in SQL This is ran against the SQL admin port of the proxy. --- src/gdb-commands.txt | 3 +++ test/proxysql_base_test.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/gdb-commands.txt diff --git a/src/gdb-commands.txt b/src/gdb-commands.txt new file mode 100644 index 000000000..ea4d2ac3c --- /dev/null +++ b/src/gdb-commands.txt @@ -0,0 +1,3 @@ +set pagination off +target remote 0.0.0.0:2345 +continue \ No newline at end of file diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index fd511b0d0..555eb12ae 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -215,6 +215,28 @@ class ProxySQLBaseTest(TestCase): if return_result: return rows + def run_query_proxysql_admin(self, query, return_result=True): + """Run a query against the ProxySQL admin. + + Note: we do not need to specify a db for this query, as it's always + against the "main" database. + TODO(andrei): revisit db assumption once stats databases from ProxySQL + are accessible via the MySQL interface. + """ + + return self.run_query_proxysql( + query, + # "main" database is hardcoded within the + # ProxySQL admin -- it contains the SQLite3 + # tables with metadata about servers and users + "main", + return_result, + username=ProxySQLBaseTest.PROXYSQL_ADMIN_USERNAME, + password=ProxySQLBaseTest.PROXYSQL_ADMIN_PASSWORD, + port=ProxySQLBaseTest.PROXYSQL_ADMIN_PORT + ) + + def run_query_mysql(self, query, db, return_result=True, hostgroup=0, username=None, password=None): """Run a query against the MySQL backend and optionally return its From d242070afae762019dc195086da192ab7913e9b9 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:06:48 +0200 Subject: [PATCH 39/54] #291 Implement API for running a bash command within the ProxySQL container The first application is for the sysbench test. --- test/proxysql_base_test.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 555eb12ae..fcfe0e58f 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -301,7 +301,6 @@ class ProxySQLBaseTest(TestCase): port = port or ProxySQLBaseTest.PROXYSQL_RW_PORT params = [ - "docker", "exec", proxysql_container_id, "sysbench", "--test=/opt/sysbench/sysbench/tests/db/oltp.lua", "--num-threads=%d" % threads, @@ -320,6 +319,22 @@ class ProxySQLBaseTest(TestCase): "--mysql-host=127.0.0.1", "--mysql-port=%s" % port ] - subprocess.call(params + ["prepare"]) - subprocess.call(params + ["run"]) - subprocess.call(params + ["cleanup"]) \ No newline at end of file + + ProxySQLBaseTest.run_bash_command_within_proxysql(params + ["prepare"]) + ProxySQLBaseTest.run_bash_command_within_proxysql(params + ["run"]) + ProxySQLBaseTest.run_bash_command_within_proxysql(params + ["cleanup"]) + + @classmethod + def run_bash_command_within_proxysql(cls, params): + """Run a bash command given as an array of tokens within the ProxySQL + container. + + This is useful in a lot of scenarios: + - running sysbench against the ProxySQL instance + - getting environment variables from the ProxySQL container + - running various debugging commands against the ProxySQL instance + """ + + proxysql_container_id = ProxySQLBaseTest._get_proxysql_container()['Id'] + exec_params = ["docker", "exec", proxysql_container_id] + params + subprocess.call(exec_params) From db370c8db11d85ea59683c6f0518d2fbcb0859da Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:08:28 +0200 Subject: [PATCH 40/54] #291 Describe API for running a sysbench test harness against the ProxySQL instance --- test/proxysql_base_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index fcfe0e58f..cbc3d87aa 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -295,6 +295,14 @@ class ProxySQLBaseTest(TestCase): def run_sysbench_proxysql(self, threads=4, time=60, db="test", username=None, password=None, port=None): + """Runs a sysbench test with the given parameters against the given + ProxySQL instance. + + In this case, due to better encapsulation and reduced latency to + ProxySQL, we are assuming that sysbench is installed on the same + container with it. + """ + proxysql_container_id = ProxySQLBaseTest._get_proxysql_container()['Id'] username = username or ProxySQLBaseTest.PROXYSQL_RW_USERNAME password = password or ProxySQLBaseTest.PROXYSQL_RW_PASSWORD From eab4a548d6d0049875b21ed36d884cdb533ee310 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:09:22 +0200 Subject: [PATCH 41/54] #291 Add API for compiling ProxySQL binary on the Docker host This is required for the interactive mode, where gdb is ran from the docker host with the debug symbols accessible, and connects to gdbserver within the ProxySQL container. --- test/proxysql_base_test.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index cbc3d87aa..406f46741 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -346,3 +346,17 @@ class ProxySQLBaseTest(TestCase): proxysql_container_id = ProxySQLBaseTest._get_proxysql_container()['Id'] exec_params = ["docker", "exec", proxysql_container_id] + params subprocess.call(exec_params) + + @classmethod + def _compile_host_proxysql(cls): + """Compile ProxySQL on the Docker host from which we're running the + tests. + + This is used for remote debugging, because that's how the + gdb + gdbserver pair works: + - local gdb with access to the binary with debug symbols + - remote gdbserver which wraps the remote binary so that it can be + debugged when it crashes. + """ + subprocess.call(["make", "clean"]) + subprocess.call(["make"]) From 514f59da03c0a80eef8bed6a4bb2eab1b9f41752 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:13:44 +0200 Subject: [PATCH 42/54] #291 Add API for running gdb on docker host that connects to gdbserver within ProxySQL docker container --- test/proxysql_base_test.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 406f46741..a0653e031 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -360,3 +360,21 @@ class ProxySQLBaseTest(TestCase): """ subprocess.call(["make", "clean"]) subprocess.call(["make"]) + + @classmethod + def _connect_gdb_to_proxysql_within_container(cls): + """Connect a local gdb running on the docker host to the remote + ProxySQL binary for remote debugging. + + This is useful in interactive mode, where we want to stop at a failing + test and prompt the developer to debug the failing instance. + + Note: gdb is ran in a separate process because otherwise it will block + the test running process, and it will not be able to run queries anymore + and make assertions. However, we save the process handle so that we can + shut down the process later on. + """ + + cls._gdb_process = subprocess.Popen(["gdb", "--command=gdb-commands.txt", + "./proxysql"], + cwd="./src") \ No newline at end of file From 5fb5da9c827bf4ecec20742f46d5a48c4e1a4a03 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:14:28 +0200 Subject: [PATCH 43/54] #291 Add flag that allows flags to be interactive or non-interactive For now, all tests will be interactive. There are several reasons for wanting tests to be non-interactive as well: produce only a report of failure or success for example. --- test/proxysql_base_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index a0653e031..78f132bca 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -17,6 +17,9 @@ class ProxySQLBaseTest(TestCase): PROXYSQL_RW_PORT = 6033 PROXYSQL_RW_USERNAME = "root" PROXYSQL_RW_PASSWORD = "root" + # TODO(andrei): make it possible to set this to False, and make False + # the default value. + INTERACTIVE_TEST = True @classmethod def _startup_docker_services(cls): @@ -181,6 +184,10 @@ class ProxySQLBaseTest(TestCase): cls._startup_docker_services() + if cls.INTERACTIVE_TEST: + cls._compile_host_proxysql() + cls._connect_gdb_to_proxysql_within_container() + # Sleep for 30 seconds because we want to populate the MySQL containers # with SQL dumps, but there is a race condition because we do not know # when the MySQL daemons inside them have actually started or not. @@ -192,6 +199,10 @@ class ProxySQLBaseTest(TestCase): @classmethod def tearDownClass(cls): + if cls.INTERACTIVE_TEST: + # TODO(andrei): find better solution like wait with timeout + + # terminate afterwards + cls._gdb_process.terminate() cls._shutdown_docker_services() def run_query_proxysql(self, query, db, return_result=True, From f907c8fc5052547b0262900e342f94322d3ccabb Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sat, 20 Jun 2015 12:14:43 +0200 Subject: [PATCH 44/54] #291 Add test that tests the PROXYSQL STOP command --- test/admin_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/admin_test.py diff --git a/test/admin_test.py b/test/admin_test.py new file mode 100644 index 000000000..bd985b8dd --- /dev/null +++ b/test/admin_test.py @@ -0,0 +1,9 @@ +from proxysql_base_test import ProxySQLBaseTest + +class AdminTest(ProxySQLBaseTest): + + DOCKER_COMPOSE_FILE = "./scenarios/1backend" + + def test_stop_main_thread(self): + # This test will just assert that PROXYSQL STOP works correctly + self.run_query_proxysql_admin("PROXYSQL STOP") \ No newline at end of file From a94bf2d04e77a336dfdf4045da98af48dcc47efe Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sun, 21 Jun 2015 10:38:09 +0200 Subject: [PATCH 45/54] #292 Re-ordering required Python packages by name --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 592c34ed3..b6381fac9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -nose==1.3.6 docker-compose==1.1.0 -requests==2.4.3 -MySQL-python==1.2.5 \ No newline at end of file +MySQL-python==1.2.5 +nose==1.3.6 +requests==2.4.3 \ No newline at end of file From b8b36b29b249d8e6d7418c7f0f3db7244222af9c Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sun, 21 Jun 2015 10:38:50 +0200 Subject: [PATCH 46/54] #292 Add thread class that continuously pings a ProxySQL instance and reports failures to respond via e-mail --- test/proxysql_ping_thread.py | 77 ++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/proxysql_ping_thread.py diff --git a/test/proxysql_ping_thread.py b/test/proxysql_ping_thread.py new file mode 100644 index 000000000..2d7813b24 --- /dev/null +++ b/test/proxysql_ping_thread.py @@ -0,0 +1,77 @@ +from email.mime.text import MIMEText +import smtplib +from threading import Thread +import time + +import MySQLdb + +class ProxySQL_Ping_Thread(Thread): + """ProxySQL_Ping_Thread's purpose is to do a continuous health check of the + ProxySQL daemon when tests are running against it. When it has crashed + or it's simply not responding anymore, it will send an e-mail to draw the + attention of the developer so that he or she will examine the situation. + + This is because the test suite is designed to be long running and we want + to find out as quickly as possible when the tests ran into trouble without + continuously keeping an eye on the tests. + """ + + def __init__(self, username, password, + hostname="127.0.0.1", port=6033, db="test", + ping_command="SELECT @@version_comment LIMIT 1", + interval=60, **kwargs): + self.username = username + self.password = password + self.hostname = hostname + self.port = port + self.db = db + self.ping_command = ping_command + self.interval = interval + self.running = False + super(ProxySQL_Ping_Thread, self).__init__(**kwargs) + + def run(self): + self.running = True + + while self.running: + time.sleep(self.interval) + + try: + connection = MySQLdb.connect(self.hostname, + self.username, + self.password, + port=self.port, + db=self.db) + cursor = connection.cursor() + cursor.execute(self.ping_command) + rows = cursor.fetchall() + cursor.close() + connection.close() + print("ProxySQL server @ %s:%d responded to query %s with %r" % + (self.hostname, self.port, self.ping_command, rows)) + except: + self.send_error_email() + self.running = False + + def stop(self): + self.running = False + + def send_error_email(self): + msg = MIMEText("ProxySQL daemon stopped responding during tests.\n" + "Please check if it has crashed and you have been left with a gdb console on!") + + # me == the sender's email address + # you == the recipient's email address + msg['Subject'] = 'Daemon has stopped responding' + msg['From'] = 'ProxySQL Tests ' + msg['To'] = 'Andrei-Adnan Ismail ' + + # Send the message via our own SMTP server, but don't include the + # envelope header. + s = smtplib.SMTP('smtp.gmail.com', 587) + s.ehlo() + s.starttls() + s.login('proxysql.tests', 'pr0xysql') + s.sendmail('proxysql.tests@gmail.com', ['iandrei@gmail.com'], msg.as_string()) + s.quit() + From 5f5b639249d8e02ca5522fc780a47f45cb8ecce4 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Sun, 21 Jun 2015 10:41:05 +0200 Subject: [PATCH 47/54] #292 Integrate ProxySQL crash detector thread into test suite --- test/proxysql_base_test.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 78f132bca..db70fcd16 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -8,6 +8,8 @@ from docker import Client from docker.utils import kwargs_from_env import MySQLdb +from proxysql_ping_thread import ProxySQL_Ping_Thread + class ProxySQLBaseTest(TestCase): DOCKER_COMPOSE_FILE = None @@ -193,12 +195,14 @@ class ProxySQLBaseTest(TestCase): # when the MySQL daemons inside them have actually started or not. # TODO(andrei): find a better solution time.sleep(30) - cls._populate_mysql_containers_with_dump() + cls._populate_mysql_containers_with_dump() cls._populate_proxy_configuration_with_backends() + cls._start_proxysql_pings() @classmethod def tearDownClass(cls): + cls._stop_proxysql_pings() if cls.INTERACTIVE_TEST: # TODO(andrei): find better solution like wait with timeout + # terminate afterwards @@ -388,4 +392,23 @@ class ProxySQLBaseTest(TestCase): cls._gdb_process = subprocess.Popen(["gdb", "--command=gdb-commands.txt", "./proxysql"], - cwd="./src") \ No newline at end of file + cwd="./src") + + @classmethod + def _start_proxysql_pings(cls): + """During the running of the tests, the test suite will continuously + monitor the ProxySQL daemon in order to check that it's up. + + This special thread will do exactly that.""" + + cls.ping_thread = ProxySQL_Ping_Thread(username=cls.PROXYSQL_RW_USERNAME, + password=cls.PROXYSQL_RW_PASSWORD, + hostname="127.0.0.1", + port=cls.PROXYSQL_RW_PORT) + cls.ping_thread.start() + + @classmethod + def _stop_proxysql_pings(cls): + """Stop the special thread which pings the ProxySQL daemon.""" + cls.ping_thread.stop() + cls.ping_thread.join() \ No newline at end of file From 138a2d27df1a0fd6c3fa08e8f4fc704a602694d0 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Mon, 22 Jun 2015 11:20:10 +0200 Subject: [PATCH 48/54] #292 gdb process needs to be wait()'ed or otherwise we won't have any way of keeping the crashed process in debug mode until someone works on it --- test/proxysql_base_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index db70fcd16..0bb4bef83 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -204,9 +204,7 @@ class ProxySQLBaseTest(TestCase): def tearDownClass(cls): cls._stop_proxysql_pings() if cls.INTERACTIVE_TEST: - # TODO(andrei): find better solution like wait with timeout + - # terminate afterwards - cls._gdb_process.terminate() + cls._gdb_process.wait() cls._shutdown_docker_services() def run_query_proxysql(self, query, db, return_result=True, From b671a35379adbdbfec65e086b36fb22a41153d86 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Mon, 22 Jun 2015 11:21:01 +0200 Subject: [PATCH 49/54] #292 Ping thread needs to connect to MySQL using timeout in order to be able to throw an exception when the proxy isn't responding anymore --- test/proxysql_ping_thread.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/proxysql_ping_thread.py b/test/proxysql_ping_thread.py index 2d7813b24..57361a57d 100644 --- a/test/proxysql_ping_thread.py +++ b/test/proxysql_ping_thread.py @@ -41,7 +41,8 @@ class ProxySQL_Ping_Thread(Thread): self.username, self.password, port=self.port, - db=self.db) + db=self.db, + connect_timeout=30) cursor = connection.cursor() cursor.execute(self.ping_command) rows = cursor.fetchall() From 50783fd237cbaa95da7eb7dff3793e6ce61ed6e7 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Mon, 22 Jun 2015 11:21:50 +0200 Subject: [PATCH 50/54] #292 Sending warning e-mail after 3 consecutive crashed connection attempts --- test/proxysql_ping_thread.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/proxysql_ping_thread.py b/test/proxysql_ping_thread.py index 57361a57d..060361670 100644 --- a/test/proxysql_ping_thread.py +++ b/test/proxysql_ping_thread.py @@ -16,6 +16,8 @@ class ProxySQL_Ping_Thread(Thread): continuously keeping an eye on the tests. """ + FAILED_CONNECTIONS_BEFORE_ALERT = 3 + def __init__(self, username, password, hostname="127.0.0.1", port=6033, db="test", ping_command="SELECT @@version_comment LIMIT 1", @@ -28,6 +30,7 @@ class ProxySQL_Ping_Thread(Thread): self.ping_command = ping_command self.interval = interval self.running = False + self.failed_connections = 0 super(ProxySQL_Ping_Thread, self).__init__(**kwargs) def run(self): @@ -50,9 +53,12 @@ class ProxySQL_Ping_Thread(Thread): connection.close() print("ProxySQL server @ %s:%d responded to query %s with %r" % (self.hostname, self.port, self.ping_command, rows)) + self.failed_connections = 0 except: - self.send_error_email() - self.running = False + self.failed_connections = self.failed_connections + 1 + if self.failed_connections == ProxySQL_Ping_Thread.FAILED_CONNECTIONS_BEFORE_ALERT: + self.send_error_email() + self.running = False def stop(self): self.running = False From 7986544808d10db6da8515ce7d9a22427859b94b Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Mon, 22 Jun 2015 11:51:52 +0200 Subject: [PATCH 51/54] #292 Stopping ProxySQL pings __after__ gdb has finished running Otherwise, it won't have the chance to ping the server and detect it's waiting for GDB to finish (and interact with the user) while the proxy process is crashed. --- test/proxysql_base_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/proxysql_base_test.py b/test/proxysql_base_test.py index 0bb4bef83..4e0f7ece3 100644 --- a/test/proxysql_base_test.py +++ b/test/proxysql_base_test.py @@ -202,9 +202,13 @@ class ProxySQLBaseTest(TestCase): @classmethod def tearDownClass(cls): - cls._stop_proxysql_pings() if cls.INTERACTIVE_TEST: cls._gdb_process.wait() + # It's essential that pings are stopped __after__ the gdb process has + # finished. This allows them to keep pinging ProxySQL in the background + # while it's stuck waiting for user interaction (user interaction needed + # in order to debug the problem causing it to crash). + cls._stop_proxysql_pings() cls._shutdown_docker_services() def run_query_proxysql(self, query, db, return_result=True, From 2c7a12d35d620e3b0bda0c9f64a3ea7f70484324 Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Mon, 22 Jun 2015 11:53:40 +0200 Subject: [PATCH 52/54] #292 Make sure that the ping thread finishes faster when not running anymore This also minimizeze the chance of a failed connection attempt after it has been instructed to shut down. --- test/proxysql_ping_thread.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/proxysql_ping_thread.py b/test/proxysql_ping_thread.py index 060361670..78907e4fc 100644 --- a/test/proxysql_ping_thread.py +++ b/test/proxysql_ping_thread.py @@ -39,6 +39,9 @@ class ProxySQL_Ping_Thread(Thread): while self.running: time.sleep(self.interval) + if not self.running: + return + try: connection = MySQLdb.connect(self.hostname, self.username, From 1d838dab3a28d21413c494517ccfafd0c90ecc2c Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Mon, 22 Jun 2015 11:54:19 +0200 Subject: [PATCH 53/54] #292 Relax the condition for sending the error e-mail #defensivecoding --- test/proxysql_ping_thread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/proxysql_ping_thread.py b/test/proxysql_ping_thread.py index 78907e4fc..dc3a37e00 100644 --- a/test/proxysql_ping_thread.py +++ b/test/proxysql_ping_thread.py @@ -59,7 +59,7 @@ class ProxySQL_Ping_Thread(Thread): self.failed_connections = 0 except: self.failed_connections = self.failed_connections + 1 - if self.failed_connections == ProxySQL_Ping_Thread.FAILED_CONNECTIONS_BEFORE_ALERT: + if self.failed_connections >= ProxySQL_Ping_Thread.FAILED_CONNECTIONS_BEFORE_ALERT: self.send_error_email() self.running = False From e464c7c1dfe96896ec43b612e468bf777ea56d7d Mon Sep 17 00:00:00 2001 From: Andrei Ismail Date: Mon, 22 Jun 2015 11:54:42 +0200 Subject: [PATCH 54/54] #292 Don't rely on going to the loop again to exit -- exit on the spot when too many failed connection attempts have been detected --- test/proxysql_ping_thread.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/proxysql_ping_thread.py b/test/proxysql_ping_thread.py index dc3a37e00..8a09f40a6 100644 --- a/test/proxysql_ping_thread.py +++ b/test/proxysql_ping_thread.py @@ -62,6 +62,7 @@ class ProxySQL_Ping_Thread(Thread): if self.failed_connections >= ProxySQL_Ping_Thread.FAILED_CONNECTIONS_BEFORE_ALERT: self.send_error_email() self.running = False + return def stop(self): self.running = False