txredisapi 1.5.0__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.5.0/txredisapi.egg-info → txredisapi-1.6.0}/PKG-INFO +2 -2
- {txredisapi-1.5.0 → txredisapi-1.6.0}/setup.py +2 -2
- txredisapi-1.6.0/tests/test_ipv6.py +98 -0
- txredisapi-1.6.0/tests/test_reconnect.py +244 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sentinel.py +72 -10
- txredisapi-1.6.0/tests/test_ssl.py +101 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_subscriber.py +7 -4
- {txredisapi-1.5.0 → txredisapi-1.6.0/txredisapi.egg-info}/PKG-INFO +2 -2
- {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.egg-info/SOURCES.txt +3 -0
- txredisapi-1.6.0/txredisapi.egg-info/requires.txt +1 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.py +384 -114
- txredisapi-1.5.0/txredisapi.egg-info/requires.txt +0 -1
- {txredisapi-1.5.0 → txredisapi-1.6.0}/LICENSE +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/MANIFEST.in +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/README.md +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/setup.cfg +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/__init__.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/mixins.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_basics.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_bitops.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_blocking.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_cancel.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_connection.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_connection_charset.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_hash_ops.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_hyperloglog.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_implicit_pipelining.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_list_ops.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_multibulk.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_number_conversions.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_operations.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_pipelining.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_protocol.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_publish.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_scan.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_scripting.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sets.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sort.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_sortedsets.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_transactions.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_unix_connection.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/tests/test_watch.py +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.egg-info/dependency_links.txt +0 -0
- {txredisapi-1.5.0 → txredisapi-1.6.0}/txredisapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
|
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
22
22
|
Requires-Python: >=3.5
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: twisted[tls]
|
|
25
|
+
Requires-Dist: twisted[tls]>=18.7.0
|
|
26
26
|
Dynamic: author
|
|
27
27
|
Dynamic: author-email
|
|
28
28
|
Dynamic: classifier
|
|
@@ -23,9 +23,9 @@ 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[tls]"],
|
|
28
|
+
install_requires=["twisted[tls]>=18.7.0"],
|
|
29
29
|
python_requires=">=3.5",
|
|
30
30
|
author="Alexandre Fiori",
|
|
31
31
|
author_email="fiorix@gmail.com",
|
|
@@ -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")
|
|
@@ -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)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import sys
|
|
2
|
-
from twisted.internet import defer, reactor
|
|
2
|
+
from twisted.internet import defer, reactor, task
|
|
3
3
|
from twisted.internet.protocol import Factory
|
|
4
4
|
from twisted.trial.unittest import TestCase
|
|
5
5
|
|
|
@@ -106,6 +106,18 @@ class FakeRedisFactory(Factory):
|
|
|
106
106
|
role = ["master", 0, ["127.0.0.1", 63791, 0]]
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
class TrackingRedisFactory(FakeRedisFactory):
|
|
110
|
+
"""Fake server that keeps every protocol it has built."""
|
|
111
|
+
|
|
112
|
+
def __init__(self):
|
|
113
|
+
self.protocols = []
|
|
114
|
+
|
|
115
|
+
def buildProtocol(self, addr):
|
|
116
|
+
protocol = FakeRedisFactory.buildProtocol(self, addr)
|
|
117
|
+
self.protocols.append(protocol)
|
|
118
|
+
return protocol
|
|
119
|
+
|
|
120
|
+
|
|
109
121
|
class FakeAuthenticatedRedisFactory(FakeRedisFactory):
|
|
110
122
|
def __init__(self, requirepass):
|
|
111
123
|
self.requirepass = requirepass
|
|
@@ -297,6 +309,19 @@ class TestConnectViaSentinel(TestCase):
|
|
|
297
309
|
reactor.callLater(secs, d.callback, None)
|
|
298
310
|
return d
|
|
299
311
|
|
|
312
|
+
@defer.inlineCallbacks
|
|
313
|
+
def _waitFor(self, predicate, message):
|
|
314
|
+
deadline = reactor.seconds() + 5
|
|
315
|
+
while not predicate():
|
|
316
|
+
if reactor.seconds() > deadline:
|
|
317
|
+
self.fail(message)
|
|
318
|
+
yield self._delay(0.02)
|
|
319
|
+
|
|
320
|
+
def _pool_is_at(self, conn, port):
|
|
321
|
+
pool = conn._factory.pool
|
|
322
|
+
return len(pool) == 3 and all(p.transport.getPeer().port == port
|
|
323
|
+
for p in pool)
|
|
324
|
+
|
|
300
325
|
@defer.inlineCallbacks
|
|
301
326
|
def test_drop_all_when_master_changes(self):
|
|
302
327
|
# When master address change detected, factory should drop and reestablish
|
|
@@ -304,10 +329,8 @@ class TestConnectViaSentinel(TestCase):
|
|
|
304
329
|
|
|
305
330
|
conn = self.client.master_for("test", poolsize=3)
|
|
306
331
|
yield conn.role() # wait for connection
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
self.assertTrue(len(addrs), 3)
|
|
310
|
-
self.assertTrue(all(addr.port == self.master_port for addr in addrs))
|
|
332
|
+
yield self._waitFor(lambda: self._pool_is_at(conn, self.master_port),
|
|
333
|
+
"the pool has not been filled from the master")
|
|
311
334
|
|
|
312
335
|
# Change master address at sentinel and change role of the slave to master
|
|
313
336
|
self.fake_sentinel.master_addr = ("127.0.0.1", self.slave_port)
|
|
@@ -316,11 +339,9 @@ class TestConnectViaSentinel(TestCase):
|
|
|
316
339
|
# Force reconnection of one connection
|
|
317
340
|
conn._factory.pool[0].transport.loseConnection()
|
|
318
341
|
|
|
319
|
-
#
|
|
320
|
-
yield self.
|
|
321
|
-
|
|
322
|
-
self.assertTrue(len(addrs), 3)
|
|
323
|
-
self.assertTrue(all(addr.port == self.slave_port for addr in addrs))
|
|
342
|
+
# All of the connections should end up at the new master
|
|
343
|
+
yield self._waitFor(lambda: self._pool_is_at(conn, self.slave_port),
|
|
344
|
+
"the connections have not moved to the new master")
|
|
324
345
|
|
|
325
346
|
yield conn.disconnect()
|
|
326
347
|
|
|
@@ -355,3 +376,44 @@ class TestAuthViaSentinel(TestCase):
|
|
|
355
376
|
reply = yield conn.role()
|
|
356
377
|
self.assertEqual(reply[0], "master")
|
|
357
378
|
yield conn.disconnect()
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class TestSentinelRetries(TestCase):
|
|
382
|
+
|
|
383
|
+
master_port = 36379
|
|
384
|
+
sentinel_port = 46379
|
|
385
|
+
|
|
386
|
+
def setUp(self):
|
|
387
|
+
self.fake_master = TrackingRedisFactory()
|
|
388
|
+
self.fake_master.role = ["slave", "127.0.0.1", 63790, "connected", 0]
|
|
389
|
+
self.master_listener = reactor.listenTCP(self.master_port,
|
|
390
|
+
self.fake_master)
|
|
391
|
+
|
|
392
|
+
self.fake_sentinel = FakeSentinelFactory()
|
|
393
|
+
self.fake_sentinel.master_addr = ("127.0.0.1", self.master_port)
|
|
394
|
+
self.fake_sentinel.slave_addrs = []
|
|
395
|
+
self.fake_sentinel.slave_flags = []
|
|
396
|
+
self.sentinel_listener = reactor.listenTCP(self.sentinel_port,
|
|
397
|
+
self.fake_sentinel)
|
|
398
|
+
|
|
399
|
+
self.client = Sentinel([("127.0.0.1", self.sentinel_port)])
|
|
400
|
+
self.client.discovery_timeout = 1
|
|
401
|
+
|
|
402
|
+
@defer.inlineCallbacks
|
|
403
|
+
def tearDown(self):
|
|
404
|
+
yield self.client.disconnect()
|
|
405
|
+
yield self.sentinel_listener.stopListening()
|
|
406
|
+
yield self.master_listener.stopListening()
|
|
407
|
+
|
|
408
|
+
@defer.inlineCallbacks
|
|
409
|
+
def test_wrong_role_backs_off(self):
|
|
410
|
+
# A node that never has the role we are looking for is dropped by
|
|
411
|
+
# SentinelRedisProtocol as soon as it replies to ROLE. Such a
|
|
412
|
+
# connection must not reset the backoff, or we would reconnect to it
|
|
413
|
+
# in a tight loop
|
|
414
|
+
conn = self.client.master_for("test")
|
|
415
|
+
|
|
416
|
+
yield task.deferLater(reactor, 0.5, lambda: None)
|
|
417
|
+
self.assertLess(len(self.fake_master.protocols), 5)
|
|
418
|
+
|
|
419
|
+
yield conn.disconnect()
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
import datetime
|
|
16
|
+
|
|
17
|
+
from cryptography import x509
|
|
18
|
+
from cryptography.hazmat.primitives import hashes
|
|
19
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
20
|
+
from cryptography.x509.oid import NameOID
|
|
21
|
+
from OpenSSL import crypto
|
|
22
|
+
from twisted.internet import defer, reactor, ssl
|
|
23
|
+
from twisted.trial import unittest
|
|
24
|
+
|
|
25
|
+
import txredisapi as redis
|
|
26
|
+
|
|
27
|
+
from tests.test_sentinel import FakeRedisFactory
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_options = []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def selfSignedOptions():
|
|
34
|
+
"""
|
|
35
|
+
Server side TLS options with a self-signed certificate, built once because
|
|
36
|
+
generating a key is slow.
|
|
37
|
+
|
|
38
|
+
twisted.internet.ssl.KeyPair.selfSignedCert() would be shorter, but it
|
|
39
|
+
needs OpenSSL.crypto.X509Req, which recent pyOpenSSL releases no longer
|
|
40
|
+
have.
|
|
41
|
+
"""
|
|
42
|
+
if not _options:
|
|
43
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
44
|
+
name = x509.Name(
|
|
45
|
+
[x509.NameAttribute(NameOID.COMMON_NAME, u"localhost")])
|
|
46
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
47
|
+
certificate = (
|
|
48
|
+
x509.CertificateBuilder()
|
|
49
|
+
.subject_name(name)
|
|
50
|
+
.issuer_name(name)
|
|
51
|
+
.public_key(key.public_key())
|
|
52
|
+
.serial_number(1)
|
|
53
|
+
.not_valid_before(now - datetime.timedelta(days=1))
|
|
54
|
+
.not_valid_after(now + datetime.timedelta(days=1))
|
|
55
|
+
.sign(key, hashes.SHA256())
|
|
56
|
+
)
|
|
57
|
+
_options.append(ssl.CertificateOptions(
|
|
58
|
+
privateKey=crypto.PKey.from_cryptography_key(key),
|
|
59
|
+
certificate=crypto.X509.from_cryptography(certificate)))
|
|
60
|
+
|
|
61
|
+
return _options[0]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestSSLConnections(unittest.TestCase):
|
|
65
|
+
timeout = 15
|
|
66
|
+
|
|
67
|
+
def setUp(self):
|
|
68
|
+
self.listener = reactor.listenSSL(0, FakeRedisFactory(),
|
|
69
|
+
selfSignedOptions(),
|
|
70
|
+
interface="127.0.0.1")
|
|
71
|
+
self.port = self.listener.getHost().port
|
|
72
|
+
self.addCleanup(self.listener.stopListening)
|
|
73
|
+
|
|
74
|
+
@defer.inlineCallbacks
|
|
75
|
+
def test_context_factory(self):
|
|
76
|
+
db = yield redis.Connection(
|
|
77
|
+
"127.0.0.1", self.port, reconnect=False,
|
|
78
|
+
ssl_context_factory=ssl.ClientContextFactory())
|
|
79
|
+
self.addCleanup(db.disconnect)
|
|
80
|
+
|
|
81
|
+
role = yield db.role()
|
|
82
|
+
self.assertEqual(role[0], "master")
|
|
83
|
+
|
|
84
|
+
@defer.inlineCallbacks
|
|
85
|
+
def test_ssl_true(self):
|
|
86
|
+
db = yield redis.Connection("127.0.0.1", self.port, reconnect=False,
|
|
87
|
+
ssl_context_factory=True)
|
|
88
|
+
self.addCleanup(db.disconnect)
|
|
89
|
+
|
|
90
|
+
role = yield db.role()
|
|
91
|
+
self.assertEqual(role[0], "master")
|
|
92
|
+
|
|
93
|
+
@defer.inlineCallbacks
|
|
94
|
+
def test_pool(self):
|
|
95
|
+
db = yield redis.ConnectionPool("127.0.0.1", self.port, poolsize=2,
|
|
96
|
+
reconnect=False,
|
|
97
|
+
ssl_context_factory=True)
|
|
98
|
+
self.addCleanup(db.disconnect)
|
|
99
|
+
|
|
100
|
+
role = yield db.role()
|
|
101
|
+
self.assertEqual(role[0], "master")
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
15
|
from twisted.internet import defer, reactor
|
|
16
|
+
from twisted.internet.endpoints import HostnameEndpoint
|
|
16
17
|
from twisted.trial import unittest
|
|
17
18
|
|
|
18
19
|
import txredisapi as redis
|
|
@@ -24,8 +25,9 @@ class TestSubscriberProtocol(unittest.TestCase):
|
|
|
24
25
|
@defer.inlineCallbacks
|
|
25
26
|
def setUp(self):
|
|
26
27
|
factory = redis.SubscriberFactory()
|
|
27
|
-
factory.
|
|
28
|
-
|
|
28
|
+
factory.reconnect = False
|
|
29
|
+
factory.startConnecting(
|
|
30
|
+
HostnameEndpoint(reactor, REDIS_HOST, REDIS_PORT))
|
|
29
31
|
self.db = yield factory.deferred
|
|
30
32
|
|
|
31
33
|
@defer.inlineCallbacks
|
|
@@ -125,8 +127,9 @@ class TestAuthenticatedSubscriberProtocol(unittest.TestCase):
|
|
|
125
127
|
factory = redis.RedisFactory(None, dbid=0, poolsize=1,
|
|
126
128
|
password="password")
|
|
127
129
|
factory.protocol = redis.SubscriberProtocol
|
|
128
|
-
factory.
|
|
129
|
-
|
|
130
|
+
factory.reconnect = False
|
|
131
|
+
factory.startConnecting(
|
|
132
|
+
HostnameEndpoint(reactor, REDIS_HOST, REDIS_PORT))
|
|
130
133
|
self.db = yield factory.deferred
|
|
131
134
|
|
|
132
135
|
@defer.inlineCallbacks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
|
@@ -22,7 +22,7 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
|
22
22
|
Requires-Python: >=3.5
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
|
25
|
-
Requires-Dist: twisted[tls]
|
|
25
|
+
Requires-Dist: twisted[tls]>=18.7.0
|
|
26
26
|
Dynamic: author
|
|
27
27
|
Dynamic: author-email
|
|
28
28
|
Dynamic: classifier
|
|
@@ -15,6 +15,7 @@ tests/test_connection_charset.py
|
|
|
15
15
|
tests/test_hash_ops.py
|
|
16
16
|
tests/test_hyperloglog.py
|
|
17
17
|
tests/test_implicit_pipelining.py
|
|
18
|
+
tests/test_ipv6.py
|
|
18
19
|
tests/test_list_ops.py
|
|
19
20
|
tests/test_multibulk.py
|
|
20
21
|
tests/test_number_conversions.py
|
|
@@ -22,12 +23,14 @@ tests/test_operations.py
|
|
|
22
23
|
tests/test_pipelining.py
|
|
23
24
|
tests/test_protocol.py
|
|
24
25
|
tests/test_publish.py
|
|
26
|
+
tests/test_reconnect.py
|
|
25
27
|
tests/test_scan.py
|
|
26
28
|
tests/test_scripting.py
|
|
27
29
|
tests/test_sentinel.py
|
|
28
30
|
tests/test_sets.py
|
|
29
31
|
tests/test_sort.py
|
|
30
32
|
tests/test_sortedsets.py
|
|
33
|
+
tests/test_ssl.py
|
|
31
34
|
tests/test_subscriber.py
|
|
32
35
|
tests/test_transactions.py
|
|
33
36
|
tests/test_unix_connection.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
twisted[tls]>=18.7.0
|