-
Notifications
You must be signed in to change notification settings - Fork 3
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
Control protocol header format #33
Comments
I think the most convenient way would be to use separate frames. Yes, it does create a bit of overhead, but I do not think this is critical for us - speed is important, but readability and versatility are too. With frames, we can use zmq's multipart framework, and directly get a well separated list of items, where we can have the first 1 to n frames as header, and then the n+1st frame (which is also the last) as the payload. |
For the names, the dot "." is already reserved due to a namespaces. Ok, so we keep it simple: Frame separation is already needed, so why not use that even more instead of inventing new methods. In that case (separated by frames), we can choose any order. I'd say: Recipient, Sender, MessageID (because necessary), ConversationID (because optional). |
As I understood it so far, Avro can take should a lot of that burden (but maybe I skim-read too quickly). The nice thing about Avro is that we get schemas for our messages, so the structure is "in the code", schemas get exchanged during handshake, so participating Components know what to expect, the schemas are used to validate and process the messages, and we don't have to keep track of those details manually (with hand-wrought formats). |
I am not perfectly sure about Avro (I did not look at it as detailed as I looked at zmq), but what we are looking at here will not be included, I think, since it is mostly about routing. For routing, we need separate frames, because when sending a |
Oh, you intend, that we serialize the receiver, sender, message, and conversation IDs via avro? I intended to do this header (routing information) manually (this issue) and add avro on top (another issue). The only requirement, from zmq, is, that the topic of the data protocol is in the first frame. Everything else is up to us. |
Ah, then I got confused, because you guys were mentioning "Recipient, Sender, MessageID, ConversationID ", I thought for the zmq routing part indeed you only need the recipient. The only thing I was not sure about is that we would duplicate the "recipient" information, once in the first zmq frame, then in the message header (packed in the zmq payload in the avro message).
No, afaik not. But zmq only needs the recipient for the routing, right? all the rest (convo tracking, message id) is stuff that LECO components put on top, right, as zmq is not doing that, either? |
Yeah, indeed, I did. If we are already using a specified/schematised message format, why do the header stuff manually separately if we don't need it for the transport layer?
or the recipient of a control message, IIUC Benjamin's reply above. |
ZMQ does not need the recipient information in the first header. ZMQ does just send messages over tcp connections. Our Coordinators need somehow the "recipient" information to match that (human readable) name to its own (arbitrary, not selectable) zmq socket addresses. Basically, the data protocol does not need any "zmq" header. We could do everything in avro (or whatever). I wanted to have a three step communication: zmq manages the connections. Then we have routing information (human readable) to route messages where we want them. Finally some serialization helps to transport data. The last two are arbitrary distinctions in my head. |
@bilderbuchi that first frame is the address the zmq socket assigns to the other side. We map our own names to that (arbitrarily chosen address). |
I understood that Routing, that we set up, as "Transport Layer", therefore my requests in the PR. It is good to have the routing information external to the payload, such that the Coordinators do not have to deserialize the whole payload, just to know, to whom to deliver the message. |
Thanks, I'll have to think. At the same time, we need to make sure that the Avro message retains enough context (I had considered the routing info just that) so that it remains "meaningful"/interpretable once its separated from the transport layer and goes through our system. |
I try to explain how a zmq ROUTER socket works:
What our Coordinators have to do, is to know, the peer better than by its (arbitrary) address. Therefore we introduced (for our own code) the recipient name. Now a Coordinator can look up the address that corresponds to that recipient's name and send it a message. This issue is exactly about that envelope. |
Thanks, that's informative! When planning the message format (this will not be restricted to the header, I think), please keep in mind the further flow of the message -- how do we keep the routing info near/associated with the Avro message - do we wrap both in some container? duplicate the info in the Avro message? Do both parts get deserialised into some data structure upon arrival (that is then passed on to the business logic? Something else? Also, by gut feeling I would prefer if the header fields are separated by something more resilient than a magic character (zmq frames sounds fine), even if we pay a little overhead cost. If that should turn out to be a bottleneck later, we can always change that then. |
This does not necessarily need to be true. If the peer first sets an identity to itself, this identity is what the ROUTER socket will see. If the ROUTERs are allowed to choose the identities of peer Components by themselves (random bytes), how would you know that identity (even more so if you do a second hop through a second Coordinator) when writing your Director which tries to request data from a specific Actor? The random peer ID works well if you have a ROUTER connected to a REQ socket, because then the ROUTER takes the first frame from the REQ as their identity, do something with the payload, and answer to it using the identity which it generated (and which contents do not matter for this purpose). In the zmqguide it is sometimes written that the ROUTER cannot start sending messages, because it does not know where to send it in advance. This is only true in certain messaging patterns, especially with all these service-oriented servers which do some jobs for clients, who do not care who actually provides them with some data (imagine a weather information service, as used as example in the zmqguide). We have a fundamentally different problem, in that we want data from very precisely defined Components, and yes, I think the user has to hand out those identities at some point. After all, they need to know whom to talk to, the temperature of which controller they now need/want to change, or the motor setting of a very specific mirror. This needs to be transparent when writing sequences, and we need some names for it. What is important for the ROUTER-DEALER connection here, is that the DEALER sets their identity BEFORE connecting to the ROUTER, so that the ROUTER receives this set identity, and does not set its own random ID for this particular connection. For this reason, we really need the Recipients ID and the Senders ID in the header, preferable in the header frames, so we can use them without deserializing whatever payload comes with the message, as discussed above. For me, this is almost as @bmoneke put it:
It is just that the human-readable names can be used and are important for the zmq connection management, because this is intimately tied to how zmq routes manages (in our current way of thinking with ROUTER sockets). |
Oh, thanks @bklebel for reminding of that possibility to set your own identity beforehand, which I had forgotten (I did not need it). With that, it could be necessary to overhaul the routing concept. I have to think about it. Here an example http://wiki.zeromq.org/tutorials:dealer-and-router |
I think the routing concept is just fine, it's only that for sending something between Coordinators, the corresponding sending socket needs to get an additional frame prepended to the rest for routing to the second Coordinator, who in turn needs to scrap that first frame (if this happens to be a ROUTER socket which will make it visible), as well as the first part of the recipientID which contains its own identity (zmq would try to send something to "Coord2.Comp1" which does not exist, because the only zmq identities which might exist are "Coord2" and "Comp1", connected to each other respectively).
How do you send your messages if you do not set your ID's yourself? How do you know where you're sending messages? I am intrigued :D - or did you chose a fundamentally different approach to send command/control messages to Actors so far? |
Until recently, I did not send to Actors, just to ha handful of Observers and I used a new port number for each one... |
Message format is: Namespace|Recipient||More frames to be determined (Namespace frame is optional) Where do we place the additional information (sender, messageID etc.?)
As we agreed upon only two Coordinators per message (and even multi-hop would not increase the number of address frames), we could do the following (based onproposal 2), regarding the full header: I prefer option 2, but I'm unsure whether suboption A or B, I tend slightly to B |
Current state is:
Now the question is, what else should enter the header, what the content. I think it is good, if we have at least a third header frame, which indicates the type of content, such that the recipient knows how to interpret it.
|
If the third frame contains "metadata", we would remain more flexible. |
For what else we could/should include in the header, maybe this can serve as a "reading inspiration". (Not judging technically if that is applicable/relevant for us). |
I think it would be a good idea to have one or more additional header frames for metadata. The CoAP protocol @bilderbuchi linked to is quite interesting I think, especially regarding the header. From an implementation point of view, it might be good to put the header frames in the order in which they are needed when reading them. Initially, having the routing frames at the front was motivated by using them together with the zmq identity itself, but this is not a thing for us anymore. Therefore the order could be (strongly taking from CoAP):
The version of the protocol in use is a good point from CoAP which @bilderbuchi linked to, although this opens a fully new rabbit-hole of "how to deal with messages from a different version than my current one", as seen from a particular Components' view. Still, that might be better than "wth do you want to tell me with that? ". Also, if we put the version all the way to the front, we are free to change anything and everything behind that from version to version (even though it might not be advisable in itself), and still get a working "this version is no longer supported" response, without errors, since the rest of the message does not need to be looked at. In general I would like to avoid serializing this too much, I simply do not know how we would benefit from it, if you have a good reason I am open for it. |
Coap is a message protocol over a tcp connection, so no routing is necessary. Therefore, I'd put the routing information in front of the type. |
I think version first is a safer choice, in case we want to make changes to the routing info in the future (e.g. allow more levels to address Paramaters directly, like Question: Is it even necessary (with the current status) anymore to have separate frames for the sender/recipient, or could we just have one header frame (with a number of bytes containing all the necessary info)? I would not serialise the header info at all, just encode info into an array of bytes (easily decoded). |
By separating it into different frames, we can avoid additional forbidden symbols, which we would otherwise need to separate the information. Also, at least in python, you can do a very straightforward implementation for the CCoordinator:
Here also my reason to put the type before the routing becomes maybe more apparent, in that we can pick out the |
Just an implementation detail: I would not pop parts of the msg, as the Coordinator normally gives the message as is to the recipient (or other Coordinator). We do not need to pick the SIGN_IN first: The Coordinator looks, whether a message is for itself (Recipient==self) and then it handles the message, be it SIGN_IN or something else.
With a fixed Recipient name (Coordinator or None or whatever we decide), the Coordinator does not need to look at any other field than the Recipient. msg = zmq.recv_multipart()
if msg["version"] != my_version:
send_error()
if msg["recipient"]:
do_routing() # might include to myself and then doing sign_in()
elif msg["type"] == SIGN_IN:
sign_in()
else:
send_error() Note: I used dictionary keys as placeholders for the actual position. |
👍
You're correct.
👍 For that reason, I'm for four header frames: version, recipient, sender, "content header" (type etc.). |
Thanks! I failed to consider that our addresses are variable-length, which makes separation into frames very attractive.
LGTM |
Only for the signin, right? In that case I guess we might as well leave it out, completely. |
No, we allowed the Recipient to be only the Component ID, if it is in the same Node. The Sender ID has always to be complete. |
Ok, thanks. We could keep in mind that we could revisit that if it makes our logic flow less complex. |
That looks good to me too, I agree with the reasoning about looking at the type twice, and very often unnecessarily looking at the type. |
We reached a conclusion, I close this issue |
Should the issue not be closed when the conclusion/relevant information turns up in |
I think so too - it is a bit cumbersome to have those issues still open even though we reached a conclusion, but as long as it is not done with a merged PR, I would also leave the corresponding issues open. I am going to reopen this now. |
That was my reasoning for those many issues with a single PR, as an exception to the default rule to close with PRs. So, no exception 😁 |
Well, you can summarize the conclusion/takeaway in a "last" comment. Then people looking at the issue know immediately what the result of a lenghty discussion was. When it has been added in a PR, you can link to that, too, in a comment. If you put "closes #blabla" in the description/first post of a Pr, that issue will automatically be closed when the PR is merged. |
Good, now I wanted to write a summarizing comment, in which I say that we agreed on everything, and find that I am not sure we did ^^ Here is the summary so far SummaryWe agreed on the following Control protocol header format:
where "content header" includes not agreed onthe content header: |
For the transport layer, it does not matter (so for my PR it is done). The format of the header (four frames) is done. The content of these frames is not yet fully agreed upon (version and content header). The content header enters the message layer, I'd say. (it has its issue #41) The version formatting is also open, but can be discussed separately (#42). |
Yes, let's discuss the content of these frames separately. |
How do we separate the individual parts of the header?
I propose the following scheme with a separation character (i.e. forbidden character):
"Recipient name";"Sender name";"ConversationID";"MessageID".
The message ID might be some binary data (#16). So we can look for the first three semi colons and the rest is the message ID independent of the ASCII representation.
Consequences:
Alternative:
If we have a messageID with a fixed byte length, we can do the following header: "Recipient";"Sender";MessageIDConversationID
The code would look for the first two semi colons for the recipient and sender. Afterwards it takes a certain number of bytes for the message ID and the rest is the ConversationID, which can be flexible in length and can be have any bytes value.
The text was updated successfully, but these errors were encountered: