distribute_reads

repository·master·Indexed 18 days ago

https://github.com/ankane/distribute_reads

A Rails gem that scales database reads to replicas by leveraging ActiveRecordProxyAdapters for automatic statement-based routing. It provides a `distribute_reads` block to route queries to replicas, supports background job integration via ApplicationJob, and includes features for managing replication lag, failover options, and eager loading of ActiveRecord relations.

Tokens
2.9K
Snippets
13
Records
17
Agent score
63%

What's inside distribute_reads

  1. Handle Lazy Evaluation in distribute_reads blocks

    master

    Because ActiveRecord uses lazy evaluation, a query might not execute until it is outside the distribute_reads block, causing it to run on the primary instead of the replica.

    To ensure a query runs on a replica, call .to_a or .load inside the block. Alternatively, you can enable automatic loading for all relations returned from distribute_reads blocks by setting DistributeReads.eager_load = true in an initializer.

    # This might run on primary if not executed immediately
    users = distribute_reads { User.where(orders_count: 1) }
    
    # This ensures execution on the replica
    users = distribute_reads { User.where(orders_count: 1).to_a }
  2. Set up database connections in ApplicationRecord

    master

    After configuring database.yml, add connects_to to your app/models/application_record.rb to define the writing and reading roles.

    class ApplicationRecord < ActiveRecord::Base
      connects_to database: {writing: :primary, reading: :replica}
    end
  3. Use distribute_reads to route queries to replicas

    master

    By default, all reads go to the primary instance. Wrap code blocks in distribute_reads to route queries to the replica. This works for single queries or multiple queries within a block. Note that writes (like .save!) inside a distribute_reads block will still correctly target the primary.

    # Single query
    distribute_reads { User.count }
    
    # Multiple queries
    distribute_reads do
      User.find_each do |user|
        user.orders_count = user.orders.count  # replica
        user.save!                             # primary
      end
    end
  4. Configure database.yml for proxy adapters

    master

    To use distribute_reads, you must use ActiveRecordProxyAdapters. Update your config/database.yml so that the primary connection uses a proxy URL and the replica connection uses a standard database URL with replica: true set.

    • Primary URL: Must start with postgresql-proxy://, mysql2-proxy://, or trilogy-proxy://.
    • Replica URL: Must start with postgresql://, mysql2://, or trilogy://.
    default: &default
      primary:
        # should start with postgresql-proxy://, mysql2-proxy://, or trilogy-proxy://
        url: <%= ENV["DATABASE_URL"] %>
      replica:
        # should start with postgresql://, mysql2://, or trilogy://
        url: <%= ENV["REPLICA_DATABASE_URL"] %>
        replica: true
    
    development:
      <<: *default
    
    production:
      <<: *default
  5. Configure global DistributeReads settings

    master

    You can set global defaults and configuration for the DistributeReads module:

    • DistributeReads.default_options: Sets the default options for all distribute_reads blocks (e.g., { lag_failover: true, failover: false }).
    • DistributeReads.by_default: If set to true, all queries are distributed to replicas by default. To force a query to the primary when this is enabled, use distribute_reads(primary: true).
    • DistributeReads.logger: Sets the logger for failover messages. Use nil to disable logging.
    • DistributeReads.eager_load: (Boolean) If true, automatically loads relations returned from distribute_reads blocks.
    DistributeReads.default_options = {
      lag_failover: true,
      failover: false
    }
    
    DistributeReads.by_default = true
    
    # Force primary when by_default is true
    distribute_reads(primary: true) do
      # ...
    end
    
    DistributeReads.logger = Logger.new(STDERR)
    DistributeReads.eager_load = true
  6. Distribute reads in background jobs

    master

    You can distribute all reads within an ApplicationJob by calling distribute_reads at the class level. You can also pass configuration options to the class method.

    class TestJob < ApplicationJob
      distribute_reads
    
      def perform
        # ...
      end
    end
  7. Force queries to the replica

    master

    If ActiveRecordProxyAdapters incorrectly routes a query to the primary, you can explicitly force all queries within a block to use the replica by passing replica: true.

    distribute_reads(replica: true) do
      # send all queries in block to replica
    end
  8. Configure DistributeReads global settings

    master

    You can configure the global behavior of DistributeReads using the following attributes on the DistributeReads module:

    • by_default: A boolean indicating if read distribution should be active by default.
    • default_options: A hash of options applied to read operations. Default values are { failover: true, lag_failover: false }.
    • eager_load: A boolean to enable/disable eager loading.
    • logger: An optional logger. If not set, it defaults to ActiveRecord::Base.logger.
  9. Configure Replica Lag and Failover options

    master

    You can control how distribute_reads handles replica lag and availability using the following options:

    • max_lag: The maximum allowed replication lag in seconds. If exceeded, it raises DistributeReads::TooMuchLag.
    • lag_failover: If true, instead of raising an error when lag is too high, the block will use the primary connection.
    • lag_on: An array of classes (e.g., [ApplicationRecord, LogRecord]) specifying which connections to check for lag. If lag on any specified connection exceeds max_lag and lag_failover is enabled, all connections will use their primary.
    • failover: If false, an error is raised if no replicas are available (preventing primary overload). If true (default), the primary is used if no replicas are available.
    # Raise error on high lag
    distribute_reads(max_lag: 3) do
      # raises DistributeReads::TooMuchLag
    end
    
    # Failover to primary on high lag
    distribute_reads(max_lag: 3, lag_failover: true) do
      # ...
    end
    
    # Prevent primary overload if no replicas available
    distribute_reads(failover: false) do
      # ...
    end
    
    # Check lag on specific connections
    distribute_reads(max_lag: 3, lag_on: [ApplicationRecord, LogRecord]) do
      # ...
    end
  10. Handle `DistributeReads::TooMuchLag` errors

    master

    If you specify a :max_lag and :lag_failover is set to false (the default), distribute_reads will raise a DistributeReads::TooMuchLag error if the replication lag exceeds the threshold.

    Common error messages include:

    • "Replication stopped on [Model] connection" (if lag cannot be determined)
    • "No replicas available for lag check on [Model] connection"
    • "Replica lag over [X] seconds on [Model] connection"