Pinning and Service #319
Labels
A-service
Area: The tower `Service` trait
C-musing
Category: musings about a better world
I-needs-decision
Issues in need of decision.
Now that we are moving to
std::future
(#279), we have to make a decision as to how this affects theService
trait. The currentService
trait does not mentionPin
:Consider what happens in the common case where
poll_ready
wants to internally poll another future. To do so, it must have aPin
to that future. Since we do not passPin<&mut Self>
topoll_ready
, this means that theService
must internally box such futures (or otherwise cause them to beUnpin
) so thatPin::new
works. It can't usePin::new_unchecked
, since it cannot guarantee that the service itself isn't moved.Effectively, this means that we are requiring
Service
to beUnpin
(note that ifService
doesn't have any internal futures to poll, then it will trivially beUnpin
). This adds some additional "hidden" costs to each service, as they effectively have to box (or otherwise heap-allocate) any internal futures. Ignoring those costs, however, currently, that restriction is implicit in the type signature. We have a few options here as to how to "fix" this.Option A: keep everything as-is
Implementors of
Service
will have to discover that they must beUnpin
, or will write unsound unsafe code to access fields (because again,Pin::new_unchecked
is not okay in this setting).Option B: add
Unpin
bound toService
This makes it explicit that implementors of
Service
must implementUnpin
, but does not actually make it more difficult to implementService
. Adding this bound does have the added benefit that hopefully implementors won't start playing with unsafePin::new_unchecked
stuff as they will be forced to make their type implementUnpin
.Option C: B, and also make
poll_ready
takePin
This makes little practical difference to safety, since we still require
Service: Unpin
, so thePin<&mut Self>
does nothing (it is alwaysDerefMut
), and can be constructed safely withPin::new
. It does have the advantage that the API now looks more like other async APIs wherepoll
methods takePin
. It comes at an ergonomic cost, in that callers must now wrap theService
withPin::new
before callingpoll_ready
.It's worth pausing here to observe that we now see why the
Unpin
bound is really necessary. We wantpoll_ready
to takePin<&mut Self>
, but we wantcall
to take&mut self
. This is fundamentally not okay unless the service isUnpin
, sincecall
could trivially moveself
even though it was pinned whenpoll_ready
was called. Which leads us to the final proposal:Option D:
Pin
everywhereThis drops the
Unpin
bound onService
, but comes at a potentially large ergonomic cost:call
now requires you to have aPin
to the service. This means that you can no longer just hold on to aService
and poll and call it at will. Instead, you will have to pin it in the caller in order to use it. The caller will then have to either annotate their code withPin::new
, usepin-project
, or otherwise arrange for aPin
to the service. The removal of the restriction thatService: Unpin
does mean that implementors ofService
no longer need to box internal futures though, which may avoid some processing overheads, especially in deep stacked services.The text was updated successfully, but these errors were encountered: