chewy 7.2.4 → 7.2.5

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: 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