Skip to content

Commit

Permalink
MONGOID-5445 Add load_async to criteria (#5454)
Browse files Browse the repository at this point in the history
Co-authored-by: Neil Shweky <[email protected]>
Co-authored-by: Oleg Pudeyev <[email protected]>
  • Loading branch information
3 people committed Sep 30, 2022
1 parent 86523a4 commit 855102f
Show file tree
Hide file tree
Showing 18 changed files with 759 additions and 21 deletions.
15 changes: 15 additions & 0 deletions docs/reference/configuration.txt
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,16 @@ for details on driver options.
# if the database name is not explicitly defined. (default: nil)
app_name: MyApplicationName

# Type of executor for queries scheduled using ``load_async`` method.
#
# There are two possible values for this option:
#
# - :immediate - Queries will be immediately executed on a current thread.
# This is the default option.
# - :global_thread_pool - Queries will be executed asynchronously in
# background using a thread pool.
#async_query_executor: :immediate

# Mark belongs_to associations as required by default, so that saving a
# model with a missing belongs_to association will trigger a validation
# error. (default: true)
Expand Down Expand Up @@ -358,6 +368,11 @@ for details on driver options.
# Raise an exception when a field is redefined. (default: false)
duplicate_fields_exception: false

# Defines how many asynchronous queries can be executed concurrently.
# This option should be set only if `async_query_executor` option is set
# to `:global_thread_pool`.
#global_executor_concurrency: nil

# Include the root model name in json serialization. (default: false)
include_root_in_json: false

Expand Down
69 changes: 68 additions & 1 deletion docs/reference/queries.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2410,5 +2410,72 @@ will query the database and separately cache their results.
To use the cached results, call ``all.to_a.first`` on the model class.


.. _legacy-query-cache-limitations:
.. _load-async:

Asynchronous Queries
====================

Mongoid allows running database queries asynchronously in the background.
This can be beneficial when there is a need to get documents from different
collections.

In order to schedule an asynchronous query call the ``load_async`` method on a
``Criteria``:

.. code-block:: ruby

class PagesController < ApplicationController
def index
@active_bands = Band.where(active: true).load_async
@best_events = Event.best.load_async
@public_articles = Article.where(public: true).load_async
end
end

In the above example three queries will be scheduled for asynchronous execution.
Results of the queries can be later accessed as usual:

.. code-block:: html

<ul>
<%- @active_bands.each do -%>
<li><%= band.name %></li>
<%- end -%>
</ul>

Even if a query is scheduled for asynchronous execution, it might be executed
synchronously on the caller's thread. There are three possible scenarios depending
on when the query results are being accessed:

#. If the scheduled asynchronous task has been already executed, the results are returned.
#. If the task has been started, but not finished yet, the caller's thread blocks until the task is finished.
#. 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.

.. note::

Even though ``load_async`` method returns a ``Criteria`` object, you should not
do any operations on this object except accessing query results. The query is
scheduled for execution immediately after calling ``load_async``, therefore
later changes to the `Criteria`` object may not be applied.


Configuring asynchronous query execution
----------------------------------------

Asynchronous queries are disabled by default. When asynchronous queries are
disabled, ``load_async`` will execute the query immediately on the current thread,
blocking as necessary. Therefore, calling ``load_async`` on criteria in this case
is roughly the equivalent of calling ``to_a`` to force query execution.

In order to enable asynchronous query execution, the following config options
must be set:

.. code-block:: yaml

development:
...
options:
# Execute asynchronous queries using a global thread pool.
async_query_executor: :global_thread_pool
# Number of threads in the pool. The default is 4.
# global_executor_concurrency: 4
15 changes: 10 additions & 5 deletions docs/release-notes/mongoid-8.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ The complete list of releases is available `on GitHub
please consult GitHub releases for detailed release notes and JIRA for
the complete list of issues fixed in each release, including bug fixes.

Added ``load_async`` method on ``Criteria`` to asynchronously load documents
----------------------------------------------------------------------------

The new ``load_async`` method on ``Criteria`` allows :ref:`running database queries asynchronously <load-async>`.


Added ``attribute_before_last_save``, ``saved_change_to_attribute``, ``saved_change_to_attribute?``, and ``will_save_change_to_attribute?`` methods
----------------------------------------------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -245,8 +250,8 @@ for more details.
Added ``readonly!`` method and ``legacy_readonly`` feature flag
---------------------------------------------------------------

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

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

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

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

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


15 changes: 15 additions & 0 deletions lib/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ en:
A collation option is only supported if the query is executed on a MongoDB server
with version >= 3.4."
resolution: "Remove the collation option from the query."
invalid_async_query_executor:
message: "Invalid async_query_executor option: %{executor}."
summary: "A invalid async query executor was specified.
The valid options are: %{options}."
resolution: "Pick an allowed option or fix the typo. If you were
expecting the option to be there, please consult the following page
with respect to Mongoid's configuration:\n\n
\_\_https://www.mongodb.com/docs/mongoid/current/reference/configuration/#mongoid-configuration-options"
invalid_config_file:
message: "Invalid configuration file: %{path}."
summary: "Your mongoid.yml configuration file does not contain the
Expand Down Expand Up @@ -222,6 +230,13 @@ en:
resolution: "Please provide a valid type value for the field.
Refer to:
https://docs.mongodb.com/mongoid/current/reference/fields/#using-symbols-or-strings-instead-of-classes"
invalid_global_executor_concurrency:
message: "Invalid global_executor_concurrency option."
summary: "You set global_executor_concurrency while async_query_executor
option is not set to :global_thread_pool. The global_executor_concurrency is
allowed only for the global thread pool executor."
resolution: "Set global_executor_concurrency option to :global_thread_pool
or remove global_executor_concurrency option."
invalid_includes:
message: "Invalid includes directive: %{klass}.includes(%{args})"
summary: "Eager loading in Mongoid only supports providing arguments
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
require "active_support/time_with_zone"
require "active_model"

require 'concurrent-ruby'

require "mongo"
require 'mongo/active_support'

Expand Down
16 changes: 15 additions & 1 deletion lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,20 @@ module Config
# always return a Hash.
option :legacy_attributes, default: false

# When this flag is false, a document will become read-only only once the
# Sets the async_query_executor for the application. By default the thread pool executor
# is set to `:immediate. Options are:
#
# - :immediate - Initializes a single +Concurrent::ImmediateExecutor+
# - :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+
# that uses the +async_query_concurrency+ for the +max_threads+ value.
option :async_query_executor, default: :immediate

# Defines how many asynchronous queries can be executed concurrently.
# This option should be set only if `async_query_executor` is set
# to `:global_thread_pool`.
option :global_executor_concurrency, default: nil

# When this flag is false, a document will become read-only only once the
# #readonly! method is called, and an error will be raised on attempting
# to save or update such documents, instead of just on delete. When this
# flag is true, a document is only read-only if it has been projected
Expand Down Expand Up @@ -308,6 +321,7 @@ def truncate!
# @param [ Hash ] options The configuration options.
def options=(options)
if options
Validators::AsyncQueryExecutor.validate(options)
options.each_pair do |option, value|
Validators::Option.validate(option)
send("#{option}=", value)
Expand Down
1 change: 1 addition & 0 deletions lib/mongoid/config/validators.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# frozen_string_literal: true

require "mongoid/config/validators/async_query_executor"
require "mongoid/config/validators/option"
require "mongoid/config/validators/client"
24 changes: 24 additions & 0 deletions lib/mongoid/config/validators/async_query_executor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module Mongoid
module Config
module Validators

# Validator for async query executor configuration.
#
# @api private
module AsyncQueryExecutor
extend self


def validate(options)
if options.key?(:async_query_executor)
if options[:async_query_executor].to_sym == :immediate && !options[:global_executor_concurrency].nil?
raise Errors::InvalidGlobalExecutorConcurrency
end
end
end
end
end
end
end
12 changes: 12 additions & 0 deletions lib/mongoid/contextual.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ def context
@context ||= create_context
end

# Instructs the context to schedule an asynchronous loading of documents
# specified by the criteria.
#
# Note that depending on the context and on the Mongoid configuration,
# documents can be loaded synchronously on the caller's thread.
#
# @return [ Criteria ] Returns self.
def load_async
context.load_async if context.respond_to?(:load_async)
self
end

private

# Create the context for the queries to execute. Will be memory for
Expand Down
47 changes: 33 additions & 14 deletions lib/mongoid/contextual/mongo.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# frozen_string_literal: true

require "mongoid/contextual/mongo/documents_loader"
require "mongoid/contextual/atomic"
require "mongoid/contextual/aggregable/mongo"
require "mongoid/contextual/command"
Expand Down Expand Up @@ -37,6 +38,8 @@ class Mongo
# @attribute [r] view The Mongo collection view.
attr_reader :view

attr_reader :documents_loader

# Get the number of documents matching the query.
#
# @example Get the number of matching documents.
Expand Down Expand Up @@ -773,6 +776,17 @@ def third_to_last!
third_to_last || raise_document_not_found_error
end

# Schedule a task to load documents for the context.
#
# Depending on the Mongoid configuration, the scheduled task can be executed
# immediately on the caller's thread, or can be scheduled for an
# asynchronous execution.
#
# @api private
def load_async
@documents_loader ||= DocumentsLoader.new(view, klass, criteria)
end

private

# Update the documents for the provided method.
Expand Down Expand Up @@ -840,24 +854,29 @@ def inverse_sorting
Hash[sort.map{|k, v| [k, -1*v]}]
end

# Get the documents the context should iterate. This follows 3 rules:
#
# 1. If the query is cached, and we already have documents loaded, use
# them.
# 2. If we are eager loading, then eager load the documents and use
# those.
# 3. Use the query.
#
# @api private
# Get the documents the context should iterate.
#
# @example Get the documents for iteration.
# context.documents_for_iteration
# If the documents have been already preloaded by `Document::Loader`
# instance, they will be used.
#
# @return [ Array<Document> | Mongo::Collection::View ] The docs to iterate.
#
# @api private
def documents_for_iteration
return view unless eager_loadable?
docs = view.map{ |doc| Factory.from_db(klass, doc, criteria) }
eager_load(docs)
if @documents_loader
if @documents_loader.started?
@documents_loader.value!
else
@documents_loader.unschedule
@documents_loader.execute
end
else
return view unless eager_loadable?
docs = view.map do |doc|
Factory.from_db(klass, doc, criteria)
end
eager_load(docs)
end
end

# Yield to the document.
Expand Down
Loading

0 comments on commit 855102f

Please sign in to comment.