Skip to content

Commit

Permalink
Merge remote-tracking branch 'remotes/mongodb/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnyshields committed May 6, 2024
2 parents 5b4975b + 88f4b1a commit 479d8b8
Show file tree
Hide file tree
Showing 20 changed files with 305 additions and 123 deletions.
109 changes: 48 additions & 61 deletions docs/reference/configuration.txt
Original file line number Diff line number Diff line change
Expand Up @@ -863,58 +863,57 @@ as the following example shows:
Usage with Forking Servers
==========================

When using Mongoid with a forking web server such as Puma, Unicorn or
Passenger, it is recommended to not perform any operations on Mongoid models
in the parent process prior to the fork.

When a process forks, Ruby threads are not transferred to the child processes
and the Ruby driver Client objects lose their background monitoring. The
application will typically seem to work just fine until the deployment
state changes (for example due to network errors, a maintenance event) at
which point the application is likely to start getting ``NoServerAvailable``
exception when performing MongoDB operations.

If the parent process needs to perform operations on the MongoDB database,
reset all clients in the workers after they forked. How to do so depends
on the web server being used.

If the parent process does not need to perform operations on the MongoDB
database after child processes are forked, close the clients in the parent
prior to forking children. If the parent process performs operations on a Mongo
client and does not close it, the parent process will continue consuming a
connection slot in the cluster and will continue monitoring the cluster for
as long as the parent remains alive.

.. note::

The close/reconnect pattern described here should be used with Ruby driver
version 2.6.2 or higher. Previous driver versions did not recreate
monitoring threads when reconnecting.
When using Mongoid with a forking web server such as Puma, or any application
that otherwise forks to spawn child processes, special considerations apply.

If possible, we recommend to not perform any MongoDB operations in the parent
process prior to forking, which will avoid any forking-related pitfalls.

A detailed technical explanation of how the Mongo Ruby Driver handles forking
is given in the `driver's "Usage with Forking Servers" documentation
<https://www.mongodb.com/docs/ruby-driver/current/reference/create-client/#usage-with-forking-servers>`.
In a nutshell, to avoid various connection errors such as ``Mongo::Error::SocketError``
and ``Mongo::Error::NoServerAvailable``, you must do the following:

1. Disconnect MongoDB clients in the parent Ruby process immediately *before*
forking using ``Mongoid.disconnect_clients``. This ensures the parent and child
process do not accidentally reuse the same sockets and have I/O conflicts.
Note that ``Mongoid.disconnect_clients`` does not disrupt any in-flight
MongoDB operations, and will automatically reconnect when you perform new
operations.
2. Reconnect your MongoDB clients in the child Ruby process immediately *after*
forking using ``Mongoid.reconnect_clients``. This is required to respawn
the driver's monitoring threads in the child process.

Most web servers provide hooks that can be used by applications to
perform actions when the worker processes are forked. The following
are configuration examples for several common Ruby web servers.

Puma
----

Use the ``on_worker_boot`` hook to reconnect clients in the workers and
the ``before_fork`` hook to close clients in the parent process
(`Puma documentation <https://puma.io/puma/>`_):
the ``before_fork`` and ``on_refork`` hooks to close clients in the
parent process (`Puma documentation <https://puma.io/puma/#clustered-mode>`_).

.. code-block:: ruby

on_worker_boot do
if defined?(Mongoid)
Mongoid::Clients.clients.each do |name, client|
client.close
client.reconnect
end
else
raise "Mongoid is not loaded. You may have forgotten to enable app preloading."
end
end
# config/puma.rb

# Runs in the Puma master process before it forks a child worker.
before_fork do
if defined?(Mongoid)
Mongoid.disconnect_clients
end
Mongoid.disconnect_clients
end

# Required when using Puma's fork_worker option. Runs in the
# child worker 0 process before it forks grandchild workers.
on_refork do
Mongoid.disconnect_clients
end

# Runs in each Puma child process after it forks from its parent.
on_worker_boot do
Mongoid.reconnect_clients
end

Unicorn
Expand All @@ -926,21 +925,14 @@ the ``before_fork`` hook to close clients in the parent process

.. code-block:: ruby

after_fork do |server, worker|
if defined?(Mongoid)
Mongoid::Clients.clients.each do |name, client|
client.close
client.reconnect
end
else
raise "Mongoid is not loaded. You may have forgotten to enable app preloading."
end
# config/unicorn.rb

before_fork do |_server, _worker|
Mongoid.disconnect_clients
end

before_fork do |server, worker|
if defined?(Mongoid)
Mongoid.disconnect_clients
end
after_fork do |_server, _worker|
Mongoid.reconnect_clients
end

Passenger
Expand All @@ -956,12 +948,7 @@ before the workers are forked.

if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
if forked
Mongoid::Clients.clients.each do |name, client|
client.close
client.reconnect
end
end
Mongoid.reconnect_clients if forked
end
end

Expand Down
10 changes: 10 additions & 0 deletions lib/mongoid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ def disconnect_clients
Clients.disconnect
end

# Reconnect all active clients.
#
# @example Reconnect all active clients.
# Mongoid.reconnect_clients
#
# @return [ true ] True.
def reconnect_clients
Clients.reconnect
end

# Convenience method for getting a named client.
#
# @example Get a named client.
Expand Down
12 changes: 12 additions & 0 deletions lib/mongoid/clients.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ def default
# @return [ true ] True.
def disconnect
clients.each_value(&:close)
true
end

# Reconnect all active clients.
#
# @example Reconnect all active clients.
# Mongoid::Clients.reconnect
#
# @return [ true ] True.
def reconnect
clients.each_value(&:reconnect)
true
end

# Get a stored client with the provided name. If no client exists
Expand Down
4 changes: 4 additions & 0 deletions lib/mongoid/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,14 @@ def time_zone
# config.running_with_passenger?
#
# @return [ true | false ] If the app is deployed on Passenger.
#
# @deprecated
def running_with_passenger?
@running_with_passenger ||= defined?(PhusionPassenger)
end

Mongoid.deprecate(self, :running_with_passenger?)

private

def set_log_levels
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/config/defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def load_defaults(version)
when '8.1'
self.immutable_ids = false
self.legacy_persistence_context_behavior = true
self.around_callbacks_for_embeds = true
self.prevent_multiple_calls_of_embedded_callbacks = false

load_defaults '9.0'
when '9.0'
Expand Down
14 changes: 7 additions & 7 deletions lib/mongoid/extensions/date.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ def demongoize(object)
def mongoize(object)
return if object.blank?

begin
time = if object.is_a?(String)
# https://jira.mongodb.org/browse/MONGOID-4460
::Time.parse(object)
else
object.__mongoize_time__
end
time = begin
if object.is_a?(String)
# https://jira.mongodb.org/browse/MONGOID-4460
::Time.parse(object)
else
object.try(:__mongoize_time__)
end
rescue ArgumentError
nil
end
Expand Down
16 changes: 0 additions & 16 deletions lib/mongoid/extensions/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,6 @@ def __find_args__
end
Mongoid.deprecate(self, :__find_args__)

# Mongoize a plain object into a time.
#
# @note This method should not be used, because it does not
# return correct results for non-Time objects. Override
# __mongoize_time__ in classes that are time-like to return an
# instance of Time or ActiveSupport::TimeWithZone.
#
# @example Mongoize the object.
# object.__mongoize_time__
#
# @return [ Object ] self.
# @deprecated
def __mongoize_time__
self
end

# Try to form a setter from this object.
#
# @example Try to form a setter.
Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/extensions/time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def mongoize(object)
return if object.blank?

begin
time = object.__mongoize_time__
time = object.try(:__mongoize_time__)
rescue ArgumentError
return
end
Expand Down
31 changes: 31 additions & 0 deletions lib/mongoid/inspectable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,37 @@ def inspect
"#<#{self.class.name} _id: #{_id}, #{inspection * ', '}>"
end

# This pretty prints the same information as the inspect method. This is
# meant to be called by the standard 'pp' library.
#
# @param [ PP ] pretty_printer The pretty printer.
#
# @example Pretty print the document.
# person.pretty_inspect
#
# @api private
def pretty_print(pretty_printer)
keys = fields.keys | attributes.keys
pretty_printer.group(1, "#<#{self.class.name}", '>') do
sep = lambda { pretty_printer.text(',') }
pretty_printer.seplist(keys, sep) do |key|
pretty_printer.breakable
field = fields[key]
as = "(#{field.options[:as]})" if field && field.options[:as]
pretty_printer.text("#{key}#{as}")
pretty_printer.text(':')
pretty_printer.group(1) do
pretty_printer.breakable
if key == "_id"
pretty_printer.text(_id.to_s)
else
pretty_printer.pp(@attributes[key])
end
end
end
end
end

private

# Get an array of inspected fields for the document.
Expand Down
20 changes: 9 additions & 11 deletions lib/mongoid/scopable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -288,17 +288,15 @@ def check_scope_validity(value)
# @return [ Method ] The defined method.
def define_scope_method(name)
singleton_class.class_eval do
ruby2_keywords(
define_method(name) do |*args, **kwargs|
scoping = _declared_scopes[name]
scope = instance_exec(*args, **kwargs, &scoping[:scope])
extension = scoping[:extension]
to_merge = scope || queryable
criteria = to_merge.empty_and_chainable? ? to_merge : with_default_scope.merge(to_merge)
criteria.extend(extension)
criteria
end
)
define_method(name) do |*args, **kwargs|
scoping = _declared_scopes[name]
scope = instance_exec(*args, **kwargs, &scoping[:scope])
extension = scoping[:extension]
to_merge = scope || queryable
criteria = to_merge.empty_and_chainable? ? to_merge : with_default_scope.merge(to_merge)
criteria.extend(extension)
criteria
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/mongoid/version.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module Mongoid
VERSION = '9.0.0.0.alpha1'
VERSION = '10.0.0.0.alpha1'

GEM_NAME = 'mongoid-ultra'

Expand Down
44 changes: 44 additions & 0 deletions spec/mongoid/clients_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1194,4 +1194,48 @@ class StoreChild2 < StoreParent
end
end
end

context '#disconnect' do

let(:clients) do
Mongoid::Clients.clients.values
end

before do
Band.all.entries
end

it 'disconnects from all active clients' do
clients.each do |client|
expect(client).to receive(:close).and_call_original
end
Mongoid::Clients.disconnect
end

it 'returns true' do
expect(Mongoid::Clients.disconnect).to eq(true)
end
end

context '#reconnect' do

let(:clients) do
Mongoid::Clients.clients.values
end

before do
Band.all.entries
end

it 'reconnects all active clients' do
clients.each do |client|
expect(client).to receive(:reconnect).and_call_original
end
Mongoid::Clients.reconnect
end

it 'returns true' do
expect(Mongoid::Clients.reconnect).to eq(true)
end
end
end
4 changes: 4 additions & 0 deletions spec/mongoid/config/defaults_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@
it 'uses settings for 8.1' do
expect(Mongoid.immutable_ids).to be false
expect(Mongoid.legacy_persistence_context_behavior).to be true
expect(Mongoid.around_callbacks_for_embeds).to be true
expect(Mongoid.prevent_multiple_calls_of_embedded_callbacks).to be false
end
end

shared_examples 'does not use settings for 8.1' do
it 'does not use settings for 8.1' do
expect(Mongoid.immutable_ids).to be true
expect(Mongoid.legacy_persistence_context_behavior).to be false
expect(Mongoid.around_callbacks_for_embeds).to be false
expect(Mongoid.prevent_multiple_calls_of_embedded_callbacks).to be true
end
end

Expand Down

0 comments on commit 479d8b8

Please sign in to comment.