ietf-tapswg / api-drafts Public
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
Rework partial sends and receives #200
Conversation
Overall, this looks good. I mostly have editorial questions.
draft-ietf-taps-interface.md
Outdated
|
|
||
| The Message Object passed to Received is complete and atomic, unless one of the following | ||
| If the minIncompleteLength was set to be infinite, the Message passed to Received | ||
| will complete and atomic (that is, endOfMessage will be set), unless one of the following |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/will/will be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
draft-ietf-taps-interface.md
Outdated
|
|
||
| The Message Object passed to Received is complete and atomic, unless one of the following | ||
| If the minIncompleteLength was set to be infinite, the Message passed to Received | ||
| will complete and atomic (that is, endOfMessage will be set), unless one of the following |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/that is/i.e.?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i.e. literally means "that is", so I think this is about the same?
draft-ietf-taps-interface.md
Outdated
| @@ -945,29 +945,46 @@ capacity that it sees fit. | |||
| # Sending Data {#sending} | |||
|
|
|||
| Once a Connection has been established, it can be used for sending data. Data | |||
| is sent by passing a Message Object and additional parameters | |||
| {{send-params}} to the Send Action on an established Connection: | |||
| is always sent in association with a Message Object and additional parameters | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we use something other than association here? That may complicate references to association elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
“bound to” maybe? “Together with”?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
draft-ietf-taps-interface.md
Outdated
| Message := NewMessage() | ||
| ~~~ | ||
|
|
||
| The messageData parameter contains any octects to be sent for this message. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove "any"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
draft-ietf-taps-interface.md
Outdated
| can be added to that Object. A sendParameters Object can be reused for | ||
| sending multiple contents with the same properties. | ||
| Parameters may be added to a Message object before the Message is used | ||
| for sendings. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe comment that message parameters cannot change after Send is invoked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering how this works with, e.g., "Final" messages. If Message Properties cannot be changed after Send, the application cannot reuse a Message object with an additional "Final" parameter added, but needs to create a new Message object just for this purpose, adding all properties of the previous messages to it, and "Final". Is this intended?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need to do a write close with Final, you should mark that on your Message in the first send. If you need to send a partial Message that isn't necessarily the Final one, then don't mark it Final. You can later send a complete message that is final with no data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chris-wood Added text
draft-ietf-taps-interface.md
Outdated
| @@ -1185,30 +1193,41 @@ number of Messages with a single call. This allows an application to provide | |||
| backpressure to the transport stack when it is temporarily not ready to | |||
| receive messages. | |||
|
|
|||
| Receive takes an optional minIncompleteLength argument. This value indicates | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's optional, it should be the last parameter, or we should specify that a value of 0 is the default when one should use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in general the question of defaults should be better addressed in this draft. I think we can say that “syntactic sugar is allowed, here are the default behaviors we recommend”. Everything about partial send should be optional. This also implies we need connection-bound default send parameters...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both min and max are optional, so they can't both be the last parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep -- I made the comment before getting to the next paragraph. Maybe add: "Both minIncompleteLength and maxLength are optional parameters." at the beginning of this paragraph, and then proceed to describe them both?
draft-ietf-taps-interface.md
Outdated
| value is infinite, which means that only complete, atomic Messages will be delivered. | ||
| If this value is set to some smaller value, the associated ReceiveHandler will | ||
| be triggered only when at least that many bytes are available, the Message is | ||
| complete, or the system needs to free up memory. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the last case is exceptional, perhaps we should mention that applications must check the received length before use, as it may be less than minIncompleteLength?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added text
draft-ietf-taps-interface.md
Outdated
| protocol, and provides methods to access metadata about the received data. | ||
| The octets of data associated with this message are delivered as messageData. | ||
| Multiple invocations of the ReceivedHandler may devlier data for the same | ||
| Message until the endOfMessage flag is delivered. See {{receive-framing}} for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial message data is sent in-order. Received data may be received out-of-order, right? Do we need to clarify that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is THE problem I have with this whole thing - it's also the question that I raised at the end of #148 and don't see addressed. Partial messages may arrive out of order, or they may never arrive at all, if we map them on an unreliable transport. At least, applications must have a number to identify that they're now getting message fragment 3 out of 7, something like that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what about partial reliability? E.g., fragments 1 and 2 out of a total of 3 arrive in time, but for the third, the time expires and the transport gives up. Shouldn't it then tell the receiving application that it will never get the third fragment, and should throw away the first two?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My inclination here is to only receive chunks in order and not deliver gaps. Does that work for you?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tfpauly I think that's a good direction, as it makes things much easier. I think we should then still add a way for the transport system to signal to the application: "throw away your chunks, the rest is never going to arrive" - e.g. to support partial reliability.
But I have a bigger concern, then: what about UDP? What if I send a large message, the chosen transport is UDP, and the message is split it into 3 packets - first, this is perhaps not a great idea; second, the receiver won't be able to know about the order, so can't even do what you suggest here. How are you going to handle this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, in our implementation, we allow the protocol to require that data for a single message fits within a limit (for UDP, whatever it can fit in a datagram). Splitting up a message over multiple UDP datagrams without any other framing layer on top doesn't make any sense, since there isn't any way for the other side to understand the message. So, in our implementation, we throw an error if you try to do this.
Protocols on top of UDP, like IKE, may support fragmenting large messages across datagrams, but then the message that you send is first interpreted by a protocol in the stack that supports unbounded message sizes.
I also do agree that there should be a signal that the chunks won't ever arrive—our implementation passes an optional error along with the ReceiveHandler to indicate this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok! Great, I think that solves it. I'll conclude by requesting the following text:
- in the API draft, some kind of a signal that the chuncks won't error arrive (as we agreed)
- in the API draft, for the receiver side, a hint that message chunks will only arrive in order
- in the implementation draft, a hint that message fragmentation of course requires support by the underlying protocol and thus the size of any message, even with fragmentation, will have to be limited to the message size that the underlying protocol supports. For example, messages can not be spread across multiple UDP packets because the receiver would not understand that the message parts belong together.
I can take care of 2 and 3 (but I'm fine with you doing it too if you want! Just offering to help instead of always only criticizing!) .... but for 1), I think it would be better for you to do it since your implementation already does it. Ok?
|
Looks like a good approach. |
thanks @tfpauly for doing this... this seems to be the simplest way to handle message non-atomicity while still providing for atomic(-like) sends and receives in the general case. All of my problems with what's here are about the elegance of the interface the programmer will see (a "Message" that is separate from the content seems misnamed; this adds complexity that we should make sure everyone knows can be papered over with syntactic sugar in the common case).
I'm happy with landing this as-is for Montreal and filing an issue to discuss in person, though.
draft-ietf-taps-interface.md
Outdated
| @@ -945,29 +945,46 @@ capacity that it sees fit. | |||
| # Sending Data {#sending} | |||
|
|
|||
| Once a Connection has been established, it can be used for sending data. Data | |||
| is sent by passing a Message Object and additional parameters | |||
| {{send-params}} to the Send Action on an established Connection: | |||
| is always sent in association with a Message Object and additional parameters | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
“bound to” maybe? “Together with”?
draft-ietf-taps-interface.md
Outdated
|
|
||
| ~~~ | ||
| Connection.Send(Message, sendParameters) | ||
| Connection.Send(Message, messageData, endOfMessage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
beating the elegance and simplicity drum again: this is correct, but it’d be nice to note here, especially, that syntactic sugar and reasonable defaults are encouraged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the split between Message and messageData awkward. Also, parameters are logically separate from the message itself, so it makes sense to write this as we had it - but "endOfMessage" is just one out of many message parameters, so why have this as a "special" parameter in the send call?
draft-ietf-taps-interface.md
Outdated
| Each Message can be marked with Send Parameters to control how Data | ||
| associated with the Message will be sent down to the underlying Protocol Stack | ||
| and transmitted. Note that these properties are per-Message, not per-Send. | ||
| All data portions associated with a single Message share properties. For example, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t like “data portions” here but I struggle to do better...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Data blocks?
draft-ietf-taps-interface.md
Outdated
|
|
||
| ~~~ | ||
| SendParameters := NewSendParameters() | ||
| SendParameters.Add(parameter, value) | ||
| Message := NewMessage() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this raises the question as to why you can’t also bind an octet array to a message instance. Also platform specific sugar, but it seems to me that if you can’t bind data to a thing on send that it should probably not be called a message...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The data needs to be specific to the send action, not the Message, or else the send action becomes somewhat meaningless. You could call the Message object just MessageContext or MessageParameters or MessageMetadata, which would be a bit more accurate.
draft-ietf-taps-interface.md
Outdated
| @@ -1185,30 +1193,41 @@ number of Messages with a single call. This allows an application to provide | |||
| backpressure to the transport stack when it is temporarily not ready to | |||
| receive messages. | |||
|
|
|||
| Receive takes an optional minIncompleteLength argument. This value indicates | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in general the question of defaults should be better addressed in this draft. I think we can say that “syntactic sugar is allowed, here are the default behaviors we recommend”. Everything about partial send should be optional. This also implies we need connection-bound default send parameters...
draft-ietf-taps-interface.md
Outdated
|
|
||
| ~~~ | ||
| Connection -> Received<Message> | ||
| Connection -> Received<Message, messageData, endOfMessage> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as with send, having a thing called a Message as well as messageData also on receive is... confusing. Renaming this to MessageContext would fix this at the expense of verbosity and slight obtuseness...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like MessageContext
I see where this is coming from, but I don't like the proposed API – it exposes a lot of low-level details about partial messages that I was hoping we'd get away from with the new API.
I wonder if we should regard messages as more active objects? That is, a Message is something that knows whether it's complete or not, and that can produce more data as appropriate. The API could then be to send() a Message object once, then internally the system will repeatedly call getData() on the Message object until it returns that it's complete. This makes the conceptual model easier: you just send a message. The complexity of dealing with partial messages, etc., is then pushed into the message object.
UPDATE: I'm much happier now after discussion with Tommy. Now it's clear that messages will only be delivered in-order, taking much of the complexity away, and we won't have messages spanning over multiple UDP packets or anything like that.
draft-ietf-taps-interface.md
Outdated
| protocol, and provides methods to access metadata about the received data. | ||
| The octets of data associated with this message are delivered as messageData. | ||
| Multiple invocations of the ReceivedHandler may devlier data for the same | ||
| Message until the endOfMessage flag is delivered. See {{receive-framing}} for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is THE problem I have with this whole thing - it's also the question that I raised at the end of #148 and don't see addressed. Partial messages may arrive out of order, or they may never arrive at all, if we map them on an unreliable transport. At least, applications must have a number to identify that they're now getting message fragment 3 out of 7, something like that.
draft-ietf-taps-interface.md
Outdated
|
|
||
| ~~~ | ||
| Connection.Send(Message, sendParameters) | ||
| Connection.Send(Message, messageData, endOfMessage) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find the split between Message and messageData awkward. Also, parameters are logically separate from the message itself, so it makes sense to write this as we had it - but "endOfMessage" is just one out of many message parameters, so why have this as a "special" parameter in the send call?
draft-ietf-taps-interface.md
Outdated
| protocol, and provides methods to access metadata about the received data. | ||
| The octets of data associated with this message are delivered as messageData. | ||
| Multiple invocations of the ReceivedHandler may devlier data for the same | ||
| Message until the endOfMessage flag is delivered. See {{receive-framing}} for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And what about partial reliability? E.g., fragments 1 and 2 out of a total of 3 arrive in time, but for the third, the time expires and the transport gives up. Shouldn't it then tell the receiving application that it will never get the third fragment, and should throw away the first two?
|
@csperkins - your idea sounds interesting, but from your description I don't understand why this reduces complexity in the API. I think the complexity is related to ordering. If we assume that, on the receiver side, message fragments are only delivered in order, things become easier. Then we may only need an extra signal to tell the receiver that remaining fragments will never arrive. |
|
I like the general idea and I agree having an explicit message object really is a way out, but I also think the API in this PR exposes too many details and makes the "common" case unnecessarily complicated. I like @csperkins Idea to move that partial send complexity into the message object. |
|
on second thought, let me +1 @csperkins on "move partial send complexity into the message object":
|
|
@csperkins Right, having Message objects be the active objects for sending and receiving is the other option. That's the one that we had implemented back when we were working on Minion, years ago. It certainly can work, but adds a ton of complexity to the application workflow, and reduces the ability to have meaningful back pressure and flow control. I think that to a large degree, this PR is just writing out the underlying way that sending and receiving in the current draft works. Here's the long form of sending that works for partial and complete: Send(messageContext, messageData, endOfMessage) The default for endOfMessage should be true, therefore most applications will just see: Send(messageContext, messageData) And the the default for messageContext should be the default parameters for a new message, so most Send(messageData) The same is true for receive. We can give the variants of Send to be partial or not different names, but they boil down to convenience/default sets of the same thing. The key points here are these:
If you move to sending data on a new Message object rather than the Connection, you would need to have back pressure on those sends too. It becomes very complicated for the application: Connection.Send(Message) -> MessageSentHandler[May have error but can't be used for flow control] Going down this road means that partial sends become front-and-center for applications dealing with the API, even when they don't want it, and adds an extra layer of indirection just to get basic bytes on the wire for a transport. The approach in this PR ends up being a way to allow expressiveness for partial sends when they are needed, but allows applications to ignore it most of the time with defaults and write the simplest code. |
To date we've avoided adding alternate action and event signatures to this API, leaving what gets defaulted and how up to the platform. Perhaps we should explicitly suggest/define which parts of the API should be defaulted out... I think that would go a long way to reducing the apparent complexity of this approach. |
|
Right, having the default support makes things much easier. For languages that support defaults (Swift, for us), you can do the scheme like I had above. For languages like C, you can write out the common sets of calls, which all can be implemented using the most complex:
|
|
+1 on introducing defaults. The API is beginning to get quite complex. Maybe we should explicitly split up the send section in "Basic Send Operations" and "Advanced Send Functionality" or something like that. Splitting up other relevant sections in a similar way. |
|
@abrunstrom Sure, I can work tomorrow on splitting out the definition of the simple send operations, versus the more full story for partial sends. We can mention as an implementation detail that everything in the simpler approach for atomic messages can be expressed in terms of defaults in the variants that support partial sends, and leave the specific symbol bindings as a per-language choice. |
|
+1 to @abrunstrom's recommendation. |
|
addressing @abrunstrom's comment here would make this ready to land IMO. I'd like to get this and #201 in soon so we can submit -01 in time, and keep working on this in person in Montreal. |
|
Okay, just updated the Sending side to break out basic sends, sending with context, and partial sends. I'll do a bit more work on receiving too today, but take a look at the sending side and see if that addresses the comments there. |
|
Thanks @tfpauly for the restructuring, I like the new structure! I find it much more clear and readable. One question on the Events in the relation to the partial sends. If I understand it correctly Sent Events relate to specific send calls whereas Expired Events relate to an entire Message. Not sure about SendError Events? Seems there it may depend on what the error is. Not sure if this is something that needs to be clarified further. |
|
@abrunstrom I personally think it's simplest if the events correspond to Send events, so if you did a 2 partial sends for a single message that expired, you should get two Expired events if neither could be sent, etc. |
|
Makes sense, thanks. |
|
I've added the rest of my reworked text, to bring Receives into the same style as Send (with the basic approach up front, and partial phrased as an extension later on). Thanks for all of the good feedback, I think this text is certainly clearer! @abrunstrom I added some text about how Send events are delivered when there are multiple Sends per Message, please check. @mwelzl I added text to explain that receiving is only in-order contiguous chunks, and clarified that you can't span a message over multiple datagrams directly, etc. |
|
Thanks @tfpauly , the added text looks good to me! |
draft-ietf-taps-interface.md
Outdated
| check the length of the data delivered to the receive event and not assume | ||
| it will be as long as minIncompleteLength in the case of shorter complete Messages | ||
| or memory issues. | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that the applications should derive the length of the delivered data from the messageData. Perhaps this should be made more explicit? Before I got to the ReceivedPartial Event when reading, where the messageContext was reused for multiple events, I was thinking that the length of the data delivered may be provided to the application as part of the messageContext meta data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I'll add an explicit comment about this.
draft-ietf-taps-interface.md
Outdated
| allowed for a single Message. If a Message is too large to fit in the Maximum Message | ||
| Size for the Connection, the Send will fail with a SendError event ({{send-error}}). For | ||
| example, it is invalid to send a Message over a UDP connection that is larger than | ||
| the available datagram sending size. | ||
|
|
||
| If Send is called on a Connection which has not yet been established, an | ||
| Initiate Action will be implicitly performed simultaneously with the Send. | ||
| Used together with the Idempotent property (see {{send-idempotent}}), this can |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A nit: I suggest to remove the "Used" at the beginning of this sentence
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, removed.
draft-ietf-taps-interface.md
Outdated
| The application can set a minIncompleteLength value to indicates the smallest partial | ||
| Message data size in bytes that should be delivered in response to this Receive. By default, | ||
| this value is infinite, which means that only complete Messages should be delivered. | ||
| If this value is set to some smaller value, the associated recieve event will be triggered |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo "recieve"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed! Thanks for catching.
|
This LGTM now, thanks a lot for doing this! |
|
@tfpauly wrote:
Agree with these.
I wasn't arguing to move the sending data onto the Message object. In my approach, the Connection would still handle sending the data, but rather than have the application provide chunks of data, I was suggesting to have the connection pull data from the message object as needed.
Don't we need that back pressure anyway? If one sends a message object from which data is pulled as needed, then I agree the connection would need to generate It's perhaps easier for the application to change what it sends, or cancel the send, if it send chunk-by-chunk – that I agree – but otherwise I don't see a complexity saving. |
|
@csperkins Regarding backpressure, you're right that we do need backpressure on all chunks that are sent. However, by allowing partial sends to be expressed in the native send action of the Connection, they automatically get the same backpressure that complete sends get. Each Sent event is one-to-one with a Send action, whether partial or complete. |
|
Okay, I think based on the reviews we're in a good state to land this, and I do want to unblock merging for #201. As such, I'm going to go ahead and merge. |
Satisfies #148