txredisapi 1.4.12__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {txredisapi-1.4.12/txredisapi.egg-info → txredisapi-1.6.0}/PKG-INFO +15 -5
- {txredisapi-1.4.12 → txredisapi-1.6.0}/setup.py +3 -3
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_bitops.py +12 -14
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_connection_charset.py +3 -5
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_hash_ops.py +2 -4
- txredisapi-1.6.0/tests/test_ipv6.py +98 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_multibulk.py +2 -4
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_number_conversions.py +1 -3
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_pipelining.py +2 -4
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_protocol.py +4 -6
- txredisapi-1.6.0/tests/test_reconnect.py +244 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_scripting.py +0 -2
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_sentinel.py +74 -13
- txredisapi-1.6.0/tests/test_ssl.py +101 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_subscriber.py +7 -6
- {txredisapi-1.4.12 → txredisapi-1.6.0/txredisapi.egg-info}/PKG-INFO +15 -5
- {txredisapi-1.4.12 → txredisapi-1.6.0}/txredisapi.egg-info/SOURCES.txt +3 -0
- txredisapi-1.6.0/txredisapi.egg-info/requires.txt +1 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/txredisapi.py +429 -161
- txredisapi-1.4.12/txredisapi.egg-info/requires.txt +0 -2
- {txredisapi-1.4.12 → txredisapi-1.6.0}/LICENSE +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/MANIFEST.in +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/README.md +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/setup.cfg +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/__init__.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/mixins.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_basics.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_blocking.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_cancel.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_connection.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_hyperloglog.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_implicit_pipelining.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_list_ops.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_operations.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_publish.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_scan.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_sets.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_sort.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_sortedsets.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_transactions.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_unix_connection.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/tests/test_watch.py +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/txredisapi.egg-info/dependency_links.txt +0 -0
- {txredisapi-1.4.12 → txredisapi-1.6.0}/txredisapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: txredisapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: non-blocking redis client for python
|
|
5
5
|
Home-page: http://github.com/IlyaSkriblovsky/txredisapi
|
|
6
6
|
Author: Alexandre Fiori
|
|
@@ -11,7 +11,6 @@ Classifier: Intended Audience :: Developers
|
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
12
|
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
13
|
Classifier: Programming Language :: Python
|
|
14
|
-
Classifier: Programming Language :: Python :: 2.7
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.5
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.6
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.7
|
|
@@ -20,10 +19,21 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
21
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
|
+
Requires-Python: >=3.5
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: twisted
|
|
26
|
-
|
|
25
|
+
Requires-Dist: twisted[tls]>=18.7.0
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: home-page
|
|
32
|
+
Dynamic: license
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
Dynamic: requires-dist
|
|
35
|
+
Dynamic: requires-python
|
|
36
|
+
Dynamic: summary
|
|
27
37
|
|
|
28
38
|
txredisapi
|
|
29
39
|
==========
|
|
@@ -23,9 +23,10 @@ long_description = (this_directory / "README.md").read_text()
|
|
|
23
23
|
|
|
24
24
|
setuptools.setup(
|
|
25
25
|
name="txredisapi",
|
|
26
|
-
version="1.
|
|
26
|
+
version="1.6.0",
|
|
27
27
|
py_modules=["txredisapi"],
|
|
28
|
-
install_requires=["twisted"
|
|
28
|
+
install_requires=["twisted[tls]>=18.7.0"],
|
|
29
|
+
python_requires=">=3.5",
|
|
29
30
|
author="Alexandre Fiori",
|
|
30
31
|
author_email="fiorix@gmail.com",
|
|
31
32
|
url="http://github.com/IlyaSkriblovsky/txredisapi",
|
|
@@ -39,7 +40,6 @@ setuptools.setup(
|
|
|
39
40
|
'Operating System :: OS Independent',
|
|
40
41
|
'License :: OSI Approved :: Apache Software License',
|
|
41
42
|
'Programming Language :: Python',
|
|
42
|
-
'Programming Language :: Python :: 2.7',
|
|
43
43
|
'Programming Language :: Python :: 3.5',
|
|
44
44
|
'Programming Language :: Python :: 3.6',
|
|
45
45
|
'Programming Language :: Python :: 3.7',
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
import six
|
|
17
|
-
|
|
18
16
|
import sys
|
|
19
17
|
import operator
|
|
20
18
|
|
|
@@ -48,7 +46,7 @@ class TestBitOps(unittest.TestCase, Redis26CheckMixin):
|
|
|
48
46
|
@defer.inlineCallbacks
|
|
49
47
|
def test_getbit(self):
|
|
50
48
|
key = self._KEYS[0]
|
|
51
|
-
yield self.db.set(key,
|
|
49
|
+
yield self.db.set(key, b'\xaa')
|
|
52
50
|
l = [1, 0, 1, 0, 1, 0, 1, 0]
|
|
53
51
|
for x in range(8):
|
|
54
52
|
r = yield self.db.getbit(key, x)
|
|
@@ -86,30 +84,30 @@ class TestBitOps(unittest.TestCase, Redis26CheckMixin):
|
|
|
86
84
|
def test_bitop_not(self):
|
|
87
85
|
return self._test_bitop([operator.__not__, operator.not_,
|
|
88
86
|
'not', 'NOT', 'NoT'],
|
|
89
|
-
|
|
87
|
+
b'\x0f\x0f\x0f\x0f',
|
|
90
88
|
None,
|
|
91
|
-
|
|
89
|
+
b'\xf0\xf0\xf0\xf0')
|
|
92
90
|
|
|
93
91
|
def test_bitop_or(self):
|
|
94
92
|
return self._test_bitop([operator.__or__, operator.or_,
|
|
95
93
|
'or', 'OR', 'oR'],
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
b'\x0f\x0f\x0f\x0f',
|
|
95
|
+
b'\xf0\xf0\xf0\xf0',
|
|
96
|
+
b'\xff\xff\xff\xff')
|
|
99
97
|
|
|
100
98
|
def test_bitop_and(self):
|
|
101
99
|
return self._test_bitop([operator.__and__, operator.and_,
|
|
102
100
|
'and', 'AND', 'AnD'],
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
b'\x0f\x0f\x0f\x0f',
|
|
102
|
+
b'\xf0\xf0\xf0\xf0',
|
|
103
|
+
b'\x00\x00\x00\x00')
|
|
106
104
|
|
|
107
105
|
def test_bitop_xor(self):
|
|
108
106
|
return self._test_bitop([operator.__xor__, operator.xor,
|
|
109
107
|
'xor', 'XOR', 'XoR'],
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
b'\x9c\x9c\x9c\x9c',
|
|
109
|
+
b'\x6c\x6c\x6c\x6c',
|
|
110
|
+
b'\xf0\xf0\xf0\xf0')
|
|
113
111
|
|
|
114
112
|
def test_bitop_invalid(self):
|
|
115
113
|
self.assertFailure(self.db.bitop('test', 'test', 'test'),
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
import six
|
|
17
|
-
|
|
18
16
|
from twisted.internet import defer
|
|
19
17
|
from twisted.trial import unittest
|
|
20
18
|
|
|
@@ -25,7 +23,7 @@ from tests.mixins import REDIS_HOST, REDIS_PORT
|
|
|
25
23
|
|
|
26
24
|
class TestConnectionCharset(unittest.TestCase):
|
|
27
25
|
TEST_KEY = 'txredisapi:test_key'
|
|
28
|
-
TEST_VALUE_UNICODE =
|
|
26
|
+
TEST_VALUE_UNICODE = str('\u262d' * 3)
|
|
29
27
|
TEST_VALUE_BINARY = b'\x00\x01' * 3
|
|
30
28
|
|
|
31
29
|
@defer.inlineCallbacks
|
|
@@ -34,7 +32,7 @@ class TestConnectionCharset(unittest.TestCase):
|
|
|
34
32
|
|
|
35
33
|
yield db.set(self.TEST_KEY, self.TEST_VALUE_BINARY)
|
|
36
34
|
result = yield db.get(self.TEST_KEY)
|
|
37
|
-
self.assertTrue(type(result) ==
|
|
35
|
+
self.assertTrue(type(result) == bytes)
|
|
38
36
|
self.assertEqual(result, self.TEST_VALUE_BINARY)
|
|
39
37
|
|
|
40
38
|
yield db.delete(self.TEST_KEY)
|
|
@@ -47,7 +45,7 @@ class TestConnectionCharset(unittest.TestCase):
|
|
|
47
45
|
yield db.set(self.TEST_KEY, self.TEST_VALUE_UNICODE)
|
|
48
46
|
result = yield db.get(self.TEST_KEY)
|
|
49
47
|
self.assertEqual(result, self.TEST_VALUE_UNICODE)
|
|
50
|
-
self.assertTrue(type(result) ==
|
|
48
|
+
self.assertTrue(type(result) == str)
|
|
51
49
|
|
|
52
50
|
yield db.delete(self.TEST_KEY)
|
|
53
51
|
yield db.disconnect()
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
import six
|
|
17
|
-
|
|
18
16
|
from twisted.internet import defer
|
|
19
17
|
from twisted.trial import unittest
|
|
20
18
|
|
|
@@ -58,8 +56,8 @@ class TestRedisHashOperations(unittest.TestCase):
|
|
|
58
56
|
t_dict['key2'] = 'dos'
|
|
59
57
|
yield db.hmset("txredisapi:HKeysHVals", t_dict)
|
|
60
58
|
|
|
61
|
-
vs_u = [
|
|
62
|
-
ks_u = [
|
|
59
|
+
vs_u = [str(v) for v in t_dict.values()]
|
|
60
|
+
ks_u = [str(k) for k in t_dict.keys()]
|
|
63
61
|
k_res = yield db.hkeys("txredisapi:HKeysHVals")
|
|
64
62
|
v_res = yield db.hvals("txredisapi:HKeysHVals")
|
|
65
63
|
self.assertEqual(sorted(ks_u), sorted(k_res))
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from twisted.internet import defer, reactor
|
|
16
|
+
from twisted.internet.address import IPv6Address
|
|
17
|
+
from twisted.internet.error import CannotListenError
|
|
18
|
+
from twisted.internet.interfaces import IHostnameResolver, IHostResolution
|
|
19
|
+
from twisted.trial import unittest
|
|
20
|
+
from zope.interface import implementer
|
|
21
|
+
|
|
22
|
+
import txredisapi as redis
|
|
23
|
+
|
|
24
|
+
from tests.test_sentinel import FakeRedisFactory
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@implementer(IHostResolution)
|
|
28
|
+
class Resolution(object):
|
|
29
|
+
def __init__(self, name):
|
|
30
|
+
self.name = name
|
|
31
|
+
|
|
32
|
+
def cancel(self):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@implementer(IHostnameResolver)
|
|
37
|
+
class IPv6OnlyResolver(object):
|
|
38
|
+
"""
|
|
39
|
+
Resolves a single hostname to a single IPv6 address and knows nothing
|
|
40
|
+
else, just like a name that has an AAAA record and no A record.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, hostname, address="::1"):
|
|
44
|
+
self.hostname = hostname
|
|
45
|
+
self.address = address
|
|
46
|
+
|
|
47
|
+
def resolveHostName(self, resolutionReceiver, hostName, portNumber=0,
|
|
48
|
+
addressTypes=None, transportSemantics="TCP"):
|
|
49
|
+
resolution = Resolution(hostName)
|
|
50
|
+
resolutionReceiver.resolutionBegan(resolution)
|
|
51
|
+
if hostName == self.hostname and \
|
|
52
|
+
(addressTypes is None or IPv6Address in addressTypes):
|
|
53
|
+
resolutionReceiver.addressResolved(
|
|
54
|
+
IPv6Address("TCP", self.address, portNumber))
|
|
55
|
+
resolutionReceiver.resolutionComplete()
|
|
56
|
+
return resolution
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestIPv6Connections(unittest.TestCase):
|
|
60
|
+
"""
|
|
61
|
+
Servers reachable over IPv6 only must be reachable as well: names are
|
|
62
|
+
resolved with getaddrinfo(), not with the IPv4-only gethostbyname().
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
hostname = "ipv6-only.invalid"
|
|
66
|
+
|
|
67
|
+
# connecting to the loopback is instant; don't sit through trial's default
|
|
68
|
+
# timeout if it ever stops working
|
|
69
|
+
timeout = 15
|
|
70
|
+
|
|
71
|
+
def setUp(self):
|
|
72
|
+
try:
|
|
73
|
+
self.listener = reactor.listenTCP(0, FakeRedisFactory(),
|
|
74
|
+
interface="::1")
|
|
75
|
+
except CannotListenError:
|
|
76
|
+
raise unittest.SkipTest("no IPv6 loopback available")
|
|
77
|
+
|
|
78
|
+
self.port = self.listener.getHost().port
|
|
79
|
+
self.addCleanup(self.listener.stopListening)
|
|
80
|
+
|
|
81
|
+
@defer.inlineCallbacks
|
|
82
|
+
def test_aaaa_only_hostname(self):
|
|
83
|
+
previous = reactor.installNameResolver(IPv6OnlyResolver(self.hostname))
|
|
84
|
+
self.addCleanup(reactor.installNameResolver, previous)
|
|
85
|
+
|
|
86
|
+
db = yield redis.Connection(self.hostname, self.port, reconnect=False)
|
|
87
|
+
self.addCleanup(db.disconnect)
|
|
88
|
+
|
|
89
|
+
role = yield db.role()
|
|
90
|
+
self.assertEqual(role[0], "master")
|
|
91
|
+
|
|
92
|
+
@defer.inlineCallbacks
|
|
93
|
+
def test_ipv6_literal(self):
|
|
94
|
+
db = yield redis.Connection("::1", self.port, reconnect=False)
|
|
95
|
+
self.addCleanup(db.disconnect)
|
|
96
|
+
|
|
97
|
+
role = yield db.role()
|
|
98
|
+
self.assertEqual(role[0], "master")
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
import six
|
|
17
|
-
|
|
18
16
|
import os
|
|
19
17
|
import base64
|
|
20
18
|
|
|
@@ -61,7 +59,7 @@ class LargeMultiBulk(unittest.TestCase):
|
|
|
61
59
|
@defer.inlineCallbacks
|
|
62
60
|
def test_bulk_numeric(self):
|
|
63
61
|
test_values = [
|
|
64
|
-
|
|
62
|
+
b'', b'.hello', b'+world', b'123test',
|
|
65
63
|
+1, 0.1, 0.01, -0.1, 0, -10]
|
|
66
64
|
for v in test_values:
|
|
67
65
|
yield self.db.set(self._KEY, v)
|
|
@@ -76,7 +74,7 @@ class LargeMultiBulk(unittest.TestCase):
|
|
|
76
74
|
if there's a '.' in the string.
|
|
77
75
|
This test is to ensure this behavior isn't broken in the future.
|
|
78
76
|
'''
|
|
79
|
-
values = [
|
|
77
|
+
values = [b'+inf', b'-inf', b'NaN']
|
|
80
78
|
for x in values:
|
|
81
79
|
yield self.db.set(self._KEY, x)
|
|
82
80
|
r = yield self.db.get(self._KEY)
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
import six
|
|
17
|
-
|
|
18
16
|
import txredisapi as redis
|
|
19
17
|
from twisted.internet import defer
|
|
20
18
|
from twisted.trial import unittest
|
|
@@ -58,7 +56,7 @@ class TestNumberConversions(unittest.TestCase):
|
|
|
58
56
|
expected = format(k, "f")
|
|
59
57
|
else:
|
|
60
58
|
expected = str(k)
|
|
61
|
-
self.assertIsInstance(result,
|
|
59
|
+
self.assertIsInstance(result, str)
|
|
62
60
|
self.assertEqual(result, expected)
|
|
63
61
|
|
|
64
62
|
@defer.inlineCallbacks
|
|
@@ -15,8 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
import sys
|
|
17
17
|
|
|
18
|
-
import six
|
|
19
|
-
|
|
20
18
|
from twisted.trial import unittest
|
|
21
19
|
from twisted.internet import defer
|
|
22
20
|
from twisted.python import log
|
|
@@ -71,8 +69,8 @@ class TestRedisConnections(unittest.TestCase):
|
|
|
71
69
|
|
|
72
70
|
# Make sure that all SET commands were sent in a single pipelined write.
|
|
73
71
|
write_history = pipeline.transport.write_history
|
|
74
|
-
lines_in_first_write = write_history[0].split(
|
|
75
|
-
sets_in_first_write = sum(1 for w in lines_in_first_write if
|
|
72
|
+
lines_in_first_write = write_history[0].split(b"\n")
|
|
73
|
+
sets_in_first_write = sum(1 for w in lines_in_first_write if b"SET" in w)
|
|
76
74
|
self.assertEqual(sets_in_first_write, 3)
|
|
77
75
|
|
|
78
76
|
@defer.inlineCallbacks
|
|
@@ -13,8 +13,6 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
import six
|
|
17
|
-
|
|
18
16
|
import txredisapi as redis
|
|
19
17
|
|
|
20
18
|
from twisted.trial import unittest
|
|
@@ -36,7 +34,7 @@ class LineReceiverSubclass(redis.LineReceiver):
|
|
|
36
34
|
|
|
37
35
|
|
|
38
36
|
class TestLineReciever(unittest.TestCase):
|
|
39
|
-
S =
|
|
37
|
+
S = b'TEST'
|
|
40
38
|
|
|
41
39
|
def setUp(self):
|
|
42
40
|
self.proto = LineReceiverSubclass()
|
|
@@ -47,14 +45,14 @@ class TestLineReciever(unittest.TestCase):
|
|
|
47
45
|
|
|
48
46
|
def test_excess_line_length(self):
|
|
49
47
|
self.assertTrue(self.transport.connected)
|
|
50
|
-
self.proto.dataReceived(
|
|
48
|
+
self.proto.dataReceived(b'\x00' * (self.proto.MAX_LENGTH + 1))
|
|
51
49
|
self.assertFalse(self.transport.connected)
|
|
52
50
|
|
|
53
51
|
def test_excess_delimited_line(self):
|
|
54
52
|
self.assertTrue(self.transport.connected)
|
|
55
53
|
self.proto.dataReceived(self.S + self.proto.delimiter)
|
|
56
54
|
self.assertEqual(self.proto._rcvd_line, self.S.decode())
|
|
57
|
-
s = (
|
|
55
|
+
s = (b'\x00' * (self.proto.MAX_LENGTH + 1)) + self.proto.delimiter
|
|
58
56
|
self.proto._rcvd_line = None
|
|
59
57
|
self.proto.dataReceived(s)
|
|
60
58
|
self.assertFalse(self.transport.connected)
|
|
@@ -94,4 +92,4 @@ class TestBaseRedisProtocol(unittest.TestCase):
|
|
|
94
92
|
|
|
95
93
|
def test_build_ping(self):
|
|
96
94
|
s = self._protocol._build_command("PING")
|
|
97
|
-
self.assertEqual(s,
|
|
95
|
+
self.assertEqual(s, b'*1\r\n$4\r\nPING\r\n')
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from twisted.internet import defer, error, reactor, task
|
|
16
|
+
from twisted.internet.endpoints import HostnameEndpoint
|
|
17
|
+
from twisted.internet.interfaces import IStreamClientEndpoint
|
|
18
|
+
from twisted.internet.protocol import Factory
|
|
19
|
+
from twisted.trial import unittest
|
|
20
|
+
from zope.interface import implementer
|
|
21
|
+
|
|
22
|
+
import txredisapi as redis
|
|
23
|
+
|
|
24
|
+
from tests.test_sentinel import TrackingRedisFactory
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@implementer(IStreamClientEndpoint)
|
|
28
|
+
class FailingEndpoint(object):
|
|
29
|
+
"""
|
|
30
|
+
Endpoint that fails synchronously, the way UNIXClientEndpoint does when
|
|
31
|
+
the socket does not exist.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.attempts = 0
|
|
36
|
+
|
|
37
|
+
def connect(self, protocolFactory):
|
|
38
|
+
self.attempts += 1
|
|
39
|
+
return defer.fail(error.ConnectionRefusedError())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@implementer(IStreamClientEndpoint)
|
|
43
|
+
class GatedEndpoint(object):
|
|
44
|
+
"""
|
|
45
|
+
Endpoint that lets the first connection through and refuses the ones
|
|
46
|
+
after it, but not before the given gate fires.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, endpoint, gate):
|
|
50
|
+
self.endpoint = endpoint
|
|
51
|
+
self.gate = gate
|
|
52
|
+
self.attempts = 0
|
|
53
|
+
|
|
54
|
+
def connect(self, protocolFactory):
|
|
55
|
+
self.attempts += 1
|
|
56
|
+
if self.attempts == 1:
|
|
57
|
+
return self.endpoint.connect(protocolFactory)
|
|
58
|
+
|
|
59
|
+
refused = defer.Deferred()
|
|
60
|
+
self.gate.addCallback(
|
|
61
|
+
lambda _: refused.errback(error.ConnectionRefusedError()))
|
|
62
|
+
return refused
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestReconnect(unittest.TestCase):
|
|
66
|
+
timeout = 30
|
|
67
|
+
|
|
68
|
+
def setUp(self):
|
|
69
|
+
self.server = TrackingRedisFactory()
|
|
70
|
+
self.listener = reactor.listenTCP(0, self.server,
|
|
71
|
+
interface="127.0.0.1")
|
|
72
|
+
self.port = self.listener.getHost().port
|
|
73
|
+
self.addCleanup(self.listener.stopListening)
|
|
74
|
+
|
|
75
|
+
def endpoint(self):
|
|
76
|
+
return HostnameEndpoint(reactor, "127.0.0.1", self.port)
|
|
77
|
+
|
|
78
|
+
@defer.inlineCallbacks
|
|
79
|
+
def waitFor(self, predicate, message):
|
|
80
|
+
deadline = reactor.seconds() + 15
|
|
81
|
+
while not predicate():
|
|
82
|
+
if reactor.seconds() > deadline:
|
|
83
|
+
self.fail(message)
|
|
84
|
+
yield task.deferLater(reactor, 0.05, lambda: None)
|
|
85
|
+
|
|
86
|
+
@defer.inlineCallbacks
|
|
87
|
+
def closedPort(self):
|
|
88
|
+
listener = reactor.listenTCP(0, Factory(), interface="127.0.0.1")
|
|
89
|
+
port = listener.getHost().port
|
|
90
|
+
yield listener.stopListening()
|
|
91
|
+
return port
|
|
92
|
+
|
|
93
|
+
@defer.inlineCallbacks
|
|
94
|
+
def test_reconnects(self):
|
|
95
|
+
db = yield redis.Connection("127.0.0.1", self.port)
|
|
96
|
+
self.addCleanup(db.disconnect)
|
|
97
|
+
yield db.role()
|
|
98
|
+
|
|
99
|
+
self.server.protocols[0].transport.loseConnection()
|
|
100
|
+
|
|
101
|
+
yield self.waitFor(lambda: len(self.server.protocols) > 1,
|
|
102
|
+
"connection has not been reestablished")
|
|
103
|
+
role = yield db.role()
|
|
104
|
+
self.assertEqual(role[0], "master")
|
|
105
|
+
|
|
106
|
+
@defer.inlineCallbacks
|
|
107
|
+
def test_does_not_reconnect_when_disabled(self):
|
|
108
|
+
# short enough for the wait below to cover several attempts
|
|
109
|
+
self.patch(redis.RedisFactory, "initialDelay", 0.05)
|
|
110
|
+
|
|
111
|
+
db = yield redis.Connection("127.0.0.1", self.port, reconnect=False)
|
|
112
|
+
self.addCleanup(db.disconnect)
|
|
113
|
+
yield db.role()
|
|
114
|
+
|
|
115
|
+
self.server.protocols[0].transport.loseConnection()
|
|
116
|
+
yield self.waitFor(lambda: db._factory.size == 0,
|
|
117
|
+
"connection has not been dropped")
|
|
118
|
+
|
|
119
|
+
# give a reconnect a chance to happen, it should not
|
|
120
|
+
yield task.deferLater(reactor, 0.3, lambda: None)
|
|
121
|
+
self.assertEqual(len(self.server.protocols), 1)
|
|
122
|
+
|
|
123
|
+
yield self.assertFailure(db.role(), redis.ConnectionError)
|
|
124
|
+
|
|
125
|
+
@defer.inlineCallbacks
|
|
126
|
+
def test_failed_connection_is_reported(self):
|
|
127
|
+
port = yield self.closedPort()
|
|
128
|
+
yield self.assertFailure(
|
|
129
|
+
redis.Connection("127.0.0.1", port, reconnect=False), ValueError)
|
|
130
|
+
|
|
131
|
+
@defer.inlineCallbacks
|
|
132
|
+
def test_first_failure_is_final(self):
|
|
133
|
+
# long enough for a second attempt to be noticed as a hang
|
|
134
|
+
self.patch(redis.RedisFactory, "initialDelay", 30)
|
|
135
|
+
|
|
136
|
+
endpoint = FailingEndpoint()
|
|
137
|
+
factory = redis.RedisFactory(None, dbid=None, poolsize=1)
|
|
138
|
+
factory.reconnect = False
|
|
139
|
+
deferred = factory.deferred
|
|
140
|
+
factory.startConnecting(endpoint)
|
|
141
|
+
|
|
142
|
+
yield self.assertFailure(deferred, ValueError)
|
|
143
|
+
self.assertEqual(endpoint.attempts, 1)
|
|
144
|
+
|
|
145
|
+
@defer.inlineCallbacks
|
|
146
|
+
def test_failed_pool_startup_closes_connections(self):
|
|
147
|
+
gate = defer.Deferred()
|
|
148
|
+
endpoint = GatedEndpoint(self.endpoint(), gate)
|
|
149
|
+
|
|
150
|
+
factory = redis.RedisFactory(None, dbid=None, poolsize=2)
|
|
151
|
+
factory.reconnect = False
|
|
152
|
+
deferred = factory.deferred
|
|
153
|
+
factory.startConnecting(endpoint)
|
|
154
|
+
|
|
155
|
+
yield self.waitFor(lambda: factory.size == 1,
|
|
156
|
+
"the first connection has not been established")
|
|
157
|
+
gate.callback(None)
|
|
158
|
+
|
|
159
|
+
yield self.assertFailure(deferred, ValueError)
|
|
160
|
+
|
|
161
|
+
# the caller got an error instead of the handler, so it can't close
|
|
162
|
+
# the connection that did come up - we have to
|
|
163
|
+
yield self.waitFor(lambda: not self.server.protocols[0].connected,
|
|
164
|
+
"the established connection has not been closed")
|
|
165
|
+
|
|
166
|
+
@defer.inlineCallbacks
|
|
167
|
+
def test_failed_lazy_pool_keeps_working_connections(self):
|
|
168
|
+
gate = defer.Deferred()
|
|
169
|
+
endpoint = GatedEndpoint(self.endpoint(), gate)
|
|
170
|
+
|
|
171
|
+
factory = redis.RedisFactory(None, dbid=None, poolsize=2, isLazy=True)
|
|
172
|
+
factory.reconnect = False
|
|
173
|
+
# nobody consumes the Deferred of a lazy factory
|
|
174
|
+
factory.deferred.addErrback(lambda failure: None)
|
|
175
|
+
factory.startConnecting(endpoint)
|
|
176
|
+
|
|
177
|
+
yield self.waitFor(lambda: factory.size == 1,
|
|
178
|
+
"the first connection has not been established")
|
|
179
|
+
gate.callback(None)
|
|
180
|
+
yield task.deferLater(reactor, 0.3, lambda: None)
|
|
181
|
+
|
|
182
|
+
# the handler is in the caller's hands already, so the connection that
|
|
183
|
+
# did come up stays usable
|
|
184
|
+
self.assertEqual(factory.size, 1)
|
|
185
|
+
role = yield factory.handler.role()
|
|
186
|
+
self.assertEqual(role[0], "master")
|
|
187
|
+
|
|
188
|
+
yield factory.handler.disconnect()
|
|
189
|
+
|
|
190
|
+
@defer.inlineCallbacks
|
|
191
|
+
def test_disconnect_cancels_reconnection(self):
|
|
192
|
+
# long enough for the reconnection to still be scheduled below
|
|
193
|
+
self.patch(redis.RedisFactory, "initialDelay", 0.5)
|
|
194
|
+
|
|
195
|
+
db = yield redis.Connection("127.0.0.1", self.port)
|
|
196
|
+
yield db.role()
|
|
197
|
+
|
|
198
|
+
self.server.protocols[0].transport.loseConnection()
|
|
199
|
+
yield self.waitFor(lambda: db._factory.size == 0,
|
|
200
|
+
"connection has not been dropped")
|
|
201
|
+
|
|
202
|
+
# a scheduled reconnection attempt must not survive disconnect()
|
|
203
|
+
yield db.disconnect()
|
|
204
|
+
yield task.deferLater(reactor, 1, lambda: None)
|
|
205
|
+
self.assertEqual(len(self.server.protocols), 1)
|
|
206
|
+
|
|
207
|
+
@defer.inlineCallbacks
|
|
208
|
+
def test_stop_trying_from_connection_made(self):
|
|
209
|
+
# the pattern examples/subscriber.py shows: give up on the connection
|
|
210
|
+
# from inside connectionMade, while the endpoint is still finishing
|
|
211
|
+
# it. Neither hook chains to the base protocol, the way protocols
|
|
212
|
+
# written for a SubscriberFactory usually don't
|
|
213
|
+
class StoppingProtocol(redis.RedisProtocol):
|
|
214
|
+
def connectionMade(self):
|
|
215
|
+
self.factory.stopTrying()
|
|
216
|
+
self.transport.loseConnection()
|
|
217
|
+
|
|
218
|
+
def connectionLost(self, reason):
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
factory = redis.RedisFactory(None, dbid=None, poolsize=1)
|
|
222
|
+
factory.protocol = StoppingProtocol
|
|
223
|
+
factory.startConnecting(self.endpoint())
|
|
224
|
+
|
|
225
|
+
yield self.waitFor(lambda: self.server.protocols and
|
|
226
|
+
not self.server.protocols[0].connected,
|
|
227
|
+
"the connection has not been closed")
|
|
228
|
+
|
|
229
|
+
yield task.deferLater(reactor, 0.3, lambda: None)
|
|
230
|
+
self.assertEqual(len(self.server.protocols), 1)
|
|
231
|
+
|
|
232
|
+
def test_dead_connection_is_not_pooled(self):
|
|
233
|
+
factory = redis.RedisFactory(None, dbid=None, poolsize=1)
|
|
234
|
+
conn = factory.buildProtocol(None)
|
|
235
|
+
# died during the reactor turn addConnection is delayed by
|
|
236
|
+
conn.connected = 0
|
|
237
|
+
|
|
238
|
+
factory.addConnection(conn)
|
|
239
|
+
self.assertEqual(factory.size, 0)
|
|
240
|
+
|
|
241
|
+
def test_retry_delay_does_not_overflow(self):
|
|
242
|
+
factory = redis.RedisFactory(None, dbid=None, poolsize=1)
|
|
243
|
+
# a long outage keeps increasing the attempt number
|
|
244
|
+
self.assertLess(factory.retryDelay(10 ** 6), factory.maxDelay * 2)
|