-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
[Feature/extensions] Extensibility support for getNamedWriteables extension point #3925
[Feature/extensions] Extensibility support for getNamedWriteables extension point #3925
Conversation
…transport mechanisms to handle named writeable entry registration from extensions and parse requests. Signed-off-by: Joshua Palis <jpalis@amazon.com>
…testing purposes, exten response will now only include the parse action status Signed-off-by: Joshua Palis <jpalis@amazon.com>
Signed-off-by: Joshua Palis <jpalis@amazon.com>
Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
if (response.getRegistry().isEmpty() == false) { | ||
|
||
// Extension has sent over entries to register, initialize inner category map | ||
Map<Class<?>, Map<String, ExtensionReader>> categoryMap = new HashMap<>(); |
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.
What's the need to have Class type as wildcard and not specific here and the for the rest?
// add name and callback method reference to inner reader map | ||
// Since appending the new reader entry returns the previous value associated with key, or null if there was no | ||
// mapping for key, validate that name has not yet been associated with a callback method to it's associated | ||
// extension |
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.
Let's use multi line comment and follow javadocs rules to write comments
// extension | ||
ExtensionReader newReader = (en, cc, context) -> parseNamedWriteable(en, cc, context); | ||
ExtensionReader oldReader = readers.put(name, newReader); | ||
if (oldReader != 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.
Better way to check
if (oldReader != null) { | |
if (oldReader != null && oldReader.getClass()) { |
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 the suggestion needs another != null there?)
} | ||
} | ||
|
||
// handle last category and reader entry |
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.
// handle last category and reader entry | |
// Handle last category and reader entry |
// handle last category and reader entry | ||
categoryMap.put(currentCategory, readers); | ||
|
||
// attach extension node to categoryMap |
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.
// attach extension node to categoryMap | |
// Attach extension node to categoryMap |
}; | ||
|
||
try { | ||
logger.info("Sending extension request type: " + REQUEST_EXTENSION_NAMED_WRITEABLE_REGISTRY); |
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.
Let's add this logger for all the request we are sending from OS to SDK.
new NamedWriteableRegistryRequest(true), | ||
namedWriteableRegistryResponseHandler | ||
); | ||
// wait for response from SDK |
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.
// wait for response from SDK | |
// Wait for response from SDK |
} | ||
|
||
@Override | ||
public void handleResponse(NamedWriteableRegistryResponse response) { |
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.
Let's create a separate class/file for handleResponse and not increase the size of this file
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 call, I'll separate out all the response handlers into different files and place them within the Extensions directory
this.context = in.readByteArray(); | ||
} | ||
|
||
public Class<?> getCategoryClass() throws ClassNotFoundException { |
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.
Wildcard type? Can we have a specific type here for the Class?
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.
Looking at opensearch-project/opensearch-sdk-java#54, it seems the type should be NamedWriteable
?
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.
Category Classes are expected to extend the NamedWriteable class, I'll change all wildcard type references to reflect this.
* | ||
* @opensearch.internal | ||
*/ | ||
public class NamedWriteableRegistryParseResponse extends TransportResponse { |
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.
Let's not create a separate class for every boolean response. We already have IndicesModuleNameResponse
doing something similar. We can make it more generic and use it for IndicesModuleNameResponse and NamedWriteableRegistryParseResponse
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.
BooleanResponse<NamedWritableRegistry>
and BooleanResponse<IndicesModuleName>
?
* | ||
* @opensearch.internal | ||
*/ | ||
public class NamedWriteableRegistryRequest extends TransportRequest { |
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.
Let's create a generic request class for Boolean Request
} | ||
} | ||
|
||
// TODO : enable dynamic registration of named writeables by returning the extension registry map within Node.java |
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 this is not the part of the PR. Let's create an issue and link here to not miss the work
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.
Great comment/advice, @owaiskazi19. Any temporary placeholder or TODO code should have an issue tracking it and put the link to the issue in a comment.
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.
Great start!
/** | ||
* Extensibility support for Named Writeable Registry: request to extensions to parse context | ||
* | ||
* @opensearch.internal |
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.
For my own education, when is this annotation used?
server/src/main/java/org/opensearch/common/io/stream/NamedWriteableRegistryParseRequest.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
public byte[] getContext() { | ||
return this.context; |
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 returns a reference to this.context
which would allow whoever received it to alter the values inside the array. You should use Arrays.copyOf(this.context, this.context.length);
} | ||
|
||
public Class<?> getCategoryClass() throws ClassNotFoundException { | ||
Class<?> categoryClass = Class.forName(this.categoryClassName); |
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.
Checking whether a valid class exists for categoryClassName
may be better placed in the constructor where the String
is passed (or read from a stream). You could then catch the exception and then throw an IllegalArgumentException
(which you should document with @throws
in the javadoc for the constructor!)
Although this opens the question of how to handle the class name when read from a stream. How do we make sure that nodes on both sides of the stream have the same classes? (Relevant to @owaiskazi19 comment above on restricting class type.)
And this opens an even bigger issue is dealing with same-named classes and getting name collisions. You probably want the fully qualified class name here. (EDIT: I see that the intention is to use Class.getName()
for this which is fully qualified. But the string parameter should have that documented clearly.)
And given that the parsing signature in ExtensionsReader
uses a Class
rather than a String
we might consider using that here as well. That would both assure a valid class as well as easily give access to the fully qualified name.
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.
Category Class definitions reside within the OpenSearch JVM and are currently exposed to the SDK via the maven local repository, thus ensuring that the extension has the same class. I'll definitely add the check for valid classes within the constructor and also add more clarification with the javadocs.
|
||
@Override | ||
public String toString() { | ||
return "NamedWriteableRegistryParseRequest{" |
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.
Question for my own education: do we have a standard format for toStrings? Should we?
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 pattern we have been following is to print out the request/response type along with the contents of the request/response
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 less the type(class name) and field listing it's more the delimiters. I've seen curly braces, square braces, newlines. Most IDEs will auto-generate a toString from fields. I'm wondering which particular format we are using, if any.
/** | ||
* Transports category class, and byte array (context), to the extension identified by the Discovery Node | ||
* | ||
* @param extensionNode Discovery Node identifying the Extension |
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.
(Style) leave an extra space between parameter name and its description.
* | ||
*/ | ||
|
||
void parse(DiscoveryNode extensionNode, Class<?> categoryClass, byte[] context) throws UnknownHostException; |
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 exception should be added to the javadoc with a @throws
. As this is an interface, this javadoc will be inherited by implementations.
* Transports category class, and byte array (context), to the extension identified by the Discovery Node | ||
* | ||
* @param extensionNode Discovery Node identifying the Extension | ||
* @param categoryClass Super class that the reader extends |
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 probably have an interface
for the superclasses the readers extend, and use that for the type parameter. (And the type parameter needs its own javadoc.)
// extension | ||
ExtensionReader newReader = (en, cc, context) -> parseNamedWriteable(en, cc, context); | ||
ExtensionReader oldReader = readers.put(name, newReader); | ||
if (oldReader != 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.
(I think the suggestion needs another != null there?)
); | ||
} | ||
} catch (ClassNotFoundException e) { | ||
logger.error(e.toString()); |
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.
May want an explanatory text string in addition to the error here.
Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
…oject/OpenSearch into namedwriteables rebasing fork with feature/extensions
Gradle Check (Jenkins) Run Completed with:
|
Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
*/ | ||
public NamedWriteableRegistryResponse(StreamInput in) throws IOException { | ||
super(in); | ||
// stream output for registry map begins with a variable integer that tells us the number of entries being sent across the wire |
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.
// stream output for registry map begins with a variable integer that tells us the number of entries being sent across the wire | |
// Stream output for registry map begins with a variable integer that tells us the number of entries being sent across the wire |
Here and everywhere 🙂
* | ||
* @opensearch.internal | ||
*/ | ||
public class BooleanResponse extends TransportResponse { |
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.
We can use the same response for IndicesModuleNameResponse and get rid of that file?
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.
Also, a better name here OpenSearchBooleanResponse
?
Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
…for OpenSearchRequest Signed-off-by: Joshua Palis <jpalis@amazon.com>
…nsionDir initialization within set up Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
*/ | ||
public class NamedWriteableRegistryParseRequest extends TransportRequest { | ||
|
||
private final Class categoryClass; |
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.
Over on your PR on the SDK side this class is restricted to one that extends NamedWriteable
so you should add that restriction here (in the constructor, throwing IllegalArgumentException
if it's not a compatible class)
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 removed the generic casting on the SDK end, as per your comment on the corresponding SDK PR. Since we already ensure that all category class entries sent over by the extension utilize the NamedWriteable framework, these restrictions are not necessary.
public NamedWriteableRegistryParseRequest(StreamInput in) throws IOException { | ||
super(in); | ||
try { | ||
this.categoryClass = Class.forName(in.readString()); |
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.
Similar comment to above, add some type safety here if you are going to cast to a superclass on the SDK side.
|
||
@Override | ||
public String toString() { | ||
return "NamedWriteableRegistryParseRequest{" |
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 less the type(class name) and field listing it's more the delimiters. I've seen curly braces, square braces, newlines. Most IDEs will auto-generate a toString from fields. I'm wondering which particular format we are using, if any.
Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
public void getNamedWriteables() { | ||
|
||
// Retrieve named writeable registry entries from each extension | ||
for (DiscoveryNode extensionNode : extensionsList) { |
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.
You can iterate over extensionsInitializedList
here, just in case extensions failed to initialize you won't waste time attempting to getNamedWriteables()
.
…hat we are only sending extension point requests to initialized nodes Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
Gradle Check (Jenkins) Run Completed with:
|
Rebasing with feature/extensions :
Gradle Check (Jenkins) Run Completed with:
|
Gradle Check (Jenkins) Run Completed with:
|
…he extensionsInitializedList Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed 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.
Thanks @joshpalis.
I did one pass, most the code looks good, really great work!!
I've dropped a couple of questions/suggestions.
I'll do another pass.
+ "categoryClass=" | ||
+ categoryClass.getName() | ||
+ ", context length=" | ||
+ context.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.
Is context
printable?
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 context is a byte array, not sure if printing out the entire array would provide any useful information, so I decided to just print the length of the array. I am open to suggestions on how to represent the context.
@@ -1155,6 +1155,7 @@ public Node start() throws NodeValidationException { | |||
: "clusterService has a different local node than the factory provided"; | |||
transportService.acceptIncomingRequests(); | |||
extensionsOrchestrator.extensionsInitialize(); | |||
extensionsOrchestrator.getNamedWriteables(); |
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.
How are these namedWritables registered with OpenSearch NamedWritableRegistry?
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.
Currently this is a placeholder trigger. Registering extensions named writeables within the central named writeable registry is a larger problem that I have linked this issue to.
* @throws UnknownHostException if connection to the extension node failed | ||
* @return A map of category classes and their associated names and readers for this discovery node | ||
*/ | ||
public Map<DiscoveryNode, Map<Class, Map<String, ExtensionReader>>> getNamedWriteables(DiscoveryNode extensionNode) |
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 method have to be public?
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.
Currently I target this method within my unit tests since extensionsInitializedList
cannot be populated unless extensionsInitialize() occurs. The method which calls this iterates through initialized discovery nodes and sends a request to the extension associated with the node, therefore, in order to test this, I would have to call this method directly, which is only possible if it is public
/** | ||
* Iterates through all discovered extensions, sends transport requests for named writeables and consolidates all entires into a central named writeable registry for extensions. | ||
*/ | ||
public void getNamedWriteables() { |
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.
Similar comment as the SDK.
Can we fold all of this logic to Another Class under extensions package?
Merging feature/extensions
Gradle Check (Jenkins) Run Completed with:
|
Gradle Check (Jenkins) Run Completed with:
|
…extension orchestrator to new class, updated tests to reflect change, fixed toString parse request override Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed with:
|
Codecov Report
@@ Coverage Diff @@
## feature/extensions #3925 +/- ##
========================================================
- Coverage 70.66% 70.56% -0.10%
+ Complexity 57107 57079 -28
========================================================
Files 4606 4612 +6
Lines 274633 274810 +177
Branches 40210 40228 +18
========================================================
- Hits 194059 193913 -146
- Misses 64331 64629 +298
- Partials 16243 16268 +25
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
…cronous since APIs will be utlizing the extensionsInitializedList to identify the correct node to send requests to. Since arraylists are not thread safe, this is achieved by adding countdown latches Signed-off-by: Joshua Palis <jpalis@amazon.com>
Gradle Check (Jenkins) Run Completed 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.
Thanks @joshpalis for taking care of this.
This is great work!!
For next time, resolve the conversations you've taken care of.
It helps the reviewer to understand what has to be addressed.
Description
Adds support for registering named writeable entries from extensions and transporting parse requests to extensions to deserialize named writeables that they have registered.
Issues Resolved
#3495
Check List
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.