Sitelet https://github.com/python/cpython/issues/74953
Skip to content

PyThread_acquire_lock_timed() should recompute the timeout when interrupted by a signal #74953

Description

@vstinner
BPO 30768
Nosy @pitrou, @vstinner
PRs
  • bpo-30768: Recompute timeout on interrupted lock #4103
  • Files
  • interrupted_lock.py
  • test.patch
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2017-06-26.13:50:01.691>
    labels = ['3.7']
    title = 'PyThread_acquire_lock_timed() should recompute the timeout when interrupted by a signal'
    updated_at = <Date 2017-10-25.00:15:14.862>
    user = 'https://github.com/vstinner'

    bugs.python.org fields:

    activity = <Date 2017-10-25.00:15:14.862>
    actor = 'vstinner'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = []
    creation = <Date 2017-06-26.13:50:01.691>
    creator = 'vstinner'
    dependencies = []
    files = ['47233', '47234']
    hgrepos = []
    issue_num = 30768
    keywords = ['patch']
    message_count = 10.0
    messages = ['296896', '296898', '296899', '304892', '304893', '304895', '304896', '304951', '304952', '304953']
    nosy_count = 3.0
    nosy_names = ['pitrou', 'vstinner', 'neologix']
    pr_nums = ['4103']
    priority = 'normal'
    resolution = None
    stage = 'resolved'
    status = 'open'
    superseder = None
    type = None
    url = 'https://bugs.python.org/issue30768'
    versions = ['Python 3.7']

    Activity

    1. vstinner commented on Jun 26, 2017

      @vstinner
      MemberAuthor

      The current code of PyThread_acquire_lock_timed() (the implementation not using semaphore) doesn't compute correctly the timeout when pthread_cond_timedwait() is interrupted by a signal. We should recompute the timeout using a deadline.

      Something like select.select():

      if (tvp)
      deadline = _PyTime_GetMonotonicClock() + timeout;

      do {
      ... use tvp
      if (errno != EINTR)
      break;

      /* select() was interrupted by a signal */
      if (PyErr_CheckSignals())
          goto finally;
      
          if (tvp) {
              timeout = deadline - _PyTime_GetMonotonicClock();
              if (timeout < 0) {
                  n = 0;
                  break;
              }
              _PyTime_AsTimeval_noraise(timeout, &tv, _PyTime_ROUND_CEILING);
              /* retry select() with the recomputed timeout */
          }
      } while (1);
    2. vstinner commented on Jun 26, 2017

      @vstinner
      MemberAuthor

      See also the old bpo-12822: "NewGIL should use CLOCK_MONOTONIC if possible.".

    3. vstinner commented on Jun 26, 2017

      @vstinner
      MemberAuthor

      See also the PEP-475 "Retry system calls failing with EINTR" and PEP-418 (time.monotonic).

    4. vstinner commented on Oct 24, 2017

      @vstinner
      MemberAuthor

      interrupted_lock.py: test threading.Lock.acquire(timeout=1.0) with SIGALRM sent every 1 ms (so up to 1000 times in total). Example:

      haypo@selma$ ./python interrupted_lock.py
      acquire(timeout=1.0) took 1.0 seconds and got 1000 signals

      Oh, in fact, threading.Lock.acquire(timeout=1.0) already recomputes the timeout when interrupted.

      In Python stdlib, PyThread_acquire_lock_timed() is only called from one place with intr_flag=0: faulthandler watchdog thread, but this thread blocks all signals:

      /* we don't want to receive any signal */
      sigfillset(&set);
      pthread_sigmask(SIG_SETMASK, &set, NULL);
      
    5. vstinner commented on Oct 24, 2017

      @vstinner
      MemberAuthor

      Ah, I found another caller of PyThread_acquire_lock_timed() with a timeout > 0 and intr_flag=0: _enter_buffered_busy() of Modules/_io/bufferedio.c:

          /* When finalizing, we don't want a deadlock to happen with daemon
           * threads abruptly shut down while they owned the lock.
           * Therefore, only wait for a grace period (1 s.).
           * Note that non-daemon threads have already exited here, so this
           * shouldn't affect carefully written threaded I/O code.
           */
          st = PyThread_acquire_lock_timed(self->lock, (PY_TIMEOUT_T)1e6, 0);
      
    6. vstinner commented on Oct 24, 2017

      @vstinner
      MemberAuthor

      I wrote PR 4103 to fix the pthread+semaphore implementation of PyThread_acquire_lock_timed().

      Apply PR 4103, apply attached test.patch, recompile Python, and run interrupted_lock.py to test the PR. Result:

      haypo@selma$ ./python interrupted_lock.py
      acquire(timeout=1.0) took 1.0 seconds and got 911 signals

    7. vstinner commented on Oct 24, 2017

      @vstinner
      MemberAuthor

      To check if you are using pthread+semaphore, use:

      haypo@selma$ ./python -c 'import sys; print(sys.thread_info)'
      sys.thread_info(name='pthread', lock='semaphore', version='NPTL 2.25')

      Here you have pthread+semaphore. It's Fedora 26 running Linux kernel 4.13.

    8. vstinner commented on Oct 24, 2017

      @vstinner
      MemberAuthor

      New changeset 850a18e by Victor Stinner in branch 'master':
      bpo-30768: Recompute timeout on interrupted lock (GH-4103)
      850a18e

    9. vstinner commented on Oct 25, 2017

      @vstinner
      MemberAuthor

      I merged my PR. Thanks Antoine Pitrou for the review!

      This change only impacts the io.BufferedWriter and io.BufferedReader during Python finalization. It has no effect on theading.Lock.acquire(). It might impact faulthandler.dump_traceback_later(), but in practice, it shouldn't change anything since the internal faulthandler watchdog thread blocks all signals.

      If I understand correctly, if the system clock is stepped back by 1 hour during Python finalization, after PyThread_acquire_lock_timed() computed the deadline, but before sem_timedwait() completed, _enter_buffered_busy() can be blocked during 1 hour.

      Moving the system clock backward by 1 hour occurs once a year on the DST change. But the race condition is unlikely since the size of the time window is only 1 second. The DST change should occur at Python shutdown when an io.BufferedReader or io.BufferedWriter is used.

      Since the race condition seems very unlikely and was never reported by another user, I propose to not backward this change. Moreover, I'm not confident to modify locks in a stable release :-)

    10. vstinner commented on Oct 25, 2017

      @vstinner
      MemberAuthor

      Oh, the pthread condvar+mutex implementation still has the bug, so I reopen the issue.

    11. transferred this issue fromon Apr 10, 2022
    12. added 2 commits that reference this issue on Jun 17, 2022
    13. added 2 commits that reference this issue on Jun 17, 2022
    14. added a commit that references this issue on Jun 19, 2022
    15. vstinner commented on Jun 21, 2022

      @vstinner
      MemberAuthor

      I looked again to this issue. IMO it's now fully fixed, see my comment for the rationale: #93946 (comment)

    16. added a commit that references this issue on Jun 21, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    No one assigned

      Labels

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions