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/onestep.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import collections
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from inspect import isgenerator, iscoroutinefunction, isasyncgenfunction, isasyncgen
|
|
7
|
+
from itertools import groupby
|
|
8
|
+
from typing import Optional, List, Dict, Any, Callable, Union, Type
|
|
9
|
+
|
|
10
|
+
from .broker.base import BaseBroker
|
|
11
|
+
from .middleware import BaseMiddleware
|
|
12
|
+
from .exception import StopMiddleware
|
|
13
|
+
from .message import Message
|
|
14
|
+
from .retry import TimesRetry
|
|
15
|
+
from .signal import message_sent, started, stopped
|
|
16
|
+
from .state import State
|
|
17
|
+
from .worker import ThreadWorker, BaseWorker
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
MAX_WORKERS = 20
|
|
22
|
+
DEFAULT_WORKERS = 1
|
|
23
|
+
DEFAULT_WORKER_CLASS = ThreadWorker
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BaseOneStep:
|
|
27
|
+
consumers: Dict[str, List[BaseWorker]] = collections.defaultdict(list)
|
|
28
|
+
state = State() # 全局状态
|
|
29
|
+
|
|
30
|
+
def __init__(self, fn,
|
|
31
|
+
group: str = "OneStep",
|
|
32
|
+
name: Optional[str] = None,
|
|
33
|
+
from_broker: Union[BaseBroker, List[BaseBroker], None] = None,
|
|
34
|
+
to_broker: Union[BaseBroker, List[BaseBroker], None] = None,
|
|
35
|
+
workers: Optional[int] = None,
|
|
36
|
+
worker_class: Optional[Type[BaseWorker]] = None,
|
|
37
|
+
middlewares: Optional[List[Any]] = None,
|
|
38
|
+
retry: Union[Callable, object] = TimesRetry(),
|
|
39
|
+
error_callback: Optional[Union[Callable, object]] = None):
|
|
40
|
+
self.group = group
|
|
41
|
+
self.fn = fn
|
|
42
|
+
self.name = name or fn.__name__
|
|
43
|
+
self.workers = workers or DEFAULT_WORKERS
|
|
44
|
+
self.worker_class = worker_class or DEFAULT_WORKER_CLASS
|
|
45
|
+
if self.workers > MAX_WORKERS:
|
|
46
|
+
logger.warning(f"workers[{self.workers}] greater than {MAX_WORKERS}")
|
|
47
|
+
self.workers = MAX_WORKERS
|
|
48
|
+
self.middlewares = middlewares or []
|
|
49
|
+
if self.middlewares:
|
|
50
|
+
for m in self.middlewares:
|
|
51
|
+
if not isinstance(m, BaseMiddleware):
|
|
52
|
+
raise TypeError(f"middleware must be BaseMiddleware instance, not {type(m)}")
|
|
53
|
+
|
|
54
|
+
self.from_brokers = self._init_broker(from_broker)
|
|
55
|
+
self.to_brokers = self._init_broker(to_broker)
|
|
56
|
+
self.retry = retry
|
|
57
|
+
self.error_callback = error_callback
|
|
58
|
+
|
|
59
|
+
for broker in self.from_brokers:
|
|
60
|
+
self._add_consumer(broker)
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _init_broker(broker: Union[BaseBroker, List[BaseBroker], None] = None):
|
|
64
|
+
if not broker:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
if isinstance(broker, BaseBroker):
|
|
68
|
+
return [broker]
|
|
69
|
+
if isinstance(broker, (list, tuple)):
|
|
70
|
+
return list(broker)
|
|
71
|
+
raise TypeError(
|
|
72
|
+
f"broker must be BaseBroker or list or tuple, not {type(broker)}")
|
|
73
|
+
|
|
74
|
+
def _add_consumer(self, broker):
|
|
75
|
+
""" 添加来源消费者 """
|
|
76
|
+
worker_class_params = inspect.signature(self.worker_class.__init__).parameters
|
|
77
|
+
if "workers" in worker_class_params:
|
|
78
|
+
self.consumers[self.group].append(
|
|
79
|
+
self.worker_class(onestep=self, broker=broker, workers=self.workers)
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
for _ in range(self.workers):
|
|
83
|
+
self.consumers[self.group].append(
|
|
84
|
+
self.worker_class(onestep=self, broker=broker)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def _find_consumers(cls, group: Optional[str] = None):
|
|
89
|
+
"""按组查找消费者"""
|
|
90
|
+
if group is None:
|
|
91
|
+
consumers = [c for v in cls.consumers.values() for c in v]
|
|
92
|
+
else:
|
|
93
|
+
consumers = cls.consumers[group]
|
|
94
|
+
return consumers
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def print_jobs(cls, group):
|
|
98
|
+
print("Jobs:")
|
|
99
|
+
prints = []
|
|
100
|
+
_consumers = cls._find_consumers(group)
|
|
101
|
+
group_instance = groupby(_consumers, key=lambda x: x.instance)
|
|
102
|
+
for instance, _ in group_instance:
|
|
103
|
+
prints.append([instance.name, instance.group, instance.workers, str(instance.from_brokers)])
|
|
104
|
+
print("{:<15} {:<10} {:<10} {:<20}".format("Job", "Group", "Workers", "From Brokers"))
|
|
105
|
+
for v in prints:
|
|
106
|
+
print("{:<15} {:<10} {:<10} {:<20}".format(*v))
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def start(cls, group: Optional[str] = None, print_jobs: bool = False):
|
|
110
|
+
logger.debug(f"start group [{group or 'all'}]")
|
|
111
|
+
_consumers = cls._find_consumers(group)
|
|
112
|
+
if not _consumers:
|
|
113
|
+
logger.debug(f"no consumer found in group [{group or 'all'}]")
|
|
114
|
+
return
|
|
115
|
+
if print_jobs:
|
|
116
|
+
cls.print_jobs(group)
|
|
117
|
+
for consumer in _consumers:
|
|
118
|
+
consumer.start()
|
|
119
|
+
logger.debug(f"started: {consumer=}")
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def shutdown(cls, group: Optional[str] = None):
|
|
123
|
+
logger.debug(f"stop group [{group or 'all'}]")
|
|
124
|
+
for consumer in cls._find_consumers(group):
|
|
125
|
+
consumer.shutdown()
|
|
126
|
+
logger.debug(f"stopped: {consumer=}")
|
|
127
|
+
|
|
128
|
+
def wraps(self, func):
|
|
129
|
+
@functools.wraps(func)
|
|
130
|
+
def wrapped_f(*args, **kwargs):
|
|
131
|
+
return self(*args, **kwargs) # noqa
|
|
132
|
+
|
|
133
|
+
return wrapped_f
|
|
134
|
+
|
|
135
|
+
def send(self, result, broker=None):
|
|
136
|
+
"""将返回的内容交给broker发送"""
|
|
137
|
+
brokers = self._init_broker(broker) or self.to_brokers
|
|
138
|
+
# 如果是Message类型,就不再封装
|
|
139
|
+
message = result if isinstance(result, Message) else Message(body=result)
|
|
140
|
+
self.before_emit("send", message=message)
|
|
141
|
+
|
|
142
|
+
if result and not brokers:
|
|
143
|
+
logger.debug("send(result): broker is empty")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if not result and brokers:
|
|
147
|
+
logger.debug("send(result): body is empty")
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
for broker in brokers:
|
|
151
|
+
message_sent.send(self, message=message, broker=broker)
|
|
152
|
+
broker.send(message, step=self)
|
|
153
|
+
self.after_emit("send", message=message)
|
|
154
|
+
|
|
155
|
+
def before_emit(self, signal, *args, **kwargs):
|
|
156
|
+
signal = "before_" + signal
|
|
157
|
+
self.emit(signal, *args, **kwargs)
|
|
158
|
+
|
|
159
|
+
def after_emit(self, signal, *args, **kwargs):
|
|
160
|
+
signal = "after_" + signal
|
|
161
|
+
self.emit(signal, *args, **kwargs)
|
|
162
|
+
|
|
163
|
+
def emit(self, signal, *args, **kwargs):
|
|
164
|
+
for middleware in self.middlewares:
|
|
165
|
+
if not hasattr(middleware, signal):
|
|
166
|
+
continue
|
|
167
|
+
try:
|
|
168
|
+
getattr(middleware, signal)(step=self, *args, **kwargs)
|
|
169
|
+
except StopMiddleware as e:
|
|
170
|
+
logger.debug(f"middleware<{middleware}> is stopped,reason: {e}")
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def is_shutdown(cls, group):
|
|
175
|
+
# check all broker
|
|
176
|
+
_consumers = cls._find_consumers(group)
|
|
177
|
+
if not _consumers:
|
|
178
|
+
return True
|
|
179
|
+
return all(broker._shutdown for broker in _consumers)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def decorator_func_proxy(func):
|
|
183
|
+
@functools.wraps(func)
|
|
184
|
+
def wrapper(*args, **kwargs):
|
|
185
|
+
return func(*args, **kwargs)
|
|
186
|
+
|
|
187
|
+
return wrapper
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class SyncOneStep(BaseOneStep):
|
|
191
|
+
|
|
192
|
+
def __call__(self, *args, **kwargs):
|
|
193
|
+
"""同步执行原函数"""
|
|
194
|
+
|
|
195
|
+
result = self.fn(*args, **kwargs)
|
|
196
|
+
|
|
197
|
+
if isgenerator(result):
|
|
198
|
+
for item in result:
|
|
199
|
+
self.send(item)
|
|
200
|
+
else:
|
|
201
|
+
self.send(result)
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class AsyncOneStep(BaseOneStep):
|
|
206
|
+
|
|
207
|
+
async def __call__(self, *args, **kwargs):
|
|
208
|
+
""""异步执行原函数"""
|
|
209
|
+
|
|
210
|
+
if iscoroutinefunction(self.fn):
|
|
211
|
+
result = await self.fn(*args, **kwargs)
|
|
212
|
+
else:
|
|
213
|
+
result = self.fn(*args, **kwargs)
|
|
214
|
+
|
|
215
|
+
if isasyncgen(result):
|
|
216
|
+
async for item in result:
|
|
217
|
+
self.send(item)
|
|
218
|
+
else:
|
|
219
|
+
self.send(result)
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class step:
|
|
224
|
+
|
|
225
|
+
def __init__(self,
|
|
226
|
+
*,
|
|
227
|
+
group: str = "OneStep",
|
|
228
|
+
name: Optional[str] = None,
|
|
229
|
+
from_broker: Union[BaseBroker, List[BaseBroker], None] = None,
|
|
230
|
+
to_broker: Union[BaseBroker, List[BaseBroker], None] = None,
|
|
231
|
+
workers: Optional[int] = None,
|
|
232
|
+
worker_class: Optional[Type[BaseWorker]] = None,
|
|
233
|
+
middlewares: Optional[List[Any]] = None,
|
|
234
|
+
retry: Union[Callable, object] = TimesRetry(),
|
|
235
|
+
error_callback: Optional[Union[Callable, object]] = None):
|
|
236
|
+
self.params = {
|
|
237
|
+
"group": group,
|
|
238
|
+
"name": name,
|
|
239
|
+
"from_broker": from_broker,
|
|
240
|
+
"to_broker": to_broker,
|
|
241
|
+
"workers": workers,
|
|
242
|
+
"worker_class": worker_class,
|
|
243
|
+
"middlewares": middlewares,
|
|
244
|
+
"retry": retry,
|
|
245
|
+
"error_callback": error_callback
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
def __call__(self, func, *_args, **_kwargs):
|
|
249
|
+
func.__step_params__ = self.params
|
|
250
|
+
if iscoroutinefunction(func) or isasyncgenfunction(func):
|
|
251
|
+
os = AsyncOneStep(fn=func, **self.params)
|
|
252
|
+
else:
|
|
253
|
+
os = SyncOneStep(fn=func, **self.params)
|
|
254
|
+
|
|
255
|
+
return os.wraps(func)
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def start(group=None, block=None, print_jobs=False):
|
|
259
|
+
BaseOneStep.start(group=group, print_jobs=print_jobs)
|
|
260
|
+
started.send()
|
|
261
|
+
if block:
|
|
262
|
+
import time
|
|
263
|
+
while not BaseOneStep.is_shutdown(group):
|
|
264
|
+
time.sleep(1)
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def shutdown(group=None):
|
|
268
|
+
BaseOneStep.shutdown(group=group)
|
|
269
|
+
stopped.send()
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def set_debugging():
|
|
273
|
+
|
|
274
|
+
if not BaseOneStep.state.debug:
|
|
275
|
+
onestep_logger = logging.getLogger("onestep")
|
|
276
|
+
handler = logging.StreamHandler()
|
|
277
|
+
handler.setFormatter(logging.Formatter(
|
|
278
|
+
"[%(levelname)s] %(asctime)s %(name)s:%(message)s"))
|
|
279
|
+
onestep_logger.addHandler(handler)
|
|
280
|
+
onestep_logger.setLevel(logging.DEBUG)
|
|
281
|
+
BaseOneStep.state.debug = True
|
onestep/retry.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional, Tuple, Union, Type
|
|
4
|
+
|
|
5
|
+
from .exception import RetryInLocal, RetryInQueue
|
|
6
|
+
from .message import Message
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RetryStatus(Enum):
|
|
10
|
+
CONTINUE = 1 # 继续(执行重试)
|
|
11
|
+
END_WITH_CALLBACK = 2 # 结束(执行回调)
|
|
12
|
+
END_IGNORE_CALLBACK = 3 # 结束(忽略回调)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BaseRetry(ABC):
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def __call__(self, *args, **kwargs) -> Optional[RetryStatus]:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NeverRetry(BaseRetry):
|
|
23
|
+
|
|
24
|
+
def __call__(self, message) -> Optional[RetryStatus]:
|
|
25
|
+
return RetryStatus.END_WITH_CALLBACK
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AlwaysRetry(BaseRetry):
|
|
29
|
+
|
|
30
|
+
def __call__(self, message) -> Optional[RetryStatus]:
|
|
31
|
+
return RetryStatus.CONTINUE
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AllRetry(BaseRetry):
|
|
35
|
+
def __init__(self, *retries):
|
|
36
|
+
self.retries = retries
|
|
37
|
+
|
|
38
|
+
def __call__(self, message) -> Optional[RetryStatus]:
|
|
39
|
+
for retry in self.retries:
|
|
40
|
+
status = retry(message)
|
|
41
|
+
if status != RetryStatus.CONTINUE:
|
|
42
|
+
return status
|
|
43
|
+
return RetryStatus.CONTINUE
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AnyRetry(BaseRetry):
|
|
47
|
+
def __init__(self, *retries):
|
|
48
|
+
self.retries = retries
|
|
49
|
+
|
|
50
|
+
def __call__(self, message) -> Optional[RetryStatus]:
|
|
51
|
+
for retry in self.retries:
|
|
52
|
+
status = retry(message)
|
|
53
|
+
if status == RetryStatus.CONTINUE:
|
|
54
|
+
return RetryStatus.CONTINUE
|
|
55
|
+
return RetryStatus.END_WITH_CALLBACK
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TimesRetry(BaseRetry):
|
|
59
|
+
|
|
60
|
+
def __init__(self, times: int = 3):
|
|
61
|
+
self.times = times
|
|
62
|
+
|
|
63
|
+
def __call__(self, message) -> Optional[RetryStatus]:
|
|
64
|
+
if message.failure_count < self.times:
|
|
65
|
+
return RetryStatus.CONTINUE
|
|
66
|
+
return RetryStatus.END_WITH_CALLBACK
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RetryIfException(BaseRetry):
|
|
70
|
+
|
|
71
|
+
def __init__(self, exceptions: Optional[Tuple[Union[Exception, Type]]] = Exception):
|
|
72
|
+
self.exceptions = exceptions
|
|
73
|
+
|
|
74
|
+
def __call__(self, message) -> Optional[RetryStatus]:
|
|
75
|
+
if isinstance(message.exception.exc_value, self.exceptions):
|
|
76
|
+
return RetryStatus.CONTINUE
|
|
77
|
+
return RetryStatus.END_WITH_CALLBACK
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AdvancedRetry(TimesRetry):
|
|
81
|
+
"""高级重试策略
|
|
82
|
+
|
|
83
|
+
1. 本地重试:如果异常是 RetryInLocal 或 指定异常,且重试次数未达到上限,则本地重试,不回调
|
|
84
|
+
2. 队列重试:如果异常是 RetryInQueue,且重试次数未达到上限,则入队重试,不回调
|
|
85
|
+
3. 其他异常:如果异常不是 RetryInLocal 或 RetryInQueue 或 指定异常,则不重试,回调
|
|
86
|
+
注:待重试的异常若继承自 RetryException,则可单独指定重试次数,否则默认为 3 次
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, times: int = 3, exceptions: Optional[Tuple[Union[Exception, Type]]] = None):
|
|
90
|
+
super().__init__(times=times)
|
|
91
|
+
self.exceptions = (RetryInLocal, RetryInQueue) + (exceptions or ())
|
|
92
|
+
|
|
93
|
+
def __call__(self, message: Message) -> Optional[RetryStatus]:
|
|
94
|
+
if isinstance(message.exception.exc_value, self.exceptions):
|
|
95
|
+
max_retry_times = getattr(message.exception.exc_value, "times", None) or self.times
|
|
96
|
+
if message.failure_count < max_retry_times:
|
|
97
|
+
if isinstance(message.exception.exc_value, RetryInQueue):
|
|
98
|
+
return RetryStatus.END_IGNORE_CALLBACK
|
|
99
|
+
return RetryStatus.CONTINUE
|
|
100
|
+
else:
|
|
101
|
+
return RetryStatus.END_WITH_CALLBACK
|
|
102
|
+
else:
|
|
103
|
+
return RetryStatus.END_WITH_CALLBACK
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# error callback
|
|
107
|
+
class BaseErrorCallback(ABC):
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def __call__(self, *args, **kwargs):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class NackErrorCallBack(BaseErrorCallback):
|
|
115
|
+
|
|
116
|
+
def __call__(self, message):
|
|
117
|
+
message.reject()
|
onestep/signal.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from blinker import signal
|
|
2
|
+
|
|
3
|
+
started = signal("started")
|
|
4
|
+
stopped = signal("stopped")
|
|
5
|
+
|
|
6
|
+
message_received = signal("message_received")
|
|
7
|
+
message_sent = signal("message_sent")
|
|
8
|
+
message_consumed = signal("message_consumed")
|
|
9
|
+
message_error = signal("message_error")
|
|
10
|
+
message_drop = signal("message_drop")
|
|
11
|
+
message_requeue = signal("message_requeue")
|
onestep/state.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class State:
|
|
6
|
+
_state: typing.Dict[str, typing.Any]
|
|
7
|
+
|
|
8
|
+
def __init__(self, state: typing.Optional[typing.Dict[str, typing.Any]] = None):
|
|
9
|
+
if state is None:
|
|
10
|
+
state = {}
|
|
11
|
+
super().__setattr__("_state", state)
|
|
12
|
+
|
|
13
|
+
def __setattr__(self, key: typing.Any, value: typing.Any) -> None:
|
|
14
|
+
self._state[key] = value
|
|
15
|
+
|
|
16
|
+
def __getattr__(self, key: typing.Any) -> typing.Any:
|
|
17
|
+
try:
|
|
18
|
+
return self._state[key]
|
|
19
|
+
except KeyError:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def __delattr__(self, key: typing.Any) -> None:
|
|
23
|
+
del self._state[key]
|
onestep/worker.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
将指定的函数放入线程中运行
|
|
3
|
+
"""
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
|
+
from typing import Dict, Iterable
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import threading
|
|
9
|
+
from asyncio import iscoroutinefunction
|
|
10
|
+
from inspect import isasyncgenfunction
|
|
11
|
+
|
|
12
|
+
from asgiref.sync import async_to_sync
|
|
13
|
+
|
|
14
|
+
from .message import Message
|
|
15
|
+
from .retry import RetryStatus
|
|
16
|
+
from .broker import BaseBroker
|
|
17
|
+
from . import exception
|
|
18
|
+
from .signal import message_received, message_consumed, message_error, message_drop, message_requeue
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseWorker:
|
|
24
|
+
broker_exit: Dict[BaseBroker, bool] = {}
|
|
25
|
+
broker_exit_lock = threading.Lock()
|
|
26
|
+
|
|
27
|
+
def __init__(self, onestep, broker: BaseBroker, *args, **kwargs):
|
|
28
|
+
self.instance = onestep
|
|
29
|
+
self.retry = self.instance.retry
|
|
30
|
+
self.error_callback = self.instance.error_callback
|
|
31
|
+
self.broker = broker
|
|
32
|
+
self.args = args
|
|
33
|
+
self.kwargs = kwargs
|
|
34
|
+
self._shutdown = False
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def instance_name(self):
|
|
38
|
+
return self.instance.fn.__name__
|
|
39
|
+
|
|
40
|
+
def start(self):
|
|
41
|
+
"""启动 Worker"""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
def run(self):
|
|
45
|
+
"""执行 Worker 的逻辑"""
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
def shutdown(self):
|
|
49
|
+
"""关闭 Worker"""
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
|
|
52
|
+
def receive_messages(self) -> Iterable[Message]:
|
|
53
|
+
""" 从broker中获取消息 """
|
|
54
|
+
for result in self.broker.consume():
|
|
55
|
+
if self._shutdown:
|
|
56
|
+
break
|
|
57
|
+
if result is None:
|
|
58
|
+
continue
|
|
59
|
+
messages = result if isinstance(result, Iterable) else [result]
|
|
60
|
+
yield from messages
|
|
61
|
+
# when broker is once, it will shut down after receive a message
|
|
62
|
+
if self.broker.once:
|
|
63
|
+
self.shutdown()
|
|
64
|
+
|
|
65
|
+
def _run_real_instance(self, message: Message) -> None:
|
|
66
|
+
""" 执行实例的逻辑 """
|
|
67
|
+
if iscoroutinefunction(self.instance.fn) or isasyncgenfunction(self.instance.fn):
|
|
68
|
+
async_to_sync(self.instance)(message, *self.args, **self.kwargs)
|
|
69
|
+
else:
|
|
70
|
+
self.instance(message, *self.args, **self.kwargs)
|
|
71
|
+
|
|
72
|
+
def handle_message(self, message: Message):
|
|
73
|
+
""" 处理消息 """
|
|
74
|
+
message.broker = message.broker or self.broker
|
|
75
|
+
logger.debug(f"{self.instance.name} receive message<{message}> from {self.broker!r}")
|
|
76
|
+
message_received.send(self, message=message)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
self.broker.before_emit("consume", message=message, step=self.instance)
|
|
80
|
+
self.instance.before_emit("consume", message=message)
|
|
81
|
+
self._run_real_instance(message)
|
|
82
|
+
self.handle_success(message)
|
|
83
|
+
self.instance.after_emit("consume", message=message)
|
|
84
|
+
self.broker.after_emit("consume", message=message, step=self.instance)
|
|
85
|
+
except (exception.DropMessage, exception.RejectMessage) as e:
|
|
86
|
+
self.handle_drop(message, e)
|
|
87
|
+
except exception.RequeueMessage as e:
|
|
88
|
+
self.handle_requeue(message, e)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
message_error.send(self, message=message, error=e)
|
|
91
|
+
self.handle_error(message, e)
|
|
92
|
+
self.handle_retry(message)
|
|
93
|
+
finally:
|
|
94
|
+
self.handle_cancel_consume(message)
|
|
95
|
+
|
|
96
|
+
def handle_success(self, message):
|
|
97
|
+
message_consumed.send(self, message=message)
|
|
98
|
+
message.confirm(step=self.instance)
|
|
99
|
+
|
|
100
|
+
def handle_drop(self, message, reason):
|
|
101
|
+
message_drop.send(self, message=message, reason=reason)
|
|
102
|
+
logger.warning(f"{self.instance.name} dropped <{type(reason).__name__}: {str(reason)}>")
|
|
103
|
+
message.reject(step=self.instance)
|
|
104
|
+
|
|
105
|
+
def handle_requeue(self, message, reason):
|
|
106
|
+
message_requeue.send(self, message=message, reason=reason)
|
|
107
|
+
logger.warning(f"{self.instance.name} requeue <{type(reason).__name__}: {str(reason)}>")
|
|
108
|
+
message.requeue(is_source=True, step=self.instance)
|
|
109
|
+
|
|
110
|
+
def handle_error(self, message, error):
|
|
111
|
+
if self.instance.state.debug:
|
|
112
|
+
logger.exception(f"{self.instance.name} run error <{type(error).__name__}: {str(error)}>")
|
|
113
|
+
else:
|
|
114
|
+
logger.error(f"{self.instance.name} run error <{type(error).__name__}: {str(error)}>")
|
|
115
|
+
message.set_exception()
|
|
116
|
+
|
|
117
|
+
def handle_cancel_consume(self, message):
|
|
118
|
+
if self.broker.cancel_consume and self.broker.cancel_consume(message):
|
|
119
|
+
self.shutdown()
|
|
120
|
+
|
|
121
|
+
def handle_retry(self, message):
|
|
122
|
+
retry_status = self.retry(message)
|
|
123
|
+
if retry_status is RetryStatus.END_WITH_CALLBACK:
|
|
124
|
+
if self.error_callback:
|
|
125
|
+
self.error_callback(message)
|
|
126
|
+
message.reject()
|
|
127
|
+
elif retry_status is RetryStatus.END_IGNORE_CALLBACK:
|
|
128
|
+
message.requeue()
|
|
129
|
+
elif retry_status is RetryStatus.CONTINUE:
|
|
130
|
+
message.requeue()
|
|
131
|
+
|
|
132
|
+
def __repr__(self):
|
|
133
|
+
return f"<{self.__class__.__name__} {self.instance.name}>"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class ThreadWorker(BaseWorker):
|
|
137
|
+
|
|
138
|
+
def __init__(self, onestep, broker: BaseBroker, *args, **kwargs):
|
|
139
|
+
"""
|
|
140
|
+
线程执行包装过的`onestep`函数
|
|
141
|
+
:param onestep: OneStep实例
|
|
142
|
+
:param broker: 监听的from broker
|
|
143
|
+
"""
|
|
144
|
+
super().__init__(onestep, broker, *args, **kwargs)
|
|
145
|
+
self.thread = None
|
|
146
|
+
|
|
147
|
+
def start(self):
|
|
148
|
+
"""启动单线程 Worker"""
|
|
149
|
+
self.thread = threading.Thread(target=self.run, daemon=True)
|
|
150
|
+
self.thread.start()
|
|
151
|
+
|
|
152
|
+
def run(self):
|
|
153
|
+
"""线程执行包装过的`onestep`函数
|
|
154
|
+
|
|
155
|
+
`fn`为`onestep`函数,执行会调用`onestep`的`__call__`方法
|
|
156
|
+
:return:
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
while not self._shutdown:
|
|
160
|
+
with ThreadWorker.broker_exit_lock:
|
|
161
|
+
if ThreadWorker.broker_exit.get(self.broker, False):
|
|
162
|
+
self.shutdown()
|
|
163
|
+
break
|
|
164
|
+
for message in self.receive_messages():
|
|
165
|
+
self.handle_message(message)
|
|
166
|
+
|
|
167
|
+
def shutdown(self):
|
|
168
|
+
ThreadWorker.broker_exit[self.broker] = True
|
|
169
|
+
self.broker.shutdown()
|
|
170
|
+
self._shutdown = True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ThreadPoolWorker(BaseWorker):
|
|
174
|
+
broker_exit: Dict[BaseBroker, bool] = {}
|
|
175
|
+
broker_exit_lock = threading.Lock()
|
|
176
|
+
|
|
177
|
+
def __init__(self, onestep, broker: BaseBroker, workers=None, *args, **kwargs):
|
|
178
|
+
super().__init__(onestep, broker, *args, **kwargs)
|
|
179
|
+
self.executor = ThreadPoolExecutor(max_workers=workers)
|
|
180
|
+
|
|
181
|
+
def start(self):
|
|
182
|
+
"""启动线程池 Worker"""
|
|
183
|
+
self.executor.submit(self.run)
|
|
184
|
+
|
|
185
|
+
def run(self):
|
|
186
|
+
"""线程执行包装过的`onestep`函数
|
|
187
|
+
|
|
188
|
+
`fn`为`onestep`函数,执行会调用`onestep`的`__call__`方法
|
|
189
|
+
:return:
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
while not self._shutdown:
|
|
193
|
+
with ThreadPoolWorker.broker_exit_lock:
|
|
194
|
+
if ThreadPoolWorker.broker_exit.get(self.broker, False):
|
|
195
|
+
self.shutdown()
|
|
196
|
+
break
|
|
197
|
+
for message in self.receive_messages():
|
|
198
|
+
# 将消息处理提交到线程池中并发执行
|
|
199
|
+
self.executor.submit(self.handle_message, message)
|
|
200
|
+
|
|
201
|
+
def shutdown(self):
|
|
202
|
+
"""关闭线程池 Worker"""
|
|
203
|
+
ThreadPoolWorker.broker_exit[self.broker] = True
|
|
204
|
+
self._shutdown = True
|
|
205
|
+
self.executor.shutdown()
|