-
-
Notifications
You must be signed in to change notification settings - Fork 720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add fail_hard decorator for worker methods #6210
Changes from all commits
6cbc05e
6eeac6b
97511ed
73471b2
b2fa15b
a991d84
b33b411
43b2962
a0721aa
b59ccd0
a08b5dd
e1c7447
a0b6f10
ebe8886
a1399a1
0505df8
50c31ef
96f7bd2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -157,6 +157,72 @@ | |||||||||||||||||||||
DEFAULT_STARTUP_INFORMATION: dict[str, Callable[[Worker], Any]] = {} | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
||||||||||||||||||||||
def fail_hard(method): | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
Decorator to close the worker if this method encounters an exception. | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
if iscoroutinefunction(method): | ||||||||||||||||||||||
|
||||||||||||||||||||||
@functools.wraps(method) | ||||||||||||||||||||||
async def wrapper(self, *args, **kwargs): | ||||||||||||||||||||||
try: | ||||||||||||||||||||||
return await method(self, *args, **kwargs) | ||||||||||||||||||||||
except Exception as e: | ||||||||||||||||||||||
if self.status not in (Status.closed, Status.closing): | ||||||||||||||||||||||
self.log_event( | ||||||||||||||||||||||
"worker-fail-hard", | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
**error_message(e), | ||||||||||||||||||||||
"worker": self.address, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
logger.exception(e) | ||||||||||||||||||||||
await _force_close(self) | ||||||||||||||||||||||
|
||||||||||||||||||||||
else: | ||||||||||||||||||||||
|
||||||||||||||||||||||
@functools.wraps(method) | ||||||||||||||||||||||
def wrapper(self, *args, **kwargs): | ||||||||||||||||||||||
try: | ||||||||||||||||||||||
return method(self, *args, **kwargs) | ||||||||||||||||||||||
except Exception as e: | ||||||||||||||||||||||
if self.status not in (Status.closed, Status.closing): | ||||||||||||||||||||||
self.log_event( | ||||||||||||||||||||||
"worker-fail-hard", | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
**error_message(e), | ||||||||||||||||||||||
"worker": self.address, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
logger.exception(e) | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
self.loop.add_callback(_force_close, self) | ||||||||||||||||||||||
|
||||||||||||||||||||||
return wrapper | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
||||||||||||||||||||||
async def _force_close(self): | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
Used with the fail_hard decorator defined above | ||||||||||||||||||||||
|
||||||||||||||||||||||
1. Wait for a worker to close | ||||||||||||||||||||||
2. If it doesn't, log and kill the process | ||||||||||||||||||||||
""" | ||||||||||||||||||||||
try: | ||||||||||||||||||||||
await asyncio.wait_for(self.close(nanny=False, executor_wait=False), 30) | ||||||||||||||||||||||
fjetter marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
except (Exception, BaseException): # <-- include BaseException here or not?? | ||||||||||||||||||||||
# Worker is in a very broken state if closing fails. We need to shut down immediately, | ||||||||||||||||||||||
# to ensure things don't get even worse and this worker potentially deadlocks the cluster. | ||||||||||||||||||||||
logger.critical( | ||||||||||||||||||||||
"Error trying close worker in response to broken internal state. " | ||||||||||||||||||||||
"Forcibly exiting worker NOW", | ||||||||||||||||||||||
exc_info=True, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
# use `os._exit` instead of `sys.exit` because of uncertainty | ||||||||||||||||||||||
# around propagating `SystemExit` from asyncio callbacks | ||||||||||||||||||||||
os._exit(1) | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
||||||||||||||||||||||
class Worker(ServerNode): | ||||||||||||||||||||||
"""Worker node in a Dask distributed cluster | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
@@ -900,14 +966,15 @@ def logs(self): | |||||||||||||||||||||
return self._deque_handler.deque | ||||||||||||||||||||||
|
||||||||||||||||||||||
def log_event(self, topic, msg): | ||||||||||||||||||||||
self.loop.add_callback( | ||||||||||||||||||||||
self.batched_stream.send, | ||||||||||||||||||||||
{ | ||||||||||||||||||||||
"op": "log-event", | ||||||||||||||||||||||
"topic": topic, | ||||||||||||||||||||||
"msg": msg, | ||||||||||||||||||||||
}, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
full_msg = { | ||||||||||||||||||||||
"op": "log-event", | ||||||||||||||||||||||
"topic": topic, | ||||||||||||||||||||||
"msg": msg, | ||||||||||||||||||||||
} | ||||||||||||||||||||||
if self.thread_id == threading.get_ident(): | ||||||||||||||||||||||
self.batched_stream.send(full_msg) | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
self.loop.add_callback(self.batched_stream.send, full_msg) | ||||||||||||||||||||||
fjetter marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
||||||||||||||||||||||
@property | ||||||||||||||||||||||
def executing_count(self) -> int: | ||||||||||||||||||||||
|
@@ -1199,22 +1266,19 @@ async def heartbeat(self): | |||||||||||||||||||||
finally: | ||||||||||||||||||||||
self.heartbeat_active = False | ||||||||||||||||||||||
|
||||||||||||||||||||||
@fail_hard | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once the decorator is here, what's the point of having this block anymore? distributed/distributed/worker.py Lines 1201 to 1210 in 198522b
The decorator is going to close the worker as soon as the I think we want to either remove the try/except entirely from |
||||||||||||||||||||||
async def handle_scheduler(self, comm): | ||||||||||||||||||||||
try: | ||||||||||||||||||||||
await self.handle_stream(comm, every_cycle=[self.ensure_communicating]) | ||||||||||||||||||||||
except Exception as e: | ||||||||||||||||||||||
logger.exception(e) | ||||||||||||||||||||||
raise | ||||||||||||||||||||||
finally: | ||||||||||||||||||||||
if self.reconnect and self.status in Status.ANY_RUNNING: | ||||||||||||||||||||||
logger.info("Connection to scheduler broken. Reconnecting...") | ||||||||||||||||||||||
self.loop.add_callback(self.heartbeat) | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
logger.info( | ||||||||||||||||||||||
"Connection to scheduler broken. Closing without reporting. Status: %s", | ||||||||||||||||||||||
self.status, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
await self.close(report=False) | ||||||||||||||||||||||
await self.handle_stream(comm, every_cycle=[self.ensure_communicating]) | ||||||||||||||||||||||
fjetter marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
||||||||||||||||||||||
if self.reconnect and self.status in Status.ANY_RUNNING: | ||||||||||||||||||||||
logger.info("Connection to scheduler broken. Reconnecting...") | ||||||||||||||||||||||
self.loop.add_callback(self.heartbeat) | ||||||||||||||||||||||
else: | ||||||||||||||||||||||
logger.info( | ||||||||||||||||||||||
"Connection to scheduler broken. Closing without reporting. Status: %s", | ||||||||||||||||||||||
self.status, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
await self.close(report=False) | ||||||||||||||||||||||
fjetter marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||
|
||||||||||||||||||||||
async def upload_file(self, comm, filename=None, data=None, load=True): | ||||||||||||||||||||||
out_filename = os.path.join(self.local_directory, filename) | ||||||||||||||||||||||
|
@@ -1654,7 +1718,9 @@ async def get_data( | |||||||||||||||||||||
assert response == "OK", response | ||||||||||||||||||||||
except OSError: | ||||||||||||||||||||||
logger.exception( | ||||||||||||||||||||||
"failed during get data with %s -> %s", self.address, who, exc_info=True | ||||||||||||||||||||||
"failed during get data with %s -> %s", | ||||||||||||||||||||||
self.address, | ||||||||||||||||||||||
who, | ||||||||||||||||||||||
) | ||||||||||||||||||||||
comm.abort() | ||||||||||||||||||||||
raise | ||||||||||||||||||||||
|
@@ -2682,6 +2748,7 @@ def handle_stimulus(self, stim: StateMachineEvent) -> None: | |||||||||||||||||||||
self.transitions(recs, stimulus_id=stim.stimulus_id) | ||||||||||||||||||||||
self._handle_instructions(instructions) | ||||||||||||||||||||||
|
||||||||||||||||||||||
@fail_hard | ||||||||||||||||||||||
@log_errors | ||||||||||||||||||||||
def _handle_stimulus_from_task( | ||||||||||||||||||||||
self, task: asyncio.Task[StateMachineEvent | None] | ||||||||||||||||||||||
|
@@ -2695,6 +2762,7 @@ def _handle_stimulus_from_task( | |||||||||||||||||||||
if stim: | ||||||||||||||||||||||
self.handle_stimulus(stim) | ||||||||||||||||||||||
|
||||||||||||||||||||||
@fail_hard | ||||||||||||||||||||||
def _handle_instructions(self, instructions: Instructions) -> None: | ||||||||||||||||||||||
# TODO this method is temporary. | ||||||||||||||||||||||
# See final design: https://github.com/dask/distributed/issues/5894 | ||||||||||||||||||||||
|
@@ -3023,6 +3091,7 @@ def _update_metrics_received_data( | |||||||||||||||||||||
self.counters["transfer-count"].add(len(data)) | ||||||||||||||||||||||
self.incoming_count += 1 | ||||||||||||||||||||||
|
||||||||||||||||||||||
@fail_hard | ||||||||||||||||||||||
@log_errors | ||||||||||||||||||||||
async def gather_dep( | ||||||||||||||||||||||
self, | ||||||||||||||||||||||
|
@@ -3548,6 +3617,7 @@ def _ensure_computing(self) -> RecsInstrs: | |||||||||||||||||||||
|
||||||||||||||||||||||
return recs, [] | ||||||||||||||||||||||
|
||||||||||||||||||||||
@fail_hard | ||||||||||||||||||||||
async def execute(self, key: str, *, stimulus_id: str) -> StateMachineEvent | None: | ||||||||||||||||||||||
if self.status in {Status.closing, Status.closed, Status.closing_gracefully}: | ||||||||||||||||||||||
return None | ||||||||||||||||||||||
|
@@ -4107,7 +4177,6 @@ def validate_state(self): | |||||||||||||||||||||
|
||||||||||||||||||||||
except Exception as e: | ||||||||||||||||||||||
logger.error("Validate state failed. Closing.", exc_info=e) | ||||||||||||||||||||||
self.loop.add_callback(self.close) | ||||||||||||||||||||||
logger.exception(e) | ||||||||||||||||||||||
if LOG_PDB: | ||||||||||||||||||||||
import pdb | ||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would fix #5958 if you also wrapped
execute
(you should wrapexecute
either way I think).The fact that BaseExceptions in callbacks aren't propagated by Tornado is pretty crazy. If we're going to add manual support for propagating exceptions like this, I don't see why we'd let BaseExceptions be ignored.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK with catching BaseException for tasks, i.e. in
apply_function
, et al. to fix #5958However, I would be worried to close workers upon a
asyncio.CancelledError
. While I don't think we're using cancellation in many places right now, this would be a very confusing behavior if that ever changes.