-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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 'TypeStrategy' to types #11888
add 'TypeStrategy' to types #11888
Conversation
* the values maximum size. If the value is null, the size will be {@link Byte#BYTES}, otherwise it will be | ||
* {@link Byte#BYTES} + {@link #estimateSizeBytes(Object)} | ||
*/ | ||
default int estimateSizeBytesNullable(@Nullable T value) |
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 not sure about hand the null handling over to the implementation here. The strategy as I understand it is using a whole byte for whether there is a null value. A byte for whether it is null consumes 8x the space than it really needs, this can actually add up in unfortunate ways when there are lots of columns in the result set.
For example, a "better" implementation would be to start every row with a bitmap of all of the columns that are null. This would consume only a single bit per column rather than a byte per column and, if we were so inclined, could also be properly padded to try to enforce word-alignment.
Having an implementation that does it with a whole byte as a stepping stone is maybe okay. But, with the way this interface is built, the interface is forcing rather sub-optimal null-handling on the query engines that use this interface. I think it would be better to perhaps declare that a size of -1
means that the value is equivalent to null
and have the thing external from this do something meaningful with that knowledge.
We would likely also need to adjust the signature of write
to return an int
to indicate the number of bytes written (once again, returning a -1
for "it was null")
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.
Thanks, I agree, I pulled the null-byte wrapper handling into a special NullableTypeStrategy
which can be used to wrap regular TypeStrategy
implementations (which are now not expected to process nulls), and added lots of javadocs to suggest not using it unless you've got space to burn or no other options, so hopefully it will limit use.
Some high level thoughts:
|
Oops, this was a leftover from the first round of changes from @cheddar 's comment where I moved most of the null byte handling stuff out of
Did this, as well as adding a
Before the most recent set of changes, estimate was actually being used to get the exact size of some value in bytes, in order to check that variably sized values did not exceed this maximum. This has been reworked as suggested in 4) to instead pass
Variably sized serialization was previously already supported (but required externally verifying that there was enough space with the estimate method). But, I liked the ideas here, so I have modified the
Variably sized support is in place, I'm not sure we need a |
@Override | ||
public int compare(Object[] o1, Object[] o2) | ||
{ | ||
final int iter = Math.max(o1.length, o2.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.
should this be math.min as might have a scenario we can get indexOutofBoundException no ?
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.
oops - yes, good catch, will add some tests since there obviously aren't any 😅
final int oldPosition = buffer.position(); | ||
buffer.position(offset); | ||
T value = read(buffer); | ||
buffer.position(oldPosition); |
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.
Consider two changes:
- Using try/finally so the buffer position is still valid if
read
throws an exception - Updating the javadoc to say that even though the buffer position is unchanged upon method exit, it may be changed temporarily while the method is running, so it isn't concurrency-safe.
buffer.position(offset); | ||
write(buffer, value); | ||
final int size = buffer.position() - offset; | ||
buffer.position(oldPosition); |
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.
Consider two changes:
- Using try/finally so the buffer position is still valid if
write
throws an exception - Updating the javadoc to say that even though the buffer position is unchanged upon method exit, it may be changed temporarily while the method is running, so it isn't concurrency-safe.
/** | ||
* Estimate the size in bytes that writing this value to memory would require. This method is not required to be | ||
* exactly correct, but many implementations might be. Implementations should err on the side of over-estimating if | ||
* exact sizing is not efficient |
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 include something here about how this estimate is expected to be used. (Like an example)
That'll help people understand when they should use this method, and also understand how to implement it. I'm suggesting this because you gotta be careful with "estimate" methods: people have wildly differing ideas of how accurate they need to be, so it pays to be specific in the doc comments.
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 to javadocs to better explain usage
|
||
|
||
/** | ||
* Read a non-null value from the {@link ByteBuffer} at the current {@link ByteBuffer#position()}. This will move |
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.
"non-null" isn't true for the NullableTypeStrategy impl, which is still a TypeStrategy, so this method contract should be adjusted.
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.
split NullableTypeStrategy
out to be standalone so the contract isn't confusing, it wasn't really used outside of tests as a TypeStrategy
so it doesn't seem like a loss and there is no longer ambiguity of whether or not nulls are handled so i think is an improvement
* Write a non-null value to the {@link ByteBuffer} at position {@link ByteBuffer#position()}. This will move the | ||
* underlying position by the size of the value written, and returns the number of bytes written. | ||
* | ||
* If writing the value would take more than 'maxSizeBytes' (or the buffer limit, whichever is smaller), this method |
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 spec this to be an error if maxSizeBytes
is greater than buffer.remaining()
? Seems like a weird thing for a caller to do. Or is there a reason a caller might do 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.
i had it this way so that callers that don't know or care about size can pass in an arbitrary value like Integer.MAX_VALUE, but it didn't seem very useful so i've made it an exception now, the old behavior was a bit strange coupled with the negative size written return values for when write doesn't have enough room since the caller knows that they definitely need to add that many bytes to maxSizeBytes to have enough space to complete the write
* If writing the value would take more than 'maxSizeBytes' (or the buffer limit, whichever is smaller), this method | ||
* will return a negative value indicating the number of additional bytes that would be required to fully write the | ||
* value. Partial results may be written to the buffer when in this state, and the position may be left at whatever | ||
* point the implementation ran out of space while writing the value. |
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.
Does this mean the caller is expected to save the position? Or is the caller safe to assume it can roll back by -retval
?
i.e., should callers do this?
int written = write(buf, value, maxBytes);
if (written < 0) {
buf.position(buf.position() + written); // roll back buf, undoing the write
}
or this?
int oldPosition = buf.position();
int written = write(buf, value, maxBytes);
if (written < 0) {
buf.position(oldPosition); // roll back buf, undoing the write
}
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 latter, caller should save the position before writing to handle an incomplete write, added javadocs to clarify
* to set and reset buffer positions as appropriate for the offset based methods, but may be overridden if a more | ||
* optimized implementation is needed. | ||
*/ | ||
public interface TypeStrategy<T> extends Comparator<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.
Will stuff serialized with this interface be persisted to disk, sent between servers that might be running different versions of code, used in caches, etc? Or will it always be in-memory, single-server, & ephemeral?
Implementers are going to want to know the answer, so they can design their formats accordingly. (If persistence is required then they might want to add a version byte, avoid using native endianness, etc.)
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 more javadocs indicating that this is primarily for transient stuffs, and if you want persistence should bring your own version and endian tracking and stuff
* | ||
* @return number of bytes written (always 9) | ||
*/ | ||
public static int writeNullableLong(ByteBuffer buffer, int offset, long value) |
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.
nit: should it be called writeLongWithNonNullMark
or something rather than NullableLong
? It reads like the long parameter can be null.
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.
changed to readNotNullNullable...
/writeNotNullNullable...
to reduce confusion
* | ||
* @return number of bytes written | ||
*/ | ||
int write(ByteBuffer buffer, T value, int maxSizeBytes); |
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 document in the javadoc that this method can explode when maxSizeBytes > buffer.remaining()
?
@Override | ||
public int write(ByteBuffer buffer, T value, int maxSizeBytes) | ||
{ | ||
byte[] bytes = objectStrategy.toBytes(value); |
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 this method also call TypeStrategies.checkMaxSize()
first?
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.
+1 after CI. Thank you @clintropolis!
thanks for review all 👍 |
* add TypeStrategy - value comparators and binary serialization for any TypeSignature (cherry picked from commit e583033)
* add TypeStrategy - value comparators and binary serialization for any TypeSignature (cherry picked from commit e583033)
Description
Following up on discussion in #11853, this PR introduces a new
TypeStrategy
interface which defines binary serde and comparison for values of anyTypeSignature
.The binary serialization methods were consolidated from the static methods in
Types
, and the code I think is overall much more simple.TypeStrategy
deals in objects, so is not optimal for primitive values like longs and doubles, so the static methods for reading/writing these values with nulls remain but now live inTypeStrategies
, along with all of the built-in implementations (string, long, double, float, array).The comparator implementations match the orderings defined in
druid-processing
... we may wish to consider bringing some of them intodruid-core
, mergingdruid-core
anddruid-processing
, or something else to try to consolidate things a bit better. The array comparator behaves mostly the same as postgres array comparison afaict, with the exception that we default to "nulls first" for most thing rather than "nulls last" so we do that instead.ObjectByteStrategy
is moved back intoObjectStrategy
, it's registry replaced with one ofTypeStrategy
, and insteadComplexMetricsSerde
has a new default implementation of a method calledgetTypeStrategy
which wraps theObjectStrategy
methods, but could be overridden for more optimal implementations.I think the utility of being able to get binary serde to use for things like buffer aggregators is on full display in the
ExprEval
serialization methods, so I think overall i'm in favor of this change, since basically anywhere you have aColumnInspector
(which includesRowSignature
) you have the means to read, write, and compare values.Key changed/added classes in this PR
TypeStrategy
TypeStrategies
Types
NullableTypeStrategy
ComplexMetricsSerde
ColumnType
ColumnTypeFactory
ExpressionType
This PR has: