Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 419 – Protecting cleanup statements from interruptions

Author:
Paul Colomiets <paul at colomiets.name>
Status:
Deferred
Type:
Standards Track
Created:
06-Apr-2012
Python-Version:
3.3

Table of Contents

Abstract

This PEP proposes a way to protect Python code from being interruptedinside a finally clause or during context manager cleanup.

PEP Deferral

Further exploration of the concepts covered in this PEP has been deferredfor lack of a current champion interested in promoting the goals of the PEPand collecting and incorporating feedback, and with sufficient availabletime to do so effectively.

Rationale

Python has two nice ways to do cleanup. One is afinallystatement and the other is a context manager (usually called using awith statement). However, neither is protected from interruptionbyKeyboardInterrupt orGeneratorExit caused bygenerator.throw(). For example:

lock.acquire()try:print('starting')do_something()finally:print('finished')lock.release()

IfKeyboardInterrupt occurs just after the secondprint()call, the lock will not be released. Similarly, the following codeusing thewith statement is affected:

fromthreadingimportLockclassMyLock:def__init__(self):self._lock_impl=Lock()def__enter__(self):self._lock_impl.acquire()print("LOCKED")def__exit__(self):print("UNLOCKING")self._lock_impl.release()lock=MyLock()withlock:do_something

IfKeyboardInterrupt occurs near any of theprint() calls, thelock will never be released.

Coroutine Use Case

A similar case occurs with coroutines. Usually coroutine librarieswant to interrupt the coroutine with a timeout. Thegenerator.throw() method works for this use case, but there is noway of knowing if the coroutine is currently suspended from inside afinally clause.

An example that uses yield-based coroutines follows. The code lookssimilar using any of the popular coroutine libraries Monocle[1],Bluelet[2], or Twisted[3].

defrun_locked():yieldconnection.sendall('LOCK')try:yielddo_something()yielddo_something_else()finally:yieldconnection.sendall('UNLOCK')withtimeout(5):yieldrun_locked()

In the example above,yieldsomething means to pause executing thecurrent coroutine and to execute coroutinesomething until itfinishes execution. Therefore, the coroutine library itself needs tomaintain a stack of generators. Theconnection.sendall() call waitsuntil the socket is writable and does a similar thing to whatsocket.sendall() does.

Thewith statement ensures that all code is executed within 5seconds timeout. It does so by registering a callback in the mainloop, which callsgenerator.throw() on the top-most frame in thecoroutine stack when a timeout happens.

Thegreenlets extension works in a similar way, except that itdoesn’t needyield to enter a new stack frame. Otherwiseconsiderations are similar.

Specification

Frame Flag ‘f_in_cleanup’

A new flag on the frame object is proposed. It is set toTrue ifthis frame is currently executing afinally clause. Internally,the flag must be implemented as a counter of nested finally statementscurrently being executed.

The internal counter also needs to be incremented during execution oftheSETUP_WITH andWITH_CLEANUP bytecodes, and decrementedwhen execution for these bytecodes is finished. This allows to alsoprotect__enter__() and__exit__() methods.

Function ‘sys.setcleanuphook’

A new function for thesys module is proposed. This function setsa callback which is executed every timef_in_cleanup becomesfalse. Callbacks get a frame object as their sole argument, so thatthey can figure out where they are called from.

The setting is thread local and must be stored in thePyThreadState structure.

Inspect Module Enhancements

Two new functions are proposed for theinspect module:isframeincleanup() andgetcleanupframe().

isframeincleanup(), given a frame or generator object as its soleargument, returns the value of thef_in_cleanup attribute of aframe itself or of thegi_frame attribute of a generator.

getcleanupframe(), given a frame object as its sole argument,returns the innermost frame which has a true value off_in_cleanup, orNone if no frames in the stack have a nonzerovalue for that attribute. It starts to inspect from the specifiedframe and walks to outer frames usingf_back pointers, just likegetouterframes() does.

Example

An example implementation of a SIGINT handler that interrupts safelymight look like:

importinspect,sys,functoolsdefsigint_handler(sig,frame):ifinspect.getcleanupframe(frame)isNone:raiseKeyboardInterrupt()sys.setcleanuphook(functools.partial(sigint_handler,0))

A coroutine example is out of scope of this document, because itsimplementation depends very much on a trampoline (or main loop) usedby coroutine library.

Unresolved Issues

Interruption Inside With Statement Expression

Given the statement

withopen(filename):do_something()

Python can be interrupted afteropen() is called, but before theSETUP_WITH bytecode is executed. There are two possibledecisions:

  • Protectwith expressions. This would require another bytecode,since currently there is no way of recognizing the start of thewith expression.
  • Let the user write a wrapper if he considers it important for theuse-case. A safe wrapper might look like this:
    classFileWrapper(object):def__init__(self,filename,mode):self.filename=filenameself.mode=modedef__enter__(self):self.file=open(self.filename,self.mode)def__exit__(self):self.file.close()

    Alternatively it can be written using thecontextmanager()decorator:

    @contextmanagerdefopen_wrapper(filename,mode):file=open(filename,mode)try:yieldfilefinally:file.close()

    This code is safe, as the first part of the generator (before yield)is executed inside theSETUP_WITH bytecode of the caller.

Exception Propagation

Sometimes afinally clause or an__enter__()/__exit__()method can raise an exception. Usually this is not a problem, sincemore important exceptions likeKeyboardInterrupt orSystemExitshould be raised instead. But it may be nice to be able to keep theoriginal exception inside a__context__ attribute. So the cleanuphook signature may grow an exception argument:

defsigint_handler(sig,frame)ifinspect.getcleanupframe(frame)isNone:raiseKeyboardInterrupt()sys.setcleanuphook(retry_sigint)defretry_sigint(frame,exception=None):ifinspect.getcleanupframe(frame)isNone:raiseKeyboardInterrupt()fromexception

Note

There is no need to have three arguments like in the__exit__method since there is a__traceback__ attribute in exception inPython 3.

However, this will set the__cause__ for the exception, which isnot exactly what’s intended. So some hidden interpreter logic may beused to put a__context__ attribute on every exception raised in acleanup hook.

Interruption Between Acquiring Resource and Try Block

The example from the first section is not totally safe. Let’s take acloser look:

lock.acquire()try:do_something()finally:lock.release()

The problem might occur if the code is interrupted just afterlock.acquire() is executed but before thetry block isentered.

There is no way the code can be fixed unmodified. The actual fixdepends very much on the use case. Usually code can be fixed using awith statement:

withlock:do_something()

However, for coroutines one usually can’t use thewith statementbecause you need toyield for both the acquire and releaseoperations. So the code might be rewritten like this:

try:yieldlock.acquire()do_something()finally:yieldlock.release()

The actual locking code might need more code to support this use case,but the implementation is usually trivial, like this: check if thelock has been acquired and unlock if it is.

Handling EINTR Inside a Finally

Even if a signal handler is prepared to check thef_in_cleanupflag,InterruptedError might be raised in the cleanup handler,because the respective system call returned anEINTR error. Theprimary use cases are prepared to handle this:

  • Posix mutexes never returnEINTR
  • Networking libraries are always prepared to handleEINTR
  • Coroutine libraries are usually interrupted with thethrow()method, not with a signal

The platform-specific functionsiginterrupt() might be used toremove the need to handleEINTR. However, it may have hardlypredictable consequences, for exampleSIGINT a handler is nevercalled if the main thread is stuck inside an IO routine.

A better approach would be to have the code, which is usually used incleanup handlers, be prepared to handleInterruptedErrorexplicitly. An example of such code might be a file-based lockimplementation.

signal.pthread_sigmask can be used to block signals insidecleanup handlers which can be interrupted withEINTR.

Setting Interruption Context Inside Finally Itself

Some coroutine libraries may need to set a timeout for the finallyclause itself. For example:

try:do_something()finally:withtimeout(0.5):try:yielddo_slow_cleanup()finally:yielddo_fast_cleanup()

With current semantics, timeout will either protect the wholewithblock or nothing at all, depending on the implementation of eachlibrary. What the author intended is to treatdo_slow_cleanup asordinary code, anddo_fast_cleanup as a cleanup (anon-interruptible one).

A similar case might occur when using greenlets or tasklets.

This case can be fixed by exposingf_in_cleanup as a counter, andby calling a cleanup hook on each decrement. A coroutine library maythen remember the value at timeout start, and compare it on each hookexecution.

But in practice, the example is considered to be too obscure to takeinto account.

Modifying KeyboardInterrupt

It should be decided if the defaultSIGINT handler should bemodified to use the described mechanism. The initial proposition isto keep old behavior, for two reasons:

  • Most application do not care about cleanup on exit (either they donot have external state, or they modify it in crash-safe way).
  • Cleanup may take too much time, not giving user a chance tointerrupt an application.

The latter case can be fixed by allowing an unsafe break if aSIGINT handler is called twice, but it seems not worth thecomplexity.

Alternative Python Implementations Support

We considerf_in_cleanup an implementation detail. The actualimplementation may have some fake frame-like object passed to signalhandler, cleanup hook and returned fromgetcleanupframe(). Theonly requirement is that theinspect module functions work asexpected on these objects. For this reason, we also allow to pass agenerator object to theisframeincleanup() function, which removesthe need to use thegi_frame attribute.

It might be necessary to specify thatgetcleanupframe() mustreturn the same object that will be passed to cleanup hook at the nextinvocation.

Alternative Names

The original proposal had af_in_finally frame attribute, as theoriginal intention was to protectfinally clauses. But as it grewup to protecting__enter__ and__exit__ methods too, thef_in_cleanup name seems better. Although the__enter__ methodis not a cleanup routine, it at least relates to cleanup done bycontext managers.

setcleanuphook,isframeincleanup andgetcleanupframe canbe unobscured toset_cleanup_hook,is_frame_in_cleanup andget_cleanup_frame, although they follow the naming convention oftheir respective modules.

Alternative Proposals

Propagating ‘f_in_cleanup’ Flag Automatically

This can makegetcleanupframe() unnecessary. But for yield-basedcoroutines you need to propagate it yourself. Making it writableleads to somewhat unpredictable behavior ofsetcleanuphook().

Add Bytecodes ‘INCR_CLEANUP’, ‘DECR_CLEANUP’

These bytecodes can be used to protect the expression inside thewith statement, as well as making counter increments more explicitand easy to debug (visible inside a disassembly). Some middle groundmight be chosen, likeEND_FINALLY andSETUP_WITH implicitlydecrementing the counter (END_FINALLY is present at end of everywith suite).

However, adding new bytecodes must be considered very carefully.

Expose ‘f_in_cleanup’ as a Counter

The original intention was to expose a minimum of neededfunctionality. However, as we consider the frame flagf_in_cleanup an implementation detail, we may expose it as acounter.

Similarly, if we have a counter we may need to have the cleanup hookcalled on every counter decrement. It’s unlikely to have muchperformance impact as nested finally clauses are an uncommon case.

Add code object flag ‘CO_CLEANUP’

As an alternative to set the flag inside theSETUP_WITH andWITH_CLEANUP bytecodes, we can introduce a flagCO_CLEANUP.When the interpreter starts to execute code withCO_CLEANUP set,it setsf_in_cleanup for the whole function body. This flag isset for code objects of__enter__ and__exit__ specialmethods. Technically it might be set on functions called__enter__ and__exit__.

This seems to be less clear solution. It also covers the case where__enter__ and__exit__ are called manually. This may beaccepted either as a feature or as an unnecessary side-effect (or,though unlikely, as a bug).

It may also impose a problem when__enter__ or__exit__functions are implemented in C, as there is no code object to checkfor thef_in_cleanup flag.

Have Cleanup Callback on Frame Object Itself

The frame object may be extended to have af_cleanup_callbackmember which is called whenf_in_cleanup is reset to 0. Thiswould help to register different callbacks to different coroutines.

Despite its apparent beauty, this solution doesn’t add anything, asthe two primary use cases are:

  • Setting the callback in a signal handler. The callback isinherently a single one for this case.
  • Use a single callback per loop for the coroutine use case. Here, inalmost all cases, there is only one loop per thread.

No Cleanup Hook

The original proposal included no cleanup hook specification, as thereare a few ways to achieve the same using current tools:

  • Usingsys.settrace() and thef_trace callback. This mayimpose some problem to debugging, and has a big performance impact(although interrupting doesn’t happen very often).
  • Sleeping a bit more and trying again. For a coroutine library thisis easy. For signals it may be achieved usingsignal.alert.

Both methods are considered too impractical and a way to catch exitfromfinally clauses is proposed.

References

[1]
Monoclehttps://github.com/saucelabs/monocle
[2]
Bluelethttps://github.com/sampsyo/bluelet
[3]
Twisted: inlineCallbackshttps://twisted.org/documents/8.1.0/api/twisted.internet.defer.html

[4] Original discussionhttps://mail.python.org/pipermail/python-ideas/2012-April/014705.html

[5] Implementation of PEP 419https://github.com/python/cpython/issues/58935

Copyright

This document has been placed in the public domain.


Source:https://github.com/python/peps/blob/main/peps/pep-0419.rst

Last modified:2025-02-01 08:59:27 GMT


[8]ページ先頭

©2009-2025 Movatter.jp