Skip to content

Conventional semantics for acquiring a stream from a resource combined with drop #552

@alexcrichton

Description

@alexcrichton

Many upcoming WASIp3 APIs are based on stream and future. Many APIs also work with a resource such as an HTTP body. This gives rise to a situation where a guest can receive an HTTP request, acquire the body as a stream<u8>, and then drop the original request resource itself. The question then faced is what happens? This basic scenario is applicable to a broad number of other situations such as receiving a stream of bytes from a TCP socket or generalizing to passing a stream<u8> to a "transformer" component which performs compression/encryption/etc.

In WASIp2 this question was resolved with the WASI-specific concept of a "child resource" where if the parent was dropped before the child a trap would be generated. A question for WASIp3 is whether we want to perpetuate this. I had some discussion today with @vados-cosmonic, @dicej, and @rvolosatovs and we discussed four possible ways to answer the question of what to do in this situation:

  1. Do what WASIp2 did and trap. This would likely require some form of officially component model integration as streams are managed by Wasmtime where body resources, for example, are managed by WASI implementations. The downside of this approach is that this is already not necessarily easy to integrate with WASIp2 today. Put another way if a trap is generated then guests have to be precise about the exact order handles are dropped in. This is not always easy for GC languages for example. Another example this is not easy to do is in a composition use case where a stream is passed to an entirely separate component. In such a situation it's not always clear when the receiver is done with the stream so the caller may not ever know when it's safe to close the "parent" resource.

  2. Reference-count all resources. This is different from WASIp2 where closing a parent before the child wouldn't do anything, the child would still be valid. Whatever the child needed to keep alive to stay operational would be kept alive. Embedders would be required to do some form of reference counting to ensure that resources/streams stay working even when the "parent" goes away. This would be easier for GC languages and composition use cases in that drop order doesn't matter, and everyone's just responsible for dropping their own handle. A downside of this approach is that it's more difficult to ensure that host state is indeed deallocated when a resource is dropped. Host state is still kept alive by any child handles, and a guest may not know where those are or how to close them.

  3. "Close children on parent drop" where when a parent resource is closed it doesn't generate a trap but it does forcibly close all child resources. For example a stream would be terminated, a future would resolve to a "default value", child resources would "start returning errors", etc. Basically the trap is no longer present but semantically the meaning/state of all child handles is different after the parent resource has dropped. This has the benefit of guaranteeing that when a resource is dropped that any host state associated with it is possible to drop. A downside of this approach though is similar to (1) where while drops can happen in any order it's still ambiguous to know when a drop of a parent resource is possible. For example if a component wants to hand off a body stream to another component it still has to wait to drop the original request resource until the body is done being processed. This may not always be as simple as waiting on an async func for example.

  4. Dropping a parent resource blocks until all children are dropped. This is sort of like "async drop" but it semantically means that when a resource is dropped that it'll wait for all children to go away and/or finish their I/O. This has the benefit of a resource can be dropped at any time. This has a downside, though, that by blocking a drop of a parent it could block a component for an indefinitely long time while I/O completes.

WASIp3 doesn't today take a firm stance on which of these possible routes is chosen for various APIs. The goal here is to make a high-level decision about how best to manage this situation and determine if it's best solved in the Component Model, WASI, or runtimes. Personally I'd like to advocate for option (2) above. While not without its downsides I feel that it has the fewest downsides and what remains is possible to solve with new WASI APIs. For example an API could be added to take an own<tcp-socket> which forcibly closes it, peforming the semantics of option (3) but in a TCP-specific context. Effectively the benefits of (2) would remain (anyone can close whenever) also with the benefits of (3) of being able to close something and know the host-side resource has been disposed of.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions