Skip to content

Commit d412e9a

Browse files
authored
Merge pull request #213 from justmobilize/no-retry-on-unauthorized
Don't retry when MQTT response is unauthorized
2 parents ecfd228 + 16b6c6d commit d412e9a

File tree

2 files changed

+71
-11
lines changed

2 files changed

+71
-11
lines changed

adafruit_minimqtt/adafruit_minimqtt.py

+32-10
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,18 @@
7272
MQTT_PKT_TYPE_MASK = const(0xF0)
7373

7474

75+
CONNACK_ERROR_INCORRECT_PROTOCOL = const(0x01)
76+
CONNACK_ERROR_ID_REJECTED = const(0x02)
77+
CONNACK_ERROR_SERVER_UNAVAILABLE = const(0x03)
78+
CONNACK_ERROR_INCORECT_USERNAME_PASSWORD = const(0x04)
79+
CONNACK_ERROR_UNAUTHORIZED = const(0x05)
80+
7581
CONNACK_ERRORS = {
76-
const(0x01): "Connection Refused - Incorrect Protocol Version",
77-
const(0x02): "Connection Refused - ID Rejected",
78-
const(0x03): "Connection Refused - Server unavailable",
79-
const(0x04): "Connection Refused - Incorrect username/password",
80-
const(0x05): "Connection Refused - Unauthorized",
82+
CONNACK_ERROR_INCORRECT_PROTOCOL: "Connection Refused - Incorrect Protocol Version",
83+
CONNACK_ERROR_ID_REJECTED: "Connection Refused - ID Rejected",
84+
CONNACK_ERROR_SERVER_UNAVAILABLE: "Connection Refused - Server unavailable",
85+
CONNACK_ERROR_INCORECT_USERNAME_PASSWORD: "Connection Refused - Incorrect username/password",
86+
CONNACK_ERROR_UNAUTHORIZED: "Connection Refused - Unauthorized",
8187
}
8288

8389
_default_sock = None # pylint: disable=invalid-name
@@ -87,6 +93,10 @@
8793
class MMQTTException(Exception):
8894
"""MiniMQTT Exception class."""
8995

96+
def __init__(self, error, code=None):
97+
super().__init__(error, code)
98+
self.code = code
99+
90100

91101
class NullLogger:
92102
"""Fake logger class that does not do anything"""
@@ -428,17 +438,24 @@ def connect(
428438
self.logger.warning(f"Socket error when connecting: {e}")
429439
backoff = False
430440
except MMQTTException as e:
431-
last_exception = e
441+
self._close_socket()
432442
self.logger.info(f"MMQT error: {e}")
443+
if e.code in [
444+
CONNACK_ERROR_INCORECT_USERNAME_PASSWORD,
445+
CONNACK_ERROR_UNAUTHORIZED,
446+
]:
447+
# No sense trying these again, re-raise
448+
raise
449+
last_exception = e
433450
backoff = True
434451

435452
if self._reconnect_attempts_max > 1:
436453
exc_msg = "Repeated connect failures"
437454
else:
438455
exc_msg = "Connect failure"
456+
439457
if last_exception:
440458
raise MMQTTException(exc_msg) from last_exception
441-
442459
raise MMQTTException(exc_msg)
443460

444461
# pylint: disable=too-many-branches, too-many-statements, too-many-locals
@@ -535,7 +552,7 @@ def _connect(
535552
rc = self._sock_exact_recv(3)
536553
assert rc[0] == 0x02
537554
if rc[2] != 0x00:
538-
raise MMQTTException(CONNACK_ERRORS[rc[2]])
555+
raise MMQTTException(CONNACK_ERRORS[rc[2]], code=rc[2])
539556
self._is_connected = True
540557
result = rc[0] & 1
541558
if self.on_connect is not None:
@@ -549,6 +566,12 @@ def _connect(
549566
f"No data received from broker for {self._recv_timeout} seconds."
550567
)
551568

569+
def _close_socket(self):
570+
if self._sock:
571+
self.logger.debug("Closing socket")
572+
self._connection_manager.close_socket(self._sock)
573+
self._sock = None
574+
552575
# pylint: disable=no-self-use
553576
def _encode_remaining_length(
554577
self, fixed_header: bytearray, remaining_length: int
@@ -577,8 +600,7 @@ def disconnect(self) -> None:
577600
self._sock.send(MQTT_DISCONNECT)
578601
except RuntimeError as e:
579602
self.logger.warning(f"Unable to send DISCONNECT packet: {e}")
580-
self.logger.debug("Closing socket")
581-
self._connection_manager.close_socket(self._sock)
603+
self._close_socket()
582604
self._is_connected = False
583605
self._subscribed_topics = []
584606
self._last_msg_sent_timestamp = 0

tests/test_backoff.py

+39-1
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,24 @@ class TestExpBackOff:
1818
"""basic exponential back-off test"""
1919

2020
connect_times = []
21+
raise_exception = None
2122

2223
# pylint: disable=unused-argument
2324
def fake_connect(self, arg):
2425
"""connect() replacement that records the call times and always raises OSError"""
2526
self.connect_times.append(time.monotonic())
26-
raise OSError("this connect failed")
27+
raise self.raise_exception
2728

2829
def test_failing_connect(self) -> None:
2930
"""test that exponential back-off is used when connect() always raises OSError"""
3031
# use RFC 1918 address to avoid dealing with IPv6 in the call list below
3132
host = "172.40.0.3"
3233
port = 1883
34+
self.connect_times = []
35+
error_code = MQTT.CONNACK_ERROR_SERVER_UNAVAILABLE
36+
self.raise_exception = MQTT.MMQTTException(
37+
MQTT.CONNACK_ERRORS[error_code], code=error_code
38+
)
3339

3440
with patch.object(socket.socket, "connect") as mock_method:
3541
mock_method.side_effect = self.fake_connect
@@ -45,6 +51,7 @@ def test_failing_connect(self) -> None:
4551
print("connecting")
4652
with pytest.raises(MQTT.MMQTTException) as context:
4753
mqtt_client.connect()
54+
assert mqtt_client._sock is None
4855
assert "Repeated connect failures" in str(context)
4956

5057
mock_method.assert_called()
@@ -54,3 +61,34 @@ def test_failing_connect(self) -> None:
5461
print(f"connect() call times: {self.connect_times}")
5562
for i in range(1, connect_retries):
5663
assert self.connect_times[i] >= 2**i
64+
65+
def test_unauthorized(self) -> None:
66+
"""test that exponential back-off is used when connect() always raises OSError"""
67+
# use RFC 1918 address to avoid dealing with IPv6 in the call list below
68+
host = "172.40.0.3"
69+
port = 1883
70+
self.connect_times = []
71+
error_code = MQTT.CONNACK_ERROR_UNAUTHORIZED
72+
self.raise_exception = MQTT.MMQTTException(
73+
MQTT.CONNACK_ERRORS[error_code], code=error_code
74+
)
75+
76+
with patch.object(socket.socket, "connect") as mock_method:
77+
mock_method.side_effect = self.fake_connect
78+
79+
connect_retries = 3
80+
mqtt_client = MQTT.MQTT(
81+
broker=host,
82+
port=port,
83+
socket_pool=socket,
84+
ssl_context=ssl.create_default_context(),
85+
connect_retries=connect_retries,
86+
)
87+
print("connecting")
88+
with pytest.raises(MQTT.MMQTTException) as context:
89+
mqtt_client.connect()
90+
assert mqtt_client._sock is None
91+
assert "Connection Refused - Unauthorized" in str(context)
92+
93+
mock_method.assert_called()
94+
assert len(self.connect_times) == 1

0 commit comments

Comments
 (0)