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 +4 -4
- data/.github/workflows/ruby.yml +12 -0
- data/CHANGELOG.md +11 -0
- data/README.md +6 -3
- data/chewy.gemspec +1 -1
- data/gemfiles/base.gemfile +3 -3
- data/lib/chewy/config.rb +2 -2
- data/lib/chewy/strategy/delayed_sidekiq/scheduler.rb +33 -16
- data/lib/chewy/strategy/delayed_sidekiq/worker.rb +30 -6
- data/lib/chewy/strategy/delayed_sidekiq.rb +4 -4
- data/lib/chewy/version.rb +1 -1
- data/spec/chewy/config_spec.rb +2 -2
- data/spec/chewy/strategy/delayed_sidekiq_spec.rb +13 -7
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a15165f889275fecc6a0d590c3339ce0ac9b7944f916306fd6883e7c2be67747
|
4
|
+
data.tar.gz: 8efc6201add68bf1c378f598934b6c52c74bd9a62056e7aafdd357358d5cd2b0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cfc7f1297fc72fcbdfdbedbcd82c6afe12dd9e15583b4e3467a8a77da89cd1aa8f20b9d0c0a8dd1841275e2916715030e28e9ed10cf510d6985904be9574890e
|
7
|
+
data.tar.gz: 6917027945ce94dab50d2f6d679570be58e4bb86472f1d591805d915ca425ecbd24e4cc13e30a459fe2dae4d572002735b69cef138a5c6038c52803a251b8a54
|
data/.github/workflows/ruby.yml
CHANGED
@@ -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
|
780
|
-
|
781
|
-
It supports `update_fields` option, so it will
|
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.
|
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
|
data/gemfiles/base.gemfile
CHANGED
@@ -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.
|
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
|
-
#
|
71
|
-
if redis.
|
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
|
-
|
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
|
-
|
13
|
-
break if timechunk_sets.empty?
|
12
|
+
keys_to_delete = redis.keys("#{Scheduler::KEY_PREFIX}*")
|
14
13
|
|
15
|
-
|
16
|
-
|
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
data/spec/chewy/config_spec.rb
CHANGED
@@ -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 '
|
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 =
|
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' =>
|
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([
|
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([
|
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([
|
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.
|
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-
|
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.
|
34
|
+
version: 7.14.0
|
35
35
|
- - "<"
|
36
36
|
- !ruby/object:Gem::Version
|
37
|
-
version:
|
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.
|
44
|
+
version: 7.14.0
|
45
45
|
- - "<"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
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.
|
323
|
+
rubygems_version: 3.4.10
|
324
324
|
signing_key:
|
325
325
|
specification_version: 4
|
326
326
|
summary: Elasticsearch ODM client wrapper
|