Hi Kenton,
No stress, your time is given freely and I appreciate it.
Your suggestion makes sense to allow an immediate method of cancelling outstanding requests wrapped inside a membrane. After a look over membrane.c++, I do not see a kj::Canceller in use, so I presume this is done using kj::Promise::exclusiveJoin. I think I see three scenarios being dealt with when kj::MembranePolicy::onRevoked resolves:
- Existing requests are eventually rejected, but the underlying call path might still run depending on exclusive join resolution order (i.e. it will run if made before onRevoked was resolved). [1][2]
- New requests against a capability obtained before revocation are rejected. [1]
- New requests against a capability obtained after revocation (replaced with a dummy) are rejected.[1]
I think requests from (1) can be immediately cancelled given access to a kj::Canceller wrapping all membraned requests. I think given the dummy capability injected in (3), those requests are safely rejected as-is. I however have a concern with (2); is it guaranteed that these new requests will be resolved after onRevoked is processed? I'd presume requests would land up on the event queue in-order, but I just wonder if there could be any race conditions involved. If it is all processed in-order, is it then also safe to assume that kj will eagerly evaluate a onRevoked.then([this]() { canceller.cancel(); } relying on the result of onRevoked, i.e. that this is still safe to use in the MembraneHook and/or MembraneRequestHook?
It's a pity that the user would need to be responsible for both manually cancelling outstanding requests in addition to rejecting the promises exposed by kj::MembranePolicy::onRevoked (unless I'm missing something). I wonder, it seems like kj::MembranePolicy::onRevoked seems to be intended to produce promises from a kj::ForkedPromise under the hood, which itself seems to have been done as a convenience as this provides a single-producer/multi-consumer interface to this revocation "event", and kj::Promise::exclusiveJoin already existed to reject calls. Could another single-producer/multi-consumer protected interface be exposed by kj::MembranePolicy which handles all this inline, i.e. without going to the event loop but leaving the public interface unchanged?
Given your current and future feedback, could I raise an issue and look into creating a draft PR on GitHub to start exploring the change that you've suggested? I will probably only get to writing any code from the 10th of April, so further discussion can occur here and/or on the issue in the meantime (whichever is preferred).
Look forward to hearing from you,
Rowan Reeve