-
Notifications
You must be signed in to change notification settings - Fork 48
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
Add checked exceptions to Runnel decoding and tweak related APIs #490
base: master
Are you sure you want to change the base?
Add checked exceptions to Runnel decoding and tweak related APIs #490
Conversation
cead920
to
fc9d96d
Compare
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 starting to have mixed feelings about this PR. I'm not against the idea of introducing the mandatory field concept, but it's going to make writing multi-version capable codecs much trickier.
Very few fields if any should ever be mandatory, and I'm worried that we mistakenly start setting most if not all fields mandatory.
I wonder if leaving the existing API untouched for the optional fields, and adding a slightly more involved one for mandatory fields (something like decoder.int32()
vs decoder.mandatory().int32()
) wouldn't be a much better idea to discourage those mandatory fields.
Or am I misguided? An example of multi-version aware codec would certainly help making our mind here.
@@ -15,7 +15,10 @@ | |||
*/ | |||
package org.terracotta.runnel.decoding; |
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 have a few problems with the changes in this class:
-
The
optional
/mandatory
prefixes are too long to my taste. I have a feeling that this is going to hurt codec readability. -
I have the same kind of feeling towards the optional versions returning
Optional<something>
. Over time, most fields will become optional because of backward compatibility and I fear that the handling ofOptional
will be more a nuisance than anything else.
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 also a little iffy about the performance of the Optional stuff... could we maybe go with:
int int32(String name) throws RunnelDecodingException,
int int32(String name, int other) throws RunnelDecodingException
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 existing enterprise code implicitly makes some fields mandatory by doing:
long value = decoder.int64("value")
which gives a NPE if the "value" field isn't present.
I'd much rather have it made explicit which fields are mandatory / optional.
I think this will make multi-version codecs easier.
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 think most fields will be mandatory. Most fields are currently handled as mandatory, in that they would throw an NPE if they are not present.
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 think that adding a decoder.mandatory() method to the API will help.
If you have a mixture of optional and mandatory fields then you would end up with something like:
String a = decoder.mandatory().string("a");
Long b = decoder.int64("b");
int c = decoder.mandatory().int32("c2);
The "mandatory().int32()", for example, if longer a involves an extra object compared to mandatoryInt32().
You could save slightly on the optional fields, but it's less explicit that they are optional (NPEs again?) and mandatory fields are probably more common.
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'd be happy to change the "optional" and "mandatory" prefixes to something else if you have a better suggestion. Although, I think they are fine.
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.
Handling an Optional is easier and less error prone than handling the possibility of null. The performance impact of adding Optional is tiny - especially when compared to the network overhead of the message. It's not like we are boxing a primitive up to an Optional. We are working with Objects (like Integer).
The problem with the API Chris D suggested:
int int32(String name) throws RunnelDecodingException,
int int32(String name, int other) throws RunnelDecodingException
is that a) it doesn't let you do anything more complex than supplying a default and b) a developers natural tendency will be to use the first version, leading to exceptions for fields that are optional. It's not putting the optionality of the field in your face enough.
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 have a slightly different take on this ... at the moment, I'd prefer to have the "mandatory-ness" of a field declared in the struct definition -- it's just as much of an error to omit the value of a mandatory field during encoding as it is to find the field absent during decoding. This gets rid of the mandatory/optional designations in the encoding and decoding methods. It also makes it more clear (at least to me) that, after initial publication, ALL amendments to the struct must be handled as optional fields. with appropriate actions (supply default or flag value; throw field-specific, actionable exception) taken if the field is necessary but missing. (This might actually be helped by methods that're specifically for "needed but missing" fields.)
Playing off of this, we should have unit tests that validate that mandatory struct members haven't been added to keep folks from adding one after initial release.
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 toyed with the idea of the struct definition containing the optionality of the fields, but you still end up with needing to change the decoding API to have optional and mandatory versions of the decoding methods because the return types are different (e.g. primitive vs Optional).
The driver for doing it in the struct would be safer coding, rather than security. Runnel would presumably, therefore, throw a RuntimeException (rather than a checked exception) if code tried to encode data that didn't match with the optionality of the fields in the struct.
If one of our codecs encodes data without a mandatory field, that is a different class of error to an attacker deliberately omitting mandatory data.
I think having the optionality declared in the struct is a good idea, but it feels like an orthogonal change to the one this PR is covering.
import java.util.NoSuchElementException; | ||
|
||
/** | ||
* @author Ludovic Orban | ||
*/ | ||
public class StructArrayDecoder<P> implements Iterator<StructDecoder<StructArrayDecoder<P>>> { | ||
public class StructArrayDecoder<P> { |
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 you can't use java.util.Iterator
anymore but this class is a close cousin, maybe we should introduce a runnel iterator interface that this (and other array decoders) would implement?
private final P parent; | ||
private final ReadBuffer arrayReadBuffer; | ||
private final int arrayLength; | ||
private final StructField field; | ||
|
||
private StructDecoder<StructArrayDecoder<P>> current = null; | ||
private int count = 0; |
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.
Why did you introduce this counter?
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.
It's used in the hasNext() method. I didn't want to use the fact that there was no more data in the buffer because, with the counter, we can detect when there wasn't enough data in the buffer to read all the elements that there are supposed to be.
|
||
import java.nio.ByteBuffer; | ||
import java.util.Optional; | ||
|
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.
Same as above regarding optional
/ mandatory
prefixes and the Optional
return types.
return fieldFetcher.fetch(name).orElseThrow(() -> new MissingMandatoryFieldException(name)); | ||
} | ||
|
||
private interface FieldFetcher<T> { |
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.
Add @FunctionalInterface
?
* Indicates there was a decoding problem caused by the data being decoded not matching Runnel's expectations | ||
*/ | ||
public class RunnelDecodingException extends Exception { | ||
protected RunnelDecodingException() { |
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.
That constructor shouldn't be needed.
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.
It's used in LimitReachedException. LimitReachedException does not have much to add in the way of message other than the data ran out unexpectedly - which is what the exception type represents.
fc9d96d
to
04824ce
Compare
@@ -37,11 +42,19 @@ public ArrayDecoder(ValueField<T> arrayedField, ReadBuffer readBuffer, P parent) | |||
this.length = readBuffer.getVlqInt(); | |||
} | |||
|
|||
public int length() { |
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.
Isn't this solving the problem at too general/low a layer? If we want to be forceful herewe could change the signature to:
public int length(int maximum) throws RunnelDecodingException;
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.
When a developer uses that API, how will they decide what the maximum should be? What if there is more data being sent than the maximum? We would have to go through all the code writing data to do batching on all messages with an array in - a much bigger change.
Otherwise, pretty much the only value a developer will supply as as an argument will be Integer.MAX_VALUE - this does not help. I don't want developers to have to think about these kind of security arguments whenever they are writing a codec.
|
||
/** | ||
* @author Ludovic Orban | ||
*/ | ||
public class ArrayDecoder<T, P> { | ||
public class ArrayDecoder<T, P> implements DecodingIterator<T> { |
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.
Iterator
isn't anywhere near as useful as Iterable
.
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.
Iterable
isn't as easy to implement as Iterator
:-)
Besides we aren't implementing Iterator
, just an interface that looks a bit like it.
I would see moving to an Iterable
-like API as outside the scope of this PR. Adding Iterable
is adding functionality to the API and is unrelated to security.
I'd be quite happy to get rid of the DecodingIterator
interface altogether. None of the enterprise code makes use of the iterability of these decoders.
|
||
import org.terracotta.runnel.utils.RunnelDecodingException; | ||
|
||
public interface DecodingIterator<T> { |
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 we adopt iterable instead of iterator then normal usage patterns would probably make the decoding exception unecessary.
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 can't see how that would get rid of the exception. Could you explain?
As far as I can tell, when you go to do the equivalent of next(), you can always encounter a problem with the data (e.g. ran out of bytes). I can't see how this changes with a different API.
@@ -15,7 +15,10 @@ | |||
*/ | |||
package org.terracotta.runnel.decoding; |
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 also a little iffy about the performance of the Optional stuff... could we maybe go with:
int int32(String name) throws RunnelDecodingException,
int int32(String name, int other) throws RunnelDecodingException
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.
Is a meeting called for to resolve the mandatory/optional concept handling?
@@ -28,6 +29,6 @@ | |||
|
|||
int index(); | |||
|
|||
void dump(ReadBuffer readBuffer, PrintStream out, int depth); | |||
void dump(ReadBuffer readBuffer, PrintStream out, int depth) throws RunnelDecodingException; |
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.
It's probably a better idea for this method to handle the RunnelDecodingException
internally and write a message to the PrintStream
that a decoding error occurred. If I understand the purpose of this method, it's for diagnostics and, in general, diagnostic methods shouldn't throw.
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've now changed dump to write to the PrintStream, as you suggested. I also changed the dump() methods to return boolean rather than void. That way when one dump() method calls another, it knows to abort early if there was a decoding error.
try { | ||
reconnectData = LeaseReconnectData.decode(bytes); | ||
} catch (RunnelDecodingException e) { | ||
throw new ReconnectRejectedException(e.getMessage(), 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.
Is this being logged anywhere? If we have a client feeding the server bad data, we need to make tracks in the log.
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 a good question. The answer is not so obvious. I have sent an email about how these exceptions are handled to @myronkscott and @cschanck.
@Override | ||
public T next() throws RunnelDecodingException { | ||
if (count >= length) { | ||
throw new NoSuchElementException(); |
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'd be inclined to throw something other than NoSuchElementException
here. I know that's what Iterator.next
throws but it's a RuntimeException
that should be handled in the same way as RunnelDecodingException
in code that uses this method (directly or indirectly).
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 think that a call to next() where count >= length
is in the same category.
It's not caused by badly-formatted data - unlike the RunnelDecodingException. It's caused by calling code incorrectly calling next() when hasNext() returned / would have returned false. It's more like an AssertionError.
} | ||
|
||
if (arrayReadBuffer.limitReached()) { | ||
if (count >= arrayLength) { | ||
throw new NoSuchElementException(); |
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.
See comment on ArrayDecoder.next()
.
04824ce
to
62cd5d3
Compare
No description provided.