elastomer-client 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -414,6 +414,13 @@ module ElastomerClient
414
414
 
415
415
  if response.body.is_a?(Hash) && (error = response.body["error"])
416
416
  root_cause = Array(error["root_cause"]).first || error
417
+ # ES9 returns these timeouts as 429 rather than retryable 5xx ServerError responses.
418
+ # Retry only these timeout types so unrelated 429s retain their existing handling.
419
+ if response.status == 429 &&
420
+ %w[timeout_exception process_cluster_event_timeout_exception search_timeout_exception].include?(root_cause["type"])
421
+ raise TimeoutError, response
422
+ end
423
+
417
424
  case root_cause["type"]
418
425
  when "index_not_found_exception"; raise IndexNotFoundError, response
419
426
  when "illegal_argument_exception"; raise IllegalArgument, response
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ElastomerClient
4
- VERSION = "6.3.0"
4
+ VERSION = "6.4.0"
5
5
 
6
6
  def self.version
7
7
  VERSION
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rubygems/version"
4
+
3
5
  module ElastomerClient
4
6
  class VersionSupport
5
7
 
@@ -9,7 +11,8 @@ module ElastomerClient
9
11
  #
10
12
  # Raises ArgumentError if version is unsupported.
11
13
  def initialize(version)
12
- if version < "5.0" || version >= "9.0"
14
+ comparable_version = Gem::Version.new(version)
15
+ if comparable_version < Gem::Version.new("5.0") || comparable_version >= Gem::Version.new("10.0")
13
16
  raise ArgumentError, "Elasticsearch version #{version} is not supported by elastomer-client"
14
17
  end
15
18
 
@@ -20,5 +23,9 @@ module ElastomerClient
20
23
  def es_version_8_plus?
21
24
  version >= "8.0.0"
22
25
  end
26
+
27
+ def es_version_9_plus?
28
+ Gem::Version.new(version) >= Gem::Version.new("9.0")
29
+ end
23
30
  end
24
31
  end
@@ -62,6 +62,9 @@ class RestApiSpecGenerator
62
62
  hash = MultiJson.load(File.read(filename))
63
63
  key = hash.keys.first
64
64
  value = hash.values.first
65
+ # ES9 exposes legacy knn_search only in ES8 compatibility mode, not native ES9.
66
+ next if @version_support.es_version_9_plus? && key == "knn_search"
67
+
65
68
  yield(key, value)
66
69
  end
67
70
  end
@@ -93,12 +93,13 @@ describe ElastomerClient::Client::NativeDeleteByQuery do
93
93
  _routing: { required: true }
94
94
  })
95
95
  })
96
- wait_for_index(@index.name)
96
+ wait_for_index(index.name)
97
97
  docs = index.docs(type)
98
+ routing, other_routing = routing_values_for_distinct_shards(docs)
98
99
 
99
- docs.index({ _id: 0, _routing: "cat", name: "mittens" })
100
- docs.index({ _id: 1, _routing: "cat", name: "luna" })
101
- docs.index({ _id: 2, _routing: "dog", name: "mittens" })
100
+ docs.index({ _id: 0, _routing: routing, name: "mittens" })
101
+ docs.index({ _id: 1, _routing: routing, name: "luna" })
102
+ docs.index({ _id: 2, _routing: other_routing, name: "mittens" })
102
103
 
103
104
  query = {
104
105
  query: {
@@ -109,15 +110,15 @@ describe ElastomerClient::Client::NativeDeleteByQuery do
109
110
  }
110
111
 
111
112
  index.refresh
112
- response = index.native_delete_by_query(query, routing: "cat")
113
+ response = index.native_delete_by_query(query, routing:)
113
114
 
114
115
  assert_equal(1, response["deleted"])
115
116
 
116
117
  response = docs.multi_get({
117
118
  docs: [
118
- { _id: 0, routing: "cat" },
119
- { _id: 1, routing: "cat" },
120
- { _id: 2, routing: "dog" },
119
+ { _id: 0, routing: },
120
+ { _id: 1, routing: },
121
+ { _id: 2, routing: other_routing },
121
122
  ]
122
123
  })
123
124
 
@@ -54,4 +54,36 @@ describe ElastomerClient::Client::RestApiSpec::ApiSpec do
54
54
  @api_spec.validate_params!(api: "search", params:)
55
55
  }
56
56
  end
57
+
58
+ it "selects the REST API spec for each supported Elasticsearch major version" do
59
+ {"8.18.0" => "8_18", "8.19.3" => "8_19", "9.5.6" => "9_5"}.each do |version, class_version|
60
+ client = ElastomerClient::Client.new(es_version: version)
61
+
62
+ assert_equal "ElastomerClient::Client::RestApiSpec::ApiSpecV#{class_version}", client.api_spec.class.name
63
+ end
64
+ end
65
+
66
+ it "keeps removed local parameters in ES8 but excludes them from ES9" do
67
+ eight = ElastomerClient::Client::RestApiSpec.api_spec("8.19.3")
68
+ nine = ElastomerClient::Client::RestApiSpec.api_spec("9.5.6")
69
+
70
+ %w[indices.get_alias indices.exists_alias cat.aliases indices.get_field_mapping].each do |api|
71
+ assert eight.valid_param?(api:, param: :local)
72
+ refute nine.valid_param?(api:, param: :local)
73
+ assert_empty(nine.select_params(api:, from: {local: true}))
74
+ assert_raises(ElastomerClient::Client::IllegalArgument) {
75
+ nine.validate_params!(api:, params: {local: true})
76
+ }
77
+ end
78
+ end
79
+
80
+ it "keeps removed endpoints in ES8 but excludes them from ES9" do
81
+ eight = ElastomerClient::Client::RestApiSpec.api_spec("8.19.3")
82
+ nine = ElastomerClient::Client::RestApiSpec.api_spec("9.5.6")
83
+
84
+ %w[indices.unfreeze knn_search].each do |api|
85
+ refute_nil eight.get(api)
86
+ assert_nil nine.get(api)
87
+ end
88
+ end
57
89
  end
@@ -96,12 +96,13 @@ describe ElastomerClient::Client::UpdateByQuery do
96
96
  _routing: { required: true }
97
97
  })
98
98
  })
99
- wait_for_index(@index.name)
99
+ wait_for_index(index.name)
100
100
  docs = index.docs(type)
101
+ routing, other_routing = routing_values_for_distinct_shards(docs)
101
102
 
102
- docs.index({ _id: 0, _routing: "cat", name: "mittens" })
103
- docs.index({ _id: 1, _routing: "cat", name: "luna" })
104
- docs.index({ _id: 2, _routing: "dog", name: "mittens" })
103
+ docs.index({ _id: 0, _routing: routing, name: "mittens" })
104
+ docs.index({ _id: 1, _routing: routing, name: "luna" })
105
+ docs.index({ _id: 2, _routing: other_routing, name: "mittens" })
105
106
 
106
107
  query = {
107
108
  query: {
@@ -115,15 +116,15 @@ describe ElastomerClient::Client::UpdateByQuery do
115
116
  }
116
117
 
117
118
  index.refresh
118
- response = index.update_by_query(query, routing: "cat")
119
+ response = index.update_by_query(query, routing:)
119
120
 
120
121
  assert_equal(1, response["updated"])
121
122
 
122
123
  response = docs.multi_get({
123
124
  docs: [
124
- { _id: 0, routing: "cat" },
125
- { _id: 1, routing: "cat" },
126
- { _id: 2, routing: "dog" },
125
+ { _id: 0, routing: },
126
+ { _id: 1, routing: },
127
+ { _id: 2, routing: other_routing },
127
128
  ]
128
129
  })
129
130
 
data/test/client_test.rb CHANGED
@@ -85,6 +85,29 @@ describe ElastomerClient::Client do
85
85
  assert_equal "it took too long :: GET /_cat/indices", wrapped.message
86
86
  end
87
87
 
88
+ it "keeps Elasticsearch timeout errors retryable when ES9 returns 429" do
89
+ %w[timeout_exception process_cluster_event_timeout_exception search_timeout_exception].each do |type|
90
+ error = {"type" => type}
91
+ [error, {"type" => "search_phase_execution_exception", "root_cause" => [error]}].each do |body|
92
+ response = Faraday::Response.new(status: 429, body: {"error" => body})
93
+ exception = assert_raises(ElastomerClient::Client::TimeoutError) { $client.handle_errors(response) }
94
+ assert_predicate exception, :retry?
95
+ end
96
+ end
97
+ end
98
+
99
+ it "does not make other 429 errors retryable" do
100
+ response = Faraday::Response.new(status: 429, body: {"error" => {"type" => "circuit_breaking_exception"}})
101
+ exception = assert_raises(ElastomerClient::Client::RequestError) { $client.handle_errors(response) }
102
+ refute_predicate exception, :retry?
103
+ end
104
+
105
+ it "preserves ES8 server timeout error handling" do
106
+ response = Faraday::Response.new(status: 503, body: {"error" => {"type" => "timeout_exception"}})
107
+ exception = assert_raises(ElastomerClient::Client::ServerError) { $client.handle_errors(response) }
108
+ assert_predicate exception, :retry?
109
+ end
110
+
88
111
  it "handles path expansions" do
89
112
  uri = $client.expand_path "/{foo}/{bar}", foo: "_cluster", bar: "health"
90
113
 
data/test/test_helper.rb CHANGED
@@ -110,6 +110,18 @@ def default_index_settings
110
110
  {settings: {index: {number_of_shards: 1, number_of_replicas: 0}}}
111
111
  end
112
112
 
113
+ def routing_values_for_distinct_shards(docs)
114
+ routing_by_shard = {}
115
+ 100.times do
116
+ routing = SecureRandom.uuid
117
+ shard = docs.search_shards(routing:).fetch("shards").fetch(0).fetch(0).fetch("shard")
118
+ routing_by_shard[shard] ||= routing
119
+ return routing_by_shard.values if routing_by_shard.size == 2
120
+ end
121
+
122
+ flunk "Could not find routing values for distinct shards after 100 UUID attempts"
123
+ end
124
+
113
125
  def run_snapshot_tests?
114
126
  unless defined? $run_snapshot_tests
115
127
  begin
@@ -4,9 +4,10 @@ require_relative "test_helper"
4
4
 
5
5
  describe ElastomerClient::VersionSupport do
6
6
  describe "supported versions" do
7
- it "allows 5.0.0 to 8.x" do
7
+ it "allows 5.0.0 to 9.x" do
8
8
  five_series = ["5.0.0", "5.0.9", "5.1.0", "5.9.0", "5.99.100"]
9
9
  eight_series = ["8.0.0", "8.0.9", "8.1.0", "8.9.0", "8.99.100"]
10
+ nine_series = ["9.0.0", "9.0.9", "9.1.0", "9.9.0", "9.99.100"]
10
11
 
11
12
  five_series.each do |version|
12
13
  assert ElastomerClient::VersionSupport.new(version)
@@ -14,6 +15,12 @@ describe ElastomerClient::VersionSupport do
14
15
 
15
16
  eight_series.each do |version|
16
17
  assert_predicate ElastomerClient::VersionSupport.new(version), :es_version_8_plus?
18
+ refute_predicate ElastomerClient::VersionSupport.new(version), :es_version_9_plus?
19
+ end
20
+
21
+ nine_series.each do |version|
22
+ assert_predicate ElastomerClient::VersionSupport.new(version), :es_version_8_plus?
23
+ assert_predicate ElastomerClient::VersionSupport.new(version), :es_version_9_plus?
17
24
  end
18
25
  end
19
26
  end
@@ -21,7 +28,7 @@ describe ElastomerClient::VersionSupport do
21
28
  describe "unsupported versions" do
22
29
  it "blow up" do
23
30
  too_low = ["0.90", "1.0.1", "2.0.0", "2.2.0"]
24
- too_high = ["9.0.0"]
31
+ too_high = ["10.0.0", "11.0.0", "20.0.0"]
25
32
 
26
33
  (too_low + too_high).each do |version|
27
34
  exception = assert_raises(ArgumentError, "expected #{version} to not be supported") do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elastomer-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.3.0
4
+ version: 6.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Pease
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-06-25 00:00:00.000000000 Z
12
+ date: 2026-11-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: addressable
@@ -151,6 +151,7 @@ files:
151
151
  - lib/elastomer_client/client/rest_api_spec/api_spec_v8_18.rb
152
152
  - lib/elastomer_client/client/rest_api_spec/api_spec_v8_19.rb
153
153
  - lib/elastomer_client/client/rest_api_spec/api_spec_v8_7.rb
154
+ - lib/elastomer_client/client/rest_api_spec/api_spec_v9_5.rb
154
155
  - lib/elastomer_client/client/rest_api_spec/rest_api.rb
155
156
  - lib/elastomer_client/client/scroller.rb
156
157
  - lib/elastomer_client/client/snapshot.rb