Skip to content

Instantly share code, notes, and snippets.

@eprothro
Created April 12, 2013 19:28
Show Gist options
  • Select an option

  • Save eprothro/5374472 to your computer and use it in GitHub Desktop.

Select an option

Save eprothro/5374472 to your computer and use it in GitHub Desktop.

Revisions

  1. eprothro revised this gist Apr 12, 2013. 1 changed file with 1 addition and 52 deletions.
    53 changes: 1 addition & 52 deletions shards.yml
    Original file line number Diff line number Diff line change
    @@ -82,55 +82,4 @@ octopus:
    <% 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](https://devcenter.heroku.com/articles/ruby-support#build-behavior).
    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.

    ## Initializer (Recommended)

    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`

    ```
    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
    <% end %>
  2. eprothro created this gist Apr 12, 2013.
    136 changes: 136 additions & 0 deletions shards.yml
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,136 @@
    <%
    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](https://devcenter.heroku.com/articles/ruby-support#build-behavior).
    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.

    ## Initializer (Recommended)

    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`

    ```
    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