DefStream-RabbitMQ 0.1.26__116-py3-none-any.whl
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.
- DefStream/Broker/Implements/RabbitMQ/Configuration.py +49 -0
- DefStream/Broker/Implements/RabbitMQ/Connection.py +587 -0
- DefStream/Broker/Implements/RabbitMQ/Consumer.py +416 -0
- DefStream/Broker/Implements/RabbitMQ/Event.py +146 -0
- DefStream/Broker/Implements/RabbitMQ/Exchange.py +277 -0
- DefStream/Broker/Implements/RabbitMQ/Queue.py +321 -0
- DefStream/Broker/Implements/RabbitMQ/Serializer.py +75 -0
- DefStream/Broker/Implements/RabbitMQ/Stream.py +181 -0
- DefStream/Broker/Implements/RabbitMQ/Subscription.py +90 -0
- DefStream/Broker/Implements/RabbitMQ/__init__.py +47 -0
- defstream_rabbitmq-0.1.26.dist-info/METADATA +92 -0
- defstream_rabbitmq-0.1.26.dist-info/RECORD +15 -0
- defstream_rabbitmq-0.1.26.dist-info/WHEEL +6 -0
- defstream_rabbitmq-0.1.26.dist-info/licenses/LICENSE +674 -0
- defstream_rabbitmq-0.1.26.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from DefStream.Broker import (
|
|
4
|
+
Sendable,
|
|
5
|
+
Callable,
|
|
6
|
+
RemoteError,
|
|
7
|
+
validate_priority,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .Serializer import (
|
|
11
|
+
Encoder,
|
|
12
|
+
Decoder,
|
|
13
|
+
)
|
|
14
|
+
from copy import deepcopy
|
|
15
|
+
from logging import getLogger
|
|
16
|
+
from math import (
|
|
17
|
+
ceil,
|
|
18
|
+
isfinite,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from pika import (
|
|
22
|
+
BlockingConnection,
|
|
23
|
+
BasicProperties,
|
|
24
|
+
ConnectionParameters,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from time import (
|
|
28
|
+
time,
|
|
29
|
+
monotonic,
|
|
30
|
+
)
|
|
31
|
+
from uuid import uuid4
|
|
32
|
+
|
|
33
|
+
from typing import (
|
|
34
|
+
Any,
|
|
35
|
+
Dict,
|
|
36
|
+
Optional,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = (
|
|
40
|
+
'Exchange',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Exchange(
|
|
45
|
+
Sendable,
|
|
46
|
+
Callable,
|
|
47
|
+
):
|
|
48
|
+
"""Exchange of Event Broker for RabbitMQ"""
|
|
49
|
+
logger = getLogger('Broker.RabbitMQ.Exchange')
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
connection: BlockingConnection,
|
|
54
|
+
encode: Encoder,
|
|
55
|
+
name: str,
|
|
56
|
+
decode: Optional[Decoder] = None,
|
|
57
|
+
rpc_parameters: Optional[ConnectionParameters] = None,
|
|
58
|
+
):
|
|
59
|
+
self.connection = connection
|
|
60
|
+
self.encode = encode
|
|
61
|
+
try:
|
|
62
|
+
self.channel = self.connection.channel()
|
|
63
|
+
except Exception as error:
|
|
64
|
+
self.logger.error('Exchange channel creation failed: exchange=%s error_type=%s', name, type(error).__name__)
|
|
65
|
+
raise
|
|
66
|
+
self.exchange = name
|
|
67
|
+
self.decode = decode
|
|
68
|
+
self.rpc_parameters = rpc_parameters
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
def __del__(self):
|
|
72
|
+
channel = getattr(self, 'channel', None)
|
|
73
|
+
try:
|
|
74
|
+
if channel and channel.is_open:
|
|
75
|
+
channel.close()
|
|
76
|
+
self.logger.debug('Exchange channel cleanup completed: exchange=%s', self.exchange)
|
|
77
|
+
except Exception as error:
|
|
78
|
+
self.logger.error('Exchange channel cleanup failed: error_type=%s', type(error).__name__)
|
|
79
|
+
raise
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
def __str__(self): return self.exchange
|
|
83
|
+
|
|
84
|
+
def send(
|
|
85
|
+
self,
|
|
86
|
+
body,
|
|
87
|
+
event: Optional[str] = None,
|
|
88
|
+
headers: Dict = {},
|
|
89
|
+
priority: Optional[int] = None,
|
|
90
|
+
expiration: Optional[int] = None,
|
|
91
|
+
timestamp: Optional[int] = None,
|
|
92
|
+
persistent: bool = True,
|
|
93
|
+
id: Optional[str] = None,
|
|
94
|
+
):
|
|
95
|
+
validate_priority(
|
|
96
|
+
priority,
|
|
97
|
+
255,
|
|
98
|
+
)
|
|
99
|
+
if not timestamp: timestamp = int(time())
|
|
100
|
+
if not id: id = uuid4().hex
|
|
101
|
+
properties = BasicProperties(
|
|
102
|
+
type=event if event else '',
|
|
103
|
+
headers=headers,
|
|
104
|
+
content_type=self.encode.format,
|
|
105
|
+
content_encoding=self.encode.charset,
|
|
106
|
+
priority=priority,
|
|
107
|
+
timestamp=timestamp,
|
|
108
|
+
expiration=str(expiration) if expiration is not None else None,
|
|
109
|
+
message_id=id,
|
|
110
|
+
delivery_mode=2 if persistent else 1,
|
|
111
|
+
)
|
|
112
|
+
self.logger.debug('Publishing message: exchange=%s routing_key=%s message_id=%s priority=%s persistent=%s publisher_confirm=False', self.exchange, event, id, priority, persistent)
|
|
113
|
+
try:
|
|
114
|
+
self.channel.basic_publish(
|
|
115
|
+
exchange=self.exchange,
|
|
116
|
+
routing_key=event if event else '',
|
|
117
|
+
properties=properties,
|
|
118
|
+
body=self.encode(body),
|
|
119
|
+
)
|
|
120
|
+
except Exception as error:
|
|
121
|
+
self.logger.error('Message publication failed: exchange=%s routing_key=%s message_id=%s error_type=%s', self.exchange, event, id, type(error).__name__)
|
|
122
|
+
raise
|
|
123
|
+
self.logger.debug('Publish API call completed: exchange=%s message_id=%s does not imply Consumer processing completion', self.exchange, id)
|
|
124
|
+
return id
|
|
125
|
+
|
|
126
|
+
def call(
|
|
127
|
+
self,
|
|
128
|
+
body: Any,
|
|
129
|
+
timeout: float = 5,
|
|
130
|
+
*,
|
|
131
|
+
event: Optional[str] = None,
|
|
132
|
+
) -> Any:
|
|
133
|
+
if self.rpc_parameters is None or self.decode is None:
|
|
134
|
+
raise RuntimeError('RPC requires decoder and connection parameters; obtain the exchange through Connection.exchange')
|
|
135
|
+
if isinstance(
|
|
136
|
+
timeout,
|
|
137
|
+
bool,
|
|
138
|
+
) or not isinstance(
|
|
139
|
+
timeout,
|
|
140
|
+
(
|
|
141
|
+
int,
|
|
142
|
+
float,
|
|
143
|
+
),
|
|
144
|
+
) or not isfinite(timeout) or timeout <= 0:
|
|
145
|
+
raise ValueError('RPC timeout must be a finite positive number of seconds')
|
|
146
|
+
deadline = monotonic() + timeout
|
|
147
|
+
settings = deepcopy(self.rpc_parameters)
|
|
148
|
+
settings.connection_attempts = 1
|
|
149
|
+
settings.socket_timeout = timeout if settings.socket_timeout is None else min(
|
|
150
|
+
settings.socket_timeout,
|
|
151
|
+
timeout,
|
|
152
|
+
)
|
|
153
|
+
settings.stack_timeout = timeout
|
|
154
|
+
settings.blocked_connection_timeout = timeout
|
|
155
|
+
timed_out = False
|
|
156
|
+
self.logger.debug('Opening dedicated RPC connection: exchange=%s routing_key=%s timeout=%s', self.exchange, event, timeout)
|
|
157
|
+
try:
|
|
158
|
+
connection = BlockingConnection(settings)
|
|
159
|
+
except Exception as error:
|
|
160
|
+
self.logger.error('Dedicated RPC connection failed: exchange=%s error_type=%s', self.exchange, type(error).__name__)
|
|
161
|
+
raise
|
|
162
|
+
try:
|
|
163
|
+
channel = connection.channel()
|
|
164
|
+
reply_queue = channel.queue_declare(
|
|
165
|
+
queue='',
|
|
166
|
+
exclusive=True,
|
|
167
|
+
auto_delete=True,
|
|
168
|
+
).method.queue
|
|
169
|
+
channel.confirm_delivery()
|
|
170
|
+
correlation_id = uuid4().hex
|
|
171
|
+
self.logger.debug('RPC reply queue created: exchange=%s reply_to=%s correlation_id=%s publisher_confirm=True', self.exchange, reply_queue, correlation_id)
|
|
172
|
+
responses = []
|
|
173
|
+
|
|
174
|
+
def receive(
|
|
175
|
+
channel,
|
|
176
|
+
method,
|
|
177
|
+
properties,
|
|
178
|
+
payload,
|
|
179
|
+
):
|
|
180
|
+
if properties.correlation_id == correlation_id:
|
|
181
|
+
responses.append((
|
|
182
|
+
properties,
|
|
183
|
+
payload,
|
|
184
|
+
))
|
|
185
|
+
self.logger.debug('RPC response received: exchange=%s correlation_id=%s', self.exchange, correlation_id)
|
|
186
|
+
else:
|
|
187
|
+
self.logger.debug('Ignoring RPC response with mismatched correlation_id: exchange=%s correlation_id=%s', self.exchange, properties.correlation_id)
|
|
188
|
+
|
|
189
|
+
channel.basic_consume(
|
|
190
|
+
reply_queue,
|
|
191
|
+
on_message_callback=receive,
|
|
192
|
+
auto_ack=True,
|
|
193
|
+
)
|
|
194
|
+
remaining = deadline - monotonic()
|
|
195
|
+
if remaining <= 0:
|
|
196
|
+
timed_out = True
|
|
197
|
+
self.logger.warning('RPC timed out before publication: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
|
|
198
|
+
raise TimeoutError('RPC timed out before request publication')
|
|
199
|
+
self.logger.debug('Publishing RPC request: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
|
|
200
|
+
channel.basic_publish(
|
|
201
|
+
exchange=self.exchange,
|
|
202
|
+
routing_key=event if event is not None else '',
|
|
203
|
+
body=self.encode(body),
|
|
204
|
+
properties=BasicProperties(
|
|
205
|
+
reply_to=reply_queue,
|
|
206
|
+
correlation_id=correlation_id,
|
|
207
|
+
content_type=self.encode.format,
|
|
208
|
+
content_encoding=self.encode.charset,
|
|
209
|
+
expiration=str(max(
|
|
210
|
+
1,
|
|
211
|
+
ceil(remaining * 1000),
|
|
212
|
+
)),
|
|
213
|
+
),
|
|
214
|
+
mandatory=True,
|
|
215
|
+
)
|
|
216
|
+
self.logger.debug('RPC request published: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
|
|
217
|
+
while not responses:
|
|
218
|
+
remaining = deadline - monotonic()
|
|
219
|
+
if remaining <= 0:
|
|
220
|
+
timed_out = True
|
|
221
|
+
self.logger.warning('RPC response timed out after publication; delivered work is not cancelled: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
|
|
222
|
+
raise TimeoutError(f'RPC response timed out for exchange {self.exchange!r} routing_key={event!r}')
|
|
223
|
+
# pika accepts a float time_limit; the bundled stub declares int only.
|
|
224
|
+
connection.process_data_events(time_limit=min( # pyright: ignore[reportArgumentType]
|
|
225
|
+
remaining,
|
|
226
|
+
0.1,
|
|
227
|
+
))
|
|
228
|
+
properties, payload = responses[0]
|
|
229
|
+
response = self.decode(
|
|
230
|
+
payload,
|
|
231
|
+
properties.content_type,
|
|
232
|
+
properties.content_encoding,
|
|
233
|
+
)
|
|
234
|
+
if not isinstance(
|
|
235
|
+
response,
|
|
236
|
+
dict,
|
|
237
|
+
) or type(response.get('ok')) is not bool:
|
|
238
|
+
raise ValueError('Invalid RPC response envelope')
|
|
239
|
+
if response['ok']:
|
|
240
|
+
if 'result' not in response:
|
|
241
|
+
raise ValueError('RPC response has no result')
|
|
242
|
+
self.logger.debug('RPC response validated: exchange=%s routing_key=%s correlation_id=%s elapsed=%s', self.exchange, event, correlation_id, timeout - (deadline - monotonic()))
|
|
243
|
+
return response['result']
|
|
244
|
+
error = response.get('error')
|
|
245
|
+
if not isinstance(
|
|
246
|
+
error,
|
|
247
|
+
dict,
|
|
248
|
+
) or not isinstance(
|
|
249
|
+
error.get('code'),
|
|
250
|
+
str,
|
|
251
|
+
) or not isinstance(
|
|
252
|
+
error.get('message'),
|
|
253
|
+
str,
|
|
254
|
+
):
|
|
255
|
+
raise ValueError('Invalid RPC error response')
|
|
256
|
+
self.logger.warning('RPC remote task error: exchange=%s routing_key=%s correlation_id=%s', self.exchange, event, correlation_id)
|
|
257
|
+
raise RemoteError(
|
|
258
|
+
error['code'],
|
|
259
|
+
error['message'],
|
|
260
|
+
)
|
|
261
|
+
except TimeoutError:
|
|
262
|
+
if not timed_out:
|
|
263
|
+
self.logger.error('RPC internal timeout error: exchange=%s routing_key=%s', self.exchange, event)
|
|
264
|
+
raise
|
|
265
|
+
except RemoteError:
|
|
266
|
+
raise
|
|
267
|
+
except Exception as error:
|
|
268
|
+
self.logger.error('RPC transmission or response handling failed: exchange=%s routing_key=%s error_type=%s', self.exchange, event, type(error).__name__)
|
|
269
|
+
raise
|
|
270
|
+
finally:
|
|
271
|
+
try:
|
|
272
|
+
if connection.is_open:
|
|
273
|
+
connection.close()
|
|
274
|
+
self.logger.debug('Dedicated RPC connection and reply queue cleanup completed: exchange=%s', self.exchange)
|
|
275
|
+
except Exception as error:
|
|
276
|
+
self.logger.error('Dedicated RPC connection cleanup failed: exchange=%s error_type=%s', self.exchange, type(error).__name__)
|
|
277
|
+
raise
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from DefStream.Broker import (
|
|
4
|
+
Sendable,
|
|
5
|
+
Callable,
|
|
6
|
+
Poppable,
|
|
7
|
+
RemoteError,
|
|
8
|
+
validate_priority,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from .Serializer import (
|
|
12
|
+
Encoder,
|
|
13
|
+
Decoder,
|
|
14
|
+
)
|
|
15
|
+
from .Event import Event
|
|
16
|
+
from copy import deepcopy
|
|
17
|
+
from logging import getLogger
|
|
18
|
+
from math import (
|
|
19
|
+
ceil,
|
|
20
|
+
isfinite,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from pika import (
|
|
24
|
+
BlockingConnection,
|
|
25
|
+
BasicProperties,
|
|
26
|
+
ConnectionParameters,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from time import (
|
|
30
|
+
time,
|
|
31
|
+
sleep,
|
|
32
|
+
monotonic,
|
|
33
|
+
)
|
|
34
|
+
from uuid import uuid4
|
|
35
|
+
|
|
36
|
+
from typing import (
|
|
37
|
+
Any,
|
|
38
|
+
Optional,
|
|
39
|
+
Dict,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = (
|
|
43
|
+
'Queue',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Queue(
|
|
48
|
+
Sendable,
|
|
49
|
+
Callable,
|
|
50
|
+
Poppable,
|
|
51
|
+
):
|
|
52
|
+
"""Queue of Event Broker for RabbitMQ"""
|
|
53
|
+
logger = getLogger('Broker.RabbitMQ.Queue')
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
connection: BlockingConnection,
|
|
58
|
+
encode: Encoder,
|
|
59
|
+
decode: Decoder,
|
|
60
|
+
name: str,
|
|
61
|
+
rpc_parameters: Optional[ConnectionParameters] = None,
|
|
62
|
+
):
|
|
63
|
+
self.connection = connection
|
|
64
|
+
self.encode = encode
|
|
65
|
+
self.decode = decode
|
|
66
|
+
try:
|
|
67
|
+
self.channel = self.connection.channel()
|
|
68
|
+
except Exception as error:
|
|
69
|
+
self.logger.error('Queue channel creation failed: queue=%s error_type=%s', name, type(error).__name__)
|
|
70
|
+
raise
|
|
71
|
+
self.queue = name
|
|
72
|
+
self.event = None
|
|
73
|
+
self.rpc_parameters = rpc_parameters
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
def __del__(self):
|
|
77
|
+
channel = getattr(self, 'channel', None)
|
|
78
|
+
try:
|
|
79
|
+
if channel and channel.is_open:
|
|
80
|
+
channel.close()
|
|
81
|
+
self.logger.debug('Queue channel cleanup completed: queue=%s', self.queue)
|
|
82
|
+
except Exception as error:
|
|
83
|
+
self.logger.error('Queue channel cleanup failed: error_type=%s', type(error).__name__)
|
|
84
|
+
raise
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
def __str__(self): return self.queue
|
|
88
|
+
|
|
89
|
+
def send(
|
|
90
|
+
self,
|
|
91
|
+
body,
|
|
92
|
+
event: Optional[str] = None,
|
|
93
|
+
headers: Dict = {},
|
|
94
|
+
priority: Optional[int] = None,
|
|
95
|
+
expiration: Optional[int] = None,
|
|
96
|
+
timestamp: Optional[int] = None,
|
|
97
|
+
persistent: bool = True,
|
|
98
|
+
id: Optional[str] = None,
|
|
99
|
+
reply_to: Optional[str] = None,
|
|
100
|
+
correlation_id: Optional[str] = None,
|
|
101
|
+
):
|
|
102
|
+
validate_priority(
|
|
103
|
+
priority,
|
|
104
|
+
255,
|
|
105
|
+
)
|
|
106
|
+
if not timestamp: timestamp = int(time())
|
|
107
|
+
if not id: id = uuid4().hex
|
|
108
|
+
properties = BasicProperties(
|
|
109
|
+
type=event if event else '',
|
|
110
|
+
headers=headers,
|
|
111
|
+
content_type=self.encode.format,
|
|
112
|
+
content_encoding=self.encode.charset,
|
|
113
|
+
priority=priority,
|
|
114
|
+
timestamp=timestamp,
|
|
115
|
+
expiration=str(expiration) if expiration is not None else None,
|
|
116
|
+
message_id=id,
|
|
117
|
+
delivery_mode=2 if persistent else 1,
|
|
118
|
+
reply_to=reply_to,
|
|
119
|
+
correlation_id=correlation_id,
|
|
120
|
+
)
|
|
121
|
+
self.logger.debug('Publishing message: queue=%s message_id=%s priority=%s persistent=%s publisher_confirm=False', self.queue, id, priority, persistent)
|
|
122
|
+
try:
|
|
123
|
+
self.channel.basic_publish(
|
|
124
|
+
exchange='',
|
|
125
|
+
routing_key=self.queue,
|
|
126
|
+
properties=properties,
|
|
127
|
+
body=self.encode(body)
|
|
128
|
+
)
|
|
129
|
+
except Exception as error:
|
|
130
|
+
self.logger.error('Message publication failed: queue=%s message_id=%s error_type=%s', self.queue, id, type(error).__name__)
|
|
131
|
+
raise
|
|
132
|
+
self.logger.debug('Publish API call completed: queue=%s message_id=%s does not imply Consumer processing completion', self.queue, id)
|
|
133
|
+
return id
|
|
134
|
+
|
|
135
|
+
def call(
|
|
136
|
+
self,
|
|
137
|
+
body: Any,
|
|
138
|
+
timeout: float = 5,
|
|
139
|
+
*,
|
|
140
|
+
priority: Optional[int] = None,
|
|
141
|
+
) -> Any:
|
|
142
|
+
validate_priority(
|
|
143
|
+
priority,
|
|
144
|
+
255,
|
|
145
|
+
)
|
|
146
|
+
if self.rpc_parameters is None:
|
|
147
|
+
raise RuntimeError('RPC requires connection parameters; obtain the queue through Connection.queue')
|
|
148
|
+
if isinstance(
|
|
149
|
+
timeout,
|
|
150
|
+
bool,
|
|
151
|
+
) or not isinstance(
|
|
152
|
+
timeout,
|
|
153
|
+
(
|
|
154
|
+
int,
|
|
155
|
+
float,
|
|
156
|
+
),
|
|
157
|
+
) or not isfinite(timeout) or timeout <= 0:
|
|
158
|
+
raise ValueError('RPC timeout must be a finite positive number of seconds')
|
|
159
|
+
deadline = monotonic() + timeout
|
|
160
|
+
settings = deepcopy(self.rpc_parameters)
|
|
161
|
+
settings.connection_attempts = 1
|
|
162
|
+
settings.socket_timeout = timeout if settings.socket_timeout is None else min(
|
|
163
|
+
settings.socket_timeout,
|
|
164
|
+
timeout,
|
|
165
|
+
)
|
|
166
|
+
settings.stack_timeout = timeout
|
|
167
|
+
settings.blocked_connection_timeout = timeout
|
|
168
|
+
timed_out = False
|
|
169
|
+
self.logger.debug('Opening dedicated RPC connection: queue=%s timeout=%s', self.queue, timeout)
|
|
170
|
+
try:
|
|
171
|
+
connection = BlockingConnection(settings)
|
|
172
|
+
except Exception as error:
|
|
173
|
+
self.logger.error('Dedicated RPC connection failed: queue=%s error_type=%s', self.queue, type(error).__name__)
|
|
174
|
+
raise
|
|
175
|
+
try:
|
|
176
|
+
channel = connection.channel()
|
|
177
|
+
reply_queue = channel.queue_declare(
|
|
178
|
+
queue=f'{self.queue}.{uuid4().hex}',
|
|
179
|
+
exclusive=True,
|
|
180
|
+
auto_delete=True,
|
|
181
|
+
).method.queue
|
|
182
|
+
channel.confirm_delivery()
|
|
183
|
+
correlation_id = uuid4().hex
|
|
184
|
+
self.logger.debug('RPC reply queue created: queue=%s reply_to=%s correlation_id=%s publisher_confirm=True', self.queue, reply_queue, correlation_id)
|
|
185
|
+
responses = []
|
|
186
|
+
|
|
187
|
+
def receive(
|
|
188
|
+
channel,
|
|
189
|
+
method,
|
|
190
|
+
properties,
|
|
191
|
+
payload,
|
|
192
|
+
):
|
|
193
|
+
if properties.correlation_id == correlation_id:
|
|
194
|
+
responses.append((
|
|
195
|
+
properties,
|
|
196
|
+
payload,
|
|
197
|
+
))
|
|
198
|
+
self.logger.debug('RPC response received: queue=%s correlation_id=%s', self.queue, correlation_id)
|
|
199
|
+
else:
|
|
200
|
+
self.logger.debug('Ignoring RPC response with mismatched correlation_id: queue=%s correlation_id=%s', self.queue, properties.correlation_id)
|
|
201
|
+
|
|
202
|
+
channel.basic_consume(
|
|
203
|
+
reply_queue,
|
|
204
|
+
on_message_callback=receive,
|
|
205
|
+
auto_ack=True,
|
|
206
|
+
)
|
|
207
|
+
remaining = deadline - monotonic()
|
|
208
|
+
if remaining <= 0:
|
|
209
|
+
timed_out = True
|
|
210
|
+
self.logger.warning('RPC timed out before publication: queue=%s correlation_id=%s', self.queue, correlation_id)
|
|
211
|
+
raise TimeoutError('RPC timed out before request publication')
|
|
212
|
+
self.logger.debug('Publishing RPC request: queue=%s correlation_id=%s priority=%s', self.queue, correlation_id, priority)
|
|
213
|
+
channel.basic_publish(
|
|
214
|
+
exchange='',
|
|
215
|
+
routing_key=self.queue,
|
|
216
|
+
body=self.encode(body),
|
|
217
|
+
properties=BasicProperties(
|
|
218
|
+
priority=priority,
|
|
219
|
+
reply_to=reply_queue,
|
|
220
|
+
correlation_id=correlation_id,
|
|
221
|
+
content_type=self.encode.format,
|
|
222
|
+
content_encoding=self.encode.charset,
|
|
223
|
+
expiration=str(max(
|
|
224
|
+
1,
|
|
225
|
+
ceil(remaining * 1000),
|
|
226
|
+
)),
|
|
227
|
+
),
|
|
228
|
+
mandatory=True,
|
|
229
|
+
)
|
|
230
|
+
self.logger.debug('RPC request published: queue=%s correlation_id=%s', self.queue, correlation_id)
|
|
231
|
+
while not responses:
|
|
232
|
+
remaining = deadline - monotonic()
|
|
233
|
+
if remaining <= 0:
|
|
234
|
+
timed_out = True
|
|
235
|
+
self.logger.warning('RPC response timed out after publication; delivered work is not cancelled: queue=%s correlation_id=%s', self.queue, correlation_id)
|
|
236
|
+
raise TimeoutError(f'RPC response timed out for queue {self.queue!r}')
|
|
237
|
+
# pika accepts a float time_limit; the bundled stub declares int only.
|
|
238
|
+
connection.process_data_events(time_limit=min( # pyright: ignore[reportArgumentType]
|
|
239
|
+
remaining,
|
|
240
|
+
0.1,
|
|
241
|
+
))
|
|
242
|
+
properties, payload = responses[0]
|
|
243
|
+
response = self.decode(
|
|
244
|
+
payload,
|
|
245
|
+
properties.content_type,
|
|
246
|
+
properties.content_encoding,
|
|
247
|
+
)
|
|
248
|
+
if not isinstance(
|
|
249
|
+
response,
|
|
250
|
+
dict,
|
|
251
|
+
) or type(response.get('ok')) is not bool:
|
|
252
|
+
raise ValueError('Invalid RPC response envelope')
|
|
253
|
+
if response['ok']:
|
|
254
|
+
if 'result' not in response:
|
|
255
|
+
raise ValueError('RPC response has no result')
|
|
256
|
+
self.logger.debug('RPC response validated: queue=%s correlation_id=%s elapsed=%s', self.queue, correlation_id, timeout - (deadline - monotonic()))
|
|
257
|
+
return response['result']
|
|
258
|
+
error = response.get('error')
|
|
259
|
+
if not isinstance(
|
|
260
|
+
error,
|
|
261
|
+
dict,
|
|
262
|
+
) or not isinstance(
|
|
263
|
+
error.get('code'),
|
|
264
|
+
str,
|
|
265
|
+
) or not isinstance(
|
|
266
|
+
error.get('message'),
|
|
267
|
+
str,
|
|
268
|
+
):
|
|
269
|
+
raise ValueError('Invalid RPC error response')
|
|
270
|
+
self.logger.warning('RPC remote task error: queue=%s correlation_id=%s', self.queue, correlation_id)
|
|
271
|
+
raise RemoteError(
|
|
272
|
+
error['code'],
|
|
273
|
+
error['message'],
|
|
274
|
+
)
|
|
275
|
+
except TimeoutError:
|
|
276
|
+
if not timed_out:
|
|
277
|
+
self.logger.error('RPC internal timeout error: queue=%s', self.queue)
|
|
278
|
+
raise
|
|
279
|
+
except RemoteError:
|
|
280
|
+
raise
|
|
281
|
+
except Exception as error:
|
|
282
|
+
self.logger.error('RPC transmission or response handling failed: queue=%s error_type=%s', self.queue, type(error).__name__)
|
|
283
|
+
raise
|
|
284
|
+
finally:
|
|
285
|
+
try:
|
|
286
|
+
if connection.is_open:
|
|
287
|
+
connection.close()
|
|
288
|
+
self.logger.debug('Dedicated RPC connection and reply queue cleanup completed: queue=%s', self.queue)
|
|
289
|
+
except Exception as error:
|
|
290
|
+
self.logger.error('Dedicated RPC connection cleanup failed: queue=%s error_type=%s', self.queue, type(error).__name__)
|
|
291
|
+
raise
|
|
292
|
+
|
|
293
|
+
def pop(self, timeout: Optional[int] = None) -> Optional[Event]:
|
|
294
|
+
self.logger.debug('Waiting for a single message: queue=%s timeout_ms=%s', self.queue, timeout)
|
|
295
|
+
channel = self.channel
|
|
296
|
+
try:
|
|
297
|
+
channel.basic_qos(0, 1, False)
|
|
298
|
+
deadline = time() + (timeout / 1000) if timeout else None
|
|
299
|
+
while True:
|
|
300
|
+
method, properties, body = channel.basic_get(queue=self.queue, auto_ack=False)
|
|
301
|
+
if method:
|
|
302
|
+
# pika returns properties and body together with a GetOk method frame.
|
|
303
|
+
assert properties is not None and body is not None
|
|
304
|
+
event = Event(
|
|
305
|
+
channel,
|
|
306
|
+
self.queue,
|
|
307
|
+
None,
|
|
308
|
+
method.delivery_tag,
|
|
309
|
+
properties,
|
|
310
|
+
self.decode(body, properties.content_type, properties.content_encoding),
|
|
311
|
+
)
|
|
312
|
+
self.logger.debug('Single message received and decoded: queue=%s delivery_tag=%s message_id=%s', self.queue, method.delivery_tag, properties.message_id)
|
|
313
|
+
return event
|
|
314
|
+
if deadline and time() >= deadline:
|
|
315
|
+
break
|
|
316
|
+
sleep(0.1)
|
|
317
|
+
except Exception as error:
|
|
318
|
+
self.logger.error('Single-message reception failed: queue=%s error_type=%s', self.queue, type(error).__name__)
|
|
319
|
+
raise
|
|
320
|
+
self.logger.debug('Single-message wait ended: queue=%s', self.queue)
|
|
321
|
+
return
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from abc import (
|
|
4
|
+
ABCMeta,
|
|
5
|
+
abstractmethod,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from json import (
|
|
9
|
+
dumps,
|
|
10
|
+
loads,
|
|
11
|
+
)
|
|
12
|
+
from typing import (
|
|
13
|
+
Any,
|
|
14
|
+
Optional,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
'Encoder',
|
|
19
|
+
'Decoder',
|
|
20
|
+
'JavaScriptObjectNotationEncoder',
|
|
21
|
+
'JavaScriptObjectNotationDecoder',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
class Encoder(metaclass=ABCMeta):
|
|
25
|
+
"""Encoder Interface"""
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def __call__(self, obj: Any) -> bytes:
|
|
28
|
+
raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def format(self) -> str:
|
|
33
|
+
raise NotImplementedError('{} must be implemented format'.format(self.__class__.__name__))
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def charset(self) -> str:
|
|
38
|
+
raise NotImplementedError('{} must be implemented charset'.format(self.__class__.__name__))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Decoder(metaclass=ABCMeta):
|
|
42
|
+
"""Decoder Interface"""
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def __call__(
|
|
45
|
+
self,
|
|
46
|
+
obj: bytes,
|
|
47
|
+
format: Optional[str],
|
|
48
|
+
charset: Optional[str],
|
|
49
|
+
) -> Any:
|
|
50
|
+
raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class JavaScriptObjectNotationEncoder(Encoder):
|
|
54
|
+
"""JavaScriptObjectNotation Encoder"""
|
|
55
|
+
def __call__(self, obj: Any) -> bytes:
|
|
56
|
+
return dumps(obj, ensure_ascii=False).encode(self.charset)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def format(self) -> str: return 'application/json'
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def charset(self) -> str: return 'utf-8'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class JavaScriptObjectNotationDecoder(Decoder):
|
|
66
|
+
"""JavaScriptObjectNotation Decoder"""
|
|
67
|
+
def __call__(
|
|
68
|
+
self,
|
|
69
|
+
obj: bytes,
|
|
70
|
+
format: Optional[str],
|
|
71
|
+
charset: Optional[str],
|
|
72
|
+
) -> Any:
|
|
73
|
+
if format != 'application/json': raise ValueError('format is not support')
|
|
74
|
+
if charset is None: raise ValueError('charset is required')
|
|
75
|
+
return loads(obj.decode(charset))
|