DefStream-RabbitMQ 0.1.28__123-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.
@@ -0,0 +1,420 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Consumer as BaseConsumer,
5
+ Recoverable,
6
+ EventHandler,
7
+ Event as BaseEvent,
8
+ )
9
+
10
+ from .Serializer import (
11
+ Decoder,
12
+ Encoder,
13
+ JavaScriptObjectNotationEncoder,
14
+ )
15
+ from logging import getLogger
16
+
17
+ from .Event import (
18
+ Event,
19
+ Requeue,
20
+ )
21
+ from .Subscription import Subscription
22
+ from DefStream.Broker.Event import EventErrorPolicy
23
+ from typing import (
24
+ Any,
25
+ Callable,
26
+ Optional,
27
+ )
28
+
29
+ from pika import (
30
+ BasicProperties,
31
+ BlockingConnection,
32
+ )
33
+ from pika.exceptions import (
34
+ AMQPConnectionError,
35
+ AuthenticationError,
36
+ ChannelClosedByBroker,
37
+ ConnectionClosedByBroker,
38
+ IncompatibleProtocolError,
39
+ ProbableAccessDeniedError,
40
+ ProbableAuthenticationError,
41
+ UnroutableError,
42
+ )
43
+
44
+ __all__ = (
45
+ 'Consumer',
46
+ )
47
+
48
+
49
+ class ConsumptionInterrupted(RuntimeError):
50
+ pass
51
+
52
+
53
+ class Consumer(
54
+ BaseConsumer,
55
+ Recoverable,
56
+ ):
57
+ """Consumer of Event Broker for RabbitMQ"""
58
+ logger = getLogger('Broker.RabbitMQ.Consumer')
59
+
60
+ def __init__(
61
+ self,
62
+ connection: BlockingConnection,
63
+ decode: Decoder,
64
+ handler: Optional[EventHandler] = None,
65
+ qos: int = 1,
66
+ offset: Optional[int] = None,
67
+ encode: Optional[Encoder] = None,
68
+ ):
69
+ self.connection = connection
70
+ self.decode = decode
71
+ self.qos = qos
72
+ try:
73
+ self.channel = self.connection.channel()
74
+ self.channel.basic_qos(
75
+ prefetch_count=qos,
76
+ global_qos=False,
77
+ )
78
+ except Exception as error:
79
+ self.logger.error('Consumption channel initialization failed: qos=%s error_type=%s', qos, type(error).__name__)
80
+ raise
81
+ self.logger.debug('Consumption channel created: qos=%s offset=%s', qos, offset)
82
+ self.offset = offset
83
+ self.handler = handler
84
+ self.encode = encode if encode is not None else JavaScriptObjectNotationEncoder()
85
+ self.response_channel = None
86
+ self.queues = set()
87
+ self.stopped = False
88
+ self.callback_error: Optional[BaseException] = None
89
+ return
90
+
91
+ def __del__(self):
92
+ self.close()
93
+ return
94
+
95
+ def subs(
96
+ self,
97
+ queue: Any,
98
+ handler: Optional[EventHandler] = None,
99
+ ) -> None:
100
+ if handler is None:
101
+ handler = self.handler
102
+ if handler is None:
103
+ raise ValueError('subs requires a handler or a default handler')
104
+ if isinstance(
105
+ queue,
106
+ str,
107
+ ):
108
+ subscription = Subscription(
109
+ queue=queue,
110
+ offset=self.offset,
111
+ )
112
+ else:
113
+ subscription = Subscription.parse(queue)
114
+ if subscription.queue in self.queues:
115
+ raise ValueError('A Run is already subscribed to queue {}'.format(subscription.queue))
116
+ qos = self.qos if subscription.qos is None else subscription.qos
117
+ self.logger.debug('Applying subscription: queue=%s qos=%s offset=%s', subscription.queue, qos, subscription.offset)
118
+ try:
119
+ self.channel.basic_qos(
120
+ prefetch_count=qos,
121
+ global_qos=False,
122
+ )
123
+ tag = self.channel.basic_consume(
124
+ subscription.queue,
125
+ Callback(
126
+ self.decode,
127
+ handler,
128
+ subscription.queue,
129
+ logger=self.logger,
130
+ owner=self,
131
+ ),
132
+ auto_ack=False,
133
+ exclusive=False,
134
+ arguments={'x-stream-offset': subscription.offset} if subscription.offset is not None else None,
135
+ )
136
+ except Exception as error:
137
+ self.logger.error('Subscription failed: queue=%s error_type=%s', subscription.queue, type(error).__name__)
138
+ raise
139
+ self.queues.add(subscription.queue)
140
+ self.logger.debug('Subscription applied: queue=%s consumer_tag=%s', subscription.queue, tag)
141
+ return
142
+
143
+ def call(self, callback: Callable[[], None]) -> None:
144
+ self.logger.debug('Scheduling callback on connection owner thread')
145
+ try:
146
+ self.connection.add_callback_threadsafe(callback)
147
+ except Exception as error:
148
+ self.logger.error('Callback scheduling failed: error_type=%s', type(error).__name__)
149
+ raise
150
+ self.logger.debug('Callback scheduled on connection owner thread')
151
+ return
152
+
153
+ def fail(
154
+ self,
155
+ event: BaseEvent,
156
+ policy: Optional[EventErrorPolicy] = None,
157
+ ) -> None:
158
+ self.logger.debug('Task failure policy selected: policy=%s', type(policy).__name__ if policy is not None else 'Requeue')
159
+ if isinstance(
160
+ event,
161
+ Event,
162
+ ) and event.properties.reply_to:
163
+ event = FailedRequest(
164
+ self,
165
+ event,
166
+ )
167
+ (policy if policy is not None else Requeue())(event)
168
+ return
169
+
170
+ def complete(
171
+ self,
172
+ event: BaseEvent,
173
+ result: Any,
174
+ ) -> None:
175
+ self.logger.debug('Task completion policy selected: ack')
176
+ if isinstance(
177
+ event,
178
+ Event,
179
+ ) and event.properties.reply_to:
180
+ self.logger.debug('Building RPC success response: queue=%s correlation_id=%s', event.queue, event.properties.correlation_id)
181
+ self.respond(
182
+ event,
183
+ {
184
+ 'ok': True,
185
+ 'result': result,
186
+ },
187
+ )
188
+ event.ack()
189
+ return
190
+
191
+ def respond(
192
+ self,
193
+ event: Event,
194
+ response: Any,
195
+ ) -> None:
196
+ if not event.properties.correlation_id:
197
+ self.logger.error('RPC response validation failed: missing correlation_id')
198
+ raise ValueError('RPC request requires correlation_id')
199
+ channel = self.response_channel
200
+ self.logger.debug('Attempting RPC response: correlation_id=%s reply_to=%s', event.properties.correlation_id, event.properties.reply_to)
201
+ if channel is None:
202
+ try:
203
+ channel = self.connection.channel()
204
+ channel.confirm_delivery()
205
+ self.response_channel = channel
206
+ self.logger.debug('RPC response channel created: publisher_confirm=True')
207
+ except Exception as error:
208
+ self.logger.error('RPC response channel creation failed: correlation_id=%s error_type=%s', event.properties.correlation_id, type(error).__name__)
209
+ raise
210
+ else:
211
+ self.logger.debug('Reusing RPC response channel')
212
+ try:
213
+ channel.basic_publish(
214
+ exchange='',
215
+ routing_key=event.properties.reply_to,
216
+ body=self.encode(response),
217
+ properties=BasicProperties(
218
+ correlation_id=event.properties.correlation_id,
219
+ content_type=self.encode.format,
220
+ content_encoding=self.encode.charset,
221
+ ),
222
+ mandatory=True,
223
+ )
224
+ except UnroutableError:
225
+ self.logger.warning(
226
+ 'RPC reply is unroutable; settling request without a response: correlation_id=%s reply_to=%s',
227
+ event.properties.correlation_id,
228
+ event.properties.reply_to,
229
+ )
230
+ except Exception as error:
231
+ self.logger.error('RPC response publication failed: correlation_id=%s error_type=%s', event.properties.correlation_id, type(error).__name__)
232
+ raise
233
+ else:
234
+ self.logger.debug('RPC response published: correlation_id=%s', event.properties.correlation_id)
235
+ return
236
+
237
+ def close(self) -> None:
238
+ response_channel = getattr(self, 'response_channel', None)
239
+ try:
240
+ if response_channel is not None and response_channel.is_open:
241
+ response_channel.close()
242
+ self.logger.debug('RPC response channel closed')
243
+ channel = getattr(self, 'channel', None)
244
+ if channel is not None and channel.is_open:
245
+ channel.close()
246
+ self.logger.debug('Consumption channel closed')
247
+ except Exception as error:
248
+ self.logger.error('Channel close failed: error_type=%s', type(error).__name__)
249
+ raise
250
+ return
251
+
252
+ def run(self):
253
+ self.logger.debug('Starting consumption loop')
254
+ self.callback_error = None
255
+ self.timer = self.connection.call_later(0.2, self.check_channel)
256
+ try:
257
+ self.channel.start_consuming()
258
+ if not self.stopped:
259
+ raise ConsumptionInterrupted('Consumption loop ended without a stop request')
260
+ except Exception as error:
261
+ if isinstance(error, ChannelClosedByBroker) and self.ack_timeout(error):
262
+ self.logger.warning(
263
+ 'Delivery acknowledgement timed out: queues=%s reason=%s',
264
+ sorted(self.queues),
265
+ error.reply_text,
266
+ )
267
+ owned_boundaries = (
268
+ Event.ack.__code__,
269
+ Event.nack.__code__,
270
+ Event.reject.__code__,
271
+ Consumer.respond.__code__,
272
+ Consumer.stop.__code__,
273
+ )
274
+ trace = error.__traceback__
275
+ observed = error is self.callback_error
276
+ while trace is not None:
277
+ if trace.tb_frame.f_code in owned_boundaries:
278
+ observed = True
279
+ self.callback_error = None
280
+ break
281
+ trace = trace.tb_next
282
+ if not observed:
283
+ self.logger.error('Consumption loop failed: error_type=%s', type(error).__name__)
284
+ raise
285
+ finally:
286
+ self.connection.remove_timeout(self.timer)
287
+ self.logger.debug('Consumption loop ended')
288
+ return
289
+
290
+ def check_channel(self) -> None:
291
+ if not self.stopped and self.channel.is_open:
292
+ self.timer = self.connection.call_later(0.2, self.check_channel)
293
+ return
294
+
295
+ @staticmethod
296
+ def ack_timeout(error: ChannelClosedByBroker) -> bool:
297
+ return (
298
+ error.reply_code == 406
299
+ and 'acknowledgement' in error.reply_text.lower()
300
+ and 'timed out' in error.reply_text.lower()
301
+ )
302
+
303
+ def is_recoverable(self, error: BaseException) -> bool:
304
+ if error is self.callback_error:
305
+ return False
306
+ if isinstance(error, (
307
+ AuthenticationError,
308
+ ProbableAuthenticationError,
309
+ ProbableAccessDeniedError,
310
+ IncompatibleProtocolError,
311
+ )):
312
+ return False
313
+ if isinstance(error, ChannelClosedByBroker):
314
+ return self.ack_timeout(error) or error.reply_code in (320, 541)
315
+ if isinstance(error, ConnectionClosedByBroker):
316
+ return error.reply_code in (320, 541)
317
+ return isinstance(error, (
318
+ AMQPConnectionError,
319
+ ConsumptionInterrupted,
320
+ ))
321
+
322
+ def stop(self):
323
+ if not self.stopped:
324
+ self.stopped = True
325
+ self.logger.debug('Consumption loop stop requested')
326
+ try:
327
+ self.channel.stop_consuming()
328
+ except Exception as error:
329
+ self.logger.error('Failed to stop consumption loop: error_type=%s', type(error).__name__)
330
+ raise
331
+ self.logger.debug('Consumption loop stopped')
332
+ return
333
+
334
+
335
+ class FailedRequest(BaseEvent):
336
+ def __init__(
337
+ self,
338
+ consumer: Consumer,
339
+ event: Event,
340
+ ):
341
+ self.consumer = consumer
342
+ self.event = event
343
+
344
+ @property
345
+ def body(self) -> Any:
346
+ return self.event.body
347
+
348
+ def ack(self) -> None:
349
+ self.event.ack()
350
+
351
+ def nack(self) -> None:
352
+ self.event.nack()
353
+
354
+ def reject(self) -> None:
355
+ self.consumer.logger.debug('Building RPC failure response: queue=%s correlation_id=%s', self.event.queue, self.event.properties.correlation_id)
356
+ self.consumer.respond(
357
+ self.event,
358
+ {
359
+ 'ok': False,
360
+ 'error': {
361
+ 'code': 'ProcessingError',
362
+ 'message': 'Request processing failed',
363
+ },
364
+ },
365
+ )
366
+ self.event.reject()
367
+ return
368
+
369
+
370
+ class Callback(object):
371
+ def __init__(
372
+ self,
373
+ decode: Decoder,
374
+ handler: EventHandler,
375
+ queue: str,
376
+ logger=None,
377
+ owner=None,
378
+ ):
379
+ self.decode = decode
380
+ self.handler = handler
381
+ self.queue = queue
382
+ self.logger = logger if logger is not None else Consumer.logger
383
+ self.owner = owner
384
+ return
385
+ def __call__(
386
+ self,
387
+ channel,
388
+ method,
389
+ properties,
390
+ body,
391
+ ):
392
+ try:
393
+ return self.deliver(channel, method, properties, body)
394
+ except Exception as error:
395
+ if self.owner is not None:
396
+ self.owner.callback_error = error
397
+ raise
398
+
399
+ def deliver(
400
+ self,
401
+ channel,
402
+ method,
403
+ properties,
404
+ body,
405
+ ):
406
+ self.logger.debug('Message received: queue=%s consumer_tag=%s delivery_tag=%s message_id=%s', self.queue, method.consumer_tag, method.delivery_tag, properties.message_id)
407
+ try:
408
+ decoded = self.decode(body, properties.content_type, properties.content_encoding)
409
+ except Exception as error:
410
+ self.logger.error('Message decoding failed: queue=%s delivery_tag=%s error_type=%s', self.queue, method.delivery_tag, type(error).__name__)
411
+ raise
412
+ self.logger.debug('Message decoded; dispatching to handler: queue=%s delivery_tag=%s', self.queue, method.delivery_tag)
413
+ return self.handler(Event(
414
+ channel,
415
+ self.queue,
416
+ method.consumer_tag,
417
+ method.delivery_tag,
418
+ properties,
419
+ decoded,
420
+ ))
@@ -0,0 +1,146 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ from DefStream.Broker import (
4
+ Event as BaseEvent,
5
+ EventHandler as BaseEventHandler,
6
+ )
7
+ from DefStream.Broker.Event import EventErrorPolicy
8
+
9
+ from abc import abstractmethod
10
+ from logging import getLogger
11
+
12
+ __all__ = (
13
+ 'Event',
14
+ 'EventHandler',
15
+ 'Requeue',
16
+ 'Reject',
17
+ )
18
+
19
+
20
+ class Event(BaseEvent):
21
+ """Event of Event Broker for RabbitMQ"""
22
+ logger = getLogger('Broker.RabbitMQ.Event')
23
+
24
+ def __init__(
25
+ self,
26
+ channel,
27
+ queue,
28
+ consumer,
29
+ transaction,
30
+ properties,
31
+ body,
32
+ ):
33
+ self.channel = channel
34
+ self.queue = queue
35
+ self.consumer = consumer
36
+ self.transaction = transaction
37
+ self.properties = properties
38
+ self.props = properties.headers or {}
39
+ self.payload = body
40
+ return
41
+
42
+ def __repr__(self):
43
+ return 'Event(id={}, body={}, type={}, headers={})'.format(
44
+ self.properties.message_id,
45
+ self.payload,
46
+ self.properties.type,
47
+ self.props,
48
+ )
49
+
50
+ def ack(self):
51
+ self.logger.debug('ack requested: queue=%s delivery_tag=%s message_id=%s', self.queue, self.transaction, self.id)
52
+ try:
53
+ self.channel.basic_ack(self.transaction)
54
+ except Exception as error:
55
+ self.logger.error('ack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, self.transaction, type(error).__name__)
56
+ raise
57
+ self.logger.debug('ack broker API call completed: queue=%s delivery_tag=%s', self.queue, self.transaction)
58
+ return
59
+
60
+ def nack(self):
61
+ self.logger.debug('nack requested: queue=%s delivery_tag=%s message_id=%s requeue=True', self.queue, self.transaction, self.id)
62
+ try:
63
+ self.channel.basic_nack(
64
+ self.transaction,
65
+ requeue=True,
66
+ )
67
+ except Exception as error:
68
+ self.logger.error('nack failed: queue=%s delivery_tag=%s error_type=%s', self.queue, self.transaction, type(error).__name__)
69
+ raise
70
+ self.logger.debug('nack broker API call completed: queue=%s delivery_tag=%s', self.queue, self.transaction)
71
+ return
72
+
73
+ def reject(self):
74
+ self.logger.debug('reject requested: queue=%s delivery_tag=%s message_id=%s requeue=False', self.queue, self.transaction, self.id)
75
+ try:
76
+ self.channel.basic_reject(
77
+ self.transaction,
78
+ requeue=False,
79
+ )
80
+ except Exception as error:
81
+ self.logger.error('reject failed: queue=%s delivery_tag=%s error_type=%s', self.queue, self.transaction, type(error).__name__)
82
+ raise
83
+ self.logger.debug('reject broker API call completed: queue=%s delivery_tag=%s', self.queue, self.transaction)
84
+ return
85
+
86
+ def headers(self):
87
+ return self.props
88
+
89
+ def header(
90
+ self,
91
+ key,
92
+ ):
93
+ if key not in self.props:
94
+ return None
95
+ return self.props[key]
96
+
97
+ @property
98
+ def src(self):
99
+ return self.queue
100
+
101
+ @property
102
+ def id(self):
103
+ return self.properties.message_id
104
+
105
+ @property
106
+ def type(self):
107
+ return self.properties.type
108
+
109
+ @property
110
+ def body(self):
111
+ return self.payload
112
+
113
+ @property
114
+ def priority(self):
115
+ if not hasattr(
116
+ self.properties,
117
+ 'priority',
118
+ ):
119
+ return None
120
+ return self.properties.priority
121
+
122
+
123
+ class EventHandler(BaseEventHandler):
124
+ """Event Handler Interface of Event Broker for RabbitMQ"""
125
+ @abstractmethod
126
+ def __call__(
127
+ self,
128
+ event: Event,
129
+ ):
130
+ raise NotImplementedError('{} must be implemented __call__'.format(self.__class__.__name__))
131
+
132
+
133
+ class Requeue(EventErrorPolicy):
134
+ def __call__(
135
+ self,
136
+ event: BaseEvent,
137
+ ) -> None:
138
+ event.nack()
139
+
140
+
141
+ class Reject(EventErrorPolicy):
142
+ def __call__(
143
+ self,
144
+ event: BaseEvent,
145
+ ) -> None:
146
+ event.reject()