-
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
[Extensions] Add DynamicActionRegistry to ActionModule #6734
Changes from all commits
7f58a03
49515ab
0fa1666
cf40500
6edcc0b
48b0997
350b847
e5eb0d0
51ed329
3b50c87
6eec3ab
52e97b2
c6921dd
f434406
1cd8278
d9ca024
27b6521
a979767
b9afc50
46b5a83
0834375
b2fe38c
032ee0d
4a9690f
c581e55
a6f72e5
cd1630a
56e37e8
c425be4
c9c753c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -282,12 +282,12 @@ | |||||||||
import org.opensearch.common.inject.TypeLiteral; | ||||||||||
import org.opensearch.common.inject.multibindings.MapBinder; | ||||||||||
import org.opensearch.common.settings.ClusterSettings; | ||||||||||
import org.opensearch.extensions.action.ExtensionProxyAction; | ||||||||||
import org.opensearch.extensions.action.ExtensionTransportAction; | ||||||||||
import org.opensearch.common.settings.IndexScopedSettings; | ||||||||||
import org.opensearch.common.settings.Settings; | ||||||||||
import org.opensearch.common.settings.SettingsFilter; | ||||||||||
import org.opensearch.common.util.FeatureFlags; | ||||||||||
import org.opensearch.extensions.action.ExtensionProxyAction; | ||||||||||
import org.opensearch.extensions.action.ExtensionProxyTransportAction; | ||||||||||
import org.opensearch.index.seqno.RetentionLeaseActions; | ||||||||||
import org.opensearch.indices.SystemIndices; | ||||||||||
import org.opensearch.indices.breaker.CircuitBreakerService; | ||||||||||
|
@@ -448,13 +448,15 @@ | |||||||||
import java.util.List; | ||||||||||
import java.util.Map; | ||||||||||
import java.util.Set; | ||||||||||
import java.util.concurrent.ConcurrentHashMap; | ||||||||||
import java.util.function.Consumer; | ||||||||||
import java.util.function.Supplier; | ||||||||||
import java.util.function.UnaryOperator; | ||||||||||
import java.util.stream.Collectors; | ||||||||||
import java.util.stream.Stream; | ||||||||||
|
||||||||||
import static java.util.Collections.unmodifiableMap; | ||||||||||
import static java.util.Objects.requireNonNull; | ||||||||||
|
||||||||||
/** | ||||||||||
* Builds and binds the generic action map, all {@link TransportAction}s, and {@link ActionFilters}. | ||||||||||
|
@@ -471,7 +473,17 @@ public class ActionModule extends AbstractModule { | |||||||||
private final ClusterSettings clusterSettings; | ||||||||||
private final SettingsFilter settingsFilter; | ||||||||||
private final List<ActionPlugin> actionPlugins; | ||||||||||
// The unmodifiable map containing OpenSearch and Plugin actions | ||||||||||
// This is initialized at node bootstrap and contains same-JVM actions | ||||||||||
// It will be wrapped in the Dynamic Action Registry but otherwise | ||||||||||
// remains unchanged from its prior purpose, and registered actions | ||||||||||
// will remain accessible. | ||||||||||
private final Map<String, ActionHandler<?, ?>> actions; | ||||||||||
// A dynamic action registry which includes the above immutable actions | ||||||||||
// and also registers dynamic actions which may be unregistered. Usually | ||||||||||
// associated with remote action execution on extensions, possibly in | ||||||||||
// a different JVM and possibly on a different server. | ||||||||||
private final DynamicActionRegistry dynamicActionRegistry; | ||||||||||
private final ActionFilters actionFilters; | ||||||||||
private final AutoCreateIndex autoCreateIndex; | ||||||||||
private final DestructiveOperations destructiveOperations; | ||||||||||
|
@@ -502,6 +514,7 @@ public ActionModule( | |||||||||
this.threadPool = threadPool; | ||||||||||
actions = setupActions(actionPlugins); | ||||||||||
actionFilters = setupActionFilters(actionPlugins); | ||||||||||
dynamicActionRegistry = new DynamicActionRegistry(); | ||||||||||
autoCreateIndex = new AutoCreateIndex(settings, clusterSettings, indexNameExpressionResolver, systemIndices); | ||||||||||
destructiveOperations = new DestructiveOperations(settings, clusterSettings); | ||||||||||
Set<RestHeaderDefinition> headers = Stream.concat( | ||||||||||
|
@@ -711,7 +724,7 @@ public <Request extends ActionRequest, Response extends ActionResponse> void reg | |||||||||
|
||||||||||
if (FeatureFlags.isEnabled(FeatureFlags.EXTENSIONS)) { | ||||||||||
// ExtensionProxyAction | ||||||||||
actions.register(ExtensionProxyAction.INSTANCE, ExtensionTransportAction.class); | ||||||||||
actions.register(ExtensionProxyAction.INSTANCE, ExtensionProxyTransportAction.class); | ||||||||||
} | ||||||||||
|
||||||||||
// Decommission actions | ||||||||||
|
@@ -954,13 +967,86 @@ protected void configure() { | |||||||||
bind(supportAction).asEagerSingleton(); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
// register dynamic ActionType -> transportAction Map used by NodeClient | ||||||||||
bind(DynamicActionRegistry.class).toInstance(dynamicActionRegistry); | ||||||||||
} | ||||||||||
|
||||||||||
public ActionFilters getActionFilters() { | ||||||||||
return actionFilters; | ||||||||||
} | ||||||||||
|
||||||||||
public DynamicActionRegistry getDynamicActionRegistry() { | ||||||||||
return dynamicActionRegistry; | ||||||||||
} | ||||||||||
|
||||||||||
public RestController getRestController() { | ||||||||||
return restController; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* The DynamicActionRegistry maintains a registry mapping {@link ActionType} instances to {@link TransportAction} instances. | ||||||||||
* <p> | ||||||||||
* This class is modeled after {@link NamedRegistry} but provides both register and unregister capabilities. | ||||||||||
* | ||||||||||
* @opensearch.internal | ||||||||||
*/ | ||||||||||
public static class DynamicActionRegistry { | ||||||||||
// This is the unmodifiable actions map created during node bootstrap, which | ||||||||||
// will continue to link ActionType and TransportAction pairs from core and plugin | ||||||||||
// action handler registration. | ||||||||||
private Map<ActionType, TransportAction> actions = Collections.emptyMap(); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please do not use raw type for type with generic type arguments:
Suggested change
Same applies for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can change for The existing code sends to NodeClient with this line:
and the NodeClient version of the map (which is now being wrapped inside the registry) is:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or I guess I can add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that would be better, thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems to not work:
I think with type erasure we have to use raw types with the injection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :o this is very surprising, will take a look shorlty |
||||||||||
// A dynamic registry to add or remove ActionType / TransportAction pairs | ||||||||||
// at times other than node bootstrap. | ||||||||||
private final Map<ActionType<?>, TransportAction<?, ?>> registry = new ConcurrentHashMap<>(); | ||||||||||
|
||||||||||
/** | ||||||||||
* Register the immutable actions in the registry. | ||||||||||
* | ||||||||||
* @param actions The injected map of {@link ActionType} to {@link TransportAction} | ||||||||||
*/ | ||||||||||
public void registerUnmodifiableActionMap(Map<ActionType, TransportAction> actions) { | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying to understand, we could've directly passed in the map L516 when There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep but it's a different map. Given The bits we need are the ActionType key and TransportAction singleton instance value. The signature of
My original implementation kept this map injected as-is and handled the two different maps in the NodeClient. However, based on initial review it was requested I keep those details out of the client. But we still need to provide an ActionType instance key (easy) and fetch a singleton TransportAction value that we can run the We could use the class and get the right instance if we keep a copy of the injector inside the registry, and in theory that might help out with the concern over rawtypes. But that's also new code/patterns and I really tried to keep this as close to the way it already was as I could. |
||||||||||
this.actions = actions; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Add a dynamic action to the registry. | ||||||||||
* | ||||||||||
* @param action The action instance to add | ||||||||||
* @param transportAction The corresponding instance of transportAction to execute | ||||||||||
*/ | ||||||||||
public void registerDynamicAction(ActionType<?> action, TransportAction<?, ?> transportAction) { | ||||||||||
requireNonNull(action, "action is required"); | ||||||||||
requireNonNull(transportAction, "transportAction is required"); | ||||||||||
if (actions.containsKey(action) || registry.putIfAbsent(action, transportAction) != null) { | ||||||||||
throw new IllegalArgumentException("action [" + action.name() + "] already registered"); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Remove a dynamic action from the registry. | ||||||||||
* | ||||||||||
* @param action The action to remove | ||||||||||
*/ | ||||||||||
public void unregisterDynamicAction(ActionType<?> action) { | ||||||||||
requireNonNull(action, "action is required"); | ||||||||||
if (registry.remove(action) == null) { | ||||||||||
throw new IllegalArgumentException("action [" + action.name() + "] was not registered"); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Gets the {@link TransportAction} instance corresponding to the {@link ActionType} instance. | ||||||||||
* | ||||||||||
* @param action The {@link ActionType}. | ||||||||||
* @return the corresponding {@link TransportAction} if it is registered, null otherwise. | ||||||||||
*/ | ||||||||||
@SuppressWarnings("unchecked") | ||||||||||
public TransportAction<? extends ActionRequest, ? extends ActionResponse> get(ActionType<?> action) { | ||||||||||
if (actions.containsKey(action)) { | ||||||||||
return actions.get(action); | ||||||||||
} | ||||||||||
return registry.get(action); | ||||||||||
} | ||||||||||
} | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* SPDX-License-Identifier: Apache-2.0 | ||
* | ||
* The OpenSearch Contributors require contributions made to | ||
* this file be licensed under the Apache-2.0 license or a | ||
* compatible open source license. | ||
*/ | ||
|
||
package org.opensearch.extensions.action; | ||
|
||
import org.opensearch.action.ActionType; | ||
|
||
import java.util.Objects; | ||
|
||
/** | ||
* An {@link ActionType} to be used in extension action transport handling. | ||
* | ||
* @opensearch.internal | ||
*/ | ||
public class ExtensionAction extends ActionType<RemoteExtensionActionResponse> { | ||
|
||
private final String uniqueId; | ||
|
||
/** | ||
* Create an instance of this action to register in the dynamic actions map. | ||
* | ||
* @param uniqueId The uniqueId of the extension which will run this action. | ||
* @param name The fully qualified class name of the extension's action to execute. | ||
*/ | ||
public ExtensionAction(String uniqueId, String name) { | ||
super(name, RemoteExtensionActionResponse::new); | ||
this.uniqueId = uniqueId; | ||
} | ||
|
||
/** | ||
* Gets the uniqueId of the extension which will run this action. | ||
* | ||
* @return the uniqueId | ||
*/ | ||
public String uniqueId() { | ||
return this.uniqueId; | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
final int prime = 31; | ||
int result = super.hashCode(); | ||
result = prime * result + Objects.hash(uniqueId); | ||
return result; | ||
} | ||
Comment on lines
+44
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks interesting, curious who uses There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are the key to the map with the TransportAction value. Having matching hashcodes means I can (and do in this code) re-create the instance to be used as a key using just the strings. |
||
|
||
@Override | ||
public boolean equals(Object obj) { | ||
if (this == obj) return true; | ||
if (!super.equals(obj)) return false; | ||
if (getClass() != obj.getClass()) return false; | ||
ExtensionAction other = (ExtensionAction) obj; | ||
return Objects.equals(uniqueId, other.uniqueId); | ||
} | ||
} |
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 how you've separated out dynamic actions differently, what concerns do you see of modifying the existing actions ? I see we aren't using them anyway.
I can think of inconsistencies with other nodes which might not have registered actions after bootstrap.
I understand the safeguards make sense for 2.x but for 3.x Im leaning towards having one set of actions which are injected and read by the client.
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.
@saratvemulapalli "injected" only happens once, and the whole ActionType instance -> TransportAction class setup is tightly integrated. The injector already has the necessary instances. Removing them from the map wouldn't take away the singleton that is still bound to that class. In order to truly "unregister" the things loaded in during bootstrap we need to get away from injection.
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.
OK..... some follow-up comments:
client.execute()
on the NodeClient needs access to both maps. We have the choice of adding a separate map and passing both to NodeClient (my original PR before refactoring in response to @peternied comments) or combining them here inside this module. Or combining them somewhere else in a whole new module. I think here is the simplest approach, particularly since they are all "actions".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.
Makes sense thanks @dbwiddis !