-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: (opt-in): terminate handling of work when the request has alrea…
…dy timed out (#328) Overhead-free (or at least very cheap). The “timeout” gunicorn config means drastically different things for sync and non-sync workers: > Workers silent for more than this many seconds are killed and restarted. > > Value is a positive number or 0. Setting it to 0 has the effect of > infinite timeouts by disabling timeouts for all workers entirely. > > Generally, the default of thirty seconds should suffice. Only set this > noticeably higher if you’re sure of the repercussions for sync workers. > For the non sync workers it just means that the worker process is still > communicating and is not tied to the length of time required to handle a > single request. So. For cases where threads = 1 (user set or our defaults), we’ll use the sync worker and let the regular timeout functionality do its thing. For cases where threads > 1, we’re using the gthread worker, and timeout means something completely different and not really user-observable. So we’ll leave the communication timeout (default gunicorn “timeout”) at 30 seconds, but create our own gthread-derived worker class to use instead, which terminates request handling (with no mind to gunicorn’s “graceful shutdown” config), to emulate GCFv1. The arbiter spawns these workers, so we have to maintain some sort of global timeout state for us to read in our custom gthread worker. In the future, we should consider letting the user adjust the graceful shutdown seconds. But the default of 30 seems like it’s worked fine historically, so it’s hard to argue for changing it. IIUC, this means that on gen 2, there’s a small behavior difference for the sync workers compared to gen 1, in that gen 2 sync worker workloads will get an extra 30 seconds of timeout to gracefully shut down. I don’t think monkeying with this config and opting-in to sync workers is very common, though, so let’s not worry about it here; everyone should be on the gthread path outlined above. * fix tests * small test fixes give up on coverage support for things that are tested in different processes, or in gthread, because it looks like pytest-cov gave up on support for these, where as coverage has out-of-the-box support * format * isort everything * skip tests on mac there's something test-specific about how mac pickles functions for execution in multiprocessing.Process which is causing problems. it seems somewhere in the innards of flask and gunicorn and macos... since this feature is opt-in anyway, let's just skip testing darwin. * sort tuple of dicts in async tests before asserting causes flakes sometimes in workflows * use double-quotes * also skip tests on windows - this is all built for gunicorn, there's no value adding it for windows anyway * skip import on windows * easy stuff * add a few tests for sync worker timeouts these shouldn't have changed with this commit
- Loading branch information
Showing
9 changed files
with
381 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import logging | ||
import time | ||
|
||
import functions_framework | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
@functions_framework.http | ||
def main(request): | ||
timeout = 2 | ||
for _ in range(timeout * 10): | ||
time.sleep(0.1) | ||
logger.info("logging message after timeout elapsed") | ||
return "Hello, world!" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import ctypes | ||
import logging | ||
import threading | ||
|
||
from .exceptions import RequestTimeoutException | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ThreadingTimeout(object): # pragma: no cover | ||
def __init__(self, seconds): | ||
self.seconds = seconds | ||
self.target_tid = threading.current_thread().ident | ||
self.timer = None | ||
|
||
def __enter__(self): | ||
self.timer = threading.Timer(self.seconds, self._raise_exc) | ||
self.timer.start() | ||
return self | ||
|
||
def __exit__(self, exc_type, exc_val, exc_tb): | ||
self.timer.cancel() | ||
if exc_type is RequestTimeoutException: | ||
logger.warning( | ||
"Request handling exceeded {0} seconds timeout; terminating request handling...".format( | ||
self.seconds | ||
), | ||
exc_info=(exc_type, exc_val, exc_tb), | ||
) | ||
return False | ||
|
||
def _raise_exc(self): | ||
ret = ctypes.pythonapi.PyThreadState_SetAsyncExc( | ||
ctypes.c_long(self.target_tid), ctypes.py_object(RequestTimeoutException) | ||
) | ||
if ret == 0: | ||
raise ValueError("Invalid thread ID {}".format(self.target_tid)) | ||
elif ret > 1: | ||
ctypes.pythonapi.PyThreadState_SetAsyncExc( | ||
ctypes.c_long(self.target_tid), None | ||
) | ||
raise SystemError("PyThreadState_SetAsyncExc failed") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import logging | ||
import time | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def function(request): | ||
# sleep for 1200 total ms (1.2 sec) | ||
for _ in range(12): | ||
time.sleep(0.1) | ||
logger.info("some extra logging message") | ||
return "success", 200 |
Oops, something went wrong.