wexample-queue 1.1.0__tar.gz → 2.0.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.
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/PKG-INFO +6 -6
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/README.md +3 -3
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/pyproject.toml +3 -3
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/src/wexample_queue/connector/rabbitmq_external_connector.py +55 -12
- wexample_queue-2.0.0/src/wexample_queue/service/abstract_queue_service.py +197 -0
- wexample_queue-1.1.0/src/wexample_queue/service/abstract_queue_service.py +0 -99
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/src/wexample_queue/__init__.py +0 -0
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/src/wexample_queue/connector/__init__.py +0 -0
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/src/wexample_queue/py.typed +0 -0
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/src/wexample_queue/service/__init__.py +0 -0
- {wexample_queue-1.1.0 → wexample_queue-2.0.0}/tests/.gitkeep +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: wexample-queue
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Manage Queuing
|
|
5
5
|
Author-Email: weeger <contact@wexample.com>
|
|
6
6
|
License: MIT
|
|
@@ -9,8 +9,8 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
9
9
|
Classifier: Operating System :: OS Independent
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Requires-Dist: pika>=1.3.0
|
|
12
|
-
Requires-Dist: wexample-app>=19.
|
|
13
|
-
Requires-Dist: wexample-helpers>=20.
|
|
12
|
+
Requires-Dist: wexample-app>=19.3.0
|
|
13
|
+
Requires-Dist: wexample-helpers>=20.1.0
|
|
14
14
|
Provides-Extra: dev
|
|
15
15
|
Requires-Dist: pytest; extra == "dev"
|
|
16
16
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -18,7 +18,7 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
|
|
19
19
|
# queue
|
|
20
20
|
|
|
21
|
-
Version:
|
|
21
|
+
Version: 2.0.0
|
|
22
22
|
|
|
23
23
|
`wexample-queue` is what a worker needs to live on a RabbitMQ queue and nothing more: a connector holding the broker connection, and a service base whose whole subject is one queue. A subclass writes the name of the queue and what to do with a message; connecting, declaring, decoding, acknowledging and giving up are the same for every worker and are already written.
|
|
24
24
|
|
|
@@ -203,8 +203,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
|
|
|
203
203
|
## Dependencies
|
|
204
204
|
|
|
205
205
|
- pika: >=1.3.0
|
|
206
|
-
- wexample-app: >=19.
|
|
207
|
-
- wexample-helpers: >=20.
|
|
206
|
+
- wexample-app: >=19.3.0
|
|
207
|
+
- wexample-helpers: >=20.1.0
|
|
208
208
|
|
|
209
209
|
## Versioning & Compatibility Policy
|
|
210
210
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# queue
|
|
2
2
|
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
|
|
5
5
|
`wexample-queue` is what a worker needs to live on a RabbitMQ queue and nothing more: a connector holding the broker connection, and a service base whose whole subject is one queue. A subclass writes the name of the queue and what to do with a message; connecting, declaring, decoding, acknowledging and giving up are the same for every worker and are already written.
|
|
6
6
|
|
|
@@ -185,8 +185,8 @@ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the comp
|
|
|
185
185
|
## Dependencies
|
|
186
186
|
|
|
187
187
|
- pika: >=1.3.0
|
|
188
|
-
- wexample-app: >=19.
|
|
189
|
-
- wexample-helpers: >=20.
|
|
188
|
+
- wexample-app: >=19.3.0
|
|
189
|
+
- wexample-helpers: >=20.1.0
|
|
190
190
|
|
|
191
191
|
## Versioning & Compatibility Policy
|
|
192
192
|
|
|
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "wexample-queue"
|
|
9
|
-
version = "
|
|
9
|
+
version = "2.0.0"
|
|
10
10
|
description = "Manage Queuing"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "weeger", email = "contact@wexample.com" },
|
|
@@ -19,8 +19,8 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
21
|
"pika>=1.3.0",
|
|
22
|
-
"wexample-app>=19.
|
|
23
|
-
"wexample-helpers>=20.
|
|
22
|
+
"wexample-app>=19.3.0",
|
|
23
|
+
"wexample-helpers>=20.1.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[project.readme]
|
|
@@ -10,10 +10,9 @@ from wexample_helpers.classes.field import public_field
|
|
|
10
10
|
from wexample_helpers.decorator.base_class import base_class
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from collections.abc import Callable
|
|
14
|
-
|
|
15
13
|
from wexample_helpers.const.types import StringsList
|
|
16
14
|
|
|
15
|
+
RABBITMQ_ENV_KEY_HEARTBEAT = "RABBITMQ_HEARTBEAT"
|
|
17
16
|
RABBITMQ_ENV_KEY_HOST = "RABBITMQ_HOST"
|
|
18
17
|
RABBITMQ_ENV_KEY_PASSWORD = "RABBITMQ_PASSWORD"
|
|
19
18
|
RABBITMQ_ENV_KEY_PORT = "RABBITMQ_PORT"
|
|
@@ -45,11 +44,27 @@ class RabbitmqExternalConnector(AbstractExternalConnector):
|
|
|
45
44
|
default="rabbitmq",
|
|
46
45
|
description="Name this connector answers to",
|
|
47
46
|
)
|
|
47
|
+
thread_id: int | None = public_field(
|
|
48
|
+
default=None,
|
|
49
|
+
description="The thread that opened the connection, the only one allowed to use it",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def ack(self, delivery_tag: int) -> None:
|
|
53
|
+
self.channel.basic_ack(delivery_tag)
|
|
48
54
|
|
|
49
55
|
def connect(self) -> None:
|
|
50
56
|
if self.connected:
|
|
51
57
|
return
|
|
52
58
|
|
|
59
|
+
import os
|
|
60
|
+
import threading
|
|
61
|
+
|
|
62
|
+
parameters: dict[str, Any] = {}
|
|
63
|
+
# Seconds between two signs of life; the broker's proposal when unset.
|
|
64
|
+
heartbeat = os.environ.get(RABBITMQ_ENV_KEY_HEARTBEAT)
|
|
65
|
+
if heartbeat:
|
|
66
|
+
parameters["heartbeat"] = int(heartbeat)
|
|
67
|
+
|
|
53
68
|
self.connection = pika.BlockingConnection(
|
|
54
69
|
pika.ConnectionParameters(
|
|
55
70
|
host=self._env(RABBITMQ_ENV_KEY_HOST),
|
|
@@ -58,17 +73,12 @@ class RabbitmqExternalConnector(AbstractExternalConnector):
|
|
|
58
73
|
self._env(RABBITMQ_ENV_KEY_USER),
|
|
59
74
|
self._env(RABBITMQ_ENV_KEY_PASSWORD),
|
|
60
75
|
),
|
|
76
|
+
**parameters,
|
|
61
77
|
)
|
|
62
78
|
)
|
|
63
79
|
self.channel = self.connection.channel()
|
|
64
80
|
self.connected = True
|
|
65
|
-
|
|
66
|
-
def consume(self, queue_name: str, callback: Callable) -> None:
|
|
67
|
-
"""Hands the channel over to the callback until consumption is stopped."""
|
|
68
|
-
self.channel.basic_consume(
|
|
69
|
-
queue=queue_name, on_message_callback=callback
|
|
70
|
-
)
|
|
71
|
-
self.channel.start_consuming()
|
|
81
|
+
self.thread_id = threading.get_ident()
|
|
72
82
|
|
|
73
83
|
def declare_queue(self, queue_name: str) -> None:
|
|
74
84
|
"""Makes sure the queue is there, durable, whoever gets there first.
|
|
@@ -83,11 +93,26 @@ class RabbitmqExternalConnector(AbstractExternalConnector):
|
|
|
83
93
|
if not self.connected:
|
|
84
94
|
return
|
|
85
95
|
|
|
86
|
-
|
|
96
|
+
# A connection the broker already dropped cannot be closed again.
|
|
97
|
+
if self.connection.is_open:
|
|
98
|
+
self.connection.close()
|
|
87
99
|
self.connection = None
|
|
88
100
|
self.channel = None
|
|
89
101
|
self.connected = False
|
|
90
102
|
|
|
103
|
+
def get(self, queue_name: str) -> tuple[int, bytes] | None:
|
|
104
|
+
"""Takes the next message off the queue, or nothing when it is empty.
|
|
105
|
+
|
|
106
|
+
Pulled one at a time rather than pushed: a message the broker pushes
|
|
107
|
+
while the previous one is still being worked on would wait unacknowledged
|
|
108
|
+
in the client, and be dropped by the broker's delivery timeout long
|
|
109
|
+
before its turn came.
|
|
110
|
+
"""
|
|
111
|
+
method, _properties, body = self.channel.basic_get(queue=queue_name)
|
|
112
|
+
if method is None:
|
|
113
|
+
return None
|
|
114
|
+
return method.delivery_tag, body
|
|
115
|
+
|
|
91
116
|
def get_expected_env_keys(self) -> StringsList:
|
|
92
117
|
return [
|
|
93
118
|
RABBITMQ_ENV_KEY_HOST,
|
|
@@ -96,13 +121,29 @@ class RabbitmqExternalConnector(AbstractExternalConnector):
|
|
|
96
121
|
RABBITMQ_ENV_KEY_PASSWORD,
|
|
97
122
|
]
|
|
98
123
|
|
|
124
|
+
def nack(self, delivery_tag: int) -> None:
|
|
125
|
+
"""Gives a message up, without putting it back on the queue."""
|
|
126
|
+
self.channel.basic_nack(delivery_tag, requeue=False)
|
|
127
|
+
|
|
99
128
|
def publish(self, queue_name: str, payload: dict[str, Any]) -> None:
|
|
100
129
|
"""Puts one message on the queue, marked to survive a broker restart.
|
|
101
130
|
|
|
102
131
|
It goes through the default exchange, whose routing key is the queue
|
|
103
132
|
name — which is also where a Symfony Messenger transport declaring that
|
|
104
133
|
queue binds its own exchange, so both land in the same place.
|
|
134
|
+
|
|
135
|
+
Callable from any thread: a connection belongs to the thread that opened
|
|
136
|
+
it, so from another one the publication is handed over to that thread,
|
|
137
|
+
which sends it the next time it tends the connection.
|
|
105
138
|
"""
|
|
139
|
+
import threading
|
|
140
|
+
|
|
141
|
+
if threading.get_ident() != self.thread_id:
|
|
142
|
+
self.connection.add_callback_threadsafe(
|
|
143
|
+
lambda: self.publish(queue_name, payload)
|
|
144
|
+
)
|
|
145
|
+
return
|
|
146
|
+
|
|
106
147
|
import json
|
|
107
148
|
|
|
108
149
|
self.channel.basic_publish(
|
|
@@ -112,8 +153,10 @@ class RabbitmqExternalConnector(AbstractExternalConnector):
|
|
|
112
153
|
properties=pika.BasicProperties(delivery_mode=2),
|
|
113
154
|
)
|
|
114
155
|
|
|
115
|
-
def
|
|
116
|
-
|
|
156
|
+
def sleep(self, seconds: float) -> None:
|
|
157
|
+
"""Waits while keeping the connection alive — heartbeats answered, and
|
|
158
|
+
what other threads handed over sent."""
|
|
159
|
+
self.connection.sleep(seconds)
|
|
117
160
|
|
|
118
161
|
def _env(self, key: str) -> str:
|
|
119
162
|
"""One of the four keys, from the process environment or from wex's own.
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
4
|
+
|
|
5
|
+
from wexample_app.service.abstract_service import AbstractService
|
|
6
|
+
from wexample_helpers.classes.abstract_method import abstract_method
|
|
7
|
+
from wexample_helpers.classes.field import public_field
|
|
8
|
+
from wexample_helpers.decorator.base_class import base_class
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from wexample_queue.connector.rabbitmq_external_connector import (
|
|
12
|
+
RabbitmqExternalConnector,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@base_class
|
|
17
|
+
class AbstractQueueService(AbstractService):
|
|
18
|
+
"""A service whose work is whatever arrives on one queue.
|
|
19
|
+
|
|
20
|
+
All a subclass writes is the name of the queue and what to do with a
|
|
21
|
+
message. Connecting, taking, decoding, acknowledging, keeping the connection
|
|
22
|
+
alive and coming back after losing it are the same for every worker, so they
|
|
23
|
+
are here.
|
|
24
|
+
|
|
25
|
+
A message may take hours. It is worked on in a thread of its own while this
|
|
26
|
+
one tends the connection — answering the broker's heartbeats, sending what
|
|
27
|
+
the work publishes — so the broker never takes a long job for a dead worker.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
ACKNOWLEDGE_ON_RECEIPT: ClassVar[bool] = False
|
|
31
|
+
"""Acknowledge a message as soon as it is taken, rather than once done.
|
|
32
|
+
|
|
33
|
+
For work longer than the broker allows a message to stay unacknowledged
|
|
34
|
+
(`consumer_timeout`, 30 minutes by default), and whose outcome is recorded
|
|
35
|
+
somewhere else: a message acknowledged at once is never delivered twice,
|
|
36
|
+
and nothing the work does can make the broker put it back.
|
|
37
|
+
"""
|
|
38
|
+
POLL_INTERVAL: ClassVar[float] = 1.0
|
|
39
|
+
"""Seconds between two looks at an empty queue."""
|
|
40
|
+
RECONNECT_DELAY: ClassVar[float] = 5.0
|
|
41
|
+
"""Seconds before trying again once the connection is lost."""
|
|
42
|
+
connector: RabbitmqExternalConnector | None = public_field(
|
|
43
|
+
default=None,
|
|
44
|
+
description="The broker connection, opened when the service starts",
|
|
45
|
+
)
|
|
46
|
+
queue_name: str = public_field(
|
|
47
|
+
description="Name of the queue this service consumes",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def on_idle(self) -> None:
|
|
51
|
+
"""Called each time the queue is found empty. Nothing by default."""
|
|
52
|
+
|
|
53
|
+
def on_tick(self, message: dict[str, Any]) -> None:
|
|
54
|
+
"""Called about once a second while a message is being worked on, from the
|
|
55
|
+
thread that tends the connection. Nothing by default."""
|
|
56
|
+
|
|
57
|
+
@abstract_method
|
|
58
|
+
def process_message(self, message: dict[str, Any]) -> None:
|
|
59
|
+
"""What to do with one message, already decoded — in a thread of its own.
|
|
60
|
+
|
|
61
|
+
Returning means the message is acknowledged. Raising means it is given
|
|
62
|
+
up on — so a subclass that wants a message tried again has to say so by
|
|
63
|
+
putting it back itself. With `ACKNOWLEDGE_ON_RECEIPT`, it is acknowledged
|
|
64
|
+
before this is called, whatever comes of it.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def start(self) -> None:
|
|
68
|
+
self._connect()
|
|
69
|
+
|
|
70
|
+
super().start()
|
|
71
|
+
|
|
72
|
+
def stop(self) -> None:
|
|
73
|
+
if self.connector:
|
|
74
|
+
self.connector.disconnect()
|
|
75
|
+
|
|
76
|
+
super().stop()
|
|
77
|
+
|
|
78
|
+
def _connect(self) -> None:
|
|
79
|
+
from wexample_queue.connector.rabbitmq_external_connector import (
|
|
80
|
+
RabbitmqExternalConnector,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.connector = RabbitmqExternalConnector(kernel=self.kernel)
|
|
84
|
+
self.connector.connect()
|
|
85
|
+
self.connector.declare_queue(self.queue_name)
|
|
86
|
+
|
|
87
|
+
def _decode(self, body: bytes) -> dict[str, Any] | None:
|
|
88
|
+
import json
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return json.loads(body.decode())
|
|
92
|
+
except json.JSONDecodeError as exception:
|
|
93
|
+
self.io.error(
|
|
94
|
+
f"[{self.get_snake_short_class_name()}] Message is not JSON: {exception}",
|
|
95
|
+
fatal=False,
|
|
96
|
+
)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def _handle(self, delivery_tag: int, body: bytes) -> None:
|
|
100
|
+
"""Works one message through, keeping the connection alive meanwhile.
|
|
101
|
+
|
|
102
|
+
A message that cannot be read, or that the service could not get
|
|
103
|
+
through, is rejected without being put back: requeueing it would be
|
|
104
|
+
asking for the same failure at once and for ever.
|
|
105
|
+
"""
|
|
106
|
+
import threading
|
|
107
|
+
|
|
108
|
+
message = self._decode(body)
|
|
109
|
+
if message is None:
|
|
110
|
+
self.connector.nack(delivery_tag)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
if self.ACKNOWLEDGE_ON_RECEIPT:
|
|
114
|
+
self.connector.ack(delivery_tag)
|
|
115
|
+
|
|
116
|
+
outcome: dict[str, Any] = {}
|
|
117
|
+
|
|
118
|
+
def work() -> None:
|
|
119
|
+
try:
|
|
120
|
+
self.process_message(message)
|
|
121
|
+
except Exception as exception:
|
|
122
|
+
import traceback
|
|
123
|
+
|
|
124
|
+
outcome["error"] = exception
|
|
125
|
+
self.io.debug(traceback.format_exc())
|
|
126
|
+
|
|
127
|
+
import time
|
|
128
|
+
|
|
129
|
+
from pika.exceptions import AMQPError
|
|
130
|
+
|
|
131
|
+
worker = threading.Thread(target=work, name=f"{self.queue_name}-message")
|
|
132
|
+
worker.start()
|
|
133
|
+
|
|
134
|
+
# Losing the connection does not stop the work: it is let finish, the
|
|
135
|
+
# connection being opened again afterwards, and nothing else is taken
|
|
136
|
+
# meanwhile.
|
|
137
|
+
lost: AMQPError | None = None
|
|
138
|
+
while worker.is_alive():
|
|
139
|
+
if lost is None:
|
|
140
|
+
try:
|
|
141
|
+
self.connector.sleep(self.POLL_INTERVAL)
|
|
142
|
+
except AMQPError as exception:
|
|
143
|
+
lost = exception
|
|
144
|
+
else:
|
|
145
|
+
time.sleep(self.POLL_INTERVAL)
|
|
146
|
+
self.on_tick(message)
|
|
147
|
+
|
|
148
|
+
if lost is not None:
|
|
149
|
+
# An unacknowledged message is given back by the broker with the
|
|
150
|
+
# connection it was delivered on; one acknowledged on receipt is done.
|
|
151
|
+
raise lost
|
|
152
|
+
|
|
153
|
+
if "error" in outcome:
|
|
154
|
+
self.io.error(
|
|
155
|
+
f"[{self.get_snake_short_class_name()}] Message failed: {outcome['error']}",
|
|
156
|
+
fatal=False,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if not self.ACKNOWLEDGE_ON_RECEIPT:
|
|
160
|
+
if "error" in outcome:
|
|
161
|
+
self.connector.nack(delivery_tag)
|
|
162
|
+
else:
|
|
163
|
+
self.connector.ack(delivery_tag)
|
|
164
|
+
|
|
165
|
+
def _run(self) -> None:
|
|
166
|
+
"""One turn: the next message worked through, or a short wait.
|
|
167
|
+
|
|
168
|
+
A lost connection is opened again rather than ending the service: a
|
|
169
|
+
worker left running for a night outlives the odd network hiccup, and a
|
|
170
|
+
broker restarted under it.
|
|
171
|
+
"""
|
|
172
|
+
from pika.exceptions import AMQPError
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
if not self.connector or not self.connector.connected:
|
|
176
|
+
self._connect()
|
|
177
|
+
self.log(f"Consuming messages on queue « {self.queue_name} »...")
|
|
178
|
+
|
|
179
|
+
delivery = self.connector.get(self.queue_name)
|
|
180
|
+
if delivery is None:
|
|
181
|
+
self.on_idle()
|
|
182
|
+
self.connector.sleep(self.POLL_INTERVAL)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
self._handle(*delivery)
|
|
186
|
+
except AMQPError as exception:
|
|
187
|
+
self.io.error(
|
|
188
|
+
f"[{self.get_snake_short_class_name()}] Connection lost: "
|
|
189
|
+
f"{type(exception).__name__}: {exception}. Reconnecting in "
|
|
190
|
+
f"{self.RECONNECT_DELAY:.0f}s.",
|
|
191
|
+
fatal=False,
|
|
192
|
+
)
|
|
193
|
+
if self.connector:
|
|
194
|
+
self.connector.connected = False
|
|
195
|
+
import time
|
|
196
|
+
|
|
197
|
+
time.sleep(self.RECONNECT_DELAY)
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import TYPE_CHECKING, Any
|
|
4
|
-
|
|
5
|
-
from wexample_app.service.abstract_service import AbstractService
|
|
6
|
-
from wexample_helpers.classes.abstract_method import abstract_method
|
|
7
|
-
from wexample_helpers.classes.field import public_field
|
|
8
|
-
from wexample_helpers.decorator.base_class import base_class
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from wexample_queue.connector.rabbitmq_external_connector import (
|
|
12
|
-
RabbitmqExternalConnector,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@base_class
|
|
17
|
-
class AbstractQueueService(AbstractService):
|
|
18
|
-
"""A service whose work is whatever arrives on one queue.
|
|
19
|
-
|
|
20
|
-
All a subclass writes is the name of the queue and what to do with a
|
|
21
|
-
message. Connecting, declaring, decoding, acknowledging and giving up are
|
|
22
|
-
the same for every worker, so they are here.
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
connector: RabbitmqExternalConnector | None = public_field(
|
|
26
|
-
default=None,
|
|
27
|
-
description="The broker connection, opened when the service starts",
|
|
28
|
-
)
|
|
29
|
-
queue_name: str = public_field(
|
|
30
|
-
description="Name of the queue this service consumes",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
@abstract_method
|
|
34
|
-
def process_message(self, message: dict[str, Any]) -> None:
|
|
35
|
-
"""What to do with one message, already decoded.
|
|
36
|
-
|
|
37
|
-
Returning means the message is acknowledged. Raising means it is given
|
|
38
|
-
up on — so a subclass that wants a message tried again has to say so by
|
|
39
|
-
putting it back itself.
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
def start(self) -> None:
|
|
43
|
-
from wexample_queue.connector.rabbitmq_external_connector import (
|
|
44
|
-
RabbitmqExternalConnector,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
self.connector = RabbitmqExternalConnector(kernel=self.kernel)
|
|
48
|
-
self.connector.connect()
|
|
49
|
-
self.connector.declare_queue(self.queue_name)
|
|
50
|
-
|
|
51
|
-
super().start()
|
|
52
|
-
|
|
53
|
-
def stop(self) -> None:
|
|
54
|
-
if self.connector:
|
|
55
|
-
self.connector.stop_consuming()
|
|
56
|
-
self.connector.disconnect()
|
|
57
|
-
|
|
58
|
-
super().stop()
|
|
59
|
-
|
|
60
|
-
def _on_message(self, channel, method_frame, header_frame, body) -> None:
|
|
61
|
-
"""Decodes one delivery and answers the broker for it.
|
|
62
|
-
|
|
63
|
-
A message that cannot be read, or that the service could not get
|
|
64
|
-
through, is rejected without being put back: requeueing it would be
|
|
65
|
-
asking for the same failure at once and for ever. Where it goes then is
|
|
66
|
-
the broker's business — a dead letter exchange if the queue has one,
|
|
67
|
-
nowhere if it has not.
|
|
68
|
-
"""
|
|
69
|
-
import json
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
message = json.loads(body.decode())
|
|
73
|
-
except json.JSONDecodeError as exception:
|
|
74
|
-
self.io.error(
|
|
75
|
-
f"[{self.get_snake_short_class_name()}] Message is not JSON: {exception}",
|
|
76
|
-
fatal=False,
|
|
77
|
-
)
|
|
78
|
-
channel.basic_nack(method_frame.delivery_tag, requeue=False)
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
try:
|
|
82
|
-
self.process_message(message)
|
|
83
|
-
except Exception as exception:
|
|
84
|
-
import traceback
|
|
85
|
-
|
|
86
|
-
self.io.error(
|
|
87
|
-
f"[{self.get_snake_short_class_name()}] Message failed: {exception}",
|
|
88
|
-
fatal=False,
|
|
89
|
-
)
|
|
90
|
-
self.io.debug(traceback.format_exc())
|
|
91
|
-
channel.basic_nack(method_frame.delivery_tag, requeue=False)
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
channel.basic_ack(method_frame.delivery_tag)
|
|
95
|
-
|
|
96
|
-
def _run(self) -> None:
|
|
97
|
-
self.log(f"Consuming messages on queue « {self.queue_name} »...")
|
|
98
|
-
|
|
99
|
-
self.connector.consume(self.queue_name, self._on_message)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|