Avoid ClassCastException when getting values from QueryContext#13022
Avoid ClassCastException when getting values from QueryContext#13022FrankChen021 merged 9 commits intoapache:masterfrom
QueryContext#13022Conversation
|
This pull request introduces 1 alert when merging 93db082 into 6805a7f - view on LGTM.com new alerts:
|
|
@clintropolis Could you review this? |
clintropolis
left a comment
There was a problem hiding this comment.
seems reasonable to me 👍
| return null; | ||
| } | ||
|
|
||
| <ContextType> ContextType getContextValue(String key); |
There was a problem hiding this comment.
hmm, actually I guess this is an @ExtensionPoint, we should probably leave the old methods here to not be backwards incompatible
| { | ||
| } | ||
|
|
||
| static <T, E extends Enum<E>> E parseEnum(Query<T> query, String key, Class<E> clazz, E defaultValue) |
There was a problem hiding this comment.
this class is also marked @PublicApi and so we should ideally leave the signatures in place
There was a problem hiding this comment.
But this method is not declared as public. Do we need to keep it?
paul-rogers
left a comment
There was a problem hiding this comment.
@FrankChen021, thanks for helping clean up this area! It is in much need of tidying up. A few comments for your consideration.
|
|
||
| default int getContextAsInt(String key, int defaultValue) | ||
| { | ||
| if (getQueryContext() != null) { |
There was a problem hiding this comment.
Nit: use == and revers the then and else clauses. Makes the code just a bit easier to read.
| } | ||
|
|
||
| @Nullable | ||
| default Boolean getContextAsBoolean(String key) |
There was a problem hiding this comment.
Nit: use the same name for both "flavors" of methods: easier to remember.
There was a problem hiding this comment.
I think you mean this method getContextBoolean?
I didn't change this method to use the same name pattern as 'getContextAs...' because there're lots reference to this method which might cause this PR so large. But indeed, it should be the same name pattern.
| } | ||
|
|
||
| @Nullable | ||
| public Boolean getAsBoolean(String parameter) |
There was a problem hiding this comment.
Nit: best to not call query context values "parameters" since a query can have "parameters" which are not in the context. Suggestion: "key".
| return QueryContexts.getAsBoolean(parameter, get(parameter), defaultValue); | ||
| } | ||
|
|
||
| public Integer getAsInt(final String parameter) |
There was a problem hiding this comment.
I wonder, since we have methods for all of these on QueryContexts, and we use QueryContexts to get each specific value type, should we just omit these methods and standard on always using QueryContexts to get context values? Thoughts?
| public static <T> long getMaxScatterGatherBytes(Query<T> query) | ||
| { | ||
| return parseLong(query, MAX_SCATTER_GATHER_BYTES_KEY, Long.MAX_VALUE); | ||
| return query.getContextAsLong(MAX_SCATTER_GATHER_BYTES_KEY, Long.MAX_VALUE); |
There was a problem hiding this comment.
Thanks for cleaning this up! I'd wondered why we have two different ways to get values.
There was a problem hiding this comment.
after this PR, only getContextAsxxxx series methods are used to get values.
| @Nullable | ||
| public static Boolean getAsBoolean( | ||
| final String parameter, | ||
| final Object value |
There was a problem hiding this comment.
This set of methods has gotten a bit muddled.
- This version takes the name of a context key (not "parameter") just so it can throw an error. But, there are places where we (or at least I) want to use this method where the map is not a context.
- There are versions that take a map and a default, and versions, like this, that take a value (not parameter) and convert it.
Maybe have the following set:
// Check if a key exists for code that wants to do something special for this case
public boolean hasValue(Map<..> context, String key) ...
// Generic method to convert objects to boolean
public boolean parseBoolean(Object value)
{
// if can't parse, throw an exception
}
// Specific method for boolean context values
public boolean getBoolean(Map<...> context, String key, boolean defaultValue)
{
value = context.get(key);
if (value == null) {
return defaultValue;
}
try {
return parseBoolean(value);
} catch (Exception e) {
// Throw the "Expected context value [%s] to be a ... exception
}
}There was a problem hiding this comment.
A parseBoolean is still kept at line 474 in this class to allow to parse a value from a map object.
| } | ||
|
|
||
| throw new ISE( | ||
| "Expected parameter [%s] must be type of [%s], actual type is [%s].", |
There was a problem hiding this comment.
Again, context values are not "parameters".
|
@FrankChen021, as it turns out, the While using maps solves some problems, it requires awkward code such as: x = QueryContexts.getSomething(context, key, default)One advantage of the old x = queryContext.getSomething(key, default);Here, you propose a solution: add those values to the query class. That's OK, but it makes the query class pretty complex: we have methods for many tasks, and now we add multiple methods for context values. Plus, there are places (such as the SQL layer) where we work with the context without also having a native query. So, we'd have to duplicate these methods in several places. This suggests that methods to work with the context belong on the context itself, not on the users of the context (such as One solution, which I didn't include in PR 13049, is to define a new class: a typed wrapper on top of the map. Something like: class QueryContext {
Object get(String key);
String getString(String key);
String getString(String key, String defaultValue);
int getInt(String key);
int getInt(String key, int defaultValue);
...
}For each of our supported types: String, Int, Long, etc. We'd then store an instance of
The problem that PR 13049 tries to solve is that there are times when the context is immutable, other times when it is mutable. And, those times trade off: the user's request is immutable, a mutable copy is made while planning the query, then the context must become immutable again once we start executing in multiple threads. So, we could have two forms: the above immutable form with only "getters", and a mutable form: class MutableQueryContext {
Object put(String key, Object value);
Object remove(String key);
...
}To do this, we'd actually want This may still lead to a muddle, however. A There is also the issue of compatibility with custom |
|
Here's a follow-on idea. Modifying a context map can perhaps be left to working directly with the map, since it is mostly just calling interface Query<...> {
default QueryContext getQueryContext() { return new QueryContext(getContext()); }Then, With this approach, all the myriad What do you think? If this will work, I can perhaps modify the PR I mentioned to include this approach. |
|
Went ahead and added the |
Hi @paul-rogers When I touch this part in this PR, I noticed there are two classes, Basically, I agree with you that there should be a facade My focus in this PR is on the safe type conversion at caller side. And the work has been done. How about let's merge this first and then resolve the conflicts after you stablize the core changes on your branch? |
|
@FrankChen021, your goal of type-safe conversion is a good one. The question is how to achieve that goal so that you can get this PR done. Toward that end, PR #13049 was refocused on just the We can still ask the question: should we add more methods to the final String timestampStringFromContext = query.getContextAsString(CTX_KEY_FUDGE_TIMESTAMP, "");With the suggested "temporary" solution, perhaps write this as: final String timestampStringFromContext = QueryContexts.getAsString(query, CTX_KEY_FUDGE_TIMESTAMP, "");In the later PR, this would become: final String timestampStringFromContext = query.queryContext().getString(CTX_KEY_FUDGE_TIMESTAMP, "");The alternative approach avoids adding new methods to the The thought is that we can easily add a few more type-safe methods to final String timestampStringFromContext = query.getContextAsString(CTX_KEY_FUDGE_TIMESTAMP, "");
final boolean flag = QueryContexts.getEnableJoinFilterRewrite(query);In #13071 we can move all the getters to a single class: final QueryContext queryContext = query.queryContext();
final String timestampStringFromContext = queryContext.getString(CTX_KEY_FUDGE_TIMESTAMP, "");
final boolean flag = queryContext.enableJoinFilterRewrite();The result is simpler code, while providing type safety, concurrency safety, and the ability to authorize context values. Thoughts? |
|
@paul-rogers I think the new APIs you proposed is what I want -- that is there's only one place for these final QueryContext queryContext = query.queryContext();
final String timestampStringFromContext = queryContext.getString(CTX_KEY_FUDGE_TIMESTAMP, "");
final boolean flag = queryContext.enableJoinFilterRewrite();Let me remove all newly addde final String timestampStringFromContext = QueryContexts.getAsString(query, CTX_KEY_FUDGE_TIMESTAMP, "");I will do this ASAP so that it won't block other PRs for long time. |
|
@paul-rogers I've replace all reference from calling old one: new one: |
|
@paul-rogers Is there anything that I need to do in this PR? If it looks good to you, I think we can merge it to continue #13071 |
|
Thank you @paul-rogers @clintropolis |
|
the last commit of this PR didn't actually run through travis and seems to be failing https://github.com/apache/druid/runs/8325253980, looking into the issue |
|
opened #13083 to fix the test |
|
@clintropolis Thanks for the fix. I didn't notice that travis not started but just saw the CI status of the last commit is OK. So why did the travis not start? |
The problem was first reported by #12760 , and #12833 solves that specific issue.
This PR solves all potential problems by providing a group of type safe methods.
Description
QueryContextholds values passed from user side in JSON format.And currently Druid requires values in JSON must be strictly type matched.
For example, a property
maxOnDiskStoragein Druid is defined as integer, it must be in the format asIf the number 100 is serialized as string as
Druid throws a
ClassCastExceptionto reject the input query.Actually, string-formatted number "100" can be parsed as number at Druid server side.
Key changes
#12833 adds a method
getContextHumanReadableBytesto get context value from any acceptable format safely. And this PR adds a group of methods to do so and changes all existing code reference to the new APIs.Newly added methods are:
Original unsafe method
getContextValueis still kept to allow us get objects which are stored by Druid server side temporarily.Some javadoc is added to tell callers know which methods should be used instead.
This PR has: