[RUBY] Lesen Sie die Implementierung des automatischen Rails 6-Multi-DB-Switching (ActiveRecord :: Middleware :: DatabaseSelector).

Überblick

Kann ich mich für mein eigenes Projekt bezüglich der Unterstützung mehrerer aus Rails 6.0 implementierter DBs bewerben? Ich werde die Umsetzung verfolgen, um das zu beurteilen

Beschreibung im Rails Guide

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

Mit der Auto-Switch-Funktion kann die Anwendung je nach HTTP-Verb und Vorhandensein oder Nichtvorhandensein des letzten Schreibvorgangs von primär zu Replikat oder von Replikat zu primär wechseln. Wenn eine Anwendung eine POST-, PUT-, DELETE- oder PATCH-Anforderung empfängt, schreibt sie automatisch in die primäre Anwendung. Die Anwendung liest von der primären bis zum angegebenen Zeitpunkt nach dem Schreiben. Wenn die Anwendung eine GET-Anforderung oder eine HEAD-Anforderung empfängt, liest sie aus dem Replikat, wenn kein aktueller Schreibvorgang vorhanden ist.

Es wird angegeben, dass es durch zwei Arten von Beurteilungen durchgeführt wird, Beurteilung durch HTTP-Methode + Beurteilung durch Zeit vom Schreiben. Die Einstellmethode wird unten beschrieben

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

Code lesen

Beginnen wir mit dem Anwendungsteil der Middleware

Anwendbarer Teil der Middleware in 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

Als nächstes werden wir die Implementierung des Middleware-Teils verfolgen

[Ergänzung] Was sind bestellte Optionen?

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

Ein Mechanismus, mit dem auf Hash in Punkten zugegriffen werden kann Gibt nil zurück, wenn nicht mit delete gesetzt

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

Implementierung der Datenbankauswahl

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

Schauen wir uns als nächstes die wichtige # select_database an

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

Da "Kontext" zuerst ausgeführt wird und "Resolver" darauf basierend erstellt wird, überprüfen wir zuerst "Kontext".

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 ist nur # new, hier wird nur die Sitzung von ActionDispatch :: Request gesetzt Überprüfen Sie als nächstes Resolvers .call

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

Ich bin auch hier nur neu, also werde ich die ursprüngliche DatabaseSelector # select_database ein wenig mehr lesen.

Beurteilungsteil der HTTP-Methode

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

Hier für GET oder HEAD => lesen Ansonsten => schreiben Führt die Resolver-Verarbeitung aus als (Resolver scheint weitere Entscheidungen zu treffen)

Beginnen wir mit dem Lesen

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

Es scheint, dass der Prozess fortgesetzt wird, nachdem dem Instrument mitgeteilt wurde, welche Rolle verwendet werden soll. Was wird dann nach "# read_from_primary" beurteilt?

  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 Die sogenannte session # last_write_timestamp ist Wenn die erste config.active_record.database_selector = {delay: 2.seconds} Verzögerung nicht verstrichen ist, setzen Sie sie auf primary. Wenn die Verzögerung abgelaufen ist, wird das Replikat angezeigt.

Überprüfen Sie also den # last_write_timestamp der Sitzung

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

Ändern Sie einfach sesion [: last_write] in ein Zeitobjekt Dies ist das Ende des Lesens, also schreiben Sie

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

Dies ist nur das Schreiben in der Schreibrolle

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

Und geben Sie den aktuellen Zeitstempel für "session [: last_write]" an

Zusammenfassung

Wie erwartet ist es Rails. Es wurde sehr leicht verständlich gemacht.

Schließlich wird es nur basierend auf ActionDispatch :: Request verarbeitet, und wenn Sie sowohl "# read" als auch "# write" von Resolver neu schreiben Es schien, als könnte ich schreiben, was ich wollte, nicht nur GET und HEAD.

Die Beurteilung nach Header, IP-Adresse, Localhost usw. kann ebenfalls verwendet werden, da es sich um die Informationen handelt, über die die Anforderung verfügt. Ich dachte, ich könnte viel tun.

Recommended Posts

Lesen Sie die Implementierung des automatischen Rails 6-Multi-DB-Switching (ActiveRecord :: Middleware :: DatabaseSelector).
Lesen Sie das Rails-Handbuch erneut (Übersicht über Action Controller).
[Rails] Implementierung der automatischen Adresseneingabe mit jpostal und jp_prefecture
Implementierung der Ajax-Entfernung in Rails
[Rails] Implementierung der Kategoriefunktion
[Rails] Implementierung der Tutorial-Funktion
[Rails] Implementierung einer ähnlichen Funktion
[Rails] Implementierung der Couponfunktion (mit automatischer Löschfunktion mittels Stapelverarbeitung)
[Rails] Implementierung des logischen Löschens durch den Benutzer
[Rails] Implementierung der CSV-Importfunktion
[Rails] Asynchrone Implementierung der Like-Funktion
[Rails] Implementierung der Bildvorschau
Erläuterung der Reihenfolge der Schienenrouten
Ich habe die Quelle von ArrayList gelesen, die ich gelesen habe
[Rails] Über die Implementierung der Like-Funktion
[Rails] Implementierung der Benutzerrückzugsfunktion
[Rails] Implementierung der CSV-Exportfunktion
Ich habe die Quelle von Integer gelesen
Überprüfen Sie den Migrationsstatus von Schienen
Ich habe die Quelle von Long gelesen
Holen Sie sich die ID der automatischen Nummerierung
Ich habe die Quelle von Short gelesen
Ich habe die Quelle von Byte gelesen
Ich habe die Quelle von String gelesen
[Rails] Ich werde die Prozedur zum Implementieren der Follow-Funktion mit form_with erklären.
[Circle CI] Ich war süchtig nach dem automatischen Test von Circle CI (Rails + MySQL) [Memo]