mongoid 7.6.1 → 7.6.2

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +26 -0
  3. data/lib/mongoid/association/depending.rb +8 -4
  4. data/lib/mongoid/association/nested/many.rb +38 -3
  5. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  6. data/lib/mongoid/association/referenced/has_many/proxy.rb +102 -3
  7. data/lib/mongoid/config.rb +62 -0
  8. data/lib/mongoid/contextual/aggregable/memory.rb +8 -2
  9. data/lib/mongoid/contextual/memory.rb +19 -8
  10. data/lib/mongoid/criteria/queryable/mergeable.rb +12 -0
  11. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  12. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  13. data/lib/mongoid/errors.rb +1 -0
  14. data/lib/mongoid/field_readable.rb +70 -0
  15. data/lib/mongoid/matchable.rb +6 -1
  16. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +3 -5
  17. data/lib/mongoid/matcher/regex.rb +8 -9
  18. data/lib/mongoid/matcher/regexp_budget.rb +389 -0
  19. data/lib/mongoid/matcher.rb +1 -0
  20. data/lib/mongoid/threaded.rb +3 -0
  21. data/lib/mongoid/version.rb +1 -1
  22. data/lib/mongoid/warnings.rb +1 -0
  23. data/spec/integration/app_spec.rb +8 -0
  24. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  25. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  26. data/spec/integration/query_operator_guard_spec.rb +89 -0
  27. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  28. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  29. data/spec/mongoid/attributes/nested_spec.rb +202 -8
  30. data/spec/mongoid/contextual/aggregable/memory_spec.rb +98 -0
  31. data/spec/mongoid/contextual/memory_spec.rb +196 -0
  32. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  33. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  34. data/spec/mongoid/criteria_spec.rb +2 -0
  35. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  36. data/spec/shared/lib/mrss/cluster_config.rb +5 -40
  37. data/spec/shared/lib/mrss/constraints.rb +7 -46
  38. data/spec/shared/lib/mrss/docker_runner.rb +10 -21
  39. data/spec/shared/lib/mrss/eg_config_utils.rb +0 -31
  40. data/spec/shared/lib/mrss/release/candidate.rb +18 -12
  41. data/spec/shared/lib/mrss/server_version_registry.rb +1 -5
  42. data/spec/shared/share/Dockerfile.erb +6 -8
  43. data/spec/shared/shlib/server.sh +0 -50
  44. data/spec/shared/shlib/set_env.sh +4 -10
  45. metadata +11 -2
@@ -0,0 +1,570 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark'
4
+ require 'spec_helper'
5
+
6
+ describe Mongoid::Matcher::RegexpBudget do
7
+ # Costly but bounded on every supported Ruby: unanchored, so every branch is
8
+ # tried at every position, and with no nested quantifier there is nothing to
9
+ # backtrack exponentially. What it costs still varies a lot by engine, about
10
+ # 6ms a match on MRI against 110ms on JRuby, so anything asserting on that
11
+ # cost calibrates rather than hard-coding it.
12
+ #
13
+ # A pattern built out of nested quantifiers would be wrong here. Those are
14
+ # merely slow only where Ruby memoizes matching, which arrived in 3.2; on
15
+ # 2.7 through 3.1 they backtrack exponentially and never finish.
16
+ let(:slow_pattern) { BSON::Regexp::Raw.new("(?:#{(1..300).map { |i| "a#{i}" }.join('|')})Z") }
17
+ let(:slow_subject) { 'a' * 5_000 }
18
+
19
+ # Backtracks exponentially: the backreference disables memoization, so this
20
+ # runs orders of magnitude longer than the limit and has to be interrupted.
21
+ #
22
+ # The subject is kept short enough that the match still finishes on its own,
23
+ # in roughly 4.5 seconds against a 0.2 second limit. That way a Ruby whose
24
+ # engine does not check for interrupts mid-match fails these examples
25
+ # visibly instead of hanging the run.
26
+ let(:catastrophic_pattern) { BSON::Regexp::Raw.new('^(a+)+\1?$') }
27
+ let(:catastrophic_subject) { "#{'a' * 28}X" }
28
+
29
+ let(:cheap_pattern) { BSON::Regexp::Raw.new('\Aabc\z') }
30
+
31
+ # A budget is only opened for a selector that carries a pattern, so anything
32
+ # exercising one has to hand .open something to bound.
33
+ let(:regexp_selector) { { 'name' => /\Aabc\z/ } }
34
+
35
+ describe '.open' do
36
+ context 'when no limit is configured' do
37
+ config_override :in_memory_regexp_time_limit, nil
38
+
39
+ it 'does not establish a budget' do
40
+ described_class.open(regexp_selector) do
41
+ expect(described_class.remaining).to be_nil
42
+ end
43
+ end
44
+ end
45
+
46
+ context 'when the configured limit is zero' do
47
+ config_override :in_memory_regexp_time_limit, 0
48
+
49
+ # Zero is a common way to spell "disabled". Taken literally it would mean
50
+ # a budget spent before the first match, so every in-memory query
51
+ # carrying a pattern would raise rather than run.
52
+ it 'does not establish a budget' do
53
+ described_class.open(regexp_selector) do
54
+ expect(described_class.remaining).to be_nil
55
+ end
56
+ end
57
+
58
+ it 'still performs matches' do
59
+ result = described_class.open(regexp_selector) do
60
+ described_class.match?('abc', cheap_pattern)
61
+ end
62
+
63
+ expect(result).to eq(0)
64
+ end
65
+ end
66
+
67
+ context 'when a limit is configured' do
68
+ config_override :in_memory_regexp_time_limit, 5.0
69
+
70
+ it 'establishes the budget for the duration of the block' do
71
+ described_class.open(regexp_selector) do
72
+ expect(described_class.remaining).to be_within(0.01).of(5.0)
73
+ end
74
+ end
75
+
76
+ it 'returns the value of the block' do
77
+ expect(described_class.open(regexp_selector) { :result }).to eq(:result)
78
+ end
79
+
80
+ it 'clears the budget when the block returns' do
81
+ described_class.open(regexp_selector) { nil }
82
+ expect(described_class.remaining).to be_nil
83
+ end
84
+
85
+ it 'clears the budget when the block raises' do
86
+ expect { described_class.open(regexp_selector) { raise 'boom' } }.to raise_error('boom')
87
+ expect(described_class.remaining).to be_nil
88
+ end
89
+
90
+ it 'joins the enclosing budget rather than starting a new one' do
91
+ described_class.open(regexp_selector) do
92
+ described_class.match?(slow_subject, slow_pattern)
93
+ spent = 5.0 - described_class.remaining
94
+ expect(spent).to be > 0
95
+
96
+ described_class.open(regexp_selector) do
97
+ expect(described_class.remaining).to be_within(0.01).of(5.0 - spent)
98
+ end
99
+ end
100
+ end
101
+
102
+ context 'when the selector carries no regular expression' do
103
+ it 'does not establish a budget' do
104
+ described_class.open('name' => 'abc') do
105
+ expect(described_class.remaining).to be_nil
106
+ end
107
+ end
108
+
109
+ it 'does not put a deadline on the block' do
110
+ # The limit bounds regular expressions, not in-memory work at large.
111
+ # On the Timeout path a scope with nothing to bound used to fail any
112
+ # slow scan -- an embedded association of any size, say -- with an
113
+ # error about regular expressions.
114
+ stub_const('Mongoid::Matcher::RegexpBudget::PER_REGEXP_TIMEOUT', false)
115
+ Mongoid::Config.in_memory_regexp_time_limit = 0.2
116
+
117
+ expect do
118
+ described_class.open('name' => 'abc') { sleep 0.3 }
119
+ end.not_to raise_error
120
+ end
121
+
122
+ it 'keeps a nested scope from scanning the selector again' do
123
+ described_class.open('name' => 'abc') do
124
+ expect(Thread.current.key?(Mongoid::Threaded::REGEXP_BUDGET_KEY)).to be(true)
125
+ end
126
+ end
127
+ end
128
+
129
+ context 'when the selector carries a string $regex' do
130
+ # FieldExpression turns it into a pattern at match time, so it needs
131
+ # bounding just as much as a Regexp written out in the selector does.
132
+ it 'establishes a budget' do
133
+ described_class.open('name' => { '$regex' => 'abc' }) do
134
+ expect(described_class.remaining).to be_within(0.01).of(5.0)
135
+ end
136
+ end
137
+ end
138
+
139
+ context 'when the selector nests a regular expression' do
140
+ it 'establishes a budget' do
141
+ described_class.open('$or' => [ { 'name' => 'abc' }, { 'name' => /abc/ } ]) do
142
+ expect(described_class.remaining).to be_within(0.01).of(5.0)
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ describe '.limit_for' do
150
+ context 'when a limit is configured' do
151
+ config_override :in_memory_regexp_time_limit, 5.0
152
+
153
+ it 'is the limit for a selector carrying a regular expression' do
154
+ expect(described_class.limit_for(regexp_selector)).to eq(5.0)
155
+ end
156
+
157
+ it 'is the limit for a selector carrying a string $regex' do
158
+ expect(described_class.limit_for('name' => { '$regex' => 'abc' })).to eq(5.0)
159
+ end
160
+
161
+ it 'is the limit for a selector nesting a regular expression' do
162
+ expect(described_class.limit_for('$or' => [ { 'name' => 'abc' }, { 'name' => /abc/ } ])).to eq(5.0)
163
+ end
164
+
165
+ it 'is nil for a selector carrying no regular expression' do
166
+ expect(described_class.limit_for('name' => 'abc')).to be_nil
167
+ end
168
+
169
+ it 'is nil for an empty selector' do
170
+ expect(described_class.limit_for({})).to be_nil
171
+ end
172
+ end
173
+
174
+ context 'when no limit is configured' do
175
+ config_override :in_memory_regexp_time_limit, nil
176
+
177
+ # Nothing would be bounded, so a caller that only rearranges its work to
178
+ # make room for a budget has no reason to.
179
+ it 'is nil even for a selector carrying a regular expression' do
180
+ expect(described_class.limit_for(regexp_selector)).to be_nil
181
+ end
182
+ end
183
+
184
+ context 'when the configured limit is zero' do
185
+ config_override :in_memory_regexp_time_limit, 0
186
+
187
+ it 'is nil even for a selector carrying a regular expression' do
188
+ expect(described_class.limit_for(regexp_selector)).to be_nil
189
+ end
190
+ end
191
+ end
192
+
193
+ describe '.open_with' do
194
+ config_override :in_memory_regexp_time_limit, 5.0
195
+
196
+ it 'establishes a budget for the limit it is given, not the configured one' do
197
+ described_class.open_with(2.0) do
198
+ expect(described_class.remaining).to be_within(0.01).of(2.0)
199
+ end
200
+ end
201
+
202
+ it 'returns the value of the block' do
203
+ expect(described_class.open_with(2.0) { :result }).to eq(:result)
204
+ end
205
+
206
+ it 'clears the budget when the block returns' do
207
+ described_class.open_with(2.0) { nil }
208
+ expect(described_class.remaining).to be_nil
209
+ end
210
+
211
+ context 'when given no limit' do
212
+ it 'does not establish a budget' do
213
+ described_class.open_with(nil) do
214
+ expect(described_class.remaining).to be_nil
215
+ end
216
+ end
217
+
218
+ # A caller that read the limit, found nothing to bound and arranged its
219
+ # work accordingly must not have a deadline imposed on it by a later read
220
+ # of the same setting. On the Timeout path that deadline would cover
221
+ # whatever the caller went on to do, which here is a query and a mutation
222
+ # of every match.
223
+ it 'does not put a deadline on the block' do
224
+ stub_const('Mongoid::Matcher::RegexpBudget::PER_REGEXP_TIMEOUT', false)
225
+ Mongoid::Config.in_memory_regexp_time_limit = 0.2
226
+
227
+ expect do
228
+ described_class.open_with(nil) { sleep 0.3 }
229
+ end.not_to raise_error
230
+ end
231
+
232
+ it 'leaves an enclosing budget alone' do
233
+ described_class.open(regexp_selector) do
234
+ described_class.open_with(nil) do
235
+ expect(described_class.remaining).to be_within(0.01).of(5.0)
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ describe '.match?' do
243
+ context 'when no budget is open' do
244
+ it 'performs the match' do
245
+ expect(described_class.match?('abc', cheap_pattern)).to eq(0)
246
+ end
247
+
248
+ it 'returns nil when the value does not match' do
249
+ expect(described_class.match?('xyz', cheap_pattern)).to be_nil
250
+ end
251
+
252
+ it "leaves an application's own Regexp timeout alone" do
253
+ # With nothing of ours to blame it on, translating the error would name
254
+ # a limit that is not set and advise unsetting it.
255
+ skip 'per-Regexp timeouts unavailable' unless described_class::PER_REGEXP_TIMEOUT
256
+
257
+ pattern = Regexp.new(catastrophic_pattern.pattern, timeout: 0.2)
258
+
259
+ expect do
260
+ described_class.match?(catastrophic_subject, pattern)
261
+ end.to raise_error(Regexp::TimeoutError)
262
+ end
263
+ end
264
+
265
+ context 'when a budget is open' do
266
+ config_override :in_memory_regexp_time_limit, 5.0
267
+
268
+ it 'performs the match' do
269
+ described_class.open(regexp_selector) do
270
+ expect(described_class.match?('abc', cheap_pattern)).to eq(0)
271
+ end
272
+ end
273
+
274
+ it 'accepts an already compiled Regexp' do
275
+ described_class.open(regexp_selector) do
276
+ expect(described_class.match?('abc', /\Aabc\z/)).to eq(0)
277
+ end
278
+ end
279
+
280
+ it 'preserves the options of the condition' do
281
+ described_class.open(regexp_selector) do
282
+ expect(described_class.match?('ABC', BSON::Regexp::Raw.new('\Aabc\z', 'i'))).to eq(0)
283
+ end
284
+ end
285
+
286
+ it 'preserves the encoding of the condition' do
287
+ # The condition is rebuilt to carry the timeout, so its source, options
288
+ # and encoding all have to survive the round trip.
289
+ described_class.open(regexp_selector) do
290
+ expect(described_class.match?('é', /\Aé+\z/u)).to eq(0)
291
+ end
292
+ end
293
+
294
+ it 'charges the elapsed time against the budget' do
295
+ described_class.open(regexp_selector) do
296
+ before = described_class.remaining
297
+ described_class.match?(slow_subject, slow_pattern)
298
+ expect(described_class.remaining).to be < before
299
+ end
300
+ end
301
+
302
+ it 'compiles a pattern once for the scope rather than once per match' do
303
+ skip 'per-Regexp timeouts unavailable' unless described_class::PER_REGEXP_TIMEOUT
304
+
305
+ # An already compiled condition, so that the only Regexp.new in play is
306
+ # the one baking in the timeout.
307
+ allow(Regexp).to receive(:new).and_call_original
308
+
309
+ described_class.open(regexp_selector) do
310
+ 10.times { described_class.match?('abc', /\Aabc\z/) }
311
+ end
312
+
313
+ expect(Regexp).to have_received(:new).once
314
+ end
315
+ end
316
+
317
+ context 'when the patterns are expensive to compile but cheap to run' do
318
+ config_override :in_memory_regexp_time_limit, nil
319
+
320
+ # Thousands of alternations cost far more to compile than to run against
321
+ # a subject that fails at the first character. Charging only the match
322
+ # would leave a selector full of these -- a long $or, say -- able to
323
+ # spend as much time as it liked without the budget noticing.
324
+ let(:costly_to_compile) do
325
+ Array.new(200) do |n|
326
+ BSON::Regexp::Raw.new("(?:#{(1..2_000).map { |i| "b#{n}x#{i}" }.join('|')})Z")
327
+ end
328
+ end
329
+
330
+ before do
331
+ # Building the fixtures is not free either, so it happens before the
332
+ # clock starts rather than inside the block being measured.
333
+ pattern = costly_to_compile.first.pattern
334
+ cost = Benchmark.realtime { Regexp.new(pattern) }
335
+ Mongoid::Config.in_memory_regexp_time_limit = cost * 5
336
+ end
337
+
338
+ it 'charges compilation against the budget' do
339
+ skip 'per-Regexp timeouts unavailable' unless described_class::PER_REGEXP_TIMEOUT
340
+
341
+ expect do
342
+ described_class.open(regexp_selector) do
343
+ costly_to_compile.each { |pattern| described_class.match?('x', pattern) }
344
+ end
345
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
346
+ end
347
+ end
348
+
349
+ context 'when the application has set a stricter Regexp timeout' do
350
+ config_override :in_memory_regexp_time_limit, 5.0
351
+
352
+ around do |example|
353
+ skip 'per-Regexp timeouts unavailable' unless described_class::PER_REGEXP_TIMEOUT
354
+
355
+ was = Regexp.timeout
356
+ Regexp.timeout = 0.2
357
+ begin
358
+ example.run
359
+ ensure
360
+ Regexp.timeout = was
361
+ end
362
+ end
363
+
364
+ it 'does not loosen it to the budget limit' do
365
+ # Baking the whole limit in would override the global and leave the
366
+ # application less protected than it configured itself to be.
367
+ elapsed = Benchmark.realtime do
368
+ expect do
369
+ described_class.open(regexp_selector) do
370
+ described_class.match?(catastrophic_subject, catastrophic_pattern)
371
+ end
372
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
373
+ end
374
+
375
+ expect(elapsed).to be < 1.0
376
+ end
377
+
378
+ it 'names the limit that fired rather than the configured one' do
379
+ # Naming the configured 5.0 would state a time that was never spent,
380
+ # and send the reader after a setting that is not the one in the way.
381
+ expect do
382
+ described_class.open(regexp_selector) do
383
+ described_class.match?(catastrophic_subject, catastrophic_pattern)
384
+ end
385
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout, /exceeded the 0\.2 second limit/)
386
+ end
387
+ end
388
+
389
+ context 'when a global Regexp timeout fires with nothing baked' do
390
+ config_override :in_memory_regexp_time_limit, 5.0
391
+
392
+ # The counterpart to the context above, for the engine that raises
393
+ # Regexp::TimeoutError but will not take a per-Regexp timeout: JRuby. The
394
+ # pattern carries nothing there, so it reports nil and the global is the
395
+ # only thing that can have fired.
396
+ around do |example|
397
+ skip 'no Regexp timeouts at all' unless defined?(Regexp::TimeoutError)
398
+
399
+ was = Regexp.timeout
400
+ Regexp.timeout = 0.2
401
+ begin
402
+ example.run
403
+ ensure
404
+ Regexp.timeout = was
405
+ end
406
+ end
407
+
408
+ before { stub_const('Mongoid::Matcher::RegexpBudget::PER_REGEXP_TIMEOUT', false) }
409
+
410
+ it 'names the global limit rather than the budget limit' do
411
+ expect do
412
+ described_class.open_with(5.0) do
413
+ described_class.match?(catastrophic_subject, catastrophic_pattern)
414
+ end
415
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout, /exceeded the 0\.2 second limit/)
416
+ end
417
+ end
418
+
419
+ context 'when the accumulated cost exceeds the limit' do
420
+ config_override :in_memory_regexp_time_limit, nil
421
+
422
+ # Calibrated rather than hard-coded: one match of the fixture costs about
423
+ # 6ms on MRI and 110ms on JRuby, so a fixed limit would be either
424
+ # unreachable on one or tripped by a single match on the other.
425
+ before do
426
+ regexp = Regexp.new(slow_pattern.pattern)
427
+ 3.times { slow_subject =~ regexp }
428
+ cost = Benchmark.realtime { slow_subject =~ regexp }
429
+ Mongoid::Config.in_memory_regexp_time_limit = cost * 5
430
+ end
431
+
432
+ it 'raises once the budget is spent' do
433
+ expect do
434
+ described_class.open(regexp_selector) do
435
+ 100.times { described_class.match?(slow_subject, slow_pattern) }
436
+ end
437
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
438
+ end
439
+
440
+ it 'names the configured limit' do
441
+ # The counterpart to the stricter-Regexp.timeout example above: where
442
+ # the budget itself is what ran out, its own limit is the one to name.
443
+ limit = Regexp.escape(Mongoid::Config.in_memory_regexp_time_limit.to_s)
444
+
445
+ expect do
446
+ described_class.open(regexp_selector) do
447
+ 100.times { described_class.match?(slow_subject, slow_pattern) }
448
+ end
449
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout, /exceeded the #{limit} second limit/)
450
+ end
451
+
452
+ it 'does not raise for a single match under that same limit' do
453
+ expect do
454
+ described_class.open(regexp_selector) { described_class.match?(slow_subject, slow_pattern) }
455
+ end.not_to raise_error
456
+ end
457
+
458
+ it 'does not raise when no single match and no accumulation exceeds the limit' do
459
+ expect do
460
+ described_class.open(regexp_selector) do
461
+ 100.times { described_class.match?('abc', cheap_pattern) }
462
+ end
463
+ end.not_to raise_error
464
+ end
465
+ end
466
+ end
467
+
468
+ context 'when a single match runs far longer than the limit' do
469
+ config_override :in_memory_regexp_time_limit, 0.2
470
+
471
+ context 'when the Ruby in use supports per-Regexp timeouts' do
472
+ # Keyed to the capability, not the version: JRuby 10 reports Ruby 3.4
473
+ # but cannot be given a per-Regexp timeout, so it runs the Timeout path.
474
+ before do
475
+ skip 'per-Regexp timeouts unavailable' unless described_class::PER_REGEXP_TIMEOUT
476
+ end
477
+
478
+ it 'interrupts the match' do
479
+ expect do
480
+ described_class.open(regexp_selector) do
481
+ described_class.match?(catastrophic_subject, catastrophic_pattern)
482
+ end
483
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
484
+ end
485
+ end
486
+
487
+ context 'when the Ruby in use has no per-Regexp timeouts' do
488
+ before do
489
+ stub_const('Mongoid::Matcher::RegexpBudget::PER_REGEXP_TIMEOUT', false)
490
+ end
491
+
492
+ it 'interrupts the match with Timeout' do
493
+ expect do
494
+ described_class.open(regexp_selector) do
495
+ described_class.match?(catastrophic_subject, catastrophic_pattern)
496
+ end
497
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
498
+ end
499
+
500
+ it 'interrupts an unprotected block part way through' do
501
+ completed = false
502
+
503
+ expect do
504
+ described_class.open(regexp_selector) do
505
+ sleep 0.3
506
+ completed = true
507
+ end
508
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
509
+
510
+ expect(completed).to be(false)
511
+ end
512
+
513
+ it 'lets a protected block finish before delivering the interruption' do
514
+ # remove_all mutates association state inside its scope. Deferring the
515
+ # exception, rather than forgoing it, is what lets that scope stay
516
+ # interruptible without unbind_one being torn in half.
517
+ completed = false
518
+
519
+ expect do
520
+ described_class.open(regexp_selector) do
521
+ described_class.protect do
522
+ sleep 0.3
523
+ completed = true
524
+ end
525
+ end
526
+ end.to raise_error(Mongoid::Errors::InMemoryRegexpTimeout)
527
+
528
+ expect(completed).to be(true)
529
+ end
530
+ end
531
+ end
532
+
533
+ describe Mongoid::Matcher::RegexpBudget::Budget do
534
+ describe '#compile' do
535
+ let(:budget) { described_class.new(5.0) }
536
+
537
+ it 'returns a pattern matching the condition' do
538
+ expect(budget.compile(BSON::Regexp::Raw.new('\Aabc\z'))).to match('abc')
539
+ end
540
+
541
+ it 'reuses the pattern compiled for an equal condition' do
542
+ # FieldExpression builds a fresh BSON::Regexp::Raw for every $regex it
543
+ # evaluates, so its own memo is worth nothing from one document to the
544
+ # next and the source would be compiled once per document. Raw does not
545
+ # override hash, either, so the cache cannot be keyed on the condition
546
+ # itself.
547
+ first = budget.compile(BSON::Regexp::Raw.new('abc', 'i'))
548
+ second = budget.compile(BSON::Regexp::Raw.new('abc', 'i'))
549
+
550
+ expect(second).to equal(first)
551
+ end
552
+
553
+ it 'does not confuse conditions differing only in their options' do
554
+ insensitive = budget.compile(BSON::Regexp::Raw.new('abc', 'i'))
555
+ sensitive = budget.compile(BSON::Regexp::Raw.new('abc'))
556
+
557
+ expect(insensitive).to match('ABC')
558
+ expect(sensitive).not_to match('ABC')
559
+ end
560
+
561
+ it 'reuses the pattern compiled for an equal Regexp' do
562
+ expect(budget.compile(/abc/i)).to equal(budget.compile(/abc/i))
563
+ end
564
+
565
+ it 'raises for a condition that is not a regular expression' do
566
+ expect { budget.compile('abc') }.to raise_error(ArgumentError, /Not a regular expression/)
567
+ end
568
+ end
569
+ end
570
+ end
@@ -45,23 +45,16 @@ module Mrss
45
45
  end
46
46
 
47
47
  # Per https://jira.mongodb.org/browse/SERVER-39052, working with FCV
48
- # in sharded topologies is annoying. Also, FCV doesn't exist in servers
49
- # less than 3.4. This method returns FCV on 3.4+ servers when in single
48
+ # in sharded topologies is annoying. This method returns FCV when in single
50
49
  # or RS topologies, and otherwise returns the major.minor server version.
51
50
  def fcv_ish
52
51
  if server_version.nil?
53
52
  raise "Deployment server version not known - check that connection to deployment succeeded"
54
53
  end
55
54
 
56
- if server_version >= '3.4' && !sharded_ish?
57
- fcv
58
- else
59
- if short_server_version == '4.1'
60
- '4.2'
61
- else
62
- short_server_version
63
- end
64
- end
55
+ return fcv unless sharded_ish?
56
+
57
+ short_server_version
65
58
  end
66
59
 
67
60
  # @return [ Mongo::Address ] The address of the primary in the deployment.
@@ -113,34 +106,6 @@ module Mrss
113
106
  @topology
114
107
  end
115
108
 
116
- def storage_engine
117
- @storage_engine ||= begin
118
- # 2.6 does not have wired tiger
119
- if short_server_version == '2.6'
120
- :mmapv1
121
- else
122
- client = ClientRegistry.instance.global_client('root_authorized')
123
- if sharded_ish?
124
- shards = client.use(:admin).command(listShards: 1).first
125
- if shards['shards'].empty?
126
- raise 'Shards are empty'
127
- end
128
- shard = shards['shards'].first
129
- address_str = shard['host'].sub(/^.*\//, '').sub(/,.*/, '')
130
- client = ClusterTools.instance.direct_client(address_str,
131
- SpecConfig.instance.test_options.merge(SpecConfig.instance.auth_options).merge(connect: :direct))
132
- end
133
- rv = client.use(:admin).command(serverStatus: 1).first
134
- rv = rv['storageEngine']['name']
135
- rv_map = {
136
- 'wiredTiger' => :wired_tiger,
137
- 'mmapv1' => :mmapv1,
138
- }
139
- rv_map[rv] || rv
140
- end
141
- end
142
- end
143
-
144
109
  # This method returns an alternate address for connecting to the configured
145
110
  # deployment. For example, if the replica set is configured with nodes at
146
111
  # of localhost:27017 and so on, this method will return 127.0.0.:27017.
@@ -216,7 +181,7 @@ module Mrss
216
181
  {}
217
182
  end
218
183
 
219
- if !sharded_ish? && short_server_version >= '3.4'
184
+ unless sharded_ish?
220
185
  rv = @server_parameters['featureCompatibilityVersion']
221
186
  @fcv = rv['version'] || rv
222
187
  end