Skip to content

SO 5.7 InDepth Enveloped Messages

Yauheni Akhotnikau edited this page Jan 10, 2020 · 1 revision

There is a special feature that can be used for solving very specific tasks: envelopes with messages inside. This feature allows sending a message or signal that enveloped in a special object called envelope. An envelope can carry some additional information and can perform some actions when the message/signal is delivered to a receiver.

An Important Note

This feature is a low-level feature that intended to be used by SObjectizer's developers and those developers who want to extend SObjectizer functionality. It seems that ordinary users will be protected from envelope-related details by some high-level code (something like special tools from so5extra).

Because of that, the sense of reading the following text exists only if you want to write your own type of an envelope.

What Is An Envelope

An envelope is a special case of a message. An envelope carries a message or signal (or another envelope) inside. An envelope is implemented as class derived from so_5::enveloped_msg::envelope_t.

Delivery Process For An Envelope

Because an envelope is a message the delivery of an envelope is almost the same. For example, dispatchers have no knowledge about envelopes at all -- they see just instances of so_5::message_t type.

Key differences in the delivery process can be found in two places:

  • passing of an envelope to a mbox or mchain for a delivery;
  • extraction of envelope's payload for calling an event handler or for the transformation of payload (for example in the case of limit_then_transform).

Passing An Envelope To A Mbox

To send an envelope the do_deliver_message of so_5::abstract_message_box_t should be used.

If you want to send an envelope you have to pass your envelope to do_deliver_message method. For example:

class my_envelope final : public so_5::enveloped_msg::envelope_t {
  const so_5::message_ref_t payload_;
public:
  my_envelope(so_5::message_ref_t payload) : payload_{std::move(payload)} {...}
  ...
};

void make_and_send_envelope(
   // The destination for an envelope.
   const so_5::mbox_t & to,
   // Type of message to be delivered inside an envelope.
   const std::type_index & msg_type,
   // Message to send inside an envelope.
   so_5::message_ref_t what)
{
   to->do_deliver_message(
      msg_type,
      // Envelope instance.
      std::make_unique<my_envelope>(std::move(what)),
      1u);
}

msg_type For An Envelope

Method do_deliver_message requires msg_type argument. This is a type of envelope's payload, not a type of envelope itself. For example:

struct my_message final : public so_5::message_t {...};
class my_envelope final : public so_5::enveloped_msg::envelope_t {...};

// Create a payload of type my_message.
auto payload = std::make_unique<my_message>(...);
// Create an envelope with payload inside.
auto envelope = std::make_unique<my_envelope>(std::move(payload));

// Send envelope with my_message inside.
mbox->do_deliver_message(
   so_5::message_payload_type<my_message>::subscription_type_index(),
   std::move(envelope),
   1u);

Extraction Of Envelope's Payload

There are several points in the message delivery process where the payload should be extracted from an envelope:

  • delivery of an envelope to a receiver. In this case, the payload is extracted to be passed to a receiver for normal processing of a message;
  • transformation of the message. For example, if limit_then_transform is used for overload control and count of messages of some type is exceeded the specified limit then the payload of an envelope should be extracted and transformed to another message or signal;
  • inspection of message content during the delivery procedure. For example, a delivery filter needs to analyze the payload's content to enable or disable message delivery to a particular receiver.

To extract the payload the corresponding envelope's hook is called.

An envelope can decide should the payload be extracted or not. It is possible that an envelope won't provide access to the payload for some reason. In that case, the payload will be ignored. It means that if an envelope is delivered to a receiver but don't provide access to the payload then an event handler for the receiver won't be called (as if the message was lost). The same is for transformation case: if an envelope doesn't provide access to the payload for limit_then_transform then the message won't be transformed and will be simply thrown out.

This is the main feature of an envelope: the possibility to "hide" the payload from a receiver (or transformation procedure).

Envelope's Hook

The access_hook() is called when SObjectizer needs to access the payload of an enveloped message.

access_hook() can be called in different cases: when a message is delivered to a receiver, when overload reaction must be performed, when a delivery filter has to analyze the message and so on. The context of access_hook() call is identified by context argument of access_hook(). That argument can have the following values:

  • access_context_t::handler_found. It means that an enveloped message is delivered to a receiver and the payload is necessary to call an event handler;
  • access_context_t::transformation. It means that the payload of an enveloped message should be transformed into another representation. For example in the case of limit_then_transform overload reaction;
  • access_context_t::inspection. It means that the payload of an enveloped message should be analyzed. For example, by a delivery filter.

If the envelope decided that the access to the payload can be provided it should call invoke() method for invoker object (the invoker object is passed as argument to access_hook()):

class my_envelope final : public so_5::enveloped_msg::envelope_t {
  so_5::message_ref_t payload_;
  ...
public:
  void access_hook(access_context_t context, handler_invoker_t & invoker) const noexcept {
    if(access_can_be_granted(context))
      invoker.invoke(payload_info_t{payload_});
  }
  ...
};

Please note that access_hook() is marked as noexcept. It must not allow exceptions to leaving the hook.

Multiple Invocations Of Hooks

If an envelope is passed to MPMC-mbox then hook methods can be called more than once. It is because there can be several receivers of the message inside the envelope.

Envelope And Multithreading

Because envelopes can be sent to MPMC-mboxes there is a possibility that envelope's hooks will be called at the same time on different threads.

It means that envelope classes should be thread safe and should allow invocation of hooks on several threads simultaneously.

Envelope And Message Limits

Message limits know almost nothing about envelopes. Strictly speaking only limit_then_transform knows that envelopes exist and knows how to extract of envelope's payload from an envelope. But no more.

It means that the behavior of an envelope has no influence on message limits.

For example, let's consider an envelope that allows revoking a message. Something like:

// A special envelope with atomic revocation flag.
class revocable_envelope_t final : so_5::enveloped_msg::envelope_t {
   friend class revocable_msg_id_t;
   so_5::message_ref_t payload_;
   std::atomic_bool revoked_;
public:
   ...
   void access_hook(access_context_t, handler_invoker_t & invoker) const noexcept {
      if(!revoked_.load(std::memory_order_acquire))
         invoker.invoke(payload_info_t{payload_});
   }
   ...
};

// A special handler that allows revoking envelope with a message.
class revocable_msg_id_t {
   so_5::intrusive_ptr_t<revocable_envelope_t> envelope_;
public:
   ... // Constructor/destructor and other stuff.
   void revoke() {
      envelope_->revoked_.store(true, std::memory_order_release);
   }
};

// A special helper function to make and send a message inside revocable message.
template<typename Msg, typename... Args>
revocable_msg_id_t make_and_send(
   const so_5::mbox_t & destination,
   Args && ...args)
{
   so_5::intrusive_ptr_t<revocable_envelope_t> envelope{
      std::make_unique<revocable_envelope_t>(
         std::make_unique<Msg>(std::forward<Args>(args)...)));
   revocable_msg_id_t result{envelope};
   destination->do_deliver_message(
      so_5::message_payload_type<Msg>::subscription_type_index(),
      envelope,
      1u);
   return result;
}

// Send a revocable message.
auto id = make_and_send<my_message>(to, ...);
...
// Revoke a message.
id.revoke();

If revoke() method is called when the message is waiting in receiver's queue then the message won't be delivered to the receiver (because envelope won't provide access to the payload). But the calling to revoke() do not decrement the quantity of messages on type my_message in receiver's queue. Quantity of messages will be decremented only after extraction of an envelope from the receiver's queue.

It means that revoke() have no influence on message limits.

Nested Envelopes

It is possible to create an envelope that holds another envelope.

There is no need to know a type of payload. For example, this simple envelope can hold envelopes as well as messages or signals:

class demo_envelope final : public so_5::enveloped_msg::envelope_t {
   so_5::message_ref_t payload_;
public:
   demo_envelope(so_5::message_ref_t payload) : payload_{std::move(payload)} {}

   void access_hook(access_context_t, handler_invoker_t & invoker) const noexcept {
      invoker.invoke(payload_info_t{payload_});
   }
};

This is because the standard implementations of handler_invoker_t check the kind of payload. And if a payload is another envelope these implementations call appropriate hook for that envelope recursively.

A user should take care of payload kind only if he or she implements own handler_invoker_t class. In that case so_5::message_t::kind_t and so_5::message_kind() should be used.

Ready To Use Envelopes

SObjectizer doesn't provide ready to use envelopes. SObjectizer core only implements basic functionality for envelopes.

Ready to use envelopes can be found in so5extra library that is built on top of SObjectizer.

Clone this wiki locally