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/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()