diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt index 93e2581773..2d82faf24a 100644 --- a/docs/reference/configuration.txt +++ b/docs/reference/configuration.txt @@ -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 +`. +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 `_): +the ``before_fork`` and ``on_refork`` hooks to close clients in the +parent process (`Puma documentation `_). .. 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 @@ -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 @@ -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 diff --git a/lib/mongoid.rb b/lib/mongoid.rb index 7bc8ac8bfb..4d305f178b 100644 --- a/lib/mongoid.rb +++ b/lib/mongoid.rb @@ -100,6 +100,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. diff --git a/lib/mongoid/clients.rb b/lib/mongoid/clients.rb index feaa889df8..25e22afa88 100644 --- a/lib/mongoid/clients.rb +++ b/lib/mongoid/clients.rb @@ -47,9 +47,19 @@ def default # # @return [ true ] True. def disconnect - clients.values.each do |client| - client.close - end + 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 diff --git a/spec/mongoid/clients_spec.rb b/spec/mongoid/clients_spec.rb index 055500cf8e..0e923ee447 100644 --- a/spec/mongoid/clients_spec.rb +++ b/spec/mongoid/clients_spec.rb @@ -1216,4 +1216,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 diff --git a/spec/mongoid_spec.rb b/spec/mongoid_spec.rb index c3a1be773a..038f8f9b9c 100644 --- a/spec/mongoid_spec.rb +++ b/spec/mongoid_spec.rb @@ -81,6 +81,32 @@ end Mongoid.disconnect_clients end + + it "returns true" do + expect(Mongoid.disconnect_clients).to eq(true) + end + end + + describe ".reconnect_clients" 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.reconnect_clients + end + + it "returns true" do + expect(Mongoid.reconnect_clients).to eq(true) + end end describe ".client" do