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