Skip to content

Commit 6468ae9

Browse files
authored
Manual PUBACK Control (#721)
1 parent 4173141 commit 6468ae9

5 files changed

Lines changed: 505 additions & 6 deletions

File tree

awscrt/mqtt5.py

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,13 @@ def _try_puback_reason_code(value):
497497
return None
498498

499499

500+
def _try_manual_puback_result(value):
501+
try:
502+
return ManualPubackResult(value)
503+
except Exception:
504+
return None
505+
506+
500507
class SubackReasonCode(IntEnum):
501508
"""Reason code inside SUBACK packet payloads.
502509
@@ -1224,14 +1231,38 @@ def set_done(self, exception=None):
12241231
self._done_future.set_exception(exception)
12251232

12261233

1234+
class PublishAcknowledgementControlHandle:
1235+
"""Opaque handle for manually controlling the publish acknowledgement for a received QoS 1 PUBLISH.
1236+
1237+
Obtained by calling acquire_publish_acknowledgement_control() within the on_publish_callback_fn callback.
1238+
Pass to Client.invoke_publish_acknowledgement() to send the publish acknowledgement to the broker.
1239+
"""
1240+
1241+
def __init__(self, control_id: int):
1242+
self._control_id = control_id
1243+
1244+
12271245
@dataclass
12281246
class PublishReceivedData:
12291247
"""Dataclass containing data related to a Publish Received Callback
12301248
12311249
Args:
12321250
publish_packet (PublishPacket): Data model of an `MQTT5 PUBLISH <https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Toc3901100>`_ packet.
1251+
acquire_publish_acknowledgement_control (Callable): For QoS 1 messages only: call this function within the
1252+
on_publish_callback_fn callback to take manual control of the publish acknowledgement for this message,
1253+
preventing the client from automatically sending a publish acknowledgement. Returns a
1254+
:class:`PublishAcknowledgementControlHandle` that can be passed to
1255+
Client.invoke_publish_acknowledgement() to send the publish acknowledgement to the broker.
1256+
1257+
Important: This function must be called within the on_publish_callback_fn callback. Calling it after the
1258+
callback returns will raise a RuntimeError. This function may only be called once per received PUBLISH;
1259+
calling it a second time will also raise a RuntimeError. If this function is not called, the client will
1260+
automatically send a publish acknowledgement for QoS 1 messages when the callback returns.
1261+
1262+
For QoS 0 messages, this field is None.
12331263
"""
12341264
publish_packet: PublishPacket = None
1265+
acquire_publish_acknowledgement_control: Callable = None
12351266

12361267

12371268
@dataclass
@@ -1440,9 +1471,11 @@ def _on_publish(
14401471
correlation_data,
14411472
subscription_identifiers_tuples,
14421473
content_type,
1443-
user_properties_tuples):
1474+
user_properties_tuples,
1475+
puback_control_id):
14441476
if self._on_publish_cb is None:
1445-
return
1477+
# Indicates that manual puback control was not taken and puback should be invoked automatically.
1478+
return False
14461479

14471480
publish_packet = PublishPacket()
14481481
publish_packet.topic = topic
@@ -1474,9 +1507,41 @@ def _on_publish(
14741507
publish_packet.content_type = content_type
14751508
publish_packet.user_properties = _init_user_properties(user_properties_tuples)
14761509

1477-
self._on_publish_cb(PublishReceivedData(publish_packet=publish_packet))
1510+
# For QoS 1 messages, set up the acquire_publish_acknowledgement_control callable.
1511+
# The native side has already called aws_mqtt5_client_acquire_publish_acknowledgement and passed us the
1512+
# puback_control_id. We wrap it in an opaque capsule handle and provide a callable that
1513+
# returns that handle. The callable may only be called once; calling it marks the publish
1514+
# acknowledgement as "taken" so the native side will not auto-invoke it after this callback returns.
1515+
puback_taken = False
1516+
callback_active = True
1517+
acquire_publish_acknowledgement_control = None
1518+
1519+
if puback_control_id != 0:
1520+
def acquire_publish_acknowledgement_control():
1521+
nonlocal puback_taken
1522+
nonlocal callback_active
1523+
if puback_taken:
1524+
raise RuntimeError(
1525+
"acquire_publish_acknowledgement_control() may only be called once per received PUBLISH.")
1526+
if not callback_active:
1527+
raise RuntimeError(
1528+
"acquire_publish_acknowledgement_control() must be called within the on_publish_callback_fn callback.")
1529+
puback_taken = True
1530+
return PublishAcknowledgementControlHandle(puback_control_id)
1531+
1532+
# Create PublishReceivedData with the acquire_publish_acknowledgement_control callable (or None for QoS 0)
1533+
publish_data = PublishReceivedData(
1534+
publish_packet=publish_packet,
1535+
acquire_publish_acknowledgement_control=acquire_publish_acknowledgement_control
1536+
)
1537+
1538+
self._on_publish_cb(publish_data)
14781539

1479-
return
1540+
callback_active = False
1541+
# Return True if the user called acquire_publish_acknowledgement_control(), signalling to the native side
1542+
# that it should NOT auto-invoke the publish acknowledgement (the user is responsible for calling
1543+
# invoke_publish_acknowledgement).
1544+
return puback_taken
14801545

14811546
def _on_lifecycle_stopped(self):
14821547
if self._on_lifecycle_stopped_cb:
@@ -1973,6 +2038,27 @@ def get_stats(self):
19732038
result = _awscrt.mqtt5_client_get_stats(self._binding)
19742039
return OperationStatisticsData(result[0], result[1], result[2], result[3])
19752040

2041+
def invoke_publish_acknowledgement(
2042+
self, publish_acknowledgement_control_handle: 'PublishAcknowledgementControlHandle'):
2043+
"""Sends a publish acknowledgement for a QoS 1 PUBLISH that was previously acquired for manual control.
2044+
2045+
To use manual publish acknowledgement control, call acquire_publish_acknowledgement_control() within
2046+
the on_publish_callback_fn callback to obtain a :class:`PublishAcknowledgementControlHandle`. Then call
2047+
this method to send the publish acknowledgement.
2048+
2049+
Args:
2050+
publish_acknowledgement_control_handle (PublishAcknowledgementControlHandle): An opaque handle obtained
2051+
from acquire_publish_acknowledgement_control() within PublishReceivedData.
2052+
2053+
Raises:
2054+
Exception: If the native client returns an error when invoking the publish acknowledgement.
2055+
"""
2056+
2057+
_awscrt.mqtt5_client_invoke_publish_acknowledgement(
2058+
self._binding,
2059+
publish_acknowledgement_control_handle._control_id
2060+
)
2061+
19762062
def new_connection(self, on_connection_interrupted=None, on_connection_resumed=None,
19772063
on_connection_success=None, on_connection_failure=None, on_connection_closed=None):
19782064
from awscrt.mqtt import Connection

source/module.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ static PyMethodDef s_module_methods[] = {
816816
AWS_PY_METHOD_DEF(mqtt5_client_subscribe, METH_VARARGS),
817817
AWS_PY_METHOD_DEF(mqtt5_client_unsubscribe, METH_VARARGS),
818818
AWS_PY_METHOD_DEF(mqtt5_client_get_stats, METH_VARARGS),
819+
AWS_PY_METHOD_DEF(mqtt5_client_invoke_publish_acknowledgement, METH_VARARGS),
819820
AWS_PY_METHOD_DEF(mqtt5_ws_handshake_transform_complete, METH_VARARGS),
820821

821822
/* MQTT Request Response Client */

source/mqtt5_client.c

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,26 @@ static void s_on_publish_received(const struct aws_mqtt5_packet_publish_view *pu
234234
PyObject *result = NULL;
235235
PyObject *subscription_identifier_list = NULL;
236236
PyObject *user_properties_list = NULL;
237+
PyObject *puback_control_id_py = NULL;
237238

238239
size_t subscription_identifier_count = publish_packet->subscription_identifier_count;
239240
size_t user_property_count = publish_packet->user_property_count;
240241

242+
/* For QoS 1 messages, take manual control of the publish acknowledgement immediately.
243+
* This gives us a pub_ack_control_id that we pass to Python as an opaque integer.
244+
* Python's _on_publish will return True if the user called acquire_publish_acknowledgement_control(),
245+
* in which case the user is responsible for calling invoke_publish_acknowledgement() later.
246+
* If _on_publish returns False/None, we auto-invoke the publish acknowledgement here. */
247+
uint64_t puback_control_id = 0;
248+
if (publish_packet->qos == AWS_MQTT5_QOS_AT_LEAST_ONCE) {
249+
puback_control_id = aws_mqtt5_client_acquire_publish_acknowledgement(client->native, publish_packet);
250+
puback_control_id_py = PyLong_FromUnsignedLongLong((unsigned long long)puback_control_id);
251+
if (!puback_control_id_py) {
252+
PyErr_WriteUnraisable(PyErr_Occurred());
253+
goto cleanup;
254+
}
255+
}
256+
241257
/* Create list of uint32_t subscription identifier tuples */
242258
subscription_identifier_list = PyList_New(subscription_identifier_count);
243259
if (!subscription_identifier_list) {
@@ -261,7 +277,7 @@ static void s_on_publish_received(const struct aws_mqtt5_packet_publish_view *pu
261277
result = PyObject_CallMethod(
262278
client->client_core,
263279
"_on_publish",
264-
"(y#iOs#OiOIOHs#y#Os#O)",
280+
"(y#iOs#OiOIOHs#y#Os#OK)",
265281
/* y */ publish_packet->payload.ptr,
266282
/* # */ publish_packet->payload.len,
267283
/* i */ (int)publish_packet->qos,
@@ -284,15 +300,29 @@ static void s_on_publish_received(const struct aws_mqtt5_packet_publish_view *pu
284300
/* O */ subscription_identifier_count > 0 ? subscription_identifier_list : Py_None,
285301
/* s */ publish_packet->content_type ? publish_packet->content_type->ptr : NULL,
286302
/* # */ publish_packet->content_type ? publish_packet->content_type->len : 0,
287-
/* O */ user_property_count > 0 ? user_properties_list : Py_None);
303+
/* O */ user_property_count > 0 ? user_properties_list : Py_None,
304+
/* K */ (unsigned long long)puback_control_id);
305+
288306
if (!result) {
289307
PyErr_WriteUnraisable(PyErr_Occurred());
308+
/* On error, auto-invoke the publish acknowledgement so it is not lost */
309+
if (puback_control_id != 0) {
310+
aws_mqtt5_client_invoke_publish_acknowledgement(client->native, puback_control_id, NULL);
311+
}
312+
goto cleanup;
313+
}
314+
315+
/* If _on_publish returned False/None, the user did not take control of the publish acknowledgement.
316+
* Auto-invoke it now. */
317+
if (puback_control_id != 0 && !PyObject_IsTrue(result)) {
318+
aws_mqtt5_client_invoke_publish_acknowledgement(client->native, puback_control_id, NULL);
290319
}
291320

292321
cleanup:
293322
Py_XDECREF(result);
294323
Py_XDECREF(subscription_identifier_list);
295324
Py_XDECREF(user_properties_list);
325+
Py_XDECREF(puback_control_id_py);
296326
PyGILState_Release(state);
297327
}
298328

@@ -1700,6 +1730,41 @@ PyObject *aws_py_mqtt5_client_publish(PyObject *self, PyObject *args) {
17001730
return NULL;
17011731
}
17021732

1733+
/*******************************************************************************
1734+
* Invoke Publish Acknowledgement
1735+
******************************************************************************/
1736+
1737+
PyObject *aws_py_mqtt5_client_invoke_publish_acknowledgement(PyObject *self, PyObject *args) {
1738+
(void)self;
1739+
bool success = true;
1740+
1741+
PyObject *impl_capsule;
1742+
unsigned long long control_id_ull = 0;
1743+
1744+
if (!PyArg_ParseTuple(
1745+
args,
1746+
"OK",
1747+
/* O */ &impl_capsule,
1748+
/* K */ &control_id_ull)) {
1749+
return NULL;
1750+
}
1751+
1752+
struct mqtt5_client_binding *client = PyCapsule_GetPointer(impl_capsule, s_capsule_name_mqtt5_client);
1753+
if (!client) {
1754+
return NULL;
1755+
}
1756+
1757+
if (aws_mqtt5_client_invoke_publish_acknowledgement(client->native, (uint64_t)control_id_ull, NULL)) {
1758+
PyErr_SetAwsLastError();
1759+
success = false;
1760+
}
1761+
1762+
if (success) {
1763+
Py_RETURN_NONE;
1764+
}
1765+
return NULL;
1766+
}
1767+
17031768
/*******************************************************************************
17041769
* Subscribe
17051770
******************************************************************************/

source/mqtt5_client.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ PyObject *aws_py_mqtt5_client_publish(PyObject *self, PyObject *args);
1414
PyObject *aws_py_mqtt5_client_subscribe(PyObject *self, PyObject *args);
1515
PyObject *aws_py_mqtt5_client_unsubscribe(PyObject *self, PyObject *args);
1616
PyObject *aws_py_mqtt5_client_get_stats(PyObject *self, PyObject *args);
17+
PyObject *aws_py_mqtt5_client_invoke_publish_acknowledgement(PyObject *self, PyObject *args);
1718

1819
PyObject *aws_py_mqtt5_ws_handshake_transform_complete(PyObject *self, PyObject *args);
1920

0 commit comments

Comments
 (0)