Using "replica" => true to simulate a replica database in development
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Hey Bike Shed! | |
# | |
# During the episode titled "Sportsaphores", Chris shared about setting up | |
# primary/replica/leader/follower/whatchamacallit databases. Something mentioned during that | |
# conversation was the discouragement that comes from losing production/development parity, | |
# especially for catching bugs before production. The big hurdle mentioned, was wanting to avoid | |
# the overhead of setting up a proper "follower" database. | |
# | |
# I wanted to share something I've done in development/test that has caught similar bugs in my | |
# Rails 6 app with multiple database connections. The "one weird trick", is to establish | |
# configuration for a leader and a follower... they just happen to point to the same database. | |
# If you include the `"replica" => true` attribute in your configuration Hash, ActiveRecord will | |
# give similiar-enough characteristics as a true read-only replica. | |
# | |
# Here's a little demo app that shows the runtime error that will happen when trying to write | |
# when the "replica" attribute is set. | |
# | |
# Run this with `ruby application.rb` | |
require "bundler/inline" | |
gemfile true do | |
source "https://rubygems.org" | |
gem "rails", "~> 6.1" | |
gem "sqlite3" | |
end | |
require "action_controller/railtie" | |
require "active_record/railtie" | |
require "rails/command" | |
require "rails/commands/server/server_command" | |
ActiveRecord::Base.configurations = ActiveRecord::DatabaseConfigurations.new({ | |
"development" => { | |
"primary" => { | |
"adapter" => "sqlite3", | |
"database" => "tmp/application.sqlite", | |
"replica" => false, | |
}, | |
"replica" => { | |
"adapter" => "sqlite3", | |
"database" => "tmp/application.sqlite", | |
# This is the secret sauce. Connect to the same database, but mark it as a replica. | |
# This will give development/test the same ActiveRecord runtime characteristics as | |
# production without the overhead of spinning up an actual follower database. | |
"replica" => true, | |
}, | |
} | |
}) | |
ActiveRecord::Base.logger = Logger.new(STDOUT) | |
ActiveRecord::Base.establish_connection | |
ActiveRecord::Schema.define do | |
create_table :logged_get_requests, force: true do |t| | |
t.timestamps | |
end | |
end | |
class ApplicationRecord < ActiveRecord::Base | |
self.abstract_class = true | |
connects_to database: { writing: :primary, reading: :replica } | |
end | |
class LoggedGetRequest < ApplicationRecord; end | |
class ApplicationController < ActionController::Base; end | |
class DemosController < ApplicationController | |
delegate :demo_path, to: "Rails.application.routes.url_helpers" | |
delegate :link_to, to: "helpers" | |
def show | |
database_role_from_param = params.fetch(:role, "reading") | |
attempt_create = params.fetch(:attempt_create, "nope") == "yep" | |
count = "not set" | |
error = "no error encountered" | |
ActiveRecord::Base.connected_to(role: database_role_from_param) do | |
begin | |
LoggedGetRequest.create! if attempt_create | |
count = LoggedGetRequest.all.count | |
rescue => e | |
error = "#{e.class}: #{e.message}" | |
end | |
end | |
render inline: <<~HTML | |
<html> | |
<body> | |
<h1>Current count: #{count}</h1> | |
<h2>Current error: #{error}</h2> | |
<ul> | |
<li>#{link_to "Get without create on primary", demo_path(role: :writing, attempt_create: "nope")}</li> | |
<li>#{link_to "Get without create on replica", demo_path(role: :reading, attempt_create: "nope")}</li> | |
<li>#{link_to "Get with create on primary", demo_path(role: :writing, attempt_create: "yep")}</li> | |
<li>#{link_to "Get with create on replica", demo_path(role: :reading, attempt_create: "yep")}</li> | |
</ul> | |
</body> | |
</html> | |
HTML | |
end | |
end | |
class Application < Rails::Application | |
config.secret_key_base = ENV["SECRET_KEY_BASE"] || SecureRandom.hex | |
config.secret_token = ENV["SECRET_TOKEN"] || SecureRandom.hex | |
config.logger = Logger.new($stdout) | |
Rails.logger = config.logger | |
routes.draw do | |
resource :demo, only: %i[show update] | |
root "demos#show" | |
end | |
end | |
Rails::Server.new(app: Application, Host: "0.0.0.0", Port: 3000).start |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment