BLOG

  • Top
  • Blog
  • Pluggable Custom Actions

Pluggable Custom Actions

Author: Artem Abeleshev

  

Posted: October 21, 2022

    
  • Solr

At the moment a voting is going for the next Solr 9.1.0 release. While it is not a major release, it is still going to bring up some new features for us. One of these features will be support of pluggable custom actions. By saying custom actions, I mean custom commands that can be executed against individual cores. Actually, support of the custom actions was introduced before, but it has numerous problems. To get more clear idea of those problems and how they have been solved in a new version, let's compare both ways of using custom actions.

Legacy Custom Actions

Before, support of the custom actions was based on the single method handleCustomAction within the org.apache.solr.handler.admin.CoreAdminHandler:

protected void handleCustomAction(SolrQueryRequest req, SolrQueryResponse rsp) {
    throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unsupported operation: " +
        req.getParams().get(ACTION));
}

All executed commands that do not match the name of the standard actions are redirected to this method. By default, it just throws an exception. Developers can apply custom logic by extending CoreAdminHandler and overriding handleCustomAction method. As an example, let's create some simple action:

public class CustomCoreAdminHandler extends CoreAdminHandler {

    public CustomCoreAdminHandler() {
        super();
    }

    public CustomCoreAdminHandler(final CoreContainer coreContainer) {
        super(coreContainer);
    }

    @Override
    protected void handleCustomAction(SolrQueryRequest req, SolrQueryResponse rsp) {

        String action = req.getParams().get(CoreAdminParams.ACTION).toLowerCase(Locale.ROOT);

        if (action.equals("hello")) {

            NamedList<Object> details = new SimpleOrderedMap<>();

            details.add("startTime", Instant.now().toString());

            // do some stuff

            details.add("status", "success");
            details.add("endTime", Instant.now().toString());

            rsp.addResponse(details);

        } else {
            super.handleCustomAction(req, rsp);
        }
    }
}

The code above does really simple logic. First, it extracts the name of the action to know which one has been called. In case of action of hello, the response will be populated with status, startTime and endTime fields. Nothing special. While that way of arranging custom actions looks simple and straightforward, it has a lot of drawbacks.

First, to make the whole thing work, we need to extend the CoreAdminHandler class. It feels a bit overwhelming to force a custom implementation of one of the core classes in the Solr. Even if you decide to walk this path, you still need to do some additional work: make sure Solr will use a custom version of CoreAdminHandler class instead of the standard one. It can be done by indicating adminHandler property within the solr.xml file:

<solr>
    ...
    <str name="adminHandler">com.example.handler.CustomCoreAdminHandler</str>
    ...
</solr>

Second, all custom actions are expected to be handled by the single method. No additional structures to separate action's contexts are not provided. It is up to the developer how to organize it within the code. It may not look like a huge problem, but feels like the developer was just dropped empty handed with the raw request and response objects.

Third problem is about async execution of the actions. An async execution workflow is built within CoreAdminHandler logic and all standard actions get support of async execution out of the box. Unfortunately, custom actions are not part of this workflow. Let's do some changes to our custom action we have implemented above. We will add some logic that takes a long time to complete:

public class CustomCoreAdminHandler extends CoreAdminHandler {

    public CustomCoreAdminHandler() {
        super();
    }

    public CustomCoreAdminHandler(final CoreContainer coreContainer) {
        super(coreContainer);
    }

    @Override
    protected void handleCustomAction(SolrQueryRequest req, SolrQueryResponse rsp) {

        String action = req.getParams().get(CoreAdminParams.ACTION).toLowerCase(Locale.ROOT);

        if (action.equals("hello")) {

            NamedList<Object> details = new SimpleOrderedMap<>();

            details.add("startTime", Instant.now().toString());

            // Emulate long running action
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                // Ignore
            }

            details.add("status", "success");
            details.add("endTime", Instant.now().toString());

            rsp.addResponse(details);

        } else {
            super.handleCustomAction(req, rsp);
        }
    }
}

If we run an action one more time, it will block an execution until the action is not completed. And the user doesn't have any means to force running action in an async way. In most cases it is unacceptable. As a result, developers will end up implementing its own async support, literally reinventing the wheel.

The last problem I want to mention is about compatibility. Various developers can provide users with some useful actions. Let's say, one developer created CoreAdminHandler with action A, and another developer created CoreAdminHandler with action B. If the user wants to use both actions A and B, the user will end up with a dilemma, as the user can override only one handler, i.e no handler chaining is supported.

Looking at all the problems above, we can say that, while it is possible to implement custom actions in a legacy way, they are not very user and developer friendly. We need a more elegant and reliable solution with a more pluggable nature. This is exactly what pluggable custom actions are about.

Pluggable Custom Actions

As a name stands, these actions have a pluggable nature. Developer doesn't need to extend CoreAdminHandler. Instead, the developer creates the action itself and the user registers it. Each custom action is an isolated class that implements org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminOp interface. Pluggable actions are handled by CoreAdminHandler exactly the same way as it handles standard actions, including async workflow. Let's create exactly the same action as before, but using pluggable action:

public class HelloAction implements CoreAdminOp {

    @Override
    public void execute(CallInfo it) throws Exception {

        NamedList<Object> details = new SimpleOrderedMap<>();

        details.add("startTime", Instant.now().toString());

        // do some stuff

        details.add("status", "success");
        details.add("endTime", Instant.now().toString());

        it.rsp.addResponse(details);
    }
}

Now it looks much better and feels more general. Note, that we don't need to care about the name of the action anymore. CoreAdminHandler will take care of it. It will check the name of the action and will try to find the corresponding action class to call. But how does Solr know the name of the custom action and which custom action class to call? To make it work, we need to register our custom action first. It is done via sorl.xml file:

<solr>
    ...
    <coreAdminHandlerActions>
        <str name="hello">com.example.action.HelloAction</str>
    </coreAdminHandlerActions>
    ...
</solr>

Each action is registered using a unique name. You can register multiple actions by adding additional entries to the coreAdminHandlerActions block:

<solr>
    ...
    <coreAdminHandlerActions>
        <str name="hello">com.example.action.HelloAction</str>
        <str name="foo">com.example.action.FooAction</str>
        <str name="bar">com.example.action.BarAction</str>
    </coreAdminHandlerActions>
    ...
</solr>

After that, we can run custom action using its name by executing the following query:

http://localhost:8983/solr/admin/cores?action=hello

Here is an example of the response:

{
    "responseHeader": {
        "status": 0,
        "QTime": 8023
    },
    "response": {
        "startTime": "2022-19-10T03:35:57.365294500Z",
        "status": "success",
        "endTime": "2022-19-10T03:35:57.365547900Z"
    }
}

To call our action in an async way we need to add async parameter to the request:

http://localhost:8983/solr/admin/cores?action=hello&async=1000

In that case we will got a simple response, indicating that action has been scheduled successfully:

{
    "responseHeader": {
        "status": 0,
        "QTime": 4020
    }
}

Later, we can check the status of the scheduled action by calling requeststatus and passing async value from the previous request:

http://localhost:8983/solr/admin/cores?action=requeststatus&requestid=1000

In case of action is already finished, response will contain an action details:

{
    "responseHeader": {
        "status": 0,
        "QTime": 2308
    },
    "STATUS": "completed",
    "msg": "TaskId: 1000 webapp=null path=/admin/cores params={async=1000&action=hello} status=0 QTime=4020",
    "response": {
        "startTime": "2022-19-10T03:36:21.910596800Z",
        "status": "success",
        "endTime": "2022-19-10T03:36:21.910596800Z"
    }
}

Summary

Pluggable custom actions is a preferred way to implement custom actions. It has a lot of advantages in comparison to the legacy way. It gives freedom to the developers to implement and support custom logic that is not tightly coupled and dependent on the main Solr codebase. While users can register only the actions they really need. Also, action names are not hardcoded, it is a user who decides on the action names. That way, no action name overlapping occured, the user builds its own actions namespace. Pluggable custom actions support can trigger growth of the developers community for building external tools. Next step will be to add support for the pluggable custom collection actions.

For estimates and details,
please feel free to contact our development team.

Contact Us
TOP