Skip to content

Commit f5d6f70

Browse files
p-mongop
andcommitted
Fix RUBY-2675 QueryCache returns wrong results after partial iteration of result set, e.g. after calling none? (#2329)
Co-authored-by: Oleg Pudeyev <code@olegp.name>
1 parent b313cd9 commit f5d6f70

File tree

4 files changed

+107
-6
lines changed

4 files changed

+107
-6
lines changed

docs/tutorials/query-cache.txt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,42 @@ And to disable the query cache in the context of a block:
6666
You may check whether the query cache is enabled at any time by calling
6767
``Mongo::QueryCache.enabled?``, which will return ``true`` or ``false``.
6868

69+
70+
Interactions With Fibers
71+
========================
72+
73+
The Query cache enablement flag is stored in fiber-local storage (using
74+
`Thread.current <https://ruby-doc.org/core/Thread.html#class-Thread-label-Fiber-local+vs.+Thread-local>`_.
75+
This, in principle, permits query cache state to be per fiber, although
76+
this is not currently tested.
77+
78+
There are methods in the Ruby standard library, like ``Enumerable#next``,
79+
that `utilize fibers <https://stackoverflow.com/questions/11057223/how-does-rubys-enumerator-object-iterate-externally-over-an-internal-iterator/11058270#11058270>`_
80+
in their implementation. These methods would not see the query cache
81+
enablement flag when it is set by the applications, and subsequently would
82+
not use the query cache. For example, the following code does not utilize
83+
the query cache despite requesting it:
84+
85+
.. code-block:: ruby
86+
87+
Mongo::QueryCache.enabled = true
88+
89+
client['artists'].find({}, limit: 1).to_enum.next
90+
# Issues the query again.
91+
client['artists'].find({}, limit: 1).to_enum.next
92+
93+
Rewriting this code to use ``first`` instead of ``next`` would make it use
94+
the query cache:
95+
96+
.. code-block:: ruby
97+
98+
Mongo::QueryCache.enabled = true
99+
100+
client['artists'].find({}, limit: 1).first
101+
# Utilizes the cached result from the first query.
102+
client['artists'].find({}, limit: 1).first
103+
104+
69105
.. _query-cache-matching:
70106

71107
Query Matching

lib/mongo/collection/view/iterable.rb

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,28 @@ module Iterable
3535
#
3636
# @yieldparam [ Hash ] Each matching document.
3737
def each
38-
@cursor = if use_query_cache? && cached_cursor
38+
# If the caching cursor is closed and was not fully iterated,
39+
# the documents we have in it are not the complete result set and
40+
# we have no way of completing that iteration.
41+
# Therefore, discard that cursor and start iteration again.
42+
# The case of the caching cursor not being closed and not having
43+
# been fully iterated isn't tested - see RUBY-2773.
44+
@cursor = if use_query_cache? && cached_cursor && (
45+
cached_cursor.fully_iterated? || !cached_cursor.closed?
46+
)
3947
cached_cursor
4048
else
4149
session = client.send(:get_session, @options)
42-
select_cursor(session)
50+
select_cursor(session).tap do |cursor|
51+
if use_query_cache?
52+
# No need to store the cursor in the query cache if there is
53+
# already a cached cursor stored at this key.
54+
QueryCache.set(cursor, **cache_options)
55+
end
56+
end
4357
end
4458

4559
if use_query_cache?
46-
# No need to store the cursor in the query cache if there is
47-
# already a cached cursor stored at this key.
48-
QueryCache.set(@cursor, **cache_options) unless cached_cursor
49-
5060
# If a query with a limit is performed, the query cache will
5161
# re-use results from an earlier query with the same or larger
5262
# limit, and then impose the lower limit during iteration.

lib/mongo/cursor.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,12 @@ def try_next
209209
unless closed?
210210
if exhausted?
211211
close
212+
@fully_iterated = true
212213
raise StopIteration
213214
end
214215
@documents = get_more
215216
else
217+
@fully_iterated = true
216218
raise StopIteration
217219
end
218220
else
@@ -229,6 +231,9 @@ def try_next
229231
# over the last document, or if the batch is empty
230232
if @documents.size <= 1
231233
cache_batch_resume_token
234+
if closed?
235+
@fully_iterated = true
236+
end
232237
end
233238

234239
return @documents.shift
@@ -348,6 +353,11 @@ def get_more
348353
process(get_more_operation.execute(@server, client: client))
349354
end
350355

356+
# @api private
357+
def fully_iterated?
358+
!!@fully_iterated
359+
end
360+
351361
private
352362

353363
def exhausted?

spec/integration/query_cache_spec.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,4 +1042,49 @@
10421042
expect(events.length).to eq(2)
10431043
end
10441044
end
1045+
1046+
context 'when result set has multiple documents and cursor is iterated partially' do
1047+
1048+
before do
1049+
Mongo::QueryCache.enabled = false
1050+
5.times do
1051+
authorized_collection.insert_one({ name: 'testing' })
1052+
end
1053+
end
1054+
1055+
shared_examples 'retrieves full result set on second iteration' do
1056+
it 'retrieves full result set on second iteration' do
1057+
Mongo::QueryCache.clear
1058+
Mongo::QueryCache.enabled = true
1059+
1060+
partial_first_iteration
1061+
1062+
authorized_collection.find.to_a.length.should == 5
1063+
end
1064+
1065+
end
1066+
1067+
context 'using each & break' do
1068+
let(:partial_first_iteration) do
1069+
called = false
1070+
authorized_collection.find.each do
1071+
called = true
1072+
break
1073+
end
1074+
called.should be true
1075+
end
1076+
1077+
include_examples 'retrieves full result set on second iteration'
1078+
end
1079+
1080+
context 'using next' do
1081+
let(:partial_first_iteration) do
1082+
# #next is executed in its own fiber, and query cache is disabled
1083+
# for that operation.
1084+
authorized_collection.find.to_enum.next
1085+
end
1086+
1087+
include_examples 'retrieves full result set on second iteration'
1088+
end
1089+
end
10451090
end

0 commit comments

Comments
 (0)