title: Waiting on condition variable with timeout: lock not reacquired in time
body: <p>I have an <code>asyncio.Condition</code> named <code>cond</code>. I wish to wait on it, but only for so long before giving up. As <code>asyncio.Condition.wait</code> does not take a timeout, this cannot be done directly. <a href="https://docs.python.org/3.6/library/asyncio-sync.html" rel="noreferrer">The docs</a> state that <code>asyncio.wait_for</code> should be used to wrap and provide a timeout instead:</p>
  
  <blockquote>
    <p>The asyncio.wait_for() function can be used to cancel a task after a timeout.</p>
  </blockquote>
  
  <p>Thus, we arrive at the following solution:</p>
  
  <pre><code>async def coro():
      print("Taking lock...")
      async with cond:
          print("Lock acquired.")
          print("Waiting!")
          await asyncio.wait_for(cond.wait(), timeout=999)
          print("Was notified!")
      print("Lock released.")
  </code></pre>
  
  <hr>
  
  <p>Now assume that <code>coro</code> itself is cancelled five seconds after being run. This throws <code>CancelledError</code> in the <code>wait_for</code>, which cancels the <code>cond.wait</code> before re-raising the error. The error then propagates to <code>coro</code>, which due to the <code>async with</code> block, implicitly attempts to release the lock in <code>cond</code>. However, the lock is not currently held; <code>cond.wait</code> has been cancelled but hasn't had a chance to process that cancellation and re-acquire the lock. Thus, we get an ugly exception like the following:</p>
  
  <pre><code>Taking lock...
  Lock acquired.
  Waiting!
  ERROR:asyncio:Task exception was never retrieved
  future: &lt;Task finished coro=&lt;coro() done, defined at [REDACTED]&gt; exception=RuntimeError('Lock is not acquired.',)&gt;
  Traceback (most recent call last):
    [REDACTED], in coro
      await asyncio.wait_for(cond.wait(), timeout=999)
    [REDACTED], in wait_for
      yield from waiter
  concurrent.futures._base.CancelledError
  
  During handling of the above exception, another exception occurred:
  
  Traceback (most recent call last):
    [REDACTED], in coro
      print("Was notified!")
    [REDACTED], in coro
      res = func(*args, **kw)
    [REDACTED], in __aexit__
      self.release()
    [REDACTED], in release
      raise RuntimeError('Lock is not acquired.')
  RuntimeError: Lock is not acquired.
  </code></pre>
  
  <p>In other words, whilst handling the <code>CancelledError</code>, <code>coro</code> raised a <code>RuntimeError</code> from trying to release a lock that wasn't held. The reason the stacktrace shows the <code>print("Was notified!")</code> line is because that is the last line of the offending <code>async with</code> block.</p>
  
  <hr>
  
  <del>
  This doesn't feel like something I can fix; I'm starting to suspect it's a bug in the library itself. However, I can't think of any way to avoid the issue or create a workaround, so any ideas would be appreciated.
  </del>
  
  <p>Whilst writing this question and investigating further, I stumbled into similar problems on the Python bug tracker, ended up inspecting the <code>asyncio</code> source code, and determined that this is, in fact, a bug in <code>asyncio</code> itself.</p>
  
  <p>I've submitted it to the issue tracker <a href="https://bugs.python.org/issue39032" rel="noreferrer">here</a> for those who have the same problem, and answered my own question with a workaround that I have created.</p>
  
  <hr>
  
  <p><strong>EDIT:</strong> As requested by ParkerD, here is the full runnable example that produces the above issue:</p>
  
  <p><strong>EDIT 2:</strong> updated example to use the new <code>asyncio.run</code> and <code>asyncio.create_task</code> features from Python 3.7+</p>
  
  <pre><code>import asyncio
  
  async def coro():
      cond = asyncio.Condition()
      print("Taking lock...")
      async with cond:
          print("Lock acquired.")
          print("Waiting!")
          await asyncio.wait_for(cond.wait(), timeout=999)
          print("Was notified!")
      print("Lock released.")
  
  async def cancel_after_5(c):
      task = asyncio.create_task(c)
      await asyncio.sleep(5)
      task.cancel()
      await asyncio.wait([task])
  
  asyncio.run(cancel_after_5(coro()))
  </code></pre>