chewy 8.3.1 → 8.4.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 810cfb80d064f5c95c437069ca5bed0813f7d443c1e0b229b8454b740f915028
4
- data.tar.gz: 6966b7c2cc643031a5194882924bb590d07196bd3e64e3fd885ea6f50a5919ee
3
+ metadata.gz: c7ffb0b83cbd361f40168edad592083762af5035dc02ca60f205f16c6224a9a6
4
+ data.tar.gz: ca56cddc2b59646fbaa9188c707c1d9f23d9dabf2166e1a1ff168d83adf6e6ef
5
5
  SHA512:
6
- metadata.gz: ab138cd69c7e7207cae2e505e7cbf7f8e70b8577a98214d9e23cfff3b9d0b5578de022012b3f35790a6fced795b2974560684ef82590d3947acba0306b1e6151
7
- data.tar.gz: f571e4199f796015d2257c21451456085ca3ccb78ca868e2a26f3f636c2f90f0b2ac478bef2d558d03c45bac1e95af185caa08a2ea5e240bc8608fed0cffc4f6
6
+ metadata.gz: 91751c0575275edd79e08a945fed07b775a6cd0cd216c3fcba95fbab4085b22dd476d23485c63ee93d740b3f2b8e08bc742789e6f60313fcc49d43013bd48ebe
7
+ data.tar.gz: 4de8630df6346a7eccbbd14ca0cc2b5d84d636e314c58840cae605433a570e6a9b6c07bf8d2f27b695808efcdc491b41006fd4a18038412ee485fe36669711bb
data/CHANGELOG.md CHANGED
@@ -8,7 +8,29 @@
8
8
 
9
9
  ### Changes
10
10
 
11
- ## 8.3.0 (2026-06-05)
11
+ ## 8.4.1 (2026-06-19)
12
+
13
+ ### New Features
14
+
15
+ ### Bug Fixes
16
+
17
+ * [#1043](https://github.com/toptal/chewy/pull/1043)Fixed `field ..., value: proc(&:method)` (and other splat-declaring procs) raising `ArgumentError: wrong number of arguments` during import. The compiled compose path introduced in 8.4.0 forwarded `crutches`/`context` as extra positional arguments to splat procs; `Symbol#to_proc` then passed them on to the method. Splat procs are now called with the object alone, matching the plain compose path. ([@AlfonsoUceda][])
18
+
19
+ ### Changes
20
+
21
+ ## 8.4.0 (2026-06-17)
22
+
23
+ ### New Features
24
+
25
+ * [#1036](https://github.com/toptal/chewy/issues/1036): Add `Chewy.close_client` and `Chewy::ElasticClient#close` to explicitly close connections to Elasticsearch, avoiding file descriptor leaks in long-lived multi-threaded processes (e.g. Sidekiq). ([@AlfonsoUceda][])
26
+
27
+ ### Bug Fixes
28
+
29
+ * ([#1034](https://github.com/toptal/chewy/issues/1034)): Fixed `delayed_sidekiq` strategy raising `TypeError: Unsupported command argument type: Array` on Sidekiq 7+. The LUA scripts now run via `EVALSHA` through a portable helper instead of redis-rb's keyword `eval`, which the redis-client-backed client Sidekiq 7+ yields does not support. ([@AlfonsoUceda][])
30
+
31
+ ### Changes
32
+
33
+ ## 8.3.1 (2026-06-05)
12
34
 
13
35
  ### New Features
14
36
 
@@ -898,6 +920,7 @@
898
920
  [@AgeevAndrew]: https://github.com/AgeevAndrew
899
921
  [@aglushkov]: https://github.com/aglushkov
900
922
  [@AlexVPopov]: https://github.com/AlexVPopov
923
+ [@AlfonsoUceda]: https://github.com/AlfonsoUceda
901
924
  [@AndreySavelyev]: https://github.com/AndreySavelyev
902
925
  [@afg419]: https://github.com/afg419
903
926
  [@arion]: https://github.com/arion
data/README.md CHANGED
@@ -133,6 +133,23 @@ development:
133
133
  ca_file: './tmp/http_ca.crt'
134
134
  ```
135
135
 
136
+ ### Closing connections
137
+
138
+ `Chewy.client` is memoized per thread, so every thread that touches Chewy gets
139
+ its own client with its own connections to Elasticsearch. Neither
140
+ `elasticsearch-ruby` nor `elastic-transport` expose a public way to close those
141
+ connections, so they are only released when Ruby's garbage collector reclaims
142
+ the client. In long-lived, multi-threaded processes that frequently spawn and
143
+ discard threads (e.g. Sidekiq, which replaces a thread on job failure), this can
144
+ leak file descriptors.
145
+
146
+ Use `Chewy.close_client` to close the current thread's connections and drop its
147
+ client. The next `Chewy.client` call rebuilds a fresh one:
148
+
149
+ ```ruby
150
+ Chewy.close_client
151
+ ```
152
+
136
153
  ### Index
137
154
 
138
155
  Create `app/chewy/users_index.rb` with User Index:
@@ -12,6 +12,21 @@ module Chewy
12
12
  @elastic_client = elastic_client
13
13
  end
14
14
 
15
+ # Closes the underlying connections to Elasticsearch.
16
+ #
17
+ # Neither elasticsearch-ruby nor elastic-transport expose a public method
18
+ # to close connections, so they are only released when Ruby's garbage
19
+ # collector reclaims the client instance. This reaches down to the Faraday
20
+ # connection of every transport connection and closes it explicitly, which
21
+ # is useful to avoid file descriptor leaks in long-lived processes that
22
+ # build a client per thread (e.g. Sidekiq workers).
23
+ def close
24
+ @elastic_client.transport.connections.each do |connection|
25
+ faraday = connection.connection
26
+ faraday.close if faraday.respond_to?(:close)
27
+ end
28
+ end
29
+
15
30
  private
16
31
 
17
32
  def method_missing(name, *args, **kwargs, &block)
@@ -187,17 +187,24 @@ module Chewy
187
187
  idx = @procs.size
188
188
  @procs << v
189
189
  procs_ref = "compiled_procs[#{idx}]"
190
- param_count = positional_param_count(v)
191
- case v.arity
192
- when 0
190
+ if v.arity.zero?
193
191
  "#{obj_var}.instance_exec(&#{procs_ref})"
192
+ elsif v.parameters.any? { |type, _| type == :rest }
193
+ # Procs that declare a splat — Symbol#to_proc (`proc(&:method)`,
194
+ # parameters `[[:req], [:rest]]`) and `->(*args)` — must NOT
195
+ # receive crutches/context as extra positional args: a symbol
196
+ # proc would forward them to the method (`obj.method(crutches,
197
+ # context)` => "wrong number of arguments"). Mirror the plain
198
+ # Base#value_by_proc negative-arity branch (`value.call(*object)`)
199
+ # so the compiled and fallback paths behave identically.
200
+ "#{procs_ref}.call(*#{obj_var})"
194
201
  else
195
202
  # Pass only as many of (object, crutches, context) as the
196
203
  # proc actually declares. This keeps lambdas with optional
197
204
  # args (negative arity like `->(o, c=nil)`) from being
198
205
  # called with too many arguments. Anything beyond context
199
206
  # truncates to all three.
200
- args = [obj_var, 'crutches', 'context'].first(param_count)
207
+ args = [obj_var, 'crutches', 'context'].first(positional_param_count(v))
201
208
  "#{procs_ref}.call(#{args.join(', ')})"
202
209
  end
203
210
  else
@@ -212,14 +219,13 @@ module Chewy
212
219
  end
213
220
  end
214
221
 
215
- # Number of positional parameters declared by `proc`, counting
216
- # required + optional. Splats and keyword args contribute one
217
- # bucket each so the caller still passes all three context args.
222
+ # Number of (object, crutches, context) args to pass to a splat-free
223
+ # proc: its required + optional positional parameters, capped at the
224
+ # three available. Splat procs are handled by the caller and never
225
+ # reach here.
218
226
  def positional_param_count(proc)
219
- params = proc.parameters
220
- required = params.count { |type, _| %i[req opt].include?(type) }
221
- has_splat = params.any? { |type, _| type == :rest }
222
- has_splat ? 3 : [required, 3].min
227
+ required = proc.parameters.count { |type, _| %i[req opt].include?(type) }
228
+ [required, 3].min
223
229
  end
224
230
 
225
231
  def safe_identifier?(name)
@@ -47,7 +47,7 @@ module Chewy
47
47
  end
48
48
 
49
49
  def index_entry(object)
50
- return [] if @fields&.empty?
50
+ return [] if @fields && @fields.empty?
51
51
 
52
52
  entry = {}
53
53
  entry[:_id] = index_object_ids[object] if index_object_ids[object]
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chewy
4
+ class Strategy
5
+ class DelayedSidekiq
6
+ # Runs a LUA script through whatever client `Sidekiq.redis` yields.
7
+ #
8
+ # redis-rb (Sidekiq <= 6) accepts `eval(script, keys:, argv:)`, but the
9
+ # real Sidekiq 7+ runtime yields a redis-client-backed CompatClient that
10
+ # has no `#eval` and forwards the keyword args as a nested array, raising
11
+ # `TypeError: Unsupported command argument type: Array` (issue #971).
12
+ #
13
+ # `evalsha(sha, keys_array, argv_array)` is the one form both clients
14
+ # expand to the flat `EVALSHA sha numkeys *keys *argv` shape, so we load
15
+ # the script once and call it by digest, reloading on NOSCRIPT.
16
+ module RedisScript
17
+ def self.call(redis, script, keys:, argv:)
18
+ reloaded = false
19
+ begin
20
+ shas[script] ||= redis.script(:load, script)
21
+ redis.evalsha(shas[script], keys, argv)
22
+ rescue StandardError => e
23
+ raise if reloaded || !e.message.to_s.include?('NOSCRIPT')
24
+
25
+ reloaded = true
26
+ shas.delete(script) # flushed cache or failover: reload and retry once
27
+ retry
28
+ end
29
+ end
30
+
31
+ def self.shas
32
+ @shas ||= {}
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -10,6 +10,7 @@ require_relative '../../index'
10
10
  module Chewy
11
11
  class Strategy
12
12
  class DelayedSidekiq
13
+ require_relative 'redis_script'
13
14
  require_relative 'worker'
14
15
 
15
16
  LUA_SCRIPT = <<~LUA
@@ -98,7 +99,7 @@ module Chewy
98
99
  def postpone
99
100
  ::Sidekiq.redis do |redis|
100
101
  # do the redis stuff in a single command to avoid concurrency issues
101
- if redis.eval(LUA_SCRIPT, keys: [timechunk_key, timechunks_key], argv: [serialize_data, at, ttl])
102
+ if RedisScript.call(redis, LUA_SCRIPT, keys: [timechunk_key, timechunks_key], argv: [serialize_data, at, ttl])
102
103
  ::Sidekiq::Client.push(
103
104
  'queue' => sidekiq_queue,
104
105
  'at' => at + margin,
@@ -39,7 +39,7 @@ module Chewy
39
39
  options[:refresh] = !Chewy.disable_refresh_async if Chewy.disable_refresh_async
40
40
 
41
41
  ::Sidekiq.redis do |redis|
42
- members = redis.eval(LUA_SCRIPT, keys: [], argv: [type, score, Scheduler::KEY_PREFIX])
42
+ members = RedisScript.call(redis, LUA_SCRIPT, keys: [], argv: [type, score, Scheduler::KEY_PREFIX])
43
43
 
44
44
  # extract ids and fields & do the reset of records
45
45
  ids, fields = extract_ids_and_fields(members)
data/lib/chewy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chewy
2
- VERSION = '8.3.1'.freeze
2
+ VERSION = '8.4.1'.freeze
3
3
  end
data/lib/chewy.rb CHANGED
@@ -102,6 +102,21 @@ module Chewy
102
102
  Chewy.current[:chewy_client] ||= Chewy::ElasticClient.new
103
103
  end
104
104
 
105
+ # Closes the current thread's client connections to Elasticsearch and
106
+ # drops the thread-local client, so the next `Chewy.client` call builds a
107
+ # fresh one.
108
+ #
109
+ # Useful in long-lived multi-threaded processes (e.g. Sidekiq) where the
110
+ # per-thread client would otherwise keep its connections open until the
111
+ # dead thread is garbage collected, leaking file descriptors.
112
+ def close_client
113
+ client = Chewy.current[:chewy_client]
114
+ return unless client
115
+
116
+ client.close
117
+ Chewy.current[:chewy_client] = nil
118
+ end
119
+
105
120
  # Sends wait_for_status request to ElasticSearch with status
106
121
  # defined in configuration.
107
122
  #
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chewy
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.3.1
4
+ version: 8.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toptal, LLC
@@ -174,6 +174,7 @@ files:
174
174
  - lib/chewy/strategy/base.rb
175
175
  - lib/chewy/strategy/bypass.rb
176
176
  - lib/chewy/strategy/delayed_sidekiq.rb
177
+ - lib/chewy/strategy/delayed_sidekiq/redis_script.rb
177
178
  - lib/chewy/strategy/delayed_sidekiq/scheduler.rb
178
179
  - lib/chewy/strategy/delayed_sidekiq/worker.rb
179
180
  - lib/chewy/strategy/lazy_sidekiq.rb
@@ -202,7 +203,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
202
203
  - !ruby/object:Gem::Version
203
204
  version: '0'
204
205
  requirements: []
205
- rubygems_version: 4.0.12
206
+ rubygems_version: 4.0.14
206
207
  specification_version: 4
207
208
  summary: Elasticsearch ODM client wrapper
208
209
  test_files: []