Elasticsearch Rails 7とnet-http-persistentを同時に使うときにはFaradayアダプターを指定する


## Railsアプリケーションのgemのバージョンを上げたときエラーが起きた

Railsアプリケーションのgemのバージョンを`bundle update`コマンドで上げていました。
`bin/rails s`コマンドでRailsアプリケーションを起動するときに次のようなエラーがでるようになりました。

```sh
ledsun@MSI:~/pubdictionaries►bin/rails s
=> Booting WEBrick
=> Rails 7.0.8 application starting in development http://localhost:3000
=> Run `bin/rails server --help` for more startup options
Exiting
/home/ledsun/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/faraday-2.9.0/lib/faraday/middleware_registry.rb:57:in `lookup_middleware': :net_http_persistent is not registered on Faraday::Adapter (Faraday::Error)
        from /home/ledsun/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/faraday-2.9.0/lib/faraday/rack_builder.rb:112:in `adapter'
        from /home/ledsun/.rbenv/versions/3.2.1/lib/ruby/3.2.0/forwardable.rb:240:in `adapter'
        from /home/ledsun/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/elasticsearch-transport-7.17.10/lib/elasticsearch/transport/client.rb:177:in `block in initialize'
```


## 結論 対応方法

先に結論を書きます。
[Elasticsearch Rails 7](https://github.com/elastic/elasticsearch-ruby) と [net-http-persistent](https://github.com/drbrain/net-http-persistent) を同時に使うときにはFaradayアダプターを明示的に指定します。

```ruby:config/initializers/elasticsearch.rb
Elasticsearch::Model.client = Elasticsearch::Client.new adapter: 'Faraday::Adapter::NetHttp'
```

上記のエラーを解消出来ます。
以下は、対応方法を特定するまでの記録です。

## 対応方法を決める


### エラー発生箇所を探す

エラーを読んでみましょう。

> :net_http_persistent is not registered on Faraday::Adapter (Faraday::Error)


Faradayに関わるエラーのようです。
もう少し読んでみましょう。

> elasticsearch-transport-7.17.10/lib/elasticsearch/transport/client.rb:177

原因はここにありそうです。
ソースコードを見てみましょう。

[https://github.com/elastic/elasticsearch-ruby/blob/7.17/elasticsearch-transport/lib/elasticsearch/transport/client.rb#L174-L179](https://github.com/elastic/elasticsearch-ruby/blob/7.17/elasticsearch-transport/lib/elasticsearch/transport/client.rb#L174-L179)

```ruby
 @arguments[:adapter] ||= __auto_detect_adapter
 set_meta_header # from include MetaHeader
 @transport_class.new(hosts: @seeds, options: @arguments) do |faraday|
   faraday.adapter(@arguments[:adapter])
   block&.call faraday
 end
```

ちょっとよくわからないです。
エラーメッセージ中の文言`:net_http_persistent`で検索してみます。

[https://github.com/elastic/elasticsearch-ruby/blob/7.17/elasticsearch-transport/lib/elasticsearch/transport/client.rb#L362-L375](https://github.com/elastic/elasticsearch-ruby/blob/7.17/elasticsearch-transport/lib/elasticsearch/transport/client.rb#L362-L375)

```ruby
  def __auto_detect_adapter
    case
    when defined?(::Patron)
      :patron
    when defined?(::Typhoeus)
      :typhoeus
    when defined?(::HTTPClient)
      :httpclient
    when defined?(::Net::HTTP::Persistent)
      :net_http_persistent
    else
      ::Faraday.default_adapter
    end
  end
```

とても怪しいソースコードが見つかりました。

```ruby
when defined?(::Net::HTTP::Persistent)
  :net_http_persistent
```

を通っていそうです。
ところで`__auto_detect_adapter`は使われているのでしょか？
先ほど引用したソースコードに

```ruby
@arguments[:adapter] ||= __auto_detect_adapter
```

が、ありました。
めちゃくちゃこいつです。

### エラーの原因を特定する

ではなぜ？エラーがでるようになったのでしょうか？
`bundle update`コマンドを実行したので、Gem関連で何かあるはずです。
`Gemfile.lock`のdiffを眺めてみます。

```diff
-    elasticsearch-transport (7.17.7)
-      faraday (~> 1)
+    elasticsearch-transport (7.17.10)
+      faraday (>= 1, < 3)
```

`elasticsearch-transport` が要求する `faraday` のバージョンが変わっています。
さらに見ていくと、実際にインストールされるfaradayのバージョンも変わっています。

```diff
-    faraday (1.10.3)
-      faraday-em_http (~> 1.0)
-      faraday-em_synchrony (~> 1.0)
-      faraday-excon (~> 1.1)
-      faraday-httpclient (~> 1.0)
-      faraday-multipart (~> 1.0)
-      faraday-net_http (~> 1.0)
-      faraday-net_http_persistent (~> 1.0)
-      faraday-patron (~> 1.0)
-      faraday-rack (~> 1.0)
-      faraday-retry (~> 1.0)
-      ruby2_keywords (>= 0.0.4)
-    faraday-em_http (1.0.0)
-    faraday-em_synchrony (1.0.0)
-    faraday-excon (1.1.0)
-    faraday-httpclient (1.0.1)
-    faraday-multipart (1.0.4)
-      multipart-post (~> 2)
-    faraday-net_http (1.0.1)
-    faraday-net_http_persistent (1.2.0)
-    faraday-patron (1.0.0)
-    faraday-rack (1.0.0)
-    faraday-retry (1.0.3)
+    faraday (2.9.0)
+      faraday-net_http (>= 2.0, < 3.2)
+    faraday-net_http (3.1.0)
+      net-http
```


というか`faraday-net_http_persistent`がなくなっています。

```diff
-      faraday-net_http_persistent (~> 1.0)
```

エラーメッセージには

> :net_http_persistent is not registered on Faraday::Adapter (Faraday::Error)

とありました。
`faraday-net_http_persistent` gemがインストールされなくなったことが、エラーの原因のようです。
エラーの原因がわかりました。
次に、どのように対応するのがいいのか考えます。

### なぜ、`faraday-net_http_persistent` gemはインストールされなくなったのか？

[Faraday 2からアダプターの扱いが変わった (Faraday::Adapter::Test::Stubs::NotFound) #Ruby - Qiita](https://qiita.com/kyntk/items/2a27172c0fc74939f628)

> アダプターがFaradayのcoreから外れたので、別途gemのインストールが必要

Faraday 2から、アダプター関連のGEMが自動的にインストールされなくなったようです。
この変更の結果が`Gemfile.lock`のdiffにも現れています。

```diff:Gemfile.lock
-      faraday-em_http (~> 1.0)
-      faraday-em_synchrony (~> 1.0)
-      faraday-excon (~> 1.1)
-      faraday-httpclient (~> 1.0)
-      faraday-multipart (~> 1.0)
-      faraday-net_http (~> 1.0)
-      faraday-net_http_persistent (~> 1.0)
-      faraday-patron (~> 1.0)
-      faraday-rack (~> 1.0)
-      faraday-retry (~> 1.0)
```

Faradayのバージョンを1に下げたら良いのでしょうか？
古いバージョンで固定すると将来のアップデートが大変になりそうです。
もう少し考えてみましょう。

### なぜ？`elasticsearch-transport`は`faraday-net_http_persistent`を使おうとするのか？

`elasticsearch-transpor`は`__auto_detect_adapter`メソッドで、使用するFaradayアダプターを選択していました。
`__auto_detect_adapter`で`:net_http_persistent`が選ばれる条件を見てみましょう。


```ruby
when defined?(::Net::HTTP::Persistent)
  :net_http_persistent
```

`::Net::HTTP::Persistent`定数が定義されているか判定しています。
`::Net::HTTP::Persistent`定数は`net_http_persistent` gemで定義される定数です。
Gemfileを確認してみましょう。

```ruby:Gemfile
gem 'net-http-persistent'
```

確かに`net_http_persistent` gemを使っています。
`net_http_persistent` gemをアンインストールすればいいのでしょうか？

アプリケーションのソースコードを確認してみます。

```ruby
@analyzer = {
  uri: analyzer_url,
  http: Net::HTTP::Persistent.new,
  post: Net::HTTP::Post.new(analyzer_url.request_uri, 'Content-Type' => 'application/json')
}
```

実際に使用しています。
`net_http_persistent` gemをアンインストールするのは良くなさそうです。

### `elasticsearch-transport`の判定がおかしいのでは？

Faradayアダプターの有無を判定したいのであれば、`::Net::HTTP::Persistent`定数より、Faradayアダプターを表す定数で判定すべきではないでしょうか？
例えば [https://github.com/lostisland/faraday-net_http_persistent/blob/10c5b8c30302d13c5217ee1d2c520ca19e699d7f/lib/faraday/net_http_persistent.rb#L16](https://github.com/lostisland/faraday-net_http_persistent/blob/10c5b8c30302d13c5217ee1d2c520ca19e699d7f/lib/faraday/net_http_persistent.rb#L16)を見ると

```ruby
Faraday::Adapter.register_middleware(net_http_persistent: Faraday::Adapter::NetHttpPersistent)
```

`Faraday::Adapter::NetHttpPersistent`定数があります。
`Faraday::Adapter::NetHttpPersistent`で判定すべきじゃないでしょうか？

今回使用している`elasticsearch-transport`は7です。
実は`elasticsearch-transport`には8があります。
最新のソースコードを見てみましょう。

[https://github.com/elastic/elastic-transport-ruby/blob/2e37d308c22fb053f88997407aec419808ceade7/lib/elastic/transport/client.rb#L315-L328](https://github.com/elastic/elastic-transport-ruby/blob/2e37d308c22fb053f88997407aec419808ceade7/lib/elastic/transport/client.rb#L315-L328)


```ruby
  def __auto_detect_adapter
    if Gem::Version.new(Faraday::VERSION) >= Gem::Version.new(2)
      return :patron if defined?(Faraday::Adapter::Patron)
      return :typhoeus if defined?(Faraday::Adapter::Typhoeus)
      return :httpclient if defined?(Faraday::Adapter::HTTPClient)
      return :net_http_persistent if defined?(Faraday::Adapter::NetHttpPersistent)
    else
      return :patron if defined?(::Patron)
      return :typhoeus if defined?(::Typhoeus)
      return :httpclient if defined?(::HTTPClient)
      return :net_http_persistent if defined?(::Net::HTTP::Persistent)
    end
    ::Faraday.default_adapter
  end
```

Faradayのバージョンが2かそれ以外かで判定方法をわけています。

`elasticsearch-transport`のバージョンを上げたらいいのでしょうか？
`elasticsearch-transport`のバージョンは、Elasticsearchのバージョンに対応しています。
`elasticsearch-transport`のバージョンを上げるには、Elasticsearchのバージョンも上げる必要があります。
一度に変更する量を減らしたいので、`elasticsearch-transport`のバージョンを上げるのも良くなさそうです。

### Faradayアダプターを指定する

`elasticsearch-transport`のソースコードから`__auto_detect_adapter`メソッドを呼んでいる箇所を確認します。

```ruby
@arguments[:adapter] ||= __auto_detect_adapter
```

`__auto_detect_adapter`を呼ぶ前に`@arguments[:adapter]`をチェックしています。
`@argument`という名前は引数を表しているように思えます。
と言うことは、外部から使用するFaradayアダプターを指定出来そうです。

アプリケーション中で`elasticsearch-trasport`を初期化している箇所をさがします。

```ruby:config/initializers/elasticsearch.rb
Elasticsearch::Model.client = Elasticsearch::Client.new
```

があります。
ここで、`adapter`引数に`'default_adapter'`を指定します。

```ruby:config/initializers/elasticsearch.rb
Elasticsearch::Model.client = Elasticsearch::Client.new adapter: 'Faraday::Adapter::NetHttp'
```

動作確認してみましょう。

```sh
ledsun@MSI:~/pubdictionaries►bin/rails s
=> Booting WEBrick
=> Rails 7.0.8 application starting in development http://localhost:3000
=> Run `bin/rails server --help` for more startup options
[2024-02-16 11:11:38] INFO  WEBrick 1.8.1
[2024-02-16 11:11:38] INFO  ruby 3.2.1 (2023-02-08) [x86_64-linux]
[2024-02-16 11:11:38] INFO  WEBrick::HTTPServer#start: pid=2424 port=3000
```

無事に起動できました。

## 別解

Gemfileに`faraday-net_http_persistent`を追加する方法はあると思います。
使用するFaradayアダプターが変わるので、アプリケーションの動作に影響が出る可能性はあります。
`::Net::HTTP`と`::Net::HTTP::Persisitent`くらいの差であれば、影響は小さそうです。