reliomq 0.1.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.
- reliomq-0.1.0/LICENSE +21 -0
- reliomq-0.1.0/PKG-INFO +438 -0
- reliomq-0.1.0/README.md +427 -0
- reliomq-0.1.0/pyproject.toml +18 -0
- reliomq-0.1.0/reliomq/__init__.py +30 -0
- reliomq-0.1.0/reliomq/ack.py +93 -0
- reliomq-0.1.0/reliomq/bridge.py +662 -0
- reliomq-0.1.0/reliomq/config.py +270 -0
- reliomq-0.1.0/reliomq/mqtt.py +108 -0
- reliomq-0.1.0/reliomq/protocol.py +333 -0
- reliomq-0.1.0/reliomq/publisher.py +579 -0
- reliomq-0.1.0/reliomq/store.py +262 -0
- reliomq-0.1.0/reliomq.egg-info/PKG-INFO +438 -0
- reliomq-0.1.0/reliomq.egg-info/SOURCES.txt +25 -0
- reliomq-0.1.0/reliomq.egg-info/dependency_links.txt +1 -0
- reliomq-0.1.0/reliomq.egg-info/requires.txt +1 -0
- reliomq-0.1.0/reliomq.egg-info/top_level.txt +1 -0
- reliomq-0.1.0/setup.cfg +4 -0
- reliomq-0.1.0/tests/test_ack.py +113 -0
- reliomq-0.1.0/tests/test_bridge.py +196 -0
- reliomq-0.1.0/tests/test_config.py +133 -0
- reliomq-0.1.0/tests/test_mosquitto_integration.py +169 -0
- reliomq-0.1.0/tests/test_mqtt.py +134 -0
- reliomq-0.1.0/tests/test_pipeline.py +183 -0
- reliomq-0.1.0/tests/test_protocol.py +71 -0
- reliomq-0.1.0/tests/test_publisher.py +287 -0
- reliomq-0.1.0/tests/test_store.py +99 -0
reliomq-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pitpiboon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
reliomq-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reliomq
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Durable, application-acknowledged MQTT publishing and forwarding
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: paho-mqtt<3.0,>=2.0
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# reliomq
|
|
13
|
+
|
|
14
|
+
**Durable, end-to-end confirmed MQTT delivery for Python.**
|
|
15
|
+
|
|
16
|
+
`reliomq` sits on top of [`paho-mqtt`](https://pypi.org/project/paho-mqtt/)
|
|
17
|
+
and adds the part QoS 1 doesn't give you: a message survives a crash, an
|
|
18
|
+
outage, or a broker restart, and is retried automatically — in order, under
|
|
19
|
+
its original ID — until an application confirms it actually arrived.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from reliomq import ReliabilityConfig, ReliablePublisher
|
|
23
|
+
|
|
24
|
+
publisher = ReliablePublisher(ReliabilityConfig(
|
|
25
|
+
host="localhost",
|
|
26
|
+
queue_path="pending.jsonl",
|
|
27
|
+
data_topic="reliable/ingress",
|
|
28
|
+
ack_topic="reliable/acks",
|
|
29
|
+
))
|
|
30
|
+
publisher.start()
|
|
31
|
+
publisher.publish(topic="factory/machine1/data", payload={"temperature": 25.2})
|
|
32
|
+
publisher.stop()
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That call durably queues the message before it ever touches the network.
|
|
36
|
+
Everything else — reconnects, retries, ordering, and knowing when it's
|
|
37
|
+
actually safe to forget the message — is handled for you.
|
|
38
|
+
|
|
39
|
+
Requires **Python 3.11+** and **Paho MQTT 2.x**.
|
|
40
|
+
|
|
41
|
+
## Why not just `qos=1`?
|
|
42
|
+
|
|
43
|
+
QoS 1 only proves the broker your process is directly connected to accepted
|
|
44
|
+
one publish. It proves nothing about:
|
|
45
|
+
|
|
46
|
+
- whether your process crashes or loses power before that publish happens;
|
|
47
|
+
- whether the broker forwards it any further (a bridge, another hop);
|
|
48
|
+
- whether anything ever confirms, at the application level, that the message
|
|
49
|
+
did its job.
|
|
50
|
+
|
|
51
|
+
`reliomq` closes that gap with a durable outbox plus an **application-level
|
|
52
|
+
ACK**, so "the network layer said OK" is never mistaken for "the message is
|
|
53
|
+
handled."
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Durable-before-network writes** — every `publish()` is `fsync`'d to disk
|
|
58
|
+
before the first network attempt, so a crash immediately after `publish()`
|
|
59
|
+
returns still can't lose the message.
|
|
60
|
+
- **QoS 1 MQTT delivery**, always — reliability isn't opt-in per call.
|
|
61
|
+
- **Application-level end-to-end ACK**, correlated by a stable `event_id`,
|
|
62
|
+
in addition to the MQTT PUBACK.
|
|
63
|
+
- **Automatic, unique event IDs** — generated for you, or you can supply
|
|
64
|
+
your own; never regenerated on retry.
|
|
65
|
+
- **Strict FIFO recovery** — the oldest pending message is always retried
|
|
66
|
+
first; a new live message can never overtake it.
|
|
67
|
+
- **Automatic restart recovery** — a fresh process just opens the same
|
|
68
|
+
queue file and continues exactly where the last one left off.
|
|
69
|
+
- **Automatic retry** on broker outage, network failure, publish errors,
|
|
70
|
+
publish-confirmation timeout, and ACK timeout — nothing is deleted on
|
|
71
|
+
any of these.
|
|
72
|
+
- **Optional bridge component** (`ReliableMqttBridge`) that relays between
|
|
73
|
+
two brokers and only ACKs the source *after* the destination publish is
|
|
74
|
+
confirmed — never before.
|
|
75
|
+
- **Fail-closed forwarding** — any bridge failure (offline, malformed
|
|
76
|
+
message, full queue, timeout, shutdown) sends no ACK, so the source keeps
|
|
77
|
+
retrying instead of silently dropping the message.
|
|
78
|
+
- **Safe ACK handling** — stale, late, duplicate, wrong-ID, and malformed
|
|
79
|
+
ACKs are all detected and ignored rather than treated as success.
|
|
80
|
+
- **Automatic reconnect/backoff** via Paho, with explicit connection-state
|
|
81
|
+
tracking so the library never publishes while it knows it's disconnected.
|
|
82
|
+
- **Clean shutdown** — stopping interrupts an in-progress ACK wait without
|
|
83
|
+
ever deleting the durable record for the message being waited on.
|
|
84
|
+
- **Corruption-safe storage** — a damaged queue line is logged and skipped,
|
|
85
|
+
never silently deleted; a crash mid-write can't destroy the next record.
|
|
86
|
+
- **Pluggable client construction** — inject your own `client_factory` for
|
|
87
|
+
TLS, auth, or any other Paho client customization.
|
|
88
|
+
- **Strict JSON envelope validation** — rejects `NaN`/`Infinity`, bytes,
|
|
89
|
+
tuples, non-string keys, and other values with no exact JSON form.
|
|
90
|
+
- **Thread-safe by design** — internal locks are never held during a
|
|
91
|
+
network wait, so an ACK can never be missed to a race.
|
|
92
|
+
|
|
93
|
+
## Reliability guarantee
|
|
94
|
+
|
|
95
|
+
`reliomq` provides **at-least-once delivery to the destination broker**
|
|
96
|
+
when the publisher and bridge are used together. It favors *never silently
|
|
97
|
+
losing a message* over *never duplicating one*.
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
application
|
|
101
|
+
| fsync durable outbox
|
|
102
|
+
v
|
|
103
|
+
ReliablePublisher -- QoS 1 --> source broker
|
|
104
|
+
|
|
|
105
|
+
v
|
|
106
|
+
ReliableMqttBridge
|
|
107
|
+
|
|
|
108
|
+
| QoS 1 + PUBACK
|
|
109
|
+
v
|
|
110
|
+
destination broker
|
|
111
|
+
|
|
|
112
|
+
| correlated application ACK
|
|
113
|
+
v
|
|
114
|
+
publisher removes outbox head
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
A record is deleted from the durable outbox **only** after a valid ACK
|
|
118
|
+
carrying its exact `event_id` arrives back on the configured ACK topic.
|
|
119
|
+
Broker acceptance alone is not proof that a final subscriber processed the
|
|
120
|
+
message — if you need that stronger boundary, use a persistent MQTT
|
|
121
|
+
subscription or extend the protocol with a consumer ACK of your own.
|
|
122
|
+
|
|
123
|
+
Duplicates remain possible: the destination publish can succeed and the
|
|
124
|
+
source ACK can then be lost, in which case the publisher correctly retries
|
|
125
|
+
the same stable `event_id`. **Consumers should store processed event IDs
|
|
126
|
+
and make handling idempotent** — see `examples/consumer_dedup.py`.
|
|
127
|
+
|
|
128
|
+
## Install
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# From GitHub, pinned to a release (recommended)
|
|
132
|
+
pip install "git+https://github.com/pitpib23/reliomq.git@v0.1.0"
|
|
133
|
+
|
|
134
|
+
# Or track the latest commit on main
|
|
135
|
+
pip install "git+https://github.com/pitpib23/reliomq.git"
|
|
136
|
+
|
|
137
|
+
# Or from a local clone
|
|
138
|
+
pip install -e /path/to/reliomq
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The only runtime dependency is `paho-mqtt>=2,<3`. There is no GPIO or other
|
|
142
|
+
hardware dependency of any kind.
|
|
143
|
+
|
|
144
|
+
## Publisher integration
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from reliomq import ReliabilityConfig, ReliablePublisher
|
|
148
|
+
|
|
149
|
+
config = ReliabilityConfig(
|
|
150
|
+
host="localhost",
|
|
151
|
+
port=1883,
|
|
152
|
+
queue_path="mqtt_pending.jsonl",
|
|
153
|
+
data_topic="reliable/ingress",
|
|
154
|
+
ack_topic="reliable/acks",
|
|
155
|
+
ack_timeout=3.0,
|
|
156
|
+
publish_timeout=2.0,
|
|
157
|
+
retry_interval=10.0,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
publisher = ReliablePublisher(config)
|
|
161
|
+
publisher.start()
|
|
162
|
+
|
|
163
|
+
event_id = publisher.publish(
|
|
164
|
+
topic="factory/machine1/data",
|
|
165
|
+
payload={"temperature": 25.2, "pressure": 4.1},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Optional: block application code until this process observes delivery.
|
|
169
|
+
delivered = publisher.wait_for_delivery(event_id, timeout=10.0)
|
|
170
|
+
|
|
171
|
+
publisher.stop()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`ReliablePublisher` also works as a context manager: `with
|
|
175
|
+
ReliablePublisher(config) as publisher: ...` calls `start()`/`stop()` for
|
|
176
|
+
you.
|
|
177
|
+
|
|
178
|
+
`payload` may be any strict JSON value: an object with string keys, an
|
|
179
|
+
array, a string, a finite number, a boolean, or `null`. `NaN`, infinity,
|
|
180
|
+
bytes, tuples, custom objects, and mappings with non-string keys are
|
|
181
|
+
rejected. An ID is generated automatically unless `event_id=` is supplied.
|
|
182
|
+
Explicit IDs must be globally unique and must not be reused for different
|
|
183
|
+
content.
|
|
184
|
+
|
|
185
|
+
`publish()` means "durably accepted," not "already delivered." If the
|
|
186
|
+
outbox cannot be written safely, it raises a `StoreError` instead of
|
|
187
|
+
pretending to have accepted the message.
|
|
188
|
+
|
|
189
|
+
### Replacing a normal Paho publish
|
|
190
|
+
|
|
191
|
+
Before:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
client.publish("factory/machine1/data", payload=json_text, qos=1)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
After:
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
event_id = publisher.publish(
|
|
201
|
+
topic="factory/machine1/data",
|
|
202
|
+
payload={"temperature": 25.2},
|
|
203
|
+
)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The caller no longer owns reconnect loops, ACK races, durable retry, or FIFO
|
|
207
|
+
recovery.
|
|
208
|
+
|
|
209
|
+
## Bridge integration
|
|
210
|
+
|
|
211
|
+
Use `ReliableMqttBridge` only if you need to relay messages from one broker
|
|
212
|
+
to another. Run it as its own service or process; its source `data_topic`
|
|
213
|
+
and `ack_topic` must match the publisher's configuration.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from reliomq import BridgeConfig, ReliableMqttBridge
|
|
217
|
+
|
|
218
|
+
bridge = ReliableMqttBridge(
|
|
219
|
+
BridgeConfig(
|
|
220
|
+
source_host="localhost",
|
|
221
|
+
source_port=1883,
|
|
222
|
+
destination_host="mqtt.example.net",
|
|
223
|
+
destination_port=1883,
|
|
224
|
+
data_topic="reliable/ingress",
|
|
225
|
+
ack_topic="reliable/acks",
|
|
226
|
+
destination_publish_timeout=2.0,
|
|
227
|
+
source_ack_publish_timeout=0.5,
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
bridge.start()
|
|
232
|
+
...
|
|
233
|
+
bridge.stop()
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The bridge forwards to the destination topic stored in each message. The
|
|
237
|
+
destination payload retains the deduplication key:
|
|
238
|
+
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"version": 1,
|
|
242
|
+
"event_id": "47913ac65ac84213a9361b393b845708",
|
|
243
|
+
"payload": {"temperature": 25.2}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
The bridge's handoff queue is intentionally memory-only. A malformed
|
|
248
|
+
message, full queue, outage, publish error, timeout, or shutdown produces no
|
|
249
|
+
success ACK; the publisher still owns the durable record and retries it.
|
|
250
|
+
Deploy only one ordinary bridge subscriber per route unless duplicate
|
|
251
|
+
forwarding is intended.
|
|
252
|
+
|
|
253
|
+
## Wire protocol
|
|
254
|
+
|
|
255
|
+
Publisher to bridge, on `data_topic` with QoS 1 and `retain=False`:
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"version": 1,
|
|
260
|
+
"event_id": "47913ac65ac84213a9361b393b845708",
|
|
261
|
+
"topic": "factory/machine1/data",
|
|
262
|
+
"payload": {"temperature": 25.2}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Bridge to publisher, on `ack_topic` with QoS 1 and `retain=False`:
|
|
267
|
+
|
|
268
|
+
```json
|
|
269
|
+
{"version": 1, "event_id": "47913ac65ac84213a9361b393b845708"}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Protocol objects are strict and versioned. Unknown/missing fields, malformed
|
|
273
|
+
UTF-8/JSON, invalid IDs, and ACKs for any ID other than the one currently in
|
|
274
|
+
flight are ignored. Correlation uses the ID alone rather than payload
|
|
275
|
+
equality, so it works with any payload shape.
|
|
276
|
+
|
|
277
|
+
## Persistence and FIFO recovery
|
|
278
|
+
|
|
279
|
+
The outbox is a lightweight JSONL file suitable for edge devices:
|
|
280
|
+
|
|
281
|
+
- one complete message per line;
|
|
282
|
+
- append, flush, and file `fsync` before `publish()` returns;
|
|
283
|
+
- one in-process lock around reads and writes;
|
|
284
|
+
- same-directory temporary file, `fsync`, and atomic replace when removing
|
|
285
|
+
the confirmed head;
|
|
286
|
+
- best-effort parent-directory `fsync` on platforms that support it;
|
|
287
|
+
- stable event ID and destination topic across restart and every retry.
|
|
288
|
+
|
|
289
|
+
Corrupt physical lines are logged, preserved byte-for-byte, and skipped when
|
|
290
|
+
finding the FIFO order among valid messages — never silently deleted. I/O
|
|
291
|
+
errors are raised instead of being mistaken for an empty queue. If a crash
|
|
292
|
+
leaves a torn final line, the next append begins on a new line so it does
|
|
293
|
+
not destroy the next valid record.
|
|
294
|
+
|
|
295
|
+
One process must own a queue path. The store is thread-safe but does not
|
|
296
|
+
attempt cross-process file locking. JSONL removal rewrites the file, so a
|
|
297
|
+
database-backed store may be more appropriate for extremely large queues or
|
|
298
|
+
sustained high write rates.
|
|
299
|
+
|
|
300
|
+
## Connection, retry, and shutdown behavior
|
|
301
|
+
|
|
302
|
+
Both components use Paho's asynchronous network loop and reconnect backoff.
|
|
303
|
+
The publisher does not send until the broker connection and ACK subscription
|
|
304
|
+
are ready. A disconnect interrupts the current ACK wait without removing its
|
|
305
|
+
record. Reconnect wakes recovery immediately; other failures retry after
|
|
306
|
+
`retry_interval`.
|
|
307
|
+
|
|
308
|
+
`ReliablePublisher.stop()` interrupts an ACK wait and joins the worker.
|
|
309
|
+
Because the in-flight message was already durable, it remains for the next
|
|
310
|
+
process. `ReliableMqttBridge.stop()` stops accepting new input, lets the
|
|
311
|
+
bounded in-flight publish/ACK sequence finish, and leaves queued items
|
|
312
|
+
unacknowledged so their publishers recover them. Start/stop are idempotent;
|
|
313
|
+
a stopped instance is not restartable, so create a new instance to restart a
|
|
314
|
+
service.
|
|
315
|
+
|
|
316
|
+
## Configuration reference
|
|
317
|
+
|
|
318
|
+
QoS is fixed at 1 on both configs. Topics are validated as publish topics
|
|
319
|
+
and cannot contain MQTT wildcards. Authentication/TLS is applied by
|
|
320
|
+
supplying a configured `client_factory` when constructing a component (see
|
|
321
|
+
`examples/tls_auth_client.py`) — the config objects intentionally carry no
|
|
322
|
+
credentials.
|
|
323
|
+
|
|
324
|
+
`ReliabilityConfig`:
|
|
325
|
+
|
|
326
|
+
| Field | Default | Meaning |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `host` | required | Broker hostname |
|
|
329
|
+
| `queue_path` | required | Durable outbox file path |
|
|
330
|
+
| `port` | `1883` | Broker port |
|
|
331
|
+
| `client_id` | auto-generated | MQTT client ID |
|
|
332
|
+
| `data_topic` | `reliomq/messages` | Topic the publisher sends on |
|
|
333
|
+
| `ack_topic` | `reliomq/acks` | Topic the publisher listens on for ACKs |
|
|
334
|
+
| `qos` | `1` | Fixed at 1 |
|
|
335
|
+
| `ack_timeout` | `3.0`s | How long to wait for the application ACK |
|
|
336
|
+
| `publish_timeout` | `2.0`s | How long to wait for MQTT publish confirmation |
|
|
337
|
+
| `retry_interval` | `10.0`s | Delay between retries after a failure |
|
|
338
|
+
| `keepalive` | `60`s | MQTT keepalive |
|
|
339
|
+
| `reconnect_min_delay` / `reconnect_max_delay` | `1.0`s / `60.0`s | Paho reconnect backoff range |
|
|
340
|
+
|
|
341
|
+
`BridgeConfig`:
|
|
342
|
+
|
|
343
|
+
| Field | Default | Meaning |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| `source_host` / `destination_host` | required | The two brokers being bridged |
|
|
346
|
+
| `source_port` / `destination_port` | `1883` | Ports for each broker |
|
|
347
|
+
| `source_client_id` / `destination_client_id` | auto-generated | MQTT client IDs for each side |
|
|
348
|
+
| `data_topic` / `ack_topic` | `reliomq/messages` / `reliomq/acks` | Must match the publisher |
|
|
349
|
+
| `qos` | `1` | Fixed at 1 |
|
|
350
|
+
| `keepalive` | `60`s | MQTT keepalive |
|
|
351
|
+
| `destination_publish_timeout` | `2.0`s | Confirmation wait on the destination publish |
|
|
352
|
+
| `source_ack_publish_timeout` | `0.5`s | Confirmation wait on the source ACK publish |
|
|
353
|
+
| `retry_interval` | `10.0`s | Subscription retry delay |
|
|
354
|
+
| `reconnect_min_delay` / `reconnect_max_delay` | `1.0`s / `60.0`s | Paho reconnect backoff range |
|
|
355
|
+
| `max_queue_size` | `1000` | Bound on the bridge's in-memory handoff queue |
|
|
356
|
+
|
|
357
|
+
## Logging
|
|
358
|
+
|
|
359
|
+
`reliomq` uses the standard library `logging` module and configures
|
|
360
|
+
nothing on its own — no handlers, no forced level, no `basicConfig()` call.
|
|
361
|
+
Nothing prints until your application configures logging.
|
|
362
|
+
|
|
363
|
+
| Logger | Used by |
|
|
364
|
+
|---|---|
|
|
365
|
+
| `reliomq.publisher` | `ReliablePublisher`, and the `DurableMessageStore` it creates internally |
|
|
366
|
+
| `reliomq.bridge` | `ReliableMqttBridge` (override with `bridge_logger=`) |
|
|
367
|
+
| `reliomq.store` | a `DurableMessageStore` you construct directly without passing `logger=` |
|
|
368
|
+
| `reliomq.mqtt` | the `confirmed_publish()` helper |
|
|
369
|
+
|
|
370
|
+
Turn logs on with one line, since propagation is never disabled:
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
import logging
|
|
374
|
+
logging.basicConfig(level=logging.INFO) # everything, INFO and up
|
|
375
|
+
logging.getLogger("reliomq").setLevel(logging.DEBUG) # or scope it to just this library
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
**Levels, by meaning:**
|
|
379
|
+
|
|
380
|
+
- **DEBUG** — routine traffic: message durably queued, ACK subscription
|
|
381
|
+
ready, delivery confirmed.
|
|
382
|
+
- **INFO** — lifecycle: publisher/bridge started/stopped, MQTT connected.
|
|
383
|
+
- **WARNING** — recoverable trouble that's expected during normal outage
|
|
384
|
+
handling: disconnects, rejected subscriptions, late/malformed/wrong-ID
|
|
385
|
+
ACKs ignored, forward failures, a full bridge queue. Nothing is lost when
|
|
386
|
+
you see these.
|
|
387
|
+
- **ERROR** (some via `logger.exception()`, with a traceback) — things that
|
|
388
|
+
should not happen: the worker failing to stop promptly on shutdown, an ACK
|
|
389
|
+
matching a message that turned out not to be the durable oldest, or an
|
|
390
|
+
unexpected exception in the delivery/forward loop.
|
|
391
|
+
|
|
392
|
+
## Examples
|
|
393
|
+
|
|
394
|
+
All scripts live in `examples/` and are runnable directly (`python
|
|
395
|
+
examples/<name>.py`) against a real broker unless noted otherwise:
|
|
396
|
+
|
|
397
|
+
- `basic.py` — minimal one-shot publish and `wait_for_delivery`.
|
|
398
|
+
- `sensor_loop.py` — a long-running periodic publisher with graceful
|
|
399
|
+
SIGINT/SIGTERM shutdown and a pending-backlog warning; the shape most
|
|
400
|
+
edge/IoT integrations actually use.
|
|
401
|
+
- `bridge.py` — minimal standalone forwarder service.
|
|
402
|
+
- `consumer_dedup.py` — a plain Paho subscriber (not part of this package)
|
|
403
|
+
showing the recommended `event_id` deduplication pattern for a final
|
|
404
|
+
consumer of bridged messages.
|
|
405
|
+
- `local_end_to_end.py` — publisher + bridge + consumer wired together
|
|
406
|
+
against one local Mosquitto instance, so you can watch real PUBACKs,
|
|
407
|
+
reconnects, and the on-disk queue file; kill and restart the broker
|
|
408
|
+
mid-run to see `pending_count()` rise and drain.
|
|
409
|
+
- `tls_auth_client.py` — injecting TLS and username/password auth through a
|
|
410
|
+
custom `client_factory` without adding security config to the library.
|
|
411
|
+
|
|
412
|
+
## Tests
|
|
413
|
+
|
|
414
|
+
Run the deterministic suite from this directory:
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
python -m unittest discover -s tests -v
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`test_protocol.py`, `test_store.py`, `test_ack.py`, `test_mqtt.py`, and
|
|
421
|
+
`test_config.py` each test one module in isolation: envelope/ACK wire
|
|
422
|
+
encoding, durable FIFO storage, the single-waiter ACK correlator, the small
|
|
423
|
+
Paho helper functions, and configuration validation, respectively.
|
|
424
|
+
`test_publisher.py` and `test_bridge.py` drive each component through a fake
|
|
425
|
+
Paho client to exercise success, broker outage, return-code failure,
|
|
426
|
+
publish-confirmation timeout, ACK timeout, restart, FIFO recovery,
|
|
427
|
+
wrong/late/malformed/duplicate ACKs, reconnect, bridge failure/success, and
|
|
428
|
+
shutdown state transitions. `test_pipeline.py` goes a level higher: it wires
|
|
429
|
+
a real `ReliablePublisher` to a real `ReliableMqttBridge` through two linked
|
|
430
|
+
fake clients that relay `publish()` calls the way a broker would, so the two
|
|
431
|
+
components run on their own real background threads and exchange genuine
|
|
432
|
+
envelope/ACK traffic — catching integration regressions that per-component
|
|
433
|
+
unit tests with directly injected ACKs cannot see. An optional Mosquitto
|
|
434
|
+
integration test is skipped when the broker executable is unavailable.
|
|
435
|
+
|
|
436
|
+
## License
|
|
437
|
+
|
|
438
|
+
MIT — see [LICENSE](LICENSE).
|