Skip to content

Commit dac141b

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 bf52eaf commit dac141b

File tree

4 files changed

+107
-6
lines changed

4 files changed

+107
-6
lines changed

docs/reference/query-cache.txt

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

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

73109
Query Matching

lib/mongo/collection/view/iterable.rb

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

4862
if use_query_cache?
49-
# No need to store the cursor in the query cache if there is
50-
# already a cached cursor stored at this key.
51-
QueryCache.set(@cursor, **cache_options) unless cached_cursor
52-
5363
# If a query with a limit is performed, the query cache will
5464
# re-use results from an earlier query with the same or larger
5565
# 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
@@ -212,10 +212,12 @@ def try_next
212212
unless closed?
213213
if exhausted?
214214
close
215+
@fully_iterated = true
215216
raise StopIteration
216217
end
217218
@documents = get_more
218219
else
220+
@fully_iterated = true
219221
raise StopIteration
220222
end
221223
else
@@ -232,6 +234,9 @@ def try_next
232234
# over the last document, or if the batch is empty
233235
if @documents.size <= 1
234236
cache_batch_resume_token
237+
if closed?
238+
@fully_iterated = true
239+
end
235240
end
236241

237242
return @documents.shift
@@ -352,6 +357,11 @@ def get_more
352357
process(get_more_operation.execute(@server, context: Operation::Context.new(client: client, session: @session)))
353358
end
354359

360+
# @api private
361+
def fully_iterated?
362+
!!@fully_iterated
363+
end
364+
355365
private
356366

357367
def exhausted?

spec/integration/query_cache_spec.rb

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

0 commit comments

Comments
 (0)