chewy 7.2.4 → 7.2.5

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: 0de5ea12714d98c68dc3d74d1b6bb9debefea211a025749b8958a804a285bdcd
4
- data.tar.gz: f9256684493364e7f6b1ae1b3da4d6376624369df3ff950750a6148cd7d6da2f
3
+ metadata.gz: 37685481eff7312e08ba8d113f030ee5ab38e3414788c6cd2a0385829c99a712
4
+ data.tar.gz: d4a2a9d86d93e9a9c6ec5d11cabe51f67511f6b347faee3be9e276cf528c4dc2
5
5
  SHA512:
6
- metadata.gz: f35594db25143614d6c88ac2aef7081975502e0434993e9b60495d6f3d63d2f13fee246275bd5b6b6c5b1deae4db06f5b734bf73369e77aeea22c7141b177417
7
- data.tar.gz: 92dbe74fb416105b4c38dcb95b40646c53597d17c2cdd2800107c27a979da24abceccb0d6ab51b0b3783f769327a3a9fed323b9d504de65cc3541e90df0bd273
6
+ metadata.gz: c973f623b179e98284019a2b2dddc1cb7506719585064673cccab0e1c790f599dce43a1395e364d5ed8be814ee0af1fe4985e188f25dcaa32e75eefa57a4f6e3
7
+ data.tar.gz: bd1c55232fd02520382a20109899f3b4d59a174c1d1ff875592f08582b5f3072dd7d6f47760df3be58f38756718f31f802832476a1e0c64ebfb9909748e31eac
data/CHANGELOG.md CHANGED
@@ -8,6 +8,18 @@
8
8
 
9
9
  ### Bugs Fixed
10
10
 
11
+ ## 7.2.5 (2022-03-04)
12
+
13
+ ### New Features
14
+
15
+ * [#827](https://github.com/toptal/chewy/pull/827): Add `:lazy_sidekiq` strategy, that defers not only importing but also `update_index` callback evaluation for created and updated objects. ([@sl4vr][])
16
+ * [#827](https://github.com/toptal/chewy/pull/827): Add `:atomic_no_refresh` strategy. Like `:atomic`, but `refresh=false` parameter is set. ([@barthez][])
17
+ * [#827](https://github.com/toptal/chewy/pull/827): Add `:no_refresh` chain call to `update_index` matcher to ensure import was called with `refresh=false`. ([@barthez][])
18
+
19
+ ### Bugs Fixed
20
+
21
+ * [#835](https://github.com/toptal/chewy/pull/835): Support keyword arguments in named scopes. ([@milk1000cc][])
22
+
11
23
  ## 7.2.4 (2022-02-03)
12
24
 
13
25
  ### New Features
data/README.md CHANGED
@@ -754,6 +754,23 @@ The default queue name is `chewy`, you can customize it in settings: `sidekiq.qu
754
754
  Chewy.settings[:sidekiq] = {queue: :low}
755
755
  ```
756
756
 
757
+ #### `:lazy_sidekiq`
758
+
759
+ This does the same thing as `:sidekiq`, but with lazy evaluation. Beware it does not allow you to use any non-persistent record state for indices and conditions because record will be re-fetched from database asynchronously using sidekiq. However for destroying records strategy will fallback to `:sidekiq` because it's not possible to re-fetch deleted records from database.
760
+
761
+ The purpose of this strategy is to improve the response time of the code that should update indexes, as it does not only defer actual ES calls to a background job but `update_index` callbacks evaluation (for created and updated objects) too. Similar to `:sidekiq`, index update is asynchronous so this strategy cannot be used when data and index synchronization is required.
762
+
763
+ ```ruby
764
+ Chewy.strategy(:lazy_sidekiq) do
765
+ City.popular.map(&:do_some_update_action!)
766
+ end
767
+ ```
768
+
769
+ The default queue name is `chewy`, you can customize it in settings: `sidekiq.queue_name`
770
+ ```
771
+ Chewy.settings[:sidekiq] = {queue: :low}
772
+ ```
773
+
757
774
  #### `:active_job`
758
775
 
759
776
  This does the same thing as `:atomic`, but using ActiveJob. This will inherit the ActiveJob configuration settings including the `active_job.queue_adapter` setting for the environment. Patch `Chewy::Strategy::ActiveJob::Worker` for index updates improving.
data/lib/chewy/config.rb CHANGED
@@ -32,7 +32,7 @@ module Chewy
32
32
  # Set number_of_replicas to 0 before reset and put the original value after
33
33
  # https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
34
34
  :reset_no_replicas,
35
- # Refresh or not when import async (sidekiq, activejob)
35
+ # Refresh or not when import async (sidekiq, lazy_sidekiq, activejob)
36
36
  :disable_refresh_async,
37
37
  # Default options for root of Chewy type. Allows to set default options
38
38
  # for type mappings like `_all`.
@@ -0,0 +1,87 @@
1
+ module Chewy
2
+ class Index
3
+ module Observe
4
+ module Helpers
5
+ def update_proc(index_name, *args, &block)
6
+ options = args.extract_options!
7
+ method = args.first
8
+
9
+ proc do
10
+ reference = if index_name.is_a?(Proc)
11
+ if index_name.arity.zero?
12
+ instance_exec(&index_name)
13
+ else
14
+ index_name.call(self)
15
+ end
16
+ else
17
+ index_name
18
+ end
19
+
20
+ index = Chewy.derive_name(reference)
21
+
22
+ next if Chewy.strategy.current.name == :bypass
23
+
24
+ backreference = if method && method.to_s == 'self'
25
+ self
26
+ elsif method
27
+ send(method)
28
+ else
29
+ instance_eval(&block)
30
+ end
31
+
32
+ index.update_index(backreference, options)
33
+ end
34
+ end
35
+
36
+ def extract_callback_options!(args)
37
+ options = args.extract_options!
38
+ result = options.each_key.with_object({}) do |key, hash|
39
+ hash[key] = options.delete(key) if %i[if unless].include?(key)
40
+ end
41
+ args.push(options) unless options.empty?
42
+ result
43
+ end
44
+ end
45
+
46
+ extend Helpers
47
+
48
+ module ActiveRecordMethods
49
+ extend ActiveSupport::Concern
50
+
51
+ def run_chewy_callbacks
52
+ chewy_callbacks.each { |callback| callback.call(self) }
53
+ end
54
+
55
+ def update_chewy_indices
56
+ Chewy.strategy.current.update_chewy_indices(self)
57
+ end
58
+
59
+ included do
60
+ class_attribute :chewy_callbacks, default: []
61
+ end
62
+
63
+ class_methods do
64
+ def initialize_chewy_callbacks
65
+ if Chewy.use_after_commit_callbacks
66
+ after_commit :update_chewy_indices, on: %i[create update]
67
+ after_commit :run_chewy_callbacks, on: :destroy
68
+ else
69
+ after_save :update_chewy_indices
70
+ after_destroy :run_chewy_callbacks
71
+ end
72
+ end
73
+
74
+ ruby2_keywords def update_index(type_name, *args, &block)
75
+ callback_options = Observe.extract_callback_options!(args)
76
+ update_proc = Observe.update_proc(type_name, *args, &block)
77
+ callback = Chewy::Index::Observe::Callback.new(update_proc, callback_options)
78
+
79
+ initialize_chewy_callbacks if chewy_callbacks.empty?
80
+
81
+ self.chewy_callbacks = chewy_callbacks.dup << callback
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,34 @@
1
+ module Chewy
2
+ class Index
3
+ module Observe
4
+ class Callback
5
+ def initialize(executable, filters = {})
6
+ @executable = executable
7
+ @if_filter = filters[:if]
8
+ @unless_filter = filters[:unless]
9
+ end
10
+
11
+ def call(context)
12
+ return if !@if_filter.nil? && !eval_filter(@if_filter, context)
13
+ return if !@unless_filter.nil? && eval_filter(@unless_filter, context)
14
+
15
+ eval_proc(@executable, context)
16
+ end
17
+
18
+ private
19
+
20
+ def eval_filter(filter, context)
21
+ case filter
22
+ when Symbol then context.send(filter)
23
+ when Proc then eval_proc(filter, context)
24
+ else filter
25
+ end
26
+ end
27
+
28
+ def eval_proc(executable, context)
29
+ executable.arity.zero? ? context.instance_exec(&executable) : executable.call(context)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,66 +1,11 @@
1
+ require 'chewy/index/observe/callback'
2
+ require 'chewy/index/observe/active_record_methods'
3
+
1
4
  module Chewy
2
5
  class Index
3
6
  module Observe
4
7
  extend ActiveSupport::Concern
5
8
 
6
- module Helpers
7
- def update_proc(index_name, *args, &block)
8
- options = args.extract_options!
9
- method = args.first
10
-
11
- proc do
12
- reference = if index_name.is_a?(Proc)
13
- if index_name.arity.zero?
14
- instance_exec(&index_name)
15
- else
16
- index_name.call(self)
17
- end
18
- else
19
- index_name
20
- end
21
-
22
- index = Chewy.derive_name(reference)
23
-
24
- next if Chewy.strategy.current.name == :bypass
25
-
26
- backreference = if method && method.to_s == 'self'
27
- self
28
- elsif method
29
- send(method)
30
- else
31
- instance_eval(&block)
32
- end
33
-
34
- index.update_index(backreference, options)
35
- end
36
- end
37
-
38
- def extract_callback_options!(args)
39
- options = args.extract_options!
40
- result = options.each_key.with_object({}) do |key, hash|
41
- hash[key] = options.delete(key) if %i[if unless].include?(key)
42
- end
43
- args.push(options) unless options.empty?
44
- result
45
- end
46
- end
47
-
48
- extend Helpers
49
-
50
- module ActiveRecordMethods
51
- ruby2_keywords def update_index(type_name, *args, &block)
52
- callback_options = Observe.extract_callback_options!(args)
53
- update_proc = Observe.update_proc(type_name, *args, &block)
54
-
55
- if Chewy.use_after_commit_callbacks
56
- after_commit(**callback_options, &update_proc)
57
- else
58
- after_save(**callback_options, &update_proc)
59
- after_destroy(**callback_options, &update_proc)
60
- end
61
- end
62
- end
63
-
64
9
  module ClassMethods
65
10
  def update_index(objects, options = {})
66
11
  Chewy.strategy.current.update(self, objects, options)
@@ -88,6 +88,11 @@ RSpec::Matchers.define :update_index do |index_name, options = {}| # rubocop:dis
88
88
  @only = true
89
89
  end
90
90
 
91
+ # Expect import to be called with refresh=false parameter
92
+ chain(:no_refresh) do
93
+ @no_refresh = true
94
+ end
95
+
91
96
  def supports_block_expectations?
92
97
  true
93
98
  end
@@ -100,11 +105,13 @@ RSpec::Matchers.define :update_index do |index_name, options = {}| # rubocop:dis
100
105
 
101
106
  index = Chewy.derive_name(index_name)
102
107
  if defined?(Mocha) && RSpec.configuration.mock_framework.to_s == 'RSpec::Core::MockingAdapters::Mocha'
103
- Chewy::Index::Import::BulkRequest.stubs(:new).with(index, any_parameters).returns(mock_bulk_request)
108
+ params_matcher = @no_refresh ? has_entry(refresh: false) : any_parameters
109
+ Chewy::Index::Import::BulkRequest.stubs(:new).with(index, params_matcher).returns(mock_bulk_request)
104
110
  else
105
111
  mocked_already = ::RSpec::Mocks.space.proxy_for(Chewy::Index::Import::BulkRequest).method_double_if_exists_for_message(:new)
106
112
  allow(Chewy::Index::Import::BulkRequest).to receive(:new).and_call_original unless mocked_already
107
- allow(Chewy::Index::Import::BulkRequest).to receive(:new).with(index, any_args).and_return(mock_bulk_request)
113
+ params_matcher = @no_refresh ? hash_including(refresh: false) : any_args
114
+ allow(Chewy::Index::Import::BulkRequest).to receive(:new).with(index, params_matcher).and_return(mock_bulk_request)
108
115
  end
109
116
 
110
117
  Chewy.strategy(options[:strategy] || :atomic) { block.call }
@@ -146,7 +153,7 @@ RSpec::Matchers.define :update_index do |index_name, options = {}| # rubocop:dis
146
153
  output = ''
147
154
 
148
155
  if mock_bulk_request.updates.none?
149
- output << "Expected index `#{index_name}` to be updated, but it was not\n"
156
+ output << "Expected index `#{index_name}` to be updated#{' with no refresh' if @no_refresh}, but it was not\n"
150
157
  elsif @missed_reindex.present? || @missed_delete.present?
151
158
  message = "Expected index `#{index_name}` "
152
159
  message << [
data/lib/chewy/search.rb CHANGED
@@ -87,6 +87,7 @@ module Chewy
87
87
  define_method method do |*args, &block|
88
88
  scoping { source.public_send(method, *args, &block) }
89
89
  end
90
+ ruby2_keywords method
90
91
  end
91
92
  end
92
93
  end
@@ -0,0 +1,18 @@
1
+ module Chewy
2
+ class Strategy
3
+ # This strategy works like atomic but import objects with `refresh=false` parameter.
4
+ #
5
+ # Chewy.strategy(:atomic_no_refresh) do
6
+ # User.all.map(&:save) # Does nothing here
7
+ # Post.all.map(&:save) # And here
8
+ # # It imports all the changed users and posts right here
9
+ # # before block leaving with bulk ES API, kinda optimization
10
+ # end
11
+ #
12
+ class AtomicNoRefresh < Atomic
13
+ def leave
14
+ @stash.all? { |type, ids| type.import!(ids, refresh: false) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -22,6 +22,16 @@ module Chewy
22
22
  # strategies stack
23
23
  #
24
24
  def leave; end
25
+
26
+ # This method called when some model record is created or updated.
27
+ # Normally it will just evaluate all the Chewy callbacks and pass results
28
+ # to current strategy's update method.
29
+ # However it's possible to override it to achieve delayed evaluation of
30
+ # callbacks, e.g. using sidekiq.
31
+ #
32
+ def update_chewy_indices(object)
33
+ object.run_chewy_callbacks
34
+ end
25
35
  end
26
36
  end
27
37
  end
@@ -0,0 +1,64 @@
1
+ module Chewy
2
+ class Strategy
3
+ # The strategy works the same way as sidekiq, but performs
4
+ # async evaluation of all index callbacks on model create and update
5
+ # driven by sidekiq
6
+ #
7
+ # Chewy.strategy(:lazy_sidekiq) do
8
+ # User.all.map(&:save) # Does nothing here
9
+ # Post.all.map(&:save) # And here
10
+ # # It schedules import of all the changed users and posts right here
11
+ # end
12
+ #
13
+ class LazySidekiq < Sidekiq
14
+ class IndicesUpdateWorker
15
+ include ::Sidekiq::Worker
16
+
17
+ def perform(models)
18
+ Chewy.strategy(strategy) do
19
+ models.each do |model_type, model_ids|
20
+ model_type.constantize.where(id: model_ids).each(&:run_chewy_callbacks)
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def strategy
28
+ Chewy.disable_refresh_async ? :atomic_no_refresh : :atomic
29
+ end
30
+ end
31
+
32
+ def initialize
33
+ # Use parent's @stash to store destroyed records, since callbacks for them have to
34
+ # be run immediately on the strategy block end because we won't be able to fetch
35
+ # records further in IndicesUpdateWorker. This will be done by avoiding of
36
+ # LazySidekiq#update_chewy_indices call and calling LazySidekiq#update instead.
37
+ super
38
+
39
+ # @lazy_stash is used to store all the lazy evaluated callbacks with call of
40
+ # strategy's #update_chewy_indices.
41
+ @lazy_stash = {}
42
+ end
43
+
44
+ def leave
45
+ # Fallback to Sidekiq#leave implementation for destroyed records stored in @stash.
46
+ super
47
+
48
+ # Proceed with other records stored in @lazy_stash
49
+ return if @lazy_stash.empty?
50
+
51
+ ::Sidekiq::Client.push(
52
+ 'queue' => sidekiq_queue,
53
+ 'class' => Chewy::Strategy::LazySidekiq::IndicesUpdateWorker,
54
+ 'args' => [@lazy_stash]
55
+ )
56
+ end
57
+
58
+ def update_chewy_indices(object)
59
+ @lazy_stash[object.class.name] ||= []
60
+ @lazy_stash[object.class.name] |= Array.wrap(object.id)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,10 +2,12 @@ require 'chewy/strategy/base'
2
2
  require 'chewy/strategy/bypass'
3
3
  require 'chewy/strategy/urgent'
4
4
  require 'chewy/strategy/atomic'
5
+ require 'chewy/strategy/atomic_no_refresh'
5
6
 
6
7
  begin
7
8
  require 'sidekiq'
8
9
  require 'chewy/strategy/sidekiq'
10
+ require 'chewy/strategy/lazy_sidekiq'
9
11
  rescue LoadError
10
12
  nil
11
13
  end
data/lib/chewy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chewy
2
- VERSION = '7.2.4'.freeze
2
+ VERSION = '7.2.5'.freeze
3
3
  end
data/lib/chewy.rb CHANGED
@@ -50,7 +50,7 @@ require 'chewy/journal'
50
50
  require 'chewy/railtie' if defined?(::Rails::Railtie)
51
51
 
52
52
  ActiveSupport.on_load(:active_record) do
53
- extend Chewy::Index::Observe::ActiveRecordMethods
53
+ include Chewy::Index::Observe::ActiveRecordMethods
54
54
  end
55
55
 
56
56
  module Chewy
data/migration_guide.md CHANGED
@@ -9,7 +9,7 @@ Chewy alongside a matching Elasticsearch version.
9
9
  In order to upgrade Chewy 6/Elasticsearch 6 to Chewy 7/Elasticsearch 7 in the most seamless manner you have to:
10
10
 
11
11
  * Upgrade to the latest 6.x stable releases, namely Chewy 6.0, Elasticsearch 6.8
12
- * Study carefully [Breaking changes in 7.0](https://www.elastic.co/guide/en/elasticsearch/reference/current/breaking-changes-7.0.html), make sure your application conforms.
12
+ * Study carefully [Breaking changes in 7.0](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/breaking-changes-7.0.html), make sure your application conforms.
13
13
  * Run your test suite on Chewy 7.0 / Elasticsearch 7
14
14
  * Run manual tests on Chewy 7.0 / Elasticsearch 7
15
15
  * Upgrade to Chewy 7.0
@@ -0,0 +1,68 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Index::Observe::ActiveRecordMethods do
4
+ describe '.update_index' do
5
+ before { stub_model(:city) }
6
+
7
+ it 'initializes chewy callbacks when first update_index is evaluated' do
8
+ expect(City).to receive(:initialize_chewy_callbacks).once
9
+ City.update_index 'cities', :self
10
+ City.update_index 'countries', -> {}
11
+ end
12
+
13
+ it 'adds chewy callbacks to model' do
14
+ expect(City.chewy_callbacks.count).to eq(0)
15
+
16
+ City.update_index 'cities', :self
17
+ City.update_index 'countries', -> {}
18
+
19
+ expect(City.chewy_callbacks.count).to eq(2)
20
+ end
21
+ end
22
+
23
+ describe 'callbacks' do
24
+ before { stub_model(:city) { update_index 'cities', :self } }
25
+ before { stub_index(:cities) { index_scope City } }
26
+ before { allow(Chewy).to receive(:use_after_commit_callbacks).and_return(use_after_commit_callbacks) }
27
+
28
+ let(:city) do
29
+ Chewy.strategy(:bypass) do
30
+ City.create!
31
+ end
32
+ end
33
+
34
+ shared_examples 'handles callbacks correctly' do
35
+ it 'handles callbacks with strategy for possible lazy evaluation on save!' do
36
+ Chewy.strategy(:urgent) do
37
+ expect(city).to receive(:update_chewy_indices).and_call_original
38
+ expect(Chewy.strategy.current).to receive(:update_chewy_indices).with(city)
39
+ expect(city).not_to receive(:run_chewy_callbacks)
40
+
41
+ city.save!
42
+ end
43
+ end
44
+
45
+ it 'runs callbacks at the moment on destroy' do
46
+ Chewy.strategy(:urgent) do
47
+ expect(city).not_to receive(:update_chewy_indices)
48
+ expect(Chewy.strategy.current).not_to receive(:update_chewy_indices)
49
+ expect(city).to receive(:run_chewy_callbacks)
50
+
51
+ city.destroy
52
+ end
53
+ end
54
+ end
55
+
56
+ context 'when Chewy.use_after_commit_callbacks is true' do
57
+ let(:use_after_commit_callbacks) { true }
58
+
59
+ include_examples 'handles callbacks correctly'
60
+ end
61
+
62
+ context 'when Chewy.use_after_commit_callbacks is false' do
63
+ let(:use_after_commit_callbacks) { false }
64
+
65
+ include_examples 'handles callbacks correctly'
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,139 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Index::Observe::Callback do
4
+ subject(:callback) { described_class.new(executable) }
5
+
6
+ before do
7
+ stub_model(:city) do
8
+ attr_accessor :population
9
+ end
10
+ end
11
+
12
+ let(:city) { City.create!(population: 100) }
13
+
14
+ describe '#call' do
15
+ context 'when executable is has arity 0' do
16
+ let(:executable) { -> { population } }
17
+
18
+ it 'calls exectuable within context' do
19
+ expect(callback.call(city)).to eq(city.population)
20
+ end
21
+ end
22
+
23
+ context 'when executable is has arity 1' do
24
+ let(:executable) { ->(record) { record.population } }
25
+
26
+ it 'calls exectuable within context' do
27
+ expect(callback.call(city)).to eq(city.population)
28
+ end
29
+ end
30
+
31
+ describe 'filters' do
32
+ let(:executable) { ->(_) {} }
33
+
34
+ describe 'if' do
35
+ subject(:callback) { described_class.new(executable, if: filter) }
36
+
37
+ shared_examples 'an if filter' do
38
+ context 'when condition is true' do
39
+ let(:condition) { true }
40
+
41
+ specify do
42
+ expect(executable).to receive(:call).with(city)
43
+
44
+ callback.call(city)
45
+ end
46
+ end
47
+
48
+ context 'when condition is false' do
49
+ let(:condition) { false }
50
+
51
+ specify do
52
+ expect(executable).not_to receive(:call)
53
+
54
+ callback.call(city)
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'when filter is symbol' do
60
+ let(:filter) { :condition }
61
+
62
+ before do
63
+ allow(city).to receive(:condition).and_return(condition)
64
+ end
65
+
66
+ include_examples 'an if filter'
67
+ end
68
+
69
+ context 'when filter is proc' do
70
+ let(:filter) { -> { condition_state } }
71
+
72
+ before do
73
+ allow_any_instance_of(City).to receive(:condition_state).and_return(condition)
74
+ end
75
+
76
+ include_examples 'an if filter'
77
+ end
78
+
79
+ context 'when filter is literal' do
80
+ let(:filter) { condition }
81
+
82
+ include_examples 'an if filter'
83
+ end
84
+ end
85
+
86
+ describe 'unless' do
87
+ subject(:callback) { described_class.new(executable, unless: filter) }
88
+
89
+ shared_examples 'an unless filter' do
90
+ context 'when condition is true' do
91
+ let(:condition) { true }
92
+
93
+ specify do
94
+ expect(executable).not_to receive(:call)
95
+
96
+ callback.call(city)
97
+ end
98
+ end
99
+
100
+ context 'when condition is false' do
101
+ let(:condition) { false }
102
+
103
+ specify do
104
+ expect(executable).to receive(:call).with(city)
105
+
106
+ callback.call(city)
107
+ end
108
+ end
109
+ end
110
+
111
+ context 'when filter is symbol' do
112
+ let(:filter) { :condition }
113
+
114
+ before do
115
+ allow(city).to receive(:condition).and_return(condition)
116
+ end
117
+
118
+ include_examples 'an unless filter'
119
+ end
120
+
121
+ context 'when filter is proc' do
122
+ let(:filter) { -> { condition_state } }
123
+
124
+ before do
125
+ allow_any_instance_of(City).to receive(:condition_state).and_return(condition)
126
+ end
127
+
128
+ include_examples 'an unless filter'
129
+ end
130
+
131
+ context 'when filter is literal' do
132
+ let(:filter) { condition }
133
+
134
+ include_examples 'an unless filter'
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -57,6 +57,9 @@ describe Chewy::Index::Observe do
57
57
  specify { expect { city.save! }.to update_index('cities').and_reindex(city).only }
58
58
  specify { expect { city.save! }.to update_index('countries').and_reindex(country1).only }
59
59
 
60
+ specify { expect { city.destroy }.to update_index('cities') }
61
+ specify { expect { city.destroy }.to update_index('countries').and_reindex(country1).only }
62
+
60
63
  specify { expect { city.update!(country: nil) }.to update_index('cities').and_reindex(city).only }
61
64
  specify { expect { city.update!(country: nil) }.to update_index('countries').and_reindex(country1).only }
62
65
 
@@ -78,9 +81,13 @@ describe Chewy::Index::Observe do
78
81
  specify { expect { country.save! }.to update_index('cities').and_reindex(country.cities).only }
79
82
  specify { expect { country.save! }.to update_index('countries').and_reindex(country).only }
80
83
 
84
+ specify { expect { country.destroy }.to update_index('cities').and_reindex(country.cities).only }
85
+ specify { expect { country.destroy }.to update_index('countries') }
86
+
81
87
  context 'conditional update' do
82
88
  let(:update_condition) { false }
83
89
  specify { expect { country.save! }.not_to update_index('cities') }
90
+ specify { expect { country.destroy }.not_to update_index('cities') }
84
91
  end
85
92
  end
86
93
  end
@@ -97,6 +104,16 @@ describe Chewy::Index::Observe do
97
104
  end
98
105
  end
99
106
  end
107
+
108
+ specify do
109
+ city = Chewy.strategy(:bypass) { City.create! }
110
+
111
+ Chewy.strategy(:urgent) do
112
+ ActiveRecord::Base.transaction do
113
+ expect { city.destroy }.not_to update_index('cities')
114
+ end
115
+ end
116
+ end
100
117
  end
101
118
 
102
119
  context do
@@ -111,6 +128,16 @@ describe Chewy::Index::Observe do
111
128
  end
112
129
  end
113
130
  end
131
+
132
+ specify do
133
+ city = Chewy.strategy(:bypass) { City.create! }
134
+
135
+ Chewy.strategy(:urgent) do
136
+ ActiveRecord::Base.transaction do
137
+ expect { city.destroy }.to update_index('cities')
138
+ end
139
+ end
140
+ end
114
141
  end
115
142
  end
116
143
  end
@@ -48,6 +48,10 @@ describe Chewy::Search do
48
48
  filter { match name: "Name#{index}" }
49
49
  end
50
50
 
51
+ def self.by_rating_with_kwargs(value, options:) # rubocop:disable Lint/UnusedMethodArgument
52
+ filter { match rating: value }
53
+ end
54
+
51
55
  index_scope City
52
56
  field :name, type: 'keyword'
53
57
  field :rating, type: :integer
@@ -114,5 +118,10 @@ describe Chewy::Search do
114
118
  specify { expect(CountriesIndex.by_rating(3).by_name(5).map(&:class)).to eq([CountriesIndex]) }
115
119
  specify { expect(CountriesIndex.order(:name).by_rating(3).map(&:rating)).to eq([3]) }
116
120
  specify { expect(CountriesIndex.order(:name).by_rating(3).map(&:class)).to eq([CountriesIndex]) }
121
+
122
+ specify 'supports keyword arguments' do
123
+ expect(CitiesIndex.by_rating_with_kwargs(3, options: 'blah blah blah').map(&:rating)).to eq([3])
124
+ expect(CitiesIndex.order(:name).by_rating_with_kwargs(3, options: 'blah blah blah').map(&:rating)).to eq([3])
125
+ end
117
126
  end
118
127
  end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe Chewy::Strategy::AtomicNoRefresh, :orm do
4
+ around { |example| Chewy.strategy(:bypass) { example.run } }
5
+
6
+ before do
7
+ stub_model(:country) do
8
+ update_index('countries') { self }
9
+ end
10
+
11
+ stub_index(:countries) do
12
+ index_scope Country
13
+ end
14
+ end
15
+
16
+ let(:country) { Country.create!(name: 'hello', country_code: 'HL') }
17
+ let(:other_country) { Country.create!(name: 'world', country_code: 'WD') }
18
+
19
+ specify do
20
+ expect { [country, other_country].map(&:save!) }
21
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
22
+ .and_reindex(country, other_country).only.no_refresh
23
+ end
24
+
25
+ specify do
26
+ expect { [country, other_country].map(&:destroy) }
27
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
28
+ .and_delete(country, other_country).only.no_refresh
29
+ end
30
+
31
+ context do
32
+ before do
33
+ stub_index(:countries) do
34
+ index_scope Country
35
+ root id: -> { country_code }
36
+ end
37
+ end
38
+
39
+ specify do
40
+ expect { [country, other_country].map(&:save!) }
41
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
42
+ .and_reindex('HL', 'WD').only.no_refresh
43
+ end
44
+
45
+ specify do
46
+ expect { [country, other_country].map(&:destroy) }
47
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
48
+ .and_delete('HL', 'WD').only.no_refresh
49
+ end
50
+
51
+ specify do
52
+ expect do
53
+ country.save!
54
+ other_country.destroy
55
+ end
56
+ .to update_index(CountriesIndex, strategy: :atomic_no_refresh)
57
+ .and_reindex('HL').and_delete('WD').only.no_refresh
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,214 @@
1
+ require 'spec_helper'
2
+
3
+ if defined?(::Sidekiq)
4
+ require 'sidekiq/testing'
5
+
6
+ describe Chewy::Strategy::LazySidekiq do
7
+ around do |example|
8
+ sidekiq_settings = Chewy.settings[:sidekiq]
9
+ Chewy.settings[:sidekiq] = {queue: 'low'}
10
+ Chewy.strategy(:bypass) { example.run }
11
+ Chewy.settings[:sidekiq] = sidekiq_settings
12
+ end
13
+ before { ::Sidekiq::Worker.clear_all }
14
+
15
+ context 'strategy' do
16
+ before do
17
+ stub_model(:city) do
18
+ update_index('cities') { self }
19
+ end
20
+
21
+ stub_index(:cities) do
22
+ index_scope City
23
+ end
24
+ end
25
+
26
+ let(:city) { City.create!(name: 'hello') }
27
+ let(:other_city) { City.create!(name: 'world') }
28
+
29
+ it 'does not update indices synchronously' do
30
+ expect { [city, other_city].map(&:save!) }
31
+ .not_to update_index(CitiesIndex, strategy: :lazy_sidekiq)
32
+ end
33
+
34
+ it 'updates indices asynchronously on record save' do
35
+ expect(::Sidekiq::Client).to receive(:push)
36
+ .with(hash_including(
37
+ 'class' => Chewy::Strategy::LazySidekiq::IndicesUpdateWorker,
38
+ 'queue' => 'low'
39
+ ))
40
+ .and_call_original
41
+ .once
42
+ ::Sidekiq::Testing.inline! do
43
+ expect { [city, other_city].map(&:save!) }
44
+ .to update_index(CitiesIndex, strategy: :lazy_sidekiq)
45
+ .and_reindex(city, other_city).only
46
+ end
47
+ end
48
+
49
+ it 'updates indices asynchronously with falling back to sidekiq strategy on record destroy' do
50
+ expect(::Sidekiq::Client).not_to receive(:push)
51
+ .with(hash_including(
52
+ 'class' => Chewy::Strategy::LazySidekiq::IndicesUpdateWorker,
53
+ 'queue' => 'low'
54
+ ))
55
+ expect(::Sidekiq::Client).to receive(:push)
56
+ .with(hash_including(
57
+ 'class' => Chewy::Strategy::Sidekiq::Worker,
58
+ 'queue' => 'low',
59
+ 'args' => ['CitiesIndex', [city.id, other_city.id]]
60
+ ))
61
+ .and_call_original
62
+ .once
63
+ ::Sidekiq::Testing.inline! do
64
+ expect { [city, other_city].map(&:destroy) }.to update_index(CitiesIndex, strategy: :sidekiq)
65
+ end
66
+ end
67
+
68
+ it 'calls Index#import!' do
69
+ allow(City).to receive(:where).with(id: [city.id, other_city.id]).and_return([city, other_city])
70
+ expect(city).to receive(:run_chewy_callbacks).and_call_original
71
+ expect(other_city).to receive(:run_chewy_callbacks).and_call_original
72
+
73
+ expect do
74
+ ::Sidekiq::Testing.inline! do
75
+ Chewy::Strategy::LazySidekiq::IndicesUpdateWorker.new.perform({'City' => [city.id, other_city.id]})
76
+ end
77
+ end.to update_index(CitiesIndex).and_reindex(city, other_city).only
78
+ end
79
+
80
+ context 'when Chewy.disable_refresh_async is true' do
81
+ before do
82
+ allow(Chewy).to receive(:disable_refresh_async).and_return(true)
83
+ end
84
+
85
+ it 'calls Index#import! with refresh false' do
86
+ allow(City).to receive(:where).with(id: [city.id, other_city.id]).and_return([city, other_city])
87
+ expect(city).to receive(:run_chewy_callbacks).and_call_original
88
+ expect(other_city).to receive(:run_chewy_callbacks).and_call_original
89
+
90
+ expect do
91
+ ::Sidekiq::Testing.inline! do
92
+ Chewy::Strategy::LazySidekiq::IndicesUpdateWorker.new.perform({'City' => [city.id, other_city.id]})
93
+ end
94
+ end.to update_index(CitiesIndex).and_reindex(city, other_city).only.no_refresh
95
+ end
96
+ end
97
+ end
98
+
99
+ context 'integration' do
100
+ around { |example| ::Sidekiq::Testing.inline! { example.run } }
101
+
102
+ let(:update_condition) { true }
103
+
104
+ before do
105
+ city_model
106
+ country_model
107
+
108
+ City.belongs_to :country
109
+ Country.has_many :cities
110
+
111
+ stub_index(:cities) do
112
+ index_scope City
113
+ end
114
+
115
+ stub_index(:countries) do
116
+ index_scope Country
117
+ end
118
+ end
119
+
120
+ context 'state dependent' do
121
+ let(:city_model) do
122
+ stub_model(:city) do
123
+ update_index(-> { 'cities' }, :self)
124
+ update_index('countries') { changes['country_id'] || previous_changes['country_id'] || country }
125
+ end
126
+ end
127
+
128
+ let(:country_model) do
129
+ stub_model(:country) do
130
+ update_index('cities', if: -> { state_dependent_update_condition }) { cities }
131
+ update_index(-> { 'countries' }, :self)
132
+ attr_accessor :state_dependent_update_condition
133
+ end
134
+ end
135
+
136
+ context 'city updates' do
137
+ let!(:country1) { Country.create!(id: 1) }
138
+ let!(:country2) { Country.create!(id: 2) }
139
+ let!(:city) { City.create!(id: 1, country: country1) }
140
+
141
+ it 'does not update index of removed entity because model state on the moment of save cannot be fetched' do
142
+ expect { city.update!(country: nil) }.not_to update_index('countries', strategy: :lazy_sidekiq)
143
+ end
144
+ it 'does not update index of removed entity because model state on the moment of save cannot be fetched' do
145
+ expect { city.update!(country: country2) }.to update_index('countries', strategy: :lazy_sidekiq).and_reindex(country2).only
146
+ end
147
+ end
148
+
149
+ context 'country updates' do
150
+ let!(:country) do
151
+ cities = Array.new(2) { |i| City.create!(id: i) }
152
+ Country.create!(id: 1, cities: cities, state_dependent_update_condition: update_condition)
153
+ end
154
+
155
+ it 'does not update index because state of attribute cannot be fetched' do
156
+ expect { country.save! }.not_to update_index('cities', strategy: :lazy_sidekiq)
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'state independent' do
162
+ let(:city_model) do
163
+ stub_model(:city) do
164
+ update_index(-> { 'cities' }, :self)
165
+ update_index('countries') { country }
166
+ end
167
+ end
168
+
169
+ let(:country_model) do
170
+ stub_model(:country) do
171
+ update_index('cities', if: -> { state_independent_update_condition }) { cities }
172
+ update_index(-> { 'countries' }, :self)
173
+ end
174
+ end
175
+
176
+ before do
177
+ allow_any_instance_of(Country).to receive(:state_independent_update_condition).and_return(update_condition)
178
+ end
179
+
180
+ context 'when city updates' do
181
+ let!(:country1) { Country.create!(id: 1) }
182
+ let!(:country2) { Country.create!(id: 2) }
183
+ let!(:city) { City.create!(id: 1, country: country1) }
184
+
185
+ specify { expect { city.save! }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(city).only }
186
+ specify { expect { city.save! }.to update_index('countries', strategy: :lazy_sidekiq).and_reindex(country1).only }
187
+
188
+ specify { expect { city.destroy }.not_to update_index('cities').and_reindex(city).only }
189
+ specify { expect { city.destroy }.to update_index('countries', strategy: :sidekiq).and_reindex(country1).only }
190
+
191
+ specify { expect { city.update!(country: nil) }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(city).only }
192
+ specify { expect { city.update!(country: country2) }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(city).only }
193
+ end
194
+
195
+ context 'when country updates' do
196
+ let!(:country) do
197
+ cities = Array.new(2) { |i| City.create!(id: i) }
198
+ Country.create!(id: 1, cities: cities)
199
+ end
200
+ specify { expect { country.save! }.to update_index('cities', strategy: :lazy_sidekiq).and_reindex(country.cities).only }
201
+ specify { expect { country.save! }.to update_index('countries', strategy: :lazy_sidekiq).and_reindex(country).only }
202
+
203
+ specify { expect { country.destroy }.to update_index('cities', strategy: :sidekiq).and_reindex(country.cities).only }
204
+ specify { expect { country.destroy }.not_to update_index('countries').and_reindex(country).only }
205
+
206
+ context 'when update condition is false' do
207
+ let(:update_condition) { false }
208
+ specify { expect { country.save! }.not_to update_index('cities', strategy: :lazy_sidekiq) }
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chewy
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.4
4
+ version: 7.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toptal, LLC
8
8
  - pyromaniac
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-02-03 00:00:00.000000000 Z
12
+ date: 2022-03-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: database_cleaner
@@ -265,6 +265,8 @@ files:
265
265
  - lib/chewy/index/import/routine.rb
266
266
  - lib/chewy/index/mapping.rb
267
267
  - lib/chewy/index/observe.rb
268
+ - lib/chewy/index/observe/active_record_methods.rb
269
+ - lib/chewy/index/observe/callback.rb
268
270
  - lib/chewy/index/settings.rb
269
271
  - lib/chewy/index/specification.rb
270
272
  - lib/chewy/index/syncer.rb
@@ -337,8 +339,10 @@ files:
337
339
  - lib/chewy/strategy.rb
338
340
  - lib/chewy/strategy/active_job.rb
339
341
  - lib/chewy/strategy/atomic.rb
342
+ - lib/chewy/strategy/atomic_no_refresh.rb
340
343
  - lib/chewy/strategy/base.rb
341
344
  - lib/chewy/strategy/bypass.rb
345
+ - lib/chewy/strategy/lazy_sidekiq.rb
342
346
  - lib/chewy/strategy/sidekiq.rb
343
347
  - lib/chewy/strategy/urgent.rb
344
348
  - lib/chewy/version.rb
@@ -360,6 +364,8 @@ files:
360
364
  - spec/chewy/index/import/routine_spec.rb
361
365
  - spec/chewy/index/import_spec.rb
362
366
  - spec/chewy/index/mapping_spec.rb
367
+ - spec/chewy/index/observe/active_record_methods_spec.rb
368
+ - spec/chewy/index/observe/callback_spec.rb
363
369
  - spec/chewy/index/observe_spec.rb
364
370
  - spec/chewy/index/settings_spec.rb
365
371
  - spec/chewy/index/specification_spec.rb
@@ -426,7 +432,9 @@ files:
426
432
  - spec/chewy/search_spec.rb
427
433
  - spec/chewy/stash_spec.rb
428
434
  - spec/chewy/strategy/active_job_spec.rb
435
+ - spec/chewy/strategy/atomic_no_refresh_spec.rb
429
436
  - spec/chewy/strategy/atomic_spec.rb
437
+ - spec/chewy/strategy/lazy_sidekiq_spec.rb
430
438
  - spec/chewy/strategy/sidekiq_spec.rb
431
439
  - spec/chewy/strategy_spec.rb
432
440
  - spec/chewy_spec.rb
@@ -438,7 +446,7 @@ homepage: https://github.com/toptal/chewy
438
446
  licenses:
439
447
  - MIT
440
448
  metadata: {}
441
- post_install_message:
449
+ post_install_message:
442
450
  rdoc_options: []
443
451
  require_paths:
444
452
  - lib
@@ -453,8 +461,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
453
461
  - !ruby/object:Gem::Version
454
462
  version: '0'
455
463
  requirements: []
456
- rubygems_version: 3.2.32
457
- signing_key:
464
+ rubygems_version: 3.1.6
465
+ signing_key:
458
466
  specification_version: 4
459
467
  summary: Elasticsearch ODM client wrapper
460
468
  test_files:
@@ -472,6 +480,8 @@ test_files:
472
480
  - spec/chewy/index/import/routine_spec.rb
473
481
  - spec/chewy/index/import_spec.rb
474
482
  - spec/chewy/index/mapping_spec.rb
483
+ - spec/chewy/index/observe/active_record_methods_spec.rb
484
+ - spec/chewy/index/observe/callback_spec.rb
475
485
  - spec/chewy/index/observe_spec.rb
476
486
  - spec/chewy/index/settings_spec.rb
477
487
  - spec/chewy/index/specification_spec.rb
@@ -538,7 +548,9 @@ test_files:
538
548
  - spec/chewy/search_spec.rb
539
549
  - spec/chewy/stash_spec.rb
540
550
  - spec/chewy/strategy/active_job_spec.rb
551
+ - spec/chewy/strategy/atomic_no_refresh_spec.rb
541
552
  - spec/chewy/strategy/atomic_spec.rb
553
+ - spec/chewy/strategy/lazy_sidekiq_spec.rb
542
554
  - spec/chewy/strategy/sidekiq_spec.rb
543
555
  - spec/chewy/strategy_spec.rb
544
556
  - spec/chewy_spec.rb