chewy 7.5.1 → 7.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64f061f379d740c56b7af2678ec03e3c80c1bd4c072b2aa3f68bf7803d4bc0fe
4
- data.tar.gz: 6226557f9cec700d91473f36bb8623ff5bbf838211463188ac0f83eaf0ab227d
3
+ metadata.gz: a15165f889275fecc6a0d590c3339ce0ac9b7944f916306fd6883e7c2be67747
4
+ data.tar.gz: 8efc6201add68bf1c378f598934b6c52c74bd9a62056e7aafdd357358d5cd2b0
5
5
  SHA512:
6
- metadata.gz: 8cb278cc5fa838a97e0c9fb94c90ea08d4a84effb041a809f850a267514c61ce78f1a919a8a0fdc5119ca968e217fe9b0aa38c307f9589f8413501f690efb314
7
- data.tar.gz: 50ad4271e4948b22b626202843be7107fcc720fd3da490c14a372ca018ab0e35c0e151d5dc5b55f23b7c382a0d19cf72f7cc63e40a2dc1fc0ca286c1de717941
6
+ metadata.gz: cfc7f1297fc72fcbdfdbedbcd82c6afe12dd9e15583b4e3467a8a77da89cd1aa8f20b9d0c0a8dd1841275e2916715030e28e9ed10cf510d6985904be9574890e
7
+ data.tar.gz: 6917027945ce94dab50d2f6d679570be58e4bb86472f1d591805d915ca425ecbd24e4cc13e30a459fe2dae4d572002735b69cef138a5c6038c52803a251b8a54
@@ -23,6 +23,18 @@ jobs:
23
23
  env:
24
24
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
25
25
 
26
+ services:
27
+ redis:
28
+ # Docker Hub image
29
+ image: redis
30
+ ports:
31
+ - '6379:6379'
32
+ # Set health checks to wait until redis has started
33
+ options: >-
34
+ --health-cmd "redis-cli ping"
35
+ --health-interval 10s
36
+ --health-timeout 5s
37
+ --health-retries 5
26
38
  steps:
27
39
  - uses: actions/checkout@v4
28
40
  - uses: ruby/setup-ruby@v1
data/CHANGELOG.md CHANGED
@@ -8,6 +8,17 @@
8
8
 
9
9
  ### Bugs Fixed
10
10
 
11
+ ## 7.6.0 (2024-05-03)
12
+
13
+ ### Changes
14
+
15
+ * [#933](https://github.com/toptal/chewy/pull/933): Relax allowed `elasticsearch` dependency versions. ([@mjankowski][])
16
+
17
+ ### Bugs Fixed
18
+ * [#937](https://github.com/toptal/chewy/pull/937): Fix for race condition while using the `delayed_sidekiq` strategy. Also, fix for Redis bloating in case of reindexing error ([@skcc321](https://github.com/skcc321))
19
+
20
+ * [#947](https://github.com/toptal/chewy/pull/947): Fix intermittent time-based failure in delayed sidekiq spec. ([@mjankowski][])
21
+
11
22
  ## 7.5.1 (2024-01-30)
12
23
 
13
24
  ### New Features
data/README.md CHANGED
@@ -776,9 +776,12 @@ Chewy.settings[:sidekiq] = {queue: :low}
776
776
 
777
777
  #### `:delayed_sidekiq`
778
778
 
779
- It accumulates ids of records to be reindexed during the latency window in redis and then does the reindexing of all accumulated records at once.
780
- The strategy is very useful in case of frequently mutated records.
781
- It supports `update_fields` option, so it will try to select just enough data from the DB
779
+ It accumulates IDs of records to be reindexed during the latency window in Redis and then performs the reindexing of all accumulated records at once.
780
+ This strategy is very useful in the case of frequently mutated records.
781
+ It supports the `update_fields` option, so it will attempt to select just enough data from the database.
782
+
783
+ Keep in mind, this strategy does not guarantee reindexing in the event of Sidekiq worker termination or an error during the reindexing phase.
784
+ This behavior is intentional to prevent continuous growth of Redis db.
782
785
 
783
786
  There are three options that can be defined in the index:
784
787
  ```ruby
data/chewy.gemspec CHANGED
@@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.require_paths = ['lib']
18
18
 
19
19
  spec.add_dependency 'activesupport', '>= 5.2' # Remove with major version bump, 8.x
20
- spec.add_dependency 'elasticsearch', '>= 7.12.0', '< 7.14.0'
20
+ spec.add_dependency 'elasticsearch', '>= 7.14.0', '< 8'
21
21
  spec.add_dependency 'elasticsearch-dsl'
22
22
  spec.metadata['rubygems_mfa_required'] = 'true'
23
23
  end
@@ -1,12 +1,12 @@
1
1
  gem 'database_cleaner'
2
2
  gem 'elasticsearch-extensions'
3
3
  gem 'method_source'
4
- gem 'mock_redis'
5
4
  gem 'rake'
5
+ gem 'redis', require: false
6
6
  gem 'rspec', '>= 3.7.0'
7
7
  gem 'rspec-collection_matchers'
8
8
  gem 'rspec-its'
9
- gem 'rubocop', '1.60.1'
10
- gem 'sqlite3'
9
+ gem 'rubocop', '1.63.4'
10
+ gem 'sqlite3', '~> 1.4'
11
11
  gem 'timecop'
12
12
  gem 'unparser'
data/lib/chewy/config.rb CHANGED
@@ -70,12 +70,12 @@ module Chewy
70
70
  end
71
71
 
72
72
  def transport_logger=(logger)
73
- Chewy.client.transport.logger = logger
73
+ Chewy.client.transport.transport.logger = logger
74
74
  @transport_logger = logger
75
75
  end
76
76
 
77
77
  def transport_tracer=(tracer)
78
- Chewy.client.transport.tracer = tracer
78
+ Chewy.client.transport.transport.tracer = tracer
79
79
  @transport_tracer = tracer
80
80
  end
81
81
 
@@ -12,13 +12,43 @@ module Chewy
12
12
  class DelayedSidekiq
13
13
  require_relative 'worker'
14
14
 
15
+ LUA_SCRIPT = <<~LUA
16
+ local timechunk_key = KEYS[1]
17
+ local timechunks_key = KEYS[2]
18
+ local serialize_data = ARGV[1]
19
+ local at = ARGV[2]
20
+ local ttl = tonumber(ARGV[3])
21
+
22
+ local schedule_job = false
23
+
24
+ -- Check if the 'sadd?' method is available
25
+ if redis.call('exists', 'sadd?') == 1 then
26
+ redis.call('sadd?', timechunk_key, serialize_data)
27
+ else
28
+ redis.call('sadd', timechunk_key, serialize_data)
29
+ end
30
+
31
+ -- Set expiration for timechunk_key
32
+ redis.call('expire', timechunk_key, ttl)
33
+
34
+ -- Check if timechunk_key exists in the sorted set
35
+ if not redis.call('zrank', timechunks_key, timechunk_key) then
36
+ -- Add timechunk_key to the sorted set
37
+ redis.call('zadd', timechunks_key, at, timechunk_key)
38
+ -- Set expiration for timechunks_key
39
+ redis.call('expire', timechunks_key, ttl)
40
+ schedule_job = true
41
+ end
42
+
43
+ return schedule_job
44
+ LUA
45
+
15
46
  class Scheduler
16
47
  DEFAULT_TTL = 60 * 60 * 24 # in seconds
17
48
  DEFAULT_LATENCY = 10
18
49
  DEFAULT_MARGIN = 2
19
50
  DEFAULT_QUEUE = 'chewy'
20
51
  KEY_PREFIX = 'chewy:delayed_sidekiq'
21
- ALL_SETS_KEY = "#{KEY_PREFIX}:all_sets".freeze
22
52
  FALLBACK_FIELDS = 'all'
23
53
  FIELDS_IDS_SEPARATOR = ';'
24
54
  IDS_SEPARATOR = ','
@@ -67,21 +97,8 @@ module Chewy
67
97
  # | chewy:delayed_sidekiq:CitiesIndex:1679347868
68
98
  def postpone
69
99
  ::Sidekiq.redis do |redis|
70
- # warning: Redis#sadd will always return an Integer in Redis 5.0.0. Use Redis#sadd? instead
71
- if redis.respond_to?(:sadd?)
72
- redis.sadd?(ALL_SETS_KEY, timechunks_key)
73
- redis.sadd?(timechunk_key, serialize_data)
74
- else
75
- redis.sadd(ALL_SETS_KEY, timechunks_key)
76
- redis.sadd(timechunk_key, serialize_data)
77
- end
78
-
79
- redis.expire(timechunk_key, ttl)
80
-
81
- unless redis.zrank(timechunks_key, timechunk_key)
82
- redis.zadd(timechunks_key, at, timechunk_key)
83
- redis.expire(timechunks_key, ttl)
84
-
100
+ # 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])
85
102
  ::Sidekiq::Client.push(
86
103
  'queue' => sidekiq_queue,
87
104
  'at' => at + margin,
@@ -6,13 +6,40 @@ module Chewy
6
6
  class Worker
7
7
  include ::Sidekiq::Worker
8
8
 
9
+ LUA_SCRIPT = <<~LUA
10
+ local type = ARGV[1]
11
+ local score = tonumber(ARGV[2])
12
+ local prefix = ARGV[3]
13
+ local timechunks_key = prefix .. ":" .. type .. ":timechunks"
14
+
15
+ -- Get timechunk_keys with scores less than or equal to the specified score
16
+ local timechunk_keys = redis.call('zrangebyscore', timechunks_key, '-inf', score)
17
+
18
+ -- Get all members from the sets associated with the timechunk_keys
19
+ local members = {}
20
+ for _, timechunk_key in ipairs(timechunk_keys) do
21
+ local set_members = redis.call('smembers', timechunk_key)
22
+ for _, member in ipairs(set_members) do
23
+ table.insert(members, member)
24
+ end
25
+ end
26
+
27
+ -- Remove timechunk_keys and their associated sets
28
+ for _, timechunk_key in ipairs(timechunk_keys) do
29
+ redis.call('del', timechunk_key)
30
+ end
31
+
32
+ -- Remove timechunks with scores less than or equal to the specified score
33
+ redis.call('zremrangebyscore', timechunks_key, '-inf', score)
34
+
35
+ return members
36
+ LUA
37
+
9
38
  def perform(type, score, options = {})
10
39
  options[:refresh] = !Chewy.disable_refresh_async if Chewy.disable_refresh_async
11
40
 
12
41
  ::Sidekiq.redis do |redis|
13
- timechunks_key = "#{Scheduler::KEY_PREFIX}:#{type}:timechunks"
14
- timechunk_keys = redis.zrangebyscore(timechunks_key, -1, score)
15
- members = timechunk_keys.flat_map { |timechunk_key| redis.smembers(timechunk_key) }.compact
42
+ members = redis.eval(LUA_SCRIPT, keys: [], argv: [type, score, Scheduler::KEY_PREFIX])
16
43
 
17
44
  # extract ids and fields & do the reset of records
18
45
  ids, fields = extract_ids_and_fields(members)
@@ -22,9 +49,6 @@ module Chewy
22
49
  index.strategy_config.delayed_sidekiq.reindex_wrapper.call do
23
50
  options.any? ? index.import!(ids, **options) : index.import!(ids)
24
51
  end
25
-
26
- redis.del(timechunk_keys)
27
- redis.zremrangebyscore(timechunks_key, -1, score)
28
52
  end
29
53
  end
30
54
 
@@ -9,11 +9,11 @@ module Chewy
9
9
  # leak and potential flaky tests.
10
10
  def self.clear_timechunks!
11
11
  ::Sidekiq.redis do |redis|
12
- timechunk_sets = redis.smembers(Chewy::Strategy::DelayedSidekiq::Scheduler::ALL_SETS_KEY)
13
- break if timechunk_sets.empty?
12
+ keys_to_delete = redis.keys("#{Scheduler::KEY_PREFIX}*")
14
13
 
15
- redis.pipelined do |pipeline|
16
- timechunk_sets.each { |set| pipeline.del(set) }
14
+ # Delete keys one by one
15
+ keys_to_delete.each do |key|
16
+ redis.del(key)
17
17
  end
18
18
  end
19
19
  end
data/lib/chewy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chewy
2
- VERSION = '7.5.1'.freeze
2
+ VERSION = '7.6.0'.freeze
3
3
  end
@@ -22,7 +22,7 @@ describe Chewy::Config do
22
22
 
23
23
  specify do
24
24
  expect { subject.transport_logger = logger }
25
- .to change { Chewy.client.transport.logger }.to(logger)
25
+ .to change { Chewy.client.transport.transport.logger }.to(logger)
26
26
  end
27
27
  specify do
28
28
  expect { subject.transport_logger = logger }
@@ -40,7 +40,7 @@ describe Chewy::Config do
40
40
 
41
41
  specify do
42
42
  expect { subject.transport_tracer = tracer }
43
- .to change { Chewy.client.transport.tracer }.to(tracer)
43
+ .to change { Chewy.client.transport.transport.tracer }.to(tracer)
44
44
  end
45
45
  specify do
46
46
  expect { subject.transport_tracer = tracer }
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  if defined?(Sidekiq)
4
4
  require 'sidekiq/testing'
5
- require 'mock_redis'
5
+ require 'redis'
6
6
 
7
7
  describe Chewy::Strategy::DelayedSidekiq do
8
8
  around do |example|
@@ -10,9 +10,10 @@ if defined?(Sidekiq)
10
10
  end
11
11
 
12
12
  before do
13
- redis = MockRedis.new
13
+ redis = Redis.new
14
14
  allow(Sidekiq).to receive(:redis).and_yield(redis)
15
15
  Sidekiq::Worker.clear_all
16
+ described_class.clear_timechunks!
16
17
  end
17
18
 
18
19
  before do
@@ -35,7 +36,7 @@ if defined?(Sidekiq)
35
36
 
36
37
  it "respects 'refresh: false' options" do
37
38
  allow(Chewy).to receive(:disable_refresh_async).and_return(true)
38
- expect(CitiesIndex).to receive(:import!).with([city.id, other_city.id], refresh: false)
39
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id]), refresh: false)
39
40
  scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id, other_city.id])
40
41
  scheduler.postpone
41
42
  Chewy::Strategy::DelayedSidekiq::Worker.drain
@@ -47,7 +48,7 @@ if defined?(Sidekiq)
47
48
  expect(Sidekiq::Client).to receive(:push).with(
48
49
  hash_including(
49
50
  'queue' => 'chewy',
50
- 'at' => (Time.current.to_i.ceil(-1) + 2.seconds).to_i,
51
+ 'at' => expected_at_time.to_i,
51
52
  'class' => Chewy::Strategy::DelayedSidekiq::Worker,
52
53
  'args' => ['CitiesIndex', an_instance_of(Integer)]
53
54
  )
@@ -62,6 +63,11 @@ if defined?(Sidekiq)
62
63
  end
63
64
  end
64
65
  end
66
+
67
+ def expected_at_time
68
+ target = described_class::Scheduler::DEFAULT_LATENCY.seconds.from_now.to_i
69
+ target - (target % described_class::Scheduler::DEFAULT_LATENCY) + described_class::Scheduler::DEFAULT_MARGIN.seconds
70
+ end
65
71
  end
66
72
 
67
73
  context 'with custom config' do
@@ -103,7 +109,7 @@ if defined?(Sidekiq)
103
109
  context 'two reindex call within the timewindow' do
104
110
  it 'accumulates all ids does the reindex one time' do
105
111
  Timecop.freeze do
106
- expect(CitiesIndex).to receive(:import!).with([other_city.id, city.id]).once
112
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id])).once
107
113
  scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id])
108
114
  scheduler.postpone
109
115
  scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id])
@@ -115,7 +121,7 @@ if defined?(Sidekiq)
115
121
  context 'one call with update_fields another one without update_fields' do
116
122
  it 'does reindex of all fields' do
117
123
  Timecop.freeze do
118
- expect(CitiesIndex).to receive(:import!).with([other_city.id, city.id]).once
124
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id])).once
119
125
  scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id], update_fields: ['name'])
120
126
  scheduler.postpone
121
127
  scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id])
@@ -128,7 +134,7 @@ if defined?(Sidekiq)
128
134
  context 'both calls with different update fields' do
129
135
  it 'deos reindex with union of fields' do
130
136
  Timecop.freeze do
131
- expect(CitiesIndex).to receive(:import!).with([other_city.id, city.id], update_fields: %w[description name]).once
137
+ expect(CitiesIndex).to receive(:import!).with(match_array([city.id, other_city.id]), update_fields: %w[name description]).once
132
138
  scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [city.id], update_fields: ['name'])
133
139
  scheduler.postpone
134
140
  scheduler = Chewy::Strategy::DelayedSidekiq::Scheduler.new(CitiesIndex, [other_city.id], update_fields: ['description'])
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: 7.5.1
4
+ version: 7.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toptal, LLC
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-01-30 00:00:00.000000000 Z
12
+ date: 2024-05-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -31,20 +31,20 @@ dependencies:
31
31
  requirements:
32
32
  - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: 7.12.0
34
+ version: 7.14.0
35
35
  - - "<"
36
36
  - !ruby/object:Gem::Version
37
- version: 7.14.0
37
+ version: '8'
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
41
41
  requirements:
42
42
  - - ">="
43
43
  - !ruby/object:Gem::Version
44
- version: 7.12.0
44
+ version: 7.14.0
45
45
  - - "<"
46
46
  - !ruby/object:Gem::Version
47
- version: 7.14.0
47
+ version: '8'
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: elasticsearch-dsl
50
50
  requirement: !ruby/object:Gem::Requirement
@@ -320,7 +320,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
320
320
  - !ruby/object:Gem::Version
321
321
  version: '0'
322
322
  requirements: []
323
- rubygems_version: 3.2.33
323
+ rubygems_version: 3.4.10
324
324
  signing_key:
325
325
  specification_version: 4
326
326
  summary: Elasticsearch ODM client wrapper