Skip to content

Consider defining an "instance reuse hint" #307

Description

@lukewagner

There's an interesting question and discussion in wasi-http/#95 that, by the end, doesn't feel specific to "HTTP" at all and thus perhaps deserving of being addressed more generally in the Component Model.

So the basic question is: when a host is given a component to run, does the host reuse the component instance between export calls (and, if so, to what degree?) or does the host create a fresh instance every time. In general, there are pragmatic benefits to using a fresh instance each time (mitigating exploits, clearing out leaks, less non-determinism) which takes advantage of wasm's potentially very-low startup cost. However, there are many valid reasons why a component can have an expensive-enough initialization (calling non-deterministic imports and thus not wizer-able) that this instance-per-export-call default will lead to unacceptable performance. If some hosts reuse instances and others don't, then the resulting performance difference may be significant enough to be a real portability problem. As with core wasm, while it's hard to explicitly specify a "cost model", it ends up being an important implicit part of the design, so I think it's worth thinking through what we want to actually happen and what to tell producer toolchains and runtimes.

First, to enumerate some "can't we just"s that are tempting but I don't think fully address the problem:

  • Do nothing. This will initially lead to unportability and then, once the pain is felt, lead to...
  • Say that the default is to reuse instances, but that hosts are free to isolate more-aggressively if they want. Over time, hosts will be pressured to adopt a default-reuse stance (anything else will occasionally surprise their users and make them look bad compared to other hosts that default-reuse) with the net-effect being to flip the default away from the pragmatically-safer-and-more-resilient option (and defaults matter).
  • Say that a component that needs instance reuse shall export a special "init" function (with a standardized name). E.g., in wasi-http/#95, I initially suggested a wasi:io/slow-init interface. This approach really wants the init function to be called transitively/automatically (just like the start function), making it more "magical" than most WASI interfaces to the toolchain and raising the question of why not build it into the C-M. More subtly but problematically: in general, adding an export to a component is expected to be a backwards-compatible action (the new component is a subtype of the old component's type, after all). But with this magic init function, that expectation breaks: if some component (in your transitive dependency DAG) adds an init function, everyone now needs to recompile, despite there being not obvious type error (you just need a special case the magic "init" interface everywhere this problem might arise).
  • Add a runonce function attribute that components can use to signal that a component instance must not be reused. Just because a component instance may be reused doesn't mean it should be reused. Moreover, runonce is rather client-unfriendly: when JS or Python or Rust or any language with module-like bindings imports a component, the default expectation is "I can call the imported functions N times"; runonce would be a totally foreign and annoying constraint. Thus, components wouldn't want to set it, but if setting it is how you get nice temporal isolation, you're encouraged to set it unnecessarily.

Given all that, the best (least-bad) option seems to be the following:

  • Hosts default to creating a fresh instance per export call when given the choice, but offer an explicit configuration option to enable instance reuse (along with additional policy options to configure autoscaling, etc).
    • As a notable exception, when native languages runtimes (like JS or Python) import components as-if they were singleton instances (which is the default assumption in most languages), instances would need to be reused (the host doesn't really have a choice).
  • The C-M defines a new "instance reuse hint" custom section (analogous to branch hinting or compilation hints).
    • Because it's a custom section, it is not allowed to affect runtime semantics (b/c stripping a custom section should never change runtime behavior).
  • If a host is given a component to run that transitively contains the instance reuse hint and the host is not configured to reuse instances, the host is expected to inform the developer (via some sort of warning message) that there may be a perf problem and how to explicitly enable instance reuse.

So yeah, the proposed solution is "a hint", which never feels like winning, but given all the constraints, it feels like the least-bad option. What I like about this approach is that:

  • There are natural incentives for component authors to not over- or under-set the instance reuse hint.
  • Changes in transitive dependencies (e.g., on version update) don't trigger implicit massive semantic changes while at the same time all components are not forced to manually propagate the instance reuse hint of all their transitive dependencies.
  • The developer feels appropriately involved at the right point to make an important decision where we can't simply eliminate one option.
  • It makes clear that the host can ultimately do what it wants (reuse instances or not).

Sorry for the long comment; happy to hear more thoughts on this!

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