Skip to content

Commit cef5c99

Browse files
committed
Async CABI: don't drop lend_count until caller notified of DONE event
1 parent 76a3927 commit cef5c99

File tree

2 files changed

+92
-63
lines changed

2 files changed

+92
-63
lines changed

design/mvp/CanonicalABI.md

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -636,15 +636,9 @@ above and allows a pending task to start.
636636

637637
While `canon_lift` creates `Task`s, `canon_lower` creates `Subtask` objects.
638638
Like `Task`, `Subtask` is a subclass of `Context` and stores the `ft` and
639-
`opts` of its `canon lower`. Importantly, the `Context.task` field of a
640-
`Subtask` refers to the [current task] wwhich called `canon lower`, thereby
641-
linking all subtasks to their supertask, maintaining a (possibly asynchronous)
642-
call tree. The `on_start` and `on_return` methods of `Subtask` are passed (by
643-
`canon_lower` below) to the callee, which will call them to lift its arguments
644-
and lower its results. Using callbacks provides the callee the flexibility to
645-
control when arguments are lowered (which can vary due to backpressure) and
646-
when results are lifted (which can also vary due to when `task.return` is
647-
called).
639+
`opts` of its `canon lower`. Importantly, the `task` field of a `Subtask`
640+
refers to the [current task] which called `canon lower`, thereby linking all
641+
subtasks to their supertask, maintaining the (possibly asynchronous) call tree.
648642
```python
649643
class Subtask(Context):
650644
ft: FuncType
@@ -664,7 +658,57 @@ class Subtask(Context):
664658
self.lenders = []
665659
self.notify_supertask = False
666660
self.enqueued = False
661+
```
662+
663+
The `lenders` field of `Subtask` maintains a list of all the owned handles
664+
that have been lent to a subtask and must therefor not be dropped until the
665+
subtask completes. The `add_lender` method is called (below) when lifting an
666+
owned handle and increments the `lend_count` of the owned handle, which is
667+
guarded to be zero by `canon_resource_drop` (below). The `release_lenders`
668+
method releases all the `lend_count`s of all such handles lifted for the
669+
subtask and is called (below) when the subtask finishes.
670+
```python
671+
def add_lender(self, lending_handle):
672+
assert(lending_handle.own)
673+
lending_handle.lend_count += 1
674+
self.lenders.append(lending_handle)
675+
676+
def release_lenders(self):
677+
for h in self.lenders:
678+
h.lend_count -= 1
679+
```
680+
Note, the `lenders` list usually has a fixed size (in all cases except when a
681+
function signature has `borrow`s in `list`s or `stream`s) and thus can be
682+
stored inline in the native stack frame.
683+
684+
The `maybe_notify_supertask` method called by `on_start`, `on_return` and
685+
`finish` (next) only sends events to the supertask if this `Subtask` actually
686+
blocked and got added to the `async_subtasks` table (signalled by
687+
`notify_supertask` being set). Additionally, `maybe_notify_supertask` uses the
688+
`enqueued` flag and the fact that "events" are first-class functions to
689+
collapse N events down to 1 if a subtask advances state multiple times before
690+
the supertask receives the event which, in turn, avoids unnecessarily spamming
691+
the event loop when only the most recent state matters.
692+
```python
693+
def maybe_notify_supertask(self):
694+
if self.notify_supertask and not self.enqueued:
695+
self.enqueued = True
696+
def subtask_event():
697+
self.enqueued = False
698+
i = self.inst.async_subtasks.array.index(self)
699+
if self.state == CallState.DONE:
700+
self.release_lenders()
701+
return (EventCode(self.state), i)
702+
self.task.notify(subtask_event)
703+
```
667704

705+
The `on_start` and `on_return` methods of `Subtask` are passed (by
706+
`canon_lower` below) to the callee to be called to lift its arguments and
707+
lower its results. Using callbacks provides the callee the flexibility to
708+
control when arguments are lowered (which can vary due to backpressure) and
709+
when results are lifted (which can also vary due to when `task.return` is
710+
called).
711+
```python
668712
def on_start(self):
669713
assert(self.state == CallState.STARTING)
670714
self.state = CallState.STARTED
@@ -681,42 +725,20 @@ class Subtask(Context):
681725
ts = self.ft.result_types()
682726
self.flat_results = lower_flat_values(self, max_flat, vs, ts, self.flat_args)
683727
```
684-
The `maybe_notify_supertask` method called by `on_start` and `on_return` only
685-
sends events to the supertask if this `Subtask` actually blocked and got added
686-
to the `async_subtasks` table (signalled by `notify_supertask` being set).
687-
Additionally, `maybe_notify_supertask` uses the `enqueued` flag and the fact
688-
that "events" are first-class functions to collapse N events down to 1 if a
689-
subtask advances state multiple times before the supertask receives the event
690-
which, in turn, avoids unnecessarily spamming the event loop when only the most
691-
recent state matters.
692-
```python
693-
def maybe_notify_supertask(self):
694-
if self.notify_supertask and not self.enqueued:
695-
self.enqueued = True
696-
def subtask_event():
697-
self.enqueued = False
698-
i = self.inst.async_subtasks.array.index(self)
699-
return (EventCode(self.state), i)
700-
self.task.notify(subtask_event)
701-
```
702-
Lastly, a `Subtask` tracks the owned handles that have been lent for the
703-
duration of the call, ensuring that the caller doesn't drop them during the
704-
call (which might create a dangling borrowed handle in the callee). Note, the
705-
`lenders` list usually has a fixed size (in all cases except when a function
706-
signature has `borrow`s in `list`s) and thus can be stored inline in the native
707-
stack frame.
708-
```python
709-
def track_owning_lend(self, lending_handle):
710-
assert(lending_handle.own)
711-
lending_handle.lend_count += 1
712-
self.lenders.append(lending_handle)
713728

729+
Lastly, when a `Subtask` finishes, it calls `release_lenders` to allow owned
730+
handles passed to this subtask to be dropped. In the synchronous or eager case
731+
this happens immediately before returning to the caller. In the
732+
asynchronous+blocking case, this happens right before the `CallState.DONE`
733+
event is delivered to the guest program.
734+
```python
714735
def finish(self):
715736
assert(self.state == CallState.RETURNED)
716-
for h in self.lenders:
717-
h.lend_count -= 1
718737
self.state = CallState.DONE
719-
self.maybe_notify_supertask()
738+
if self.opts.sync or not self.notify_supertask:
739+
self.release_lenders()
740+
else:
741+
self.maybe_notify_supertask()
720742
return self.flat_results
721743
```
722744

@@ -1141,12 +1163,12 @@ def lift_borrow(cx, i, t):
11411163
assert(isinstance(cx, Subtask))
11421164
h = cx.inst.handles.get(t.rt, i)
11431165
if h.own:
1144-
cx.track_owning_lend(h)
1166+
cx.add_lender(h)
11451167
return h.rep
11461168
```
1147-
The `track_owning_lend` call to `Context` participates in the enforcement
1148-
of the dynamic borrow rules, which keep the source `own` handle alive until the
1149-
end of the call (as an intentionally-conservative upper bound on how long the
1169+
The `add_lender` call to `Context` participates in the enforcement of the
1170+
dynamic borrow rules, which keep the source `own` handle alive until the end
1171+
of the call (as an intentionally-conservative upper bound on how long the
11501172
`borrow` handle can be held). This tracking is only required when `h` is an
11511173
`own` handle because, when `h` is a `borrow` handle, this tracking has already
11521174
happened (when the originating `own` handle was lifted) for a strictly longer

design/mvp/canonical-abi/definitions.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,26 @@ def __init__(self, opts, ft, task, flat_args):
489489
self.notify_supertask = False
490490
self.enqueued = False
491491

492+
def add_lender(self, lending_handle):
493+
assert(lending_handle.own)
494+
lending_handle.lend_count += 1
495+
self.lenders.append(lending_handle)
496+
497+
def release_lenders(self):
498+
for h in self.lenders:
499+
h.lend_count -= 1
500+
501+
def maybe_notify_supertask(self):
502+
if self.notify_supertask and not self.enqueued:
503+
self.enqueued = True
504+
def subtask_event():
505+
self.enqueued = False
506+
i = self.inst.async_subtasks.array.index(self)
507+
if self.state == CallState.DONE:
508+
self.release_lenders()
509+
return (EventCode(self.state), i)
510+
self.task.notify(subtask_event)
511+
492512
def on_start(self):
493513
assert(self.state == CallState.STARTING)
494514
self.state = CallState.STARTED
@@ -505,26 +525,13 @@ def on_return(self, vs):
505525
ts = self.ft.result_types()
506526
self.flat_results = lower_flat_values(self, max_flat, vs, ts, self.flat_args)
507527

508-
def maybe_notify_supertask(self):
509-
if self.notify_supertask and not self.enqueued:
510-
self.enqueued = True
511-
def subtask_event():
512-
self.enqueued = False
513-
i = self.inst.async_subtasks.array.index(self)
514-
return (EventCode(self.state), i)
515-
self.task.notify(subtask_event)
516-
517-
def track_owning_lend(self, lending_handle):
518-
assert(lending_handle.own)
519-
lending_handle.lend_count += 1
520-
self.lenders.append(lending_handle)
521-
522528
def finish(self):
523529
assert(self.state == CallState.RETURNED)
524-
for h in self.lenders:
525-
h.lend_count -= 1
526530
self.state = CallState.DONE
527-
self.maybe_notify_supertask()
531+
if self.opts.sync or not self.notify_supertask:
532+
self.release_lenders()
533+
else:
534+
self.maybe_notify_supertask()
528535
return self.flat_results
529536

530537
### Despecialization
@@ -820,7 +827,7 @@ def lift_borrow(cx, i, t):
820827
assert(isinstance(cx, Subtask))
821828
h = cx.inst.handles.get(t.rt, i)
822829
if h.own:
823-
cx.track_owning_lend(h)
830+
cx.add_lender(h)
824831
return h.rep
825832

826833
### Storing

0 commit comments

Comments
 (0)