[RUBY] Lire l'implémentation de la commutation automatique multi-DB Rails 6 (ActiveRecord :: Middleware :: DatabaseSelector)

Aperçu

Est-il possible de postuler à mon propre projet concernant la prise en charge de plusieurs bases de données implémentées à partir de Rails 6.0? Je suivrai la mise en œuvre pour juger que

Description dans le guide des rails

https://railsguides.jp/active_record_multiple_databases.html#コネクションの自動切り替えを有効にする

La fonction de commutation automatique permet à l'application de passer du primaire au réplica ou du réplica au primaire en fonction du verbe HTTP et de la présence ou de l'absence de l'écriture la plus récente. Lorsqu'une application reçoit une requête POST, PUT, DELETE ou PATCH, elle écrit automatiquement dans le primaire. L'application lira à partir du primaire jusqu'à ce que le temps spécifié s'écoule après l'écriture. Lorsque l'application reçoit une demande GET ou HEAD, elle lit à partir du réplica s'il n'y a pas d'écriture récente.

Il est précisé qu'il est jugé par deux types de jugement par méthode HTTP + jugement par temps d'écriture La méthode de réglage est décrite ci-dessous

config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

Lecture de code

Commençons par la partie application du middleware

Partie applicable du middleware dans Railtie

activerecord-6.0.3.2/lib/active_record/railtie.rb

  class Railtie < Rails::Railtie # :nodoc:
    config.active_record = ActiveSupport::OrderedOptions.new

    initializer "active_record.database_selector" do
      if options = config.active_record.delete(:database_selector)
        resolver = config.active_record.delete(:database_resolver)
        operations = config.active_record.delete(:database_resolver_context)
        config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options
      end
    end

--Exécuté si database_selector est défini --database_resolver et database_resolver_context sont maintenant juste passés

Ensuite, nous suivrons l'implémentation de la partie middleware

[Supplément] Que sont les options commandées?

https://api.rubyonrails.org/classes/ActiveSupport/OrderedOptions.html

Un mécanisme qui permet d'accéder à Hash par points Renvoie nil s'il n'est pas défini avec delete

Ruby2.6.5 pry(main)> x = ActiveSupport::OrderedOptions.new
=> {}
Ruby2.6.5 pry(main)> x.delete(:hoge)
=> nil

Implémentation du sélecteur de base de données

activerecord-6.0.3.2/lib/active_record/middleware/database_selector.rb

module ActiveRecord
  module Middleware
    class DatabaseSelector
      def initialize(app, resolver_klass = nil, context_klass = nil, options = {})
        @app = app
        @resolver_klass = resolver_klass || Resolver
        @context_klass = context_klass || Resolver::Session
        @options = options
      end

      attr_reader :resolver_klass, :context_klass, :options

      def call(env)
        request = ActionDispatch::Request.new(env)

        select_database(request) do
          @app.call(env)
        end
      end

--Si database_resolver n'est pas défini, DatabaseSelector :: Resolver est défini. --Si database_resolver_context n'est pas défini, DatabaseSelector :: Resolver :: Session est défini.

Ensuite, jetons un œil à l'important # select_database

DatabaseSelector#select_database

module ActiveRecord
  module Middleware
    class DatabaseSelector
      private
        def select_database(request, &blk)
          context = context_klass.call(request)
          resolver = resolver_klass.call(context, options)

          if reading_request?(request)
            resolver.read(&blk)
          else
            resolver.write(&blk)
          end
        end

        def reading_request?(request)
          request.get? || request.head?
        end
    end
  end
end

Puisque «context» est exécuté en premier et «résolveur» est créé sur cette base, vérifions d'abord «context».

Resolver::Session.call

class Resolver # :nodoc:
  class Session # :nodoc:
    def self.call(request)
      new(request.session)
    end

    def initialize(session)
      @session = session
    end

    attr_reader :session

.call est juste # new, ici seule la session de ʻActionDispatch :: Requestest définie Alors vérifiez ensuite le.call` du résolveur

Resolver.call

class Resolver # :nodoc:
  SEND_TO_REPLICA_DELAY = 2.seconds

  def self.call(context, options = {})
    new(context, options)
  end

  def initialize(context, options = {})
    @context = context
    @options = options
    @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
    @instrumenter = ActiveSupport::Notifications.instrumenter
  end

  attr_reader :context, :delay, :instrumenter

Je suis juste nouveau ici aussi, donc je vais lire un peu plus l'original DatabaseSelector # select_database.

Jugement partie de la méthode HTTP

def select_database(request, &blk)
  context = context_klass.call(request)
  resolver = resolver_klass.call(context, options)

  if reading_request?(request)
    resolver.read(&blk)
  else
    resolver.write(&blk)
  end
end

def reading_request?(request)
  request.get? || request.head?
end

Ici pour GET ou HEAD => lire Sinon => écrire Exécute le traitement du résolveur en tant que (le résolveur semble faire d'autres jugements)

Commençons par lire

Resolver#read

class Resolver # :nodoc:
  def read(&blk)
    if read_from_primary?
      read_from_primary(&blk)
    else
      read_from_replica(&blk)
    end
  end

  private
    def read_from_primary(&blk)
      ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: true) do
        instrumenter.instrument("database_selector.active_record.read_from_primary") do
          yield
        end
      end
    end

    def read_from_replica(&blk)
      ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role, prevent_writes: true) do
        instrumenter.instrument("database_selector.active_record.read_from_replica") do
          yield
        end
      end
    end

Il semble que le processus se déroule après avoir notifié par instrument le rôle à utiliser. Alors, qu'est-ce qui est jugé par «# read_from_primary?»?

  private
    def read_from_primary?
      !time_since_last_write_ok?
    end

    def send_to_replica_delay
      delay
    end

    def time_since_last_write_ok?
      Time.now - context.last_write_timestamp >= send_to_replica_delay
    end

context La soi-disant session # last_write_timestamp est Si le délai du premier config.active_record.database_selector = {delay: 2.seconds} n'est pas passé, définissez-le sur primaire. Si le délai est passé, la réplique sera visualisée.

Vérifiez donc le # last_write_timestamp de la session

class Session # :nodoc:
  # Converts milliseconds since epoch timestamp into a time object.
  def self.convert_timestamp_to_time(timestamp)
    timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
  end

  def last_write_timestamp
    self.class.convert_timestamp_to_time(session[:last_write])
  end

Changez simplement sesion [: last_write] en un objet temporel C'est la fin de la lecture, alors écrivez

Resolver#write

class Resolver # :nodoc:
  def write(&blk)
    write_to_primary(&blk)
  end

  private
    def write_to_primary(&blk)
      ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role, prevent_writes: false) do
        instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
          yield
        ensure
          context.update_last_write_timestamp
        end
      end
    end

C'est juste l'écriture dans le rôle d'écriture

class Session # :nodoc:
  def self.convert_time_to_timestamp(time)
    time.to_i * 1000 + time.usec / 1000
  end

  def update_last_write_timestamp
    session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
  end

Et donnez l'horodatage actuel à session [: last_write]

Résumé

Comme prévu, c'est Rails. Cela a été rendu très facile à comprendre.

Après tout, il n'est traité que sur la base de ActionDispatch :: Request, et si vous réécrivez à la fois # read et # write de Resolver Il me semblait que je pouvais écrire tout ce que je voulais, pas seulement GET and HEAD.

Le jugement par en-tête, adresse IP, hôte local, etc. peut également être utilisé car ce sont les informations dont dispose Request. Je pensais que je pouvais faire beaucoup.

Recommended Posts

Lire l'implémentation de la commutation automatique multi-DB Rails 6 (ActiveRecord :: Middleware :: DatabaseSelector)
Relisez le guide des rails (vue d'ensemble du contrôleur d'action)
[Rails] Implémentation de la saisie d'adresse automatique avec jpostal et jp_prefecture
Implémentation de la suppression d'ajax dans Rails
[Rails] Implémentation de la fonction de catégorie
[Rails] Implémentation de la fonction tutoriel
[Rails] Implémentation d'une fonction similaire
[Rails] Implémentation de la fonction coupon (avec fonction de suppression automatique par traitement par lots)
[Rails] Implémentation de la suppression logique utilisateur
[Rails] Implémentation de la fonction d'importation CSV
[Rails] Implémentation asynchrone de la fonction similaire
[Rails] Implémentation de la fonction de prévisualisation d'image
Explication de l'ordre des itinéraires ferroviaires
J'ai lu la source de ArrayList que j'ai lu
[Rails] À propos de la mise en œuvre de la fonction similaire
[Rails] Implémentation de la fonction de retrait utilisateur
[Rails] Implémentation de la fonction d'exportation CSV
J'ai lu la source d'Integer
Vérifier l'état de migration des rails
J'ai lu la source de Long
Obtenez l'ID de la numérotation automatique
J'ai lu la source de Short
J'ai lu la source de Byte
J'ai lu la source de String
[Rails] Je vais expliquer la procédure d'implémentation de la fonction follow en utilisant form_with.
[Circle CI] J'étais accro au test automatique de Circle CI (rails + mysql) [Memo]