chewy 7.5.1 → 7.6.0

Sign up to get free protection for your applications and to get access to all the features.
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