現在Solr 9.1.0
の次期リリースに関する票決が進められている。これはメジャーリリースではないものの新機能がいくつか登場することを示す。それらの機能の1つがプラグイン対応カスタムアクションのサポートだ。カスタムアクションは個々のコアに対して実行されるカスタムコマンドを意味する。実際、カスタムアクションは以前にもサポートされたことがあったものの多数の問題があった。これらの問題と、それらが新バージョンでどのように解決されたのかを明確に理解するために、両方のカスタムアクションの使い方を見ていこう。
レガシーカスタムアクション
これまでカスタムアクションのサポートは「org.apache.solr.handler.admin.CoreAdminHandler」の中の「handleCustomAction」シングルメソッドがベースだった。
protected void handleCustomAction(SolrQueryRequest req, SolrQueryResponse rsp) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unsupported operation: " +
req.getParams().get(ACTION));
}
標準アクションの名前に適合しない実行コマンドはすべてこのメソッドにリダイレクトされる。デフォルトでは例外になるだけだ。開発者は「CoreAdminHandler」を拡張し、「handleCustomAction」メソッドをオーバーライドすることでカスタムロジックを適用できる。一例として、シンプルなアクションを作成してみよう。
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(ACTION).toLowerCase(Locale.ROOT);
if(action.equals("hello")) {
NamedList<Object> details = new SimpleOrderedMap<>();
details.add("startTime", Instant.now().toString());
// 何らかの処理
details.add("status", "success");
details.add("endTime", Instant.now().toString());
rsp.addResponse(details);
} else {
super.handleCustomAction(req, rsp);
}
}
}
上のコードは非常にシンプルなロジックを実行する。まず、アクションの名前を取り出して、どれがコールされたかを知る。アクションが「hello」の場合は、レスポンスに「status」、「startTime」、そして 「endTime」の各フィールドを入れる。特別なことは何もない。ただ、このようなカスタムアクションの配列方法はシンプルでわかりやすいものの欠点も多く存在する。
まず、全てが機能するようにするには、「CoreAdminHandler」クラスを拡張する必要がある。Solrのコアクラスの1つについて無理やりカスタムインプリメンテーションを行うことは手に負えない負担のように思える。たとえこのような手法を取ることにしたとしても、さらに新しい作業がいくつか必要になってくる。Solrが標準のものではなく、必ずカスタムバージョンの「CoreAdminHandler」クラスを使うようにする。これは、「solr.xml」ファイルで「adminHandler」プロパティを示すことで可能になる。
<solr>
...
<str name="adminHandler">com.example.handler.CustomCoreAdminHandler</str>
...
<solr>
2番目に、カスタムアクションは全て当然シングルメソッドで処理されると思われている。アクションのコンテキストを区別する構造が別途提供されることはない。コード内でそれを整理するのは開発者次第なのだ。これは大きな問題だとは思えないかもしれないが、開発者としては未加工の「リクエスト」と「レスポンス」のオブジェクトだけで放り出されたような気持ちになる。
3番目の問題はアクションの非同期実行だ。非同期実行ワークフローは「CoreAdminHandler」ロジックの中で構築され、標準アクションは全て最初から非同期実行がサポートされる。ただ残念ながら、カスタムアクションはこのワークフローには含まれないので、上でインプリメントしたカスタムアクションに少しだけ変更を加える。ここでは完了するまでに時間のかかるロジックを加えることにする。
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(ACTION).toLowerCase(Locale.ROOT);
if(action.equals("hello")) {
NamedList<Object> details = new SimpleOrderedMap<>();
details.add("startTime", Instant.now().toString());
// 長いアクションをエミュレート
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// 無視
}
details.add("status", "success");
details.add("endTime", Instant.now().toString());
rsp.addResponse(details);
} else {
super.handleCustomAction(req, rsp);
}
}
}
ここでもう一度アクションを実行すると、アクションが完了しなくなるまで実行がブロックされる。そして、ユーザにはアクションを無理に非同期で実行させることができなくなる。多くの場合、これは受け入れられないことである。その結果、開発者は最終的には自分たち独自の非同期サポートをインプリメントし、文字通りわざわざ一からやり直すことになる。
ここで言及しておきたい最後の問題は互換性だ。さまざまな開発者がユーザに便利なアクションを提供することができる。たとえば、ある開発者がアクション「A」を持つ「CoreAdminHandler」を作成し、別の開発者がアクション「B」を持つ「CoreAdminHandler」を作成したとする。もしユーザがアクション「A」と「B」の両方を使いたい場合、ハンドラチェイニングがサポートされていないためユーザは1つのハンドラしかオーバーライドできず、ジレンマに陥ってしまうのだ。
上記のような問題全てを考えると、レガシー手法でカスタムアクションを実装することは可能なものの、それではユーザにも開発者にもフレンドリーではなくなってしまう。われわれにはもっとエレガントで信頼性が高く、プラグイン対応特性の高いソリューションが必要なのだ。これこそまさにプラグイン対応カスタムアクションの目的だ。
プラグイン対応カスタムアクション
その名の通り、これらのアクションはプラグイン対応という性質を持つ。開発者は「CoreAdminHandler」を拡張する必要がない。その代わりに、開発者はアクションそのものを作成し、ユーザはそれを登録するのだ。各カスタムアクションは隔離されたクラスで、「org.apache.solr.handler.admin.CoreAdminHandler.CoreAdminOp」インターフェースをインプリメントしている。プラグイン対応アクションは非同期ワークフローを含め標準アクションと全く同じように「CoreAdminHandler」で処理される。ここで前と全く同じアクションをプラグイン対応アクションを使って作成してみよう。
public class HelloAction implements CoreAdminOp {
@Override
public void execute(CallInfo it) throws Exception {
NamedList<Object> details = new SimpleOrderedMap<>();
details.add("startTime", Instant.now().toString());
// 何らかの処理
details.add("status", "success");
details.add("endTime", Instant.now().toString());
it.rsp.addResponse(details);
}
}
これで格段に優れ、かつ一般的な感じになった。これでアクションの名前はもう気にする必要がなくなったのだ。これからは「CoreAdminHandler」が処理してくれる。これがアクションの名前をチェックし、コールする対応アクションクラスを探してくれる。しかし、Solrはどのようにカスタムアクションの名前や、コールするカスタムアクションクラスを知るのだろうか?これをうまく処理するためには、自分たちのカスタムアクションを最初に登録する必要がある。これは「sorl.xml」ファイルで行う。
<solr>
...
<coreAdminHandlerActions>
<str name="hello">com.example.action.HelloAction</str>
</coreAdminHandlerActions>
...
<solr>
各アクションはユニークな名前で登録される。「coreAdminHandlerActions」ブロックに新しいエントリーを追加することで複数のアクションも登録できる。
<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>
こうすれば、名前を使って以下のクエリでカスタムアクションを実行することができる。
http://localhost:8983/solr/admin/cores?action=hello
レスポンスの一例は以下のようになる。
{
"responseHeader": {
"status": 0,
"QTime": 8023
},
"response": {
"startTime": "2022-19-10T03:35:57.365294500Z",
"status": "success",
"endTime": "2022-19-10T03:35:57.365547900Z"
}
}
非同期手法でアクションをコールするには、リクエストに「async」パラメータを追加する必要がある。
http://localhost:8983/solr/admin/cores?action=hello&async=1000
このような場合、レスポンスはシンプルなものとなり、アクションがうまくスケジューリングされたことが示される。
{
"responseHeader": {
"status": 0,
"QTime": 4020
}
}
スケジューリングされたアクションのステータスは、「requeststatus」をコールし、非同期の値を先のリクエストから渡すことによって後でチェックできる。
http://localhost:8983/solr/admin/cores?action=requeststatus&requestid=1000
アクションが既に終了している場合、レスポンスにはアクションの詳細が入りる。
{
"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"
}
}
まとめ
カスタムアクションのインプリメントで好まれる手法はプラグイン対応カスタムアクションだ。これには従来の方法と比較して多くの利点がある。メインとなるSolrのコードベースと密接には結び付かず、依存もしないカスタムロジックの実装とサポートにおいて開発者の自由度が高まる。ユーザーは本当に必要なアクションだけ登録することができる。また、アクションの名前はハードコーディングされておらず、ユーザがアクションの名前を決めることができる。こうすることで、アクション名の重複が発生することはなく、ユーザは自分だけのアクション名前空間を作成できる。プラグイン対応カスタムアクションのサポートは外部ツールを構築する開発者コミュニティーの成長を促すことになるかもしれない。次のステップはプラグイン対応カスタムコレクションアクションのサポート追加になるだろう。