Post by Chris JonesPost by Chris JonesPost by Benjamin SmedbergHow does IPDL deal with destructors that are fired simultaneously from both
sides, e.g. ~PBrowserStream? Even if we make one side "win the race", won't
IPDL reject the second destructor message (because it has an invalid actor
ID) and abort the process? If it doesn't abort the process, what is the
behavior (return values) of the second destructor message?
Right now, IPDL treats dtors as it does any other message, and by "any
message" standards that's not a race.
But this is just a good old fashioned bug, as dtors define an implicit
transition, [alive]-->DEAD. I'll add this in soon.
Hmm, I was hasty in my response. On further consideration this is a
deeper problem.
There are two facets to the problem. The first mainly applies to
nominally stateless, inherently racy protocols like NPAPI. The example
you present with ~PBrowserStream forces a design decision: whether or
not to allow destructor messages to race. I would prefer to just
disallow racy destructors; I think there's a workaround for the NPAPI
(generally, crappy API) case. The workaround is two-phase destruction.
protocol PBrowserStream {
child: RequestDestruction; // first phase
parent: NotifyDestruction; // first phase
};
protocol PPluginInstance {
manages PBrowserStream;
parent: PBrowserStream;
~PBrowserStream; // second phase
};
(I'm not sure "which" NPStream the PBrowserStream models; I'm assuming
it's the one created with NPN_NewStream() and so sort of "owned" by the
child).
If the browser wants to destroy the BrowserStream, instead of directly
invoking the dtor, it destroys the "real" NPStream wrt to NPAPI and then
sends "RequestDestruction." In the common case, the plugin will be idle
wrt the stream and simply send back the actual ~PBrowserStream message.
If the plugin wants to destroy a BrowserStream, it first sends
"NotifyDestruction" to warn the browser. In the common case, the
browser will be idle wrt the stream, clean up the NPStream, and send
"RequestDestruction" back to the plugin, upon which ~PBrowserStream will
be sent.
If, however, RequestDestruction and NotifyDestruction race, it causes no
problems as the browser is guaranteed to have processed
NotifyDestruction *before* ~PBrowserStream arrives. The C++
implementations of browser stream actors just have to be careful not to
double-"free" the NPStreams.
That's one option, again (to me eyes) a hack around NPAPI limitations.
Another option for this case is to *allow* racy destructor messages. All
that has to be done in IPDL is, only for protocols with racy
destructors, not to blow up when a destructor references an unknown
actor. This approach trades a safety check for racy destructors. Both
are feasible and easy, but again I lean towards two-phase destruction.
I now see that the second facet of this problem isn't worth discussing
until we resolve this first issue.
OK, looks like we should cover both issues together.
As Benjamin just pointed out, destructors are kind of busted
semantically right now. C'est la guerre. (Beware the easy analogy to C++!)
I see a few ways to fix them.
(1)
Make destructor messages part of the protocol of the actors they
destruct, rather than part of the manager's protocol. This would imply
that dtors can't race with any other message (including other dtors),
since dtors transition into a special _DEAD_ state. (I.e., the dtor and
another message can never commute.) For example
protocol PluginInstance {
manages BrowserStream;
parent: BrowserStream();
// no more ~BrowserStream()
}
protocol BrowserStream {
child: M1();
parent: M2();
child: delete();
// could also be ~BrowserStream()
state S1: send M1 goto S2;
state S2; recv M2 goto DYING;
state DYING: send delete;
}
This proposal adds a new keyword "delete" which is understood to mean
"the destructor message for this protocol's actors." (We could also
just reuse ~Protocol, it's just a syntax thing that I don't have strong
feelings about.)
I think this would work quite well ... except for "stateless" protocols
like some in NPAPI. For them it's impossible to declare a non-racy
destructor. They could be special-cased in that for them, IPDL would
not treat sending a message to a nonexistent actor as a "fatal" error,
only to the extent that doing so would cause a spurious fatal error.
(Fancier implementations are possible.)
(2)
Keep dtors as part of the manager protocol, but only allow dtors to be
uni-directional. I.e., |both: ~Subprotocol()| would be explicitly
disallowed. Then, on the side that *sends* a particular dtor, receiving
a message addressed to a nonexistent actor would no longer be fatal.
(3)
Do either or none of { (1), (2) }, and treat destructor messages as
special in that the IPDL actors aren't "actually" destroyed until the
other side ACKs the dtor message. That is, actors can continue to
receive messages (though not send them) until the other side replies to
the dtor message. It's not really clear what should happen when a
"half-deleted" actor receives a message. Options:
(a) IPDL automatically returns EDESTROYED or something like that
(b) IPDL invokes a special handler that decides what to do
(c) the actor is marked as DEAD, that state is visible to C++, and
it's up to the implementation to take care of itself.
Strong opinions? I lean towards (1) plus (3a), that is, use (3a) for
stateless protocols.
Cheers,
Chris