Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling any message type #24

Closed
ilpropheta opened this issue Feb 9, 2021 · 12 comments
Closed

Handling any message type #24

ilpropheta opened this issue Feb 9, 2021 · 12 comments

Comments

@ilpropheta
Copy link

Hi,
I have a very simple question: is it possible to make a subscription handler that will be called regardless of the message type?

Something like this:

so_subscribe(board_).event([](any_message_t m){
   // do something
});

// ...
send<int>(board_, 5); // handler called
send<MyType>(board_, MyType{}); // handler called
// ...

What's the recommended practice here?

Thanks.

@eao197
Copy link
Member

eao197 commented Feb 9, 2021

Hi!

It's impossible in SObjectizer-5. It goes against the SObjectizer's ideology: every message has its type, and a handler for a message is being found by the message type in the current agent's state.

We didn't encounter real-world use cases where a handler for any message type would be needed.

Can you provide an example of where you need such functionality?

@ilpropheta
Copy link
Author

ilpropheta commented Feb 9, 2021

I am happy to share with you a use case!

Basically I have an application that grabs images and other measures from industrial cameras. Imagine that this application has an abstraction that enables the users to configure some "blocks" to process such data in a sort of "flow graph" fashion. The users can also code their own blocks and also add new devices.

You can see a block like an agent.

Handling several devices usually means managing several SDKs of several vendors, each having a specific image type.

Some blocks leverage the underlying type information to exploit some SDK capabilities. For instance, the SDK of a certain thermal camera provides tons of useful metrics on the thermal image, however those are strongly coupled with the internal image type. This particular block won't work with an incompatible image source.

In other cases, a "common image type" would be enough for performing the operation. For example, for printing a label on the image and save it. In this case the image dimensions and raw data are enough. For this reason, the application provides a special block that is just a converter from any supported image type to a common image type.

However, for some other tasks (like sending a heartbeat when a message comes, or logging the number of messages arrived), such a conversion is useless since it does not take any information of the image at all. Since the conversion adds some copy cost (that makes some difference in very specific cases), the current solution is to have another conversion block that just maps any supported image type to a fake type (a sort of signal).

Handling "any message type" was just a possible way to make things a bit simpler:

  • every "simple" block just works as-is, without any prior conversion
  • when a new image type is added (possibly by the user), such blocks do not need to be updated, but only the "common image type" converter will be, rightly, revised

Let me know your thoughts.

Many thanks!

@eao197
Copy link
Member

eao197 commented Feb 9, 2021

An interesting use-case, thanks for sharing.

At the first glance, it's needed to look to an OO-hierarchy of message types. Something like:

class image_base : public so_5::message_t
{
public:
  ... // some common image properties.
};

class vendor_one_image : public image_base
{
  ... // some vendor specific stuff.
}

class vendor_two_image : public image_base
{
  ... // some vendor specific stuff.
};
...

A message-subscription should be done for image_base type:

class some_block : public so_5::agent_t
{
  void on_image(mhood_t<image_base> cmd) {...}
  ...
  void so_define_agent() override {
    so_subscribe(board_).event(&some_block::on_image);
    ...
  }
};

Messages have to be sent as image_base:

so_5::message_holder_t<image_base> msg{std::make_unique<vendor_one_image>(...)};
so_5::send(board, std::move(msg));

Every message handler can access common image properties easily. If some vendor-specific part has to be accessed it can be done via dynamic_cast or via the usage of visitor-pattern.

There is a working example showing this technique:

#include <so_5/all.hpp>

class msg_base_t : public so_5::message_t
{
public:
	using so_5::message_t::message_t;

	virtual std::string say_hello() const = 0;
};

class msg_receiver_t : public so_5::agent_t
{
public:
	using so_5::agent_t::agent_t;

	struct stop_t final : public so_5::signal_t {};

	void so_define_agent() override
	{
		so_subscribe_self()
			.event([](mhood_t<msg_base_t> cmd) {
				std::cout << "incoming message is: "
						<< cmd->say_hello() << std::endl;
			} )
			.event( [this](mhood_t<stop_t>) {
				so_deregister_agent_coop_normally();
			});
	}
};

class image_type_A_t : public msg_base_t
{
public:
	using msg_base_t::msg_base_t;

	std::string say_hello() const override
	{
		return "img-typ-a";
	}
};

class image_type_B_t : public msg_base_t
{
	std::string m_additional_info;

public:
	image_type_B_t(std::string additional_info)
		:	m_additional_info{ std::move(additional_info) }
	{}

	std::string say_hello() const override
	{
		return "img-typ-b::" + m_additional_info;
	}
};

int main() {
	so_5::launch( [](so_5::environment_t & env) {
		auto dest_mbox = env.introduce_coop( [](so_5::coop_t & coop) {
				return coop.make_agent<msg_receiver_t>()->so_direct_mbox();
			} );

		{
			so_5::message_holder_t<msg_base_t> msg{
				std::make_unique<image_type_A_t>()
			};
			so_5::send(dest_mbox, std::move(msg));
		}

		{
			so_5::message_holder_t<msg_base_t> msg{
				std::make_unique<image_type_B_t>("First")
			};
			so_5::send(dest_mbox, std::move(msg));
		}

		{
			so_5::message_holder_t<msg_base_t> msg{
				std::make_unique<image_type_B_t>("Second")
			};
			so_5::send(dest_mbox, std::move(msg));
		}

		so_5::send<msg_receiver_t::stop_t>(dest_mbox);
	} );
}

@ilpropheta
Copy link
Author

Thanks for the code. It confirms one of my ideas to handle this scenario!
The cons is that I have to wrap every image type by hand. Alternatively, what about using a variant?

@eao197
Copy link
Member

eao197 commented Feb 9, 2021

Alternatively, what about using a variant?

Yes, you can use a variant as a message type, I don't expect problems here.

@ilpropheta
Copy link
Author

Great, thanks for your ideas. I think I can close this one.

@eao197
Copy link
Member

eao197 commented Feb 9, 2021

The cons is that I have to wrap every image type by hand.

Maybe this issue can be solved by a helper function template?

@ilpropheta
Copy link
Author

Maybe this issue can be solved by a helper function template?

That's definitely helpful but maintaining both the wrapper classes and the code calling the helper function is still required. It's not a big deal but needs to be taken into account.

Speaking in general, both the variant and the inheritance-based techniques work nicely as you described, however they do not leverage the pattern matching capabilities of SObjectizer. You cannot just do:

void so_define_agent() override
{
so_subscribe_self()
	.event([](mhood<image_vendor_A> vendorA) {
                 // converts from vendorA to CommonImage
	} )
	.event([](mhood<image_vendor_B> vendorB) {
                 // converts from vendorB to CommonImage
	} )
	.event( [](SOME_ANY_TYPE) {
               throw runtime_exception("this conversion has not been implemented");
	});
}

//... another agent:
void so_define_agent() override
{
so_subscribe_self()	
	.event( [this](SOME_ANY_TYPE) {
               m_counter++; // just counts incoming messages
	});
}

On the other hand, both the variant and the inheritance requires you to manage an extra level of indirection (e.g. visitor, virtual functions) that gets the message and translates it into your domain logic. SObjectizer, instead, enables us to express the domain logic directly in the subscription handlers, without any indirections. That's just awesome.

@eao197
Copy link
Member

eao197 commented Feb 9, 2021

From my point of view the cases like:

// one agent
void so_define_agent() override
{
so_subscribe_self()
	...
	.event( [](SOME_ANY_TYPE) {
               throw runtime_exception("this conversion has not been implemented");
	});
}

//... another agent:
void so_define_agent() override
{
so_subscribe_self()
	...	
	.event( [this](SOME_ANY_TYPE) {
               m_counter++; // just counts incoming messages
	});
}

are rare and do not allow to do something really useful because you don't know what message you are handling.

But if OO-hierarchy is used for message types then it opens a possibility to do something like that:

class image_base : so_5::message_with_fallback_t {...};
class image_vendor_A : public image_base {...};
class image_vendor_B : public image_base {...};
...
void first_agent::so_define_agent() {
  so_subscribe_self()
    .event([this](mhood_t<image_vendor_A> cmd) {...})
    .event([this](mhood_t<image_vendor_B> cmd) {...})
    .event([this](mhood_t<image_base> cmd) {...})
    ...
}

void second_agent::so_define_agent() {
  so_subscribe_self()
    .event([this](mhood_t<image_base>) { ++m_counter; });
}
...

An event-handler searching procedure has to be changed:

  • on the first pass an event-handler for the message type (e.g. image_vendor_A) will be searched;
  • if the first pass failed, SObjectizer will fallback to the underlying type (image_base in that case) and will try to search an event-handler for a message of type image_base.

This mechanism looks pretty implementable, but it just an idea now.

@ilpropheta
Copy link
Author

It would be nice to have this!

By the way, just as a side note, I am mildly biased by CAF because it provides a default handler concept. I am not a CAF user but I admit I tried it before getting to SObjectizer.

Thanks @eao197 , you are always very clear and kind. Looking forward to host your community in March.

@eao197
Copy link
Member

eao197 commented Feb 9, 2021

It would be nice to have this!

IIRC, this is just the second real-world use-case reported where such functionality is applicable.

By the way, just as a side note, I am mildly biased by CAF because it provides a default handler concept.

SObjectizer and CAF have very different roots. SObjectizer's principles grow from SCADA software where discarding unhandled messages was a normal and usual thing. That's why SObjectizer just ignores a message if there is no event-handler for it (in the current state and in all parent states).

@ilpropheta
Copy link
Author

Many thanks for explaining this, I didn't know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants