-
-
Notifications
You must be signed in to change notification settings - Fork 322
/
reference-core.rst
1928 lines (1487 loc) · 77.5 KB
/
reference-core.rst
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
Trio's core functionality
=========================
.. module:: trio
Entering Trio
-------------
If you want to use Trio, then the first thing you have to do is call
:func:`trio.run`:
.. autofunction:: run
General principles
------------------
.. _checkpoints:
Checkpoints
~~~~~~~~~~~
When writing code using Trio, it's very important to understand the
concept of a *checkpoint*. Many of Trio's functions act as checkpoints.
A checkpoint is two things:
1. It's a point where Trio checks for cancellation. For example, if
the code that called your function set a timeout, and that timeout
has expired, then the next time your function executes a checkpoint
Trio will raise a :exc:`Cancelled` exception. See
:ref:`cancellation` below for more details.
2. It's a point where the Trio scheduler checks its scheduling policy
to see if it's a good time to switch to another task, and
potentially does so. (Currently, this check is very simple: the
scheduler always switches at every checkpoint. But `this might
change in the future
<https://github.com/python-trio/trio/issues/32>`__.)
When writing Trio code, you need to keep track of where your
checkpoints are. Why? First, because checkpoints require extra
scrutiny: whenever you execute a checkpoint, you need to be prepared
to handle a :exc:`Cancelled` error, or for another task to run and
`rearrange some state out from under you
<https://glyph.twistedmatrix.com/2014/02/unyielding.html>`__. And
second, because you also need to make sure that you have *enough*
checkpoints: if your code doesn't pass through a checkpoint on a
regular basis, then it will be slow to notice and respond to
cancellation and – much worse – since Trio is a cooperative
multi-tasking system where the *only* place the scheduler can switch
tasks is at checkpoints, it'll also prevent the scheduler from fairly
allocating time between different tasks and adversely effect the
response latency of all the other code running in the same
process. (Informally we say that a task that does this is "hogging the
run loop".)
So when you're doing code review on a project that uses Trio, one of
the things you'll want to think about is whether there are enough
checkpoints, and whether each one is handled correctly. Of course this
means you need a way to recognize checkpoints. How do you do that?
The underlying principle is that any operation that blocks has to be a
checkpoint. This makes sense: if an operation blocks, then it might
block for a long time, and you'll want to be able to cancel it if a
timeout expires; and in any case, while this task is blocked we want
another task to be scheduled to run so our code can make full use of
the CPU.
But if we want to write correct code in practice, then this principle
is a little too sloppy and imprecise to be useful. How do we know
which functions might block? What if a function blocks sometimes, but
not others, depending on the arguments passed / network speed / phase
of the moon? How do we figure out where the checkpoints are when
we're stressed and sleep deprived but still want to get this code
review right, and would prefer to reserve our mental energy for
thinking about the actual logic instead of worrying about checkpoints?
.. _checkpoint-rule:
Don't worry – Trio's got your back. Since checkpoints are important
and ubiquitous, we make it as simple as possible to keep track of
them. Here are the rules:
* Regular (synchronous) functions never contain any checkpoints.
* If you call an async function provided by Trio (``await
<something in trio>``), and it doesn't raise an exception,
then it *always* acts as a checkpoint. (If it does raise an
exception, it might act as a checkpoint or might not.)
* This includes async iterators: If you write ``async for ... in <a
trio object>``, then there will be at least one checkpoint in
each iteration of the loop, and it will still checkpoint if the
iterable is empty.
* Partial exception for async context managers:
Both the entry and exit of an ``async with`` block are
defined as async functions; but for a
particular type of async context manager, it's often the
case that only one of them is able to block, which means
only that one will act as a checkpoint. This is documented
on a case-by-case basis.
* Third-party async functions / iterators / context managers can act
as checkpoints; if you see ``await <something>`` or one of its
friends, then that *might* be a checkpoint. So to be safe, you
should prepare for scheduling or cancellation happening there.
The reason we distinguish between Trio functions and other functions
is that we can't make any guarantees about third party
code. Checkpoint-ness is a transitive property: if function A acts as
a checkpoint, and you write a function that calls function A, then
your function also acts as a checkpoint. If you don't, then it
isn't. So there's nothing stopping someone from writing a function
like::
# technically legal, but bad style:
async def why_is_this_async():
return 7
that never calls any of Trio's async functions. This is an async
function, but it's not a checkpoint. But why make a function async if
it never calls any async functions? It's possible, but it's a bad
idea. If you have a function that's not calling any async functions,
then you should make it synchronous. The people who use your function
will thank you, because it makes it obvious that your function is not
a checkpoint, and their code reviews will go faster.
(Remember how in the tutorial we emphasized the importance of the
:ref:`"async sandwich" <async-sandwich>`, and the way it means that
``await`` ends up being a marker that shows when you're calling a
function that calls a function that ... eventually calls one of Trio's
built-in async functions? The transitivity of async-ness is a
technical requirement that Python imposes, but since it exactly
matches the transitivity of checkpoint-ness, we're able to exploit it
to help you keep track of checkpoints. Pretty sneaky, eh?)
A slightly trickier case is a function like::
async def sleep_or_not(should_sleep):
if should_sleep:
await trio.sleep(1)
else:
pass
Here the function acts as a checkpoint if you call it with
``should_sleep`` set to a true value, but not otherwise. This is why
we emphasize that Trio's own async functions are *unconditional* checkpoints:
they *always* check for cancellation and check for scheduling,
regardless of what arguments they're passed. If you find an async
function in Trio that doesn't follow this rule, then it's a bug and
you should `let us know
<https://github.com/python-trio/trio/issues>`__.
Inside Trio, we're very picky about this, because Trio is the
foundation of the whole system so we think it's worth the extra effort
to make things extra predictable. It's up to you how picky you want to
be in your code. To give you a more realistic example of what this
kind of issue looks like in real life, consider this function::
async def recv_exactly(sock, nbytes):
data = bytearray()
while nbytes > 0:
# recv() reads up to 'nbytes' bytes each time
chunk = await sock.recv(nbytes)
if not chunk:
raise RuntimeError("socket unexpected closed")
nbytes -= len(chunk)
data += chunk
return data
If called with an ``nbytes`` that's greater than zero, then it will
call ``sock.recv`` at least once, and ``recv`` is an async Trio
function, and thus an unconditional checkpoint. So in this case,
``recv_exactly`` acts as a checkpoint. But if we do ``await
recv_exactly(sock, 0)``, then it will immediately return an empty
buffer without executing a checkpoint. If this were a function in
Trio itself, then this wouldn't be acceptable, but you may decide you
don't want to worry about this kind of minor edge case in your own
code.
If you do want to be careful, or if you have some CPU-bound code that
doesn't have enough checkpoints in it, then it's useful to know that
``await trio.sleep(0)`` is an idiomatic way to execute a checkpoint
without doing anything else, and that
:func:`trio.testing.assert_checkpoints` can be used to test that an
arbitrary block of code contains a checkpoint.
Thread safety
~~~~~~~~~~~~~
The vast majority of Trio's API is *not* thread safe: it can only be
used from inside a call to :func:`trio.run`. This manual doesn't
bother documenting this on individual calls; unless specifically noted
otherwise, you should assume that it isn't safe to call any Trio
functions from anywhere except the Trio thread. (But :ref:`see below
<threads>` if you really do need to work with threads.)
.. _time-and-clocks:
Time and clocks
---------------
Every call to :func:`run` has an associated clock.
By default, Trio uses an unspecified monotonic clock, but this can be
changed by passing a custom clock object to :func:`run` (e.g. for
testing).
You should not assume that Trio's internal clock matches any other
clock you have access to, including the clocks of simultaneous calls
to :func:`trio.run` happening in other processes or threads!
The default clock is currently implemented as :func:`time.perf_counter`
plus a large random offset. The idea here is to catch code that
accidentally uses :func:`time.perf_counter` early, which should help keep
our options open for `changing the clock implementation later
<https://github.com/python-trio/trio/issues/33>`__, and (more importantly)
make sure you can be confident that custom clocks like
:class:`trio.testing.MockClock` will work with third-party libraries
you don't control.
.. autofunction:: current_time
.. autofunction:: sleep
.. autofunction:: sleep_until
.. autofunction:: sleep_forever
If you're a mad scientist or otherwise feel the need to take direct
control over the PASSAGE OF TIME ITSELF, then you can implement a
custom :class:`~trio.abc.Clock` class:
.. autoclass:: trio.abc.Clock
:members:
.. _cancellation:
Cancellation and timeouts
-------------------------
Trio has a rich, composable system for cancelling work, either
explicitly or when a timeout expires.
A simple timeout example
~~~~~~~~~~~~~~~~~~~~~~~~
In the simplest case, you can apply a timeout to a block of code::
with trio.move_on_after(30):
result = await do_http_get("https://...")
print("result is", result)
print("with block finished")
We refer to :func:`move_on_after` as creating a "cancel scope", which
contains all the code that runs inside the ``with`` block. If the HTTP
request takes more than 30 seconds to run, then it will be cancelled:
we'll abort the request and we *won't* see ``result is ...`` printed
on the console; instead we'll go straight to printing the ``with block
finished`` message.
.. note::
Note that this is a single 30 second timeout for the entire body of
the ``with`` statement. This is different from what you might have
seen with other Python libraries, where timeouts often refer to
something `more complicated
<https://requests.kennethreitz.org/en/master/user/quickstart/#timeouts>`__. We
think this way is easier to reason about.
How does this work? There's no magic here: Trio is built using
ordinary Python functionality, so we can't just abandon the code
inside the ``with`` block. Instead, we take advantage of Python's
standard way of aborting a large and complex piece of code: we raise
an exception.
Here's the idea: whenever you call a cancellable function like ``await
trio.sleep(...)`` or ``await sock.recv(...)`` – see :ref:`checkpoints`
– then the first thing that function does is to check if there's a
surrounding cancel scope whose timeout has expired, or otherwise been
cancelled. If so, then instead of performing the requested operation,
the function fails immediately with a :exc:`Cancelled` exception. In
this example, this probably happens somewhere deep inside the bowels
of ``do_http_get``. The exception then propagates out like any normal
exception (you could even catch it if you wanted, but that's generally
a bad idea), until it reaches the ``with move_on_after(...):``. And at
this point, the :exc:`Cancelled` exception has done its job – it's
successfully unwound the whole cancelled scope – so
:func:`move_on_after` catches it, and execution continues as normal
after the ``with`` block. And this all works correctly even if you
have nested cancel scopes, because every :exc:`Cancelled` object
carries an invisible marker that makes sure that the cancel scope that
triggered it is the only one that will catch it.
Handling cancellation
~~~~~~~~~~~~~~~~~~~~~
Pretty much any code you write using Trio needs to have some strategy
to handle :exc:`Cancelled` exceptions – even if you didn't set a
timeout, then your caller might (and probably will).
You can catch :exc:`Cancelled`, but you shouldn't! Or more precisely,
if you do catch it, then you should do some cleanup and then re-raise
it or otherwise let it continue propagating (unless you encounter an
error, in which case it's OK to let that propagate instead). To help
remind you of this fact, :exc:`Cancelled` inherits from
:exc:`BaseException`, like :exc:`KeyboardInterrupt` and
:exc:`SystemExit` do, so that it won't be caught by catch-all ``except
Exception:`` blocks.
It's also important in any long-running code to make sure that you
regularly check for cancellation, because otherwise timeouts won't
work! This happens implicitly every time you call a cancellable
operation; see :ref:`below <cancellable-primitives>` for details. If
you have a task that has to do a lot of work without any I/O, then you
can use ``await sleep(0)`` to insert an explicit cancel+schedule
point.
Here's a rule of thumb for designing good Trio-style ("trionic"?)
APIs: if you're writing a reusable function, then you shouldn't take a
``timeout=`` parameter, and instead let your caller worry about
it. This has several advantages. First, it leaves the caller's options
open for deciding how they prefer to handle timeouts – for example,
they might find it easier to work with absolute deadlines instead of
relative timeouts. If they're the ones calling into the cancellation
machinery, then they get to pick, and you don't have to worry about
it. Second, and more importantly, this makes it easier for others to
reuse your code. If you write a ``http_get`` function, and then I come
along later and write a ``log_in_to_twitter`` function that needs to
internally make several ``http_get`` calls, I don't want to have to
figure out how to configure the individual timeouts on each of those
calls – and with Trio's timeout system, it's totally unnecessary.
Of course, this rule doesn't apply to APIs that need to impose
internal timeouts. For example, if you write a ``start_http_server``
function, then you probably should give your caller some way to
configure timeouts on individual requests.
Cancellation semantics
~~~~~~~~~~~~~~~~~~~~~~
You can freely nest cancellation blocks, and each :exc:`Cancelled`
exception "knows" which block it belongs to. So long as you don't stop
it, the exception will keep propagating until it reaches the block
that raised it, at which point it will stop automatically.
Here's an example::
print("starting...")
with trio.move_on_after(5):
with trio.move_on_after(10):
await trio.sleep(20)
print("sleep finished without error")
print("move_on_after(10) finished without error")
print("move_on_after(5) finished without error")
In this code, the outer scope will expire after 5 seconds, causing the
:func:`sleep` call to return early with a :exc:`Cancelled`
exception. Then this exception will propagate through the ``with
move_on_after(10)`` line until it's caught by the ``with
move_on_after(5)`` context manager. So this code will print:
.. code-block:: none
starting...
move_on_after(5) finished without error
The end result is that Trio has successfully cancelled exactly the
work that was happening within the scope that was cancelled.
Looking at this, you might wonder how you can tell whether the inner
block timed out – perhaps you want to do something different, like try
a fallback procedure or report a failure to our caller. To make this
easier, :func:`move_on_after`\´s ``__enter__`` function returns an
object representing this cancel scope, which we can use to check
whether this scope caught a :exc:`Cancelled` exception::
with trio.move_on_after(5) as cancel_scope:
await trio.sleep(10)
print(cancel_scope.cancelled_caught) # prints "True"
The ``cancel_scope`` object also allows you to check or adjust this
scope's deadline, explicitly trigger a cancellation without waiting
for the deadline, check if the scope has already been cancelled, and
so forth – see :class:`CancelScope` below for the full details.
.. _blocking-cleanup-example:
Cancellations in Trio are "level triggered", meaning that once a block
has been cancelled, *all* cancellable operations in that block will
keep raising :exc:`Cancelled`. This helps avoid some pitfalls around
resource clean-up. For example, imagine that we have a function that
connects to a remote server and sends some messages, and then cleans
up on the way out::
with trio.move_on_after(TIMEOUT):
conn = make_connection()
try:
await conn.send_hello_msg()
finally:
await conn.send_goodbye_msg()
Now suppose that the remote server stops responding, so our call to
``await conn.send_hello_msg()`` hangs forever. Fortunately, we were
clever enough to put a timeout around this code, so eventually the
timeout will expire and ``send_hello_msg`` will raise
:exc:`Cancelled`. But then, in the ``finally`` block, we make another
blocking operation, which will also hang forever! At this point, if we
were using :mod:`asyncio` or another library with "edge-triggered"
cancellation, we'd be in trouble: since our timeout already fired, it
wouldn't fire again, and at this point our application would lock up
forever. But in Trio, this *doesn't* happen: the ``await
conn.send_goodbye_msg()`` call is still inside the cancelled block, so
it will also raise :exc:`Cancelled`.
Of course, if you really want to make another blocking call in your
cleanup handler, Trio will let you; it's trying to prevent you from
accidentally shooting yourself in the foot. Intentional foot-shooting
is no problem (or at least – it's not Trio's problem). To do this,
create a new scope, and set its :attr:`~CancelScope.shield`
attribute to :data:`True`::
with trio.move_on_after(TIMEOUT):
conn = make_connection()
try:
await conn.send_hello_msg()
finally:
with trio.move_on_after(CLEANUP_TIMEOUT) as cleanup_scope:
cleanup_scope.shield = True
await conn.send_goodbye_msg()
So long as you're inside a scope with ``shield = True`` set, then
you'll be protected from outside cancellations. Note though that this
*only* applies to *outside* cancellations: if ``CLEANUP_TIMEOUT``
expires then ``await conn.send_goodbye_msg()`` will still be
cancelled, and if ``await conn.send_goodbye_msg()`` call uses any
timeouts internally, then those will continue to work normally as
well. This is a pretty advanced feature that most people probably
won't use, but it's there for the rare cases where you need it.
.. _cancellable-primitives:
Cancellation and primitive operations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
We've talked a lot about what happens when an operation is cancelled,
and how you need to be prepared for this whenever calling a
cancellable operation... but we haven't gone into the details about
which operations are cancellable, and how exactly they behave when
they're cancelled.
Here's the rule: if it's in the ``trio`` namespace, and you use ``await``
to call it, then it's cancellable (see :ref:`checkpoints`
above). Cancellable means:
* If you try to call it when inside a cancelled scope, then it will
raise :exc:`Cancelled`.
* If it blocks, and while it's blocked then one of the scopes around
it becomes cancelled, it will return early and raise
:exc:`Cancelled`.
* Raising :exc:`Cancelled` means that the operation *did not
happen*. If a Trio socket's ``send`` method raises :exc:`Cancelled`,
then no data was sent. If a Trio socket's ``recv`` method raises
:exc:`Cancelled` then no data was lost – it's still sitting in the
socket receive buffer waiting for you to call ``recv`` again. And so
forth.
There are a few idiosyncratic cases where external constraints make it
impossible to fully implement these semantics. These are always
documented. There is also one systematic exception:
* Async cleanup operations – like ``__aexit__`` methods or async close
methods – are cancellable just like anything else *except* that if
they are cancelled, they still perform a minimum level of cleanup
before raising :exc:`Cancelled`.
For example, closing a TLS-wrapped socket normally involves sending a
notification to the remote peer, so that they can be cryptographically
assured that you really meant to close the socket, and your connection
wasn't just broken by a man-in-the-middle attacker. But handling this
robustly is a bit tricky. Remember our :ref:`example
<blocking-cleanup-example>` above where the blocking
``send_goodbye_msg`` caused problems? That's exactly how closing a TLS
socket works: if the remote peer has disappeared, then our code may
never be able to actually send our shutdown notification, and it would
be nice if it didn't block forever trying. Therefore, the method for
closing a TLS-wrapped socket will *try* to send that notification –
and if it gets cancelled, then it will give up on sending the message,
but *will* still close the underlying socket before raising
:exc:`Cancelled`, so at least you don't leak that resource.
Cancellation API details
~~~~~~~~~~~~~~~~~~~~~~~~
:func:`move_on_after` and all the other cancellation facilities provided
by Trio are ultimately implemented in terms of :class:`CancelScope`
objects.
.. autoclass:: trio.CancelScope
.. autoattribute:: deadline
.. autoattribute:: shield
.. automethod:: cancel()
.. attribute:: cancelled_caught
Readonly :class:`bool`. Records whether this scope caught a
:exc:`~trio.Cancelled` exception. This requires two things: (1)
the ``with`` block exited with a :exc:`~trio.Cancelled`
exception, and (2) this scope is the one that was responsible
for triggering this :exc:`~trio.Cancelled` exception.
.. autoattribute:: cancel_called
Often there is no need to create :class:`CancelScope` object. Trio
already includes :attr:`~trio.Nursery.cancel_scope` attribute in a
task-related :class:`Nursery` object. We will cover nurseries later in
the manual.
Trio also provides several convenience functions for the common
situation of just wanting to impose a timeout on some code:
.. autofunction:: move_on_after
:with: cancel_scope
.. autofunction:: move_on_at
:with: cancel_scope
.. autofunction:: fail_after
:with: cancel_scope
.. autofunction:: fail_at
:with: cancel_scope
Cheat sheet:
* If you want to impose a timeout on a function, but you don't care
whether it timed out or not::
with trio.move_on_after(TIMEOUT):
await do_whatever()
# carry on!
* If you want to impose a timeout on a function, and then do some
recovery if it timed out::
with trio.move_on_after(TIMEOUT) as cancel_scope:
await do_whatever()
if cancel_scope.cancelled_caught:
# The operation timed out, try something else
try_to_recover()
* If you want to impose a timeout on a function, and then if it times
out then just give up and raise an error for your caller to deal
with::
with trio.fail_after(TIMEOUT):
await do_whatever()
It's also possible to check what the current effective deadline is,
which is sometimes useful:
.. autofunction:: current_effective_deadline
.. _tasks:
Tasks let you do multiple things at once
----------------------------------------
One of Trio's core design principles is: *no implicit
concurrency*. Every function executes in a straightforward,
top-to-bottom manner, finishing each operation before moving on to the
next – *like Guido intended*.
But, of course, the entire point of an async library is to let you do
multiple things at once. The one and only way to do that in Trio is
through the task spawning interface. So if you want your program to
walk *and* chew gum, this is the section for you.
Nurseries and spawning
~~~~~~~~~~~~~~~~~~~~~~
Most libraries for concurrent programming let you start new child
tasks (or threads, or whatever) willy-nilly, whenever and where-ever
you feel like it. Trio is a bit different: you can't start a child
task unless you're prepared to be a responsible parent. The way you
demonstrate your responsibility is by creating a nursery::
async with trio.open_nursery() as nursery:
...
And once you have a reference to a nursery object, you can start
children in that nursery::
async def child():
...
async def parent():
async with trio.open_nursery() as nursery:
# Make two concurrent calls to child()
nursery.start_soon(child)
nursery.start_soon(child)
This means that tasks form a tree: when you call :func:`run`, then
this creates an initial task, and all your other tasks will be
children, grandchildren, etc. of the initial task.
Essentially, the body of the ``async with`` block acts like an initial
task that's running inside the nursery, and then each call to
``nursery.start_soon`` adds another task that runs in parallel. Two
crucial things to keep in mind:
* If any task inside the nursery finishes with an unhandled exception,
then the nursery immediately cancels all the tasks inside the
nursery.
* Since all of the tasks are running concurrently inside the ``async
with`` block, the block does not exit until *all* tasks have
completed. If you've used other concurrency frameworks, then you can
think of it as, the de-indentation at the end of the ``async with``
automatically "joins" (waits for) all of the tasks in the nursery.
* Once all the tasks have finished, then:
* The nursery is marked as "closed", meaning that no new tasks can
be started inside it.
* Any unhandled exceptions are re-raised inside the parent task. If
there are multiple exceptions, then they're collected up into a
single :exc:`BaseExceptionGroup` or :exc:`ExceptionGroup` exception.
Since all tasks are descendents of the initial task, one consequence
of this is that :func:`run` can't finish until all tasks have
finished.
.. note::
A return statement will not cancel the nursery if it still has tasks running::
async def main():
async with trio.open_nursery() as nursery:
nursery.start_soon(trio.sleep, 5)
return
trio.run(main)
This code will wait 5 seconds (for the child task to finish), and then return.
Child tasks and cancellation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In Trio, child tasks inherit the parent nursery's cancel scopes. So in
this example, both the child tasks will be cancelled when the timeout
expires::
with trio.move_on_after(TIMEOUT):
async with trio.open_nursery() as nursery:
nursery.start_soon(child1)
nursery.start_soon(child2)
Note that what matters here is the scopes that were active when
:func:`open_nursery` was called, *not* the scopes active when
``start_soon`` is called. So for example, the timeout block below does
nothing at all::
async with trio.open_nursery() as nursery:
with trio.move_on_after(TIMEOUT): # don't do this!
nursery.start_soon(child)
Why is this so? Well, ``start_soon()`` returns as soon as it has scheduled the new task to start running. The flow of execution in the parent then continues on to exit the ``with trio.move_on_after(TIMEOUT):`` block, at which point Trio forgets about the timeout entirely. In order for the timeout to apply to the child task, Trio must be able to tell that its associated cancel scope will stay open for at least as long as the child task is executing. And Trio can only know that for sure if the cancel scope block is outside the nursery block.
You might wonder why Trio can't just remember "this task should be cancelled in ``TIMEOUT`` seconds", even after the ``with trio.move_on_after(TIMEOUT):`` block is gone. The reason has to do with :ref:`how cancellation is implemented <cancellation>`. Recall that cancellation is represented by a `Cancelled` exception, which eventually needs to be caught by the cancel scope that caused it. (Otherwise, the exception would take down your whole program!) In order to be able to cancel the child tasks, the cancel scope has to be able to "see" the `Cancelled` exceptions that they raise -- and those exceptions come out of the ``async with open_nursery()`` block, not out of the call to ``start_soon()``.
If you want a timeout to apply to one task but not another, then you need to put the cancel scope in that individual task's function -- ``child()``, in this example.
.. _exceptiongroups:
Errors in multiple child tasks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Normally, in Python, only one thing happens at a time, which means
that only one thing can go wrong at a time. Trio has no such
limitation. Consider code like::
async def broken1():
d = {}
return d["missing"]
async def broken2():
seq = range(10)
return seq[20]
async def parent():
async with trio.open_nursery() as nursery:
nursery.start_soon(broken1)
nursery.start_soon(broken2)
``broken1`` raises ``KeyError``. ``broken2`` raises
``IndexError``. Obviously ``parent`` should raise some error, but
what? The answer is that both exceptions are grouped in an :exc:`ExceptionGroup`.
:exc:`ExceptionGroup` and its parent class :exc:`BaseExceptionGroup` are used to
encapsulate multiple exceptions being raised at once.
To catch individual exceptions encapsulated in an exception group, the ``except*``
clause was introduced in Python 3.11 (:pep:`654`). Here's how it works::
try:
async with trio.open_nursery() as nursery:
nursery.start_soon(broken1)
nursery.start_soon(broken2)
except* KeyError as excgroup:
for exc in excgroup.exceptions:
... # handle each KeyError
except* IndexError as excgroup:
for exc in excgroup.exceptions:
... # handle each IndexError
If you want to reraise exceptions, or raise new ones, you can do so, but be aware that
exceptions raised in ``except*`` sections will be raised together in a new exception
group.
But what if you can't use ``except*`` just yet? Well, for that there is the handy
exceptiongroup_ library which lets you approximate this behavior with exception handler
callbacks::
from exceptiongroup import catch
def handle_keyerrors(excgroup):
for exc in excgroup.exceptions:
... # handle each KeyError
def handle_indexerrors(excgroup):
for exc in excgroup.exceptions:
... # handle each IndexError
with catch({
KeyError: handle_keyerrors,
IndexError: handle_indexerrors
}):
async with trio.open_nursery() as nursery:
nursery.start_soon(broken1)
nursery.start_soon(broken2)
The semantics for the handler functions are equal to ``except*`` blocks, except for
setting local variables. If you need to set local variables, you need to declare them
inside the handler function(s) with the ``nonlocal`` keyword::
def handle_keyerrors(excgroup):
nonlocal myflag
myflag = True
myflag = False
with catch({KeyError: handle_keyerrors}):
async with trio.open_nursery() as nursery:
nursery.start_soon(broken1)
.. _strict_exception_groups:
"Strict" versus "loose" ExceptionGroup semantics
++++++++++++++++++++++++++++++++++++++++++++++++
..
TODO: rewrite this (and possible other) sections from the new strict-by-default perspective, under the heading "Deprecated: non-strict ExceptionGroups" - to explain that it only exists for backwards-compatibility, will be removed in future, and that we recommend against it for all new code.
Ideally, in some abstract sense we'd want everything that *can* raise an
`ExceptionGroup` to *always* raise an `ExceptionGroup` (rather than, say, a single
`ValueError`). Otherwise, it would be easy to accidentally write something like ``except
ValueError:`` (not ``except*``), which works if a single exception is raised but fails to
catch _anything_ in the case of multiple simultaneous exceptions (even if one of them is
a ValueError). However, this is not how Trio worked in the past: as a concession to
practicality when the ``except*`` syntax hadn't been dreamed up yet, the old
``trio.MultiError`` was raised only when at least two exceptions occurred
simultaneously. Adding a layer of `ExceptionGroup` around every nursery, while
theoretically appealing, would probably break a lot of existing code in practice.
Therefore, we've chosen to gate the newer, "stricter" behavior behind a parameter
called ``strict_exception_groups``. This is accepted as a parameter to
:func:`open_nursery`, to set the behavior for that nursery, and to :func:`trio.run`,
to set the default behavior for any nursery in your program that doesn't override it.
* With ``strict_exception_groups=True``, the exception(s) coming out of a nursery will
always be wrapped in an `ExceptionGroup`, so you'll know that if you're handling
single errors correctly, multiple simultaneous errors will work as well.
* With ``strict_exception_groups=False``, a nursery in which only one task has failed
will raise that task's exception without an additional layer of `ExceptionGroup`
wrapping, so you'll get maximum compatibility with code that was written to
support older versions of Trio.
The default is set to ``strict_exception_groups=True``, in line with the default behaviour
of ``TaskGroup`` in asyncio and anyio. We've also found that non-strict mode makes it
too easy to neglect the possibility of several exceptions being raised concurrently,
causing nasty latent bugs when errors occur under load.
.. _exceptiongroup: https://pypi.org/project/exceptiongroup/
Spawning tasks without becoming a parent
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sometimes it doesn't make sense for the task that starts a child to
take on responsibility for watching it. For example, a server task may
want to start a new task for each connection, but it can't listen for
connections and supervise children at the same time.
The solution here is simple once you see it: there's no requirement
that a nursery object stay in the task that created it! We can write
code like this::
async def new_connection_listener(handler, nursery):
while True:
conn = await get_new_connection()
nursery.start_soon(handler, conn)
async def server(handler):
async with trio.open_nursery() as nursery:
nursery.start_soon(new_connection_listener, handler, nursery)
Notice that ``server`` opens a nursery and passes it to
``new_connection_listener``, and then ``new_connection_listener`` is
able to start new tasks as "siblings" of itself. Of course, in this
case, we could just as well have written::
async def server(handler):
async with trio.open_nursery() as nursery:
while True:
conn = await get_new_connection()
nursery.start_soon(handler, conn)
\...but sometimes things aren't so simple, and this trick comes in
handy.
One thing to remember, though: cancel scopes are inherited from the
nursery, **not** from the task that calls ``start_soon``. So in this
example, the timeout does *not* apply to ``child`` (or to anything
else)::
async def do_spawn(nursery):
with trio.move_on_after(TIMEOUT): # don't do this, it has no effect
nursery.start_soon(child)
async with trio.open_nursery() as nursery:
nursery.start_soon(do_spawn, nursery)
Custom supervisors
~~~~~~~~~~~~~~~~~~
The default cleanup logic is often sufficient for simple cases, but
what if you want a more sophisticated supervisor? For example, maybe
you have `Erlang envy <http://learnyousomeerlang.com/supervisors>`__
and want features like automatic restart of crashed tasks. Trio itself
doesn't provide these kinds of features, but you can build them on
top; Trio's goal is to enforce basic hygiene and then get out of your
way. (Specifically: Trio won't let you build a supervisor that exits
and leaves orphaned tasks behind, and if you have an unhandled
exception due to bugs or laziness then Trio will make sure they
propagate.) And then you can wrap your fancy supervisor up in a
library and put it on PyPI, because supervisors are tricky and there's
no reason everyone should have to write their own.
For example, here's a function that takes a list of functions, runs
them all concurrently, and returns the result from the one that
finishes first::
async def race(*async_fns):
if not async_fns:
raise ValueError("must pass at least one argument")
winner = None
async def jockey(async_fn, cancel_scope):
nonlocal winner
winner = await async_fn()
cancel_scope.cancel()
async with trio.open_nursery() as nursery:
for async_fn in async_fns:
nursery.start_soon(jockey, async_fn, nursery.cancel_scope)
return winner
This works by starting a set of tasks which each try to run their
function. As soon as the first function completes its execution, the task will set the nonlocal variable ``winner``
from the outer scope to the result of the function, and cancel the other tasks using the passed in cancel scope. Once all tasks
have been cancelled (which exits the nursery block), the variable ``winner`` will be returned.
Here if one or more of the racing functions raises an unhandled
exception then Trio's normal handling kicks in: it cancels the others
and then propagates the exception. If you want different behavior, you
can get that by adding a ``try`` block to the ``jockey`` function to
catch exceptions and handle them however you like.
Task-related API details
~~~~~~~~~~~~~~~~~~~~~~~~
The nursery API
+++++++++++++++
.. autofunction:: open_nursery
:async-with: nursery
.. autoclass:: Nursery()
:members: child_tasks, parent_task
.. automethod:: start(async_fn, *args, name = None)
.. automethod:: start_soon(async_fn, *args, name = None)
.. attribute:: TASK_STATUS_IGNORED
:type: TaskStatus
See :meth:`Nursery.start`.
.. autoclass:: TaskStatus(Protocol[StatusT])
:members:
.. _task-local-storage:
Task-local storage
------------------
Suppose you're writing a server that responds to network requests, and
you log some information about each request as you process it. If the
server is busy and there are multiple requests being handled at the
same time, then you might end up with logs like this:
.. code-block:: none
Request handler started
Request handler started
Request handler finished
Request handler finished
In this log, it's hard to know which lines came from which
request. (Did the request that started first also finish first, or
not?) One way to solve this is to assign each request a unique
identifier, and then include this identifier in each log message:
.. code-block:: none
request 1: Request handler started
request 2: Request handler started
request 2: Request handler finished
request 1: Request handler finished
This way we can see that request 1 was slow: it started before request
2 but finished afterwards. (You can also get `much fancier
<https://opentracing.io/docs/>`__, but this is enough for an
example.)
Now, here's the problem: how does the logging code know what the
request identifier is? One approach would be to explicitly pass it
around to every function that might want to emit logs... but that's
basically every function, because you never know when you might need
to add a ``log.debug(...)`` call to some utility function buried deep
in the call stack, and when you're in the middle of a debugging a
nasty problem that last thing you want is to have to stop first and
refactor everything to pass through the request identifier! Sometimes
this is the right solution, but other times it would be much more
convenient if we could store the identifier in a global variable, so
that the logging function could look it up whenever it needed
it. Except... a global variable can only have one value at a time, so
if we have multiple handlers running at once then this isn't going to
work. What we need is something that's *like* a global variable, but
that can have different values depending on which request handler is
accessing it.
To solve this problem, Python has a module in the standard
library: :mod:`contextvars`.
Here's a toy example demonstrating how to use :mod:`contextvars`:
.. literalinclude:: reference-core/contextvar-example.py
Example output (yours may differ slightly):
.. code-block:: none
request 1: Request handler started
request 2: Request handler started
request 0: Request handler started
request 2: Helper task a started
request 2: Helper task b started
request 1: Helper task a started