Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MONGOID-5445 Add load_async to criteria #5454

Merged
merged 37 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f1feea3
MONGOID-5445 Add load_async to criteria
comandeo-mongo Sep 7, 2022
b08665e
Add specs
comandeo-mongo Sep 8, 2022
b82701c
Docs
comandeo-mongo Sep 9, 2022
471eeda
Merge remote-tracking branch 'upstream/master' into 5445-load-async
comandeo-mongo Sep 9, 2022
3998d46
Merge remote-tracking branch 'upstream/master' into 5445-load-async
comandeo-mongo Sep 14, 2022
686b772
Specs and docs
comandeo-mongo Sep 14, 2022
bd23dc7
Fix spec
comandeo-mongo Sep 14, 2022
ea28262
More docs
comandeo-mongo Sep 14, 2022
5330c3b
Update docs/reference/queries.txt
comandeo-mongo Sep 14, 2022
ffe952c
Update docs/reference/queries.txt
comandeo-mongo Sep 14, 2022
0e8b843
Update docs/reference/queries.txt
comandeo-mongo Sep 14, 2022
acd1a94
Update docs/reference/queries.txt
comandeo-mongo Sep 14, 2022
a166d8a
Update docs/reference/queries.txt
comandeo-mongo Sep 14, 2022
a7a975d
Update docs/release-notes/mongoid-8.1.txt
comandeo-mongo Sep 14, 2022
6de107f
Update lib/mongoid/config.rb
comandeo-mongo Sep 14, 2022
171c8a9
Update lib/mongoid/contextual/mongo/documents_loader.rb
comandeo-mongo Sep 14, 2022
69f68bb
Update lib/mongoid/contextual/mongo/documents_loader.rb
comandeo-mongo Sep 14, 2022
4a3130a
Update spec/mongoid/contextual/mongo/documents_loader_spec.rb
comandeo-mongo Sep 14, 2022
5a99a83
Fix code review remarks
comandeo-mongo Sep 14, 2022
7424de1
Update docs/reference/queries.txt
comandeo-mongo Sep 14, 2022
1bee49a
Update lib/mongoid/config.rb
comandeo-mongo Sep 14, 2022
26ebe60
Fix code review remarks
comandeo-mongo Sep 15, 2022
83633da
Fix dependencies
comandeo-mongo Sep 15, 2022
b178fbf
Fix locking
comandeo-mongo Sep 16, 2022
0c2d4ca
Update docs/reference/queries.txt
comandeo-mongo Sep 27, 2022
a8bd588
Update docs/reference/queries.txt
comandeo-mongo Sep 27, 2022
a917015
Update docs/reference/queries.txt
comandeo-mongo Sep 27, 2022
7ceb4c4
Update docs/reference/queries.txt
comandeo-mongo Sep 27, 2022
5a80cc4
Update docs/reference/queries.txt
comandeo-mongo Sep 27, 2022
c8d351b
Update lib/mongoid/config/validators/async_query_executor.rb
comandeo-mongo Sep 27, 2022
2374e42
MONGOID-5164 Use None criteria instead of $in with empty list in HABT…
Neilshweky Sep 15, 2022
73059a9
RUBY-2846 use BSON _bson_to_i method if it exists (#5473)
Neilshweky Sep 23, 2022
f010f08
MONGOID-5227 Add test and adjust docs for dotted attribute assignment…
Neilshweky Sep 23, 2022
58c2d49
Fix code review remarks
comandeo-mongo Sep 28, 2022
25b8f75
Shutdown pool gracefully
comandeo-mongo Sep 29, 2022
34a76da
5445
comandeo-mongo Sep 29, 2022
5e85f80
Disable some specs on JRuby
comandeo-mongo Sep 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion docs/reference/queries.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2388,5 +2388,67 @@ 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 ``load_async`` method on a
comandeo marked this conversation as resolved.
Show resolved Hide resolved
``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 ``Criteria`` object, you should not
comandeo marked this conversation as resolved.
Show resolved Hide resolved
do any operations on this object except accessing query results. The query is
p-mongo marked this conversation as resolved.
Show resolved Hide resolved
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. In order to enable them, the
following config options should be set:
p-mongo marked this conversation as resolved.
Show resolved Hide resolved

.. 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. This option is defaulted to 4.
comandeo marked this conversation as resolved.
Show resolved Hide resolved
global_executor_concurrency: 5
p-mongo marked this conversation as resolved.
Show resolved Hide resolved
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.


10 changes: 10 additions & 0 deletions lib/mongoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,14 @@ def discriminator_key=(value)
class << self
prepend GlobalDiscriminatorKeyAssignment
end

def global_thread_pool_async_query_executor
concurrency = Mongoid.global_executor_concurrency || 4
@@global_thread_pool_async_query_executor ||= Concurrent::ThreadPoolExecutor.new(
min_threads: 0,
max_threads: concurrency,
max_queue: concurrency * 4,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the max queue 4 times the number of threads?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied this from Rails code. Unfortunately, there is no explanations why they pick four. Honestly, I do not know what would be a sane default here.

Do you think we should make this configurable?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if rails does it, it must be right!

fallback_policy: :caller_runs
)
end
end
15 changes: 14 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 an application. By default the thread pool executor
comandeo marked this conversation as resolved.
Show resolved Hide resolved
# 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
p-mongo marked this conversation as resolved.
Show resolved Hide resolved

# 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`, it will be ignored otherwise.
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
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 @@ -777,6 +780,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 @@ -844,24 +858,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
Neilshweky marked this conversation as resolved.
Show resolved Hide resolved
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
135 changes: 135 additions & 0 deletions lib/mongoid/contextual/mongo/documents_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
require "mongoid/association/eager_loadable"

module Mongoid
module Contextual
class Mongo
# Loads documents for the provided criteria.
#
# @api private
class DocumentsLoader
extend Forwardable
include Association::EagerLoadable

def_delegators :@future, :value!, :value, :wait!, :wait

# Synchronous executor to be used when async_query_executor config option
# is set to :immediate. This excutor runs all operations on the current
# thread, blocking as necessary.
IMMEDIATE_EXECUTOR = Concurrent::ImmediateExecutor.new
p-mongo marked this conversation as resolved.
Show resolved Hide resolved

# Returns suitable executor according to Mongoid config options.
#
# @return [ Concurrent::ImmediateExecutor | Concurrent::ThreadPoolExecutor ] The executor
# to be used to execute document loading tasks.
def self.executor
case Mongoid.async_query_executor
when :immediate
IMMEDIATE_EXECUTOR
when :global_thread_pool
Mongoid.global_thread_pool_async_query_executor
end
p-mongo marked this conversation as resolved.
Show resolved Hide resolved
end

# @return [ Mongoid::Criteria ] Criteria that specifies which documents should
# be loaded. Exposed here because `eager_loadable?` method from
# `Association::EagerLoadable` expects is to be available.
attr_accessor :criteria

# Instantiates the document loader instance and immediately schedules
# its execution using the provided executor.
#
# @param [ Mongo::Collection::View ] view The collection view to get
# records from the database.
# @param [ Class ] klass Mongoid model class to instantiate documents.
# All records obtained from the database will be converted to an
# instance of this class, if possible.
# @param [ Mongoid::Criteria ] criteria. Criteria that specifies which
# documents should be loaded.
# @param [ Concurrent::AbstractExecutorService ] executor. Executor that
# is capable of running `Concurrent::Promises::Future` instances.
def initialize(view, klass, criteria, executor: self.class.executor)
@view = view
@klass = klass
@criteria = criteria
@mutex = Mutex.new
@state = :pending
@future = Concurrent::Promises.future_on(executor, self) do |task|
if task.pending?
p-mongo marked this conversation as resolved.
Show resolved Hide resolved
task.send(:start)
p-mongo marked this conversation as resolved.
Show resolved Hide resolved
task.execute
end
end
end

# Returns false or true whether the loader is in pending state.
#
# Pending state means that the loader execution has been scheduled,
# but has not been started yet.
#
# @return [ true | false ] true if the loader is in pending state,
# otherwise false.
def pending?
@mutex.synchronize do
@state == :pending
end
end

# Returns false or true whether the loader is in started state.
#
# Started state means that the loader execution has been started.
# Note that the loader stays in this state even after the execution
# completed (successfully or failed).
#
# @return [ true | false ] true if the loader is in started state,
# otherwise false.
def started?
@mutex.synchronize do
@state == :started
end
end

# Mark the loader as unscheduled.
#
# If the loader is marked unscheduled, it will not be executed. The only
# option to load the documents is to call `execute` method directly.
#
# Please note that if execution of a task has been already started,
# unscheduling does not have any effect.
def unschedule
@mutex.synchronize do
@state = :cancelled unless @state == :started
end
end

# Loads records specified by `@criteria` from the database, and convert
# them to Mongoid documents of `@klass` type.
#
# This method is called by the task (possibly asynchronous) scheduled
# when creating an instance of the loader. However, this method can be
# called directly, if it is desired to execute loading on the caller
# thread immediately.
#
# Calling this method does not change the state of the loader.
#
# @return [ Array<Mongoid::Document> ] Array of document loaded from
# the database.
def execute
documents = @view.map do |doc|
Factory.from_db(@klass, doc, @criteria)
end
eager_load(documents) if eager_loadable?
documents
end

private

# Mark the loader as started.
def start
@mutex.synchronize do
@state = :started
end
end
end
end
end
end
1 change: 1 addition & 0 deletions mongoid.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Gem::Specification.new do |s|
# See: https://github.com/rails/rails/pull/43951
s.add_dependency("activemodel", ['>=5.1', '<7.1', '!= 7.0.0'])
s.add_dependency("mongo", ['>=2.18.0', '<3.0.0'])
s.add_dependency("concurrent-ruby", ['>= 1.1.10', '<1.2'])

# The ruby2_keywords gem is recommended for handling argument delegation issues,
# especially if support for 2.6 or prior is required.
Expand Down
Loading