Skip to content

Commit dc0d505

Browse files
comandeo-mongoneilshwekyp-mongo
authored
MONGOID-5445 Add load_async to criteria (#5454)
Co-authored-by: Neil Shweky <[email protected]> Co-authored-by: Oleg Pudeyev <[email protected]>
1 parent 05c86f1 commit dc0d505

File tree

18 files changed

+759
-21
lines changed

18 files changed

+759
-21
lines changed

docs/reference/configuration.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,16 @@ for details on driver options.
265265
# if the database name is not explicitly defined. (default: nil)
266266
app_name: MyApplicationName
267267

268+
# Type of executor for queries scheduled using ``load_async`` method.
269+
#
270+
# There are two possible values for this option:
271+
#
272+
# - :immediate - Queries will be immediately executed on a current thread.
273+
# This is the default option.
274+
# - :global_thread_pool - Queries will be executed asynchronously in
275+
# background using a thread pool.
276+
#async_query_executor: :immediate
277+
268278
# Mark belongs_to associations as required by default, so that saving a
269279
# model with a missing belongs_to association will trigger a validation
270280
# error. (default: true)
@@ -358,6 +368,11 @@ for details on driver options.
358368
# Raise an exception when a field is redefined. (default: false)
359369
duplicate_fields_exception: false
360370

371+
# Defines how many asynchronous queries can be executed concurrently.
372+
# This option should be set only if `async_query_executor` option is set
373+
# to `:global_thread_pool`.
374+
#global_executor_concurrency: nil
375+
361376
# Include the root model name in json serialization. (default: false)
362377
include_root_in_json: false
363378

docs/reference/queries.txt

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2419,5 +2419,72 @@ will query the database and separately cache their results.
24192419
To use the cached results, call ``all.to_a.first`` on the model class.
24202420

24212421

2422-
.. _legacy-query-cache-limitations:
2422+
.. _load-async:
24232423

2424+
Asynchronous Queries
2425+
====================
2426+
2427+
Mongoid allows running database queries asynchronously in the background.
2428+
This can be beneficial when there is a need to get documents from different
2429+
collections.
2430+
2431+
In order to schedule an asynchronous query call the ``load_async`` method on a
2432+
``Criteria``:
2433+
2434+
.. code-block:: ruby
2435+
2436+
class PagesController < ApplicationController
2437+
def index
2438+
@active_bands = Band.where(active: true).load_async
2439+
@best_events = Event.best.load_async
2440+
@public_articles = Article.where(public: true).load_async
2441+
end
2442+
end
2443+
2444+
In the above example three queries will be scheduled for asynchronous execution.
2445+
Results of the queries can be later accessed as usual:
2446+
2447+
.. code-block:: html
2448+
2449+
<ul>
2450+
<%- @active_bands.each do -%>
2451+
<li><%= band.name %></li>
2452+
<%- end -%>
2453+
</ul>
2454+
2455+
Even if a query is scheduled for asynchronous execution, it might be executed
2456+
synchronously on the caller's thread. There are three possible scenarios depending
2457+
on when the query results are being accessed:
2458+
2459+
#. If the scheduled asynchronous task has been already executed, the results are returned.
2460+
#. If the task has been started, but not finished yet, the caller's thread blocks until the task is finished.
2461+
#. If the task has not been started yet, it is removed from the execution queue, and the query is executed synchronously on the caller's thread.
2462+
2463+
.. note::
2464+
2465+
Even though ``load_async`` method returns a ``Criteria`` object, you should not
2466+
do any operations on this object except accessing query results. The query is
2467+
scheduled for execution immediately after calling ``load_async``, therefore
2468+
later changes to the `Criteria`` object may not be applied.
2469+
2470+
2471+
Configuring asynchronous query execution
2472+
----------------------------------------
2473+
2474+
Asynchronous queries are disabled by default. When asynchronous queries are
2475+
disabled, ``load_async`` will execute the query immediately on the current thread,
2476+
blocking as necessary. Therefore, calling ``load_async`` on criteria in this case
2477+
is roughly the equivalent of calling ``to_a`` to force query execution.
2478+
2479+
In order to enable asynchronous query execution, the following config options
2480+
must be set:
2481+
2482+
.. code-block:: yaml
2483+
2484+
development:
2485+
...
2486+
options:
2487+
# Execute asynchronous queries using a global thread pool.
2488+
async_query_executor: :global_thread_pool
2489+
# Number of threads in the pool. The default is 4.
2490+
# global_executor_concurrency: 4

docs/release-notes/mongoid-8.1.txt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ The complete list of releases is available `on GitHub
1717
please consult GitHub releases for detailed release notes and JIRA for
1818
the complete list of issues fixed in each release, including bug fixes.
1919

20+
Added ``load_async`` method on ``Criteria`` to asynchronously load documents
21+
----------------------------------------------------------------------------
22+
23+
The new ``load_async`` method on ``Criteria`` allows :ref:`running database queries asynchronously <load-async>`.
24+
2025

2126
Added ``attribute_before_last_save``, ``saved_change_to_attribute``, ``saved_change_to_attribute?``, and ``will_save_change_to_attribute?`` methods
2227
----------------------------------------------------------------------------------------------------------------------------------------------------
@@ -245,8 +250,8 @@ for more details.
245250
Added ``readonly!`` method and ``legacy_readonly`` feature flag
246251
---------------------------------------------------------------
247252

248-
Mongoid 8.1 changes the meaning of read-only documents. In Mongoid 8.1 with
249-
this feature flag turned off, a document becomes read-only when calling the
253+
Mongoid 8.1 changes the meaning of read-only documents. In Mongoid 8.1 with
254+
this feature flag turned off, a document becomes read-only when calling the
250255
``readonly!`` method:
251256

252257
.. code:: ruby
@@ -261,8 +266,8 @@ this feature flag turned off, a document becomes read-only when calling the
261266
With this feature flag turned off, a ``ReadonlyDocument`` error will be
262267
raised when destroying or deleting, as well as when saving or updating.
263268

264-
Prior to Mongoid 8.1 and in 8.1 with the ``legacy_readonly`` feature flag
265-
turned on, documents become read-only when they are projected (i.e. using
269+
Prior to Mongoid 8.1 and in 8.1 with the ``legacy_readonly`` feature flag
270+
turned on, documents become read-only when they are projected (i.e. using
266271
``#only`` or ``#without``).
267272

268273
.. code:: ruby
@@ -278,7 +283,7 @@ turned on, documents become read-only when they are projected (i.e. using
278283
band.destroy # => raises ReadonlyDocument error
279284

280285
Note that with this feature flag on, a ``ReadonlyDocument`` error will only be
281-
raised when destroying or deleting, and not on saving or updating. See the
286+
raised when destroying or deleting, and not on saving or updating. See the
282287
section on :ref:`Read-only Documents <readonly-documents>` for more details.
283288

284289

lib/config/locales/en.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ en:
133133
A collation option is only supported if the query is executed on a MongoDB server
134134
with version >= 3.4."
135135
resolution: "Remove the collation option from the query."
136+
invalid_async_query_executor:
137+
message: "Invalid async_query_executor option: %{executor}."
138+
summary: "A invalid async query executor was specified.
139+
The valid options are: %{options}."
140+
resolution: "Pick an allowed option or fix the typo. If you were
141+
expecting the option to be there, please consult the following page
142+
with respect to Mongoid's configuration:\n\n
143+
\_\_https://www.mongodb.com/docs/mongoid/current/reference/configuration/#mongoid-configuration-options"
136144
invalid_config_file:
137145
message: "Invalid configuration file: %{path}."
138146
summary: "Your mongoid.yml configuration file does not contain the
@@ -238,6 +246,13 @@ en:
238246
resolution: "Please provide a valid type value for the field.
239247
Refer to:
240248
https://docs.mongodb.com/mongoid/current/reference/fields/#using-symbols-or-strings-instead-of-classes"
249+
invalid_global_executor_concurrency:
250+
message: "Invalid global_executor_concurrency option."
251+
summary: "You set global_executor_concurrency while async_query_executor
252+
option is not set to :global_thread_pool. The global_executor_concurrency is
253+
allowed only for the global thread pool executor."
254+
resolution: "Set global_executor_concurrency option to :global_thread_pool
255+
or remove global_executor_concurrency option."
241256
invalid_includes:
242257
message: "Invalid includes directive: %{klass}.includes(%{args})"
243258
summary: "Eager loading in Mongoid only supports providing arguments

lib/mongoid.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
require "active_support/time_with_zone"
1313
require "active_model"
1414

15+
require 'concurrent-ruby'
16+
1517
require "mongo"
1618
require 'mongo/active_support'
1719

lib/mongoid/config.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,20 @@ module Config
127127
# always return a Hash.
128128
option :legacy_attributes, default: false
129129

130-
# When this flag is false, a document will become read-only only once the
130+
# Sets the async_query_executor for the application. By default the thread pool executor
131+
# is set to `:immediate. Options are:
132+
#
133+
# - :immediate - Initializes a single +Concurrent::ImmediateExecutor+
134+
# - :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+
135+
# that uses the +async_query_concurrency+ for the +max_threads+ value.
136+
option :async_query_executor, default: :immediate
137+
138+
# Defines how many asynchronous queries can be executed concurrently.
139+
# This option should be set only if `async_query_executor` is set
140+
# to `:global_thread_pool`.
141+
option :global_executor_concurrency, default: nil
142+
143+
# When this flag is false, a document will become read-only only once the
131144
# #readonly! method is called, and an error will be raised on attempting
132145
# to save or update such documents, instead of just on delete. When this
133146
# flag is true, a document is only read-only if it has been projected
@@ -308,6 +321,7 @@ def truncate!
308321
# @param [ Hash ] options The configuration options.
309322
def options=(options)
310323
if options
324+
Validators::AsyncQueryExecutor.validate(options)
311325
options.each_pair do |option, value|
312326
Validators::Option.validate(option)
313327
send("#{option}=", value)

lib/mongoid/config/validators.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# frozen_string_literal: true
22

3+
require "mongoid/config/validators/async_query_executor"
34
require "mongoid/config/validators/option"
45
require "mongoid/config/validators/client"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
module Mongoid
4+
module Config
5+
module Validators
6+
7+
# Validator for async query executor configuration.
8+
#
9+
# @api private
10+
module AsyncQueryExecutor
11+
extend self
12+
13+
14+
def validate(options)
15+
if options.key?(:async_query_executor)
16+
if options[:async_query_executor].to_sym == :immediate && !options[:global_executor_concurrency].nil?
17+
raise Errors::InvalidGlobalExecutorConcurrency
18+
end
19+
end
20+
end
21+
end
22+
end
23+
end
24+
end

lib/mongoid/contextual.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ def context
3535
@context ||= create_context
3636
end
3737

38+
# Instructs the context to schedule an asynchronous loading of documents
39+
# specified by the criteria.
40+
#
41+
# Note that depending on the context and on the Mongoid configuration,
42+
# documents can be loaded synchronously on the caller's thread.
43+
#
44+
# @return [ Criteria ] Returns self.
45+
def load_async
46+
context.load_async if context.respond_to?(:load_async)
47+
self
48+
end
49+
3850
private
3951

4052
# Create the context for the queries to execute. Will be memory for

lib/mongoid/contextual/mongo.rb

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require "mongoid/contextual/mongo/documents_loader"
34
require "mongoid/contextual/atomic"
45
require "mongoid/contextual/aggregable/mongo"
56
require "mongoid/contextual/command"
@@ -37,6 +38,8 @@ class Mongo
3738
# @attribute [r] view The Mongo collection view.
3839
attr_reader :view
3940

41+
attr_reader :documents_loader
42+
4043
# Get the number of documents matching the query.
4144
#
4245
# @example Get the number of matching documents.
@@ -777,6 +780,17 @@ def third_to_last!
777780
third_to_last || raise_document_not_found_error
778781
end
779782

783+
# Schedule a task to load documents for the context.
784+
#
785+
# Depending on the Mongoid configuration, the scheduled task can be executed
786+
# immediately on the caller's thread, or can be scheduled for an
787+
# asynchronous execution.
788+
#
789+
# @api private
790+
def load_async
791+
@documents_loader ||= DocumentsLoader.new(view, klass, criteria)
792+
end
793+
780794
private
781795

782796
# Update the documents for the provided method.
@@ -844,24 +858,29 @@ def inverse_sorting
844858
Hash[sort.map{|k, v| [k, -1*v]}]
845859
end
846860

847-
# Get the documents the context should iterate. This follows 3 rules:
848-
#
849-
# 1. If the query is cached, and we already have documents loaded, use
850-
# them.
851-
# 2. If we are eager loading, then eager load the documents and use
852-
# those.
853-
# 3. Use the query.
854-
#
855-
# @api private
861+
# Get the documents the context should iterate.
856862
#
857-
# @example Get the documents for iteration.
858-
# context.documents_for_iteration
863+
# If the documents have been already preloaded by `Document::Loader`
864+
# instance, they will be used.
859865
#
860866
# @return [ Array<Document> | Mongo::Collection::View ] The docs to iterate.
867+
#
868+
# @api private
861869
def documents_for_iteration
862-
return view unless eager_loadable?
863-
docs = view.map{ |doc| Factory.from_db(klass, doc, criteria) }
864-
eager_load(docs)
870+
if @documents_loader
871+
if @documents_loader.started?
872+
@documents_loader.value!
873+
else
874+
@documents_loader.unschedule
875+
@documents_loader.execute
876+
end
877+
else
878+
return view unless eager_loadable?
879+
docs = view.map do |doc|
880+
Factory.from_db(klass, doc, criteria)
881+
end
882+
eager_load(docs)
883+
end
865884
end
866885

867886
# Yield to the document.

0 commit comments

Comments
 (0)