Skip to content

Commit 5049dfd

Browse files
author
nick
committed
Added integration test
1 parent 70beac2 commit 5049dfd

File tree

2 files changed

+85
-1
lines changed

2 files changed

+85
-1
lines changed

tests/integration/simulacron/test_connection.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from cassandra.io.asyncorereactor import AsyncoreConnection
2929
from tests import connection_class, thread_pool_executor_class
3030
from tests.integration import (PROTOCOL_VERSION, requiressimulacron)
31-
from tests.integration.util import assert_quiescent_pool_state
31+
from tests.integration.util import assert_quiescent_pool_state, late
3232
from tests.integration.simulacron import SimulacronBase
3333
from tests.integration.simulacron.utils import (NO_THEN, PrimeOptions,
3434
prime_query, prime_request,
@@ -178,6 +178,79 @@ def test_callbacks_and_pool_when_oto(self):
178178
errback.assert_called_once()
179179
callback.assert_not_called()
180180

181+
def test_heartbeat_defunct_deadlock(self):
182+
"""
183+
Ensure that there is no deadlock when request is in-flight and heartbeat defuncts connection
184+
@since 3.16
185+
@jira_ticket PYTHON-1044
186+
@expected_result an OperationTimeout is raised and no deadlock occurs
187+
188+
@test_category connection
189+
"""
190+
start_and_prime_singledc()
191+
192+
# This is all about timing. We will need the QUERY response future to time out and the heartbeat to defunct
193+
# at the same moment. The latter will schedule a QUERY retry to another node in case the pool is not
194+
# already shut down. If and only if the response future timeout falls in between the retry scheduling and
195+
# its execution the deadlock occurs. The odds are low, so we need to help fate a bit:
196+
# 1) Make one heartbeat messages be sent to every node
197+
# 2) Our QUERY goes always to the same host
198+
# 3) This host needs to defunct first
199+
# 4) Open a small time window for the response future timeout, i.e. block executor threads for retry
200+
# execution and last connection to defunct
201+
query_to_prime = "SELECT * from testkesypace.testtable"
202+
query_host = "127.0.0.2"
203+
heartbeat_interval = 1
204+
heartbeat_timeout = 1
205+
lag = 0.05
206+
never = 9999
207+
208+
class PatchedRoundRobinPolicy(RoundRobinPolicy):
209+
# Send always to same host
210+
def make_query_plan(self, working_keyspace=None, query=None):
211+
print query
212+
print self._live_hosts
213+
if query and query.query_string == query_to_prime:
214+
return filter(lambda h: h == query_host, self._live_hosts)
215+
else:
216+
return super(PatchedRoundRobinPolicy, self).make_query_plan()
217+
218+
class PatchedCluster(Cluster):
219+
# Make sure that QUERY connection will timeout first
220+
def get_connection_holders(self):
221+
holders = super(PatchedCluster, self).get_connection_holders()
222+
return sorted(holders, reverse=True, key=lambda v: int(v._connection.host == query_host))
223+
224+
# Block executor thread like closing a dead socket could do
225+
def connection_factory(self, *args, **kwargs):
226+
conn = super(PatchedCluster, self).connection_factory(*args, **kwargs)
227+
conn.defunct = late(seconds=2*lag)(conn.defunct)
228+
return conn
229+
230+
cluster = PatchedCluster(
231+
protocol_version=PROTOCOL_VERSION,
232+
compression=False,
233+
idle_heartbeat_interval=heartbeat_interval,
234+
idle_heartbeat_timeout=heartbeat_timeout,
235+
load_balancing_policy=PatchedRoundRobinPolicy()
236+
)
237+
session = cluster.connect()
238+
self.addCleanup(cluster.shutdown)
239+
240+
prime_query(query_to_prime, then={"delay_in_ms": never})
241+
242+
# Make heartbeat due
243+
time.sleep(heartbeat_interval)
244+
245+
future = session.execute_async(query_to_prime, timeout=heartbeat_interval+heartbeat_timeout+3*lag)
246+
# Delay thread execution like kernel could do
247+
future._retry_task = late(seconds=4*lag)(future._retry_task)
248+
249+
prime_request(PrimeOptions(then={"result": "no_result", "delay_in_ms": never}))
250+
prime_request(RejectConnections("unbind"))
251+
252+
self.assertRaisesRegexp(OperationTimedOut, "Connection defunct by heartbeat", future.result)
253+
181254
def test_close_when_query(self):
182255
"""
183256
Test to ensure the driver behaves correctly if the connection is closed

tests/integration/util.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
from tests.integration import PROTOCOL_VERSION
16+
from functools import wraps
1617
import time
1718

1819

@@ -96,3 +97,13 @@ def wrapped_condition():
9697

9798
# last attempt, let the exception raise
9899
condition()
100+
101+
102+
def late(seconds=1):
103+
def decorator(func):
104+
@wraps(func)
105+
def wrapper(*args, **kwargs):
106+
time.sleep(seconds)
107+
func(*args, **kwargs)
108+
return wrapper
109+
return decorator

0 commit comments

Comments
 (0)