Skip to content

Message Ownership

Paul N Stickney edited this page Feb 2, 2020 · 34 revisions

Message Lifetime

Messages in VT are allocated off-stack and have internally managed reference counts. As such, they must be handled correctly to avoid memory leaks. The MsgPtr (in 1.0 this is named MsgSharedPtr) type acts a a shared-ref wrapper to ensure proper lifetime management.

Note: The method of message creation is currently under some updates. The original method uses makeSharedMessage which returns a MsgT*. VT takes ownership of the messages these raw pointers represent in send and broadcast calls. A message is created with makeSharedMessage MUST either be guaranteed to be passed to VT for transmission or wrapped inside a MsgPtr (via promoteMsg in 1.0).

Good (1.0):

M* msg = makeSharedMessage<M>(..);
theMsg->sendMsg(dest, msg);

theMsg->sendMsg(dest, makeSharedMessage<M>(..));

MsgPtr<M> msg = makeMessage<M>(..);
theMsg->sendMsg(dest, msg.get()) // MsgPtr<M>.get() -> M*

theMsg->sendMsg(dest, makeMessage<M>(..).get())

Bad (1.0):

// Messages must be created via appropriate methods.
M* msg = new M{..};

// Message is leaked when it is not supplied to sendMsg.
// Only create messages as they are immediately going to be used.
M* msg = makeSharedMessage<M>(..);
if (maybe_send) {
    theMsg->sendMsg(dest, msg);
}

1.1 Proposal A: Allow direct new calls, with the same caveats as makeSharedMessage and discourage use of makeSharedMessage.

// Inline usages of message creation is encouraged for send/broadcast calls.
theMsg()->sendMsg(dest, new M{..});

// Still prone to memory leaks if lifetime ownership is not established.
M* msg = new M{..};
if (maybe_send) {
    theMsg()->sendMsg(dest, msg);
}

1.1 Proposal B: Accept MsgPtr&/MsgPtr&& in send/broadcast and discourage the use of makeSharedMessage. Usages of new M{..} are considered coding errors. This requires the use of temporaries or std::move.

// Temporary for MsgPtr&
MsgPtr<M> msg = makeMessage<M>{..};
theMsg()->sendMsg(dest, msg);                             // MsgPtr& param

// std::move for lvalue-to-MsgPtr&&
theMsg()->sendMsg(dest, std::move(makeMessage<M>{..}));   // MsgPtr&& param

1.1 Proposal B-Extended: Messages should be created with MsgPtr<T>{..}, which has a templated perfect-forwarding overloaded. This allows usage as an xref directly. Usages of makeSharedMessage and makeMessage are discouraged. Usages of new M{..} are considered coding errors. Usages of a temporary (and MsgPtr& parameters) are not affected.

theMsg()->sendMsg(dest, MsgPtr<M>{..});    // MsgPtr&& param (xref)

auto msg = MsgPtr<M>{..};                  // uniform message creation
theMsg()->sendMsg(dest, msg);              // MsgPtr& param

This has a negative effect of possible confusing error messages. It has a positive side-effect of being incompatible with lambda-copy captures, which may be suspect (a separate internal type can be exposed for this case).

1.1 Proposal B-Extended: MsgSharedPtr is renamed to Msg (instead of just MsgPtr).

This is then the fundamental method of user code to supply messages into VT. From an end-user view there is no reason to consider it is a 'pointer', or has any relation to a unique/shared ptr, as it is really only the lifetime guarantee that is of relevance.

theMsg()->sendMsg(dest, Msg<M>{..}); 

Callbacks are still M*, although such may be covered by a future proposal as such might be warranted for new API endpoints.

1.1 Proposal: Split handlers from send/broadcast templates. This may require one more specification of M.

theMsg()->send(dest,
  Msg<M>{..},
  handleWith<M,handler>());

Clearly defines roles and reduces template variance if the same message is sent many times with different handlers. Type-safety is preserved as handleWith yields a Handler<M> or similar.

Message Send/Re-Send Semantics

Messages in VT can be sent at most once. It is an error if the same message is sent multiple times.

Messages received from send/broadcast callbacks are considered sent and cannot be directly sent again. A new message must be constructed.

Invalid code:

T* msg = makeSharedMessage<T>(..);
theMsg()->sendMsg(dest, msg);
// Invalid: message has already been sent
theMsg()->sendMsg(dest, msg);

T* msg = makeSharedMessage<T>(..);
theMsg()->sendMsg(dest, msg, [](T* cb_message) {
  // Invalid: messages from callbacks cannot be re-sent.
  theMsg()->sendMsg(dest, cb_message);
});

1.1 Proposal: A run-time assertion is raised if a message is re-used for transmission.

1.1 Proposal: Active message supports a Copy Constructor that resets the transmission state of the new message such that the following is valid:

T* msg = makeSharedMessage<T>(..);
theMsg()->sendMsg(dest, msg, [](T* cb_message) {
  // Valid: a copy of the message without transmission state is created and sent.
  theMsg()->sendMsg(dest, makeSharedMessage<T>(*cb_message));
});

The new message has a new lifetime as well, subject to all other lifetime rules. In degenerate cases this might result in excessive data copying.

1.1 Proposal-Extended: Add special Msg<T>(..) copy overload.

 // Create a new copy of the message for transmission.
 // This might allow future changes to be more flexible in how the copy is performed.
 // (ie. Msg might contain a separate msg and envelop field allowing the same msg to be
 //  used with a new envelope.)
 theMsg()->sendMsg(dest, Msg<T>(copymsg_t, cb_message));

Tagged overload specifically to avoid forwarding constructor directly without more sophisticated template guards and increase explicitness while still being an xref. The 1.1 implementation will still use the message's intrinsic copy constructor. Arguably, perhaps such should be also be added to the forwarding constructor for explicitness as well (Msg<T>(newmsg_t, ..)).

Messages in Callbacks

Callbacks are given a M* value once a message is received. The lifetime of message is only for the duration of the callback. If the message needs to extend beyond the callback lifetime, wrap it inside a MsgPtr. For 1.0 this is done using promoteMsg<M>(rawMsgPtr) which returns a MsgPtr<M>.

Messages received in callbacks should be treated as mutable and should not be modified. Modifying the data in a message received in a callback may result in undefined behavior (especially in the future..).

1.1 Proposal: Create a MsgPtr with MsgPtr<M>{rawMsgPtr}.

Message Mutability

To avoid unexpected behavior, messages should not be modified after they are created.

1.1 Proposal: This imposition may be enforced in the future with runtime checks.