lib/mongoid/tasks/database.rb
# frozen_string_literal: true
# rubocop:todo all
module Mongoid
module Tasks
# Utility module to manage database collections, indexes, sharding, etc.
# Invoked from Rake tasks.
module Database
extend self
# Create collections for each model given the provided globs and the class is
# not embedded.
#
# @param [ Array<Mongoid::Document> ] models. Array of document classes for
# which collections should be created. Defaulted to all document classes
# in the application.
# @param [ true | false ] force If true, the method will drop existing
# collections before creating new ones. If false, the method will create
# only new collection (that do not exist in the database).
def create_collections(models = ::Mongoid.models, force: false)
models.each do |model|
if !model.embedded? || model.cyclic?
model.create_collection(force: force)
logger.info("MONGOID: Created collection for #{model}:")
else
logger.info("MONGOID: collection options ignored on: #{model}, please define in the root model.")
end
rescue Exception
logger.error "error while creating collection for #{model}"
raise
end
end
# Create indexes for each model given the provided globs and the class is
# not embedded.
#
# @example Create all the indexes.
# Mongoid::Tasks::Database.create_indexes
#
# @return [ Array<Class> ] The indexed models.
def create_indexes(models = ::Mongoid.models)
models.each do |model|
next if model.index_specifications.empty?
if !model.embedded? || model.cyclic?
model.create_indexes
logger.info("MONGOID: Created indexes on #{model}:")
model.index_specifications.each do |spec|
logger.info("MONGOID: Index: #{spec.key}, Options: #{spec.options}")
end
model
else
logger.info("MONGOID: Index ignored on: #{model}, please define in the root model.")
nil
end
end.compact
end
# Submit requests for the search indexes to be created. This will happen
# asynchronously. If "wait" is true, the method will block while it
# waits for the indexes to be created.
#
# @param [ Array<Mongoid::Document> ] models the models to build search
# indexes for.
# @param [ true | false ] wait whether to wait for the indexes to be
# built.
def create_search_indexes(models = ::Mongoid.models, wait: true)
searchable = models.select { |m| m.search_index_specs.any? }
# queue up the search index creation requests
index_names_by_model = searchable.each_with_object({}) do |model, obj|
logger.info("MONGOID: Creating search indexes on #{model}...")
obj[model] = model.create_search_indexes
end
wait_for_search_indexes(index_names_by_model) if wait
end
# Return the list of indexes by model that exist in the database but aren't
# specified on the models.
#
# @example Return the list of unused indexes.
# Mongoid::Tasks::Database.undefined_indexes
#
# @return [ Array<Hash> ] The list of undefined indexes by model.
def undefined_indexes(models = ::Mongoid.models)
undefined_by_model = {}
models.each do |model|
unless model.embedded?
begin
model.collection.indexes(session: model.send(:_session)).each do |index|
# ignore default index
unless index['name'] == '_id_'
key = index['key'].symbolize_keys
spec = model.index_specification(key, index['name'])
unless spec
# index not specified
undefined_by_model[model] ||= []
undefined_by_model[model] << index
end
end
end
rescue Mongo::Error::OperationFailure; end
end
end
undefined_by_model
end
# Remove indexes that exist in the database but aren't specified on the
# models.
#
# @example Remove undefined indexes.
# Mongoid::Tasks::Database.remove_undefined_indexes
#
# @return [ Hash{Class => Array(Hash)}] The list of indexes that were removed by model.
def remove_undefined_indexes(models = ::Mongoid.models)
undefined_indexes(models).each do |model, indexes|
indexes.each do |index|
key = index['key'].symbolize_keys
collection = model.collection
collection.indexes(session: model.send(:_session)).drop_one(key)
logger.info(
"MONGOID: Removed index '#{index['name']}' on collection " +
"'#{collection.name}' in database '#{collection.database.name}'."
)
end
end
end
# Remove indexes for each model given the provided globs and the class is
# not embedded.
#
# @example Remove all the indexes.
# Mongoid::Tasks::Database.remove_indexes
#
# @return [ Array<Class> ] The un-indexed models.
def remove_indexes(models = ::Mongoid.models)
models.each do |model|
next if model.embedded?
begin
model.remove_indexes
rescue Mongo::Error::OperationFailure
next
end
model
end.compact
end
# Remove all search indexes from the given models.
#
# @params [ Array<Mongoid::Document> ] models the models to remove
# search indexes from.
def remove_search_indexes(models = ::Mongoid.models)
models.each do |model|
next if model.embedded?
model.remove_search_indexes
end
end
# Shard collections for models that declare shard keys.
#
# Returns the model classes that have had their collections sharded,
# including model classes whose collections had already been sharded
# prior to the invocation of this method.
#
# @example Shard all collections
# Mongoid::Tasks::Database.shard_collections
#
# @return [ Array<Class> ] The sharded models
def shard_collections(models = ::Mongoid.models)
models.map do |model|
next if model.shard_config.nil?
if model.embedded? && !model.cyclic?
logger.warn("MONGOID: #{model} has shard config but is embedded")
next
end
unless model.collection.cluster.sharded?
logger.warn("MONGOID: #{model} has shard config but is not persisted in a sharded cluster: #{model.collection.cluster.summary}")
next
end
# Database of the collection must exist in order to run collStats.
# Depending on server version, the collection itself must also
# exist.
# MongoDB does not have a command to create the database; the best
# approximation of it is to create the collection we want.
# On older servers, creating a collection that already exists is
# an error.
# Additionally, 3.6 and potentially older servers do not provide
# the error code when they are asked to collStats a non-existent
# collection (https://jira.mongodb.org/browse/SERVER-50070).
begin
stats = model.collection.database.command(collStats: model.collection.name).first
rescue Mongo::Error::OperationFailure => exc
# Code 26 is database does not exist.
# Code 8 is collection does not exist, as of 4.0.
# On 3.6 and earlier match the text of exception message.
if exc.code == 26 || exc.code == 8 ||
exc.code.nil? && exc.message =~ /not found/
then
model.collection.create
stats = model.collection.database.command(collStats: model.collection.name).first
else
raise
end
end
stats = model.collection.database.command(collStats: model.collection.name).first
if stats[:sharded]
logger.info("MONGOID: #{model.collection.namespace} is already sharded for #{model}")
next model
end
admin_db = model.collection.client.use(:admin).database
begin
admin_db.command(enableSharding: model.collection.database.name)
rescue Mongo::Error::OperationFailure => exc
# Server 2.6 fails if sharding is already enabled
if exc.code == 23 || exc.code.nil? && exc.message =~ /already enabled/
# Nothing
else
raise
end
end
begin
admin_db.command(shardCollection: model.collection.namespace, **model.shard_config)
rescue Mongo::Error::OperationFailure => e
logger.error("MONGOID: Failed to shard collection #{model.collection.namespace} for #{model}: #{e.class}: #{e}")
next
end
logger.info("MONGOID: Sharded collection #{model.collection.namespace} for #{model}")
model
end.compact
end
private
def logger
Mongoid.logger
end
# Waits for the search indexes to be built on the given models.
#
# @param [ Hash<Mongoid::Document, Array<String>> ] models a mapping of
# index names for each model
def wait_for_search_indexes(models)
logger.info('MONGOID: Waiting for search indexes to be created')
logger.info('MONGOID: Press ctrl-c to skip the wait and let the indexes be created in the background')
models.each do |model, names|
model.wait_for_search_indexes(names) do |status|
if status.ready?
puts
logger.info("MONGOID: Search indexes on #{model} are READY")
else
print '.'
$stdout.flush
end
end
end
rescue Interrupt
# ignore ctrl-C here; we assume it is meant only to skip
# the wait, and that subsequent tasks ought to continue.
logger.info('MONGOID: Skipping the wait for search indexes; they will be created in the background')
end
end
end
end