Package: pynisher
Pynisher is a library to limit resources of a function call in a synchronous manner.
You can use this to ensure that your function doesn't use up more resources than it
should.UsageLimit the time a process can takeimportpynisherdefsleepy(x:int)->int:time.sleep(x)returnx# You can also use `cpu_time` insteadwithpynisher.limit(sleepy,wall_time=7)aslimited_sleep:x=limited_sleep(10)# Will raise a TimeoutExceptionLimit the memory usage in a sequential mannerfrompynisherimportlimit,MemoryLimitException,WallTimeoutExceptiondeftrain_memory_hungry_model(X,y)->Model:# ... do some thingreturnmodelmodel_trainer=limit(train_memory_hungry_model,memory=(500,"MB"),wall_time=(1.5,"h")# 1h30m)try:model=model_trainer(X,y)except(WallTimeoutException,MemoryLimitException):model=NonePassingraises=Falsemeans it will hide all errors and will returnEMPTYif
there is no result to give back.frompynisherimportlimit,EMPTYdeff():raiseValueError()limited_f=limit(f,wall_time=(2,"m"),raises=False)result=limited_f()ifresultisnotEMPTY:# ...You can even use the decorator, in which case it will always be limited.
Please note inDetailsthat support for this is limited and mostly
for Linux.frompynisherimportrestricted@restricted(wall_time=1,raises=False)defnotify_remote_server()->Response:"""We don't care that this fails, just give it a second to try"""server=block_until_access(...)response=server.notify()notify_remote_server()# ... continue on even if it failedYou can safely raise errors from inside your function and the same kind of error will be reraised
with a traceback.frompynisherimportlimitdeff():raiseValueError()limited_f=limit(f)try:limited_f()exceptValueErrorase:...# do what you needIf returning very large items, prefer to save them to file first and then read the result as
sending large objects through pipes can be very slow.frompathlibimportPathimportpicklefrompynisherimportlimitdeftrain_gpt3(save_path:Path)->bool:gpt3=...gpt3.train()withsave_path.open('wb')asf:pickle.dump(gpt3,f)returnTruepath=Path('gpt3.model')trainer=limit(train_gpt3,memory=(1_000_000,"gb")):try:trainer(save_path=path)withpath.open("rb")asf:gpt3=pickle.load(f)exceptMemoryLimitExceptionase:...DetailsPynisher works by running your function inside of a subprocess.
Once in the subprocess, the resources will be limited for that process before running your
function. The methods for limiting specific resources can be found within the respectivepynisher/limiters/<platform>.py.FeaturesTo check if a feature is supported on your system:frompynisherimportlimitforlimitin["cpu_time","wall_time","memory","decorator"]:print(f"Supports{limit}-{supports(limit)}")limited_f=limit(f,...)ifnotlimited_f.supports("memory"):...Currently we mainly support Linux with partial support for Mac and Windows:OSwall_timecpu_timememory@restrictedLinux:heavy_check_mark::heavy_check_mark::heavy_check_mark::heavy_check_mark:Windows:heavy_check_mark::heavy_check_mark: (1.):heavy_check_mark: (1.):x:  (3.)Mac:heavy_check_mark::heavy_check_mark: (4.):x: (2.):x:  (3.)Limiting memory and cputime on Windows is done with the librarypywin32. There seem
to be installation issues when instead of usingconda install <x>, you usepip install <x>inside a conda environment, specifically only withPython 3.8andPython 3.9.
The workaround is to instead installpywin32with conda, which can be done withpip uninstall pywin32; conda install pywin32.
Please see thisissuefor updates.Mac doesn't seem to allow for limiting a processes memory. No workaround has been found
including tryinglaunchctlwhich seems global and ignores memory limiting. Possiblyulimitcould work but needs to be tested. Usingsetrlimit(RLIMIT_AS, (soft, hard))does nothing
and will either fail explicitly or silently, hence we advertise it is not supported.
However, passing a memory limit on mac is still possible but may not do anything useful or
even raise an error. If you are aware of a solution, please let us know.This is something due to how multiprocessing pickling protocols work, hence@restricted(...)does
not work for your Mac/Windows. Please use thelimitmethod of limiting resources in this case.
(Technically this is supported for Mac Python 3.7 though). This is likely due to the defaultspawncontext for Windows and Mac but using other available methods on Mac also seems to not work.
For Linux, theforkandforkservercontext seems to work.For unknown reasons, usingtime.process_time()to query the cpu time usage within a pynished function
will cause thecpu_timelimits to be ignored on Mac, leading to a function that will hang indefinitly
unless using some other limit. Please let us know if this is some known issue or any workarounds are
available.ParametersThe full list of options available with bothlimitand@restrictedare:# The name given to the multiprocessing.Processname:str|None=None# The memory limit to place. Specify the amount of bytes or (int, unit) where unit# can be "B", "KB", "MB" or "GB"memory:int|tuple[int,str]|None=None# The cpu time in seconds to limit the process to. This time is only counted while the# process is active.# Can provide in (time, units) such as (1.5, "h") to indicate one and a half hours.# Units available are "s", "m", "h"cpu_time:int|tuple[float,str]|None=None# The wall time in seconds to limit the process to# Can provide in (time, units) such as (1.5, "h") to indicate one and a half hours.# Units available are "s", "m", "h"wall_time:int|tuple[float,str]|None=None# Whether to throw any errors that occured in the subprocess or to silently# throw them away. If `True` and an Error was raised, `None` will be returned.# The errors raised in the subprocess will be the same type that are raised in# the controlling process. The exception to this are MemoryErrors which occur# in the subprocess, we convert these to MemoryLimitException.raises:bool=True# This is the multiprocess context used, please refer to their documentation# https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methodscontext:"fork"|"spawn"|"forkserver"|BaseContext|None=None# Whether to emit warnings from  limit or not. The current warnings:# * When the memory limit is lower than the starting memory of a process# * When trying to remove the memory limit for sending back information#   from the subprocess to the main processwarnings:bool=True# How to handle errors. If `bool` then this decides whether or not to wrap them in# a pynisher exception. If `list`, you can specify which errors get wrapped in a# pynisher exception and if `dict`, you can specify what kind of errors get wrapped# and how. See `pynisher::Pynisher::__init__` for more details on `dict`## * wrap_errors={ "memory": [ImportError, (OSError, 22)], "pynisher": [ValueError] }## We check that the exception is explicitly of the same type and not just a subclass.# This is to prevent accidentally wrapping to eagerly.wrap_errors:bool|list[Exception]|dict=False# Whether to terminate child processes of your limited function.# By default, pynisher will kill any subprocesses your function may spawn. If this# is not desired behaviour, please use `daemon=True` with your spawned subprocesses# and set `terminate_child_processes` to `False`terminate_child_processes:bool=True# Whether keyboard interrupts should forceably kill any subprocess or the# pynished function. If True, it will temrinate the process tree of# the pynished function and then reraise the KeyboardInterrupt.forceful_keyboard_interrupt:bool=TrueExceptionsPynisher will let all subprocessExceptionsbuble up to the controlling process.
If a subprocess exceeds a limit, one ofCpuTimeoutException,WallTimeoutExceptionorMemoryLimitExceptionare raised, but you can use their base classes to cover them more generally.classPynisherException(Exception):..."""When a subprocess exceeds a limit"""classTimeoutException(PynisherException):..."""When a subprocess exceeds a time limit (walltime or cputime)"""classCpuTimeoutException(TimeoutException):..."""When a subprocess exceeds its cpu time limit"""classWallTimeoutException(TimeoutException):"""When a subprocess exceeds its wall time limit"""classMemoryLimitException(PynisherException,MemoryError):"""When a subprocess tries to allocate memory that would take it over the limitThis also inherits from MemoryError as it is technically a MemoryError that wecatch and convert."""Changes from v0.6.0For simplicity, pynisher will no longer try to controlstdout,stderr, instead
users can use the builtinsredirect_stdoutandredirect_stderrof Python to
send things as needed.Pynisher issues warnings throughstderr. Depending on how you set up thecontextto spawn a new process, using objects may now work as intended. The safest option
is to write to a file if needed.fromcontextlibimportredirect_stderr# You can always disable warningslimited_f=limit(func,warnings=False)# Capture warnings in a file# Only seems to work properly on Linuxwithopen("stderr.txt","w")asstderr,redirect_stderr(stderr):limited_f()withopen("stderr.txt","r")asstderr:print(stderr.readlines())The support for passing aloggertoPynisherhas also been removed. The only diagnostics
information that would have been sent to the logger is not communicated with prints tostderr.
These diagnostic messages only occur when an attempt to limit resources failed
This can be captured or disabled as above.Any other kind of issue will raise an exception with relevant information.The support for checkingexit_statuswas removed and the success of a pynisher process can
be handled in the usual Python manner of checking for errors, with atry: except:. If you
don't care for theexit_statusthen usef = limit(func, raises=False)and you can
check for outputoutput = f(...). This will beNoneif an error was raised and wasraises=False.Pynisher no longer times your function for you withself.wall_clock_time. If you need to measure
the duration it ran, please do so outside ofPynisher.The exceptions were also changed, please seeExceptionsControlling namespace pollutionAs an advanced use case, sometimes you might want to keep the modules imported for your
limited function to be local only, preventing this from leaking to the main process that
runs created the limited function. You have three ways to control that the locally imported
error does not pollute the main namespace.importsysfrompynisherimportPynisherException,limitdefimport_sklearn()->None:"""Imports sklearn into a local namespace and has an sklearn object in its args"""fromsklearn.exceptionsimportNotFittedErrorfromsklearn.svmimportSVRassert"sklearn"insys.modules.keys()raiseNotFittedError(SVR())if__name__=="__main__":# Wrapping all errorslf=limit(import_sklearn,wrap_errors=True)try:lf()exceptPynisherException:assert"sklearn"notinsys.modules.keys()# Wrapping only specific errorslf=limit(import_sklearn,wrap_errors=["NotFittedError"])try:lf()exceptPynisherException:assert"sklearn"notinsys.modules.keys()# Wrapping that error specifically as a PynisherExceptionlf=limit(import_sklearn,wrap_errors={"pynisher":["NotFittedError"]})try:lf()exceptPynisherException:assert"sklearn"notinsys.modules.keys()Pynisher and MultithreadingWhen Pynisher is used together with the Python Threading library, it is possible to run into
a deadlock when using the standardforkmethod to start new processes as described inhttps://github.com/Delgan/loguru/issues/231https://gist.github.com/mfm24/e62ec5d50c672524107ca00a391e6104https://github.com/dask/dask/issues/3759One way of solving this would be to change the forking behavior as describedhere <https://github.com/google/python-atfork/blob/main/atfork/stdlib_fixer.py>, but this is
also makes very strong assumptions on how the code is executed. An alternative is passing aContext <https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods>which uses eitherspawnorforkserveras the process startup method.Nested Pynisher and Multiprocessing contextsBe careful when using multiple contexts for multiprocessing while usingpynisher. If your
pynished function spawns subprocess using"forkserver"while you setpynisherto use
the context"fork", then issues can begin to occur when terminate processes.Project originThis repository is based on Stefan Falkner'shttps://github.com/sfalkner/pynisher.