1# curio/time.py 2# 3# Functionality related to time handling including timeouts and sleeping 4 5__all__ = [ 6 'clock', 'sleep', 'timeout_after', 'ignore_after', 7 ] 8 9# -- Standard library 10 11import logging 12log = logging.getLogger(__name__) 13 14# --- Curio 15 16from .task import current_task 17from .traps import * 18from .errors import * 19from . import meta 20 21async def clock(): 22 ''' 23 Immediately return the current value of the kernel clock. There 24 are no side-effects such as task preemption or cancellation. 25 ''' 26 return await _clock() 27 28async def sleep(seconds): 29 ''' 30 Sleep for a specified number of seconds. Sleeping for 0 seconds 31 makes a task immediately switch to the next ready task (if any). 32 Returns the value of the kernel clock when awakened. 33 ''' 34 return await _sleep(seconds) 35 36class _TimeoutAfter(object): 37 ''' 38 Helper class used by timeout_after() and ignore_after() functions 39 when used as a context manager. For example: 40 41 async with timeout_after(delay): 42 statements 43 ... 44 ''' 45 46 def __init__(self, clock, ignore=False, timeout_result=None): 47 self._clock = clock 48 self._ignore = ignore 49 self._timeout_result = timeout_result 50 self.expired = False 51 self.result = True 52 53 async def __aenter__(self): 54 task = await current_task() 55 # Clock adjusted to absolute time 56 if self._clock is not None: 57 self._clock += await _clock() 58 self._deadlines = task._deadlines 59 self._deadlines.append(self._clock) 60 self._prior = await _set_timeout(self._clock) 61 return self 62 63 async def __aexit__(self, ty, val, tb): 64 current_clock = await _unset_timeout(self._prior) 65 66 # Discussion. If a timeout has occurred, it will either 67 # present itself here as a TaskTimeout or TimeoutCancellationError 68 # exception. The value of this exception is set to the current 69 # kernel clock which can be compared against our own deadline. 70 # What happens next is driven by these rules: 71 # 72 # 1. If we are the outer-most context where the timeout 73 # period has expired, then a TaskTimeout is raised. 74 # 75 # 2. If the deadline has expired for at least one outer 76 # context, (but not us), a TimeoutCancellationError is 77 # raised. This means that time has expired elsewhere. 78 # We're being cancelled because of that, but the reason 79 # for the cancellation wasn't due to a timeout on our 80 # part. 81 # 82 # 3. If the timeout period has not expired on ANY remaining 83 # timeout context, it means that a timeout has escaped 84 # some inner timeout context where it should have been 85 # caught. This is an operational error. We raise 86 # UncaughtTimeoutError. 87 88 try: 89 if ty in (TaskTimeout, TimeoutCancellationError): 90 timeout_clock = val.args[0] 91 # Find the outer most deadline that has expired 92 for n, deadline in enumerate(self._deadlines): 93 if deadline <= timeout_clock: 94 break 95 else: 96 # No remaining context has expired. An operational error 97 raise UncaughtTimeoutError('Uncaught timeout received') 98 99 if n < len(self._deadlines) - 1: 100 if ty is TaskTimeout: 101 raise TimeoutCancellationError(val.args[0]).with_traceback(tb) from None 102 else: 103 return False 104 else: 105 # The timeout is us. Make sure it's a TaskTimeout (unless ignored) 106 self.result = self._timeout_result 107 self.expired = True 108 if self._ignore: 109 return True 110 else: 111 if ty is TimeoutCancellationError: 112 raise TaskTimeout(val.args[0]).with_traceback(tb) from None 113 else: 114 return False 115 elif ty is None: 116 if current_clock > self._deadlines[-1]: 117 # Further discussion. In the presence of threads and blocking 118 # operations, it's possible that a timeout has expired, but 119 # there was simply no opportunity to catch it because there was 120 # no suspension point. 121 badness = current_clock - self._deadlines[-1] 122 log.warning('%r. Operation completed successfully, ' 123 'but it took longer than an enclosing timeout. Badness delta=%r.', 124 await current_task(), badness) 125 126 finally: 127 self._deadlines.pop() 128 129 def __enter__(self): 130 return thread.AWAIT(self.__aenter__()) 131 132 def __exit__(self, *args): 133 return thread.AWAIT(self.__aexit__(*args)) 134 135async def _timeout_after_func(clock, coro, args, 136 ignore=False, timeout_result=None): 137 coro = meta.instantiate_coroutine(coro, *args) 138 async with _TimeoutAfter(clock, ignore=ignore, timeout_result=timeout_result): 139 return await coro 140 141def timeout_after(seconds, coro=None, *args): 142 ''' 143 Raise a TaskTimeout exception in the calling task after seconds 144 have elapsed. This function may be used in two ways. You can 145 apply it to the execution of a single coroutine: 146 147 await timeout_after(seconds, coro(args)) 148 149 or you can use it as an asynchronous context manager to apply 150 a timeout to a block of statements: 151 152 async with timeout_after(seconds): 153 await coro1(args) 154 await coro2(args) 155 ... 156 ''' 157 if coro is None: 158 return _TimeoutAfter(seconds) 159 else: 160 return _timeout_after_func(seconds, coro, args) 161 162def ignore_after(seconds, coro=None, *args, timeout_result=None): 163 ''' 164 Stop the enclosed task or block of code after seconds have 165 elapsed. No exception is raised when time expires. Instead, None 166 is returned. This is often more convenient that catching an 167 exception. You can apply the function to a single coroutine: 168 169 if await ignore_after(5, coro(args)) is None: 170 # A timeout occurred 171 ... 172 173 Alternatively, you can use this function as an async context 174 manager on a block of statements like this: 175 176 async with ignore_after(5) as r: 177 await coro1(args) 178 await coro2(args) 179 ... 180 if r.result is None: 181 # A timeout occurred 182 183 When used as a context manager, the return manager object has 184 a result attribute that will be set to None if the time 185 period expires (or True otherwise). 186 187 You can change the return result to a different value using 188 the timeout_result keyword argument. 189 ''' 190 if coro is None: 191 return _TimeoutAfter(seconds, ignore=True, timeout_result=timeout_result) 192 else: 193 return _timeout_after_func(seconds, coro, args, ignore=True, timeout_result=timeout_result) 194 195from . import thread 196