-
Notifications
You must be signed in to change notification settings - Fork 41
WeakRef collection semantics violate encapsulation #19
Comments
Hi @lars-t-hansen , could you be more explicit about how the issue you raise is an "encapsulation" issue? |
I don't know if I can do much more than restate what I already wrote in slightly different terms: Suppose I have a JS object that is a proxy for some resource and that, provided my application continues to run long enough, after the JS object is reaped I need to run a finalizer to destruct that resource. In order to do that, the weakref creating the object<->finalizer relationship must stay alive until the finalizer is invoked, for all practical purposes, since it's its liveness that lets the finalizer run at all. The need to keep the weakref alive means there is a data structure that holds the weakref. In some applications there may be a natural data structure for that. In other applications there's none, so one must be created. My general concern about encapsulation is that this data structure that's created for managing weakrefs will leak out beyond library boundaries because it needs to be, say, scanned periodically to remove dead weakrefs. (Since we're talking about it, the fact that weakrefs must stay alive longer than the objects they reference for the finalizer to be run is an additional source of incompatibility across implementations since different object management strategies can lead to diverging lifetimes for weakrefs and the objects they reference. Not only does the firing of a finalizer depends on tenuring strategies for the weakly held object, but on the tenuring strategies of the weakref and the relative strategies for managing the two objects.) |
@lars-t-hansen Thanks for the write-up. I'll take the two issue here in mostly opposite order. I'd also be happy to discuss this in realtime if that's easier to come to closure. Finalizer lifetimeA design goal for the solution is specifically to support post-mortem finalization. Pre-mortem finalization leads to emergent resurrection bugs that are typically extremely difficult to debug and infeasible to reliably prevent. The only known way to prevent them is to preclude them by using a post-mortem approach. This is discussed more in the proposal. Fundamentally, post-mortem means that the finalizer necessarily and deliberately must have a longer lifetime than the target object because it must run after the target object is permanently and irrevocably inaccessible. Reachability of finalizersI think in practice the concern you are raising does not arise. As scenarios become more concrete, a natural context for the lifetime of finalizers will typically become clear. For wasm host-binding, it's the wasm module instance; for a remote object system, it's the I'll illustrate the intended solution in the context of the Subsystem finalizationA basic remote comm system approach has a Here's the wrinkle, if the last In practice, finalization generally applies within a sub-system, and the maximum lifetime of finalizers is ideally the lifetime of the sub-system. This does require some explicit management of the WeakRefs used in the subsystem, but the appropriate structure is enough different in different scenarios that framework developers need to express their requirements in code. A System retention is a trapAny element of the GC or finalization framework that deliberately retains objects becomes a source of retention bugs. Continuing the RemoteConnection example, connection services are typically provided by using reserved remote IDs (this technique is used in systems from CORBA to Java's RMI to Midori). Those reserved objects are added to the Similar patterns show up because e.g., modules are retained. A design goal is to have a coherent solution in which nothing is retained by default. If necessary, users could build global collections with controlled retention if they had to, and be in an identical position as if the sysem retained them. But I know of no use case where that is either necessary or desirable. |
A simpler example that perhaps makes the point clearer: Say we went with the alternative policy: whether or not a weak-ref is strongly reachable, if its target is collected, then its executor must eventually be invoked. Now consider this program: function executor(...) {...}
function subsystem(obj, i) {
const wr = makeWeakRef(obj, executor, i);
...
// The objects made above are reachable only from the returned s
return s;
}
for (const i = 0; i < N; i++) {
const s = subsystem(Object, i);
} Assume each call to Under the alternative policy, assuming the engine has no proof that |
@erights, yes indeed - that's how it breaks down. Either you have a guarantee of executor execution when the object is gc'd (if it is gc'd), in which case you maintain state to track that, or you have the opposite problem, that there's uncertainty about whether your executor will be invoked. Which one of these you favor plausibly depends on usage patterns. If you're using your weakref for managing an external resource you really want the guarantee, so you'll end up anchoring the weakref in some external data structure to guarantee that it is reaped after the object it protects. I'm not completely sure which use case is well-served by the lack of the guarantee, but I don't doubt that there are some. |
I believe the current specification is based on the design that @lars-t-hansen started the thread with. |
When and how did that change? Do we now have the ironic cost I mentioned:
Why is this guarantee useful in a system in which there is never any guarantee that any particular object is collected, or that the process will survive long enough for the eventually-called executor to actually be called? Does this extra utility pay for the extra cost? |
Sorry, I had a typo above--I meant, I don't believe the current specification is based on the design that @lars-t-hansen responded to. WeakRefs are not coupled to finalizers, and a WeakRef dying doesn't cause any sort of finalizer to be cancelled. So I think this issue is resolved; please reopen or file an other issue if there are remaining problems. |
The spec currently states that if the weakref becomes condemned before or at the same time as the target object, then no finalizer is executed. I can see why this is desirable choice for the implementation, but it seems to me that it violates reasonable assumptions about encapsulation.
Suppose a wasm application maintains many objects within its flat heap, which it reifies to JS as JS objects. These JS objects require finalization, so that the finalizer can destruct the wasm resource properly. Suppose that that's all there's to it (ie we don't have to map an address to the JS object representing it so there's no natural external data structure that will reference the weakrefs).
With the current design, we must retain an additional table of weakrefs in the JS heap to ensure that the JS finalizers will be run when the JS objects that are held weakly are condemned, and when an object is finalized the finalizer must ensure that the weakref that caused the finalizer to run is removed from the table, or it must ensure that the table is eventually swept for dead weakrefs.
Furthermore, libraries will themselves need to have their own tables or data structures for ensuring that weakrefs are kept alive long enough (they wouldn't likely share a single table for this), so this problem becomes somewhat non-local.
With a design that does not require the weakref to stay alive, the weakref could be attached to the JS object, or, I suppose, simply dropped on the floor, creating and dropping the weakref thus being equivalent to registering the object for finalization. This puts a larger burden on the implementation but the ergonomics seem to me to be better.
The text was updated successfully, but these errors were encountered: