DefStream 0.1.22__93-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.
- DefStream/Broker/Configuration.py +17 -0
- DefStream/Broker/Connection.py +141 -0
- DefStream/Broker/Consumer.py +59 -0
- DefStream/Broker/Errors.py +14 -0
- DefStream/Broker/Event.py +35 -0
- DefStream/Broker/Implements/RabbitMQ/Configuration.py +49 -0
- DefStream/Broker/Implements/RabbitMQ/Connection.py +550 -0
- DefStream/Broker/Implements/RabbitMQ/Consumer.py +416 -0
- DefStream/Broker/Implements/RabbitMQ/Event.py +146 -0
- DefStream/Broker/Implements/RabbitMQ/Exchange.py +277 -0
- DefStream/Broker/Implements/RabbitMQ/Queue.py +321 -0
- DefStream/Broker/Implements/RabbitMQ/Serializer.py +75 -0
- DefStream/Broker/Implements/RabbitMQ/Stream.py +181 -0
- DefStream/Broker/Implements/RabbitMQ/Subscription.py +90 -0
- DefStream/Broker/Implements/RabbitMQ/__init__.py +45 -0
- DefStream/Broker/Implements/Redis/Configuration.py +59 -0
- DefStream/Broker/Implements/Redis/Connection.py +265 -0
- DefStream/Broker/Implements/Redis/Consumer.py +368 -0
- DefStream/Broker/Implements/Redis/Event.py +212 -0
- DefStream/Broker/Implements/Redis/Queue.py +284 -0
- DefStream/Broker/Implements/Redis/Serializer.py +48 -0
- DefStream/Broker/Implements/Redis/Subscription.py +46 -0
- DefStream/Broker/Implements/Redis/__init__.py +29 -0
- DefStream/Broker/Implements/__init__.py +1 -0
- DefStream/Broker/Priority.py +26 -0
- DefStream/Broker/Protocol.py +54 -0
- DefStream/Broker/__init__.py +55 -0
- DefStream/Descriptor/DataObject.py +409 -0
- DefStream/Descriptor/Types.py +248 -0
- DefStream/Descriptor/Value.py +188 -0
- DefStream/Descriptor/__init__.py +172 -0
- DefStream/Inspector/Extension.py +69 -0
- DefStream/Inspector/Extensions/Metrics.py +316 -0
- DefStream/Inspector/Extensions/Ping.py +107 -0
- DefStream/Inspector/Extensions/Play.html +373 -0
- DefStream/Inspector/Extensions/Play.py +415 -0
- DefStream/Inspector/Extensions/Specifications.py +176 -0
- DefStream/Inspector/Extensions/Status.html +206 -0
- DefStream/Inspector/Extensions/Status.py +124 -0
- DefStream/Inspector/Extensions/__init__.py +13 -0
- DefStream/Inspector/Inspector.py +354 -0
- DefStream/Inspector/Response.py +14 -0
- DefStream/Inspector/__init__.py +9 -0
- DefStream/Validator/Pattern.py +36 -0
- DefStream/Validator/Patterns/Array.py +30 -0
- DefStream/Validator/Patterns/Common.py +92 -0
- DefStream/Validator/Patterns/Compare.py +180 -0
- DefStream/Validator/Patterns/Condition.py +60 -0
- DefStream/Validator/Patterns/DataObject.py +93 -0
- DefStream/Validator/Patterns/DateTime.py +111 -0
- DefStream/Validator/Patterns/Number.py +43 -0
- DefStream/Validator/Patterns/Object.py +158 -0
- DefStream/Validator/Patterns/Size.py +110 -0
- DefStream/Validator/Patterns/String.py +92 -0
- DefStream/Validator/Patterns/Type.py +355 -0
- DefStream/Validator/Patterns/__init__.py +137 -0
- DefStream/Validator/Validate.py +29 -0
- DefStream/Validator/Validator.py +30 -0
- DefStream/Validator/__init__.py +11 -0
- DefStream/Worker/Executor.py +525 -0
- DefStream/Worker/Message.py +180 -0
- DefStream/Worker/Pool.py +282 -0
- DefStream/Worker/Run.py +357 -0
- DefStream/Worker/Specification.py +87 -0
- DefStream/Worker/Startup.py +174 -0
- DefStream/Worker/Worker.py +571 -0
- DefStream/Worker/__init__.py +47 -0
- DefStream/__init__.py +0 -0
- defstream-0.1.22.dist-info/METADATA +92 -0
- defstream-0.1.22.dist-info/RECORD +73 -0
- defstream-0.1.22.dist-info/WHEEL +6 -0
- defstream-0.1.22.dist-info/licenses/LICENSE +674 -0
- defstream-0.1.22.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# COMMON
|
|
4
|
+
from .Common import (
|
|
5
|
+
SetDefault,
|
|
6
|
+
IsOptional,
|
|
7
|
+
IsNotToNone,
|
|
8
|
+
IsNotEmpty,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# SIZE
|
|
12
|
+
from .Size import (
|
|
13
|
+
IsSizeOf,
|
|
14
|
+
IsSizeIn,
|
|
15
|
+
IsMinSizeOf,
|
|
16
|
+
IsMaxSizeOf,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# COMPARE
|
|
20
|
+
from .Compare import (
|
|
21
|
+
IsEqualTo,
|
|
22
|
+
IsNotEqualTo,
|
|
23
|
+
IsGreaterThan,
|
|
24
|
+
IsGreaterEqualTo,
|
|
25
|
+
IsLessEqualTo,
|
|
26
|
+
IsLessThan,
|
|
27
|
+
IsIn,
|
|
28
|
+
IsNotIn,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# CONDITION
|
|
32
|
+
from .Condition import (
|
|
33
|
+
All,
|
|
34
|
+
Any,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# TYPE
|
|
38
|
+
from .Type import (
|
|
39
|
+
IsTypeOf,
|
|
40
|
+
IsBool,
|
|
41
|
+
IsInteger,
|
|
42
|
+
IsFloat,
|
|
43
|
+
IsNumber,
|
|
44
|
+
IsString,
|
|
45
|
+
IsList,
|
|
46
|
+
IsTuple,
|
|
47
|
+
IsSet,
|
|
48
|
+
IsArray,
|
|
49
|
+
IsObject,
|
|
50
|
+
IsByteString,
|
|
51
|
+
IsByteArray,
|
|
52
|
+
IsDecimal,
|
|
53
|
+
IsDataObject,
|
|
54
|
+
ToTypeOf,
|
|
55
|
+
ToBool,
|
|
56
|
+
ToInteger,
|
|
57
|
+
ToFloat,
|
|
58
|
+
ToString,
|
|
59
|
+
ToList,
|
|
60
|
+
ToTuple,
|
|
61
|
+
ToSet,
|
|
62
|
+
ToObject,
|
|
63
|
+
ToByteString,
|
|
64
|
+
ToByteArray,
|
|
65
|
+
ToDecimal,
|
|
66
|
+
ToDataObject,
|
|
67
|
+
)
|
|
68
|
+
from .DateTime import (
|
|
69
|
+
IsDateTime,
|
|
70
|
+
IsDate,
|
|
71
|
+
IsTime,
|
|
72
|
+
ToDateTime,
|
|
73
|
+
ToDate,
|
|
74
|
+
ToTime,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
__all__ = (
|
|
79
|
+
# COMMON
|
|
80
|
+
'SetDefault',
|
|
81
|
+
'IsOptional',
|
|
82
|
+
'IsNotToNone',
|
|
83
|
+
'IsNotEmpty',
|
|
84
|
+
# SIZE
|
|
85
|
+
'IsSizeOf',
|
|
86
|
+
'IsSizeIn',
|
|
87
|
+
'IsMinSizeOf',
|
|
88
|
+
'IsMaxSizeOf',
|
|
89
|
+
# COMPARE
|
|
90
|
+
'IsEqualTo',
|
|
91
|
+
'IsNotEqualTo',
|
|
92
|
+
'IsGreaterThan',
|
|
93
|
+
'IsGreaterEqualTo',
|
|
94
|
+
'IsLessEqualTo',
|
|
95
|
+
'IsLessThan',
|
|
96
|
+
'IsIn',
|
|
97
|
+
'IsNotIn',
|
|
98
|
+
# CONDITION
|
|
99
|
+
'All',
|
|
100
|
+
'Any',
|
|
101
|
+
# TYPE
|
|
102
|
+
'IsTypeOf',
|
|
103
|
+
'IsBool',
|
|
104
|
+
'IsInteger',
|
|
105
|
+
'IsFloat',
|
|
106
|
+
'IsNumber',
|
|
107
|
+
'IsString',
|
|
108
|
+
'IsList',
|
|
109
|
+
'IsTuple',
|
|
110
|
+
'IsSet',
|
|
111
|
+
'IsArray',
|
|
112
|
+
'IsObject',
|
|
113
|
+
'IsByteString',
|
|
114
|
+
'IsByteArray',
|
|
115
|
+
'IsDecimal',
|
|
116
|
+
'IsDataObject',
|
|
117
|
+
'ToTypeOf',
|
|
118
|
+
'ToBool',
|
|
119
|
+
'ToInteger',
|
|
120
|
+
'ToFloat',
|
|
121
|
+
'ToString',
|
|
122
|
+
'ToList',
|
|
123
|
+
'ToTuple',
|
|
124
|
+
'ToSet',
|
|
125
|
+
'ToObject',
|
|
126
|
+
'ToByteString',
|
|
127
|
+
'ToByteArray',
|
|
128
|
+
'ToDecimal',
|
|
129
|
+
'ToDataObject',
|
|
130
|
+
# DateTime
|
|
131
|
+
'IsDateTime',
|
|
132
|
+
'IsDate',
|
|
133
|
+
'IsTime',
|
|
134
|
+
'ToDateTime',
|
|
135
|
+
'ToDate',
|
|
136
|
+
'ToTime',
|
|
137
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from .Validator import Validator
|
|
4
|
+
from .Patterns import IsObject
|
|
5
|
+
from .Patterns.Object import IsMappingOf
|
|
6
|
+
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from inspect import signature
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
'Validate',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Validate(object):
|
|
16
|
+
"""Decorator for Validator of Function and Method"""
|
|
17
|
+
def __init__(self, mappings: dict):
|
|
18
|
+
self.mappings = mappings
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def __call__(self, fn: callable):
|
|
22
|
+
@wraps(fn)
|
|
23
|
+
def wrapper(*args, **kwargs):
|
|
24
|
+
validator = Validator(IsObject(IsMappingOf(self.mappings)))
|
|
25
|
+
bound = signature(fn).bind(*args, **kwargs)
|
|
26
|
+
bound.apply_defaults()
|
|
27
|
+
bound.arguments = validator(dict(bound.arguments))
|
|
28
|
+
return fn(*bound.args, **bound.kwargs)
|
|
29
|
+
return wrapper
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from .Pattern import Pattern
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
'Validator',
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Validator(object):
|
|
11
|
+
"""
|
|
12
|
+
Validator Class
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, *args):
|
|
15
|
+
for pattern in args:
|
|
16
|
+
if not isinstance(pattern, Pattern):
|
|
17
|
+
raise RuntimeError('{} must be based {}'.format(pattern, Pattern))
|
|
18
|
+
self.patterns = args
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def __call__(self, parameter):
|
|
22
|
+
for pattern in self.patterns:
|
|
23
|
+
parameter = pattern(parameter)
|
|
24
|
+
return parameter
|
|
25
|
+
|
|
26
|
+
def __repr__(self):
|
|
27
|
+
return 'Validator({})'.format(
|
|
28
|
+
', '.join([repr(p) for p in self.patterns])
|
|
29
|
+
)
|
|
30
|
+
|
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
from concurrent.futures import (
|
|
2
|
+
Executor as PyExecutor,
|
|
3
|
+
Future,
|
|
4
|
+
ThreadPoolExecutor,
|
|
5
|
+
)
|
|
6
|
+
from multiprocessing import get_context
|
|
7
|
+
from multiprocessing.process import BaseProcess
|
|
8
|
+
from multiprocessing.synchronize import Event as ProcessEvent
|
|
9
|
+
from logging import getLogger
|
|
10
|
+
from multiprocessing.managers import SyncManager
|
|
11
|
+
from pickle import (
|
|
12
|
+
dumps,
|
|
13
|
+
loads,
|
|
14
|
+
)
|
|
15
|
+
from queue import (
|
|
16
|
+
Empty,
|
|
17
|
+
Queue,
|
|
18
|
+
)
|
|
19
|
+
from signal import (
|
|
20
|
+
SIGINT,
|
|
21
|
+
SIG_IGN,
|
|
22
|
+
signal,
|
|
23
|
+
)
|
|
24
|
+
from threading import (
|
|
25
|
+
Event,
|
|
26
|
+
Lock,
|
|
27
|
+
Thread,
|
|
28
|
+
)
|
|
29
|
+
from traceback import format_exc
|
|
30
|
+
from typing import (
|
|
31
|
+
Any,
|
|
32
|
+
Callable,
|
|
33
|
+
Dict,
|
|
34
|
+
List,
|
|
35
|
+
Optional,
|
|
36
|
+
Sequence,
|
|
37
|
+
Set,
|
|
38
|
+
TypeVar,
|
|
39
|
+
Union,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = (
|
|
43
|
+
'Executor',
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
Result = TypeVar('Result')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def initialize_process() -> None:
|
|
50
|
+
# 부모 프로세스가 종료 요청과 정리를 조정한다.
|
|
51
|
+
signal(
|
|
52
|
+
SIGINT,
|
|
53
|
+
SIG_IGN,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def execute_loop(
|
|
58
|
+
incoming: Any,
|
|
59
|
+
outgoing: Any,
|
|
60
|
+
setups: Sequence[Callable[[], None]],
|
|
61
|
+
cleanups: Sequence[Callable[[], None]],
|
|
62
|
+
serialized: bool,
|
|
63
|
+
stopping: Union[Event, ProcessEvent],
|
|
64
|
+
draining: Optional[ProcessEvent] = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
if serialized:
|
|
67
|
+
initialize_process()
|
|
68
|
+
logger = Executor.logger
|
|
69
|
+
ready = False
|
|
70
|
+
try:
|
|
71
|
+
for setup in setups:
|
|
72
|
+
logger.debug(
|
|
73
|
+
'Starting execution environment setup: hook=%s',
|
|
74
|
+
type(setup).__qualname__,
|
|
75
|
+
)
|
|
76
|
+
setup()
|
|
77
|
+
logger.debug(
|
|
78
|
+
'Execution environment setup completed: hook=%s',
|
|
79
|
+
type(setup).__qualname__,
|
|
80
|
+
)
|
|
81
|
+
ready = True
|
|
82
|
+
logger.debug(
|
|
83
|
+
'Execution unit ready: serialized=%s',
|
|
84
|
+
serialized,
|
|
85
|
+
)
|
|
86
|
+
while True:
|
|
87
|
+
item = incoming.get()
|
|
88
|
+
if item is None or stopping.is_set():
|
|
89
|
+
if draining is not None:
|
|
90
|
+
draining.set()
|
|
91
|
+
return
|
|
92
|
+
key, payload = item
|
|
93
|
+
logger.debug(
|
|
94
|
+
'Execution unit task received: key=%s',
|
|
95
|
+
key,
|
|
96
|
+
)
|
|
97
|
+
try:
|
|
98
|
+
function, args, kwargs = loads(payload) if serialized else payload
|
|
99
|
+
result = function(
|
|
100
|
+
*args,
|
|
101
|
+
**kwargs,
|
|
102
|
+
)
|
|
103
|
+
except BaseException as error:
|
|
104
|
+
logger.debug(
|
|
105
|
+
'Execution unit task failed: key=%s error_type=%s',
|
|
106
|
+
key,
|
|
107
|
+
type(error).__qualname__,
|
|
108
|
+
)
|
|
109
|
+
outcome = (
|
|
110
|
+
False,
|
|
111
|
+
error,
|
|
112
|
+
format_exc() if serialized else None,
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
outcome = (
|
|
116
|
+
True,
|
|
117
|
+
result,
|
|
118
|
+
None,
|
|
119
|
+
)
|
|
120
|
+
if serialized:
|
|
121
|
+
try:
|
|
122
|
+
outcome = dumps(outcome)
|
|
123
|
+
except Exception as error:
|
|
124
|
+
logger.error(
|
|
125
|
+
'Task outcome serialization failed: key=%s error_type=%s',
|
|
126
|
+
key,
|
|
127
|
+
type(error).__qualname__,
|
|
128
|
+
)
|
|
129
|
+
outcome = dumps((
|
|
130
|
+
False,
|
|
131
|
+
RuntimeError('Cannot serialize task outcome: ' + repr(error)),
|
|
132
|
+
format_exc(),
|
|
133
|
+
))
|
|
134
|
+
outgoing.put((
|
|
135
|
+
key,
|
|
136
|
+
outcome,
|
|
137
|
+
))
|
|
138
|
+
logger.debug(
|
|
139
|
+
'Execution unit result sent: key=%s',
|
|
140
|
+
key,
|
|
141
|
+
)
|
|
142
|
+
finally:
|
|
143
|
+
if ready:
|
|
144
|
+
logger.debug(
|
|
145
|
+
'Starting execution environment cleanup: hooks=%s',
|
|
146
|
+
len(cleanups),
|
|
147
|
+
)
|
|
148
|
+
run_cleanups(cleanups)
|
|
149
|
+
logger.debug('Execution environment cleanup completed')
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def run_cleanups(
|
|
153
|
+
cleanups: Sequence[Callable[..., None]],
|
|
154
|
+
*args: Any,
|
|
155
|
+
) -> None:
|
|
156
|
+
failure: Optional[BaseException] = None
|
|
157
|
+
for cleanup in cleanups:
|
|
158
|
+
try:
|
|
159
|
+
cleanup(*args)
|
|
160
|
+
except BaseException as error:
|
|
161
|
+
if failure is not None:
|
|
162
|
+
error.__context__ = failure
|
|
163
|
+
failure = error
|
|
164
|
+
if failure is not None:
|
|
165
|
+
raise failure
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def execute_process(
|
|
169
|
+
incoming: Any,
|
|
170
|
+
outgoing: Any,
|
|
171
|
+
setups: Sequence[Callable[[], None]],
|
|
172
|
+
cleanups: Sequence[Callable[[], None]],
|
|
173
|
+
serialized: bool,
|
|
174
|
+
stopping: Union[Event, ProcessEvent],
|
|
175
|
+
draining: ProcessEvent,
|
|
176
|
+
completed: ProcessEvent,
|
|
177
|
+
) -> None:
|
|
178
|
+
try:
|
|
179
|
+
execute_loop(
|
|
180
|
+
incoming,
|
|
181
|
+
outgoing,
|
|
182
|
+
setups,
|
|
183
|
+
cleanups,
|
|
184
|
+
serialized,
|
|
185
|
+
stopping,
|
|
186
|
+
draining,
|
|
187
|
+
)
|
|
188
|
+
except BaseException as error:
|
|
189
|
+
traceback = format_exc()
|
|
190
|
+
try:
|
|
191
|
+
outcome = dumps((
|
|
192
|
+
False,
|
|
193
|
+
(
|
|
194
|
+
error,
|
|
195
|
+
draining.is_set(),
|
|
196
|
+
),
|
|
197
|
+
traceback,
|
|
198
|
+
))
|
|
199
|
+
except Exception:
|
|
200
|
+
Executor.logger.exception('Execution environment error serialization failed')
|
|
201
|
+
outcome = dumps((
|
|
202
|
+
False,
|
|
203
|
+
(
|
|
204
|
+
RuntimeError('Cannot serialize execution environment error'),
|
|
205
|
+
draining.is_set(),
|
|
206
|
+
),
|
|
207
|
+
traceback,
|
|
208
|
+
))
|
|
209
|
+
outgoing.put((
|
|
210
|
+
None,
|
|
211
|
+
outcome,
|
|
212
|
+
))
|
|
213
|
+
finally:
|
|
214
|
+
completed.set()
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Executor(PyExecutor):
|
|
219
|
+
logger = getLogger('Worker.Executor')
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
size: int,
|
|
224
|
+
setups: Sequence[Callable[[], None]],
|
|
225
|
+
cleanups: Sequence[Callable[[], None]],
|
|
226
|
+
process: bool = False,
|
|
227
|
+
):
|
|
228
|
+
self.size = size
|
|
229
|
+
self.setups = setups
|
|
230
|
+
self.cleanups = cleanups
|
|
231
|
+
self.process = process
|
|
232
|
+
self.lock = Lock()
|
|
233
|
+
self.pending: Dict[int, Future[Any]] = {}
|
|
234
|
+
self.workers: List[Future] = []
|
|
235
|
+
self.processes: List[BaseProcess] = []
|
|
236
|
+
self.draining: Dict[int, ProcessEvent] = {}
|
|
237
|
+
self.completed: Dict[int, ProcessEvent] = {}
|
|
238
|
+
self.process_lock = Lock()
|
|
239
|
+
self.stopping: Union[Event, ProcessEvent] = Event()
|
|
240
|
+
self.closed = False
|
|
241
|
+
self.failure: Optional[BaseException] = None
|
|
242
|
+
self.manager: Optional[SyncManager] = None
|
|
243
|
+
self.executor: Optional[PyExecutor] = None
|
|
244
|
+
self.monitor: Optional[Thread] = None
|
|
245
|
+
self.incoming: Any = None
|
|
246
|
+
self.outgoing: Any = None
|
|
247
|
+
self.sequence = 0
|
|
248
|
+
|
|
249
|
+
def start(self) -> None:
|
|
250
|
+
self.logger.info(
|
|
251
|
+
'Starting Executor: concurrency=%s process=%s',
|
|
252
|
+
self.size,
|
|
253
|
+
self.process,
|
|
254
|
+
)
|
|
255
|
+
try:
|
|
256
|
+
context = get_context('spawn')
|
|
257
|
+
if self.process:
|
|
258
|
+
self.stopping = context.Event()
|
|
259
|
+
self.manager = context.Manager()
|
|
260
|
+
self.incoming = self.manager.Queue()
|
|
261
|
+
self.outgoing = self.manager.Queue()
|
|
262
|
+
else:
|
|
263
|
+
self.incoming = Queue()
|
|
264
|
+
self.outgoing = Queue()
|
|
265
|
+
self.executor = ThreadPoolExecutor(self.size)
|
|
266
|
+
for _ in range(self.size):
|
|
267
|
+
parameters = (
|
|
268
|
+
self.incoming,
|
|
269
|
+
self.outgoing,
|
|
270
|
+
self.setups,
|
|
271
|
+
self.cleanups,
|
|
272
|
+
self.process,
|
|
273
|
+
self.stopping,
|
|
274
|
+
)
|
|
275
|
+
if self.process:
|
|
276
|
+
draining = context.Event()
|
|
277
|
+
completed = context.Event()
|
|
278
|
+
process = context.Process(
|
|
279
|
+
target=execute_process,
|
|
280
|
+
args=parameters + (
|
|
281
|
+
draining,
|
|
282
|
+
completed,
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
process.start()
|
|
286
|
+
self.processes.append(process)
|
|
287
|
+
if process.pid is None:
|
|
288
|
+
raise RuntimeError('Started process has no PID')
|
|
289
|
+
# Keep named semaphores alive until spawned children have exited.
|
|
290
|
+
self.draining[process.pid] = draining
|
|
291
|
+
self.completed[process.pid] = completed
|
|
292
|
+
else:
|
|
293
|
+
if self.executor is None:
|
|
294
|
+
raise RuntimeError('Thread executor is not initialized')
|
|
295
|
+
self.workers.append(self.executor.submit(
|
|
296
|
+
execute_loop,
|
|
297
|
+
*parameters,
|
|
298
|
+
))
|
|
299
|
+
monitor = Thread(target=self.collect)
|
|
300
|
+
monitor.start()
|
|
301
|
+
self.monitor = monitor
|
|
302
|
+
self.logger.info('Executor execution environment ready')
|
|
303
|
+
except BaseException:
|
|
304
|
+
self.logger.exception('Executor startup failed')
|
|
305
|
+
self.closed = True
|
|
306
|
+
try:
|
|
307
|
+
for _ in self.workers:
|
|
308
|
+
self.incoming.put(None)
|
|
309
|
+
finally:
|
|
310
|
+
try:
|
|
311
|
+
if self.executor is not None:
|
|
312
|
+
self.executor.shutdown(wait=True)
|
|
313
|
+
for process in self.processes:
|
|
314
|
+
process.terminate()
|
|
315
|
+
process.join()
|
|
316
|
+
finally:
|
|
317
|
+
if self.manager is not None:
|
|
318
|
+
self.manager.shutdown()
|
|
319
|
+
raise
|
|
320
|
+
|
|
321
|
+
def submit(
|
|
322
|
+
self,
|
|
323
|
+
fn: Callable[..., Result],
|
|
324
|
+
/,
|
|
325
|
+
*args: Any,
|
|
326
|
+
**kwargs: Any,
|
|
327
|
+
) -> Future[Result]:
|
|
328
|
+
payload = (
|
|
329
|
+
fn,
|
|
330
|
+
args,
|
|
331
|
+
kwargs,
|
|
332
|
+
)
|
|
333
|
+
if self.process:
|
|
334
|
+
payload = dumps(payload)
|
|
335
|
+
with self.lock:
|
|
336
|
+
if self.closed:
|
|
337
|
+
raise RuntimeError('Pool is closed')
|
|
338
|
+
if self.failure is not None:
|
|
339
|
+
raise RuntimeError('Pool execution environment failed') from self.failure
|
|
340
|
+
if self.monitor is None:
|
|
341
|
+
self.start()
|
|
342
|
+
key = self.sequence
|
|
343
|
+
self.sequence += 1
|
|
344
|
+
future: Future[Result] = Future()
|
|
345
|
+
future.set_running_or_notify_cancel()
|
|
346
|
+
self.pending[key] = future
|
|
347
|
+
self.logger.debug(
|
|
348
|
+
'Dispatching task to execution environment: key=%s pending=%s',
|
|
349
|
+
key,
|
|
350
|
+
len(self.pending),
|
|
351
|
+
)
|
|
352
|
+
try:
|
|
353
|
+
self.incoming.put((
|
|
354
|
+
key,
|
|
355
|
+
payload,
|
|
356
|
+
))
|
|
357
|
+
except BaseException:
|
|
358
|
+
del self.pending[key]
|
|
359
|
+
raise
|
|
360
|
+
return future
|
|
361
|
+
|
|
362
|
+
def collect(self) -> None:
|
|
363
|
+
try:
|
|
364
|
+
self.collect_results()
|
|
365
|
+
except BaseException as error:
|
|
366
|
+
self.fail(error)
|
|
367
|
+
|
|
368
|
+
def fail(
|
|
369
|
+
self,
|
|
370
|
+
error: BaseException,
|
|
371
|
+
*,
|
|
372
|
+
report: bool = True,
|
|
373
|
+
) -> None:
|
|
374
|
+
with self.lock:
|
|
375
|
+
first_failure = self.failure is None
|
|
376
|
+
self.failure = error
|
|
377
|
+
pending = tuple(self.pending.values())
|
|
378
|
+
self.pending.clear()
|
|
379
|
+
if self.process:
|
|
380
|
+
self.stopping.set()
|
|
381
|
+
self.stop_processes()
|
|
382
|
+
if report and (first_failure or self.stopping.is_set()):
|
|
383
|
+
self.logger.error(
|
|
384
|
+
'Executor execution environment failed: pending=%s',
|
|
385
|
+
len(pending),
|
|
386
|
+
exc_info=(
|
|
387
|
+
type(error),
|
|
388
|
+
error,
|
|
389
|
+
error.__traceback__,
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
for future in pending:
|
|
393
|
+
future.set_exception(error)
|
|
394
|
+
|
|
395
|
+
def collect_results(self) -> None:
|
|
396
|
+
observed: Set[Future] = set()
|
|
397
|
+
while True:
|
|
398
|
+
no_outcome = False
|
|
399
|
+
try:
|
|
400
|
+
key, outcome = self.outgoing.get(timeout=0.05)
|
|
401
|
+
except Empty:
|
|
402
|
+
no_outcome = True
|
|
403
|
+
else:
|
|
404
|
+
success, value, traceback = loads(outcome) if self.process else outcome
|
|
405
|
+
if key is None:
|
|
406
|
+
error, draining = value
|
|
407
|
+
if traceback is not None:
|
|
408
|
+
error.__cause__ = RuntimeError('Remote execution traceback:\n' + traceback)
|
|
409
|
+
if draining and self.closed and not self.stopping.is_set():
|
|
410
|
+
with self.lock:
|
|
411
|
+
self.failure = error
|
|
412
|
+
self.logger.error('Execution environment cleanup failed', exc_info=(
|
|
413
|
+
type(error),
|
|
414
|
+
error,
|
|
415
|
+
error.__traceback__,
|
|
416
|
+
))
|
|
417
|
+
else:
|
|
418
|
+
self.fail(error)
|
|
419
|
+
continue
|
|
420
|
+
self.logger.debug(
|
|
421
|
+
'Execution result collected: key=%s success=%s',
|
|
422
|
+
key,
|
|
423
|
+
success,
|
|
424
|
+
)
|
|
425
|
+
with self.lock:
|
|
426
|
+
future = self.pending.pop(
|
|
427
|
+
key,
|
|
428
|
+
None,
|
|
429
|
+
)
|
|
430
|
+
if future is not None:
|
|
431
|
+
if success:
|
|
432
|
+
future.set_result(value)
|
|
433
|
+
else:
|
|
434
|
+
if traceback is not None:
|
|
435
|
+
value.__cause__ = RuntimeError('Remote execution traceback:\n' + traceback)
|
|
436
|
+
future.set_exception(value)
|
|
437
|
+
for worker in self.workers:
|
|
438
|
+
if worker.done() and worker not in observed:
|
|
439
|
+
observed.add(worker)
|
|
440
|
+
failure: Optional[BaseException] = worker.exception()
|
|
441
|
+
if failure is not None:
|
|
442
|
+
self.fail(failure)
|
|
443
|
+
if not self.stopping.is_set() and any(
|
|
444
|
+
process.pid is not None and process.exitcode is not None and (
|
|
445
|
+
process.exitcode != 0 or not self.completed[process.pid].is_set()
|
|
446
|
+
)
|
|
447
|
+
for process in self.processes
|
|
448
|
+
):
|
|
449
|
+
self.fail(RuntimeError('Pool execution process failed'))
|
|
450
|
+
with self.lock:
|
|
451
|
+
finished = (
|
|
452
|
+
not self.pending
|
|
453
|
+
and all(worker.done() for worker in self.workers)
|
|
454
|
+
and all(not process.is_alive() for process in self.processes)
|
|
455
|
+
and (not self.process or no_outcome)
|
|
456
|
+
)
|
|
457
|
+
if finished:
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
def shutdown(
|
|
461
|
+
self,
|
|
462
|
+
wait: bool = True,
|
|
463
|
+
*,
|
|
464
|
+
cancel_futures: bool = False,
|
|
465
|
+
) -> None:
|
|
466
|
+
self.logger.info(
|
|
467
|
+
'Executor shutdown requested: wait=%s',
|
|
468
|
+
wait,
|
|
469
|
+
)
|
|
470
|
+
with self.lock:
|
|
471
|
+
if not self.closed:
|
|
472
|
+
self.closed = True
|
|
473
|
+
if self.monitor is not None:
|
|
474
|
+
for _ in range(self.size):
|
|
475
|
+
self.incoming.put(None)
|
|
476
|
+
executor = self.executor
|
|
477
|
+
if self.monitor is None:
|
|
478
|
+
self.logger.debug('Closing an unstarted Executor')
|
|
479
|
+
return
|
|
480
|
+
if executor is not None:
|
|
481
|
+
executor.shutdown(wait=wait)
|
|
482
|
+
if wait:
|
|
483
|
+
for process in self.processes:
|
|
484
|
+
process.join()
|
|
485
|
+
if self.monitor is not None:
|
|
486
|
+
self.monitor.join()
|
|
487
|
+
if self.manager is not None:
|
|
488
|
+
self.manager.shutdown()
|
|
489
|
+
if self.failure is not None:
|
|
490
|
+
raise RuntimeError('Pool execution environment failed') from self.failure
|
|
491
|
+
self.logger.info('Executor shutdown completed')
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
def replacement(self) -> 'Executor':
|
|
495
|
+
return Executor(
|
|
496
|
+
self.size,
|
|
497
|
+
self.setups,
|
|
498
|
+
self.cleanups,
|
|
499
|
+
process=self.process,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def stop_processes(self) -> None:
|
|
503
|
+
"""Reap surviving processes before publishing failures for their tasks."""
|
|
504
|
+
with self.process_lock:
|
|
505
|
+
for process in self.processes:
|
|
506
|
+
if process.is_alive():
|
|
507
|
+
process.kill()
|
|
508
|
+
for process in self.processes:
|
|
509
|
+
process.join()
|
|
510
|
+
return
|
|
511
|
+
|
|
512
|
+
def abort(self) -> None:
|
|
513
|
+
"""Discard this execution generation without waiting for running threads."""
|
|
514
|
+
self.stopping.set()
|
|
515
|
+
if self.process:
|
|
516
|
+
self.stop_processes()
|
|
517
|
+
self.logger.warning('Discarding execution generation: pending=%s', len(self.pending))
|
|
518
|
+
self.fail(RuntimeError('Execution generation discarded'), report=False)
|
|
519
|
+
self.shutdown(wait=False)
|
|
520
|
+
if self.process:
|
|
521
|
+
if self.monitor is not None:
|
|
522
|
+
self.monitor.join()
|
|
523
|
+
if self.manager is not None:
|
|
524
|
+
self.manager.shutdown()
|
|
525
|
+
return
|