-
Notifications
You must be signed in to change notification settings - Fork 504
Replication with Rails on Heroku
First, install the gem as usual (see 'Install' section in the README).
If you have one or more read-only follower databases on a Heroku deployment, to have each follower configured as slaves via Octopus, place the following dynamic configuration in config/shards.yml
:
<%
require 'cgi'
require 'uri'
def attribute(name, value, force_string = false)
if value
value_string =
if force_string
'"' + value + '"'
else
value
end
"#{name}: #{value_string}"
else
""
end
end
configs = case Rails.env
when 'development', 'test'
# use dev and test DB as feaux 'follower'
Array.new(2){YAML::load_file(File.open("config/database.yml"))[Rails.env]}
else
# staging, production, etc with Heroku config vars for follower DBs
master_url = ENV['DATABASE_URL']
slave_keys = ENV.keys.select{|k| k =~ /HEROKU_POSTGRESQL_.*_URL/}
slave_keys.delete_if{ |k| ENV[k] == master_url }
slave_keys.map do |env_key|
config = {}
begin
uri = URI.parse(ENV["#{env_key}"])
rescue URI::InvalidURIError
raise "Invalid DATABASE_URL"
end
raise "No RACK_ENV or RAILS_ENV found" unless ENV["RAILS_ENV"] || ENV["RACK_ENV"]
config['color'] = env_key.match(/HEROKU_POSTGRESQL_(.*)_URL/)[1].downcase
config['adapter'] = uri.scheme
config['adapter'] = "postgresql" if config['adapter'] == "postgres"
config['database'] = (uri.path || "").split("/")[1]
config['username'] = uri.user
config['password'] = uri.password
config['host'] = uri.host
config['port'] = uri.port
config['params'] = CGI.parse(uri.query || "")
config
end
end
whitelist = ENV['SLAVE_ENABLED_FOLLOWERS'].downcase.split(', ') rescue nil
blacklist = ENV['SLAVE_DISABLED_FOLLOWERS'].downcase.split(', ') rescue nil
configs.delete_if do |c|
( whitelist && !c['color'].in?(whitelist) ) || ( blacklist && c['color'].in?(blacklist) )
end
%>
octopus:
replicated: true
fully_replicated: false
environments:
<% if configs.present? %>
<%= "- #{ENV["RAILS_ENV"] || ENV["RACK_ENV"] || Rails.env}" %>
<%= ENV["RAILS_ENV"] || ENV["RACK_ENV"] || Rails.env %>:
followers:
<% configs.each_with_index do |c, i| %>
<%= c.has_key?('color') ? "#{c['color']}_follower" : "follower_#{i + 1}" %>:
<%= attribute "adapter", c['adapter'] %>
<%= attribute "database", c['database'] %>
<%= attribute "username", c['username'] %>
<%= attribute "password", c['password'], true %>
<%= attribute "host", c['host'] %>
<%= attribute "port", c['port'] %>
<% (c['params'] || {}).each do |key, value| %>
<%= key %>: <%= value.first %>
<% end %>
<% end %>
<% else %>
- none
<% end %>
This configuration uses the environmental variables that Heroku sets up when you create your heroku-postgresql add-on databases to automatically set up any slaves that are present. This assumes that you desire all non-primary databases to be used as read-only slaves. If you don't, see the 'More Info and Options' section below for your options.
Your primary database will be configured as usual by the configuration that Heroku injects into database.yml.
How closely your followers (slaves) follow master is application specific, so the followers are not configured to automatically send all read queries to the followers by default. In Octopus lingo, we have not configured to be 'fully replicated'.
Mark the appropriate AR models by setting replicated_model
:
class StaticThing < ActiveRecord::Base
replicated_model
end
This results in using your followers for read queries, and master for write queries on that model. This is appropriate for models that won't yield unexpected behavior when read queries come from a slave that may be a few seconds behind the master they follow.
That's everything required to get started. You can read more about Octopus to learn how to use the using
methods in controllers, models and AR relations in a more granular fashion, if needed. If you do, read below about the Octopus.followers
monkey-patch to ensure your code is ready for future scaling, which is necessary until using_group functionality is more robust.
Add the following to config/initializers/octopus.rb
for:
- Convenient logging of the slaves configured at app initialization
- Use of
Octopus.followers
to retrieve configured followers- Example:
StaticThing.using(Octopus.followers).all
- Example:
module Octopus
def self.shards_in(group=nil)
config[Rails.env][group.to_s].keys
end
def self.followers
shards_in(:followers)
end
class << self
alias_method :followers_in, :shards_in
alias_method :slaves_in, :shards_in
end
end
if Octopus.enabled?
count = case (Octopus.config[Rails.env].values[0].values[0] rescue nil)
when Hash
Octopus.config[Rails.env].map{|group, configs| configs.count}.sum rescue 0
else
Octopus.config[Rails.env].keys.count rescue 0
end
puts "=> #{count} #{'database'.pluralize(count)} enabled as read-only #{'slave'.pluralize(count)}"
if Octopus.followers.count == count
Octopus.followers.each{ |f| puts " * #{f.split('_')[0].upcase} #{f.split('_')[1]}" }
end
end
Octopus integrates with Rails logging and will prepend the SQL log line with the shard/slave queried:
Master:
DynamicThing Load (0.2ms) SELECT "dynamic_things".* FROM "dynamic_things"
Follower(s):
[Shard: orange_follower] StaticThing Load (0.3ms) SELECT "static_things".* FROM "static_things"
[Shard: pink_follower] StaticThing Load (0.2ms) SELECT "static_things".* FROM "static_things"
There was a bug with this logging customization for the recommended Rails configuration format (used above), this is fixed as of issue/pull #166. Use the rocketmobile/octopus fork if this pull request isn't merged yet upon your reading.
If using the above configuration, you can choose the followers you want to have slave duties with environmental variables. Whitelist followers you want or blacklist the followers you don't want with SLAVE_ENABLED_FOLLOWERS
and/or SLAVE_DISABLED_FOLLOWERS
. Use a comma+space separated list of (case insensitive) follower colors:
heroku config:add SLAVE_ENABLED_FOLLOWERS=PINK, CRIMSON
Without these variables set (the default), all followers will be used as slaves. This may be undesirable for you. One example of usage is when adding an additional follower to a live application, where the above ENV vars are used to ensure the new follower is excluded until it is sufficiently caught up to master for duty.
Note: the blacklist config isn't currently written to work in development or test environments. The blacklist var is effectively ignored in non-Heroku environments.
By default, 2 followers will be simulated (pointing at your development or test database) in development or test environments. The number of dev/test followers is easily changed with the following code in the configuration file:
Array.new(2){YAML::load_file(File.open("config/database.yml"))[Rails.env]}
Heroku's dynamic configuration for your primary database allows you too add additional configuration params as URI params on the DB URL configuration variable (cross-reference: SO thread).
This same method of additional params via the _URL ENV key is supported for slave configuration with the above dynamic Octopus configuration.