Skip to content

Commit 74e0538

Browse files
authored
Fix mqtt3 client shutdown callback (#498)
1 parent 8523a84 commit 74e0538

File tree

5 files changed

+605
-162
lines changed

5 files changed

+605
-162
lines changed

source/mqtt5_client.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,24 @@ struct mqtt5_client_binding {
7878
/* Called on either failed client creation or by the client upon normal client termination */
7979
static void s_mqtt5_client_on_terminate(void *user_data) {
8080
struct mqtt5_client_binding *client = user_data;
81+
82+
PyGILState_STATE state;
83+
if (aws_py_gilstate_ensure(&state)) {
84+
return; /* Python has shut down. Nothing matters anymore, but don't crash */
85+
}
86+
if (client->client_core != NULL) {
87+
// Make sure to release the python client object
88+
Py_XDECREF(client->client_core);
89+
}
8190
aws_mem_release(aws_py_get_allocator(), client);
91+
PyGILState_Release(state);
8292
}
8393

8494
/* Called when capsule's refcount hits 0 */
8595
static void s_mqtt5_python_client_destructor(PyObject *client_capsule) {
8696
struct mqtt5_client_binding *client = PyCapsule_GetPointer(client_capsule, s_capsule_name_mqtt5_client);
8797
assert(client);
8898

89-
Py_XDECREF(client->client_core);
90-
9199
if (client->native != NULL) {
92100
/* If client is not NULL, it can be shutdown and cleaned normally */
93101
aws_mqtt5_client_release(client->native);

source/mqtt_client_connection.c

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,6 @@ struct mqtt_connection_binding {
4949
* Lets us invoke callbacks on the python object without preventing the GC from cleaning it up. */
5050
PyObject *self_proxy;
5151

52-
/* To not run into a segfault calling on_close with the connection being freed before the callback
53-
* can be invoked, we need to keep the PyCapsule alive. */
54-
PyObject *self_capsule;
55-
5652
PyObject *on_connect;
5753
PyObject *on_any_publish;
5854

@@ -62,24 +58,25 @@ struct mqtt_connection_binding {
6258

6359
static void s_mqtt_python_connection_finish_destruction(struct mqtt_connection_binding *py_connection) {
6460

65-
/* Do not call the on_stopped callback if the python object is finished/destroyed */
66-
aws_mqtt_client_connection_set_connection_closed_handler(py_connection->native, NULL, NULL);
67-
68-
aws_mqtt_client_connection_release(py_connection->native);
69-
7061
Py_DECREF(py_connection->self_proxy);
7162
Py_DECREF(py_connection->client);
7263
Py_XDECREF(py_connection->on_any_publish);
7364

7465
aws_mem_release(aws_py_get_allocator(), py_connection);
7566
}
7667

77-
static void s_mqtt_python_connection_destructor_on_disconnect(
78-
struct aws_mqtt_client_connection *connection,
79-
void *userdata) {
68+
static void s_start_destroy_native(struct mqtt_connection_binding *py_connection) {
69+
if (py_connection == NULL || py_connection->native == NULL) {
70+
return;
71+
}
8072

81-
if (connection == NULL || userdata == NULL) {
82-
return; // The connection is dead - skip!
73+
aws_mqtt_client_connection_release(py_connection->native);
74+
}
75+
76+
static void s_mqtt_python_connection_termination(void *userdata) {
77+
78+
if (userdata == NULL) {
79+
return; // The binding is dead - skip!
8380
}
8481

8582
struct mqtt_connection_binding *py_connection = userdata;
@@ -93,20 +90,36 @@ static void s_mqtt_python_connection_destructor_on_disconnect(
9390
PyGILState_Release(state);
9491
}
9592

93+
static void s_mqtt_python_connection_destructor_on_disconnect(
94+
struct aws_mqtt_client_connection *connection,
95+
void *user_data) {
96+
if (connection == NULL || user_data == NULL) {
97+
return; // The connection is dead - skip!
98+
}
99+
100+
struct mqtt_connection_binding *py_connection = user_data;
101+
PyGILState_STATE state;
102+
if (aws_py_gilstate_ensure(&state)) {
103+
return; /* Python has shut down. Nothing matters anymore, but don't crash */
104+
}
105+
s_start_destroy_native(py_connection);
106+
PyGILState_Release(state);
107+
}
108+
96109
static void s_mqtt_python_connection_destructor(PyObject *connection_capsule) {
97110

98111
struct mqtt_connection_binding *py_connection =
99112
PyCapsule_GetPointer(connection_capsule, s_capsule_name_mqtt_client_connection);
100-
assert(py_connection);
113+
AWS_FATAL_ASSERT(py_connection);
114+
AWS_FATAL_ASSERT(py_connection->native);
101115

102116
/* This is the destructor from Python - so we can ignore the closed callback here */
103117
aws_mqtt_client_connection_set_connection_closed_handler(py_connection->native, NULL, NULL);
104118

105119
if (aws_mqtt_client_connection_disconnect(
106120
py_connection->native, s_mqtt_python_connection_destructor_on_disconnect, py_connection)) {
107-
108-
/* If this returns an error, we should immediately destroy the connection */
109-
s_mqtt_python_connection_finish_destruction(py_connection);
121+
/* If we already disconnected, we should immediately release the native connection */
122+
s_start_destroy_native(py_connection);
110123
}
111124
}
112125

@@ -254,15 +267,6 @@ static void s_on_connection_closed(
254267
PyErr_WriteUnraisable(PyErr_Occurred());
255268
}
256269
}
257-
Py_DECREF(py_connection->self_proxy);
258-
259-
/** Allow the PyCapsule to be freed like normal again.
260-
* If this is the last reference (I.E customer code called disconnect and threw the Python object away)
261-
* Then this will allow the MQTT311 class to be fully cleaned.
262-
* If it is not the last reference (customer still has reference) then when the customer is done
263-
* it will be freed like normal.
264-
**/
265-
Py_DECREF(py_connection->self_capsule);
266270

267271
PyGILState_Release(state);
268272
}
@@ -272,6 +276,7 @@ PyObject *aws_py_mqtt_client_connection_new(PyObject *self, PyObject *args) {
272276

273277
struct aws_allocator *allocator = aws_py_get_allocator();
274278

279+
PyObject *self_proxy;
275280
PyObject *self_py;
276281
PyObject *client_py;
277282
PyObject *use_websocket_py;
@@ -310,13 +315,19 @@ PyObject *aws_py_mqtt_client_connection_new(PyObject *self, PyObject *args) {
310315
}
311316
if (!py_connection->native) {
312317
PyErr_SetAwsLastError();
313-
goto connection_new_failed;
318+
goto on_error;
319+
}
320+
321+
if (aws_mqtt_client_connection_set_connection_termination_handler(
322+
py_connection->native, s_mqtt_python_connection_termination, py_connection)) {
323+
PyErr_SetAwsLastError();
324+
goto on_error;
314325
}
315326

316327
if (aws_mqtt_client_connection_set_connection_result_handlers(
317328
py_connection->native, s_on_connection_success, py_connection, s_on_connection_failure, py_connection)) {
318329
PyErr_SetAwsLastError();
319-
goto set_connection_handlers_failed;
330+
goto on_error;
320331
}
321332

322333
if (aws_mqtt_client_connection_set_connection_interruption_handlers(
@@ -327,13 +338,13 @@ PyObject *aws_py_mqtt_client_connection_new(PyObject *self, PyObject *args) {
327338
py_connection)) {
328339

329340
PyErr_SetAwsLastError();
330-
goto set_interruption_failed;
341+
goto on_error;
331342
}
332343

333344
if (aws_mqtt_client_connection_set_connection_closed_handler(
334345
py_connection->native, s_on_connection_closed, py_connection)) {
335346
PyErr_SetAwsLastError();
336-
goto set_interruption_failed;
347+
goto on_error;
337348
}
338349

339350
if (PyObject_IsTrue(use_websocket_py)) {
@@ -345,39 +356,32 @@ PyObject *aws_py_mqtt_client_connection_new(PyObject *self, PyObject *args) {
345356
NULL /*validator userdata*/)) {
346357

347358
PyErr_SetAwsLastError();
348-
goto use_websockets_failed;
359+
goto on_error;
349360
}
350361
}
351362

352-
PyObject *self_proxy = PyWeakref_NewProxy(self_py, NULL);
363+
self_proxy = PyWeakref_NewProxy(self_py, NULL);
353364
if (!self_proxy) {
354-
goto proxy_new_failed;
365+
goto on_error;
355366
}
356367

357368
PyObject *capsule =
358369
PyCapsule_New(py_connection, s_capsule_name_mqtt_client_connection, s_mqtt_python_connection_destructor);
359370
if (!capsule) {
360-
goto capsule_new_failed;
371+
goto on_error;
361372
}
362373

363374
/* From hereon, nothing will fail */
364-
365-
py_connection->self_capsule = capsule;
366375
py_connection->self_proxy = self_proxy;
367376

368377
py_connection->client = client_py;
369378
Py_INCREF(py_connection->client);
370379

371380
return capsule;
372381

373-
capsule_new_failed:
374-
Py_DECREF(self_proxy);
375-
proxy_new_failed:
376-
use_websockets_failed:
377-
set_interruption_failed:
378-
set_connection_handlers_failed:
382+
on_error:
383+
Py_XDECREF(self_proxy);
379384
aws_mqtt_client_connection_release(py_connection->native);
380-
connection_new_failed:
381385
aws_mem_release(allocator, py_connection);
382386
return NULL;
383387
}
@@ -1329,14 +1333,10 @@ PyObject *aws_py_mqtt_client_connection_disconnect(PyObject *self, PyObject *arg
13291333
}
13301334

13311335
Py_INCREF(on_disconnect);
1332-
Py_INCREF(connection->self_proxy); /* We need to keep self_proxy alive for on_closed, which will dec-ref this */
1333-
Py_INCREF(connection->self_capsule); /* Do not allow the PyCapsule to be freed, we need it alive for on_closed */
13341336

13351337
int err = aws_mqtt_client_connection_disconnect(connection->native, s_on_disconnect, on_disconnect);
13361338
if (err) {
13371339
Py_DECREF(on_disconnect);
1338-
Py_DECREF(connection->self_proxy);
1339-
Py_DECREF(connection->self_capsule);
13401340
return PyErr_AwsLastError();
13411341
}
13421342

test/test_mqtt5.py

Lines changed: 0 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,117 +1549,6 @@ def test_operation_statistics_uc1(self):
15491549
client.stop()
15501550
callbacks.future_stopped.result(TIMEOUT)
15511551

1552-
# ==============================================================
1553-
# 5to3 ADAPTER TEST CASES
1554-
# ==============================================================
1555-
def test_5to3Adapter_connection_creation_minimum(self):
1556-
client5 = self._create_client()
1557-
connection = client5.new_connection()
1558-
1559-
def test_5to3Adapter_connection_creation_maximum(self):
1560-
input_host_name = _get_env_variable("AWS_TEST_MQTT5_IOT_CORE_HOST")
1561-
1562-
user_properties = []
1563-
user_properties.append(mqtt5.UserProperty(name="name1", value="value1"))
1564-
user_properties.append(mqtt5.UserProperty(name="name2", value="value2"))
1565-
1566-
publish_packet = mqtt5.PublishPacket(
1567-
payload="TEST_PAYLOAD",
1568-
qos=mqtt5.QoS.AT_LEAST_ONCE,
1569-
retain=False,
1570-
topic="TEST_TOPIC",
1571-
payload_format_indicator=mqtt5.PayloadFormatIndicator.AWS_MQTT5_PFI_UTF8,
1572-
message_expiry_interval_sec=10,
1573-
topic_alias=1,
1574-
response_topic="TEST_RESPONSE_TOPIC",
1575-
correlation_data="TEST_CORRELATION_DATA",
1576-
content_type="TEST_CONTENT_TYPE",
1577-
user_properties=user_properties
1578-
)
1579-
1580-
connect_options = mqtt5.ConnectPacket(
1581-
keep_alive_interval_sec=10,
1582-
client_id="TEST_CLIENT",
1583-
username="USERNAME",
1584-
password="PASSWORD",
1585-
session_expiry_interval_sec=100,
1586-
request_response_information=1,
1587-
request_problem_information=1,
1588-
receive_maximum=1000,
1589-
maximum_packet_size=10000,
1590-
will_delay_interval_sec=1000,
1591-
will=publish_packet,
1592-
user_properties=user_properties
1593-
)
1594-
client_options = mqtt5.ClientOptions(
1595-
host_name=input_host_name,
1596-
port=8883,
1597-
connect_options=connect_options,
1598-
session_behavior=mqtt5.ClientSessionBehaviorType.CLEAN,
1599-
extended_validation_and_flow_control_options=mqtt5.ExtendedValidationAndFlowControlOptions.AWS_IOT_CORE_DEFAULTS,
1600-
offline_queue_behavior=mqtt5.ClientOperationQueueBehaviorType.FAIL_ALL_ON_DISCONNECT,
1601-
retry_jitter_mode=mqtt5.ExponentialBackoffJitterMode.DECORRELATED,
1602-
min_reconnect_delay_ms=100,
1603-
max_reconnect_delay_ms=50000,
1604-
min_connected_time_to_reset_reconnect_delay_ms=1000,
1605-
ping_timeout_ms=1000,
1606-
connack_timeout_ms=1000,
1607-
ack_timeout_sec=100)
1608-
client = self._create_client(client_options=client_options)
1609-
connection = client.new_connection()
1610-
1611-
def test_5to3Adapter_direct_connect_minimum(self):
1612-
input_host_name = _get_env_variable("AWS_TEST_MQTT5_DIRECT_MQTT_HOST")
1613-
input_port = int(_get_env_variable("AWS_TEST_MQTT5_DIRECT_MQTT_PORT"))
1614-
1615-
client_options = mqtt5.ClientOptions(
1616-
host_name=input_host_name,
1617-
port=input_port
1618-
)
1619-
callbacks = Mqtt5TestCallbacks()
1620-
client = self._create_client(client_options=client_options, callbacks=callbacks)
1621-
1622-
connection = client.new_connection()
1623-
connection.connect().result(TIMEOUT)
1624-
connection.disconnect().result(TIMEOUT)
1625-
1626-
def test_5to3Adapter_websocket_connect_minimum(self):
1627-
input_host_name = _get_env_variable("AWS_TEST_MQTT5_WS_MQTT_HOST")
1628-
input_port = int(_get_env_variable("AWS_TEST_MQTT5_WS_MQTT_PORT"))
1629-
1630-
client_options = mqtt5.ClientOptions(
1631-
host_name=input_host_name,
1632-
port=input_port
1633-
)
1634-
callbacks = Mqtt5TestCallbacks()
1635-
client_options.websocket_handshake_transform = callbacks.ws_handshake_transform
1636-
1637-
client = self._create_client(client_options=client_options, callbacks=callbacks)
1638-
connection = client.new_connection()
1639-
connection.connect().result(TIMEOUT)
1640-
callbacks.future_connection_success.result(TIMEOUT)
1641-
connection.disconnect().result(TIMEOUT)
1642-
1643-
def test_5to3Adapter_direct_connect_mutual_tls(self):
1644-
input_host_name = _get_env_variable("AWS_TEST_MQTT5_IOT_CORE_HOST")
1645-
input_cert = _get_env_variable("AWS_TEST_MQTT5_IOT_CORE_RSA_CERT")
1646-
input_key = _get_env_variable("AWS_TEST_MQTT5_IOT_CORE_RSA_KEY")
1647-
1648-
client_options = mqtt5.ClientOptions(
1649-
host_name=input_host_name,
1650-
port=8883
1651-
)
1652-
tls_ctx_options = io.TlsContextOptions.create_client_with_mtls_from_path(
1653-
input_cert,
1654-
input_key
1655-
)
1656-
client_options.tls_ctx = io.ClientTlsContext(tls_ctx_options)
1657-
callbacks = Mqtt5TestCallbacks()
1658-
client = self._create_client(client_options=client_options, callbacks=callbacks)
1659-
connection = client.new_connection()
1660-
connection.connect().result(TIMEOUT)
1661-
connection.disconnect().result(TIMEOUT)
1662-
16631552

16641553
if __name__ == 'main':
16651554
unittest.main()

0 commit comments

Comments
 (0)