Repository navigation
Closed
Labels
bugSomething is brokenSomething is brokendeadlockThe cluster appears to not make any progressThe cluster appears to not make any progressstabilityIssue or feature related to cluster stability (e.g. deadlock)Issue or feature related to cluster stability (e.g. deadlock)
Description
What happened:
If a task raises a BaseException (KeyboardInterrupt, SystemExit, etc.), the task will appear to be processing forever.
What you expected to happen:
The task should definitely not deadlock. But what should actually happen, I'm not sure. Could go two ways:
- The entire worker should shut down gracefully.
- We should catch them just like any other exceptions and error the task.
More discussion in comments.
Minimal Complete Verifiable Example:
In [1]: import distributed
In [2]: client = distributed.Client(n_workers=1)
In [3]: def raiser():
...: raise BaseException("this could be a KeyboardInterrupt!")
...:
In [4]: f = client.submit(raiser)
In [5]: Exception in callback IOLoop.add_future.<locals>.<lambda>(<Task finishe...dInterrupt!')>) at /Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/site-packages/tornado/ioloop.py:688
handle: <Handle IOLoop.add_future.<locals>.<lambda>(<Task finishe...dInterrupt!')>) at /Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/site-packages/tornado/ioloop.py:688>
Traceback (most recent call last):
File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/asyncio/events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/site-packages/tornado/ioloop.py", line 688, in <lambda>
lambda f: self._run_callback(functools.partial(callback, future))
File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/site-packages/tornado/ioloop.py", line 741, in _run_callback
ret = callback()
File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/site-packages/tornado/ioloop.py", line 765, in _discard_future_result
future.result()
File "/Users/gabe/dev/distributed/distributed/worker.py", line 3504, in execute
result = await self.loop.run_in_executor(
File "/Users/gabe/dev/distributed/distributed/_concurrent_futures_thread.py", line 65, in run
result = self.fn(*self.args, **self.kwargs)
File "/Users/gabe/dev/distributed/distributed/worker.py", line 4503, in apply_function
msg = apply_function_simple(function, args, kwargs, time_delay)
File "/Users/gabe/dev/distributed/distributed/worker.py", line 4525, in apply_function_simple
result = function(*args, **kwargs)
File "<ipython-input-3-7f701e80695b>", line 2, in raiser
BaseException: this could be a KeyboardInterrupt!
In [5]:
In [5]: client.processing()
Out[5]: {'tcp://127.0.0.1:58316': ('raiser-e3b7ab59305f9e4ddb4ecddd75c55f85',)}
In [6]: client.call_stack()
Out[6]:
{'tcp://127.0.0.1:58316': {'raiser-e3b7ab59305f9e4ddb4ecddd75c55f85': (' File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/threading.py", line 912, in _bootstrap\n\tself._bootstrap_inner()\n',
' File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/threading.py", line 954, in _bootstrap_inner\n\tself.run()\n',
' File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/threading.py", line 892, in run\n\tself._target(*self._args, **self._kwargs)\n',
' File "/Users/gabe/dev/distributed/distributed/threadpoolexecutor.py", line 51, in _worker\n\ttask = work_queue.get(timeout=1)\n',
' File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/queue.py", line 180, in get\n\tself.not_empty.wait(remaining)\n',
' File "/Users/gabe/miniconda3/envs/dask-distributed/lib/python3.9/threading.py", line 316, in wait\n\tgotit = waiter.acquire(True, timeout)\n')}}
In [7]: client.submit(lambda: 1).result(timeout=5) # the worker still works fine; just that task is stuck now
Out[7]: 1Anything else we need to know?:
Environment:
- Dask version: 2022.2.1
- Python version: 3.9.5
- Operating System: macOS
- Install method (conda, pip, source): source
Reacted by Oliver Mannion
Metadata
Metadata
Assignees
Labels
bugSomething is brokenSomething is brokendeadlockThe cluster appears to not make any progressThe cluster appears to not make any progressstabilityIssue or feature related to cluster stability (e.g. deadlock)Issue or feature related to cluster stability (e.g. deadlock)
Activity
gjoseph92 commentedon Mar 17, 2022
Arguments for the two possible ways we could handle
BaseExceptions from tasks:The entire worker should shut down gracefully.
The
BaseExceptionshould be propagated back to the event loop, and handled as usual there (which hopefully would mean shutting down gracefully; we're known to not handle KeyboardInterrupts that well though).Rationale: Python explicitly states that
BaseExceptionsshould not be considered user errors:Furthermore, Python says this about normal
Exception:That seems to imply that
BaseExceptionis considered a "system-exiting exception".So a
BaseExceptionindicates a system/process-level problem, therefore the process overall should handle the exception. We should propagate this from the thread and re-raise it in the event loop.We should catch them just like any other exceptions
Just change this to
BaseException:distributed/distributed/worker.py
Lines 4526 to 4529 in 1b4d13f
Rationale: if a
BaseExceptionis coming from a OS signal, the event loop would be the one to receive it. So if it's happening in a task, that means user code must be causing it. Which is silly, so we should treat it like any other user error.These are the possible
BaseExceptions that could be raised (based on https://docs.python.org/3/library/exceptions.html#exception-hierarchy):KeyboardInterrupt: main one I'd worry about. Raised when the process receives SIGINT, by asignalhander that's registered by default.However, if the process actually gets SIGINT'd by the OS,
KeyboardInterruptwill raised in the main thread (unlike in C):So if a
KeyboardInterruptis coming from a task, I think that means user code is actually callingraise KeyboardInterrupt, so in principle it's a user exception.SystemExit: raised bysys.exit().sys.exit()is basically just a wrapper aroundraise SystemExit. You could argue this is equivalent to user code intentionally sayingraise SystemExit, so it's a user exception. (If code within the worker event loop calledsys.exit(), I don't think the exception would show up within a thread.) Whether or not user code should be able to shut down the whole worker viasys.exit()is a reasonable debate.GeneratorExit: no reason why this should even come out of user code, but it's definitely not a reason to shut down the whole worker.Some user-defined subclass of
BaseException(or plainBaseExceptionitself): clearly, this is a user raising this, so we can consider it a user error.crusaderky commentedon Mar 17, 2022
asyncio.CancelledError is also a BaseException and should be considered carefully.
gjoseph92 commentedon Mar 17, 2022
A synchronous task running in a tread raising
CancelledErrorwould be... odd. But likely not a reason to re-raiseCancelledErrorin the main worker event loop.fjetter commentedon Mar 21, 2022
It's not necessarily about the task itself but about the entire code surrounding it. Right now, we have a big block around the entire
Worker.executeand handle anyExceptionin this block the same.CancelledErrormust be dealt with separately. I also thinkSystemExitneeds special treatment. I'm not entirely sure aboutKeyboardInterruptgjoseph92 commentedon Mar 22, 2022
I think it is just about the task. We're talking about an error raised by task code within the threadpool here, within
apply_function_simple:distributed/distributed/worker.py
Lines 4526 to 4529 in 1b4d13f
I'd be against special-casing certain
BaseExceptions coming from tasks (KeyboardInterruptvsCancelledError, etc.). I think we should either propagate allBaseExceptions from the threadpool back toWorker.execute, or catch all of them and treat them just like any other exception raised in a task (error the task).I'm currently leaning towards treating
BaseExceptions just like any other user error and catching them. I feel confident that they all indicate users are just doing something weird, not that there's any external signal that the worker should stop.SystemExitcould be the one exception, but task code really should not be able to expectsys.exit()will shut down the whole worker (what if you're using aProcessPoolExecutorinstead of threads?).fjetter commentedon Mar 23, 2022
yes, my point is that we need the other exception handling in there as well. we previously missed a lot of the code and that also caused us to deadlock
ProcessPoolExecutor are weird and should not be our primary concern, see
#5128 (comment)
gjoseph92 commentedon Mar 24, 2022
To me this try/except in
Worker.executeis a separate topic. I'm surprised it's so broad actually. I get that there are a couple places that could raise errors due to user input (self.executors[executor],_maybe_deserialize_task), but I'd prefer to see tighter-scoped try/excepts around those specific places. Having that one big try/except could also incorrectly catch state-machine errors and treat them like user errors.I wonder if part of the reason we have this giant try/except is because
Worker.executingonly ever gets called vialoop.add_callbackhere and here. So if something goes wrong, the callback will just silently fail and the exception will be lost to the void of Tornado. These are the consequences of unstructured concurrency 😁 .Reading the traceback I posted again, I realize this is the actual problem.
await self.loop.run_in_executoris properly re-raising theBaseExceptionback intoWorker.execute. AndWorker.executeis properly letting thatBaseExceptionpropagate (just as we'd want to let aSystemExitorKeyboardInterruptpropagate). The problem is that because we're using Tornado callbacks, there's nothing aboveWorker.executeto handle the error. Tornado just prints thatException in callbackhappened and moves on as though nothing went wrong. cc @graingert—another reason why Trio is great for not letting you do stuff like this!For this issue, I still think we should handle
BaseExceptionwithinapply_function_simplethough and treat it as user error. That's kind of whatconcurrent.futures.ThreadPoolExecutordoes.