Skip to content

Instantly share code, notes, and snippets.

@danott
Last active Sep 8, 2021
Embed
What would you like to do?
Using "replica" => true to simulate a replica database in development
# 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