summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/drivers.rst19
-rw-r--r--doc/source/tutorial/index.rst1
-rw-r--r--doc/source/tutorial/lock.rst14
-rw-r--r--examples/coordinator.py2
-rw-r--r--examples/coordinator_heartbeat.py2
-rw-r--r--examples/group_membership.py2
-rw-r--r--examples/group_membership_watch.py2
-rw-r--r--examples/leader_election.py2
-rw-r--r--examples/lock.py11
-rwxr-xr-xsetup-mysql-env.sh37
-rw-r--r--setup.cfg1
-rw-r--r--test-requirements.txt2
-rw-r--r--tooz/drivers/memcached.py3
-rw-r--r--tooz/drivers/mysql.py126
-rw-r--r--tooz/drivers/pgsql.py83
-rw-r--r--tooz/tests/test_coordination.py22
-rw-r--r--tooz/tests/test_postgresql.py114
-rw-r--r--tox.ini35
18 files changed, 429 insertions, 49 deletions
diff --git a/doc/source/drivers.rst b/doc/source/drivers.rst
index 4f73f61..4c6a560 100644
--- a/doc/source/drivers.rst
+++ b/doc/source/drivers.rst
@@ -10,17 +10,30 @@ API, some of them have different properties:
features as it's possible to build a cluster of ZooKeeper that is
resilient towards network partitions for example.
-* `memcached`_ is a basic implementation and provides less resiliency, though
- it's much simpler to setup. A lot of the features provided in tooz are
- based on timeout (heartbeats, locks, etc) so are less resilient than other
+* `memcached`_ is a basic implementation and provides little resiliency, though
+ it's much simpler to setup. A lot of the features provided in tooz are based
+ on timeout (heartbeats, locks, etc) so are less resilient than other
backends.
+* `redis`_ is a basic implementation and provides little resiliency.
+ A lot of the features provided in tooz are based on timeout (heartbeats,
+ locks, etc) so are less resilient than other backends.
+
* `ipc` is based on Posix IPC and only implements a lock mechanism for now.
The lock can only be distributed locally to a computer processes.
* `zake`_ is a driver using a fake implementation of ZooKeeper and can be
used to use Tooz in your unit tests suite for example.
+* `postgresql`_ is a driver providing only distributed lock (for now)
+ and based on the PostgreSQL database server.
+
+* `mysql`_ is a driver providing only distributed lock (for now)
+ and based on the MySQL database server.
+
.. _zookeeper: http://zookeeper.apache.org/
.. _memcached: http://memcached.org/
.. _zake: https://pypi.python.org/pypi/zake
+.. _redis: http://redis.io
+.. _postgresql: http://postgresql.org
+.. _mysql: http://mysql.org
diff --git a/doc/source/tutorial/index.rst b/doc/source/tutorial/index.rst
index e83c0fc..8cb31a3 100644
--- a/doc/source/tutorial/index.rst
+++ b/doc/source/tutorial/index.rst
@@ -11,3 +11,4 @@ use tooz in your application.
coordinator
group_membership
leader_election
+ lock
diff --git a/doc/source/tutorial/lock.rst b/doc/source/tutorial/lock.rst
new file mode 100644
index 0000000..91c3262
--- /dev/null
+++ b/doc/source/tutorial/lock.rst
@@ -0,0 +1,14 @@
+======
+ Lock
+======
+
+Tooz provides distributed locks. A lock is identified by a name, and a lock can
+only be acquired by one coordinator at a time.
+
+.. literalinclude:: ../../../examples/lock.py
+ :language: python
+
+The method :meth:`tooz.coordination.CoordinationDriver.get_lock` allows
+to create a lock identified by a name. Once the you retrieve this lock, you can
+use it as a context manager or use the :meth:`tooz.locking.Lock.acquire` and
+:meth:`tooz.locking.Lock.release` methods to acquire and release the lock.
diff --git a/examples/coordinator.py b/examples/coordinator.py
index 6d5cf03..f0efef4 100644
--- a/examples/coordinator.py
+++ b/examples/coordinator.py
@@ -1,5 +1,5 @@
from tooz import coordination
-coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1')
+coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
coordinator.stop()
diff --git a/examples/coordinator_heartbeat.py b/examples/coordinator_heartbeat.py
index 8b0659f..ddbea13 100644
--- a/examples/coordinator_heartbeat.py
+++ b/examples/coordinator_heartbeat.py
@@ -4,7 +4,7 @@ from tooz import coordination
ALIVE_TIME = 5
-coordinator = coordination.get_coordinator('memcached://localhost', b'host-1')
+coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
start = time.time()
diff --git a/examples/group_membership.py b/examples/group_membership.py
index 58adb2d..7b85e6c 100644
--- a/examples/group_membership.py
+++ b/examples/group_membership.py
@@ -4,7 +4,7 @@ import six
from tooz import coordination
-coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1')
+coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
# Create a group
diff --git a/examples/group_membership_watch.py b/examples/group_membership_watch.py
index 28b786f..51836c2 100644
--- a/examples/group_membership_watch.py
+++ b/examples/group_membership_watch.py
@@ -4,7 +4,7 @@ import six
from tooz import coordination
-coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1')
+coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
# Create a group
diff --git a/examples/leader_election.py b/examples/leader_election.py
index b27129e..661c314 100644
--- a/examples/leader_election.py
+++ b/examples/leader_election.py
@@ -6,7 +6,7 @@ import six
from tooz import coordination
ALIVE_TIME = 1
-coordinator = coordination.get_coordinator('kazoo://localhost', b'host-1')
+coordinator = coordination.get_coordinator('zake://', b'host-1')
coordinator.start()
# Create a group
diff --git a/examples/lock.py b/examples/lock.py
new file mode 100644
index 0000000..42a1d03
--- /dev/null
+++ b/examples/lock.py
@@ -0,0 +1,11 @@
+from tooz import coordination
+
+coordinator = coordination.get_coordinator('zake://', b'host-1')
+coordinator.start()
+
+# Create a lock
+lock = coordinator.get_lock("foobar")
+with lock:
+ print("Do something that is distributed")
+
+coordinator.stop()
diff --git a/setup-mysql-env.sh b/setup-mysql-env.sh
new file mode 100755
index 0000000..2a8b9bf
--- /dev/null
+++ b/setup-mysql-env.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+set -x -e
+
+. functions.sh
+
+clean_exit () {
+ local error_code="$?"
+ kill $(jobs -p)
+ rm -rf ${MYSQL_DATA}
+ return $error_code
+}
+
+wait_for_line () {
+ while read line
+ do
+ echo "$line" | grep -q "$1" && break
+ done < "$2"
+ # Read the fifo for ever otherwise process would block
+ cat "$2" >/dev/null &
+}
+
+trap "clean_exit" EXIT
+
+# On systems like Fedora here's where mysqld can be found
+export PATH=$PATH:/usr/libexec
+
+# Start MySQL process for tests
+MYSQL_DATA=`mktemp -d /tmp/tooz-mysql-XXXXX`
+mkfifo ${MYSQL_DATA}/out
+mysqld --datadir=${MYSQL_DATA} --pid-file=${MYSQL_DATA}/mysql.pid --socket=${MYSQL_DATA}/mysql.socket --skip-networking --skip-grant-tables &> ${MYSQL_DATA}/out &
+# Wait for MySQL to start listening to connections
+wait_for_line "mysqld: ready for connections." ${MYSQL_DATA}/out
+mysql -S ${MYSQL_DATA}/mysql.socket -e 'CREATE DATABASE test;'
+export TOOZ_TEST_MYSQL_URL="mysql://root@localhost/test?unix_socket=${MYSQL_DATA}/mysql.socket"
+
+# Yield execution to venv command
+$*
diff --git a/setup.cfg b/setup.cfg
index a166108..5221a3b 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -31,6 +31,7 @@ tooz.backends =
ipc = tooz.drivers.ipc:IPCDriver
redis = tooz.drivers.redis:RedisDriver
postgresql = tooz.drivers.pgsql:PostgresDriver
+ mysql = tooz.drivers.mysql:MySQLDriver
[build_sphinx]
all_files = 1
diff --git a/test-requirements.txt b/test-requirements.txt
index 97c53f6..83873b4 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -2,6 +2,7 @@ pep8>=1.4.5
pyflakes>=0.7.2,<0.7.4
flake8>=2.1.0
discover
+mock>=1.0 # only needed on < python 3.3
sphinx>=1.1.2,<1.2
python-subunit
testrepository>=0.0.17
@@ -10,3 +11,4 @@ testscenarios>=0.4
coverage>=3.6
sysv_ipc>=0.6.8
psycopg2
+pymysql
diff --git a/tooz/drivers/memcached.py b/tooz/drivers/memcached.py
index 5dde0c4..2c9e2f2 100644
--- a/tooz/drivers/memcached.py
+++ b/tooz/drivers/memcached.py
@@ -325,8 +325,7 @@ class MemcachedDriver(coordination.CoordinationDriver):
def run_watchers(self):
result = []
for group_id in self.client.get(self._GROUP_LIST_KEY):
- encoded_group = self._encode_group_id(group_id)
- group_members = set(self.client.get(encoded_group))
+ group_members = set(self._get_members(group_id))
old_group_members = self._group_members[group_id]
for member_id in (group_members - old_group_members):
diff --git a/tooz/drivers/mysql.py b/tooz/drivers/mysql.py
new file mode 100644
index 0000000..c517a0a
--- /dev/null
+++ b/tooz/drivers/mysql.py
@@ -0,0 +1,126 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © 2014 eNovance
+#
+# Author: Julien Danjou <julien@danjou.info>
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import pymysql
+
+import tooz
+from tooz import coordination
+from tooz.drivers import _retry
+from tooz import locking
+from tooz import utils
+
+
+class MySQLLock(locking.Lock):
+ """A MySQL based lock."""
+
+ def __init__(self, name, connection):
+ super(MySQLLock, self).__init__(name)
+ self._conn = connection
+
+ def acquire(self, blocking=True):
+ if blocking is False:
+ try:
+ cur = self._conn.cursor()
+ cur.execute("SELECT GET_LOCK(%s, 0);", self.name)
+ # Can return NULL on error
+ if cur.fetchone()[0] is 1:
+ return True
+ return False
+ except pymysql.MySQLError as e:
+ raise coordination.ToozError(utils.exception_message(e))
+ else:
+ def _acquire():
+ try:
+ cur = self._conn.cursor()
+ cur.execute("SELECT GET_LOCK(%s, 0);", self.name)
+ if cur.fetchone()[0] is 1:
+ return True
+ except pymysql.MySQLError as e:
+ raise coordination.ToozError(utils.exception_message(e))
+ raise _retry.Retry
+ kwargs = _retry.RETRYING_KWARGS.copy()
+ if blocking is not True:
+ kwargs['stop_max_delay'] = blocking
+ return _retry.Retrying(**kwargs).call(_acquire)
+
+ def release(self):
+ try:
+ cur = self._conn.cursor()
+ cur.execute("SELECT RELEASE_LOCK(%s);", self.name)
+ return cur.fetchone()[0]
+ except pymysql.MySQLError as e:
+ raise coordination.ToozError(utils.exception_message(e))
+
+
+class MySQLDriver(coordination.CoordinationDriver):
+
+ def __init__(self, member_id, parsed_url, options):
+ """Initialize the MySQL driver."""
+ super(MySQLDriver, self).__init__()
+ self._host = parsed_url.netloc
+ self._port = parsed_url.port
+ self._dbname = parsed_url.path[1:]
+ self._username = parsed_url.username
+ self._password = parsed_url.password
+ self._unix_socket = options.get("unix_socket", [None])[-1]
+
+ def _start(self):
+ try:
+ if self._unix_socket:
+ self._conn = pymysql.Connect(unix_socket=self._unix_socket,
+ port=self._port,
+ user=self._username,
+ passwd=self._password,
+ database=self._dbname)
+ else:
+ self._conn = pymysql.Connect(host=self._host,
+ port=self._port,
+ user=self._username,
+ passwd=self._password,
+ database=self._dbname)
+ except pymysql.err.OperationalError as e:
+ raise coordination.ToozConnectionError(utils.exception_message(e))
+
+ def _stop(self):
+ self._conn.close()
+
+ def get_lock(self, name):
+ return MySQLLock(name, self._conn)
+
+ @staticmethod
+ def watch_join_group(group_id, callback):
+ raise tooz.NotImplemented
+
+ @staticmethod
+ def unwatch_join_group(group_id, callback):
+ raise tooz.NotImplemented
+
+ @staticmethod
+ def watch_leave_group(group_id, callback):
+ raise tooz.NotImplemented
+
+ @staticmethod
+ def unwatch_leave_group(group_id, callback):
+ raise tooz.NotImplemented
+
+ @staticmethod
+ def watch_elected_as_leader(group_id, callback):
+ raise tooz.NotImplemented
+
+ @staticmethod
+ def unwatch_elected_as_leader(group_id, callback):
+ raise tooz.NotImplemented
diff --git a/tooz/drivers/pgsql.py b/tooz/drivers/pgsql.py
index d142137..8494066 100644
--- a/tooz/drivers/pgsql.py
+++ b/tooz/drivers/pgsql.py
@@ -15,6 +15,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
+
+import contextlib
import hashlib
import psycopg2
@@ -24,6 +26,64 @@ import tooz
from tooz import coordination
from tooz.drivers import _retry
from tooz import locking
+from tooz import utils
+
+
+# See: psycopg/diagnostics_type.c for what kind of fields these
+# objects may have (things like 'schema_name', 'internal_query'
+# and so-on which are useful for figuring out what went wrong...)
+_DIAGNOSTICS_ATTRS = tuple([
+ 'column_name',
+ 'constraint_name',
+ 'context',
+ 'datatype_name',
+ 'internal_position',
+ 'internal_query',
+ 'message_detail',
+ 'message_hint',
+ 'message_primary',
+ 'schema_name',
+ 'severity',
+ 'source_file',
+ 'source_function',
+ 'source_line',
+ 'sqlstate',
+ 'statement_position',
+ 'table_name',
+])
+
+
+def _format_exception(e):
+ lines = [
+ "%s: %s" % (type(e).__name__, utils.exception_message(e).strip()),
+ ]
+ if hasattr(e, 'pgcode') and e.pgcode is not None:
+ lines.append("Error code: %s" % e.pgcode)
+ # The reason this hasattr check is done is that the 'diag' may not always
+ # be present, depending on how new of a psycopg is installed... so better
+ # to be safe than sorry...
+ if hasattr(e, 'diag') and e.diag is not None:
+ diagnostic_lines = []
+ for attr_name in _DIAGNOSTICS_ATTRS:
+ if not hasattr(e.diag, attr_name):
+ continue
+ attr_value = getattr(e.diag, attr_name)
+ if attr_value is None:
+ continue
+ diagnostic_lines.append(" %s = %s" (attr_name, attr_value))
+ if diagnostic_lines:
+ lines.append('Diagnostics:')
+ lines.extend(diagnostic_lines)
+ return "\n".join(lines)
+
+
+@contextlib.contextmanager
+def _translating_cursor(conn):
+ try:
+ with conn.cursor() as cur:
+ yield cur
+ except psycopg2.Error as e:
+ raise coordination.ToozError(_format_exception(e))
class PostgresLock(locking.Lock):
@@ -41,16 +101,16 @@ class PostgresLock(locking.Lock):
def acquire(self, blocking=True):
if blocking is True:
- with self._conn.cursor() as cur:
+ with _translating_cursor(self._conn) as cur:
cur.execute("SELECT pg_advisory_lock(%s, %s);", self.key)
- return True
+ return True
elif blocking is False:
- with self._conn.cursor() as cur:
+ with _translating_cursor(self._conn) as cur:
cur.execute("SELECT pg_try_advisory_lock(%s, %s);", self.key)
return cur.fetchone()[0]
else:
def _acquire():
- with self._conn.cursor() as cur:
+ with _translating_cursor(self._conn) as cur:
cur.execute("SELECT pg_try_advisory_lock(%s, %s);",
self.key)
if cur.fetchone()[0] is True:
@@ -61,7 +121,7 @@ class PostgresLock(locking.Lock):
return _retry.Retrying(**kwargs).call(_acquire)
def release(self):
- with self._conn.cursor() as cur:
+ with _translating_cursor(self._conn) as cur:
cur.execute("SELECT pg_advisory_unlock(%s, %s);", self.key)
return cur.fetchone()[0]
@@ -78,11 +138,14 @@ class PostgresDriver(coordination.CoordinationDriver):
self._password = parsed_url.password
def _start(self):
- self._conn = psycopg2.connect(host=self._host,
- port=self._port,
- user=self._username,
- password=self._password,
- database=self._dbname)
+ try:
+ self._conn = psycopg2.connect(host=self._host,
+ port=self._port,
+ user=self._username,
+ password=self._password,
+ database=self._dbname)
+ except psycopg2.Error as e:
+ raise coordination.ToozConnectionError(_format_exception(e))
def _stop(self):
self._conn.close()
diff --git a/tooz/tests/test_coordination.py b/tooz/tests/test_coordination.py
index e88047b..b76d578 100644
--- a/tooz/tests/test_coordination.py
+++ b/tooz/tests/test_coordination.py
@@ -29,12 +29,18 @@ class TestAPI(testscenarios.TestWithScenarios,
tests.TestCaseSkipNotImplemented):
scenarios = [
- ('zookeeper', {'url': 'kazoo://127.0.0.1:2181?timeout=5'}),
+ ('zookeeper', {'url': 'kazoo://127.0.0.1:2181?timeout=5',
+ 'bad_url': 'kazoo://localhost:1'}),
('zake', {'url': 'zake://?timeout=5'}),
- ('memcached', {'url': 'memcached://?timeout=5'}),
+ ('memcached', {'url': 'memcached://?timeout=5',
+ 'bad_url': 'memcached://localhost:1'}),
('ipc', {'url': 'ipc://'}),
- ('redis', {'url': 'redis://localhost:6379?timeout=5'}),
- ('postgresql', {'url': os.getenv("TOOZ_TEST_PGSQL_URL")}),
+ ('redis', {'url': 'redis://localhost:6379?timeout=5',
+ 'bad_url': 'redis://localhost:1'}),
+ ('postgresql', {'url': os.getenv("TOOZ_TEST_PGSQL_URL"),
+ 'bad_url': 'postgresql://localhost:1'}),
+ ('mysql', {'url': os.getenv("TOOZ_TEST_MYSQL_URL"),
+ 'bad_url': 'mysql://localhost:1'}),
]
# Only certain drivers have the tested support for timeouts that we test
@@ -78,6 +84,14 @@ class TestAPI(testscenarios.TestWithScenarios,
self._coord.stop()
super(TestAPI, self).tearDown()
+ def test_connection_error(self):
+ if not hasattr(self, "bad_url"):
+ raise testcase.TestSkipped("No bad URL provided")
+ coord = tooz.coordination.get_coordinator(self.bad_url,
+ self.member_id)
+ self.assertRaises(tooz.coordination.ToozConnectionError,
+ coord.start)
+
def test_stop_first(self):
c = tooz.coordination.get_coordinator(self.url,
self.member_id)
diff --git a/tooz/tests/test_postgresql.py b/tooz/tests/test_postgresql.py
new file mode 100644
index 0000000..35c8d0e
--- /dev/null
+++ b/tooz/tests/test_postgresql.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2014 Yahoo! Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import uuid
+
+try:
+ # Added in python 3.3+
+ from unittest import mock
+except ImportError:
+ import mock
+
+import testtools
+from testtools import testcase
+
+from tooz import coordination
+from tooz import utils
+
+# Handle the case gracefully where the driver is not installed.
+try:
+ import psycopg2
+ PGSQL_AVAILABLE = True
+except ImportError:
+ PGSQL_AVAILABLE = False
+
+
+@testtools.skipUnless(PGSQL_AVAILABLE, 'psycopg2 is not available')
+class TestPostgreSQLFailures(testcase.TestCase):
+
+ # Not actually used (but required none the less), since we mock out
+ # the connect() method...
+ FAKE_URL = "postgresql://localhost:1"
+
+ def _create_coordinator(self):
+
+ def _safe_stop(coord):
+ try:
+ coord.stop()
+ except coordination.ToozError as e:
+ # TODO(harlowja): make this better, so that we don't have to
+ # do string checking...
+ message = utils.exception_message(e)
+ if (message != 'Can not stop a driver which has not'
+ ' been started'):
+ raise
+
+ coord = coordination.get_coordinator(self.FAKE_URL,
+ str(uuid.uuid4()).encode('ascii'))
+ self.addCleanup(_safe_stop, coord)
+ return coord
+
+ @mock.patch("tooz.drivers.pgsql.psycopg2.connect")
+ def test_connect_failure(self, psycopg2_connector):
+ psycopg2_connector.side_effect = psycopg2.Error("Broken")
+ c = self._create_coordinator()
+ self.assertRaises(coordination.ToozConnectionError, c.start)
+
+ @mock.patch("tooz.drivers.pgsql.psycopg2.connect")
+ def test_connect_failure_operational(self, psycopg2_connector):
+ psycopg2_connector.side_effect = psycopg2.OperationalError("Broken")
+ c = self._create_coordinator()
+ self.assertRaises(coordination.ToozConnectionError, c.start)
+
+ @mock.patch("tooz.drivers.pgsql.psycopg2.connect")
+ def test_failure_acquire_lock(self, psycopg2_connector):
+ execute_mock = mock.MagicMock()
+ execute_mock.execute.side_effect = psycopg2.OperationalError("Broken")
+
+ cursor_mock = mock.MagicMock()
+ cursor_mock.__enter__ = mock.MagicMock(return_value=execute_mock)
+ cursor_mock.__exit__ = mock.MagicMock(return_value=False)
+
+ conn_mock = mock.MagicMock()
+ conn_mock.cursor.return_value = cursor_mock
+ psycopg2_connector.return_value = conn_mock
+
+ c = self._create_coordinator()
+ c.start()
+ test_lock = c.get_lock(b'test-lock')
+ self.assertRaises(coordination.ToozError, test_lock.acquire)
+
+ @mock.patch("tooz.drivers.pgsql.psycopg2.connect")
+ def test_failure_release_lock(self, psycopg2_connector):
+ execute_mock = mock.MagicMock()
+ execute_mock.execute.side_effect = [
+ True,
+ psycopg2.OperationalError("Broken"),
+ ]
+
+ cursor_mock = mock.MagicMock()
+ cursor_mock.__enter__ = mock.MagicMock(return_value=execute_mock)
+ cursor_mock.__exit__ = mock.MagicMock(return_value=False)
+
+ conn_mock = mock.MagicMock()
+ conn_mock.cursor.return_value = cursor_mock
+ psycopg2_connector.return_value = conn_mock
+
+ c = self._create_coordinator()
+ c.start()
+ test_lock = c.get_lock(b'test-lock')
+ self.assertTrue(test_lock.acquire())
+ self.assertRaises(coordination.ToozError, test_lock.release)
diff --git a/tox.ini b/tox.ini
index 757f3da..35463e7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
[tox]
minversion = 1.6
skipsdist = True
-envlist = py26,py27,py33,py34,py27-zookeeper,py33-zookeeper,py34-zookeeper,py27-redis,py33-redis,py34-redis,py27-memcached,py33-memcached,py34-memcached,pep8
+envlist = py26,py27,py34,py27-zookeeper,py34-zookeeper,py27-redis,py34-redis,py27-memcached,py34-memcached,py27-postgresql,py34-postgresql,py27-mysql,py34-mysql,pep8
[testenv]
deps = -r{toxinidir}/requirements.txt
@@ -22,10 +22,6 @@ commands = python setup.py testr --slowest --testr-args="{posargs}"
{toxinidir}/run-examples.sh
doc8 doc/source
-[testenv:py33]
-deps = -r{toxinidir}/requirements-py3.txt
- -r{toxinidir}/test-requirements.txt
-
[testenv:py34]
deps = -r{toxinidir}/requirements-py3.txt
-r{toxinidir}/test-requirements.txt
@@ -33,11 +29,6 @@ deps = -r{toxinidir}/requirements-py3.txt
[testenv:py27-zookeeper]
commands = {toxinidir}/setup-zookeeper-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-[testenv:py33-zookeeper]
-deps = {[testenv:py33]deps}
-basepython = python3.3
-commands = {toxinidir}/setup-zookeeper-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-
[testenv:py34-zookeeper]
deps = {[testenv:py34]deps}
basepython = python3.4
@@ -46,11 +37,6 @@ commands = {toxinidir}/setup-zookeeper-env.sh python setup.py testr --slowest --
[testenv:py27-redis]
commands = {toxinidir}/setup-redis-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-[testenv:py33-redis]
-deps = {[testenv:py33]deps}
-basepython = python3.3
-commands = {toxinidir}/setup-redis-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-
[testenv:py34-redis]
deps = {[testenv:py34]deps}
basepython = python3.4
@@ -59,11 +45,6 @@ commands = {toxinidir}/setup-redis-env.sh python setup.py testr --slowest --test
[testenv:py27-memcached]
commands = {toxinidir}/setup-memcached-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-[testenv:py33-memcached]
-deps = {[testenv:py33]deps}
-basepython = python3.3
-commands = {toxinidir}/setup-memcached-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-
[testenv:py34-memcached]
deps = {[testenv:py34]deps}
basepython = python3.4
@@ -72,16 +53,19 @@ commands = {toxinidir}/setup-memcached-env.sh python setup.py testr --slowest --
[testenv:py27-postgresql]
commands = {toxinidir}/setup-postgresql-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-[testenv:py33-postgresql]
-deps = {[testenv:py33]deps}
-basepython = python3.3
-commands = {toxinidir}/setup-postgresql-env.sh python setup.py testr --slowest --testr-args="{posargs}"
-
[testenv:py34-postgresql]
deps = {[testenv:py34]deps}
basepython = python3.4
commands = {toxinidir}/setup-postgresql-env.sh python setup.py testr --slowest --testr-args="{posargs}"
+[testenv:py27-mysql]
+commands = {toxinidir}/setup-mysql-env.sh python setup.py testr --slowest --testr-args="{posargs}"
+
+[testenv:py34-mysql]
+deps = {[testenv:py34]deps}
+basepython = python3.4
+commands = {toxinidir}/setup-mysql-env.sh python setup.py testr --slowest --testr-args="{posargs}"
+
[testenv:cover]
commands = python setup.py testr --slowest --coverage --testr-args="{posargs}"
@@ -103,3 +87,4 @@ show-source = True
[hacking]
import_exceptions = six.moves
+ unittest.mock