onestep 0.5.0__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.
- onestep/__init__.py +67 -0
- onestep/_utils.py +21 -0
- onestep/broker/__init__.py +22 -0
- onestep/broker/base.py +144 -0
- onestep/broker/cron.py +51 -0
- onestep/broker/memory.py +75 -0
- onestep/broker/mysql.py +63 -0
- onestep/broker/rabbitmq.py +153 -0
- onestep/broker/redis/__init__.py +9 -0
- onestep/broker/redis/pubsub.py +93 -0
- onestep/broker/redis/stream.py +114 -0
- onestep/broker/sqs/__init__.py +4 -0
- onestep/broker/sqs/sns.py +53 -0
- onestep/broker/sqs/sqs.py +181 -0
- onestep/broker/webhook.py +84 -0
- onestep/cli.py +80 -0
- onestep/cron.py +211 -0
- onestep/exception.py +35 -0
- onestep/message.py +169 -0
- onestep/middleware/__init__.py +7 -0
- onestep/middleware/base.py +32 -0
- onestep/middleware/config.py +77 -0
- onestep/middleware/unique.py +48 -0
- onestep/onestep.py +281 -0
- onestep/retry.py +117 -0
- onestep/signal.py +11 -0
- onestep/state.py +23 -0
- onestep/worker.py +205 -0
- onestep-0.5.0.dist-info/METADATA +116 -0
- onestep-0.5.0.dist-info/RECORD +32 -0
- onestep-0.5.0.dist-info/WHEEL +4 -0
- onestep-0.5.0.dist-info/entry_points.txt +2 -0
onestep/__init__.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from .onestep import step
|
|
2
|
+
from .cron import Cron
|
|
3
|
+
from .retry import (
|
|
4
|
+
BaseRetry, BaseErrorCallback, NackErrorCallBack,
|
|
5
|
+
NeverRetry, AlwaysRetry, TimesRetry, RetryIfException, AdvancedRetry
|
|
6
|
+
)
|
|
7
|
+
from .broker import (
|
|
8
|
+
BaseBroker, BaseConsumer,
|
|
9
|
+
MemoryBroker, RabbitMQBroker, WebHookBroker, CronBroker, RedisStreamBroker, RedisPubSubBroker, SQSBroker
|
|
10
|
+
)
|
|
11
|
+
from .middleware import (
|
|
12
|
+
BaseMiddleware, BaseConfigMiddleware,
|
|
13
|
+
NacosPublishConfigMiddleware, NacosConsumeConfigMiddleware,
|
|
14
|
+
RedisPublishConfigMiddleware, RedisConsumeConfigMiddleware,
|
|
15
|
+
UniqueMiddleware, MemoryUniqueMiddleware,
|
|
16
|
+
)
|
|
17
|
+
from .exception import (
|
|
18
|
+
StopMiddleware, DropMessage,
|
|
19
|
+
RetryException, RetryInQueue, RetryInLocal
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
'step', 'Cron',
|
|
24
|
+
|
|
25
|
+
# broker
|
|
26
|
+
'BaseBroker',
|
|
27
|
+
'BaseConsumer',
|
|
28
|
+
'MemoryBroker',
|
|
29
|
+
'RabbitMQBroker',
|
|
30
|
+
'WebHookBroker',
|
|
31
|
+
'CronBroker',
|
|
32
|
+
'RedisStreamBroker',
|
|
33
|
+
'RedisPubSubBroker',
|
|
34
|
+
'SQSBroker',
|
|
35
|
+
|
|
36
|
+
# retry
|
|
37
|
+
'BaseRetry',
|
|
38
|
+
'NeverRetry',
|
|
39
|
+
'AlwaysRetry',
|
|
40
|
+
'TimesRetry',
|
|
41
|
+
'RetryIfException',
|
|
42
|
+
'AdvancedRetry',
|
|
43
|
+
# error callback
|
|
44
|
+
'BaseErrorCallback',
|
|
45
|
+
'NackErrorCallBack',
|
|
46
|
+
|
|
47
|
+
# middleware
|
|
48
|
+
'BaseMiddleware',
|
|
49
|
+
'BaseConfigMiddleware',
|
|
50
|
+
'NacosPublishConfigMiddleware',
|
|
51
|
+
'NacosConsumeConfigMiddleware',
|
|
52
|
+
'RedisPublishConfigMiddleware',
|
|
53
|
+
'RedisConsumeConfigMiddleware',
|
|
54
|
+
'UniqueMiddleware',
|
|
55
|
+
'MemoryUniqueMiddleware',
|
|
56
|
+
|
|
57
|
+
# exception
|
|
58
|
+
'StopMiddleware',
|
|
59
|
+
'DropMessage',
|
|
60
|
+
'RetryException',
|
|
61
|
+
'RetryInQueue',
|
|
62
|
+
'RetryInLocal',
|
|
63
|
+
|
|
64
|
+
'__version__'
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
__version__ = '0.4.4'
|
onestep/_utils.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def catch_error(return_val=None):
|
|
6
|
+
"""
|
|
7
|
+
捕获异常装饰器
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def decorator(func):
|
|
11
|
+
@functools.wraps(func)
|
|
12
|
+
def wrapper(*args, **kwargs):
|
|
13
|
+
try:
|
|
14
|
+
return func(*args, **kwargs)
|
|
15
|
+
except Exception as e:
|
|
16
|
+
logging.debug(e)
|
|
17
|
+
return return_val
|
|
18
|
+
|
|
19
|
+
return wrapper
|
|
20
|
+
|
|
21
|
+
return decorator
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .base import (
|
|
2
|
+
BaseBroker, BaseConsumer
|
|
3
|
+
)
|
|
4
|
+
from .memory import MemoryBroker, MemoryConsumer
|
|
5
|
+
from .webhook import WebHookBroker
|
|
6
|
+
from .rabbitmq import RabbitMQBroker, RabbitMQConsumer
|
|
7
|
+
from .mysql import MysqlBroker
|
|
8
|
+
from .sqs import SQSBroker, SQSConsumer, SNSBroker
|
|
9
|
+
from .redis import RedisStreamBroker, RedisPubSubBroker
|
|
10
|
+
from .cron import CronBroker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BaseBroker", "BaseConsumer",
|
|
15
|
+
"MemoryBroker", "MemoryConsumer",
|
|
16
|
+
"WebHookBroker",
|
|
17
|
+
"RabbitMQBroker", "RabbitMQConsumer",
|
|
18
|
+
"MysqlBroker",
|
|
19
|
+
"SQSBroker", "SQSConsumer", "SNSBroker",
|
|
20
|
+
"RedisStreamBroker", "RedisPubSubBroker",
|
|
21
|
+
"CronBroker"
|
|
22
|
+
]
|
onestep/broker/base.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import abc
|
|
3
|
+
import logging
|
|
4
|
+
from queue import Queue, Empty
|
|
5
|
+
from typing import Any, Optional, List, Callable
|
|
6
|
+
|
|
7
|
+
from onestep.middleware import BaseMiddleware
|
|
8
|
+
from onestep.exception import StopMiddleware
|
|
9
|
+
from onestep.message import Message
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseBroker:
|
|
15
|
+
message_cls = Message
|
|
16
|
+
|
|
17
|
+
def __init__(self,
|
|
18
|
+
name: Optional[str] = None,
|
|
19
|
+
queue: Optional[Queue] = None,
|
|
20
|
+
middlewares: Optional[List[BaseMiddleware]] = None,
|
|
21
|
+
once: bool = False,
|
|
22
|
+
cancel_consume: Optional[Callable] = None):
|
|
23
|
+
"""
|
|
24
|
+
@param name: broker name
|
|
25
|
+
@param queue: broker queue name
|
|
26
|
+
@param middlewares: broker middlewares
|
|
27
|
+
@param once: just run once
|
|
28
|
+
if once is True, when broker receive a message, it will shutdown
|
|
29
|
+
@param cancel_consume: cancel consume
|
|
30
|
+
cancel_consume(message) -> bool
|
|
31
|
+
if cancel_consume return True, broker will shutdown
|
|
32
|
+
"""
|
|
33
|
+
self.queue = queue
|
|
34
|
+
self.name = name or "broker"
|
|
35
|
+
self.middlewares = []
|
|
36
|
+
self.once = once
|
|
37
|
+
self.cancel_consume = cancel_consume
|
|
38
|
+
|
|
39
|
+
if middlewares:
|
|
40
|
+
for middleware in middlewares:
|
|
41
|
+
self.add_middleware(middleware)
|
|
42
|
+
|
|
43
|
+
def add_middleware(self, middleware: BaseMiddleware):
|
|
44
|
+
if not isinstance(middleware, BaseMiddleware):
|
|
45
|
+
raise TypeError(f"middleware must be BaseMiddleware instance, not {type(middleware)}")
|
|
46
|
+
self.middlewares.append(middleware)
|
|
47
|
+
|
|
48
|
+
def send(self, message, *args, **kwargs):
|
|
49
|
+
"""对消息进行预处理,然后再发送"""
|
|
50
|
+
if not isinstance(message, Message):
|
|
51
|
+
message = self.message_cls(body=message)
|
|
52
|
+
self.before_emit("send", message=message, step=kwargs.get("step"))
|
|
53
|
+
# TODO: 对消息发送进行N次重试,确保消息发送成功。
|
|
54
|
+
result = self.publish(message.to_json(), *args, **kwargs)
|
|
55
|
+
self.after_emit("send", message=message, step=kwargs.get("step"))
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def publish(self, message: Any, *args, **kwargs):
|
|
60
|
+
"""
|
|
61
|
+
将消息原样发布到 broker 中。如果当前Broker是Job的to_broker, 则必须实现此方法
|
|
62
|
+
"""
|
|
63
|
+
raise NotImplementedError('Please implement in subclasses.')
|
|
64
|
+
|
|
65
|
+
@abc.abstractmethod
|
|
66
|
+
def consume(self, *args, **kwargs):
|
|
67
|
+
"""
|
|
68
|
+
消费消息。如果当前Broker是Job的from_broker, 则必须实现此方法
|
|
69
|
+
"""
|
|
70
|
+
raise NotImplementedError('Please implement in subclasses.')
|
|
71
|
+
|
|
72
|
+
@abc.abstractmethod
|
|
73
|
+
def confirm(self, message: Message):
|
|
74
|
+
"""确认消息"""
|
|
75
|
+
raise NotImplementedError('Please implement in subclasses.')
|
|
76
|
+
|
|
77
|
+
@abc.abstractmethod
|
|
78
|
+
def reject(self, message: Message):
|
|
79
|
+
"""拒绝消息"""
|
|
80
|
+
raise NotImplementedError('Please implement in subclasses.')
|
|
81
|
+
|
|
82
|
+
@abc.abstractmethod
|
|
83
|
+
def requeue(self, message: Message, is_source=False):
|
|
84
|
+
"""
|
|
85
|
+
重发消息:先拒绝 再 重入
|
|
86
|
+
is_source = False 重入使用消息的当前状态
|
|
87
|
+
is_source = True 重入使用消息的初始状态
|
|
88
|
+
"""
|
|
89
|
+
raise NotImplementedError('Please implement in subclasses.')
|
|
90
|
+
|
|
91
|
+
def before_emit(self, signal, **kwargs):
|
|
92
|
+
signal = "before_" + signal
|
|
93
|
+
self._emit(signal, **kwargs)
|
|
94
|
+
|
|
95
|
+
def after_emit(self, signal, **kwargs):
|
|
96
|
+
signal = "after_" + signal
|
|
97
|
+
self._emit(signal, **kwargs)
|
|
98
|
+
|
|
99
|
+
def _emit(self, signal, **kwargs):
|
|
100
|
+
for middleware in self.middlewares:
|
|
101
|
+
if not hasattr(middleware, signal):
|
|
102
|
+
continue
|
|
103
|
+
try:
|
|
104
|
+
params = dict(kwargs)
|
|
105
|
+
params.setdefault("broker", self)
|
|
106
|
+
params.setdefault("step", None)
|
|
107
|
+
getattr(middleware, signal)(**params)
|
|
108
|
+
except StopMiddleware:
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
def shutdown(self):
|
|
112
|
+
"""关闭Broker"""
|
|
113
|
+
|
|
114
|
+
def __repr__(self):
|
|
115
|
+
return f"<{self.__class__.__name__} {self.name}>"
|
|
116
|
+
|
|
117
|
+
def __str__(self):
|
|
118
|
+
return self.name
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class BaseConsumer:
|
|
122
|
+
|
|
123
|
+
def __init__(self, broker: BaseBroker, *args, **kwargs):
|
|
124
|
+
self.queue = broker.queue
|
|
125
|
+
self.message_cls = broker.message_cls or Message
|
|
126
|
+
self.timeout = kwargs.pop("timeout", 1000)
|
|
127
|
+
|
|
128
|
+
def __next__(self):
|
|
129
|
+
try:
|
|
130
|
+
q = self.queue
|
|
131
|
+
if q is None:
|
|
132
|
+
return None
|
|
133
|
+
timeout_ms = self.timeout
|
|
134
|
+
if isinstance(timeout_ms, (int, float)) and timeout_ms > 0:
|
|
135
|
+
data = q.get(timeout=timeout_ms / 1000.0)
|
|
136
|
+
else:
|
|
137
|
+
# 当超时为0、负数或非数字时,使用非阻塞获取以避免ValueError
|
|
138
|
+
data = q.get_nowait()
|
|
139
|
+
return self.message_cls.from_broker(broker_message=data)
|
|
140
|
+
except (Empty, ValueError):
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def __iter__(self):
|
|
144
|
+
return self
|
onestep/broker/cron.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
使用 CRON 表达式触发任务执行
|
|
3
|
+
|
|
4
|
+
支持人性化 DSL 与宏:
|
|
5
|
+
- DSL:Cron.every(minutes=5)、Cron.daily(at="09:00")、Cron.weekly(on=["mon","fri"], at="10:30") 等
|
|
6
|
+
- 宏:@hourly/@daily/@weekly/@monthly/@yearly 以及扩展 @workdays/@weekends/@every 5m/2h/3d/1mo
|
|
7
|
+
"""
|
|
8
|
+
import logging
|
|
9
|
+
import threading
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from croniter import croniter
|
|
14
|
+
from ..cron import resolve_cron
|
|
15
|
+
|
|
16
|
+
from .memory import MemoryBroker, MemoryConsumer
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CronBroker(MemoryBroker):
|
|
22
|
+
|
|
23
|
+
def __init__(self, cron, name=None, middlewares=None, body: Any = None, start_time=None, *args, **kwargs):
|
|
24
|
+
super().__init__(name=name, middlewares=middlewares, *args, **kwargs)
|
|
25
|
+
# 支持 DSL/宏/原始字符串:统一解析为标准表达式
|
|
26
|
+
self.cron = resolve_cron(cron)
|
|
27
|
+
self.start_time = start_time or datetime.now()
|
|
28
|
+
self.itr = croniter(self.cron, self.start_time)
|
|
29
|
+
self.next_fire_time = self.itr.get_next(datetime)
|
|
30
|
+
self.body = body
|
|
31
|
+
self._thread = None
|
|
32
|
+
|
|
33
|
+
def _scheduler(self):
|
|
34
|
+
if self.next_fire_time <= datetime.now():
|
|
35
|
+
self.next_fire_time = self.itr.get_next(datetime)
|
|
36
|
+
self.publish(self.body)
|
|
37
|
+
|
|
38
|
+
self._thread = threading.Timer(interval=1, function=self._scheduler)
|
|
39
|
+
self._thread.start()
|
|
40
|
+
|
|
41
|
+
def consume(self, *args, **kwargs):
|
|
42
|
+
self._scheduler()
|
|
43
|
+
return CronConsumer(self, *args, **kwargs)
|
|
44
|
+
|
|
45
|
+
def shutdown(self):
|
|
46
|
+
if self._thread:
|
|
47
|
+
self._thread.cancel()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CronConsumer(MemoryConsumer):
|
|
51
|
+
...
|
onestep/broker/memory.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import json
|
|
3
|
+
from queue import Queue, Full as FullException
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .base import BaseBroker, BaseConsumer
|
|
7
|
+
|
|
8
|
+
from ..message import Message
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MemoryMessage(Message):
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_broker(cls, broker_message: Any):
|
|
17
|
+
if isinstance(broker_message, (str, bytes, bytearray)):
|
|
18
|
+
try:
|
|
19
|
+
message = json.loads(broker_message)
|
|
20
|
+
except json.JSONDecodeError:
|
|
21
|
+
message = {"body": broker_message}
|
|
22
|
+
else:
|
|
23
|
+
message = broker_message
|
|
24
|
+
if not isinstance(message, dict):
|
|
25
|
+
message = {"body": message}
|
|
26
|
+
if "body" not in message:
|
|
27
|
+
# 来自 外部的消息 可能没有 body, 故直接认为都是 message.body
|
|
28
|
+
message = {"body": message}
|
|
29
|
+
extra_val = message.get("extra")
|
|
30
|
+
if not isinstance(extra_val, dict):
|
|
31
|
+
extra_val = None
|
|
32
|
+
return cls(body=message.get("body"), extra=extra_val, message=broker_message)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MemoryBroker(BaseBroker):
|
|
36
|
+
message_cls = MemoryMessage
|
|
37
|
+
|
|
38
|
+
def __init__(self, maxsize=0, *args, **kwargs):
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
self.queue = Queue(maxsize)
|
|
41
|
+
|
|
42
|
+
def publish(self, message: Any, *args, **kwargs):
|
|
43
|
+
try:
|
|
44
|
+
self.queue.put_nowait(message)
|
|
45
|
+
except FullException:
|
|
46
|
+
logger.warning("CronBroker queue is full, skip this task, "
|
|
47
|
+
"you can increase maxsize with `maxsize` argument")
|
|
48
|
+
|
|
49
|
+
def consume(self, *args, **kwargs):
|
|
50
|
+
return MemoryConsumer(self, *args, **kwargs)
|
|
51
|
+
|
|
52
|
+
def confirm(self, message: Message):
|
|
53
|
+
"""确认消息"""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
def reject(self, message: Message):
|
|
57
|
+
"""拒绝消息"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def requeue(self, message: Message, is_source=False):
|
|
61
|
+
"""重发消息:先拒绝 再 重入"""
|
|
62
|
+
if is_source:
|
|
63
|
+
self.publish(message.message)
|
|
64
|
+
else:
|
|
65
|
+
self.send(message)
|
|
66
|
+
|
|
67
|
+
def __repr__(self):
|
|
68
|
+
return f"<{self.__class__.__name__} {self.name}>"
|
|
69
|
+
|
|
70
|
+
def __str__(self):
|
|
71
|
+
return self.name
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MemoryConsumer(BaseConsumer):
|
|
75
|
+
...
|
onestep/broker/mysql.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from queue import Queue
|
|
2
|
+
from typing import Optional, Dict, Any, cast
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from use_mysql import MysqlStore, Model, SQLModel
|
|
6
|
+
except ImportError:
|
|
7
|
+
MysqlStore = None
|
|
8
|
+
|
|
9
|
+
from .base import BaseBroker, BaseConsumer
|
|
10
|
+
from ..message import Message
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MysqlBroker(BaseBroker):
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
params: Optional[Dict] = None,
|
|
18
|
+
auto_create: Optional[bool] = True,
|
|
19
|
+
*args,
|
|
20
|
+
**kwargs
|
|
21
|
+
):
|
|
22
|
+
if MysqlStore is None or Model is None:
|
|
23
|
+
raise ImportError("未安装 use-mysql 依赖")
|
|
24
|
+
|
|
25
|
+
super().__init__(*args, **kwargs)
|
|
26
|
+
self.queue = Queue()
|
|
27
|
+
params = params or {}
|
|
28
|
+
auto_create = auto_create or False
|
|
29
|
+
self.client = MysqlStore(generate_schemas=auto_create, **params)
|
|
30
|
+
self.client.init()
|
|
31
|
+
self._shutdown = False
|
|
32
|
+
|
|
33
|
+
def send(self, message, *args, **kwargs):
|
|
34
|
+
if not isinstance(message, Message):
|
|
35
|
+
message = self.message_cls(body=message)
|
|
36
|
+
return self.publish(message, *args, **kwargs)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def publish(self, message: Any, **kwargs):
|
|
40
|
+
body_raw = message.body if isinstance(message, Message) else message
|
|
41
|
+
if not isinstance(body_raw, SQLModel):
|
|
42
|
+
raise TypeError("MySQL Broker 仅支持 Model 类型的消息体")
|
|
43
|
+
|
|
44
|
+
body = cast(SQLModel, body_raw)
|
|
45
|
+
return self.client.create(body.__class__, **body.model_dump())
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def confirm(self, message: Message):
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def reject(self, message: Message):
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def requeue(self, message: Message, is_source=False):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def shutdown(self):
|
|
58
|
+
self._shutdown = True
|
|
59
|
+
self.client.shutdown()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MysqlConsumer(BaseConsumer):
|
|
63
|
+
...
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
from queue import Queue
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import amqpstorm
|
|
8
|
+
from use_rabbitmq import useRabbitMQ as RabbitMQStore
|
|
9
|
+
except ImportError:
|
|
10
|
+
amqpstorm = None
|
|
11
|
+
RabbitMQStore = None
|
|
12
|
+
|
|
13
|
+
from .base import BaseBroker, BaseConsumer
|
|
14
|
+
from ..message import Message
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _RabbitMQMessage(Message):
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_broker(cls, broker_message):
|
|
21
|
+
try:
|
|
22
|
+
message = json.loads(broker_message.body)
|
|
23
|
+
except json.JSONDecodeError:
|
|
24
|
+
message = {"body": broker_message.body}
|
|
25
|
+
if not isinstance(message, dict):
|
|
26
|
+
message = {"body": message}
|
|
27
|
+
if "body" not in message:
|
|
28
|
+
# 来自 外部的消息 可能没有 body, 故直接认为都是 message.body
|
|
29
|
+
message = {"body": message}
|
|
30
|
+
|
|
31
|
+
return cls(body=message.get("body"), extra=message.get("extra"), message=broker_message)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RabbitMQBroker(BaseBroker):
|
|
35
|
+
message_cls = _RabbitMQMessage
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
queue_name,
|
|
40
|
+
params: Optional[Dict] = None,
|
|
41
|
+
prefetch: Optional[int] = 1,
|
|
42
|
+
auto_create: Optional[bool]=True,
|
|
43
|
+
queue_params: Optional[Dict]=None,
|
|
44
|
+
*args,
|
|
45
|
+
**kwargs
|
|
46
|
+
):
|
|
47
|
+
"""
|
|
48
|
+
Initializes the RabbitMQ broker.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
queue_name (str): The name of the queue.
|
|
52
|
+
params (Optional[Dict], optional): Parameters for RabbitMQStore. Defaults to None.
|
|
53
|
+
prefetch (Optional[int], optional): Number of messages to prefetch. Defaults to 1.
|
|
54
|
+
auto_create (Optional[bool], optional): Whether to automatically create the queue. Defaults to True.
|
|
55
|
+
queue_params (Optional[Dict], optional): Parameters for queue declaration. Defaults to None.
|
|
56
|
+
*args: Variable length argument list.
|
|
57
|
+
**kwargs: Arbitrary keyword arguments.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
queue_name (str): The name of the queue.
|
|
61
|
+
queue (Queue): The queue instance.
|
|
62
|
+
client (RabbitMQStore): The RabbitMQ client instance.
|
|
63
|
+
prefetch (int): Number of messages to prefetch.
|
|
64
|
+
threads (list): List of threads.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
if RabbitMQStore is None:
|
|
68
|
+
raise ImportError("RabbitMQ dependencies not installed. Please install 'use-rabbitmq' package.")
|
|
69
|
+
|
|
70
|
+
super().__init__(*args, **kwargs)
|
|
71
|
+
self.queue_name = queue_name
|
|
72
|
+
self.queue = Queue()
|
|
73
|
+
params = params or {}
|
|
74
|
+
self.client = RabbitMQStore(**params)
|
|
75
|
+
if auto_create:
|
|
76
|
+
self.client.declare_queue(self.queue_name, **(queue_params or {}))
|
|
77
|
+
self.prefetch = prefetch
|
|
78
|
+
self.threads = []
|
|
79
|
+
self._consuming_started = False
|
|
80
|
+
self._consume_lock = threading.Lock()
|
|
81
|
+
|
|
82
|
+
def _consume(self, *args, **kwargs):
|
|
83
|
+
def callback(message):
|
|
84
|
+
self.queue.put(message)
|
|
85
|
+
|
|
86
|
+
prefetch = kwargs.pop("prefetch", self.prefetch)
|
|
87
|
+
self.client.start_consuming(queue_name=self.queue_name, callback=callback, prefetch=prefetch, **kwargs)
|
|
88
|
+
|
|
89
|
+
def consume(self, *args, **kwargs):
|
|
90
|
+
daemon = kwargs.pop('daemon', True)
|
|
91
|
+
with self._consume_lock:
|
|
92
|
+
if not self._consuming_started:
|
|
93
|
+
thread = threading.Thread(target=self._consume, args=args, kwargs=kwargs)
|
|
94
|
+
thread.daemon = daemon
|
|
95
|
+
thread.start()
|
|
96
|
+
self.threads.append(thread)
|
|
97
|
+
self._consuming_started = True
|
|
98
|
+
return RabbitMQConsumer(self)
|
|
99
|
+
|
|
100
|
+
def publish(self, message: Any, properties: Optional[dict] = None, **kwargs):
|
|
101
|
+
"""发布消息"""
|
|
102
|
+
self.client.send(self.queue_name, message, properties=properties, **kwargs)
|
|
103
|
+
|
|
104
|
+
def confirm(self, message: Message):
|
|
105
|
+
"""确认消息"""
|
|
106
|
+
broker_msg = getattr(message, "message", None)
|
|
107
|
+
if broker_msg is not None and hasattr(broker_msg, "ack"):
|
|
108
|
+
broker_msg.ack()
|
|
109
|
+
else:
|
|
110
|
+
# 无可确认的原始消息,忽略确认
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
def reject(self, message: Message):
|
|
114
|
+
"""拒绝消息"""
|
|
115
|
+
broker_msg = getattr(message, "message", None)
|
|
116
|
+
if broker_msg is not None and hasattr(broker_msg, "reject"):
|
|
117
|
+
broker_msg.reject(requeue=False)
|
|
118
|
+
else:
|
|
119
|
+
# 无法拒绝(无原始消息对象),忽略
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def requeue(self, message: Message, is_source=False):
|
|
123
|
+
"""
|
|
124
|
+
重发消息:先拒绝 再 重入
|
|
125
|
+
|
|
126
|
+
:param message: 消息
|
|
127
|
+
:param is_source: 是否是原始消息,True: 使用原始消息重入当前队列,False: 使用消息的最新数据重入当前队列
|
|
128
|
+
"""
|
|
129
|
+
broker_msg = getattr(message, "message", None)
|
|
130
|
+
if broker_msg is not None and hasattr(broker_msg, "reject"):
|
|
131
|
+
if is_source:
|
|
132
|
+
broker_msg.reject(requeue=True)
|
|
133
|
+
else:
|
|
134
|
+
broker_msg.reject(requeue=False)
|
|
135
|
+
self.send(message)
|
|
136
|
+
else:
|
|
137
|
+
# 没有原始消息控制能力,回退为直接发送当前消息状态
|
|
138
|
+
if is_source and broker_msg is not None and hasattr(broker_msg, "body"):
|
|
139
|
+
# 尝试使用原始消息体重入队列
|
|
140
|
+
self.client.send(self.queue_name, broker_msg.body)
|
|
141
|
+
else:
|
|
142
|
+
self.send(message)
|
|
143
|
+
|
|
144
|
+
def shutdown(self):
|
|
145
|
+
self.client.shutdown()
|
|
146
|
+
for thread in self.threads:
|
|
147
|
+
thread.join()
|
|
148
|
+
self._consuming_started = False
|
|
149
|
+
self.threads = []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class RabbitMQConsumer(BaseConsumer):
|
|
153
|
+
...
|