autograder.util.invoke
1import multiprocessing 2import sys 3import time 4import traceback 5import typing 6 7REAP_TIME_SEC: float = 5 8 9_multiprocessing_initialized: bool = False # pylint: disable=invalid-name 10 11def _init_multiprocessing() -> None: 12 """ 13 Initialize Python multiprocessing. 14 Note that this should only be called on Linux code paths. 15 """ 16 17 global _multiprocessing_initialized # pylint: disable=global-statement 18 19 if (_multiprocessing_initialized): 20 return 21 22 multiprocessing.set_start_method('fork') 23 _multiprocessing_initialized = True 24 25def with_timeout(timeout: typing.Union[float, None], function: typing.Callable) -> typing.Tuple[bool, typing.Any]: 26 """ 27 Run the given function in a different process with the given timeout. 28 If the timeout is None, then no timeout will be checked (and the code will be run on the same process). 29 30 Return: (success, function return value) 31 On timeout, success will be false and the value will be None. 32 On error, success will be false and value will be the string stacktrace. 33 On successful completion, success will be true and value may be None (if nothing was returned). 34 """ 35 36 if ((timeout is None) or (not sys.platform.startswith('linux'))): 37 # Mac and Windows have some pickling issues with multiprocessing. 38 # Just run them without a timeout. 39 # Any autograder will be run on a Linux machine and will be safe. 40 start_time = time.time() 41 value = function() 42 runtime = time.time() - start_time 43 44 if ((timeout is not None) and (runtime > timeout)): 45 return (False, None) 46 47 return (True, value) 48 49 _init_multiprocessing() 50 51 result: multiprocessing.Queue = multiprocessing.Queue(1) 52 53 # Note that we use processes instead of threads so they can be more completely killed. 54 process = multiprocessing.Process(target = _invoke_helper, args = (result, function)) 55 process.start() 56 57 # Wait for at most the timeout. 58 process.join(timeout) 59 60 # Check to see if the process is still running. 61 if (process.is_alive()): 62 # Kill the long-running process. 63 process.terminate() 64 65 # Try to reap the process once before just giving up on it. 66 process.join(REAP_TIME_SEC) 67 68 return (False, None) 69 70 # Check to see if the process explicitly existed (like via sys.exit()). 71 if (result.empty()): 72 return (False, 'Code explicitly exited (like via sys.exit()).') 73 74 value, error = result.get() 75 76 if (error is not None): 77 _, stacktrace = error 78 return (False, stacktrace) 79 80 return (True, value) 81 82def _invoke_helper(result: multiprocessing.Queue, function: typing.Callable) -> None: 83 """ A helper function for running the given function. """ 84 85 value = None 86 error = None 87 88 try: 89 value = function() 90 except Exception as ex: 91 error = (ex, traceback.format_exc()) 92 93 sys.stdout.flush() 94 95 result.put((value, error)) 96 result.close()
REAP_TIME_SEC: float =
5
def
with_timeout(timeout: Optional[float], function: Callable) -> Tuple[bool, Any]:
26def with_timeout(timeout: typing.Union[float, None], function: typing.Callable) -> typing.Tuple[bool, typing.Any]: 27 """ 28 Run the given function in a different process with the given timeout. 29 If the timeout is None, then no timeout will be checked (and the code will be run on the same process). 30 31 Return: (success, function return value) 32 On timeout, success will be false and the value will be None. 33 On error, success will be false and value will be the string stacktrace. 34 On successful completion, success will be true and value may be None (if nothing was returned). 35 """ 36 37 if ((timeout is None) or (not sys.platform.startswith('linux'))): 38 # Mac and Windows have some pickling issues with multiprocessing. 39 # Just run them without a timeout. 40 # Any autograder will be run on a Linux machine and will be safe. 41 start_time = time.time() 42 value = function() 43 runtime = time.time() - start_time 44 45 if ((timeout is not None) and (runtime > timeout)): 46 return (False, None) 47 48 return (True, value) 49 50 _init_multiprocessing() 51 52 result: multiprocessing.Queue = multiprocessing.Queue(1) 53 54 # Note that we use processes instead of threads so they can be more completely killed. 55 process = multiprocessing.Process(target = _invoke_helper, args = (result, function)) 56 process.start() 57 58 # Wait for at most the timeout. 59 process.join(timeout) 60 61 # Check to see if the process is still running. 62 if (process.is_alive()): 63 # Kill the long-running process. 64 process.terminate() 65 66 # Try to reap the process once before just giving up on it. 67 process.join(REAP_TIME_SEC) 68 69 return (False, None) 70 71 # Check to see if the process explicitly existed (like via sys.exit()). 72 if (result.empty()): 73 return (False, 'Code explicitly exited (like via sys.exit()).') 74 75 value, error = result.get() 76 77 if (error is not None): 78 _, stacktrace = error 79 return (False, stacktrace) 80 81 return (True, value)
Run the given function in a different process with the given timeout. If the timeout is None, then no timeout will be checked (and the code will be run on the same process).
Return: (success, function return value) On timeout, success will be false and the value will be None. On error, success will be false and value will be the string stacktrace. On successful completion, success will be true and value may be None (if nothing was returned).