blob: 19fa654edbed1355d29f89e60bd486d24ade2760 [file] [log] [blame]
Guido van Rossum27b7c7e2013-10-17 13:40:50 -07001"""Support for tasks, coroutines and the scheduler."""
2
3__all__ = ['coroutine', 'Task',
Guido van Rossum68816ef2013-12-28 08:06:40 -10004 'iscoroutinefunction', 'iscoroutine',
Guido van Rossum27b7c7e2013-10-17 13:40:50 -07005 'FIRST_COMPLETED', 'FIRST_EXCEPTION', 'ALL_COMPLETED',
6 'wait', 'wait_for', 'as_completed', 'sleep', 'async',
Guido van Rossumde3a1362013-11-29 09:29:00 -08007 'gather', 'shield',
Guido van Rossum27b7c7e2013-10-17 13:40:50 -07008 ]
9
Guido van Rossum27b7c7e2013-10-17 13:40:50 -070010import concurrent.futures
11import functools
12import inspect
13import linecache
Victor Stinner0f3e6bc2014-02-19 23:15:02 +010014import os
15import sys
Guido van Rossum27b7c7e2013-10-17 13:40:50 -070016import traceback
17import weakref
18
19from . import events
20from . import futures
Guido van Rossumfc29e0f2013-10-17 15:39:45 -070021from .log import logger
Guido van Rossum27b7c7e2013-10-17 13:40:50 -070022
23# If you set _DEBUG to true, @coroutine will wrap the resulting
24# generator objects in a CoroWrapper instance (defined below). That
25# instance will log a message when the generator is never iterated
26# over, which may happen when you forget to use "yield from" with a
27# coroutine call. Note that the value of the _DEBUG flag is taken
28# when the decorator is used, so to be of any use it must be set
29# before you define your coroutines. A downside of using this feature
30# is that tracebacks show entries for the CoroWrapper.__next__ method
31# when _DEBUG is true.
Victor Stinner0f3e6bc2014-02-19 23:15:02 +010032_DEBUG = (not sys.flags.ignore_environment
33 and bool(os.environ.get('PYTHONASYNCIODEBUG')))
Guido van Rossum27b7c7e2013-10-17 13:40:50 -070034
35
36class CoroWrapper:
Guido van Rossume1f55442014-01-16 11:05:23 -080037 # Wrapper for coroutine in _DEBUG mode.
38
Victor Stinnerbac77932014-01-16 01:55:29 +010039 __slots__ = ['gen', 'func', '__name__', '__doc__']
Guido van Rossum27b7c7e2013-10-17 13:40:50 -070040
41 def __init__(self, gen, func):
42 assert inspect.isgenerator(gen), gen
43 self.gen = gen
44 self.func = func
45
46 def __iter__(self):
47 return self
48
49 def __next__(self):
50 return next(self.gen)
51
52 def send(self, value):
53 return self.gen.send(value)
54
55 def throw(self, exc):
56 return self.gen.throw(exc)
57
58 def close(self):
59 return self.gen.close()
60
61 def __del__(self):
62 frame = self.gen.gi_frame
63 if frame is not None and frame.f_lasti == -1:
64 func = self.func
65 code = func.__code__
66 filename = code.co_filename
67 lineno = code.co_firstlineno
Guido van Rossum2b430b82013-11-01 14:13:30 -070068 logger.error(
69 'Coroutine %r defined at %s:%s was never yielded from',
70 func.__name__, filename, lineno)
Guido van Rossum27b7c7e2013-10-17 13:40:50 -070071
72
73def coroutine(func):
74 """Decorator to mark coroutines.
75
76 If the coroutine is not yielded from before it is destroyed,
77 an error message is logged.
78 """
79 if inspect.isgeneratorfunction(func):
80 coro = func
81 else:
82 @functools.wraps(func)
83 def coro(*args, **kw):
84 res = func(*args, **kw)
85 if isinstance(res, futures.Future) or inspect.isgenerator(res):
86 res = yield from res
87 return res
88
89 if not _DEBUG:
90 wrapper = coro
91 else:
92 @functools.wraps(func)
93 def wrapper(*args, **kwds):
94 w = CoroWrapper(coro(*args, **kwds), func)
95 w.__name__ = coro.__name__
96 w.__doc__ = coro.__doc__
97 return w
98
99 wrapper._is_coroutine = True # For iscoroutinefunction().
100 return wrapper
101
102
103def iscoroutinefunction(func):
104 """Return True if func is a decorated coroutine function."""
105 return getattr(func, '_is_coroutine', False)
106
107
108def iscoroutine(obj):
109 """Return True if obj is a coroutine object."""
110 return isinstance(obj, CoroWrapper) or inspect.isgenerator(obj)
111
112
113class Task(futures.Future):
114 """A coroutine wrapped in a Future."""
115
116 # An important invariant maintained while a Task not done:
117 #
118 # - Either _fut_waiter is None, and _step() is scheduled;
119 # - or _fut_waiter is some Future, and _step() is *not* scheduled.
120 #
121 # The only transition from the latter to the former is through
122 # _wakeup(). When _fut_waiter is not None, one of its callbacks
123 # must be _wakeup().
124
125 # Weak set containing all tasks alive.
126 _all_tasks = weakref.WeakSet()
127
Guido van Rossum1a605ed2013-12-06 12:57:40 -0800128 # Dictionary containing tasks that are currently active in
129 # all running event loops. {EventLoop: Task}
130 _current_tasks = {}
131
132 @classmethod
133 def current_task(cls, loop=None):
134 """Return the currently running task in an event loop or None.
135
136 By default the current task for the current event loop is returned.
137
138 None is returned when called not in the context of a Task.
139 """
140 if loop is None:
141 loop = events.get_event_loop()
142 return cls._current_tasks.get(loop)
143
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700144 @classmethod
145 def all_tasks(cls, loop=None):
146 """Return a set of all tasks for an event loop.
147
148 By default all tasks for the current event loop are returned.
149 """
150 if loop is None:
151 loop = events.get_event_loop()
152 return {t for t in cls._all_tasks if t._loop is loop}
153
154 def __init__(self, coro, *, loop=None):
155 assert iscoroutine(coro), repr(coro) # Not a coroutine function!
156 super().__init__(loop=loop)
157 self._coro = iter(coro) # Use the iterator just in case.
158 self._fut_waiter = None
159 self._must_cancel = False
160 self._loop.call_soon(self._step)
161 self.__class__._all_tasks.add(self)
162
163 def __repr__(self):
164 res = super().__repr__()
165 if (self._must_cancel and
166 self._state == futures._PENDING and
167 '<PENDING' in res):
168 res = res.replace('<PENDING', '<CANCELLING', 1)
169 i = res.find('<')
170 if i < 0:
171 i = len(res)
172 res = res[:i] + '(<{}>)'.format(self._coro.__name__) + res[i:]
173 return res
174
175 def get_stack(self, *, limit=None):
176 """Return the list of stack frames for this task's coroutine.
177
178 If the coroutine is active, this returns the stack where it is
179 suspended. If the coroutine has completed successfully or was
180 cancelled, this returns an empty list. If the coroutine was
181 terminated by an exception, this returns the list of traceback
182 frames.
183
184 The frames are always ordered from oldest to newest.
185
Yury Selivanovdec1a452014-02-18 22:27:48 -0500186 The optional limit gives the maximum number of frames to
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700187 return; by default all available frames are returned. Its
188 meaning differs depending on whether a stack or a traceback is
189 returned: the newest frames of a stack are returned, but the
190 oldest frames of a traceback are returned. (This matches the
191 behavior of the traceback module.)
192
193 For reasons beyond our control, only one stack frame is
194 returned for a suspended coroutine.
195 """
196 frames = []
197 f = self._coro.gi_frame
198 if f is not None:
199 while f is not None:
200 if limit is not None:
201 if limit <= 0:
202 break
203 limit -= 1
204 frames.append(f)
205 f = f.f_back
206 frames.reverse()
207 elif self._exception is not None:
208 tb = self._exception.__traceback__
209 while tb is not None:
210 if limit is not None:
211 if limit <= 0:
212 break
213 limit -= 1
214 frames.append(tb.tb_frame)
215 tb = tb.tb_next
216 return frames
217
218 def print_stack(self, *, limit=None, file=None):
219 """Print the stack or traceback for this task's coroutine.
220
221 This produces output similar to that of the traceback module,
222 for the frames retrieved by get_stack(). The limit argument
223 is passed to get_stack(). The file argument is an I/O stream
224 to which the output goes; by default it goes to sys.stderr.
225 """
226 extracted_list = []
227 checked = set()
228 for f in self.get_stack(limit=limit):
229 lineno = f.f_lineno
230 co = f.f_code
231 filename = co.co_filename
232 name = co.co_name
233 if filename not in checked:
234 checked.add(filename)
235 linecache.checkcache(filename)
236 line = linecache.getline(filename, lineno, f.f_globals)
237 extracted_list.append((filename, lineno, name, line))
238 exc = self._exception
239 if not extracted_list:
240 print('No stack for %r' % self, file=file)
241 elif exc is not None:
242 print('Traceback for %r (most recent call last):' % self,
243 file=file)
244 else:
245 print('Stack for %r (most recent call last):' % self,
246 file=file)
247 traceback.print_list(extracted_list, file=file)
248 if exc is not None:
249 for line in traceback.format_exception_only(exc.__class__, exc):
250 print(line, file=file, end='')
251
252 def cancel(self):
253 if self.done():
254 return False
255 if self._fut_waiter is not None:
256 if self._fut_waiter.cancel():
257 # Leave self._fut_waiter; it may be a Task that
258 # catches and ignores the cancellation so we may have
259 # to cancel it again later.
260 return True
261 # It must be the case that self._step is already scheduled.
262 self._must_cancel = True
263 return True
264
265 def _step(self, value=None, exc=None):
266 assert not self.done(), \
267 '_step(): already done: {!r}, {!r}, {!r}'.format(self, value, exc)
268 if self._must_cancel:
269 if not isinstance(exc, futures.CancelledError):
270 exc = futures.CancelledError()
271 self._must_cancel = False
272 coro = self._coro
273 self._fut_waiter = None
Guido van Rossum1a605ed2013-12-06 12:57:40 -0800274
275 self.__class__._current_tasks[self._loop] = self
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700276 # Call either coro.throw(exc) or coro.send(value).
277 try:
278 if exc is not None:
279 result = coro.throw(exc)
280 elif value is not None:
281 result = coro.send(value)
282 else:
283 result = next(coro)
284 except StopIteration as exc:
285 self.set_result(exc.value)
286 except futures.CancelledError as exc:
287 super().cancel() # I.e., Future.cancel(self).
288 except Exception as exc:
289 self.set_exception(exc)
290 except BaseException as exc:
291 self.set_exception(exc)
292 raise
293 else:
294 if isinstance(result, futures.Future):
295 # Yielded Future must come from Future.__iter__().
296 if result._blocking:
297 result._blocking = False
298 result.add_done_callback(self._wakeup)
299 self._fut_waiter = result
300 if self._must_cancel:
301 if self._fut_waiter.cancel():
302 self._must_cancel = False
303 else:
304 self._loop.call_soon(
305 self._step, None,
306 RuntimeError(
307 'yield was used instead of yield from '
308 'in task {!r} with {!r}'.format(self, result)))
309 elif result is None:
310 # Bare yield relinquishes control for one event loop iteration.
311 self._loop.call_soon(self._step)
312 elif inspect.isgenerator(result):
313 # Yielding a generator is just wrong.
314 self._loop.call_soon(
315 self._step, None,
316 RuntimeError(
317 'yield was used instead of yield from for '
318 'generator in task {!r} with {}'.format(
319 self, result)))
320 else:
321 # Yielding something else is an error.
322 self._loop.call_soon(
323 self._step, None,
324 RuntimeError(
325 'Task got bad yield: {!r}'.format(result)))
Guido van Rossum1a605ed2013-12-06 12:57:40 -0800326 finally:
327 self.__class__._current_tasks.pop(self._loop)
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700328 self = None
329
330 def _wakeup(self, future):
331 try:
332 value = future.result()
333 except Exception as exc:
334 # This may also be a cancellation.
335 self._step(None, exc)
336 else:
337 self._step(value, None)
338 self = None # Needed to break cycles when an exception occurs.
339
340
341# wait() and as_completed() similar to those in PEP 3148.
342
343FIRST_COMPLETED = concurrent.futures.FIRST_COMPLETED
344FIRST_EXCEPTION = concurrent.futures.FIRST_EXCEPTION
345ALL_COMPLETED = concurrent.futures.ALL_COMPLETED
346
347
348@coroutine
349def wait(fs, *, loop=None, timeout=None, return_when=ALL_COMPLETED):
350 """Wait for the Futures and coroutines given by fs to complete.
351
352 Coroutines will be wrapped in Tasks.
353
354 Returns two sets of Future: (done, pending).
355
356 Usage:
357
358 done, pending = yield from asyncio.wait(fs)
359
360 Note: This does not raise TimeoutError! Futures that aren't done
361 when the timeout occurs are returned in the second set.
362 """
Victor Stinner208556c2014-02-11 11:54:08 +0100363 if isinstance(fs, futures.Future) or iscoroutine(fs):
364 raise TypeError("expect a list of futures, not %s" % type(fs).__name__)
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700365 if not fs:
366 raise ValueError('Set of coroutines/Futures is empty.')
367
368 if loop is None:
369 loop = events.get_event_loop()
370
Yury Selivanov622be342014-02-06 22:06:16 -0500371 fs = {async(f, loop=loop) for f in set(fs)}
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700372
373 if return_when not in (FIRST_COMPLETED, FIRST_EXCEPTION, ALL_COMPLETED):
374 raise ValueError('Invalid return_when value: {}'.format(return_when))
375 return (yield from _wait(fs, timeout, return_when, loop))
376
377
378def _release_waiter(waiter, value=True, *args):
379 if not waiter.done():
380 waiter.set_result(value)
381
382
383@coroutine
384def wait_for(fut, timeout, *, loop=None):
385 """Wait for the single Future or coroutine to complete, with timeout.
386
387 Coroutine will be wrapped in Task.
388
Victor Stinner421e49b2014-01-23 17:40:59 +0100389 Returns result of the Future or coroutine. When a timeout occurs,
390 it cancels the task and raises TimeoutError. To avoid the task
391 cancellation, wrap it in shield().
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700392
393 Usage:
394
395 result = yield from asyncio.wait_for(fut, 10.0)
396
397 """
398 if loop is None:
399 loop = events.get_event_loop()
400
Guido van Rossum48c66c32014-01-29 14:30:38 -0800401 if timeout is None:
402 return (yield from fut)
403
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700404 waiter = futures.Future(loop=loop)
405 timeout_handle = loop.call_later(timeout, _release_waiter, waiter, False)
406 cb = functools.partial(_release_waiter, waiter, True)
407
408 fut = async(fut, loop=loop)
409 fut.add_done_callback(cb)
410
411 try:
412 if (yield from waiter):
413 return fut.result()
414 else:
415 fut.remove_done_callback(cb)
Victor Stinner421e49b2014-01-23 17:40:59 +0100416 fut.cancel()
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700417 raise futures.TimeoutError()
418 finally:
419 timeout_handle.cancel()
420
421
422@coroutine
423def _wait(fs, timeout, return_when, loop):
424 """Internal helper for wait() and _wait_for().
425
426 The fs argument must be a collection of Futures.
427 """
428 assert fs, 'Set of Futures is empty.'
429 waiter = futures.Future(loop=loop)
430 timeout_handle = None
431 if timeout is not None:
432 timeout_handle = loop.call_later(timeout, _release_waiter, waiter)
433 counter = len(fs)
434
435 def _on_completion(f):
436 nonlocal counter
437 counter -= 1
438 if (counter <= 0 or
439 return_when == FIRST_COMPLETED or
440 return_when == FIRST_EXCEPTION and (not f.cancelled() and
441 f.exception() is not None)):
442 if timeout_handle is not None:
443 timeout_handle.cancel()
444 if not waiter.done():
445 waiter.set_result(False)
446
447 for f in fs:
448 f.add_done_callback(_on_completion)
449
450 try:
451 yield from waiter
452 finally:
453 if timeout_handle is not None:
454 timeout_handle.cancel()
455
456 done, pending = set(), set()
457 for f in fs:
458 f.remove_done_callback(_on_completion)
459 if f.done():
460 done.add(f)
461 else:
462 pending.add(f)
463 return done, pending
464
465
466# This is *not* a @coroutine! It is just an iterator (yielding Futures).
467def as_completed(fs, *, loop=None, timeout=None):
Guido van Rossum2303fec2014-02-12 17:58:19 -0800468 """Return an iterator whose values are coroutines.
469
470 When waiting for the yielded coroutines you'll get the results (or
471 exceptions!) of the original Futures (or coroutines), in the order
472 in which and as soon as they complete.
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700473
474 This differs from PEP 3148; the proper way to use this is:
475
476 for f in as_completed(fs):
477 result = yield from f # The 'yield from' may raise.
478 # Use result.
479
Guido van Rossum2303fec2014-02-12 17:58:19 -0800480 If a timeout is specified, the 'yield from' will raise
481 TimeoutError when the timeout occurs before all Futures are done.
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700482
483 Note: The futures 'f' are not necessarily members of fs.
484 """
Victor Stinner208556c2014-02-11 11:54:08 +0100485 if isinstance(fs, futures.Future) or iscoroutine(fs):
486 raise TypeError("expect a list of futures, not %s" % type(fs).__name__)
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700487 loop = loop if loop is not None else events.get_event_loop()
Yury Selivanov622be342014-02-06 22:06:16 -0500488 todo = {async(f, loop=loop) for f in set(fs)}
Guido van Rossum2303fec2014-02-12 17:58:19 -0800489 from .queues import Queue # Import here to avoid circular import problem.
490 done = Queue(loop=loop)
491 timeout_handle = None
492
493 def _on_timeout():
494 for f in todo:
495 f.remove_done_callback(_on_completion)
496 done.put_nowait(None) # Queue a dummy value for _wait_for_one().
497 todo.clear() # Can't do todo.remove(f) in the loop.
498
499 def _on_completion(f):
500 if not todo:
501 return # _on_timeout() was here first.
502 todo.remove(f)
503 done.put_nowait(f)
504 if not todo and timeout_handle is not None:
505 timeout_handle.cancel()
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700506
507 @coroutine
508 def _wait_for_one():
Guido van Rossum2303fec2014-02-12 17:58:19 -0800509 f = yield from done.get()
510 if f is None:
511 # Dummy value from _on_timeout().
512 raise futures.TimeoutError
513 return f.result() # May raise f.exception().
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700514
Guido van Rossum2303fec2014-02-12 17:58:19 -0800515 for f in todo:
516 f.add_done_callback(_on_completion)
517 if todo and timeout is not None:
518 timeout_handle = loop.call_later(timeout, _on_timeout)
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700519 for _ in range(len(todo)):
520 yield _wait_for_one()
521
522
523@coroutine
524def sleep(delay, result=None, *, loop=None):
525 """Coroutine that completes after a given time (in seconds)."""
526 future = futures.Future(loop=loop)
527 h = future._loop.call_later(delay, future.set_result, result)
528 try:
529 return (yield from future)
530 finally:
531 h.cancel()
532
533
534def async(coro_or_future, *, loop=None):
535 """Wrap a coroutine in a future.
536
537 If the argument is a Future, it is returned directly.
538 """
539 if isinstance(coro_or_future, futures.Future):
540 if loop is not None and loop is not coro_or_future._loop:
541 raise ValueError('loop argument must agree with Future')
542 return coro_or_future
543 elif iscoroutine(coro_or_future):
544 return Task(coro_or_future, loop=loop)
545 else:
546 raise TypeError('A Future or coroutine is required')
547
548
549class _GatheringFuture(futures.Future):
550 """Helper for gather().
551
552 This overrides cancel() to cancel all the children and act more
553 like Task.cancel(), which doesn't immediately mark itself as
554 cancelled.
555 """
556
557 def __init__(self, children, *, loop=None):
558 super().__init__(loop=loop)
559 self._children = children
560
561 def cancel(self):
562 if self.done():
563 return False
564 for child in self._children:
565 child.cancel()
566 return True
567
568
569def gather(*coros_or_futures, loop=None, return_exceptions=False):
570 """Return a future aggregating results from the given coroutines
571 or futures.
572
573 All futures must share the same event loop. If all the tasks are
574 done successfully, the returned future's result is the list of
575 results (in the order of the original sequence, not necessarily
Yury Selivanovf317cb72014-02-06 12:03:53 -0500576 the order of results arrival). If *return_exceptions* is True,
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700577 exceptions in the tasks are treated the same as successful
578 results, and gathered in the result list; otherwise, the first
579 raised exception will be immediately propagated to the returned
580 future.
581
582 Cancellation: if the outer Future is cancelled, all children (that
583 have not completed yet) are also cancelled. If any child is
584 cancelled, this is treated as if it raised CancelledError --
585 the outer Future is *not* cancelled in this case. (This is to
586 prevent the cancellation of one child to cause other children to
587 be cancelled.)
588 """
Yury Selivanov622be342014-02-06 22:06:16 -0500589 arg_to_fut = {arg: async(arg, loop=loop) for arg in set(coros_or_futures)}
590 children = [arg_to_fut[arg] for arg in coros_or_futures]
Guido van Rossum27b7c7e2013-10-17 13:40:50 -0700591 n = len(children)
592 if n == 0:
593 outer = futures.Future(loop=loop)
594 outer.set_result([])
595 return outer
596 if loop is None:
597 loop = children[0]._loop
598 for fut in children:
599 if fut._loop is not loop:
600 raise ValueError("futures are tied to different event loops")
601 outer = _GatheringFuture(children, loop=loop)
602 nfinished = 0
603 results = [None] * n
604
605 def _done_callback(i, fut):
606 nonlocal nfinished
607 if outer._state != futures._PENDING:
608 if fut._exception is not None:
609 # Mark exception retrieved.
610 fut.exception()
611 return
612 if fut._state == futures._CANCELLED:
613 res = futures.CancelledError()
614 if not return_exceptions:
615 outer.set_exception(res)
616 return
617 elif fut._exception is not None:
618 res = fut.exception() # Mark exception retrieved.
619 if not return_exceptions:
620 outer.set_exception(res)
621 return
622 else:
623 res = fut._result
624 results[i] = res
625 nfinished += 1
626 if nfinished == n:
627 outer.set_result(results)
628
629 for i, fut in enumerate(children):
630 fut.add_done_callback(functools.partial(_done_callback, i))
631 return outer
632
633
634def shield(arg, *, loop=None):
635 """Wait for a future, shielding it from cancellation.
636
637 The statement
638
639 res = yield from shield(something())
640
641 is exactly equivalent to the statement
642
643 res = yield from something()
644
645 *except* that if the coroutine containing it is cancelled, the
646 task running in something() is not cancelled. From the POV of
647 something(), the cancellation did not happen. But its caller is
648 still cancelled, so the yield-from expression still raises
649 CancelledError. Note: If something() is cancelled by other means
650 this will still cancel shield().
651
652 If you want to completely ignore cancellation (not recommended)
653 you can combine shield() with a try/except clause, as follows:
654
655 try:
656 res = yield from shield(something())
657 except CancelledError:
658 res = None
659 """
660 inner = async(arg, loop=loop)
661 if inner.done():
662 # Shortcut.
663 return inner
664 loop = inner._loop
665 outer = futures.Future(loop=loop)
666
667 def _done_callback(inner):
668 if outer.cancelled():
669 # Mark inner's result as retrieved.
670 inner.cancelled() or inner.exception()
671 return
672 if inner.cancelled():
673 outer.cancel()
674 else:
675 exc = inner.exception()
676 if exc is not None:
677 outer.set_exception(exc)
678 else:
679 outer.set_result(inner.result())
680
681 inner.add_done_callback(_done_callback)
682 return outer