mongoid 6.3.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/lib/config/locales/en.yml +21 -0
  5. data/lib/mongoid.rb +1 -1
  6. data/lib/mongoid/clients.rb +2 -0
  7. data/lib/mongoid/clients/sessions.rb +113 -0
  8. data/lib/mongoid/clients/storage_options.rb +1 -0
  9. data/lib/mongoid/contextual/aggregable/mongo.rb +1 -1
  10. data/lib/mongoid/contextual/map_reduce.rb +6 -2
  11. data/lib/mongoid/contextual/memory.rb +7 -2
  12. data/lib/mongoid/contextual/mongo.rb +11 -2
  13. data/lib/mongoid/criteria.rb +1 -0
  14. data/lib/mongoid/criteria/queryable/mergeable.rb +3 -1
  15. data/lib/mongoid/errors.rb +1 -0
  16. data/lib/mongoid/errors/invalid_session_use.rb +24 -0
  17. data/lib/mongoid/indexable.rb +4 -4
  18. data/lib/mongoid/persistable.rb +1 -1
  19. data/lib/mongoid/persistable/creatable.rb +4 -2
  20. data/lib/mongoid/persistable/deletable.rb +4 -2
  21. data/lib/mongoid/persistable/destroyable.rb +1 -5
  22. data/lib/mongoid/persistable/updatable.rb +2 -2
  23. data/lib/mongoid/persistable/upsertable.rb +2 -1
  24. data/lib/mongoid/relations/embedded/batchable.rb +10 -4
  25. data/lib/mongoid/relations/many.rb +4 -0
  26. data/lib/mongoid/relations/referenced/many.rb +1 -1
  27. data/lib/mongoid/relations/touchable.rb +1 -1
  28. data/lib/mongoid/reloadable.rb +1 -1
  29. data/lib/mongoid/tasks/database.rb +3 -2
  30. data/lib/mongoid/threaded.rb +38 -0
  31. data/lib/mongoid/version.rb +1 -1
  32. data/spec/mongoid/attributes/nested_spec.rb +4 -0
  33. data/spec/mongoid/clients/sessions_spec.rb +325 -0
  34. data/spec/mongoid/contextual/mongo_spec.rb +38 -0
  35. data/spec/mongoid/criteria/queryable/selectable_spec.rb +32 -3
  36. data/spec/mongoid/interceptable_spec.rb +1 -1
  37. data/spec/mongoid/persistable/deletable_spec.rb +19 -0
  38. data/spec/mongoid/persistable/destroyable_spec.rb +19 -0
  39. data/spec/mongoid/persistable_spec.rb +16 -16
  40. data/spec/spec_helper.rb +70 -0
  41. metadata +17 -7
  42. metadata.gz.sig +0 -0
@@ -21,7 +21,8 @@ module Mongoid
21
21
  # @since 3.0.0
22
22
  def upsert(options = {})
23
23
  prepare_upsert(options) do
24
- collection.find(atomic_selector).update_one(as_attributes, upsert: true)
24
+ collection.find(atomic_selector).update_one(
25
+ as_attributes, upsert: true, session: session)
25
26
  end
26
27
  end
27
28
 
@@ -38,7 +38,8 @@ module Mongoid
38
38
  pre_process_batch_remove(docs, :delete)
39
39
  unless docs.empty?
40
40
  collection.find(selector).update_one(
41
- positionally(selector, "$unset" => { path => true })
41
+ positionally(selector, "$unset" => { path => true }),
42
+ session: session
42
43
  )
43
44
  post_process_batch_remove(docs, :delete)
44
45
  end
@@ -58,7 +59,8 @@ module Mongoid
58
59
  removals = pre_process_batch_remove(docs, method)
59
60
  if !docs.empty?
60
61
  collection.find(selector).update_one(
61
- positionally(selector, "$pullAll" => { path => removals })
62
+ positionally(selector, "$pullAll" => { path => removals }),
63
+ session: session
62
64
  )
63
65
  post_process_batch_remove(docs, method)
64
66
  end
@@ -133,7 +135,9 @@ module Mongoid
133
135
  inserts = pre_process_batch_insert(docs)
134
136
  if insertable?
135
137
  collection.find(selector).update_one(
136
- positionally(selector, '$set' => { path => inserts }))
138
+ positionally(selector, '$set' => { path => inserts }),
139
+ session: session
140
+ )
137
141
  post_process_batch_insert(docs)
138
142
  end
139
143
  inserts
@@ -156,7 +160,9 @@ module Mongoid
156
160
  pushes = pre_process_batch_insert(docs)
157
161
  if insertable?
158
162
  collection.find(selector).update_one(
159
- positionally(selector, '$push' => { path => { '$each' => pushes } }))
163
+ positionally(selector, '$push' => { path => { '$each' => pushes } }),
164
+ session: session
165
+ )
160
166
  post_process_batch_insert(docs)
161
167
  end
162
168
  pushes
@@ -213,6 +213,10 @@ module Mongoid
213
213
 
214
214
  private
215
215
 
216
+ def session
217
+ base.send(:session)
218
+ end
219
+
216
220
  # Find the first object given the supplied attributes or create/initialize it.
217
221
  #
218
222
  # @example Find or create|initialize.
@@ -477,7 +477,7 @@ module Mongoid
477
477
  # @since 3.0.0
478
478
  def persist_delayed(docs, inserts)
479
479
  unless docs.empty?
480
- collection.insert_many(inserts)
480
+ collection.insert_many(inserts, session: session)
481
481
  docs.each do |doc|
482
482
  doc.new_record = false
483
483
  doc.run_after_callbacks(:create, :save)
@@ -31,7 +31,7 @@ module Mongoid
31
31
  touches = touch_atomic_updates(field)
32
32
  unless touches["$set"].blank?
33
33
  selector = atomic_selector
34
- _root.collection.find(selector).update_one(positionally(selector, touches))
34
+ _root.collection.find(selector).update_one(positionally(selector, touches), session: session)
35
35
  end
36
36
  run_callbacks(:touch)
37
37
  true
@@ -58,7 +58,7 @@ module Mongoid
58
58
  #
59
59
  # @since 2.3.2
60
60
  def reload_root_document
61
- {}.merge(collection.find(_id: _id).read(mode: :primary).first || {})
61
+ {}.merge(collection.find({ _id: _id }, session: session).read(mode: :primary).first || {})
62
62
  end
63
63
 
64
64
  # Reload the embedded document.
@@ -44,7 +44,7 @@ module Mongoid
44
44
  models.each do |model|
45
45
  unless model.embedded?
46
46
  begin
47
- model.collection.indexes.each do |index|
47
+ model.collection.indexes(session: model.send(:session)).each do |index|
48
48
  # ignore default index
49
49
  unless index['name'] == '_id_'
50
50
  key = index['key'].symbolize_keys
@@ -77,7 +77,7 @@ module Mongoid
77
77
  indexes.each do |index|
78
78
  key = index['key'].symbolize_keys
79
79
  collection = model.collection
80
- collection.indexes.drop_one(key)
80
+ collection.indexes(session: model.send(:session)).drop_one(key)
81
81
  logger.info(
82
82
  "MONGOID: Removed index '#{index['name']}' on collection " +
83
83
  "'#{collection.name}' in database '#{collection.database.name}'."
@@ -107,6 +107,7 @@ module Mongoid
107
107
  end
108
108
 
109
109
  private
110
+
110
111
  def logger
111
112
  Mongoid.logger
112
113
  end
@@ -325,5 +325,43 @@ module Mongoid
325
325
  def validations_for(klass)
326
326
  validations[klass] ||= []
327
327
  end
328
+
329
+ # Cache a session for this thread.
330
+ #
331
+ # @example Save a session for this thread.
332
+ # Threaded.set_session(session)
333
+ #
334
+ # @param [ Mongo::Session ] session The session to save.
335
+ #
336
+ # @since 6.4.0
337
+ def set_session(session)
338
+ Thread.current[:session] = session
339
+ end
340
+
341
+ # Get the cached session for this thread.
342
+ #
343
+ # @example Get the session for this thread.
344
+ # Threaded.get_session
345
+ #
346
+ # @return [ Mongo::Session, nil ] The session cached on this thread or nil.
347
+ #
348
+ # @since 6.4.0
349
+ def get_session
350
+ Thread.current[:session]
351
+ end
352
+
353
+ # Clear the cached session for this thread.
354
+ #
355
+ # @example Clear this thread's session.
356
+ # Threaded.clear_session
357
+ #
358
+ # @return [ nil ]
359
+ #
360
+ # @since 6.4.0
361
+ def clear_session
362
+ session = get_session
363
+ session.end_session if session
364
+ Thread.current[:session] = nil
365
+ end
328
366
  end
329
367
  end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Mongoid
3
- VERSION = "6.3.0"
3
+ VERSION = "6.4.0"
4
4
  end
@@ -205,6 +205,10 @@ describe Mongoid::Attributes::Nested do
205
205
  Post.accepts_nested_attributes_for :person
206
206
  end
207
207
 
208
+ after do
209
+ Post.reset_callbacks(:save)
210
+ end
211
+
208
212
  let(:post) do
209
213
  Post.new(person_attributes: { title: "Sir" })
210
214
  end
@@ -0,0 +1,325 @@
1
+ require "spec_helper"
2
+
3
+ describe Mongoid::Clients::Sessions do
4
+
5
+ before(:all) do
6
+ CONFIG[:clients][:other] = CONFIG[:clients][:default].dup
7
+ CONFIG[:clients][:other][:database] = 'other'
8
+ Mongoid::Clients.clients.values.each(&:close)
9
+ Mongoid::Config.send(:clients=, CONFIG[:clients])
10
+ Mongoid::Clients.with_name(:other).subscribe(Mongo::Monitoring::COMMAND, EventSubscriber.new)
11
+ end
12
+
13
+ after(:all) do
14
+ Mongoid::Clients.with_name(:other).close
15
+ Mongoid::Clients.clients.delete(:other)
16
+ end
17
+
18
+ let(:subscriber) do
19
+ Mongoid::Clients.with_name(:other).instance_variable_get(:@monitoring).subscribers['Command'].find do |s|
20
+ s.is_a?(EventSubscriber)
21
+ end
22
+ end
23
+
24
+ let(:insert_events) do
25
+ subscriber.started_events.select { |event| event.command_name == :insert }
26
+ end
27
+
28
+ let(:update_events) do
29
+ subscriber.started_events.select { |event| event.command_name == :update }
30
+ end
31
+
32
+ context 'when a session is used on a model class' do
33
+
34
+ context 'when sessions are supported', if: sessions_supported? do
35
+
36
+ around do |example|
37
+ Mongoid::Clients.with_name(:other).database.collections.each(&:drop)
38
+ subscriber.clear_events!
39
+ Person.with(client: :other) do
40
+ example.run
41
+ end
42
+ Mongoid::Clients.with_name(:other).database.collections.each(&:drop)
43
+ end
44
+
45
+ context 'when another thread is started' do
46
+
47
+ let!(:last_use_diff) do
48
+ Person.with_session do |session|
49
+ Person.create
50
+ Person.create
51
+ last_use = session.instance_variable_get(:@server_session).last_use
52
+ Thread.new { Person.create }.value
53
+ session.instance_variable_get(:@server_session).last_use - last_use
54
+ end
55
+ end
56
+
57
+ it 'does not use the session for that thread' do
58
+ expect(Person.count).to be(2)
59
+ expect(Person.with(client: :default) { Person.count }).to be(1)
60
+ lsids_sent = insert_events.collect { |event| event.command['lsid'] }
61
+ expect(lsids_sent.size).to eq(2)
62
+ expect(lsids_sent.uniq.size).to eq(1)
63
+ expect(last_use_diff).to eq(0)
64
+ end
65
+ end
66
+
67
+ context 'when the operations in the session block are all on the class' do
68
+
69
+ before do
70
+ Person.with_session do
71
+ Person.create
72
+ Person.create
73
+ end
74
+ end
75
+
76
+ it 'uses a single session id for all operations on the class' do
77
+ expect(Person.count).to be(2)
78
+ lsids_sent = insert_events.collect { |event| event.command['lsid'] }
79
+ expect(lsids_sent.size).to eq(2)
80
+ expect(lsids_sent.uniq.size).to eq(1)
81
+ end
82
+ end
83
+
84
+ context 'when the operations in the session block are also on another class' do
85
+
86
+ context 'when the other class uses the same client' do
87
+
88
+ before do
89
+ Post.with(client: :other) do
90
+ Person.with_session do
91
+ Person.create
92
+ Person.create
93
+ Post.create
94
+ end
95
+ end
96
+ end
97
+
98
+ it 'uses a single session id for all operations on the class' do
99
+ expect(Post.with(client: :other) { |klass| klass.count }).to be(1)
100
+ lsids_sent = insert_events.collect { |event| event.command['lsid'] }
101
+ expect(lsids_sent.size).to eq(3)
102
+ expect(lsids_sent.uniq.size).to eq(1)
103
+ end
104
+ end
105
+
106
+ context 'when the other class uses a different client' do
107
+
108
+ let!(:error) do
109
+ e = nil
110
+ begin
111
+ Person.with_session do
112
+ Person.create
113
+ Person.create
114
+ Post.create
115
+ end
116
+ rescue => ex
117
+ e = ex
118
+ end
119
+ e
120
+ end
121
+
122
+ it 'raises an error' do
123
+ expect(error).to be_a(Mongoid::Errors::InvalidSessionUse)
124
+ end
125
+
126
+ it 'uses a single session id for all operations on the class' do
127
+ expect(Person.count).to be(2)
128
+ lsids_sent = insert_events.collect { |event| event.command['lsid'] }
129
+ expect(lsids_sent.size).to eq(2)
130
+ expect(lsids_sent.uniq.size).to eq(1)
131
+ end
132
+ end
133
+
134
+ context 'when sessions are nested' do
135
+
136
+ let!(:error) do
137
+ e = nil
138
+ begin
139
+ Person.with_session do
140
+ Person.with_session do
141
+ Person.create
142
+ Post.create
143
+ end
144
+ end
145
+ rescue => ex
146
+ e = ex
147
+ end
148
+ e
149
+ end
150
+
151
+ it 'raises an error' do
152
+ expect(error).to be_a(Mongoid::Errors::InvalidSessionUse)
153
+ end
154
+
155
+ it 'does not execute any operations' do
156
+ expect(Person.count).to be(0)
157
+ expect(insert_events).to be_empty
158
+ end
159
+ end
160
+ end
161
+ end
162
+
163
+ context 'when sessions are not supported', unless: sessions_supported? do
164
+
165
+ let!(:error) do
166
+ e = nil
167
+ begin
168
+ Person.with_session {}
169
+ rescue => ex
170
+ e = ex
171
+ end
172
+ e
173
+ end
174
+
175
+ it 'raises a sessions not supported error' do
176
+ expect(error).to be_a(Mongoid::Errors::InvalidSessionUse)
177
+ expect(error.message).to include('not supported')
178
+ end
179
+ end
180
+ end
181
+
182
+ context 'when a session is used on a model instance' do
183
+
184
+ let!(:person) do
185
+ Person.with(client: :other) do |klass|
186
+ klass.create
187
+ end
188
+ end
189
+
190
+ context 'when sessions are supported', if: sessions_supported? do
191
+
192
+ around do |example|
193
+ Mongoid::Clients.with_name(:other).database.collections.each(&:drop)
194
+ subscriber.clear_events!
195
+ person.with(client: :other) do
196
+ example.run
197
+ end
198
+ Mongoid::Clients.with_name(:other).database.collections.each(&:drop)
199
+ end
200
+
201
+ context 'when the operations in the session block are all on the instance' do
202
+
203
+ before do
204
+ person.with_session do
205
+ person.username = 'Emily'
206
+ person.save
207
+ person.age = 80
208
+ person.save
209
+ end
210
+ end
211
+
212
+ it 'uses a single session id for all operations on the class' do
213
+ expect(person.reload.username).to eq('Emily')
214
+ expect(person.reload.age).to eq(80)
215
+ lsids_sent = update_events.collect { |event| event.command['lsid'] }
216
+ expect(lsids_sent.size).to eq(2)
217
+ expect(lsids_sent.uniq.size).to eq(1)
218
+ end
219
+ end
220
+
221
+ context 'when the operations in the session block are also on another class' do
222
+
223
+ context 'when the other class uses the same client' do
224
+
225
+ before do
226
+ Post.with(client: :other) do
227
+ person.with_session do
228
+ person.username = 'Emily'
229
+ person.save
230
+ person.posts << Post.create
231
+ end
232
+ end
233
+ end
234
+
235
+ it 'uses a single session id for all operations on the class' do
236
+ expect(person.reload.username).to eq('Emily')
237
+ expect(Post.with(client: :other) { Post.count }).to be(1)
238
+ update_lsids_sent = update_events.collect { |event| event.command['lsid'] }
239
+ expect(update_lsids_sent.size).to eq(3) # person update, counter cache, post assignment
240
+ expect(update_lsids_sent.uniq.size).to eq(1) # person update, counter cache, post assignment
241
+ insert_lsids_sent = insert_events.collect { |event| event.command['lsid'] }
242
+ expect(insert_lsids_sent.size).to eq(2)
243
+ expect(insert_lsids_sent.uniq.size).to eq(1)
244
+ expect(update_lsids_sent.uniq).to eq(insert_lsids_sent.uniq)
245
+ end
246
+ end
247
+
248
+ context 'when the other class uses a different client' do
249
+
250
+ let!(:error) do
251
+ e = nil
252
+ begin
253
+ person.with_session do
254
+ person.username = 'Emily'
255
+ person.save
256
+ person.posts << Post.create
257
+ end
258
+ rescue => ex
259
+ e = ex
260
+ end
261
+ e
262
+ end
263
+
264
+ it 'raises an error' do
265
+ expect(error).to be_a(Mongoid::Errors::InvalidSessionUse)
266
+ end
267
+
268
+ it 'uses a single session id for all operations on the class' do
269
+ expect(person.reload.username).to eq('Emily')
270
+ expect(Post.count).to be(0)
271
+ update_lsids_sent = update_events.collect { |event| event.command['lsid'] }
272
+ expect(update_lsids_sent.size).to eq(1)
273
+ end
274
+ end
275
+
276
+ context 'when sessions are nested' do
277
+
278
+ let!(:error) do
279
+ e = nil
280
+ begin
281
+ person.with_session do
282
+ person.with_session do
283
+ person.username = 'Emily'
284
+ person.save
285
+ person.posts << Post.create
286
+ end
287
+ end
288
+ rescue => ex
289
+ e = ex
290
+ end
291
+ e
292
+ end
293
+
294
+ it 'raises an error' do
295
+ expect(error).to be_a(Mongoid::Errors::InvalidSessionUse)
296
+ end
297
+
298
+ it 'does not execute any operations' do
299
+ expect(person.reload.username).not_to eq('Emily')
300
+ expect(Post.count).to be(0)
301
+ expect(update_events).to be_empty
302
+ end
303
+ end
304
+ end
305
+ end
306
+
307
+ context 'when sessions are not supported', unless: sessions_supported? do
308
+
309
+ let!(:error) do
310
+ e = nil
311
+ begin
312
+ person.with_session {}
313
+ rescue => ex
314
+ e = ex
315
+ end
316
+ e
317
+ end
318
+
319
+ it 'raises a sessions not supported error' do
320
+ expect(error).to be_a(Mongoid::Errors::InvalidSessionUse)
321
+ expect(error.message).to include('not supported')
322
+ end
323
+ end
324
+ end
325
+ end