blackant-sdk 1.0.2__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.
- blackant/__init__.py +31 -0
- blackant/auth/__init__.py +10 -0
- blackant/auth/blackant_auth.py +518 -0
- blackant/auth/keycloak_manager.py +363 -0
- blackant/auth/request_id.py +52 -0
- blackant/auth/role_assignment.py +443 -0
- blackant/auth/tokens.py +57 -0
- blackant/client.py +400 -0
- blackant/config/__init__.py +0 -0
- blackant/config/docker_config.py +457 -0
- blackant/config/keycloak_admin_config.py +107 -0
- blackant/docker/__init__.py +12 -0
- blackant/docker/builder.py +616 -0
- blackant/docker/client.py +983 -0
- blackant/docker/dao.py +462 -0
- blackant/docker/registry.py +172 -0
- blackant/exceptions.py +111 -0
- blackant/http/__init__.py +8 -0
- blackant/http/client.py +125 -0
- blackant/patterns/__init__.py +1 -0
- blackant/patterns/singleton.py +20 -0
- blackant/services/__init__.py +10 -0
- blackant/services/dao.py +414 -0
- blackant/services/registry.py +635 -0
- blackant/utils/__init__.py +8 -0
- blackant/utils/initialization.py +32 -0
- blackant/utils/logging.py +337 -0
- blackant/utils/request_id.py +13 -0
- blackant/utils/store.py +50 -0
- blackant_sdk-1.0.2.dist-info/METADATA +117 -0
- blackant_sdk-1.0.2.dist-info/RECORD +70 -0
- blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
- blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
- calculation/__init__.py +0 -0
- calculation/base.py +26 -0
- calculation/errors.py +2 -0
- calculation/impl/__init__.py +0 -0
- calculation/impl/my_calculation.py +144 -0
- calculation/impl/simple_calc.py +53 -0
- calculation/impl/test.py +1 -0
- calculation/impl/test_calc.py +36 -0
- calculation/loader.py +227 -0
- notifinations/__init__.py +8 -0
- notifinations/mail_sender.py +212 -0
- storage/__init__.py +0 -0
- storage/errors.py +10 -0
- storage/factory.py +26 -0
- storage/interface.py +19 -0
- storage/minio.py +106 -0
- task/__init__.py +0 -0
- task/dao.py +38 -0
- task/errors.py +10 -0
- task/log_adapter.py +11 -0
- task/parsers/__init__.py +0 -0
- task/parsers/base.py +13 -0
- task/parsers/callback.py +40 -0
- task/parsers/cmd_args.py +52 -0
- task/parsers/freetext.py +19 -0
- task/parsers/objects.py +50 -0
- task/parsers/request.py +56 -0
- task/resource.py +84 -0
- task/states/__init__.py +0 -0
- task/states/base.py +14 -0
- task/states/error.py +47 -0
- task/states/idle.py +12 -0
- task/states/ready.py +51 -0
- task/states/running.py +21 -0
- task/states/set_up.py +40 -0
- task/states/tear_down.py +29 -0
- task/task.py +358 -0
task/states/running.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from calculation.errors import CalculationError
|
|
2
|
+
|
|
3
|
+
from task.states.base import TaskStateBase
|
|
4
|
+
from task.states.error import TaskErrorState
|
|
5
|
+
from task.states.tear_down import TaskTearDownState
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TaskRunningState(TaskStateBase):
|
|
9
|
+
@property
|
|
10
|
+
def name(self):
|
|
11
|
+
return "running"
|
|
12
|
+
|
|
13
|
+
def run(self):
|
|
14
|
+
try:
|
|
15
|
+
self._task.logger.info("Running calculation")
|
|
16
|
+
self._task.calculation.run()
|
|
17
|
+
self._task.change_state(TaskTearDownState(self._task))
|
|
18
|
+
except CalculationError as calculation_error:
|
|
19
|
+
self._task.logger.error("Calculation error occurred during running phase")
|
|
20
|
+
self._task.logger.exception(calculation_error)
|
|
21
|
+
self._task.change_state(TaskErrorState(self._task))
|
task/states/set_up.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from calculation.errors import CalculationError
|
|
2
|
+
|
|
3
|
+
from storage.errors import StorageCreationFailedException
|
|
4
|
+
from storage.factory import StorageFactory
|
|
5
|
+
|
|
6
|
+
from task.states.base import TaskStateBase
|
|
7
|
+
from task.states.error import TaskErrorState
|
|
8
|
+
from task.states.running import TaskRunningState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskSetUpState(TaskStateBase):
|
|
12
|
+
def __init__(self, task):
|
|
13
|
+
super().__init__(task)
|
|
14
|
+
try:
|
|
15
|
+
self.__storage = StorageFactory.create()
|
|
16
|
+
except (StorageCreationFailedException, Exception) as exc:
|
|
17
|
+
# Catch all storage errors - storage is optional
|
|
18
|
+
self._task.logger.error("Cannot connect to object storage")
|
|
19
|
+
self._task.logger.info("Continuing, because it is possible to not use it at all")
|
|
20
|
+
self._task.logger.debug(str(exc))
|
|
21
|
+
self.__storage = None # Explicitly set to None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self):
|
|
25
|
+
return "set up"
|
|
26
|
+
|
|
27
|
+
def run(self):
|
|
28
|
+
try:
|
|
29
|
+
self._task.logger.info("Run set up phase of calculation")
|
|
30
|
+
self.__fetch_configured_objects()
|
|
31
|
+
self._task.calculation.set_up()
|
|
32
|
+
self._task.change_state(TaskRunningState(self._task))
|
|
33
|
+
except CalculationError as calculation_error:
|
|
34
|
+
self._task.logger.error("Calculation error occured during set up phase")
|
|
35
|
+
self._task.logger.exception(calculation_error)
|
|
36
|
+
self._task.change_state(TaskErrorState(self._task))
|
|
37
|
+
|
|
38
|
+
def __fetch_configured_objects(self):
|
|
39
|
+
for obj in self._task.configuration.objects:
|
|
40
|
+
self.__storage.download_file(obj.local, obj.remote)
|
task/states/tear_down.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from calculation.errors import CalculationError
|
|
4
|
+
|
|
5
|
+
from task.states.base import TaskStateBase
|
|
6
|
+
from task.states.error import TaskErrorState
|
|
7
|
+
from task.states.ready import TaskReadyState
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskTearDownState(TaskStateBase):
|
|
11
|
+
@property
|
|
12
|
+
def name(self):
|
|
13
|
+
return "tear down"
|
|
14
|
+
|
|
15
|
+
def run(self):
|
|
16
|
+
try:
|
|
17
|
+
self._task.logger.info("Run tear down phase of calculation")
|
|
18
|
+
self._task.calculation.tear_down()
|
|
19
|
+
self.__delete_unpreserved_files()
|
|
20
|
+
self._task.change_state(TaskReadyState(self._task))
|
|
21
|
+
except CalculationError as calculation_error:
|
|
22
|
+
self._task.logger.error("Calculation error occurred during tear down phase")
|
|
23
|
+
self._task.logger.exception(calculation_error)
|
|
24
|
+
self._task.change_state(TaskErrorState(self._task))
|
|
25
|
+
|
|
26
|
+
def __delete_unpreserved_files(self):
|
|
27
|
+
for obj in self._task.configuration.objects:
|
|
28
|
+
if not obj.preserve and os.path.exists(obj.local):
|
|
29
|
+
os.remove(obj.local)
|
task/task.py
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
import copy
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
import random
|
|
7
|
+
import sys
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
# Import BlackAnt HTTP client for proper request handling
|
|
14
|
+
try:
|
|
15
|
+
from blackant.http.client import HTTPClient
|
|
16
|
+
from blackant.utils.logging import get_logger
|
|
17
|
+
except ImportError:
|
|
18
|
+
# Fallback if BlackAnt SDK not available in this context
|
|
19
|
+
HTTPClient = None
|
|
20
|
+
get_logger = None
|
|
21
|
+
|
|
22
|
+
from calculation.base import CalculationBase
|
|
23
|
+
|
|
24
|
+
from task.parsers.request import TaskConfiguration
|
|
25
|
+
from task.parsers.callback import CallbackParameter
|
|
26
|
+
from task.states.idle import TaskIdleState
|
|
27
|
+
from task.states.base import TaskStateBase
|
|
28
|
+
from task.states.error import TaskErrorState
|
|
29
|
+
from task.errors import TaskCallbackFailedError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Logger writer to redirect stdout/stderr to logger
|
|
33
|
+
class LoggerWriter:
|
|
34
|
+
"""A class to wrap the logger and redirect writes to it."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, logger, level="info"):
|
|
37
|
+
self.logger = logger
|
|
38
|
+
self.level = level
|
|
39
|
+
|
|
40
|
+
def write(self, message):
|
|
41
|
+
"""Write a message to the logger."""
|
|
42
|
+
if message.strip(): # Avoid logging empty lines
|
|
43
|
+
if self.level == "error":
|
|
44
|
+
self.logger.error(message.strip())
|
|
45
|
+
else:
|
|
46
|
+
self.logger.info(message.strip())
|
|
47
|
+
|
|
48
|
+
def flush(self):
|
|
49
|
+
"""Flush the stream (not required, but needed for compatibility)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Context manager to redirect stdout and stderr to a logger
|
|
53
|
+
@contextmanager
|
|
54
|
+
def redirect_stdout_stderr_to_logger(logger):
|
|
55
|
+
"""Context manager to redirect stdout and stderr to a logger."""
|
|
56
|
+
original_stdout = sys.stdout
|
|
57
|
+
original_stderr = sys.stderr
|
|
58
|
+
|
|
59
|
+
sys.stdout = LoggerWriter(logger)
|
|
60
|
+
sys.stderr = LoggerWriter(logger, level="error")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
yield
|
|
64
|
+
finally:
|
|
65
|
+
sys.stdout = original_stdout
|
|
66
|
+
sys.stderr = original_stderr
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Task: # pylint: disable=too-many-instance-attributes
|
|
70
|
+
"""Task entity in science module
|
|
71
|
+
|
|
72
|
+
Whenever the server receives a request,
|
|
73
|
+
a task is created to handle the calculation.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
__scheduler_callback: CallbackParameter = CallbackParameter(
|
|
77
|
+
url=os.environ["SCHEDULER_URL"],
|
|
78
|
+
method="POST",
|
|
79
|
+
headers={"Content-type": "application/json"},
|
|
80
|
+
request_body={},
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
calculation: CalculationBase,
|
|
86
|
+
configuration: TaskConfiguration,
|
|
87
|
+
log_file_handler,
|
|
88
|
+
logger,
|
|
89
|
+
):
|
|
90
|
+
"""Task constructor
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
calculation (CalculationBase): Calculation to perform
|
|
94
|
+
configuration (TaskConfiguration): Configuration composed by the incoming GET request
|
|
95
|
+
logger (Logger): Logger to use
|
|
96
|
+
"""
|
|
97
|
+
self.calculation = calculation
|
|
98
|
+
self.logger = logger
|
|
99
|
+
self.task_id = None
|
|
100
|
+
self.log_file_handler = log_file_handler
|
|
101
|
+
self.__configuration = configuration
|
|
102
|
+
self.__start_execution_time = None
|
|
103
|
+
|
|
104
|
+
if not self.calculation:
|
|
105
|
+
self.logger.error("The calculation object in not defined.")
|
|
106
|
+
self.__state = TaskErrorState(self)
|
|
107
|
+
else:
|
|
108
|
+
self.__state = TaskIdleState(self)
|
|
109
|
+
|
|
110
|
+
self.__finished = False
|
|
111
|
+
self.__executor = threading.Thread(target=self.__execute_state_machine)
|
|
112
|
+
self.__thread_lock = threading.Lock()
|
|
113
|
+
|
|
114
|
+
def change_state(self, state: TaskStateBase):
|
|
115
|
+
"""Handles state changes (State design pattern)
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
state (TaskStateBase): New state
|
|
119
|
+
"""
|
|
120
|
+
self.__state = state
|
|
121
|
+
|
|
122
|
+
def set_finished(self, finished: bool):
|
|
123
|
+
"""Setter for finished state
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
finished (bool): Sets whether the task is in finished state
|
|
127
|
+
"""
|
|
128
|
+
self.__finished = finished
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def state(self) -> str:
|
|
132
|
+
"""Returns with the current state's name
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
str: The current state's name
|
|
136
|
+
"""
|
|
137
|
+
return self.__state.name
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def configuration(self) -> TaskConfiguration:
|
|
141
|
+
"""Returns with the copy of the task's configuration
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
TaskConfiguration: The configuration
|
|
145
|
+
"""
|
|
146
|
+
return copy.deepcopy(self.__configuration)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def response(self) -> dict:
|
|
150
|
+
"""Gives the response for the server about the task state
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
dict: The task's state
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
with open("/host_hostname", "r") as file:
|
|
158
|
+
host_hostname = file.read().strip()
|
|
159
|
+
except (OSError, IOError):
|
|
160
|
+
self.logger.exception(
|
|
161
|
+
"Cannot get Host name (where the task ran). 'UNKNOWN_HOST_FROM_TASK' is set."
|
|
162
|
+
)
|
|
163
|
+
host_hostname = "UNKNOWN_HOST_FROM_TASK"
|
|
164
|
+
|
|
165
|
+
self.logger.info(f"The host's hostname is: {host_hostname}")
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"id": self.task_id,
|
|
169
|
+
"state": self.state,
|
|
170
|
+
"config": self.configuration.json,
|
|
171
|
+
"node": host_hostname,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def send_callbacks(self, calculation_result):
|
|
175
|
+
"""Sends a callback to both the scheduler and the callback url specified by the customer
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
calculation_result (any): The result of the calculation.
|
|
179
|
+
It can be the real result or an error too
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
self.__scheduler_callback.request_body = self.response
|
|
183
|
+
self.__scheduler_callback.request_body["result"] = calculation_result
|
|
184
|
+
self.__scheduler_callback.request_body["calculation_log"] = calculation_result["log"]
|
|
185
|
+
if self.__start_execution_time is not None:
|
|
186
|
+
self.__scheduler_callback.request_body["calculation_execution_time"] = (
|
|
187
|
+
time.time() - self.__start_execution_time
|
|
188
|
+
)
|
|
189
|
+
else:
|
|
190
|
+
self.__scheduler_callback.request_body["calculation_execution_time"] = "NOT_DEFINED"
|
|
191
|
+
|
|
192
|
+
if "error" in calculation_result:
|
|
193
|
+
self.__scheduler_callback.request_body["calculation_result"] = calculation_result[
|
|
194
|
+
"error"
|
|
195
|
+
]
|
|
196
|
+
elif "result" in calculation_result:
|
|
197
|
+
self.__scheduler_callback.request_body["calculation_result"] = calculation_result[
|
|
198
|
+
"result"
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
with self.__thread_lock:
|
|
202
|
+
self.__send_callback(self.__scheduler_callback)
|
|
203
|
+
|
|
204
|
+
if self.__configuration.callback.url:
|
|
205
|
+
self.__send_callback(self.__configuration.callback)
|
|
206
|
+
self.logger.info(
|
|
207
|
+
"Callback(s) has been sent-out successfully: {}".format(
|
|
208
|
+
self.__scheduler_callback.request_body["id"]
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def __send_callback(self, callback_param):
|
|
213
|
+
"""Sends a callback to the scheduler
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
callback_param (CallbackParameter): Response to be sent
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
TaskCallbackFailedError: Raised when the callback cannot be successfully
|
|
220
|
+
sent after five retries
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
last_exception = None
|
|
224
|
+
response = None
|
|
225
|
+
|
|
226
|
+
for _ in range(5):
|
|
227
|
+
last_exception = None
|
|
228
|
+
response = None
|
|
229
|
+
time.sleep(random.uniform(0.2, 1))
|
|
230
|
+
self.logger.info("Start to send prepared request: {}".format(callback_param))
|
|
231
|
+
try:
|
|
232
|
+
# with requests.Session() as session:
|
|
233
|
+
# request = requests.Request(
|
|
234
|
+
# callback_param.method,
|
|
235
|
+
# callback_param.url,
|
|
236
|
+
# json=callback_param.request_body,
|
|
237
|
+
# headers=callback_param.headers,
|
|
238
|
+
# )
|
|
239
|
+
#
|
|
240
|
+
# prepared = session.prepare_request(request)
|
|
241
|
+
# response = session.send(prepared)
|
|
242
|
+
|
|
243
|
+
# Use BlackAnt HTTPClient for better request handling with connection pooling
|
|
244
|
+
# and authentication support, fallback to direct requests if not available
|
|
245
|
+
if HTTPClient:
|
|
246
|
+
try:
|
|
247
|
+
# Parse URL to extract base_url and endpoint
|
|
248
|
+
parsed_url = urlparse(callback_param.url)
|
|
249
|
+
base_url = f"{parsed_url.scheme}://{parsed_url.netloc}"
|
|
250
|
+
endpoint = parsed_url.path or "/"
|
|
251
|
+
|
|
252
|
+
# Create HTTP client instance
|
|
253
|
+
http_client = HTTPClient(base_url=base_url)
|
|
254
|
+
|
|
255
|
+
# Send request through BlackAnt HTTPClient
|
|
256
|
+
response = http_client.send_request(
|
|
257
|
+
endpoint=endpoint,
|
|
258
|
+
method=callback_param.method,
|
|
259
|
+
json=callback_param.request_body,
|
|
260
|
+
anonymous=True # Callback doesn't need authentication
|
|
261
|
+
)
|
|
262
|
+
except Exception as client_error:
|
|
263
|
+
self.logger.warning(f"HTTPClient failed, falling back to requests: {client_error}")
|
|
264
|
+
# Fallback to original requests implementation
|
|
265
|
+
response = requests.request(
|
|
266
|
+
callback_param.method,
|
|
267
|
+
callback_param.url,
|
|
268
|
+
json=callback_param.request_body,
|
|
269
|
+
headers=callback_param.headers,
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
# Fallback to original requests implementation
|
|
273
|
+
response = requests.request(
|
|
274
|
+
callback_param.method,
|
|
275
|
+
callback_param.url,
|
|
276
|
+
json=callback_param.request_body,
|
|
277
|
+
headers=callback_param.headers,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if response.status_code != 200:
|
|
281
|
+
self.logger.warning(
|
|
282
|
+
"Could not send callback. Status code: {}. Response: {}".format(
|
|
283
|
+
response.status_code, response.text
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
time.sleep(random.uniform(0, 1))
|
|
287
|
+
continue
|
|
288
|
+
|
|
289
|
+
self.logger.info(
|
|
290
|
+
"Response status code: {} ; Response text: {}".format(
|
|
291
|
+
response.status_code, response.text
|
|
292
|
+
)
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
task_id_in_response = json.loads(response.request.body)["id"]
|
|
296
|
+
|
|
297
|
+
if task_id_in_response != callback_param.request_body["id"]:
|
|
298
|
+
self.logger.warning(
|
|
299
|
+
"The Task ID in the request "
|
|
300
|
+
"and response are not the same (<response> - <request>): "
|
|
301
|
+
"{} - {}".format(task_id_in_response, callback_param.request_body["id"])
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return response
|
|
305
|
+
|
|
306
|
+
# The too-broad exception warning is suppressed because we want to handle exceptions
|
|
307
|
+
except Exception as ex: # pylint: disable=broad-except
|
|
308
|
+
self.logger.warning("Could not send callback")
|
|
309
|
+
last_exception = ex
|
|
310
|
+
time.sleep(random.uniform(0, 1))
|
|
311
|
+
|
|
312
|
+
if last_exception:
|
|
313
|
+
raise TaskCallbackFailedError(last_exception)
|
|
314
|
+
if not response:
|
|
315
|
+
raise TaskCallbackFailedError(
|
|
316
|
+
"Cannot send out the callback. Please see the previous part of log further details"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if response.status_code != 200:
|
|
320
|
+
raise TaskCallbackFailedError(
|
|
321
|
+
"Status code: {} , Response: {}".format(response.status_code, response.text)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def run(self):
|
|
325
|
+
"""Starts the executor thread"""
|
|
326
|
+
self.__executor.start()
|
|
327
|
+
|
|
328
|
+
def stop(self):
|
|
329
|
+
"""Sends a stop signal to the calculation"""
|
|
330
|
+
self.calculation.stop()
|
|
331
|
+
|
|
332
|
+
def __execute_state_machine(self):
|
|
333
|
+
"""Runs the internal state machine until it does not reach a finished state"""
|
|
334
|
+
# The below 2-3 sec sleep is necessary because sometimes the return statement of POST method
|
|
335
|
+
# is faster than the result request.
|
|
336
|
+
# It means that the Task ID is not available in the DB when the callback handler
|
|
337
|
+
# gets the request about the result of the calculation.
|
|
338
|
+
# The Task running is executed on a separated thread, so it's independent of the POST
|
|
339
|
+
# method. This is the reason why the POST response is faster than Result request.
|
|
340
|
+
time.sleep(random.uniform(2, 3))
|
|
341
|
+
|
|
342
|
+
with redirect_stdout_stderr_to_logger(self.logger):
|
|
343
|
+
while not self.__finished:
|
|
344
|
+
try:
|
|
345
|
+
self.__start_execution_time = time.time()
|
|
346
|
+
self.__state.run()
|
|
347
|
+
# The too-broad exception warning is suppressed because we want to handle exceptions
|
|
348
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
349
|
+
if self.__state.name != "error":
|
|
350
|
+
self.__state = TaskErrorState(self)
|
|
351
|
+
self.logger.error("Unhandled exception caught, going to error state")
|
|
352
|
+
self.logger.exception(exc)
|
|
353
|
+
else:
|
|
354
|
+
self.logger.critical(
|
|
355
|
+
"Unhandled exception caught during error state. Terminating..."
|
|
356
|
+
)
|
|
357
|
+
self.logger.exception(exc)
|
|
358
|
+
return
|