├── init.rb
├── test-project-2.x
├── db
├── app
├── .rspec
├── .gitignore
├── spec
│ ├── models
│ ├── unit
│ ├── fixtures
│ ├── sharding
│ ├── support
│ ├── controllers
│ ├── spec.opts
│ └── spec_helper.rb
├── script
│ └── console
├── config
│ ├── initializers
│ │ ├── db_charmer.rb
│ │ ├── mime_types.rb
│ │ ├── inflections.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── sharding.rb
│ │ ├── new_rails_defaults.rb
│ │ └── session_store.rb
│ ├── locales
│ │ └── en.yml
│ ├── environment.rb
│ ├── routes.rb
│ ├── preinitializer.rb
│ ├── environments
│ │ └── test.rb
│ ├── database.yml.example
│ └── boot.rb
├── Gemfile
└── Rakefile
├── test-project
├── app
│ ├── models
│ │ ├── ford.rb
│ │ ├── toyota.rb
│ │ ├── avatar.rb
│ │ ├── car.rb
│ │ ├── category.rb
│ │ ├── comment.rb
│ │ ├── house.rb
│ │ ├── categories_posts.rb
│ │ ├── log_record.rb
│ │ ├── user.rb
│ │ ├── range_sharded_model.rb
│ │ ├── event.rb
│ │ └── post.rb
│ ├── helpers
│ │ └── application_helper.rb
│ ├── views
│ │ ├── posts
│ │ │ ├── new.html.erb
│ │ │ ├── show.html.erb
│ │ │ └── index.html.erb
│ │ └── layouts
│ │ │ └── application.html.erb
│ └── controllers
│ │ ├── application_controller.rb
│ │ └── posts_controller.rb
├── .rspec
├── config
│ ├── initializers
│ │ ├── db_charmer.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── session_store.rb
│ │ ├── secret_token.rb
│ │ └── sharding.rb
│ ├── environment.rb
│ ├── boot.rb
│ ├── locales
│ │ └── en.yml
│ ├── environments
│ │ └── test.rb
│ ├── database.yml.example
│ ├── application.rb
│ └── routes.rb
├── .gitignore
├── spec
│ ├── fixtures
│ │ ├── categories.yml
│ │ ├── avatars.yml
│ │ ├── log_records.yml
│ │ ├── comments.yml
│ │ ├── users.yml
│ │ ├── categories_posts.yml
│ │ ├── posts.yml
│ │ ├── event_shards_map.yml
│ │ └── event_shards_info.yml
│ ├── support
│ │ └── rails31_stub_connection.rb
│ ├── models
│ │ ├── category_spec.rb
│ │ ├── avatar_spec.rb
│ │ ├── post_spec.rb
│ │ ├── categories_posts_spec.rb
│ │ ├── comment_spec.rb
│ │ ├── log_record_spec.rb
│ │ ├── cars_spec.rb
│ │ ├── user_spec.rb
│ │ ├── range_sharded_model_spec.rb
│ │ └── event_spec.rb
│ ├── sharding
│ │ ├── sharding_spec.rb
│ │ ├── connection_spec.rb
│ │ └── method
│ │ │ ├── hash_map_spec.rb
│ │ │ ├── range_spec.rb
│ │ │ └── db_block_map_spec.rb
│ ├── unit
│ │ ├── abstract_adapter
│ │ │ └── log_formatting_spec.rb
│ │ ├── connection_proxy_spec.rb
│ │ ├── action_controller
│ │ │ └── force_slave_reads_spec.rb
│ │ ├── active_record
│ │ │ ├── association_preload_spec.rb
│ │ │ ├── named_scope
│ │ │ │ └── named_scope_spec.rb
│ │ │ ├── relation_spec.rb
│ │ │ ├── db_magic_spec.rb
│ │ │ ├── association_proxy_spec.rb
│ │ │ ├── connection_switching_spec.rb
│ │ │ ├── class_attributes_spec.rb
│ │ │ └── master_slave_routing_spec.rb
│ │ ├── db_charmer_spec.rb
│ │ ├── connection_factory_spec.rb
│ │ ├── with_remapped_databases_spec.rb
│ │ └── multi_db_proxy_spec.rb
│ ├── spec_helper.rb
│ ├── integration
│ │ └── multi_threading_spec.rb
│ └── controllers
│ │ └── posts_controller_spec.rb
├── TODO
├── db
│ ├── migrate
│ │ ├── 20100305234245_create_categories.rb
│ │ ├── 20100817191548_create_cars.rb
│ │ ├── 20090810221944_create_users.rb
│ │ ├── 20100305235831_create_avatars.rb
│ │ ├── 20090810013922_create_posts.rb
│ │ ├── 20090810013829_create_log_records.rb
│ │ ├── 20100305234340_create_categories_posts.rb
│ │ ├── 20111005193941_create_comments.rb
│ │ ├── 20100330180517_create_event_tables.rb
│ │ └── 20100328201317_create_sharding_map_tables.rb
│ ├── seeds.rb
│ ├── create_databases.sql
│ └── sharding.sql
├── Rakefile
└── Gemfile
├── .gitignore
├── Makefile
├── Rakefile
├── lib
└── db_charmer
│ ├── railtie.rb
│ ├── version.rb
│ ├── rails31
│ └── active_record
│ │ ├── migration
│ │ └── command_recorder.rb
│ │ └── preloader
│ │ ├── association.rb
│ │ └── has_and_belongs_to_many.rb
│ ├── sharding
│ ├── method.rb
│ ├── method
│ │ ├── hash_map.rb
│ │ ├── range.rb
│ │ └── db_block_map.rb
│ ├── connection.rb
│ └── stub_connection.rb
│ ├── core_extensions.rb
│ ├── rails2
│ ├── active_record
│ │ ├── named_scope
│ │ │ └── scope_proxy.rb
│ │ └── master_slave_routing.rb
│ └── abstract_adapter
│ │ └── log_formatting.rb
│ ├── sharding.rb
│ ├── rails3
│ ├── active_record
│ │ ├── log_subscriber.rb
│ │ ├── relation_method.rb
│ │ ├── master_slave_routing.rb
│ │ └── relation
│ │ │ └── connection_routing.rb
│ └── abstract_adapter
│ │ └── connection_name.rb
│ ├── active_record
│ ├── association_preload.rb
│ ├── sharding.rb
│ ├── multi_db_proxy.rb
│ ├── db_magic.rb
│ ├── connection_switching.rb
│ ├── class_attributes.rb
│ └── migration
│ │ └── multi_db_migrations.rb
│ ├── with_remapped_databases.rb
│ ├── connection_proxy.rb
│ ├── force_slave_reads.rb
│ ├── action_controller
│ └── force_slave_reads.rb
│ ├── tasks
│ └── databases.rake
│ └── connection_factory.rb
├── .travis.yml
├── LICENSE
├── db-charmer.gemspec
├── ci_build
└── README.rdoc
/init.rb:
--------------------------------------------------------------------------------
1 | require 'db_charmer'
2 |
--------------------------------------------------------------------------------
/test-project-2.x/db:
--------------------------------------------------------------------------------
1 | ../test-project/db
--------------------------------------------------------------------------------
/test-project-2.x/app:
--------------------------------------------------------------------------------
1 | ../test-project/app
--------------------------------------------------------------------------------
/test-project-2.x/.rspec:
--------------------------------------------------------------------------------
1 | ../test-project/.rspec
--------------------------------------------------------------------------------
/test-project-2.x/.gitignore:
--------------------------------------------------------------------------------
1 | ../test-project/.gitignore
--------------------------------------------------------------------------------
/test-project-2.x/spec/models:
--------------------------------------------------------------------------------
1 | ../../test-project/spec/models
--------------------------------------------------------------------------------
/test-project-2.x/spec/unit:
--------------------------------------------------------------------------------
1 | ../../test-project/spec/unit
--------------------------------------------------------------------------------
/test-project/app/models/ford.rb:
--------------------------------------------------------------------------------
1 | class Ford < Car
2 | end
--------------------------------------------------------------------------------
/test-project-2.x/spec/fixtures:
--------------------------------------------------------------------------------
1 | ../../test-project/spec/fixtures
--------------------------------------------------------------------------------
/test-project-2.x/spec/sharding:
--------------------------------------------------------------------------------
1 | ../../test-project/spec/sharding
--------------------------------------------------------------------------------
/test-project-2.x/spec/support:
--------------------------------------------------------------------------------
1 | ../../test-project/spec/support
--------------------------------------------------------------------------------
/test-project/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 | --format documentation
3 |
--------------------------------------------------------------------------------
/test-project/app/models/toyota.rb:
--------------------------------------------------------------------------------
1 | class Toyota < Car
2 | end
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | doc
2 | pkg
3 | .DS_Store
4 | _site
5 | .idea
6 |
7 |
--------------------------------------------------------------------------------
/test-project-2.x/spec/controllers:
--------------------------------------------------------------------------------
1 | ../../test-project/spec/controllers
--------------------------------------------------------------------------------
/test-project-2.x/spec/spec.opts:
--------------------------------------------------------------------------------
1 | --colour
2 | --format specdoc
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | doc/files/README_rdoc.html: README.rdoc
2 | rdoc README.rdoc
--------------------------------------------------------------------------------
/test-project/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/test-project/app/models/avatar.rb:
--------------------------------------------------------------------------------
1 | class Avatar < ActiveRecord::Base
2 | end
3 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rake'
2 | require 'bundler'
3 |
4 | Bundler::GemHelper.install_tasks
5 |
--------------------------------------------------------------------------------
/test-project/app/models/car.rb:
--------------------------------------------------------------------------------
1 | class Car < ActiveRecord::Base
2 | db_magic :slave => :slave01
3 | end
4 |
--------------------------------------------------------------------------------
/test-project/app/views/posts/new.html.erb:
--------------------------------------------------------------------------------
1 |
Posts#new
2 | Find me in app/views/posts/new.html.erb
3 |
--------------------------------------------------------------------------------
/test-project/app/views/posts/show.html.erb:
--------------------------------------------------------------------------------
1 | Posts#show
2 | Find me in app/views/posts/show.html.erb
3 |
--------------------------------------------------------------------------------
/test-project/app/models/category.rb:
--------------------------------------------------------------------------------
1 | class Category < ActiveRecord::Base
2 | has_and_belongs_to_many :posts
3 | end
4 |
--------------------------------------------------------------------------------
/test-project/app/models/comment.rb:
--------------------------------------------------------------------------------
1 | class Comment < ActiveRecord::Base
2 | belongs_to :commentable, :polymorphic => true
3 | end
4 |
--------------------------------------------------------------------------------
/test-project-2.x/script/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require File.dirname(__FILE__) + '/../config/boot'
3 | require 'commands/console'
4 |
--------------------------------------------------------------------------------
/test-project/app/models/house.rb:
--------------------------------------------------------------------------------
1 | class House < ActiveRecord::Base
2 | db_magic :slave => :slave01, :force_slave_reads => false
3 | end
4 |
--------------------------------------------------------------------------------
/test-project/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 | end
4 |
--------------------------------------------------------------------------------
/test-project/app/models/categories_posts.rb:
--------------------------------------------------------------------------------
1 | class CategoriesPosts < ActiveRecord::Base
2 | belongs_to :category
3 | belongs_to :post
4 | end
5 |
--------------------------------------------------------------------------------
/test-project/app/models/log_record.rb:
--------------------------------------------------------------------------------
1 | class LogRecord < ActiveRecord::Base
2 | db_magic :connection => :logs
3 | belongs_to :user
4 | end
5 |
--------------------------------------------------------------------------------
/test-project/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | has_many :posts
3 | has_many :log_records
4 | has_one :avatar
5 | end
6 |
--------------------------------------------------------------------------------
/test-project-2.x/config/initializers/db_charmer.rb:
--------------------------------------------------------------------------------
1 | DbCharmer.connections_should_exist = false # Since we are not in production
2 | DbCharmer.enable_controller_magic!
--------------------------------------------------------------------------------
/test-project/config/initializers/db_charmer.rb:
--------------------------------------------------------------------------------
1 | DbCharmer.connections_should_exist = false # Since we are not in production
2 | DbCharmer.enable_controller_magic!
3 |
--------------------------------------------------------------------------------
/test-project/.gitignore:
--------------------------------------------------------------------------------
1 | log/*.log
2 | db/schema.*
3 | .idea
4 | TAGS
5 | config/database.yml
6 | .bundle
7 | tmp
8 | .DS_Store
9 | vendor
10 | Gemfile.lock
11 | doc
12 |
--------------------------------------------------------------------------------
/lib/db_charmer/railtie.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | class Railtie < Rails::Railtie
3 |
4 | rake_tasks do
5 | load "db_charmer/tasks/databases.rake"
6 | end
7 |
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/test-project/app/models/range_sharded_model.rb:
--------------------------------------------------------------------------------
1 | class RangeShardedModel < ActiveRecord::Base
2 | db_magic :sharded => {
3 | :key => :id,
4 | :sharded_connection => :texts
5 | }
6 | end
7 |
8 |
--------------------------------------------------------------------------------
/test-project/app/views/posts/index.html.erb:
--------------------------------------------------------------------------------
1 | Posts
2 |
3 | <% @posts.each do |post| %>
4 |
5 | Post #<%= post.id %>
6 |
<%= post.inspect %>
7 |
8 | <% end %>
9 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/categories.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2 |
3 | one:
4 | id: 1
5 | name: one
6 |
7 | two:
8 | id: 2
9 | name: two
10 |
--------------------------------------------------------------------------------
/test-project/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | DbCharmerSandbox::Application.initialize!
6 |
--------------------------------------------------------------------------------
/test-project/app/models/event.rb:
--------------------------------------------------------------------------------
1 | class Event < ActiveRecord::Base
2 | self.table_name = :timeline_events
3 |
4 | db_magic :sharded => {
5 | :key => :to_uid,
6 | :sharded_connection => :social
7 | }
8 | end
9 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/avatars.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2 |
3 | one:
4 | user_id: 1
5 | name: avatar1
6 |
7 | two:
8 | user_id: 2
9 | name: avatar2
10 |
--------------------------------------------------------------------------------
/test-project/spec/support/rails31_stub_connection.rb:
--------------------------------------------------------------------------------
1 | def stub_columns_for_rails31(connection)
2 | return unless DbCharmer.rails31?
3 | connection.abstract_connection_class.retrieve_connection.stub(:columns).and_return([])
4 | end
5 |
--------------------------------------------------------------------------------
/lib/db_charmer/version.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module Version
3 | MAJOR = 1
4 | MINOR = 9
5 | PATCH = 1
6 | BUILD = nil
7 |
8 | STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/test-project/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | # Set up gems listed in the Gemfile.
4 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
5 |
6 | require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
7 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/log_records.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2 |
3 | one:
4 | level: MyString
5 | message: MyString
6 |
7 | two:
8 | level: MyString
9 | message: MyString
10 |
--------------------------------------------------------------------------------
/test-project-2.x/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
--------------------------------------------------------------------------------
/test-project/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/test-project-2.x/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/comments.yml:
--------------------------------------------------------------------------------
1 | avatar:
2 | commentable: one (Avatar)
3 | body: "This is an avatar"
4 |
5 | post:
6 | commentable: one (Post)
7 | body: "This is a post"
8 |
9 | user:
10 | commentable: one (User)
11 | body: "This is a user"
12 |
--------------------------------------------------------------------------------
/test-project-2.x/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 |
3 | gem 'rails', '2.3.18'
4 |
5 | gem 'rake', '0.9.2.2'
6 | gem 'mysql'
7 |
8 | gem 'rspec', '1.3.2'
9 | gem 'rspec-rails', '1.3.4'
10 |
11 | # Load DbCharmer as a gem
12 | gem 'db-charmer', :path => '..', :require => 'db_charmer'
13 |
--------------------------------------------------------------------------------
/test-project/TODO:
--------------------------------------------------------------------------------
1 | Functionality:
2 | - Add a controller wrapper to force all queries to the master (thanks mascohism for the idea)
3 |
4 | Docs:
5 | - Document (make more obvious) multi-db migrations code with migration_connections_should_exist
6 | - Document the fact, that all queries in a transaction go to the master
7 |
--------------------------------------------------------------------------------
/test-project/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | DbCharmerSandbox
5 | <%= stylesheet_link_tag :all %>
6 | <%= javascript_include_tag :defaults %>
7 | <%= csrf_meta_tag %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20100305234245_create_categories.rb:
--------------------------------------------------------------------------------
1 | class CreateCategories < ActiveRecord::Migration
2 | def self.up
3 | create_table :categories do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 |
10 | def self.down
11 | drop_table :categories
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20100817191548_create_cars.rb:
--------------------------------------------------------------------------------
1 | class CreateCars < ActiveRecord::Migration
2 | def self.up
3 | create_table :cars do |t|
4 | t.string :type
5 | t.string :license
6 | t.timestamps
7 | end
8 | end
9 |
10 | def self.down
11 | drop_table :cars
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20090810221944_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def self.up
3 | create_table :users do |t|
4 | t.string :login
5 | t.string :password
6 | t.timestamps
7 | end
8 | end
9 |
10 | def self.down
11 | drop_table :users
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails31/active_record/migration/command_recorder.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module Migration
4 | module CommandRecorder
5 | def invert_on_db(args)
6 | [:replay_commands_on_db, [args.first, args[1].inverse]]
7 | end
8 | end
9 | end
10 | end
11 | end
--------------------------------------------------------------------------------
/test-project/spec/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2 |
3 | one:
4 | id: 1
5 | login: MyString
6 | password: MyString
7 |
8 | two:
9 | id: 2
10 | login: MyString
11 | password: MyString
12 |
13 | bill:
14 | id: 3
15 | login: bill
16 | password: windoze
17 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20100305235831_create_avatars.rb:
--------------------------------------------------------------------------------
1 | class CreateAvatars < ActiveRecord::Migration
2 | def self.up
3 | create_table :avatars do |t|
4 | t.integer :user_id
5 | t.string :name
6 |
7 | t.timestamps
8 | end
9 | end
10 |
11 | def self.down
12 | drop_table :avatars
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test-project/spec/models/category_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Category do
4 | before(:each) do
5 | @valid_attributes = {
6 | :name => "value for name"
7 | }
8 | end
9 |
10 | it "should create a new instance given valid attributes" do
11 | Category.create!(@valid_attributes)
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20090810013922_create_posts.rb:
--------------------------------------------------------------------------------
1 | class CreatePosts < ActiveRecord::Migration
2 | def self.up
3 | create_table :posts do |t|
4 | t.string :title
5 | t.text :body
6 | t.integer :user_id
7 | t.timestamps
8 | end
9 | end
10 |
11 | def self.down
12 | drop_table :posts
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test-project/Rakefile:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] = 'test'
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require File.expand_path('../config/application', __FILE__)
7 | require 'rake'
8 |
9 | DbCharmerSandbox::Application.load_tasks
10 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/categories_posts.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2 |
3 | one_one:
4 | post_id: 1
5 | category_id: 1
6 |
7 | one_two:
8 | post_id: 1
9 | category_id: 2
10 |
11 | two_one:
12 | post_id: 2
13 | category_id: 1
14 |
15 | windoze_two:
16 | post_id: 4
17 | category_id: 2
18 |
--------------------------------------------------------------------------------
/test-project/spec/models/avatar_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Avatar do
4 | before(:each) do
5 | @valid_attributes = {
6 | :user_id => 1,
7 | :name => "value for name"
8 | }
9 | end
10 |
11 | it "should create a new instance given valid attributes" do
12 | Avatar.create!(@valid_attributes)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test-project-2.x/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require(File.join(File.dirname(__FILE__), 'config', 'boot'))
5 |
6 | require 'rake'
7 | require 'rake/testtask'
8 | require 'rake/rdoctask'
9 |
10 | require 'tasks/rails'
11 |
--------------------------------------------------------------------------------
/test-project/spec/models/post_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Post do
4 | before(:each) do
5 | @valid_attributes = {
6 | :title => "value for title",
7 | :body => "value for body"
8 | }
9 | end
10 |
11 | it "should create a new instance given valid attributes" do
12 | Post.create!(@valid_attributes)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test-project/spec/models/categories_posts_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe CategoriesPosts do
4 | before(:each) do
5 | @valid_attributes = {
6 | :post_id => 1,
7 | :category_id => 1
8 | }
9 | end
10 |
11 | it "should create a new instance given valid attributes" do
12 | CategoriesPosts.create!(@valid_attributes)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test-project/spec/models/comment_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Comment do
4 | fixtures :comments, :avatars, :posts, :users
5 |
6 | describe "preload polymorphic association" do
7 | subject do
8 | lambda {
9 | Comment.find(:all, :include => :commentable)
10 | }
11 | end
12 |
13 | it { should_not raise_error }
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test-project/spec/models/log_record_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe LogRecord do
4 | before(:each) do
5 | @valid_attributes = {
6 | :level => "value for level",
7 | :message => "value for message"
8 | }
9 | end
10 |
11 | it "should create a new instance given valid attributes" do
12 | LogRecord.create!(@valid_attributes)
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/test-project/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
7 | # Mayor.create(:name => 'Daley', :city => cities.first)
8 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/posts.yml:
--------------------------------------------------------------------------------
1 | one:
2 | id: 1
3 | title: MyString
4 | body: MyText
5 | user_id: 1
6 |
7 | two:
8 | id: 2
9 | title: MyString
10 | body: MyText
11 | user_id: 2
12 |
13 | windoze:
14 | id: 3
15 | title: Windows Sucks
16 | body: Yeah, it does!
17 | user_id: 3
18 |
19 | foo:
20 | id: 4
21 | title: Foo
22 | body: Foo body
23 | user_id: 3
24 |
--------------------------------------------------------------------------------
/test-project-2.x/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Specifies gem version of Rails to use when vendor/rails is not present
2 | RAILS_GEM_VERSION = '2.3.18' unless defined? RAILS_GEM_VERSION
3 |
4 | # Bootstrap the Rails environment, frameworks, and default configuration
5 | require File.join(File.dirname(__FILE__), 'boot')
6 |
7 | Rails::Initializer.run do |config|
8 | config.time_zone = 'UTC'
9 | end
10 |
11 |
--------------------------------------------------------------------------------
/lib/db_charmer/sharding/method.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module Sharding
3 | module Method
4 | autoload :Range, 'db_charmer/sharding/method/range'
5 | autoload :HashMap, 'db_charmer/sharding/method/hash_map'
6 | autoload :DbBlockMap, 'db_charmer/sharding/method/db_block_map'
7 | autoload :DbBlockGroupMap, 'db_charmer/sharding/method/db_block_group_map'
8 | end
9 | end
10 | end
--------------------------------------------------------------------------------
/test-project/db/migrate/20090810013829_create_log_records.rb:
--------------------------------------------------------------------------------
1 | class CreateLogRecords < ActiveRecord::Migration
2 | db_magic :connection => :logs
3 |
4 | def self.up
5 | create_table :log_records do |t|
6 | t.integer :user_id
7 | t.string :level
8 | t.string :message
9 | t.timestamps
10 | end
11 | end
12 |
13 | def self.down
14 | drop_table :log_records
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20100305234340_create_categories_posts.rb:
--------------------------------------------------------------------------------
1 | class CreateCategoriesPosts < ActiveRecord::Migration
2 | def self.up
3 | pk_in_join_table = !DbCharmer.rails3?
4 | create_table :categories_posts, :id => pk_in_join_table do |t|
5 | t.integer :post_id
6 | t.integer :category_id
7 | end
8 | end
9 |
10 | def self.down
11 | drop_table :categories_posts
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20111005193941_create_comments.rb:
--------------------------------------------------------------------------------
1 | class CreateComments < ActiveRecord::Migration
2 | def self.up
3 | create_table :comments do |t|
4 | t.string :commentable_type, :null => false
5 | t.integer :commentable_id, :null => false
6 | t.text :body, :null => false
7 |
8 | t.timestamps
9 | end
10 | end
11 |
12 | def self.down
13 | drop_table :comments
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test-project-2.x/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 |
--------------------------------------------------------------------------------
/test-project-2.x/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying do debug a problem that might steem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
--------------------------------------------------------------------------------
/test-project/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/test-project/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | DbCharmerSandbox::Application.config.session_store :cookie_store, :key => '_db-charmer-sandbox_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # DbCharmerSandbox::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/test-project-2.x/config/routes.rb:
--------------------------------------------------------------------------------
1 | ActionController::Routing::Routes.draw do |map|
2 | # Resource routes
3 | map.resources :posts
4 | map.resources :cars
5 |
6 | # Install the default routes as the lowest priority.
7 | # Note: These default routes make all actions in every controller accessible via GET requests. You should
8 | # consider removing or commenting them out if you're using named routes and resources.
9 | map.connect ':controller/:action/:id'
10 | map.connect ':controller/:action/:id.:format'
11 | end
12 |
--------------------------------------------------------------------------------
/lib/db_charmer/core_extensions.rb:
--------------------------------------------------------------------------------
1 | class Object
2 | unless defined?(try)
3 | def try(method, *options, &block)
4 | send(method, *options, &block)
5 | end
6 | end
7 |
8 | # These methods are added to all objects so we could call proxy? on anything
9 | # and figure if an object is a proxy w/o hitting method_missing or respond_to?
10 | def self.proxy?
11 | false
12 | end
13 |
14 | def proxy?
15 | false
16 | end
17 | end
18 |
19 | class NilClass
20 | def try(*args)
21 | nil
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test-project/db/create_databases.sql:
--------------------------------------------------------------------------------
1 | drop database if exists db_charmer_sandbox_test;
2 | create database db_charmer_sandbox_test;
3 |
4 | drop database if exists db_charmer_logs_test;
5 | create database db_charmer_logs_test;
6 |
7 | drop database if exists db_charmer_events_test_shard01;
8 | create database db_charmer_events_test_shard01;
9 |
10 | drop database if exists db_charmer_events_test_shard02;
11 | create database db_charmer_events_test_shard02;
12 |
13 | grant all privileges on db_charmer_sandbox_test.* to 'db_charmer_ro'@'localhost';
14 |
--------------------------------------------------------------------------------
/test-project/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | DbCharmerSandbox::Application.config.secret_token = 'bf10223f7ec7f4b2f2c6f98545ee0d172d5fe052deeba6e416e34e0a7534bc2f5a9983f331b5a799ac6544bf99d906c2a5a3bee8260d4cb985f2c096527aa3ad'
8 |
--------------------------------------------------------------------------------
/test-project/app/models/post.rb:
--------------------------------------------------------------------------------
1 | class Post < ActiveRecord::Base
2 | DB_MAGIC_DEFAULT_PARAMS = { :slave => :slave01, :force_slave_reads => false }
3 | db_magic DB_MAGIC_DEFAULT_PARAMS
4 |
5 | belongs_to :user
6 | has_and_belongs_to_many :categories
7 |
8 | def self.define_scope(*args, &block)
9 | if DbCharmer.rails3?
10 | scope(*args, &block)
11 | else
12 | named_scope(*args, &block)
13 | end
14 | end
15 |
16 | define_scope :windows_posts, :conditions => "title like '%win%'"
17 | define_scope :dummy_scope, :conditions => '1'
18 | end
19 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/event_shards_map.yml:
--------------------------------------------------------------------------------
1 | block1:
2 | start_id: 0
3 | end_id: 10
4 | shard_id: 1
5 | block_size: 10
6 | created_at: <%= Time.now.to_s(:db) %>
7 | updated_at: <%= Time.now.to_s(:db) %>
8 |
9 | block2:
10 | start_id: 10
11 | end_id: 20
12 | shard_id: 2
13 | block_size: 10
14 | created_at: <%= Time.now.to_s(:db) %>
15 | updated_at: <%= Time.now.to_s(:db) %>
16 |
17 | block3:
18 | start_id: 20
19 | end_id: 30
20 | shard_id: 1
21 | block_size: 10
22 | created_at: <%= Time.now.to_s(:db) %>
23 | updated_at: <%= Time.now.to_s(:db) %>
24 |
--------------------------------------------------------------------------------
/lib/db_charmer/sharding/method/hash_map.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module Sharding
3 | module Method
4 | class HashMap
5 | attr_accessor :map
6 |
7 | def initialize(config)
8 | @map = config[:map].clone or raise ArgumentError, "No :map defined!"
9 | end
10 |
11 | def shard_for_key(key)
12 | res = map[key] || map[:default]
13 | raise ArgumentError, "Invalid key value, no shards found for this key!" unless res
14 | return res
15 | end
16 |
17 | def support_default_shard?
18 | map.has_key?(:default)
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails2/active_record/named_scope/scope_proxy.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module NamedScope
4 | module ScopeProxy
5 |
6 | def proxy?
7 | true
8 | end
9 |
10 | def on_db(con, proxy_target = nil, &block)
11 | proxy_target ||= self
12 | proxy_scope.on_db(con, proxy_target, &block)
13 | end
14 |
15 | def on_slave(con = nil, &block)
16 | proxy_scope.on_slave(con, self, &block)
17 | end
18 |
19 | def on_master(&block)
20 | proxy_scope.on_master(self, &block)
21 | end
22 |
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/db_charmer/sharding.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module Sharding
3 | autoload :Connection, 'db_charmer/sharding/connection'
4 | autoload :StubConnection, 'db_charmer/sharding/stub_connection'
5 | autoload :Method, 'db_charmer/sharding/method'
6 |
7 | @@sharded_connections = {}
8 |
9 | def self.register_connection(config)
10 | name = config[:name] or raise ArgumentError, "No :name in connection!"
11 | @@sharded_connections[name] = DbCharmer::Sharding::Connection.new(config)
12 | end
13 |
14 | def self.sharded_connection(name)
15 | @@sharded_connections[name] or raise ArgumentError, "Invalid sharded connection name!"
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/test-project-2.x/config/preinitializer.rb:
--------------------------------------------------------------------------------
1 | begin
2 | require "rubygems"
3 | require "bundler"
4 | rescue LoadError
5 | raise "Could not load the bundler gem. Install it with `gem install bundler`."
6 | end
7 |
8 | if Gem::Version.new(Bundler::VERSION) <= Gem::Version.new("0.9.24")
9 | raise RuntimeError, "Your bundler version is too old for Rails 2.3." +
10 | "Run `gem install bundler` to upgrade."
11 | end
12 |
13 | begin
14 | # Set up load paths for all bundled gems
15 | ENV["BUNDLE_GEMFILE"] = File.expand_path("../../Gemfile", __FILE__)
16 | Bundler.setup
17 | rescue Bundler::GemNotFound
18 | raise RuntimeError, "Bundler couldn't find some gems." +
19 | "Did you run `bundle install`?"
20 | end
21 |
--------------------------------------------------------------------------------
/test-project/app/controllers/posts_controller.rb:
--------------------------------------------------------------------------------
1 | class PostsController < ApplicationController
2 | force_slave_reads :only => [ :index, :show, :new ], :except => :new
3 |
4 | # We'll use this to make sure count query would be sent to a proper server
5 | before_filter do
6 | Post.count
7 | end
8 |
9 | def index
10 | @posts = Post.all
11 | end
12 |
13 | def show
14 | @post = Post.find(params[:id])
15 | end
16 |
17 | def new
18 | @post = Post.new
19 | end
20 |
21 | def create
22 | post = Post.create!(params[:post])
23 | redirect_to(post_url(post))
24 | end
25 |
26 | def destroy
27 | Post.delete(params[:id])
28 | redirect_to(:action => :index)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test-project/config/initializers/sharding.rb:
--------------------------------------------------------------------------------
1 | # Range-based shards for testing
2 |
3 | TEXTS_SHARDING_RANGES = {
4 | 0...100 => :shard1,
5 | 100..200 => :shard2,
6 | :default => :shard3
7 | }
8 |
9 | DbCharmer::Sharding.register_connection(
10 | :name => :texts,
11 | :method => :range,
12 | :ranges => TEXTS_SHARDING_RANGES
13 | )
14 |
15 | #------------------------------------------------
16 | # Db blocks map sharding for testing
17 |
18 | SOCIAL_SHARDING = DbCharmer::Sharding.register_connection(
19 | :name => :social,
20 | :method => :db_block_map,
21 | :block_size => 10,
22 | :map_table => :event_shards_map,
23 | :shards_table => :event_shards_info,
24 | :connection => :social_shard_info
25 | )
26 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails31/active_record/preloader/association.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module Preloader
4 | module Association
5 | extend ActiveSupport::Concern
6 | included do
7 | alias_method_chain :build_scope, :db_magic
8 | end
9 |
10 | def build_scope_with_db_magic
11 | if model.db_charmer_top_level_connection? || reflection.options[:polymorphic] ||
12 | model.db_charmer_default_connection != klass.db_charmer_default_connection
13 | build_scope_without_db_magic
14 | else
15 | build_scope_without_db_magic.on_db(model)
16 | end
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test-project-2.x/config/initializers/sharding.rb:
--------------------------------------------------------------------------------
1 | # Range-based shards for testing
2 |
3 | TEXTS_SHARDING_RANGES = {
4 | 0...100 => :shard1,
5 | 100..200 => :shard2,
6 | :default => :shard3
7 | }
8 |
9 | DbCharmer::Sharding.register_connection(
10 | :name => :texts,
11 | :method => :range,
12 | :ranges => TEXTS_SHARDING_RANGES
13 | )
14 |
15 | #------------------------------------------------
16 | # Db blocks map sharding for testing
17 |
18 | SOCIAL_SHARDING = DbCharmer::Sharding.register_connection(
19 | :name => :social,
20 | :method => :db_block_map,
21 | :block_size => 10,
22 | :map_table => :event_shards_map,
23 | :shards_table => :event_shards_info,
24 | :connection => :social_shard_info
25 | )
26 |
--------------------------------------------------------------------------------
/test-project/spec/fixtures/event_shards_info.yml:
--------------------------------------------------------------------------------
1 | shard1:
2 | id: 1
3 | db_host: localhost
4 | db_name: db_charmer_events_test_shard01
5 | open: 1
6 | enabled: 1
7 | blocks_count: 2
8 | created_at: <%= Time.now.to_s(:db) %>
9 | updated_at: <%= Time.now.to_s(:db) %>
10 |
11 | shard2:
12 | id: 2
13 | db_host: localhost
14 | db_name: db_charmer_events_test_shard02
15 | open: 1
16 | enabled: 1
17 | blocks_count: 1
18 | created_at: <%= Time.now.to_s(:db) %>
19 | updated_at: <%= Time.now.to_s(:db) %>
20 |
21 | empty:
22 | id: 3
23 | db_host: localhost
24 | db_name: db_charmer_events_test_shard01
25 | open: 1
26 | enabled: 1
27 | blocks_count: 0
28 | created_at: <%= Time.now.to_s(:db) %>
29 | updated_at: <%= Time.now.to_s(:db) %>
30 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails3/active_record/log_subscriber.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module LogSubscriber
4 |
5 | def self.included(base)
6 | base.send(:attr_accessor, :connection_name)
7 | base.alias_method_chain :sql, :connection_name
8 | base.alias_method_chain :debug, :connection_name
9 | end
10 |
11 | def sql_with_connection_name(event)
12 | self.connection_name = event.payload[:connection_name]
13 | sql_without_connection_name(event)
14 | end
15 |
16 | def debug_with_connection_name(msg)
17 | conn = connection_name ? color(" [#{connection_name}]", ActiveSupport::LogSubscriber::BLUE, true) : ''
18 | debug_without_connection_name(conn + msg)
19 | end
20 |
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails31/active_record/preloader/has_and_belongs_to_many.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module Preloader
4 | module HasAndBelongsToMany
5 | extend ActiveSupport::Concern
6 | included do
7 | alias_method_chain :records_for, :db_magic
8 | end
9 |
10 | def records_for_with_db_magic(ids)
11 | if model.db_charmer_top_level_connection? || reflection.options[:polymorphic] ||
12 | model.db_charmer_default_connection != klass.db_charmer_default_connection
13 | records_for_without_db_magic(ids)
14 | else
15 | klass.on_db(model) do
16 | records_for_without_db_magic(ids)
17 | end
18 | end
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails2/abstract_adapter/log_formatting.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module AbstractAdapter
3 | module LogFormatting
4 |
5 | def self.included(base)
6 | base.alias_method_chain :format_log_entry, :connection_name
7 | end
8 |
9 | def connection_name
10 | raise "Can't find connection configuration!" unless @config
11 | @config[:connection_name]
12 | end
13 |
14 | # Rails 2.X specific logging method
15 | def format_log_entry_with_connection_name(message, dump = nil)
16 | msg = connection_name ? "[#{connection_name}] " : ''
17 | msg = " \e[0;34;1m#{msg}\e[0m" if connection_name && ::ActiveRecord::Base.colorize_logging
18 | msg << format_log_entry_without_connection_name(message, dump)
19 | end
20 |
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test-project/spec/models/cars_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Ford, "STI model" do
4 | before(:each) do
5 | @valid_attributes = {
6 | :license => "FFGH-9134"
7 | }
8 | end
9 |
10 | it "should create a new instance given valid attributes" do
11 | Ford.create!(@valid_attributes)
12 | end
13 |
14 | it "should properly handle slave find calls" do
15 | Ford.first.should be_valid
16 | end
17 | end
18 |
19 | describe Toyota, "STI model" do
20 | before(:each) do
21 | @valid_attributes = {
22 | :license => "TFGH-9134"
23 | }
24 | end
25 |
26 | it "should create a new instance given valid attributes" do
27 | Toyota.create!(@valid_attributes)
28 | end
29 |
30 | it "should properly handle slave find calls" do
31 | Toyota.first.should be_valid
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test-project-2.x/config/initializers/new_rails_defaults.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # These settings change the behavior of Rails 2 apps and will be defaults
4 | # for Rails 3. You can remove this initializer when Rails 3 is released.
5 |
6 | if defined?(ActiveRecord)
7 | # Include Active Record class name as root for JSON serialized output.
8 | ActiveRecord::Base.include_root_in_json = true
9 |
10 | # Store the full class name (including module namespace) in STI type column.
11 | ActiveRecord::Base.store_full_sti_class = true
12 | end
13 |
14 | # Use ISO 8601 format for JSON serialized times and dates.
15 | ActiveSupport.use_standard_json_time_format = true
16 |
17 | # Don't escape HTML entities in JSON, leave that for the #json_escape helper.
18 | # if you're including raw json in an HTML page.
19 | ActiveSupport.escape_html_entities_in_json = false
--------------------------------------------------------------------------------
/test-project-2.x/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying cookie session data integrity.
4 | # If you change this key, all old sessions will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | ActionController::Base.session = {
8 | :key => '_db_charmer_sandbox_session',
9 | :secret => '9b67feed7aa8a2741d9f0ac6efde543d726f7a017c8a635346be733f287fd479fbd8521c1e8a06e91af7920de1fb50b942bdf24b6ecee1569ed947c13f6697af'
10 | }
11 |
12 | # Use the database for sessions instead of the cookie-based default,
13 | # which shouldn't be used to store highly confidential information
14 | # (create the session table with "rake db:sessions:create")
15 | # ActionController::Base.session_store = :active_record_store
16 |
--------------------------------------------------------------------------------
/test-project/spec/sharding/sharding_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "DbCharmer::Sharding" do
4 | describe "in register_connection method" do
5 | it "should raise an exception if passed config has no :name parameter" do
6 | lambda {
7 | DbCharmer::Sharding.register_connection(:method => :range, :ranges => { :default => :foo })
8 | }.should raise_error(ArgumentError)
9 | end
10 |
11 | it "should not raise an exception if passed config has all required params" do
12 | lambda {
13 | DbCharmer::Sharding.register_connection(:method => :range, :ranges => { :default => :foo }, :name => :foo)
14 | }.should_not raise_error
15 | end
16 | end
17 |
18 | describe "in sharded_connection method" do
19 | it "should raise an error for invalid connection names" do
20 | lambda { DbCharmer::Sharding.sharded_connection(:blah) }.should raise_error(ArgumentError)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test-project/spec/models/user_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe User do
4 | before(:each) do
5 | @valid_attributes = {
6 | :login => "value for login",
7 | :password => "value for password"
8 | }
9 | User.switch_connection_to(nil)
10 | User.db_charmer_default_connection = nil
11 | end
12 |
13 | it "should create a new instance given valid attributes" do
14 | User.create!(@valid_attributes)
15 | end
16 |
17 | it "should create a new instance in a specified db" do
18 | # Just to make sure
19 | User.on_db(:user_master).connection.object_id.should_not == User.connection.object_id
20 |
21 | # Default connection should not be touched
22 | User.connection.should_not_receive(:insert)
23 |
24 | # Only specified connection receives an insert
25 | User.on_db(:user_master).connection.should_receive(:insert)
26 |
27 | # Test!
28 | User.on_db(:user_master).create!(@valid_attributes)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails3/active_record/relation_method.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module RelationMethod
4 |
5 | def self.extended(base)
6 | class << base
7 | alias_method_chain :relation, :db_charmer
8 | alias_method_chain :arel_engine, :db_charmer
9 | end
10 | end
11 |
12 | # Create a relation object and initialize its default connection
13 | def relation_with_db_charmer(*args, &block)
14 | relation_without_db_charmer(*args, &block).tap do |rel|
15 | rel.db_charmer_connection = self.connection
16 | rel.db_charmer_enable_slaves = self.db_charmer_slaves.any?
17 | rel.db_charmer_connection_is_forced = !db_charmer_top_level_connection?
18 | end
19 | end
20 |
21 | # Use the model itself an engine for Arel, do not fall back to AR::Base
22 | def arel_engine_with_db_charmer(*)
23 | self
24 | end
25 |
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/db_charmer/sharding/method/range.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module Sharding
3 | module Method
4 | class Range
5 | attr_accessor :ranges
6 |
7 | def initialize(config)
8 | @ranges = config[:ranges] ? config[:ranges].clone : raise(ArgumentError, "No :ranges defined!")
9 | end
10 |
11 | def shard_for_key(key)
12 | return ranges[:default] if key == :default
13 |
14 | ranges.each do |range, shard|
15 | next if range == :default
16 | return shard if range.member?(key.to_i)
17 | end
18 |
19 | return ranges[:default] if ranges[:default]
20 | raise ArgumentError, "Invalid key value, no shards found for this key!"
21 | end
22 |
23 | def support_default_shard?
24 | ranges.has_key?(:default)
25 | end
26 |
27 | def shard_connections
28 | ranges.values.uniq
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/test-project/spec/sharding/connection_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DbCharmer::Sharding::Connection do
4 | describe "in constructor" do
5 | it "should not fail if method name is correct" do
6 | lambda { DbCharmer::Sharding::Connection.new(:name => :foo, :method => :range, :ranges => {}) }.should_not raise_error
7 | end
8 |
9 | it "should fail if method name is missing" do
10 | lambda { DbCharmer::Sharding::Connection.new(:name => :foo) }.should raise_error(ArgumentError)
11 | end
12 |
13 | it "should fail if method name is invalid" do
14 | lambda { DbCharmer::Sharding::Connection.new(:name => :foo, :method => :foo) }.should raise_error(NameError)
15 | end
16 |
17 | it "should instantiate a sharder class according to the :method value" do
18 | DbCharmer::Sharding::Method::Range.should_receive(:new)
19 | DbCharmer::Sharding::Connection.new(:name => :foo, :method => :range, :ranges => {})
20 | end
21 | end
22 | end
23 |
24 |
--------------------------------------------------------------------------------
/lib/db_charmer/active_record/association_preload.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module AssociationPreload
4 | ASSOCIATION_TYPES = [ :has_one, :has_many, :belongs_to, :has_and_belongs_to_many ]
5 |
6 | def self.extended(base)
7 | ASSOCIATION_TYPES.each do |association_type|
8 | base.class_eval <<-EOF, __FILE__, __LINE__ + 1
9 | def self.preload_#{association_type}_association(records, reflection, preload_options = {})
10 | if self.db_charmer_top_level_connection? || reflection.options[:polymorphic] ||
11 | self.db_charmer_default_connection != reflection.klass.db_charmer_default_connection
12 | return super(records, reflection, preload_options)
13 | end
14 | reflection.klass.on_db(self) do
15 | super(records, reflection, preload_options)
16 | end
17 | end
18 | EOF
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/db_charmer/sharding/connection.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module Sharding
3 | class Connection
4 | attr_accessor :config, :sharder
5 |
6 | def initialize(config)
7 | @config = config
8 | @sharder = self.instantiate_sharder
9 | end
10 |
11 | def instantiate_sharder
12 | raise ArgumentError, "No :method passed!" unless config[:method]
13 | sharder_class_name = "DbCharmer::Sharding::Method::#{config[:method].to_s.classify}"
14 | sharder_class = sharder_class_name.constantize
15 | sharder_class.new(config)
16 | end
17 |
18 | def shard_connections
19 | sharder.respond_to?(:shard_connections) ? sharder.shard_connections : nil
20 | end
21 |
22 | def support_default_shard?
23 | sharder.respond_to?(:support_default_shard?) && sharder.support_default_shard?
24 | end
25 |
26 | def default_connection
27 | @default_connection ||= DbCharmer::Sharding::StubConnection.new(self)
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.8.7
4 | - 1.9.3
5 | - 2.0.0
6 |
7 | env:
8 | - RAILS_VERSION=2.x
9 | - RAILS_VERSION=3.0.20
10 | - RAILS_VERSION=3.1.12
11 | - RAILS_VERSION=3.2.3
12 | - RAILS_VERSION=3.2.15
13 | - RAILS_VERSION=3.2.15 DB_CHARMER_GEM=1.9.0
14 |
15 | notifications:
16 | recipients:
17 | - alexey@kovyrin.net
18 |
19 | script: ./ci_build
20 |
21 | # Whitelist branches to test
22 | branches:
23 | only:
24 | - master
25 | - rails4
26 |
27 | # Build matrix configuration
28 | matrix:
29 | exclude:
30 | # Do not run Rails 2.x tests on ruby 1.9
31 | - rvm: 1.9.3
32 | env: RAILS_VERSION=2.x
33 |
34 | # Do not run Rails 2.x tests on ruby 2.0
35 | - rvm: 2.0.0
36 | env: RAILS_VERSION=2.x
37 |
38 | # Do not run Rails 3.0 tests on ruby 2.0
39 | - rvm: 2.0.0
40 | env: RAILS_VERSION=3.0.20
41 |
42 | # Do not run Rails 3.1 tests on ruby 2.0
43 | - rvm: 2.0.0
44 | env: RAILS_VERSION=3.1.12
45 |
46 | # Do not run early Rails 3.2 tests on ruby 2.0
47 | - rvm: 2.0.0
48 | env: RAILS_VERSION=3.2.3
49 |
--------------------------------------------------------------------------------
/test-project/spec/unit/abstract_adapter/log_formatting_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | if DbCharmer.rails2?
4 | describe 'AbstractAdapter' do
5 | it "should respond to connection_name accessor" do
6 | ActiveRecord::Base.connection.respond_to?(:connection_name).should be_true
7 | end
8 |
9 | it "should have connection_name read accessor working" do
10 | DbCharmer::ConnectionFactory.generate_abstract_class('logs').connection.connection_name.should == 'logs'
11 | DbCharmer::ConnectionFactory.generate_abstract_class('slave01').connection.connection_name.should == 'slave01'
12 | ActiveRecord::Base.connection.connection_name.should be_nil
13 | end
14 |
15 | it "should append connection name to log records on non-default connections" do
16 | User.switch_connection_to nil
17 | default_message = User.connection.send(:format_log_entry, 'hello world')
18 | switched_message = User.on_db(:slave01).connection.send(:format_log_entry, 'hello world')
19 | switched_message.should_not == default_message
20 | switched_message.should match(/slave01/)
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Copyright (c) 2011, Oleksiy Kovyrin
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test-project/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to spec/ when you run 'rails generate rspec:install'
2 | ENV["RAILS_ENV"] = 'test'
3 | require File.expand_path("../../config/environment", __FILE__)
4 | require 'rspec/rails'
5 |
6 | # Requires supporting ruby files with custom matchers and macros, etc,
7 | # in spec/support/ and its subdirectories.
8 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
9 |
10 | RSpec.configure do |config|
11 | # == Mock Framework
12 | #
13 | # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
14 | #
15 | # config.mock_with :mocha
16 | # config.mock_with :flexmock
17 | # config.mock_with :rr
18 | config.mock_with :rspec
19 |
20 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
21 | config.fixture_path = "#{::Rails.root}/spec/fixtures"
22 |
23 | # If you're not using ActiveRecord, or you'd prefer not to run each of your
24 | # examples within a transaction, remove the following line or assign false
25 | # instead of true.
26 | config.use_transactional_fixtures = false
27 | config.use_instantiated_fixtures = false
28 | end
29 |
--------------------------------------------------------------------------------
/test-project/spec/unit/connection_proxy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DbCharmer::ConnectionProxy do
4 | before(:each) do
5 | class ProxyTest; end
6 | @conn = mock('connection')
7 | @proxy = DbCharmer::ConnectionProxy.new(ProxyTest, :foo)
8 | end
9 |
10 | it "should retrieve connection from an underlying class" do
11 | ProxyTest.should_receive(:retrieve_connection).and_return(@conn)
12 | @proxy.inspect
13 | end
14 |
15 | it "should be a blankslate for the connection" do
16 | ProxyTest.stub!(:retrieve_connection).and_return(@conn)
17 | @proxy.should be(@conn)
18 | end
19 |
20 | it "should proxy methods with a block parameter" do
21 | module MockConnection
22 | def self.foo
23 | raise "No block given!" unless block_given?
24 | yield
25 | end
26 | end
27 | ProxyTest.stub!(:retrieve_connection).and_return(MockConnection)
28 | res = @proxy.foo { :foo }
29 | res.should == :foo
30 | end
31 |
32 | it "should proxy all calls to the underlying class connections" do
33 | ProxyTest.stub!(:retrieve_connection).and_return(@conn)
34 | @conn.should_receive(:foo)
35 | @proxy.foo
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20100330180517_create_event_tables.rb:
--------------------------------------------------------------------------------
1 | class CreateEventTables < ActiveRecord::Migration
2 | # In test environment just use database.yml-defined connections
3 | if Rails.env.test?
4 | db_magic :connections => [ :social_shard01, :social_shard02 ]
5 | else
6 | db_magic :sharded_connection => :social
7 | end
8 |
9 | def self.up
10 | sql = <<-SQL
11 | CREATE TABLE `timeline_events` (
12 | `event_id` int(11) NOT NULL AUTO_INCREMENT,
13 | `from_uid` int(11) NOT NULL,
14 | `to_uid` int(11) NOT NULL,
15 | `original_created_at` datetime NOT NULL,
16 | `event_type` int(11) NOT NULL,
17 | `event_data` text,
18 | `replies_count` int(11) NOT NULL DEFAULT '0',
19 | `parent_id` int(11) NOT NULL DEFAULT '0',
20 | `touched_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
21 | `on_profile` int(1) NOT NULL DEFAULT '0',
22 | PRIMARY KEY (`to_uid`,`parent_id`,`touched_at`,`event_id`),
23 | UNIQUE KEY `event_id_and_to_uid_key` (`event_id`,`to_uid`),
24 | KEY `on_profile_index` (`to_uid`,`on_profile`,`touched_at`)
25 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8
26 | SQL
27 | execute(sql)
28 | end
29 |
30 | def self.down
31 | drop_table :timeline_events
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails3/abstract_adapter/connection_name.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module AbstractAdapter
3 | module ConnectionName
4 |
5 | # We use this proxy to push connection name down to instrumenters w/o monkey-patching the log method itself
6 | class InstrumenterDecorator < ActiveSupport::BasicObject
7 | def initialize(adapter, instrumenter)
8 | @adapter = adapter
9 | @instrumenter = instrumenter
10 | end
11 |
12 | def instrument(name, payload = {}, &block)
13 | payload[:connection_name] ||= @adapter.connection_name
14 | @instrumenter.instrument(name, payload, &block)
15 | end
16 |
17 | def method_missing(meth, *args, &block)
18 | @instrumenter.send(meth, *args, &block)
19 | end
20 | end
21 |
22 | def self.included(base)
23 | base.alias_method_chain :initialize, :connection_name
24 | end
25 |
26 | def connection_name
27 | raise "Can't find connection configuration!" unless @config
28 | @config[:connection_name]
29 | end
30 |
31 | def initialize_with_connection_name(*args)
32 | initialize_without_connection_name(*args)
33 | @instrumenter = InstrumenterDecorator.new(self, @instrumenter)
34 | end
35 |
36 | end
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/db-charmer.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $:.push File.expand_path('../lib', __FILE__)
3 | require 'db_charmer/version'
4 |
5 | Gem::Specification.new do |s|
6 | s.name = 'db-charmer'
7 | s.version = DbCharmer::Version::STRING
8 | s.platform = Gem::Platform::RUBY
9 |
10 | s.authors = [ 'Oleksiy Kovyrin' ]
11 | s.email = 'alexey@kovyrin.net'
12 | s.homepage = 'http://kovyrin.github.io/db-charmer/'
13 | s.summary = 'ActiveRecord Connections Magic (slaves, multiple connections, etc)'
14 | s.description = 'DbCharmer is a Rails plugin (and gem) that could be used to manage AR model connections, implement master/slave query schemes, sharding and other magic features many high-scale applications need.'
15 | s.license = 'MIT'
16 |
17 | s.rdoc_options = [ '--charset=UTF-8' ]
18 |
19 | s.files = Dir['lib/**/*'] + Dir['*.rb']
20 | s.files += %w[ README.rdoc LICENSE CHANGES ]
21 |
22 | s.require_paths = [ 'lib' ]
23 | s.extra_rdoc_files = [ 'LICENSE', 'README.rdoc' ]
24 |
25 | # Dependencies
26 | s.add_dependency 'activesupport', '< 4.0.0'
27 | s.add_dependency 'activerecord', '< 4.0.0'
28 |
29 | s.add_development_dependency 'rspec'
30 | s.add_development_dependency 'yard'
31 | s.add_development_dependency 'actionpack'
32 | end
33 |
--------------------------------------------------------------------------------
/test-project/db/migrate/20100328201317_create_sharding_map_tables.rb:
--------------------------------------------------------------------------------
1 | class CreateShardingMapTables < ActiveRecord::Migration
2 | db_magic :connection => :social_shard_info
3 |
4 | def self.up
5 | create_table :event_shards_info, :force => true do |t|
6 | t.timestamps
7 | t.string :db_host, :null => false
8 | t.integer :db_port, :null => false, :default => 3306
9 | t.string :db_user, :null => false, :default => 'root'
10 | t.string :db_pass, :null => false, :default => ''
11 | t.string :db_name, :null => false
12 | t.boolean :open, :null => false, :default => false
13 | t.boolean :enabled, :null => false, :default => false
14 | t.integer :blocks_count, :null => false, :default => 0
15 | end
16 |
17 | add_index :event_shards_info, [:enabled, :open, :blocks_count], :name => "alloc"
18 |
19 | create_table :event_shards_map, :id => false, :force => true do |t|
20 | t.integer :start_id, :null => false
21 | t.integer :end_id, :null => false
22 | t.integer :shard_id, :null => false
23 | t.integer :block_size, :null => false, :default => 0
24 | t.timestamps
25 | end
26 |
27 | add_index :event_shards_map, [:start_id, :end_id], :unique => true
28 | add_index :event_shards_map, :shard_id
29 | end
30 |
31 | def self.down
32 | drop_table :event_shards_map
33 | drop_table :event_shards_info
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test-project/spec/sharding/method/hash_map_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DbCharmer::Sharding::Method::HashMap do
4 | SHARDING_MAP = {
5 | 'US' => :us_users,
6 | 'CA' => :ca_users,
7 | :default => :other_users
8 | }
9 |
10 | before do
11 | @sharder = DbCharmer::Sharding::Method::HashMap.new(:map => SHARDING_MAP)
12 | end
13 |
14 | describe "standard interface" do
15 | it "should respond to shard_for_id" do
16 | @sharder.should respond_to(:shard_for_key)
17 | end
18 |
19 | it "should return a shard name to be used for an key" do
20 | @sharder.shard_for_key('US').should be_kind_of(Symbol)
21 | end
22 |
23 | it "should support default shard" do
24 | @sharder.support_default_shard?.should be_true
25 | end
26 | end
27 |
28 | describe "should correctly return shards for all keys defined in the map" do
29 | SHARDING_MAP.except(:default).each do |key, val|
30 | it "for #{key}" do
31 | @sharder.shard_for_key(key).should == val
32 | end
33 | end
34 | end
35 |
36 | it "should correctly return default shard" do
37 | @sharder.shard_for_key('UA').should == :other_users
38 | end
39 |
40 | it "should raise an exception when there is no default shard and nothing matched" do
41 | @sharder.map.delete(:default)
42 | lambda { @sharder.shard_for_key('UA') }.should raise_error(ArgumentError)
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/test-project-2.x/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | # Settings specified here will take precedence over those in config/environment.rb
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 | config.cache_classes = true
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.action_controller.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 | config.action_view.cache_template_loading = true
16 |
17 | # Disable request forgery protection in test environment
18 | config.action_controller.allow_forgery_protection = false
19 |
20 | # Tell Action Mailer not to deliver emails to the real world.
21 | # The :test delivery method accumulates sent emails in the
22 | # ActionMailer::Base.deliveries array.
23 | config.action_mailer.delivery_method = :test
24 |
25 | # Use SQL instead of Active Record's schema dumper when creating the test database.
26 | # This is necessary if your schema can't be completely dumped by the schema dumper,
27 | # like if you have constraints or database-specific column types
28 | # config.active_record.schema_format = :sql
29 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails3/active_record/master_slave_routing.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module MasterSlaveRouting
4 |
5 | module ClassMethods
6 | SLAVE_METHODS = [ :find_by_sql, :count_by_sql ]
7 | MASTER_METHODS = [ ] # I don't know any methods in AR::Base that change data directly w/o going to the relation object
8 |
9 | SLAVE_METHODS.each do |slave_method|
10 | class_eval <<-EOF, __FILE__, __LINE__ + 1
11 | def #{slave_method}(*args, &block)
12 | first_level_on_slave do
13 | super(*args, &block)
14 | end
15 | end
16 | EOF
17 | end
18 |
19 | MASTER_METHODS.each do |master_method|
20 | class_eval <<-EOF, __FILE__, __LINE__ + 1
21 | def #{master_method}(*args, &block)
22 | on_master do
23 | super(*args, &block)
24 | end
25 | end
26 | EOF
27 | end
28 | end
29 |
30 | module InstanceMethods
31 | MASTER_METHODS = [ :reload ]
32 |
33 | MASTER_METHODS.each do |master_method|
34 | class_eval <<-EOF, __FILE__, __LINE__ + 1
35 | def #{master_method}(*args, &block)
36 | self.class.on_master do
37 | super(*args, &block)
38 | end
39 | end
40 | EOF
41 | end
42 | end
43 |
44 | end
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/db_charmer/active_record/sharding.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module Sharding
4 |
5 | def self.extended(model)
6 | model.cattr_accessor(:sharded_connection)
7 | end
8 |
9 | def shard_for(key, proxy_target = nil, &block)
10 | raise ArgumentError, "No sharded connection configured!" unless sharded_connection
11 | conn = sharded_connection.sharder.shard_for_key(key)
12 | on_db(conn, proxy_target, &block)
13 | end
14 |
15 | # Run on default shard (if supported by the sharding method)
16 | def on_default_shard(proxy_target = nil, &block)
17 | raise ArgumentError, "No sharded connection configured!" unless sharded_connection
18 |
19 | if sharded_connection.support_default_shard?
20 | shard_for(:default, proxy_target, &block)
21 | else
22 | raise ArgumentError, "This model's sharding method does not support default shard"
23 | end
24 | end
25 |
26 | # Enumerate shards
27 | def on_each_shard(proxy_target = nil, &block)
28 | raise ArgumentError, "No sharded connection configured!" unless sharded_connection
29 |
30 | conns = sharded_connection.shard_connections
31 | raise ArgumentError, "This model's sharding method does not support shards enumeration" unless conns
32 |
33 | conns.each do |conn|
34 | on_db(conn, proxy_target, &block)
35 | end
36 | end
37 |
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails2/active_record/master_slave_routing.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module MasterSlaveRouting
4 |
5 | module ClassMethods
6 | SLAVE_METHODS = [ :find_by_sql, :count_by_sql, :calculate ]
7 | MASTER_METHODS = [ :update, :create, :delete, :destroy, :delete_all, :destroy_all, :update_all, :update_counters ]
8 |
9 | SLAVE_METHODS.each do |slave_method|
10 | class_eval <<-EOF, __FILE__, __LINE__ + 1
11 | def #{slave_method}(*args, &block)
12 | first_level_on_slave do
13 | super(*args, &block)
14 | end
15 | end
16 | EOF
17 | end
18 |
19 | MASTER_METHODS.each do |master_method|
20 | class_eval <<-EOF, __FILE__, __LINE__ + 1
21 | def #{master_method}(*args, &block)
22 | on_master do
23 | super(*args, &block)
24 | end
25 | end
26 | EOF
27 | end
28 |
29 | def find(*args, &block)
30 | options = args.last
31 | if options.is_a?(Hash) && options[:lock]
32 | on_master { super(*args, &block) }
33 | else
34 | super(*args, &block)
35 | end
36 | end
37 | end
38 |
39 | module InstanceMethods
40 | def reload(*args, &block)
41 | self.class.on_master do
42 | super(*args, &block)
43 | end
44 | end
45 | end
46 |
47 | end
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/test-project/spec/models/range_sharded_model_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe RangeShardedModel do
4 | describe "class method shard_for" do
5 | describe "should correctly set shards in range-defined shards" do
6 | [ 0, 1, 50, 99].each do |id|
7 | it "for #{id}" do
8 | RangeShardedModel.shard_for(id) do |m|
9 | m.connection.object_id.should == RangeShardedModel.on_db(:shard1).connection.object_id
10 | end
11 | end
12 | end
13 |
14 | [ 100, 101, 150, 199, 200].each do |id|
15 | it "for #{id}" do
16 | RangeShardedModel.shard_for(id) do |m|
17 | m.connection.object_id.should == RangeShardedModel.on_db(:shard2).connection.object_id
18 | end
19 | end
20 | end
21 | end
22 |
23 | describe "should correctly set shards in default shard" do
24 | [ 201, 500].each do |id|
25 | it "for #{id}" do
26 | RangeShardedModel.shard_for(id) do |m|
27 | m.connection.object_id.should == RangeShardedModel.on_db(:shard3).connection.object_id
28 | end
29 | end
30 | end
31 | end
32 |
33 | it "should raise an exception when there is no default shard and no ranged shards matched" do
34 | begin
35 | default_shard = RangeShardedModel.sharded_connection.sharder.ranges.delete(:default)
36 | lambda { RangeShardedModel.shard_for(500) }.should raise_error(ArgumentError)
37 | ensure
38 | RangeShardedModel.sharded_connection.sharder.ranges[:default] = default_shard
39 | end
40 | end
41 | end
42 | end
43 |
44 |
--------------------------------------------------------------------------------
/test-project/Gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org'
2 |
3 | gem 'rake', "0.9.2.2"
4 | gem 'mysql', "2.8.1"
5 |
6 | gem 'rspec', '< 3.0'
7 | gem 'rspec-core', '< 3.0'
8 | gem 'rspec-rails', '< 3.0'
9 |
10 | # Load DbCharmer as a gem
11 | if ENV['DB_CHARMER_GEM'].to_s == ''
12 | gem_path = File.expand_path(File.dirname(File.dirname(__FILE__)))
13 | puts "Using on-disk db-charmer code from '#{gem_path}'..."
14 | gem 'db-charmer', :path => gem_path, :require => 'db_charmer'
15 | else
16 | puts "Using db-charmer gem: #{ENV['DB_CHARMER_GEM']}..."
17 | gem 'db-charmer', ENV['DB_CHARMER_GEM'], :require => 'db_charmer'
18 | end
19 |
20 | # Detect Rails version we need to use
21 | rails_version_file = File.expand_path("../.rails-version", __FILE__)
22 | version = File.exists?(rails_version_file) && File.read(rails_version_file).chomp
23 | version ||= ENV['RAILS_VERSION']
24 | version ||= '3-2-stable'
25 |
26 | # Require gems for selected rails version
27 | case version
28 | when /master/
29 | gem "rails", :git => "git://github.com/rails/rails.git"
30 | gem "arel", :git => "git://github.com/rails/arel.git"
31 | gem "journey", :git => "git://github.com/rails/journey.git"
32 | when /3-0-stable/
33 | gem "rails", :git => "git://github.com/rails/rails.git", :branch => "3-0-stable"
34 | gem "arel", :git => "git://github.com/rails/arel.git", :branch => "2-0-stable"
35 | when /3-1-stable/
36 | gem "rails", :git => "git://github.com/rails/rails.git", :branch => "3-1-stable"
37 | when /3-2-stable/
38 | gem "rails", :git => "git://github.com/rails/rails.git", :branch => "3-2-stable"
39 | else
40 | gem "rails", version
41 | end
42 |
--------------------------------------------------------------------------------
/test-project/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | DbCharmerSandbox::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Log error messages when you accidentally call methods on nil.
11 | config.whiny_nils = true
12 |
13 | # Show full error reports and disable caching
14 | config.consider_all_requests_local = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Raise exceptions instead of rendering exception templates
18 | config.action_dispatch.show_exceptions = false
19 |
20 | # Disable request forgery protection in test environment
21 | config.action_controller.allow_forgery_protection = false
22 |
23 | # Tell Action Mailer not to deliver emails to the real world.
24 | # The :test delivery method accumulates sent emails in the
25 | # ActionMailer::Base.deliveries array.
26 | config.action_mailer.delivery_method = :test
27 |
28 | # Use SQL instead of Active Record's schema dumper when creating the test database.
29 | # This is necessary if your schema can't be completely dumped by the schema dumper,
30 | # like if you have constraints or database-specific column types
31 | # config.active_record.schema_format = :sql
32 |
33 | # Print deprecation notices to the stderr
34 | config.active_support.deprecation = :stderr
35 | end
36 |
--------------------------------------------------------------------------------
/test-project/spec/sharding/method/range_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DbCharmer::Sharding::Method::Range do
4 | SHARDING_RANGES = {
5 | 0...100 => :shard1,
6 | 100..200 => :shard2,
7 | :default => :shard3
8 | }
9 |
10 | before do
11 | @sharder = DbCharmer::Sharding::Method::Range.new(:ranges => SHARDING_RANGES)
12 | end
13 |
14 | describe "standard interface" do
15 | it "should respond to shard_for_id" do
16 | @sharder.should respond_to(:shard_for_key)
17 | end
18 |
19 | it "should return a shard name to be used for an key" do
20 | @sharder.shard_for_key(1).should be_kind_of(Symbol)
21 | end
22 |
23 | it "should support default shard" do
24 | @sharder.support_default_shard?.should be_true
25 | end
26 | end
27 |
28 | describe "should correctly return shards for all ids in defined ranges" do
29 | [ 0, 1, 50, 99].each do |id|
30 | it "for #{id}" do
31 | @sharder.shard_for_key(id).should == :shard1
32 | end
33 | end
34 |
35 | [ 100, 101, 150, 199, 200].each do |id|
36 | it "for #{id}" do
37 | @sharder.shard_for_key(id).should == :shard2
38 | end
39 | end
40 | end
41 |
42 | describe "should correctly return shard for all ids outside the ranges if has a default" do
43 | [ 201, 500].each do |id|
44 | it "for #{id}" do
45 | @sharder.shard_for_key(id).should == :shard3
46 | end
47 | end
48 | end
49 |
50 | it "should raise an exception when there is no default shard and no ranges matched" do
51 | @sharder.ranges.delete(:default)
52 | lambda { @sharder.shard_for_key(500) }.should raise_error(ArgumentError)
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/db_charmer/with_remapped_databases.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | def self.with_remapped_databases(mappings, &proc)
3 | old_mappings = ::ActiveRecord::Base.db_charmer_database_remappings
4 | begin
5 | ::ActiveRecord::Base.db_charmer_database_remappings = mappings
6 | if mappings[:master] || mappings['master']
7 | with_all_hijacked(&proc)
8 | else
9 | proc.call
10 | end
11 | ensure
12 | ::ActiveRecord::Base.db_charmer_database_remappings = old_mappings
13 | end
14 | end
15 |
16 | def self.hijack_new_classes?
17 | !! Thread.current[:db_charmer_hijack_new_classes]
18 | end
19 |
20 | private
21 |
22 | def self.with_all_hijacked
23 | old_hijack_new_classes = Thread.current[:db_charmer_hijack_new_classes]
24 | begin
25 | Thread.current[:db_charmer_hijack_new_classes] = true
26 | subclasses_method = DbCharmer.rails3? ? :descendants : :subclasses
27 | ::ActiveRecord::Base.send(subclasses_method).each do |subclass|
28 | subclass.hijack_connection!
29 | end
30 | yield
31 | ensure
32 | Thread.current[:db_charmer_hijack_new_classes] = old_hijack_new_classes
33 | end
34 | end
35 | end
36 |
37 | #---------------------------------------------------------------------------------------------------
38 | # Hijack connection on all new AR classes when we're in a block with main AR connection remapped
39 | class ActiveRecord::Base
40 | class << self
41 | def inherited_with_hijacking(subclass)
42 | out = inherited_without_hijacking(subclass)
43 | hijack_connection! if DbCharmer.hijack_new_classes?
44 | out
45 | end
46 |
47 | alias_method_chain :inherited, :hijacking
48 | end
49 | end
50 |
--------------------------------------------------------------------------------
/ci_build:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Making the script more robust
4 | set -e # Exit on errors
5 | set -u # Exit on uninitialized variables
6 |
7 | RAILS_VERSION=${RAILS_VERSION:-}
8 | if [ "$RAILS_VERSION" == "" ]; then
9 | echo "Please specify rails version using RAILS_VERSION environment variable!"
10 | exit 1
11 | fi
12 |
13 | # Change directory according to the rails version
14 | if [ "$RAILS_VERSION" == "2.x" ]; then
15 | # Downgrade rubygems because rails 2.3 does not work on 2.0+
16 | gem update --system 1.8.25
17 | cd test-project-2.x
18 | else
19 | cd test-project
20 | fi
21 |
22 | # Print version info
23 | echo "-----------------------------------------------------------------------------------------------------------------"
24 | echo " * Running specs for Rails version $RAILS_VERSION..."
25 | echo " * Ruby version: `ruby --version`"
26 | echo " * Rubygems version: `gem --version`"
27 | echo " * DbCharmer gem version: '${DB_CHARMER_GEM:-trunk}'"
28 | echo "-----------------------------------------------------------------------------------------------------------------"
29 |
30 | # Test environment
31 | export RAILS_ENV=test
32 |
33 | # Configure database access
34 | cp -f config/database.yml.example config/database.yml
35 |
36 | # Create databases and sharding tables
37 | mysql -u root < db/create_databases.sql
38 | mysql -u root db_charmer_sandbox_test < db/sharding.sql
39 |
40 | # Install gems
41 | rm -f Gemfile.lock
42 | bundle install
43 |
44 | # Run migrations
45 | bundle exec rake --trace db:migrate
46 |
47 | # Run the build and return its exit code
48 | if [ "$RAILS_VERSION" == "2.x" ]; then
49 | exec bundle exec spec -p '/*/**/*_spec.rb' -cbfs spec
50 | else
51 | exec bundle exec rspec -cbfs spec
52 | fi
53 |
--------------------------------------------------------------------------------
/test-project-2.x/config/database.yml.example:
--------------------------------------------------------------------------------
1 | common: &common
2 | adapter: mysql
3 | encoding: utf8
4 | reconnect: false
5 | pool: 1
6 | username: root
7 | password:
8 |
9 | #----------------------------------------------------------------
10 | test:
11 | <<: *common
12 | database: db_charmer_sandbox_test
13 |
14 | # logs database
15 | logs:
16 | <<: *common
17 | database: db_charmer_logs_test
18 |
19 | # slave database
20 | slave01:
21 | <<: *common
22 | username: db_charmer_ro
23 | database: db_charmer_sandbox_test
24 |
25 | user_master:
26 | <<: *common
27 | database: db_charmer_sandbox_test
28 |
29 | # shard mapping db
30 | social_shard_info:
31 | <<: *common
32 | database: db_charmer_sandbox_test
33 |
34 | # for migrations only
35 | social_shard01:
36 | <<: *common
37 | database: db_charmer_events_test_shard01
38 |
39 | # for migrations only
40 | social_shard02:
41 | <<: *common
42 | database: db_charmer_events_test_shard02
43 |
44 | #----------------------------------------------------------------
45 | test22:
46 | <<: *common
47 | database: db_charmer_sandbox22_test
48 |
49 | # logs database
50 | logs:
51 | <<: *common
52 | database: db_charmer_logs22_test
53 |
54 | # slave database
55 | slave01:
56 | <<: *common
57 | username: db_charmer_ro
58 | database: db_charmer_sandbox22_test
59 |
60 | user_master:
61 | <<: *common
62 | database: db_charmer_sandbox22_test
63 |
64 | # shard mapping db
65 | social_shard_info:
66 | <<: *common
67 | database: db_charmer_sandbox22_test
68 |
69 | # for migrations only
70 | social_shard01:
71 | <<: *common
72 | database: db_charmer_events22_test_shard01
73 |
74 | # for migrations only
75 | social_shard02:
76 | <<: *common
77 | database: db_charmer_events22_test_shard02
78 |
--------------------------------------------------------------------------------
/test-project/config/database.yml.example:
--------------------------------------------------------------------------------
1 | common: &common
2 | adapter: mysql
3 | encoding: utf8
4 | reconnect: false
5 | pool: 10
6 | username: root
7 | password:
8 |
9 | #----------------------------------------------------------------
10 | test:
11 | <<: *common
12 | database: db_charmer_sandbox_test
13 |
14 | # logs database
15 | logs:
16 | <<: *common
17 | database: db_charmer_logs_test
18 |
19 | # slave database
20 | slave01:
21 | <<: *common
22 | username: db_charmer_ro
23 | database: db_charmer_sandbox_test
24 |
25 | user_master:
26 | <<: *common
27 | database: db_charmer_sandbox_test
28 |
29 | # shard mapping db
30 | social_shard_info:
31 | <<: *common
32 | database: db_charmer_sandbox_test
33 |
34 | # for migrations only
35 | social_shard01:
36 | <<: *common
37 | database: db_charmer_events_test_shard01
38 |
39 | # for migrations only
40 | social_shard02:
41 | <<: *common
42 | database: db_charmer_events_test_shard02
43 |
44 | #----------------------------------------------------------------
45 | test22:
46 | <<: *common
47 | database: db_charmer_sandbox22_test
48 |
49 | # logs database
50 | logs:
51 | <<: *common
52 | database: db_charmer_logs22_test
53 |
54 | # slave database
55 | slave01:
56 | <<: *common
57 | username: db_charmer_ro
58 | database: db_charmer_sandbox22_test
59 |
60 | user_master:
61 | <<: *common
62 | database: db_charmer_sandbox22_test
63 |
64 | # shard mapping db
65 | social_shard_info:
66 | <<: *common
67 | database: db_charmer_sandbox22_test
68 |
69 | # for migrations only
70 | social_shard01:
71 | <<: *common
72 | database: db_charmer_events22_test_shard01
73 |
74 | # for migrations only
75 | social_shard02:
76 | <<: *common
77 | database: db_charmer_events22_test_shard02
78 |
--------------------------------------------------------------------------------
/test-project/spec/unit/action_controller/force_slave_reads_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | class BlahController < ActionController::Base; end
4 |
5 | describe ActionController, "with force_slave_reads extension" do
6 | before do
7 | BlahController.force_slave_reads({}) # cleanup status
8 | end
9 |
10 | it "should not force slave reads when there are no actions defined as forced" do
11 | BlahController.force_slave_reads_action?(:index).should be_false
12 | end
13 |
14 | it "should force slave reads for :only actions" do
15 | BlahController.force_slave_reads :only => :index
16 | BlahController.force_slave_reads_action?(:index).should be_true
17 | end
18 |
19 | it "should not force slave reads for non-listed actions when there is :only parameter" do
20 | BlahController.force_slave_reads :only => :index
21 | BlahController.force_slave_reads_action?(:show).should be_false
22 | end
23 |
24 | it "should not force slave reads for :except actions" do
25 | BlahController.force_slave_reads :except => :delete
26 | BlahController.force_slave_reads_action?(:delete).should be_false
27 | end
28 |
29 | it "should force slave reads for non-listed actions when there is :except parameter" do
30 | BlahController.force_slave_reads :except => :delete
31 | BlahController.force_slave_reads_action?(:index).should be_true
32 | end
33 |
34 | it "should not force slave reads for actions listed in both :except and :only lists" do
35 | BlahController.force_slave_reads :only => :delete, :except => :delete
36 | BlahController.force_slave_reads_action?(:delete).should be_false
37 | end
38 |
39 | it "should not force slave reads for non-listed actions when there are :except and :only lists present" do
40 | BlahController.force_slave_reads :only => :index, :except => :delete
41 | BlahController.force_slave_reads_action?(:show).should be_false
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/lib/db_charmer/connection_proxy.rb:
--------------------------------------------------------------------------------
1 | # Simple proxy that sends all method calls to a real database connection
2 | module DbCharmer
3 | class ConnectionProxy < ActiveSupport::BasicObject
4 | # We need to do this because in Rails 2.3 BasicObject does not remove object_id method, which is stupid
5 | undef_method(:object_id) if instance_methods.member?('object_id')
6 |
7 | # We use this to get a connection class from the proxy
8 | attr_accessor :abstract_connection_class
9 |
10 | def initialize(abstract_class, db_name)
11 | @abstract_connection_class = abstract_class
12 | @db_name = db_name
13 | end
14 |
15 | def db_charmer_connection_name
16 | @db_name
17 | end
18 |
19 | def db_charmer_connection_proxy
20 | self
21 | end
22 |
23 | def db_charmer_retrieve_connection
24 | @abstract_connection_class.retrieve_connection
25 | end
26 |
27 | def nil?
28 | false
29 | end
30 |
31 | #-----------------------------------------------------------------------------------------------
32 | RESPOND_TO_METHODS = [
33 | :abstract_connection_class,
34 | :db_charmer_connection_name,
35 | :db_charmer_connection_proxy,
36 | :db_charmer_retrieve_connection,
37 | :nil?
38 | ].freeze
39 |
40 | # Short-circuit some of the methods for which we know there is a separate check in coercion code
41 | DOESNT_RESPOND_TO_METHODS = [
42 | :set_real_connection
43 | ].freeze
44 |
45 | def respond_to?(method_name, include_all = false)
46 | return true if RESPOND_TO_METHODS.include?(method_name)
47 | return false if DOESNT_RESPOND_TO_METHODS.include?(method_name)
48 | db_charmer_retrieve_connection.respond_to?(method_name, include_all)
49 | end
50 |
51 | #-----------------------------------------------------------------------------------------------
52 | def method_missing(meth, *args, &block)
53 | db_charmer_retrieve_connection.send(meth, *args, &block)
54 | end
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/test-project-2.x/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file is copied to ~/spec when you run 'ruby script/generate rspec'
2 | # from the project root directory.
3 | ENV["RAILS_ENV"] = 'test'
4 | require File.expand_path(File.join(File.dirname(__FILE__),'..','config','environment'))
5 | require 'spec/autorun'
6 | require 'spec/rails'
7 |
8 | # Requires supporting files with custom matchers and macros, etc,
9 | # in ./support/ and its subdirectories.
10 | Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
11 |
12 | Spec::Runner.configure do |config|
13 | # If you're not using ActiveRecord you should remove these
14 | # lines, delete config/database.yml and disable :active_record
15 | # in your config/boot.rb
16 | config.use_transactional_fixtures = false
17 | config.use_instantiated_fixtures = false
18 | config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
19 |
20 | # == Fixtures
21 | #
22 | # You can declare fixtures for each example_group like this:
23 | # describe "...." do
24 | # fixtures :table_a, :table_b
25 | #
26 | # Alternatively, if you prefer to declare them only once, you can
27 | # do so right here. Just uncomment the next line and replace the fixture
28 | # names with your fixtures.
29 | #
30 | # config.global_fixtures = :table_a, :table_b
31 | #
32 | # If you declare global fixtures, be aware that they will be declared
33 | # for all of your examples, even those that don't use them.
34 | #
35 | # You can also declare which fixtures to use (for example fixtures for test/fixtures):
36 | #
37 | # config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
38 | #
39 | # == Mock Framework
40 | #
41 | # RSpec uses its own mocking framework by default. If you prefer to
42 | # use mocha, flexmock or RR, uncomment the appropriate line:
43 | #
44 | # config.mock_with :mocha
45 | # config.mock_with :flexmock
46 | # config.mock_with :rr
47 | #
48 | # == Notes
49 | #
50 | # For more information take a look at Spec::Runner::Configuration and Spec::Runner
51 | end
52 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/association_preload_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | if DbCharmer.rails2?
4 | describe "ActiveRecord preload_associations method" do
5 | it "should be public" do
6 | ActiveRecord::Base.public_methods.collect(&:to_s).member?('preload_associations').should be_true
7 | end
8 | end
9 | end
10 |
11 | describe "ActiveRecord in finder methods" do
12 | fixtures :categories, :users, :posts, :categories_posts, :avatars
13 |
14 | before do
15 | Post.db_magic :connection => nil
16 | User.db_magic :connection => nil
17 | end
18 |
19 | after do
20 | Post.db_magic(Post::DB_MAGIC_DEFAULT_PARAMS)
21 | end
22 |
23 | it "should switch all belongs_to association connections when :include is used" do
24 | User.connection.should_not_receive(:select_all)
25 | Post.on_db(:slave01).all(:include => :user)
26 | end
27 |
28 | it "should switch all has_many association connections when :include is used" do
29 | Post.connection.should_not_receive(:select_all)
30 | User.on_db(:slave01).all(:include => :posts)
31 | end
32 |
33 | it "should switch all has_one association connections when :include is used" do
34 | Avatar.connection.should_not_receive(:select_all)
35 | User.on_db(:slave01).all(:include => :avatar)
36 | end
37 |
38 | it "should switch all has_and_belongs_to_many association connections when :include is used" do
39 | Post.connection.should_not_receive(:select_all)
40 | Category.on_db(:slave01).all(:include => :posts)
41 | end
42 |
43 | #-------------------------------------------------------------------------------------------
44 | it "should not switch assocations when called on a top-level connection" do
45 | User.connection.should_receive(:select_all).and_return([])
46 | Post.all(:include => :user)
47 | end
48 |
49 | it "should not switch connection when association model and main model are on different servers" do
50 | LogRecord.connection.should_receive(:select_all).and_return([])
51 | User.on_db(:slave01).all(:include => :log_records)
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/test-project/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # If you have a Gemfile, require the gems listed there, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(:default, Rails.env) if defined?(Bundler)
8 |
9 | module DbCharmerSandbox
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 |
15 | # Custom directories with classes and modules you want to be autoloadable.
16 | # config.autoload_paths += %W(#{config.root}/extras)
17 |
18 | # Only load the plugins named here, in the order given (default is alphabetical).
19 | # :all can be used as a placeholder for all plugins not explicitly named.
20 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
21 |
22 | # Activate observers that should always be running.
23 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
24 |
25 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
26 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
27 | # config.time_zone = 'Central Time (US & Canada)'
28 |
29 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
30 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
31 | # config.i18n.default_locale = :de
32 |
33 | # JavaScript files you want as :defaults (application.js is always included).
34 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
35 |
36 | # Configure the default encoding used in templates for Ruby 1.9.
37 | config.encoding = "utf-8"
38 |
39 | # Configure sensitive parameters which will be filtered from the log file.
40 | config.filter_parameters += [:password]
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/test-project/config/routes.rb:
--------------------------------------------------------------------------------
1 | DbCharmerSandbox::Application.routes.draw do
2 | # The priority is based upon order of creation:
3 | # first created -> highest priority.
4 |
5 | # Resource routes
6 | resources :posts
7 | resources :cars
8 |
9 | # Sample of regular route:
10 | # match 'products/:id' => 'catalog#view'
11 | # Keep in mind you can assign values other than :controller and :action
12 |
13 | # Sample of named route:
14 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
15 | # This route can be invoked with purchase_url(:id => product.id)
16 |
17 | # Sample resource route (maps HTTP verbs to controller actions automatically):
18 | # resources :products
19 |
20 | # Sample resource route with options:
21 | # resources :products do
22 | # member do
23 | # get 'short'
24 | # post 'toggle'
25 | # end
26 | #
27 | # collection do
28 | # get 'sold'
29 | # end
30 | # end
31 |
32 | # Sample resource route with sub-resources:
33 | # resources :products do
34 | # resources :comments, :sales
35 | # resource :seller
36 | # end
37 |
38 | # Sample resource route with more complex sub-resources
39 | # resources :products do
40 | # resources :comments
41 | # resources :sales do
42 | # get 'recent', :on => :collection
43 | # end
44 | # end
45 |
46 | # Sample resource route within a namespace:
47 | # namespace :admin do
48 | # # Directs /admin/products/* to Admin::ProductsController
49 | # # (app/controllers/admin/products_controller.rb)
50 | # resources :products
51 | # end
52 |
53 | # You can have the root of your site routed with "root"
54 | # just remember to delete public/index.html.
55 | # root :to => "welcome#index"
56 |
57 | # See how all your routes lay out with "rake routes"
58 |
59 | # This is a legacy wild controller route that's not recommended for RESTful applications.
60 | # Note: This route will make all actions in every controller accessible via GET requests.
61 | match ':controller(/:action(/:id(.:format)))'
62 | end
63 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/named_scope/named_scope_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "Named scopes" do
4 | fixtures :users, :posts
5 |
6 | before(:all) do
7 | Post.switch_connection_to(nil)
8 | User.switch_connection_to(nil)
9 | end
10 |
11 | describe "prefixed by on_db" do
12 | it "should work on the proxy" do
13 | Post.on_db(:slave01).windows_posts.should == Post.windows_posts
14 | end
15 |
16 | it "should actually run queries on the specified db" do
17 | Post.on_db(:slave01).connection.should_receive(:select_all).once.and_return([])
18 | Post.on_db(:slave01).windows_posts.all
19 | # Post.windows_posts.all
20 | end
21 |
22 | it "should work with long scope chains" do
23 | Post.on_db(:slave01).connection.should_not_receive(:select_all)
24 | Post.on_db(:slave01).connection.should_receive(:select_value).and_return(5)
25 | Post.on_db(:slave01).windows_posts.count.should == 5
26 | end
27 |
28 | it "should work with associations" do
29 | users(:bill).posts.on_db(:slave01).windows_posts.all.should == users(:bill).posts.windows_posts
30 | end
31 | end
32 |
33 | describe "postfixed by on_db" do
34 | it "should work on the proxy" do
35 | Post.windows_posts.on_db(:slave01).should == Post.windows_posts
36 | end
37 |
38 | it "should actually run queries on the specified db" do
39 | Post.on_db(:slave01).connection.object_id.should_not == Post.connection.object_id
40 | Post.on_db(:slave01).connection.should_receive(:select_all).and_return([])
41 | Post.windows_posts.on_db(:slave01).all
42 | Post.windows_posts.all
43 | end
44 |
45 | it "should work with long scope chains" do
46 | Post.on_db(:slave01).connection.should_not_receive(:select_all)
47 | Post.on_db(:slave01).connection.should_receive(:select_value).and_return(5)
48 | Post.windows_posts.on_db(:slave01).count.should == 5
49 | end
50 |
51 | it "should work with associations" do
52 | users(:bill).posts.windows_posts.on_db(:slave01).all.should == users(:bill).posts.windows_posts
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/test-project/spec/integration/multi_threading_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "DbCharmer integration tests" do
4 | def do_test(test_seconds, thread_count)
5 | start_time = Time.now.to_f
6 | threads = Array.new
7 |
8 | while threads.size < thread_count
9 | threads << Thread.new do
10 | while Time.now.to_f - start_time < test_seconds do
11 | User.create!(:login => "user#{rand}", :password => rand)
12 | User.uncached { User.on_db(:slave01).first }
13 | end
14 | end
15 | end
16 |
17 | # Wait for threads to finish
18 | threads.each(&:join)
19 | end
20 |
21 | it "should work in single-threaded mode" do
22 | do_test(10, 1)
23 | end
24 |
25 | it "should work with 5 threads" do
26 | do_test(10, 5)
27 | end
28 |
29 | it "should use default connection passed in db_magic call in all threads" do
30 | # Define a class with db magic in it
31 | class TestLogRecordWithThreads < ActiveRecord::Base
32 | self.table_name = :log_records
33 | db_magic :connection => :logs
34 | end
35 |
36 | # Check conection in the same thread
37 | TestLogRecordWithThreads.connection.db_charmer_connection_name.should == "logs"
38 |
39 | # Check connection in a different thread
40 | Thread.new {
41 | TestLogRecordWithThreads.connection.db_charmer_connection_name.should == "logs"
42 | }.join
43 | end
44 |
45 | it "should use default connection passed in db_magic call when master connection is being remapped" do
46 | class TestLogRecordWithThreadsAndRemapping < ActiveRecord::Base
47 | self.table_name = :log_records
48 | db_magic :connection => :logs
49 | end
50 |
51 | # Test in main thread
52 | expect {
53 | DbCharmer.with_remapped_databases(:master => :slave01) do
54 | TestLogRecordWithThreadsAndRemapping.first
55 | end
56 | }.to_not raise_error
57 |
58 | # Test in another thread
59 | Thread.new {
60 | expect {
61 | DbCharmer.with_remapped_databases(:master => :slave01) do
62 | TestLogRecordWithThreadsAndRemapping.first
63 | end
64 | }.to_not raise_error
65 | }.join
66 | end
67 | end unless ENV['SKIP_MT_TESTS']
68 |
--------------------------------------------------------------------------------
/lib/db_charmer/force_slave_reads.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | def self.current_controller
3 | Thread.current[:db_charmer_current_controller]
4 | end
5 |
6 | def self.current_controller=(val)
7 | Thread.current[:db_charmer_current_controller] = val
8 | end
9 |
10 | #-------------------------------------------------------------------------------------------------
11 | def self.forced_slave_reads_setting
12 | Thread.current[:db_charmer_forced_slave_reads]
13 | end
14 |
15 | def self.forced_slave_reads_setting=(val)
16 | Thread.current[:db_charmer_forced_slave_reads] = val
17 | end
18 |
19 | #-------------------------------------------------------------------------------------------------
20 | def self.force_slave_reads?
21 | # If global force slave reads is requested, do it
22 | return true if Thread.current[:db_charmer_forced_slave_reads]
23 |
24 | # If not, try to use current controller to decide on this
25 | return false unless current_controller.respond_to?(:force_slave_reads?)
26 |
27 | slave_reads = current_controller.force_slave_reads?
28 | logger.debug("Using controller to figure out if slave reads should be forced: #{slave_reads}")
29 | return slave_reads
30 | end
31 |
32 | #-------------------------------------------------------------------------------------------------
33 | def self.with_controller(controller)
34 | raise ArgumentError, "No block given" unless block_given?
35 | logger.debug("Setting current controller for db_charmer: #{controller.class.name}")
36 | self.current_controller = controller
37 | yield
38 | ensure
39 | logger.debug('Clearing current controller for db_charmer')
40 | self.current_controller = nil
41 | end
42 |
43 | #-------------------------------------------------------------------------------------------------
44 | # Force all reads in a block of code to go to a slave
45 | def self.force_slave_reads
46 | raise ArgumentError, "No block given" unless block_given?
47 | old_forced_slave_reads = self.forced_slave_reads_setting
48 | begin
49 | self.forced_slave_reads_setting = true
50 | yield
51 | ensure
52 | self.forced_slave_reads_setting = old_forced_slave_reads
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/db_charmer/action_controller/force_slave_reads.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActionController
3 | module ForceSlaveReads
4 |
5 | module ClassMethods
6 | @@db_charmer_force_slave_reads_actions = {}
7 | def force_slave_reads(params = {})
8 | @@db_charmer_force_slave_reads_actions[self.name] = {
9 | :except => params[:except] ? [*params[:except]].map(&:to_s) : [],
10 | :only => params[:only] ? [*params[:only]].map(&:to_s) : []
11 | }
12 | end
13 |
14 | def force_slave_reads_options
15 | @@db_charmer_force_slave_reads_actions[self.name]
16 | end
17 |
18 | def force_slave_reads_action?(name = nil)
19 | name = name.to_s
20 |
21 | options = force_slave_reads_options
22 | # If no options were defined for this controller, all actions are not forced to use slaves
23 | return false unless options
24 |
25 | # Actions where force_slave_reads mode was turned off
26 | return false if options[:except].include?(name)
27 |
28 | # Only for these actions force_slave_reads was turned on
29 | return options[:only].include?(name) if options[:only].any?
30 |
31 | # If :except is not empty, we're done with the checks and rest of the actions are should force slave reads
32 | # Otherwise, all the actions are not in force_slave_reads mode
33 | options[:except].any?
34 | end
35 | end
36 |
37 | module InstanceMethods
38 | DISPATCH_METHOD = (DbCharmer.rails3?) ? :process_action : :perform_action
39 |
40 | def self.included(base)
41 | base.alias_method_chain DISPATCH_METHOD, :forced_slave_reads
42 | end
43 |
44 | def force_slave_reads!
45 | @db_charmer_force_slave_reads = true
46 | end
47 |
48 | def dont_force_slave_reads!
49 | @db_charmer_force_slave_reads = false
50 | end
51 |
52 | def force_slave_reads?
53 | @db_charmer_force_slave_reads || self.class.force_slave_reads_action?(params[:action])
54 | end
55 |
56 | protected
57 |
58 | class_eval <<-EOF, __FILE__, __LINE__+1
59 | def #{DISPATCH_METHOD}_with_forced_slave_reads(*args, &block)
60 | DbCharmer.with_controller(self) do
61 | #{DISPATCH_METHOD}_without_forced_slave_reads(*args, &block)
62 | end
63 | end
64 | EOF
65 | end
66 |
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/db_charmer/sharding/stub_connection.rb:
--------------------------------------------------------------------------------
1 | # This is a simple proxy class used as a default connection on sharded models
2 | #
3 | # The idea is to proxy all utility method calls to a real connection (set by
4 | # the +set_real_connection+ method when we switch shards) and fail on real
5 | # database querying calls forcing users to switch shard connections.
6 | #
7 | module DbCharmer
8 | module Sharding
9 | class StubConnection
10 | attr_accessor :sharded_connection
11 |
12 | def initialize(sharded_connection)
13 | @sharded_connection = sharded_connection
14 | @real_conn = nil
15 | end
16 |
17 | def set_real_connection(real_conn)
18 | @real_conn = real_conn
19 | end
20 |
21 | def db_charmer_connection_name
22 | "StubConnection"
23 | end
24 |
25 | def real_connection
26 | # Return memoized real connection
27 | return @real_conn if @real_conn
28 |
29 | # If sharded connection supports shards enumeration, get the first shard
30 | conn = sharded_connection.shard_connections.try(:first)
31 |
32 | # If we do not have real connection yet, try to use the default one (if it is supported by the sharder)
33 | conn ||= sharded_connection.sharder.shard_for_key(:default) if sharded_connection.support_default_shard?
34 |
35 | # Get connection proxy for our real connection
36 | return nil unless conn
37 | @real_conn = ::ActiveRecord::Base.coerce_to_connection_proxy(conn, DbCharmer.connections_should_exist?)
38 | end
39 |
40 | def respond_to?(method_name, include_all = false)
41 | return true if super
42 | return false if real_connection.object_id == self.object_id
43 | real_connection.respond_to?(method_name, include_all)
44 | end
45 |
46 | def method_missing(meth, *args, &block)
47 | # Fail on database statements
48 | if ::ActiveRecord::ConnectionAdapters::DatabaseStatements.instance_methods.member?(meth.to_s)
49 | raise ::ActiveRecord::ConnectionNotEstablished, "You have to switch connection on your model before using it!"
50 | end
51 |
52 | # Fail if no connection has been established yet
53 | unless real_connection
54 | raise ::ActiveRecord::ConnectionNotEstablished, "No real connection to proxy this method to!"
55 | end
56 |
57 | if real_connection.kind_of?(DbCharmer::Sharding::StubConnection)
58 | raise ::ActiveRecord::ConnectionNotEstablished, "You have to switch connection on your model before using it!"
59 | end
60 |
61 | # Proxy the call to our real connection target
62 | real_connection.__send__(meth, *args, &block)
63 | end
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/relation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | if DbCharmer.rails3?
4 | describe "ActiveRecord::Relation for a model with db_magic" do
5 | before do
6 | class RelTestModel < ActiveRecord::Base
7 | db_magic :connection => nil
8 | self.table_name = :users
9 | end
10 | end
11 |
12 | it "should be created with correct default connection" do
13 | rel = RelTestModel.on_db(:user_master).where("1=1")
14 | rel.db_charmer_connection.object_id.should == RelTestModel.on_db(:user_master).connection.object_id
15 | end
16 |
17 | it "should switch the default connection when on_db called" do
18 | rel = RelTestModel.where("1=1")
19 | rel_master = rel.on_db(:user_master)
20 | rel_master.db_charmer_connection.object_id.should_not == rel.db_charmer_connection.object_id
21 | end
22 |
23 | it "should keep default connection value when relation is cloned in chained calls" do
24 | rel = RelTestModel.on_db(:user_master).where("1=1")
25 | rel.where("2=2").db_charmer_connection.object_id.should == rel.db_charmer_connection.object_id
26 | end
27 |
28 | it "should execute select queries on the default connection" do
29 | rel = RelTestModel.on_db(:user_master).where("1=1")
30 |
31 | RelTestModel.on_db(:user_master).connection.should_receive(:select_all).and_return([])
32 | RelTestModel.connection.should_not_receive(:select_all)
33 |
34 | rel.first
35 | end
36 |
37 | it "should execute delete queries on the default connection" do
38 | rel = RelTestModel.on_db(:user_master).where("1=1")
39 |
40 | RelTestModel.on_db(:user_master).connection.should_receive(:delete)
41 | RelTestModel.connection.should_not_receive(:delete)
42 |
43 | rel.delete_all
44 | end
45 |
46 | it "should execute update_all queries on the default connection" do
47 | rel = RelTestModel.on_db(:user_master).where("1=1")
48 |
49 | RelTestModel.on_db(:user_master).connection.should_receive(:update)
50 | RelTestModel.connection.should_not_receive(:update)
51 |
52 | rel.update_all("login = login + 'new'")
53 | end
54 |
55 | it "should execute update queries on the default connection" do
56 | rel = RelTestModel.on_db(:user_master).where("1=1")
57 | user = RelTestModel.create!(:login => 'login')
58 |
59 | RelTestModel.on_db(:user_master).connection.should_receive(:update)
60 | RelTestModel.connection.should_not_receive(:update)
61 |
62 | rel.update(user.id, :login => "foobar")
63 | end
64 |
65 | it "should return correct connection" do
66 | rel = RelTestModel.on_db(:user_master).where("1=1")
67 | rel.connection.object_id.should == rel.db_charmer_connection.object_id
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/test-project/spec/models/event_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Event, "sharded model" do
4 | fixtures :event_shards_info, :event_shards_map
5 |
6 | it "should respond to shard_for method" do
7 | Event.should respond_to(:shard_for)
8 | end
9 |
10 | it "should correctly switch shards" do
11 | # Cleanup sharded tables
12 | Event.on_each_shard { |event| event.delete_all }
13 |
14 | # Check that they are empty
15 | Event.shard_for(2).all.should be_empty
16 | Event.shard_for(12).all.should be_empty
17 |
18 | # Create some data (one record in each shard)
19 | Event.shard_for(2).create!(
20 | :from_uid => 1,
21 | :to_uid => 2,
22 | :original_created_at => Time.now,
23 | :event_type => 1,
24 | :event_data => 'foo'
25 | )
26 | Event.shard_for(12).create!(
27 | :from_uid => 1,
28 | :to_uid => 12,
29 | :original_created_at => Time.now,
30 | :event_type => 1,
31 | :event_data => 'bar'
32 | )
33 |
34 | # Check sharded tables to make sure they have the data
35 | Event.shard_for(2).find_all_by_from_uid(1).map(&:event_data).should == [ 'foo' ]
36 | Event.shard_for(12).find_all_by_from_uid(1).map(&:event_data).should == [ 'bar' ]
37 | end
38 |
39 | it "should allocate new blocks when needed" do
40 | # Cleanup sharded tables
41 | Event.on_each_shard { |event| event.delete_all }
42 |
43 | # Check new block, it should be empty
44 | Event.shard_for(100).count.should be_zero
45 |
46 | # Create an object
47 | Event.shard_for(100).create!(
48 | :from_uid => 1,
49 | :to_uid => 100,
50 | :original_created_at => Time.now,
51 | :event_type => 1,
52 | :event_data => 'blah'
53 | )
54 |
55 | # Check the new block
56 | Event.shard_for(100).count.should == 1
57 | end
58 |
59 | it "should fail to perform any database operations w/o a shard specification" do
60 | Event.stub(:column_defaults).and_return({})
61 | Event.stub(:columns_hash).and_return({})
62 |
63 | lambda { Event.first }.should raise_error(ActiveRecord::ConnectionNotEstablished)
64 | lambda { Event.create }.should raise_error(ActiveRecord::ConnectionNotEstablished)
65 | lambda { Event.delete_all }.should raise_error(ActiveRecord::ConnectionNotEstablished)
66 | end
67 |
68 | it "should not fail when AR does some internal calls to the database" do
69 | # Cleanup sharded tables
70 | Event.on_each_shard { |event| event.delete_all }
71 |
72 | # Create an object
73 | x = Event.shard_for(100).create!(
74 | :from_uid => 1,
75 | :to_uid => 100,
76 | :original_created_at => Time.now,
77 | :event_type => 1,
78 | :event_data => 'blah'
79 | )
80 |
81 | Event.reset_column_information
82 | lambda { x.inspect }.should_not raise_error
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/lib/db_charmer/active_record/multi_db_proxy.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module MultiDbProxy
4 | # Simple proxy class that switches connections and then proxies all the calls
5 | # This class is used to implement chained on_db calls
6 | class OnDbProxy < ActiveSupport::BasicObject
7 | # We need to do this because in Rails 2.3 BasicObject does not remove object_id method, which is stupid
8 | undef_method(:object_id) if instance_methods.member?('object_id')
9 |
10 | def initialize(proxy_target, slave)
11 | @proxy_target = proxy_target
12 | @slave = slave
13 | end
14 |
15 | private
16 |
17 | def method_missing(meth, *args, &block)
18 | # Switch connection and proxy the method call
19 | @proxy_target.on_db(@slave) do |proxy_target|
20 | res = proxy_target.__send__(meth, *args, &block)
21 |
22 | # If result is a scope/association, return a new proxy for it, otherwise return the result itself
23 | (res.proxy?) ? OnDbProxy.new(res, @slave) : res
24 | end
25 | end
26 | end
27 |
28 | module ClassMethods
29 | def on_db(con, proxy_target = nil)
30 | proxy_target ||= self
31 |
32 | # Chain call
33 | return OnDbProxy.new(proxy_target, con) unless block_given?
34 |
35 | # Block call
36 | begin
37 | self.db_charmer_connection_level += 1
38 | old_proxy = db_charmer_connection_proxy
39 | switch_connection_to(con, DbCharmer.connections_should_exist?)
40 | yield(proxy_target)
41 | ensure
42 | switch_connection_to(old_proxy)
43 | self.db_charmer_connection_level -= 1
44 | end
45 | end
46 | end
47 |
48 | module InstanceMethods
49 | def on_db(con, proxy_target = nil, &block)
50 | proxy_target ||= self
51 | self.class.on_db(con, proxy_target, &block)
52 | end
53 | end
54 |
55 | module MasterSlaveClassMethods
56 | def on_slave(con = nil, proxy_target = nil, &block)
57 | con ||= db_charmer_random_slave
58 | raise ArgumentError, "No slaves found in the class and no slave connection given" unless con
59 | on_db(con, proxy_target, &block)
60 | end
61 |
62 | def on_master(proxy_target = nil, &block)
63 | on_db(db_charmer_default_connection, proxy_target, &block)
64 | end
65 |
66 | def first_level_on_slave
67 | first_level = db_charmer_top_level_connection? && on_master.connection.open_transactions.zero?
68 | if first_level && db_charmer_force_slave_reads? && db_charmer_slaves.any?
69 | on_slave { yield }
70 | else
71 | yield
72 | end
73 | end
74 | end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/db_charmer/tasks/databases.rake:
--------------------------------------------------------------------------------
1 | namespace :db_charmer do
2 | namespace :create do
3 | desc 'Create all the local databases defined in config/database.yml'
4 | task :all => "db:load_config" do
5 | ::ActiveRecord::Base.configurations.each_value do |config|
6 | # Skip entries that don't have a database key, such as the first entry here:
7 | #
8 | # defaults: &defaults
9 | # adapter: mysql
10 | # username: root
11 | # password:
12 | # host: localhost
13 | #
14 | # development:
15 | # database: blog_development
16 | # <<: *defaults
17 | next unless config['database']
18 | # Only connect to local databases
19 | local_database?(config) { create_core_and_sub_database(config) }
20 | end
21 | end
22 | end
23 |
24 | desc 'Create the databases defined in config/database.yml for the current RAILS_ENV'
25 | task :create => "db:load_config" do
26 | create_core_and_sub_database(ActiveRecord::Base.configurations[RAILS_ENV])
27 | end
28 |
29 | def create_core_and_sub_database(config)
30 | create_database(config)
31 | config.each_value do | sub_config |
32 | next unless sub_config.is_a?(Hash)
33 | next unless sub_config['database']
34 | create_database(sub_config)
35 | end
36 | end
37 |
38 | namespace :drop do
39 | desc 'Drops all the local databases defined in config/database.yml'
40 | task :all => "db:load_config" do
41 | ::ActiveRecord::Base.configurations.each_value do |config|
42 | # Skip entries that don't have a database key
43 | next unless config['database']
44 | # Only connect to local databases
45 | local_database?(config) { drop_core_and_sub_database(config) }
46 | end
47 | end
48 | end
49 |
50 | desc 'Drops the database for the current RAILS_ENV'
51 | task :drop => "db:load_config" do
52 | config = ::ActiveRecord::Base.configurations[RAILS_ENV || 'development']
53 | begin
54 | drop_core_and_sub_database(config)
55 | rescue Exception => e
56 | puts "Couldn't drop #{config['database']} : #{e.inspect}"
57 | end
58 | end
59 |
60 |
61 | def local_database?(config, &block)
62 | if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank?
63 | yield
64 | else
65 | puts "This task only modifies local databases. #{config['database']} is on a remote host."
66 | end
67 | end
68 | end
69 |
70 | def drop_core_and_sub_database(config)
71 | begin
72 | drop_database(config)
73 | rescue
74 | $stderr.puts "#{config['database']} not exists"
75 | end
76 | config.each_value do | sub_config |
77 | next unless sub_config.is_a?(Hash)
78 | next unless sub_config['database']
79 | begin
80 | drop_database(sub_config)
81 | rescue
82 | $stderr.puts "#{config['database']} not exists"
83 | end
84 | end
85 | end
86 |
87 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/db_magic_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | class Blah < ActiveRecord::Base; end
4 |
5 | describe "In ActiveRecord models" do
6 | describe "db_magic method" do
7 | context "with :connection parameter" do
8 | after do
9 | DbCharmer.connections_should_exist = false
10 | end
11 |
12 | it "should change model's connection to specified one" do
13 | Blah.db_magic :connection => :logs
14 | Blah.connection.object_id.should == DbCharmer::ConnectionFactory.connect(:logs).object_id
15 | end
16 |
17 | it "should pass :should_exist paramater value to the underlying connection logic" do
18 | DbCharmer::ConnectionFactory.should_receive(:connect).with(:logs, 'blah')
19 | Blah.db_magic :connection => :logs, :should_exist => 'blah'
20 | DbCharmer.connections_should_exist = true
21 | DbCharmer::ConnectionFactory.should_receive(:connect).with(:logs, false)
22 | Blah.db_magic :connection => :logs, :should_exist => false
23 | end
24 |
25 | it "should use global DbCharmer's connections_should_exist attribute if no :should_exist passed" do
26 | DbCharmer.connections_should_exist = true
27 | DbCharmer::ConnectionFactory.should_receive(:connect).with(:logs, true)
28 | Blah.db_magic :connection => :logs
29 | end
30 | end
31 |
32 | context "with :slave or :slaves parameter" do
33 | it "should merge :slave and :slaves values" do
34 | Blah.db_charmer_slaves = []
35 | Blah.db_charmer_slaves.should be_empty
36 |
37 | Blah.db_magic :slave => :slave01
38 | Blah.db_charmer_slaves.size.should == 1
39 |
40 | Blah.db_magic :slaves => [ :slave01 ]
41 | Blah.db_charmer_slaves.size.should == 1
42 |
43 | Blah.db_magic :slaves => [ :slave01 ], :slave => :logs
44 | Blah.db_charmer_slaves.size.should == 2
45 | end
46 |
47 | it "should make db_charmer_force_slave_reads = true by default" do
48 | Blah.db_magic :slave => :slave01
49 | Blah.db_charmer_force_slave_reads.should be_true
50 | end
51 |
52 | it "should pass force_slave_reads value to db_charmer_force_slave_reads" do
53 | Blah.db_magic :slave => :slave01, :force_slave_reads => false
54 | Blah.db_charmer_force_slave_reads.should be_false
55 |
56 | Blah.db_magic :slave => :slave01, :force_slave_reads => true
57 | Blah.db_charmer_force_slave_reads.should be_true
58 | end
59 | end
60 |
61 | it "should set up a hook to propagate db_magic params to all the children models" do
62 | class ParentFoo < ActiveRecord::Base
63 | db_magic :foo => :bar
64 | end
65 | class ChildFoo < ParentFoo; end
66 |
67 | ChildFoo.db_charmer_opts.should == ParentFoo.db_charmer_opts
68 | end
69 |
70 | context "with :sharded parameter" do
71 | class ShardTestingFoo < ActiveRecord::Base
72 | db_magic :sharded => { :key => :id, :sharded_connection => :texts }
73 | end
74 |
75 | it "should add shard_for method to the model" do
76 | ShardTestingFoo.should respond_to(:shard_for)
77 | end
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/test-project/spec/unit/db_charmer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DbCharmer do
4 | after do
5 | DbCharmer.current_controller = nil
6 | DbCharmer.connections_should_exist = false
7 | end
8 |
9 | it "should define version constants" do
10 | DbCharmer::Version::STRING.should match(/^\d+\.\d+\.\d+/)
11 | end
12 |
13 | it "should have connections_should_exist accessors" do
14 | DbCharmer.connections_should_exist.should_not be_nil
15 | DbCharmer.connections_should_exist = :foo
16 | DbCharmer.connections_should_exist.should == :foo
17 | end
18 |
19 | it "should have connections_should_exist? method" do
20 | DbCharmer.connections_should_exist = true
21 | DbCharmer.connections_should_exist?.should be_true
22 | DbCharmer.connections_should_exist = false
23 | DbCharmer.connections_should_exist?.should be_false
24 | DbCharmer.connections_should_exist = "shit"
25 | DbCharmer.connections_should_exist?.should be_true
26 | DbCharmer.connections_should_exist = nil
27 | DbCharmer.connections_should_exist?.should be_false
28 | end
29 |
30 | it "should have current_controller accessors" do
31 | DbCharmer.respond_to?(:current_controller).should be_true
32 | DbCharmer.current_controller = :foo
33 | DbCharmer.current_controller.should == :foo
34 | DbCharmer.current_controller = nil
35 | end
36 |
37 | context "in force_slave_reads? method" do
38 | it "should return true if force_slave_reads=true" do
39 | DbCharmer.force_slave_reads?.should be_false
40 |
41 | DbCharmer.force_slave_reads do
42 | DbCharmer.force_slave_reads?.should be_true
43 | end
44 |
45 | DbCharmer.force_slave_reads?.should be_false
46 | end
47 |
48 | it "should return false if no controller defined and global force_slave_reads=false" do
49 | DbCharmer.current_controller = nil
50 | DbCharmer.force_slave_reads?.should be_false
51 | end
52 |
53 | it "should consult with the controller about forcing slave reads if possible" do
54 | DbCharmer.current_controller = mock("controller")
55 |
56 | DbCharmer.current_controller.should_receive(:force_slave_reads?).and_return(true)
57 | DbCharmer.force_slave_reads?.should be_true
58 |
59 | DbCharmer.current_controller.should_receive(:force_slave_reads?).and_return(false)
60 | DbCharmer.force_slave_reads?.should be_false
61 | end
62 | end
63 |
64 | context "in with_controller method" do
65 | it "should fail if no block given" do
66 | lambda { DbCharmer.with_controller(:foo) }.should raise_error(ArgumentError)
67 | end
68 |
69 | it "should switch controller while running the block" do
70 | DbCharmer.current_controller = nil
71 | DbCharmer.current_controller.should be_nil
72 |
73 | DbCharmer.with_controller(:foo) do
74 | DbCharmer.current_controller.should == :foo
75 | end
76 |
77 | DbCharmer.current_controller.should be_nil
78 | end
79 |
80 | it "should ensure current controller is reverted to nil in case of errors" do
81 | lambda {
82 | DbCharmer.with_controller(:foo) { raise "fuck" }
83 | }.should raise_error
84 | DbCharmer.current_controller.should be_nil
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/association_proxy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "DbCharmer::AssociationProxy extending AR::Associations" do
4 | fixtures :users, :posts
5 |
6 | it "should add proxy? => true method" do
7 | users(:bill).posts.proxy?.should be_true
8 | end
9 |
10 | describe "in has_many associations" do
11 | before do
12 | @user = users(:bill)
13 | @posts = @user.posts.all
14 | Post.switch_connection_to(:logs)
15 | User.switch_connection_to(:logs)
16 | end
17 |
18 | after do
19 | Post.switch_connection_to(nil)
20 | User.switch_connection_to(nil)
21 | end
22 |
23 | it "should implement on_db proxy" do
24 | Post.connection.should_not_receive(:select_all)
25 | User.connection.should_not_receive(:select_all)
26 |
27 | stub_columns_for_rails31 Post.on_db(:logs).connection
28 | Post.on_db(:slave01).connection.should_receive(:select_all).and_return(@posts.map { |p| p.attributes })
29 | assert_equal @posts, @user.posts.on_db(:slave01)
30 | end
31 |
32 | it "on_db should work in prefix mode" do
33 | Post.connection.should_not_receive(:select_all)
34 | User.connection.should_not_receive(:select_all)
35 |
36 | stub_columns_for_rails31 Post.on_db(:logs).connection
37 | Post.on_db(:slave01).connection.should_receive(:select_all).and_return(@posts.map { |p| p.attributes })
38 | @user.on_db(:slave01).posts.should == @posts
39 | end
40 |
41 | it "should actually proxy calls to the rails association proxy" do
42 | Post.switch_connection_to(nil)
43 | @user.posts.on_db(:slave01).count.should == @user.posts.count
44 | end
45 |
46 | it "should work with named scopes" do
47 | Post.switch_connection_to(nil)
48 | @user.posts.windows_posts.on_db(:slave01).count.should == @user.posts.windows_posts.count
49 | end
50 |
51 | it "should work with chained named scopes" do
52 | Post.switch_connection_to(nil)
53 | @user.posts.windows_posts.dummy_scope.on_db(:slave01).count.should == @user.posts.windows_posts.dummy_scope.count
54 | end
55 | end
56 |
57 | describe "in belongs_to associations" do
58 | before do
59 | @post = posts(:windoze)
60 | @user = users(:bill)
61 | User.switch_connection_to(:logs)
62 | User.connection.object_id.should_not == Post.connection.object_id
63 | end
64 |
65 | after do
66 | User.switch_connection_to(nil)
67 | end
68 |
69 | it "should implement on_db proxy" do
70 | pending
71 | Post.connection.should_not_receive(:select_all)
72 | User.connection.should_not_receive(:select_all)
73 | User.on_db(:slave01).connection.should_receive(:select_all).once.and_return([ @user ])
74 | @post.user.on_db(:slave01).should == @post.user
75 | end
76 |
77 | it "on_db should work in prefix mode" do
78 | pending
79 | Post.connection.should_not_receive(:select_all)
80 | User.connection.should_not_receive(:select_all)
81 | User.on_db(:slave01).connection.should_receive(:select_all).once.and_return([ @user ])
82 | @post.on_db(:slave01).user.should == @post.user
83 | end
84 |
85 | it "should actually proxy calls to the rails association proxy" do
86 | User.switch_connection_to(nil)
87 | @post.user.on_db(:slave01).should == @post.user
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/test-project/db/sharding.sql:
--------------------------------------------------------------------------------
1 | -- MySQL dump 10.13 Distrib 5.1.44, for apple-darwin10.2.0 (i386)
2 | --
3 | -- Host: localhost Database: db_charmer_sandbox_test
4 | -- ------------------------------------------------------
5 | -- Server version 5.1.44
6 |
7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
10 | /*!40101 SET NAMES utf8 */;
11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
12 | /*!40103 SET TIME_ZONE='+00:00' */;
13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
17 |
18 | --
19 | -- Table structure for table `events_shard_info`
20 | --
21 |
22 | DROP TABLE IF EXISTS `events_shard_info`;
23 | /*!40101 SET @saved_cs_client = @@character_set_client */;
24 | /*!40101 SET character_set_client = utf8 */;
25 | CREATE TABLE `events_shard_info` (
26 | `id` int(10) unsigned NOT NULL,
27 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
28 | `db_host` varchar(255) NOT NULL,
29 | `db_port` int(10) unsigned NOT NULL DEFAULT '3306',
30 | `db_user` varchar(255) NOT NULL DEFAULT 'root',
31 | `db_pass` varchar(255) NOT NULL DEFAULT '',
32 | `open` tinyint(1) unsigned NOT NULL DEFAULT '0',
33 | `enabled` tinyint(1) unsigned NOT NULL DEFAULT '0',
34 | `blocks_count` int(10) unsigned NOT NULL DEFAULT '0',
35 | PRIMARY KEY (`id`),
36 | KEY `alloc` (`enabled`,`open`,`blocks_count`)
37 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
38 | /*!40101 SET character_set_client = @saved_cs_client */;
39 |
40 | --
41 | -- Dumping data for table `events_shard_info`
42 | --
43 |
44 | LOCK TABLES `events_shard_info` WRITE;
45 | /*!40000 ALTER TABLE `events_shard_info` DISABLE KEYS */;
46 | /*!40000 ALTER TABLE `events_shard_info` ENABLE KEYS */;
47 | UNLOCK TABLES;
48 |
49 | --
50 | -- Table structure for table `events_shard_dict`
51 | --
52 |
53 | DROP TABLE IF EXISTS `events_shard_dict`;
54 | /*!40101 SET @saved_cs_client = @@character_set_client */;
55 | /*!40101 SET character_set_client = utf8 */;
56 | CREATE TABLE `events_shard_dict` (
57 | `start_id` int(10) unsigned NOT NULL,
58 | `end_id` int(10) unsigned NOT NULL,
59 | `shard_id` int(10) unsigned NOT NULL,
60 | `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
61 | `block_size` int(10) unsigned NOT NULL,
62 | PRIMARY KEY (`start_id`,`end_id`),
63 | KEY `shard_id` (`shard_id`)
64 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
65 | /*!40101 SET character_set_client = @saved_cs_client */;
66 |
67 | --
68 | -- Dumping data for table `events_shard_dict`
69 | --
70 |
71 | LOCK TABLES `events_shard_dict` WRITE;
72 | /*!40000 ALTER TABLE `events_shard_dict` DISABLE KEYS */;
73 | /*!40000 ALTER TABLE `events_shard_dict` ENABLE KEYS */;
74 | UNLOCK TABLES;
75 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
76 |
77 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
78 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
79 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
80 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
81 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
82 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
83 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
84 |
85 | -- Dump completed on 2010-03-22 1:37:30
86 |
--------------------------------------------------------------------------------
/lib/db_charmer/active_record/db_magic.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module DbMagic
4 |
5 | def db_magic(opt = {})
6 | # Make sure we could use our connections management here
7 | hijack_connection!
8 |
9 | # Should requested connections exist in the config?
10 | should_exist = opt.has_key?(:should_exist) ? opt[:should_exist] : DbCharmer.connections_should_exist?
11 |
12 | # Main connection management
13 | setup_connection_magic(opt[:connection], should_exist)
14 |
15 | # Set up slaves pool
16 | opt[:slaves] ||= []
17 | opt[:slaves] = [ opt[:slaves] ].flatten
18 | opt[:slaves] << opt[:slave] if opt[:slave]
19 |
20 | # Forced reads are enabled for all models by default, could be disabled by the user
21 | forced_slave_reads = opt.has_key?(:force_slave_reads) ? opt[:force_slave_reads] : true
22 |
23 | # Setup all the slaves related magic if needed
24 | setup_slaves_magic(opt[:slaves], forced_slave_reads, should_exist)
25 |
26 | # Setup inheritance magic
27 | setup_children_magic(opt)
28 |
29 | # Setup sharding if needed
30 | if opt[:sharded]
31 | raise ArgumentError, "Can't use sharding on a model with slaves!" if opt[:slaves].any?
32 | setup_sharding_magic(opt[:sharded])
33 | end
34 | end
35 |
36 | private
37 |
38 | def setup_children_magic(opt)
39 | self.db_charmer_opts = opt.clone
40 |
41 | unless self.respond_to?(:inherited_with_db_magic)
42 | class << self
43 | def inherited_with_db_magic(child)
44 | o = inherited_without_db_magic(child)
45 | child.db_magic(self.db_charmer_opts)
46 | o
47 | end
48 | alias_method_chain :inherited, :db_magic
49 | end
50 | end
51 | end
52 |
53 | def setup_sharding_magic(config)
54 | # Add sharding-specific methods
55 | self.extend(DbCharmer::ActiveRecord::Sharding)
56 |
57 | # Get configuration
58 | name = config[:sharded_connection] or raise ArgumentError, "No :sharded_connection!"
59 | # Assign sharded connection
60 | self.sharded_connection = DbCharmer::Sharding.sharded_connection(name)
61 |
62 | # Setup model default connection
63 | setup_connection_magic(sharded_connection.default_connection)
64 | end
65 |
66 | def setup_connection_magic(conn, should_exist = true)
67 | conn_proxy = coerce_to_connection_proxy(conn, should_exist)
68 | self.db_charmer_default_connection = conn_proxy
69 | switch_connection_to(conn_proxy, should_exist)
70 | end
71 |
72 | def setup_slaves_magic(slaves, force_slave_reads, should_exist = true)
73 | self.db_charmer_force_slave_reads = force_slave_reads
74 |
75 | # Initialize the slave connections list
76 | self.db_charmer_slaves = slaves.collect do |slave|
77 | coerce_to_connection_proxy(slave, should_exist)
78 | end
79 | return if db_charmer_slaves.empty?
80 |
81 | # Enable on_slave/on_master methods
82 | self.extend(DbCharmer::ActiveRecord::MultiDbProxy::MasterSlaveClassMethods)
83 |
84 | # Enable automatic master/slave queries routing (we have specialized versions on those modules for rails2/3)
85 | self.extend(DbCharmer::ActiveRecord::MasterSlaveRouting::ClassMethods)
86 | self.send(:include, DbCharmer::ActiveRecord::MasterSlaveRouting::InstanceMethods)
87 | end
88 |
89 | end
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/lib/db_charmer/connection_factory.rb:
--------------------------------------------------------------------------------
1 | #
2 | # This class is used to automatically generate small abstract ActiveRecord classes
3 | # that would then be used as a source of database connections for DbCharmer magic.
4 | # This way we do not need to re-implement all the connection establishing code
5 | # that ActiveRecord already has and we make our code less dependant on Rails versions.
6 | #
7 | module DbCharmer
8 | module ConnectionFactory
9 | def self.connection_classes
10 | Thread.current[:db_charmer_generated_connection_classes] ||= {}
11 | end
12 |
13 | def self.connection_classes=(val)
14 | Thread.current[:db_charmer_generated_connection_classes] = val
15 | end
16 |
17 | def self.reset!
18 | self.connection_classes = {}
19 | end
20 |
21 | # Establishes connection or return an existing one from cache
22 | def self.connect(connection_name, should_exist = true)
23 | connection_name = connection_name.to_s
24 | connection_classes[connection_name] ||= establish_connection(connection_name, should_exist)
25 | end
26 |
27 | # Establishes connection or return an existing one from cache (not using AR database configs)
28 | def self.connect_to_db(connection_name, config)
29 | connection_name = connection_name.to_s
30 | connection_classes[connection_name] ||= establish_connection_to_db(connection_name, config)
31 | end
32 |
33 | # Establish connection with a specified name
34 | def self.establish_connection(connection_name, should_exist = true)
35 | abstract_class = generate_abstract_class(connection_name, should_exist)
36 | DbCharmer::ConnectionProxy.new(abstract_class, connection_name)
37 | end
38 |
39 | # Establish connection with a specified name (not using AR database configs)
40 | def self.establish_connection_to_db(connection_name, config)
41 | abstract_class = generate_abstract_class_for_db(connection_name, config)
42 | DbCharmer::ConnectionProxy.new(abstract_class, connection_name)
43 | end
44 |
45 | # Generate an abstract AR class with specified connection established
46 | def self.generate_abstract_class(connection_name, should_exist = true)
47 | # Generate class
48 | klass = generate_empty_abstract_ar_class(abstract_connection_class_name(connection_name))
49 |
50 | # Establish connection
51 | klass.establish_real_connection_if_exists(connection_name.to_sym, !!should_exist)
52 |
53 | # Return the class
54 | return klass
55 | end
56 |
57 | # Generate an abstract AR class with specified connection established (not using AR database configs)
58 | def self.generate_abstract_class_for_db(connection_name, config)
59 | # Generate class
60 | klass = generate_empty_abstract_ar_class(abstract_connection_class_name(connection_name))
61 |
62 | # Establish connection
63 | klass.establish_connection(config)
64 |
65 | # Return the class
66 | return klass
67 | end
68 |
69 | def self.generate_empty_abstract_ar_class(klass)
70 | # Define class
71 | module_eval "class #{klass} < ::ActiveRecord::Base; self.abstract_class = true; end"
72 |
73 | # Return class
74 | klass.constantize
75 | end
76 |
77 | # Generates unique names for our abstract AR classes
78 | def self.abstract_connection_class_name(connection_name)
79 | conn_name_klass = connection_name.to_s.gsub(/\W+/, '_').camelize
80 | thread = Thread.current.object_id.abs # need to make sure it is non-negative
81 | "::AutoGeneratedAbstractConnectionClass#{conn_name_klass}ForThread#{thread}"
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/test-project-2.x/config/boot.rb:
--------------------------------------------------------------------------------
1 | # We only have test environment here
2 | ENV['RAILS_ENV'] = 'test'
3 |
4 | # Don't change this file!
5 | # Configure your app in config/environment.rb and config/environments/*.rb
6 |
7 | RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
8 |
9 | module Rails
10 | class << self
11 | def boot!
12 | unless booted?
13 | preinitialize
14 | pick_boot.run
15 | end
16 | end
17 |
18 | def booted?
19 | defined? Rails::Initializer
20 | end
21 |
22 | def pick_boot
23 | (vendor_rails? ? VendorBoot : GemBoot).new
24 | end
25 |
26 | def vendor_rails?
27 | File.exist?("#{RAILS_ROOT}/vendor/rails")
28 | end
29 |
30 | def preinitialize
31 | load(preinitializer_path) if File.exist?(preinitializer_path)
32 | end
33 |
34 | def preinitializer_path
35 | "#{RAILS_ROOT}/config/preinitializer.rb"
36 | end
37 | end
38 |
39 | class Boot
40 | def run
41 | load_initializer
42 |
43 | Rails::Initializer.class_eval do
44 | def load_gems
45 | @bundler_loaded ||= Bundler.require :default, Rails.env
46 | end
47 | end
48 |
49 | Rails::Initializer.run(:set_load_path)
50 | end
51 | end
52 |
53 | class VendorBoot < Boot
54 | def load_initializer
55 | require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
56 | Rails::Initializer.run(:install_gem_spec_stubs)
57 | Rails::GemDependency.add_frozen_gem_path
58 | end
59 | end
60 |
61 | class GemBoot < Boot
62 | def load_initializer
63 | self.class.load_rubygems
64 | load_rails_gem
65 | require 'initializer'
66 | end
67 |
68 | def load_rails_gem
69 | if version = self.class.gem_version
70 | gem 'rails', version
71 | else
72 | gem 'rails'
73 | end
74 | rescue Gem::LoadError => load_error
75 | $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
76 | exit 1
77 | end
78 |
79 | class << self
80 | def rubygems_version
81 | Gem::RubyGemsVersion rescue nil
82 | end
83 |
84 | def gem_version
85 | if defined? RAILS_GEM_VERSION
86 | RAILS_GEM_VERSION
87 | elsif ENV.include?('RAILS_GEM_VERSION')
88 | ENV['RAILS_GEM_VERSION']
89 | else
90 | parse_gem_version(read_environment_rb)
91 | end
92 | end
93 |
94 | def load_rubygems
95 | require 'rubygems'
96 | min_version = '1.3.1'
97 | unless rubygems_version >= min_version
98 | $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
99 | exit 1
100 | end
101 |
102 | rescue LoadError
103 | $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
104 | exit 1
105 | end
106 |
107 | def parse_gem_version(text)
108 | $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
109 | end
110 |
111 | private
112 | def read_environment_rb
113 | File.read("#{RAILS_ROOT}/config/environment.rb")
114 | end
115 | end
116 | end
117 | end
118 |
119 | # All that for this:
120 | Rails.boot!
121 |
--------------------------------------------------------------------------------
/test-project/spec/controllers/posts_controller_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe PostsController do
4 | fixtures :posts
5 |
6 | # Delete these examples and add some real ones
7 | it "should support db_charmer readonly actions method" do
8 | PostsController.respond_to?(:force_slave_reads).should be_true
9 | end
10 |
11 | it "index action should force slave reads" do
12 | PostsController.force_slave_reads_action?(:index).should be_true
13 | end
14 |
15 | it "create action should not force slave reads" do
16 | PostsController.force_slave_reads_action?(:create).should be_false
17 | end
18 |
19 | describe "GET 'index'" do
20 | context "slave reads enforcing (action is listed in :only)" do
21 | it "should enable enforcing" do
22 | get 'index'
23 | controller.force_slave_reads?.should be_true
24 | end
25 |
26 | it "should actually force slave reads" do
27 | Post.connection.should_not_receive(:select_value) # no counts
28 | Post.connection.should_not_receive(:select_all) # no finds
29 | Post.on_slave.connection.should_receive(:select_value).and_return(1)
30 | get 'index'
31 | end
32 | end
33 | end
34 |
35 | describe "GET 'show'" do
36 | context "slave reads enforcing (action is listed in :only)" do
37 | it "should enable enforcing" do
38 | get 'show', :id => Post.first.id
39 | controller.force_slave_reads?.should be_true
40 | end
41 |
42 | it "should actually force slave reads" do
43 | post = Post.first
44 | Post.connection.should_not_receive(:select_value) # no counts
45 | Post.connection.should_not_receive(:select_all) # no finds
46 | Post.on_slave.connection.should_receive(:select_value).and_return(1)
47 | Post.on_slave.connection.should_receive(:select_all).and_return([post.attributes])
48 | get 'show', :id => post.id
49 | end
50 | end
51 | end
52 |
53 | describe "GET 'new'" do
54 | context "slave reads enforcing (action is listed in :except)" do
55 | it "should not enable enforcing" do
56 | get 'new'
57 | controller.force_slave_reads?.should be_false
58 | end
59 |
60 | it "should not do any actual enforcing" do
61 | Post.connection.should_receive(:select_value).and_return(0) # count
62 | Post.on_slave.connection.should_not_receive(:select_value) # no counts
63 | Post.on_slave.connection.should_not_receive(:select_all) # no selects
64 | get 'new'
65 | end
66 | end
67 | end
68 |
69 | describe "GET 'create'" do
70 | it "should redirect to post url upon successful completion" do
71 | get 'create', :post => { :title => 'xxx', :user_id => 1 }
72 | response.should redirect_to(post_url(Post.last))
73 | end
74 |
75 | it "should create a Post record" do
76 | lambda {
77 | get 'create', :post => { :title => 'xxx', :user_id => 1 }
78 | }.should change { Post.count }.by(+1)
79 | end
80 |
81 | context "slave reads enforcing (action is not listed in force_slave_reads params)" do
82 | it "should not enable enforcing" do
83 | get 'create'
84 | controller.force_slave_reads?.should_not be_true
85 | end
86 |
87 | it "should not do any actual enforcing" do
88 | Post.on_slave.connection.should_not_receive(:select_value)
89 | Post.connection.should_receive(:select_value).once.and_return(1)
90 | get 'create'
91 | end
92 | end
93 | end
94 |
95 | describe "GET 'destroy'" do
96 | it "should redurect to index upon completion" do
97 | get 'destroy', :id => Post.first.id
98 | response.should redirect_to(:action => :index)
99 | end
100 |
101 | it "should delete a record" do
102 | lambda {
103 | get 'destroy', :id => Post.first.id
104 | }.should change { Post.count }.by(-1)
105 | end
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/lib/db_charmer/active_record/connection_switching.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module ConnectionSwitching
4 | def establish_real_connection_if_exists(name, should_exist = false)
5 | name = name.to_s
6 |
7 | # Check environment name
8 | config = configurations[DbCharmer.env]
9 | unless config
10 | error = "Invalid environment name (does not exist in database.yml): #{DbCharmer.env}. Please set correct Rails.env or DbCharmer.env."
11 | raise ArgumentError, error
12 | end
13 |
14 | # Check connection name
15 | config = config[name]
16 | unless config
17 | if should_exist
18 | raise ArgumentError, "Invalid connection name (does not exist in database.yml): #{DbCharmer.env}/#{name}"
19 | end
20 | return # No need to establish connection - they do not want us to
21 | end
22 |
23 | # Pass connection name with config
24 | config[:connection_name] = name
25 | establish_connection(config)
26 | end
27 |
28 | #-----------------------------------------------------------------------------------------------------------------
29 | def hijack_connection!
30 | return if self.respond_to?(:connection_with_magic)
31 | class << self
32 | # Make sure we check our accessors before going to the default connection retrieval method
33 | def connection_with_magic
34 | db_charmer_remapped_connection || db_charmer_model_connection_proxy || connection_without_magic
35 | end
36 | alias_method_chain :connection, :magic
37 |
38 | def connection_pool_with_magic
39 | if connection.respond_to?(:abstract_connection_class)
40 | abstract_connection_class = connection.abstract_connection_class
41 | connection_handler.retrieve_connection_pool(abstract_connection_class) || connection_pool_without_magic
42 | else
43 | connection_pool_without_magic
44 | end
45 | end
46 | alias_method_chain :connection_pool, :magic
47 | end
48 | end
49 |
50 | #-----------------------------------------------------------------------------------------------------------------
51 | def coerce_to_connection_proxy(conn, should_exist = true)
52 | # Return nil if given no connection specification
53 | return nil if conn.nil?
54 |
55 | # For sharded proxies just use them as-is
56 | return conn if conn.respond_to?(:set_real_connection)
57 |
58 | # For connection proxies and objects that could be coerced into a proxy just call the coercion method
59 | return conn.db_charmer_connection_proxy if conn.respond_to?(:db_charmer_connection_proxy)
60 |
61 | # For plain AR connection adapters, just use them as-is
62 | return conn if conn.kind_of?(::ActiveRecord::ConnectionAdapters::AbstractAdapter)
63 |
64 | # For connection names, use connection factory to create new connections
65 | if conn.kind_of?(Symbol) || conn.kind_of?(String)
66 | return DbCharmer::ConnectionFactory.connect(conn, should_exist)
67 | end
68 |
69 | # For connection configs (hashes), create connections
70 | if conn.kind_of?(Hash)
71 | conn = conn.symbolize_keys
72 | raise ArgumentError, "Missing required :connection_name parameter" unless conn[:connection_name]
73 | return DbCharmer::ConnectionFactory.connect_to_db(conn[:connection_name], conn)
74 | end
75 |
76 | # Fails for unsupported connection types
77 | raise "Unsupported connection type: #{conn.class}"
78 | end
79 |
80 | #-----------------------------------------------------------------------------------------------------------------
81 | def switch_connection_to(conn, should_exist = true)
82 | new_conn = coerce_to_connection_proxy(conn, should_exist)
83 |
84 | if db_charmer_connection_proxy.respond_to?(:set_real_connection)
85 | db_charmer_connection_proxy.set_real_connection(new_conn)
86 | end
87 |
88 | self.db_charmer_connection_proxy = new_conn
89 | self.hijack_connection!
90 | end
91 |
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/connection_switching_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | class FooModelForConnSwitching < ActiveRecord::Base; end
4 | class BarModelForConnSwitching < ActiveRecord::Base; end
5 |
6 | describe DbCharmer, "AR connection switching" do
7 | describe "in switch_connection_to method" do
8 | before(:all) do
9 | BarModelForConnSwitching.hijack_connection!
10 | end
11 |
12 | before :each do
13 | @proxy = double('proxy')
14 | @proxy.stub(:db_charmer_connection_name).and_return(:myproxy)
15 | end
16 |
17 | before do
18 | BarModelForConnSwitching.db_charmer_connection_proxy = @proxy
19 | BarModelForConnSwitching.connection.should be(@proxy)
20 | end
21 |
22 | it "should accept nil and reset connection to default" do
23 | BarModelForConnSwitching.switch_connection_to(nil)
24 | BarModelForConnSwitching.connection.should be(ActiveRecord::Base.connection)
25 | end
26 |
27 | it "should accept a string and generate an abstract class with connection factory" do
28 | BarModelForConnSwitching.switch_connection_to('logs')
29 | BarModelForConnSwitching.connection.object_id == DbCharmer::ConnectionFactory.connect('logs').object_id
30 | end
31 |
32 | it "should accept a symbol and generate an abstract class with connection factory" do
33 | BarModelForConnSwitching.switch_connection_to(:logs)
34 | BarModelForConnSwitching.connection.object_id.should == DbCharmer::ConnectionFactory.connect('logs').object_id
35 | end
36 |
37 | it "should accept a model and use its connection proxy value" do
38 | FooModelForConnSwitching.switch_connection_to(:logs)
39 | BarModelForConnSwitching.switch_connection_to(FooModelForConnSwitching)
40 | BarModelForConnSwitching.connection.object_id.should == DbCharmer::ConnectionFactory.connect('logs').object_id
41 | end
42 |
43 | context "with a hash parameter" do
44 | before do
45 | @conf = {
46 | :adapter => 'mysql',
47 | :username => "db_charmer_ro",
48 | :database => "db_charmer_sandbox_test",
49 | :connection_name => 'sanbox_ro'
50 | }
51 | end
52 |
53 | it "should fail if there is no :connection_name parameter" do
54 | @conf.delete(:connection_name)
55 | lambda { BarModelForConnSwitching.switch_connection_to(@conf) }.should raise_error(ArgumentError)
56 | end
57 |
58 | it "generate an abstract class with connection factory" do
59 | BarModelForConnSwitching.switch_connection_to(@conf)
60 | BarModelForConnSwitching.connection.object_id.should == DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf).object_id
61 | end
62 | end
63 |
64 | it "should support connection switching for AR::Base" do
65 | ActiveRecord::Base.switch_connection_to(:logs)
66 | ActiveRecord::Base.connection.object_id == DbCharmer::ConnectionFactory.connect('logs').object_id
67 | ActiveRecord::Base.switch_connection_to(nil)
68 | end
69 | end
70 | end
71 |
72 | describe DbCharmer, "for ActiveRecord models" do
73 | describe "in establish_real_connection_if_exists method" do
74 | it "should check connection name if requested" do
75 | lambda { FooModelForConnSwitching.establish_real_connection_if_exists(:foo, true) }.should raise_error(ArgumentError)
76 | end
77 |
78 | it "should not check connection name if not reqested" do
79 | lambda { FooModelForConnSwitching.establish_real_connection_if_exists(:foo) }.should_not raise_error
80 | end
81 |
82 | it "should not check connection name if reqested not to" do
83 | lambda { FooModelForConnSwitching.establish_real_connection_if_exists(:foo, false) }.should_not raise_error
84 | end
85 |
86 | it "should establish connection when connection configuration exists" do
87 | FooModelForConnSwitching.should_receive(:establish_connection)
88 | FooModelForConnSwitching.establish_real_connection_if_exists(:logs)
89 | end
90 |
91 | it "should not establish connection even when connection configuration does not exist" do
92 | FooModelForConnSwitching.should_not_receive(:establish_connection)
93 | FooModelForConnSwitching.establish_real_connection_if_exists(:blah)
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/class_attributes_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | class FooModel < ActiveRecord::Base; end
4 |
5 | describe DbCharmer, "for ActiveRecord models" do
6 | context "in db_charmer_connection_proxy methods" do
7 | before do
8 | FooModel.db_charmer_connection_proxy = nil
9 | FooModel.db_charmer_default_connection = nil
10 | end
11 |
12 | it "should implement both accessor methods" do
13 | proxy = double('connection proxy')
14 | FooModel.db_charmer_connection_proxy = proxy
15 | FooModel.db_charmer_connection_proxy.should be(proxy)
16 | end
17 | end
18 |
19 | context "in db_charmer_default_connection methods" do
20 | before do
21 | FooModel.db_charmer_default_connection = nil
22 | FooModel.db_charmer_default_connection = nil
23 | end
24 |
25 | it "should implement both accessor methods" do
26 | conn = double('connection')
27 | FooModel.db_charmer_default_connection = conn
28 | FooModel.db_charmer_default_connection.should be(conn)
29 | end
30 | end
31 |
32 | context "in db_charmer_opts methods" do
33 | before do
34 | FooModel.db_charmer_opts = nil
35 | end
36 |
37 | it "should implement both accessor methods" do
38 | opts = { :foo => :bar}
39 | FooModel.db_charmer_opts = opts
40 | FooModel.db_charmer_opts.should be(opts)
41 | end
42 | end
43 |
44 | context "in db_charmer_slaves methods" do
45 | it "should return [] if no slaves set for a model" do
46 | FooModel.db_charmer_slaves = nil
47 | FooModel.db_charmer_slaves.should == []
48 | end
49 |
50 | it "should implement both accessor methods" do
51 | proxy = double('connection proxy')
52 | FooModel.db_charmer_slaves = [ proxy ]
53 | FooModel.db_charmer_slaves.should == [ proxy ]
54 | end
55 |
56 | it "should implement random slave selection" do
57 | FooModel.db_charmer_slaves = [ :proxy1, :proxy2, :proxy3 ]
58 | srand(0)
59 | FooModel.db_charmer_random_slave.should == :proxy1
60 | FooModel.db_charmer_random_slave.should == :proxy2
61 | FooModel.db_charmer_random_slave.should == :proxy1
62 | FooModel.db_charmer_random_slave.should == :proxy2
63 | FooModel.db_charmer_random_slave.should == :proxy2
64 | FooModel.db_charmer_random_slave.should == :proxy3
65 | end
66 | end
67 |
68 | context "in db_charmer_connection_levels methods" do
69 | it "should return 0 by default" do
70 | FooModel.db_charmer_connection_level = nil
71 | FooModel.db_charmer_connection_level.should == 0
72 | end
73 |
74 | it "should implement both accessor methods and support inc/dec operations" do
75 | FooModel.db_charmer_connection_level = 1
76 | FooModel.db_charmer_connection_level.should == 1
77 | FooModel.db_charmer_connection_level += 1
78 | FooModel.db_charmer_connection_level.should == 2
79 | FooModel.db_charmer_connection_level -= 1
80 | FooModel.db_charmer_connection_level.should == 1
81 | end
82 |
83 | it "should implement db_charmer_top_level_connection? method" do
84 | FooModel.db_charmer_connection_level = 1
85 | FooModel.should_not be_db_charmer_top_level_connection
86 | FooModel.db_charmer_connection_level = 0
87 | FooModel.should be_db_charmer_top_level_connection
88 | end
89 | end
90 |
91 | context "in connection method" do
92 | it "should return AR's original connection if no connection proxy is set" do
93 | FooModel.db_charmer_connection_proxy = nil
94 | FooModel.db_charmer_default_connection = nil
95 | FooModel.connection.should be_kind_of(ActiveRecord::ConnectionAdapters::AbstractAdapter)
96 | end
97 | end
98 |
99 | context "in db_charmer_force_slave_reads? method" do
100 | it "should use per-model settings when possible" do
101 | FooModel.db_charmer_force_slave_reads = true
102 | DbCharmer.should_not_receive(:force_slave_reads?)
103 | FooModel.db_charmer_force_slave_reads?.should be_true
104 | end
105 |
106 | it "should use global settings when local setting is false" do
107 | FooModel.db_charmer_force_slave_reads = false
108 |
109 | DbCharmer.should_receive(:force_slave_reads?).and_return(true)
110 | FooModel.db_charmer_force_slave_reads?.should be_true
111 |
112 | DbCharmer.should_receive(:force_slave_reads?).and_return(false)
113 | FooModel.db_charmer_force_slave_reads?.should be_false
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/lib/db_charmer/active_record/class_attributes.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module ClassAttributes
4 | @@db_charmer_opts = {}
5 | def db_charmer_opts=(opts)
6 | @@db_charmer_opts[self.name] = opts
7 | end
8 |
9 | def db_charmer_opts
10 | @@db_charmer_opts[self.name] || {}
11 | end
12 |
13 | #---------------------------------------------------------------------------------------------
14 | @@db_charmer_default_connections = {}
15 | def db_charmer_default_connection=(conn)
16 | @@db_charmer_default_connections[self.name] = conn
17 | end
18 |
19 | def db_charmer_default_connection
20 | @@db_charmer_default_connections[self.name]
21 | end
22 |
23 | #---------------------------------------------------------------------------------------------
24 | @@db_charmer_slaves = {}
25 | def db_charmer_slaves=(slaves)
26 | @@db_charmer_slaves[self.name] = slaves
27 | end
28 |
29 | def db_charmer_slaves
30 | @@db_charmer_slaves[self.name] || []
31 | end
32 |
33 | # Returns a random connection from the list of slaves configured for this AR class
34 | def db_charmer_random_slave
35 | return nil unless db_charmer_slaves.any?
36 | db_charmer_slaves[rand(db_charmer_slaves.size)]
37 | end
38 |
39 | #---------------------------------------------------------------------------------------------
40 | def db_charmer_connection_proxies
41 | Thread.current[:db_charmer_connection_proxies] ||= {}
42 | end
43 |
44 | def db_charmer_connection_proxy=(proxy)
45 | db_charmer_connection_proxies[self.name] = proxy
46 | end
47 |
48 | def db_charmer_connection_proxy
49 | db_charmer_connection_proxies[self.name]
50 | end
51 |
52 | #---------------------------------------------------------------------------------------------
53 | def db_charmer_force_slave_reads_flags
54 | Thread.current[:db_charmer_force_slave_reads] ||= {}
55 | end
56 |
57 | def db_charmer_force_slave_reads=(force)
58 | db_charmer_force_slave_reads_flags[self.name] = force
59 | end
60 |
61 | def db_charmer_force_slave_reads
62 | db_charmer_force_slave_reads_flags[self.name]
63 | end
64 |
65 | # Slave reads are used in two cases:
66 | # - per-model slave reads are enabled (see db_magic method for more details)
67 | # - global slave reads enforcing is enabled (in a controller action)
68 | def db_charmer_force_slave_reads?
69 | db_charmer_force_slave_reads || DbCharmer.force_slave_reads?
70 | end
71 |
72 | #---------------------------------------------------------------------------------------------
73 | def db_charmer_connection_levels
74 | Thread.current[:db_charmer_connection_levels] ||= Hash.new(0)
75 | end
76 |
77 | def db_charmer_connection_level=(level)
78 | db_charmer_connection_levels[self.name] = level
79 | end
80 |
81 | def db_charmer_connection_level
82 | db_charmer_connection_levels[self.name] || 0
83 | end
84 |
85 | def db_charmer_top_level_connection?
86 | db_charmer_connection_level.zero?
87 | end
88 |
89 | #---------------------------------------------------------------------------------------------
90 | def db_charmer_remapped_connection
91 | return nil unless db_charmer_top_level_connection?
92 | name = :master
93 | proxy = db_charmer_model_connection_proxy
94 | name = proxy.db_charmer_connection_name.to_sym if proxy
95 |
96 | remapped = db_charmer_database_remappings[name]
97 | remapped ? DbCharmer::ConnectionFactory.connect(remapped, true) : nil
98 | end
99 |
100 | def db_charmer_database_remappings
101 | Thread.current[:db_charmer_database_remappings] ||= Hash.new
102 | end
103 |
104 | def db_charmer_database_remappings=(mappings)
105 | raise "Mappings must be nil or respond to []" if mappings && (! mappings.respond_to?(:[]))
106 | Thread.current[:db_charmer_database_remappings] = mappings || {}
107 | end
108 |
109 | #---------------------------------------------------------------------------------------------
110 | # Returns model-specific connection proxy, ignoring any global connection remappings
111 | def db_charmer_model_connection_proxy
112 | db_charmer_connection_proxy || db_charmer_default_connection
113 | end
114 | end
115 | end
116 | end
117 |
--------------------------------------------------------------------------------
/README.rdoc:
--------------------------------------------------------------------------------
1 | = WARNING: The Project Has Been Suspended
2 |
3 | Please note, that this project has been suspended. No updates will be provided and no Rails versions
4 | beyond 3.2.x will be supported. For more information please check out this blog post: http://kovyrin.net/2014/11/14/dbcharmer-suspended/
5 |
6 | = DB Charmer - ActiveRecord Connection Magic Plugin
7 |
8 | +DbCharmer+ is a simple yet powerful plugin for ActiveRecord that significantly extends its ability to work with
9 | multiple databases and/or database servers. The major features we add to ActiveRecord are:
10 |
11 | 1. Simple management for AR model connections (+switch_connection_to+ method)
12 | 2. Switching of default AR model connections to separate servers/databases
13 | 3. Ability to easily choose where your query should go (Model.on_* methods family)
14 | 4. Automated master/slave queries routing (selects go to a slave, updates handled by the master).
15 | 5. Multiple database migrations with very flexible query routing controls.
16 | 6. Simple database sharding functionality with multiple sharding methods (value, range, mapping table).
17 |
18 | For more information on the project, you can check out our web site at http://kovyrin.github.io/db-charmer/.
19 |
20 | == Installation
21 |
22 | There are two options when approaching +DbCharmer+ installation:
23 | * using the gem (recommended and the only way of using it with Rails 3.2+)
24 | * install as a Rails plugin (works in Rails 2.x only)
25 |
26 | To install as a gem, add this to your Gemfile:
27 |
28 | gem 'db-charmer', :require => 'db_charmer'
29 |
30 | To install +DbCharmer+ as a Rails plugin use the following command:
31 |
32 | ./script/plugin install git://github.com/kovyrin/db-charmer.git
33 |
34 | _Notice_: If you use +DbCharmer+ in a non-rails project, you may need to set DbCharmer.env to a correct value
35 | before using any of its connection management methods. Correct value here is a valid database.yml
36 | first-level section name.
37 |
38 |
39 | == Documentation/Questions
40 |
41 | For more information about the library, please visit our site at http://dbcharmer.net.
42 | If you need more defails on DbCharmer internals, please check out the source code. All the plugin's
43 | code is ~100% covered with tests. The project located in test-project directory has unit
44 | tests for all or, at least, the most actively used code paths.
45 |
46 | If you have any questions regarding this project, you could contact the author using
47 | the DbCharmer Users Group mailing list:
48 |
49 | - Group Info: http://groups.google.com/group/db-charmer
50 | - Subscribe using the info page or by sending an email to mailto:db-charmer-subscribe@googlegroups.com
51 |
52 |
53 | == What Ruby and Rails implementations does it work for?
54 |
55 | We have a continuous integration setup for this gem on with Rails 2.3, 3.0, 3.1 and 3.2 using a few
56 | different versions of Ruby.
57 |
58 | CI is running on TravisCI.org: https://travis-ci.org/kovyrin/db-charmer
59 | Build status is: {
}[https://travis-ci.org/kovyrin/db-charmer]
60 |
61 | At the moment we have the following build matrix:
62 | * Rails versions:
63 | - 2.3
64 | - 3.0
65 | - 3.1
66 | - 3.2
67 | * Ruby versions:
68 | - 1.8.7
69 | - 1.9.3 (Rails 3.0+ only)
70 | - 2.0.0 (Rails 3.2+ only)
71 | * Databases:
72 | - MySQL
73 |
74 | In addition to CI testing, this gem is used in production on Scribd.com (one of the largest RoR
75 | sites in the world) with Ruby Enterprise Edition and Rails 2.2, Rails 2.3, Sinatra and plain
76 | Rack applications.
77 |
78 | Starting with version 1.8.0 we support Rails versions 3.2.8 and higher. Please note, that Rails 3.2.4
79 | is not officially supported. Your code may work on that version, but no bug reports will be
80 | accepted about this version.
81 |
82 |
83 | == Is it Thread-Safe?
84 |
85 | Starting with version 1.9.0 we have started working on making the code thread-safe and making sure
86 | DbCharmer works correctly in multi-threaded environments. At this moment we consider multi-threaded
87 | mode experimental. If you use it and it works for you - please let us know, if it does not - please
88 | make sure to file a ticket so that we could improve the code and make it work in your situation.
89 |
90 |
91 | == Who are the authors?
92 |
93 | This plugin has been created in Scribd.com for our internal use and then the sources were opened for
94 | other people to use. Most of the code in this package has been developed by Oleksiy Kovyrin for
95 | Scribd.com and is released under the MIT license. For more details, see the LICENSE file.
96 |
97 | Other contributors who have helped with the development of this library are (alphabetically ordered):
98 | * Allen Madsen
99 | * Andrew Geweke
100 | * Ashley Martens
101 | * Cauê Guerra
102 | * David Dai
103 | * Dmytro Shteflyuk
104 | * Eric Lindvall
105 | * Eugene Pimenov
106 | * Jonathan Viney
107 | * Gregory Man
108 | * Michael Birk
109 | * Tyler McMullen
110 |
--------------------------------------------------------------------------------
/test-project/spec/sharding/method/db_block_map_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DbCharmer::Sharding::Method::DbBlockMap do
4 | fixtures :event_shards_info, :event_shards_map
5 |
6 | before(:each) do
7 | @sharder = DbCharmer::Sharding::Method::DbBlockMap.new(
8 | :name => :social,
9 | :block_size => 10,
10 | :map_table => :event_shards_map,
11 | :shards_table => :event_shards_info,
12 | :connection => :social_shard_info
13 | )
14 | @conn = DbCharmer::ConnectionFactory.connect(:social_shard_info)
15 | end
16 |
17 | describe "standard interface" do
18 | it "should respond to shard_for_id" do
19 | @sharder.should respond_to(:shard_for_key)
20 | end
21 |
22 | it "should return a shard config to be used for a key" do
23 | @sharder.shard_for_key(1).should be_kind_of(Hash)
24 | end
25 |
26 | it "should have shard_connections method and return a list of db connections" do
27 | @sharder.shard_connections.should_not be_empty
28 | end
29 | end
30 |
31 | it "should correctly return shards for all blocks defined in the mapping table" do
32 | blocks = @conn.select_all("SELECT * FROM event_shards_map")
33 |
34 | blocks.each do |blk|
35 | shard = @sharder.shard_for_key(blk['start_id'])
36 | shard[:connection_name].should match(/social.*#{blk['shard_id']}$/)
37 |
38 | shard = @sharder.shard_for_key(blk['start_id'].to_i + 1)
39 | shard[:connection_name].should match(/social.*#{blk['shard_id']}$/)
40 |
41 | shard = @sharder.shard_for_key(blk['end_id'].to_i - 1)
42 | shard[:connection_name].should match(/social.*#{blk['shard_id']}$/)
43 | end
44 | end
45 |
46 | describe "for non-existing blocks" do
47 | before do
48 | @max_id = @conn.select_value("SELECT max(end_id) FROM event_shards_map").to_i
49 | Rails.cache.clear
50 | end
51 |
52 | it "should not fail" do
53 | lambda {
54 | @sharder.shard_for_key(@max_id + 1)
55 | }.should_not raise_error
56 | end
57 |
58 | it "should create a new one" do
59 | @sharder.shard_for_key(@max_id + 1).should_not be_nil
60 | end
61 |
62 | it "should assign it to the least loaded shard" do
63 | @sharder.shard_for_key(@max_id + 1)[:connection_name].should match(/shard.*03$/)
64 | end
65 |
66 | it "should not consider non-open shards" do
67 | @conn.execute("UPDATE event_shards_info SET open = 0 WHERE id = 3")
68 | @sharder.shard_for_key(@max_id + 1)[:connection_name].should_not match(/shard.*03$/)
69 | end
70 |
71 | it "should not consider disabled shards" do
72 | @conn.execute("UPDATE event_shards_info SET enabled = 0 WHERE id = 3")
73 | @sharder.shard_for_key(@max_id + 1)[:connection_name].should_not match(/shard.*03$/)
74 | end
75 |
76 | it "should increment the blocks counter on the shard" do
77 | lambda {
78 | @sharder.shard_for_key(@max_id + 1)
79 | }.should change {
80 | @conn.select_value("SELECT blocks_count FROM event_shards_info WHERE id = 3").to_i
81 | }.by(+1)
82 | end
83 |
84 | it "should raise duplicate key error when allocating same block twice" do
85 | @sharder.allocate_new_block_for_key(@max_id + 1)
86 | lambda {
87 | @sharder.allocate_new_block_for_key(@max_id + 1)
88 | }.should raise_error(ActiveRecord::StatementInvalid)
89 | end
90 |
91 | it "should handle duplicate key errors" do
92 | @sharder.shard_for_key(@max_id + 1)
93 |
94 | actual_block = @sharder.block_for_key(@max_id + 1)
95 | @sharder.should_receive(:block_for_key).twice.and_return(nil, actual_block)
96 |
97 | @sharder.shard_for_key(@max_id + 1)
98 | end
99 | end
100 |
101 | it "should fail on invalid shard references" do
102 | @conn.execute("DELETE FROM event_shards_info")
103 | lambda { @sharder.shard_for_key(1) }.should raise_error(ArgumentError)
104 | end
105 |
106 | it "should cache shards info" do
107 | shard = DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.first
108 | DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.should_receive(:find_by_id).once.and_return(shard)
109 | @sharder.shard_info_by_id(1)
110 | @sharder.shard_info_by_id(1)
111 | end
112 |
113 | it "should not cache shards info when explicitly asked not to" do
114 | shard = DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.first
115 | DbCharmer::Sharding::Method::DbBlockMap::ShardInfo.should_receive(:find_by_id).twice.and_return(shard)
116 | @sharder.shard_info_by_id(1, false)
117 | @sharder.shard_info_by_id(1, false)
118 | end
119 |
120 | it "should cache blocks" do
121 | @sharder.block_for_key(1)
122 | @sharder.connection.should_not_receive(:select_one)
123 | @sharder.block_for_key(1)
124 | @sharder.block_for_key(2)
125 | end
126 |
127 | it "should not cache blocks if asked not to" do
128 | block = @sharder.block_for_key(1)
129 | @sharder.connection.should_receive(:select_one).twice.and_return(block)
130 | @sharder.block_for_key(1, false)
131 | @sharder.block_for_key(2, false)
132 | end
133 |
134 |
135 | end
136 |
--------------------------------------------------------------------------------
/test-project/spec/unit/connection_factory_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe DbCharmer::ConnectionFactory do
4 | context "in generate_abstract_class method" do
5 | it "should fail if requested connection config does not exists" do
6 | lambda { DbCharmer::ConnectionFactory.generate_abstract_class('foo') }.should raise_error(ArgumentError)
7 | end
8 |
9 | it "should not fail if requested connection config does not exists and should_exist = false" do
10 | lambda { DbCharmer::ConnectionFactory.generate_abstract_class('foo', false) }.should_not raise_error
11 | end
12 |
13 | it "should fail if requested connection config does not exists and should_exist = true" do
14 | lambda { DbCharmer::ConnectionFactory.generate_abstract_class('foo', true) }.should raise_error(ArgumentError)
15 | end
16 |
17 | it "should generate abstract connection classes" do
18 | klass = DbCharmer::ConnectionFactory.generate_abstract_class('foo', false)
19 | klass.superclass.should be(ActiveRecord::Base)
20 | end
21 |
22 | it "should work with weird connection names" do
23 | klass = DbCharmer::ConnectionFactory.generate_abstract_class('foo.bar@baz#blah', false)
24 | klass.superclass.should be(ActiveRecord::Base)
25 | end
26 | end
27 |
28 | context "in generate_empty_abstract_ar_class method" do
29 | it "should generate an abstract connection class" do
30 | klass = DbCharmer::ConnectionFactory.generate_empty_abstract_ar_class('::MyFooAbstractClass')
31 | klass.superclass.should be(ActiveRecord::Base)
32 | end
33 | end
34 |
35 | context "in establish_connection method" do
36 | it "should generate an abstract class" do
37 | klass = mock('AbstractClass')
38 | conn = mock('connection1')
39 | klass.stub!(:retrieve_connection).and_return(conn)
40 | DbCharmer::ConnectionFactory.should_receive(:generate_abstract_class).and_return(klass)
41 | DbCharmer::ConnectionFactory.establish_connection(:foo).should be(conn)
42 | end
43 |
44 | it "should create and return a connection proxy for the abstract class" do
45 | klass = mock('AbstractClass')
46 | DbCharmer::ConnectionFactory.should_receive(:generate_abstract_class).and_return(klass)
47 | DbCharmer::ConnectionProxy.should_receive(:new).with(klass, :foo)
48 | DbCharmer::ConnectionFactory.establish_connection(:foo)
49 | end
50 | end
51 |
52 | context "in establish_connection_to_db method" do
53 | it "should generate an abstract class" do
54 | klass = mock('AbstractClass')
55 | conn = mock('connection2')
56 | klass.stub!(:establish_connection)
57 | klass.stub!(:retrieve_connection).and_return(conn)
58 | DbCharmer::ConnectionFactory.should_receive(:generate_empty_abstract_ar_class).and_return(klass)
59 | DbCharmer::ConnectionFactory.establish_connection_to_db(:foo, :username => :foo).should be(conn)
60 | end
61 |
62 | it "should create and return a connection proxy for the abstract class" do
63 | klass = mock('AbstractClass')
64 | klass.stub!(:establish_connection)
65 | DbCharmer::ConnectionFactory.should_receive(:generate_empty_abstract_ar_class).and_return(klass)
66 | DbCharmer::ConnectionProxy.should_receive(:new).with(klass, :foo)
67 | DbCharmer::ConnectionFactory.establish_connection_to_db(:foo, :username => :foo)
68 | end
69 | end
70 |
71 | context "in connect method" do
72 | before do
73 | DbCharmer::ConnectionFactory.reset!
74 | end
75 |
76 | it "should return a connection proxy" do
77 | DbCharmer::ConnectionFactory.connect(:logs).should be_kind_of(ActiveRecord::ConnectionAdapters::AbstractAdapter)
78 | end
79 |
80 | # should_receive is evil on a singletone classes
81 | # it "should memoize proxies" do
82 | # conn = mock('connection3')
83 | # DbCharmer::ConnectionFactory.should_receive(:establish_connection).with('foo', false).once.and_return(conn)
84 | # DbCharmer::ConnectionFactory.connect(:foo)
85 | # DbCharmer::ConnectionFactory.connect(:foo)
86 | # end
87 | end
88 |
89 | context "in connect_to_db method" do
90 | before do
91 | DbCharmer::ConnectionFactory.reset!
92 | @conf = {
93 | :adapter => 'mysql',
94 | :username => "db_charmer_ro",
95 | :database => "db_charmer_sandbox_test",
96 | :connection_name => 'sanbox_ro'
97 | }
98 | end
99 |
100 | it "should return a connection proxy" do
101 | DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf).should be_kind_of(ActiveRecord::ConnectionAdapters::AbstractAdapter)
102 | end
103 |
104 | # should_receive is evil on a singletone classes
105 | # it "should memoize proxies" do
106 | # conn = mock('connection4')
107 | # DbCharmer::ConnectionFactory.should_receive(:establish_connection_to_db).with(@conf[:connection_name], @conf).once.and_return(conn)
108 | # DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf)
109 | # DbCharmer::ConnectionFactory.connect_to_db(@conf[:connection_name], @conf)
110 | # end
111 | end
112 |
113 | end
114 |
--------------------------------------------------------------------------------
/test-project/spec/unit/active_record/master_slave_routing_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "ActiveRecord slave-enabled models" do
4 | before do
5 | class User < ActiveRecord::Base
6 | db_magic :connection => :user_master, :slave => :slave01
7 | end
8 | end
9 |
10 | describe "in finder method" do
11 | [ :last, :first, :all ].each do |meth|
12 | describe meth do
13 | it "should go to the slave if called on the first level connection" do
14 | User.on_slave.connection.should_receive(:select_all).and_return([])
15 | User.send(meth)
16 | end
17 |
18 | it "should not change connection if called in an on_db block" do
19 | stub_columns_for_rails31 User.on_db(:logs).connection
20 | User.on_db(:logs).connection.should_receive(:select_all).and_return([])
21 | User.on_slave.connection.should_not_receive(:select_all)
22 | User.on_db(:logs).send(meth)
23 | end
24 |
25 | it "should not change connection when it's already been changed by on_slave call" do
26 | pending "rails3: not sure if we need this spec" if DbCharmer.rails3?
27 | User.on_slave do
28 | User.on_slave.connection.should_receive(:select_all).and_return([])
29 | User.should_not_receive(:on_db)
30 | User.send(meth)
31 | end
32 | end
33 |
34 | it "should not change connection if called in a transaction" do
35 | User.on_db(:user_master).connection.should_receive(:select_all).and_return([])
36 | User.on_slave.connection.should_not_receive(:select_all)
37 | User.transaction { User.send(meth) }
38 | end
39 | end
40 | end
41 |
42 | it "should go to the master if called find with :lock => true option" do
43 | User.on_db(:user_master).connection.should_receive(:select_all).and_return([])
44 | User.on_slave.connection.should_not_receive(:select_all)
45 | User.find(:first, :lock => true)
46 | end
47 |
48 | it "should not go to the master if no :lock => true option passed" do
49 | User.on_db(:user_master).connection.should_not_receive(:select_all)
50 | User.on_slave.connection.should_receive(:select_all).and_return([])
51 | User.find(:first)
52 | end
53 |
54 | it "should correctly pass all find params to the underlying code" do
55 | User.delete_all
56 | u1 = User.create(:login => 'foo')
57 | u2 = User.create(:login => 'bar')
58 |
59 | User.find(:all, :conditions => { :login => 'foo' }).should == [ u1 ]
60 | User.find(:all, :limit => 1).size.should == 1
61 | User.find(:first, :conditions => { :login => 'bar' }).should == u2
62 | end
63 | end
64 |
65 | describe "in calculation method" do
66 | [ :count, :minimum, :maximum, :average ].each do |meth|
67 | describe meth do
68 | it "should go to the slave if called on the first level connection" do
69 | User.on_slave.connection.should_receive(:select_value).and_return(1)
70 | User.send(meth, :id).should == 1
71 | end
72 |
73 | it "should not change connection if called in an on_db block" do
74 | User.on_db(:logs).connection.should_receive(:select_value).and_return(1)
75 | User.on_slave.connection.should_not_receive(:select_value)
76 | User.on_db(:logs).send(meth, :id).should == 1
77 | end
78 |
79 | it "should not change connection when it's already been changed by an on_slave call" do
80 | pending "rails3: not sure if we need this spec" if DbCharmer.rails3?
81 | User.on_slave do
82 | User.on_slave.connection.should_receive(:select_value).and_return(1)
83 | User.should_not_receive(:on_db)
84 | User.send(meth, :id).should == 1
85 | end
86 | end
87 |
88 | it "should not change connection if called in a transaction" do
89 | User.on_db(:user_master).connection.should_receive(:select_value).and_return(1)
90 | User.on_slave.connection.should_not_receive(:select_value)
91 | User.transaction { User.send(meth, :id).should == 1 }
92 | end
93 | end
94 | end
95 | end
96 |
97 | describe "in data manipulation methods" do
98 | it "should go to the master by default" do
99 | User.on_db(:user_master).connection.should_receive(:delete)
100 | User.delete_all
101 | end
102 |
103 | it "should go to the master even in slave-enabling chain calls" do
104 | User.on_db(:user_master).connection.should_receive(:delete)
105 | User.on_slave.delete_all
106 | end
107 |
108 | it "should go to the master even in slave-enabling block calls" do
109 | User.on_db(:user_master).connection.should_receive(:delete)
110 | User.on_slave { |u| u.delete_all }
111 | end
112 | end
113 |
114 | describe "in instance method" do
115 | describe "reload" do
116 | it "should always be done on the master" do
117 | User.delete_all
118 | u = User.create
119 |
120 | User.on_db(:user_master).connection.should_receive(:select_all).and_return([{}])
121 | User.on_slave.connection.should_not_receive(:select_all)
122 |
123 | User.on_slave { u.reload }
124 | end
125 | end
126 | end
127 | end
128 |
--------------------------------------------------------------------------------
/lib/db_charmer/active_record/migration/multi_db_migrations.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module Migration
4 | module MultiDbMigrations
5 |
6 | def self.append_features(base)
7 | return false if base < self
8 | super
9 | base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
10 |
11 | base.class_eval do
12 | if DbCharmer.rails31?
13 | alias_method_chain :migrate, :db_wrapper
14 | else
15 | class << self
16 | alias_method_chain :migrate, :db_wrapper
17 | end
18 | end
19 | end
20 | end
21 |
22 | module ClassMethods
23 | @@multi_db_names = {}
24 | def multi_db_names
25 | @@multi_db_names[self.name] || @@multi_db_names['ActiveRecord::Migration']
26 | end
27 |
28 | def multi_db_names=(names)
29 | @@multi_db_names[self.name] = names
30 | end
31 |
32 | unless DbCharmer.rails31?
33 | def migrate_with_db_wrapper(direction)
34 | if names = multi_db_names
35 | names.each do |multi_db_name|
36 | on_db(multi_db_name) do
37 | migrate_without_db_wrapper(direction)
38 | end
39 | end
40 | else
41 | migrate_without_db_wrapper(direction)
42 | end
43 | end
44 |
45 | def on_db(db_name)
46 | name = db_name.is_a?(Hash) ? db_name[:connection_name] : db_name.inspect
47 | announce "Switching connection to #{name}"
48 | # Switch connection
49 | old_proxy = ::ActiveRecord::Base.db_charmer_connection_proxy
50 | db_name = nil if db_name == :default
51 | ::ActiveRecord::Base.switch_connection_to(db_name, DbCharmer.connections_should_exist?)
52 | # Yield the block
53 | yield
54 | ensure
55 | # Switch it back
56 | ::ActiveRecord::Base.verify_active_connections!
57 | announce "Switching connection back"
58 | ::ActiveRecord::Base.switch_connection_to(old_proxy)
59 | end
60 | end
61 |
62 | def db_magic(opts = {})
63 | # Collect connections from all possible options
64 | conns = [ opts[:connection], opts[:connections] ]
65 | conns << shard_connections(opts[:sharded_connection]) if opts[:sharded_connection]
66 |
67 | # Get a unique set of connections
68 | conns = conns.flatten.compact.uniq
69 | raise ArgumentError, "No connection name - no magic!" unless conns.any?
70 |
71 | # Save connections
72 | self.multi_db_names = conns
73 | end
74 |
75 | # Return a list of connections to shards in a sharded connection
76 | def shard_connections(conn_name)
77 | conn = DbCharmer::Sharding.sharded_connection(conn_name)
78 | conn.shard_connections
79 | end
80 | end
81 |
82 | def migrate_with_db_wrapper(direction)
83 | if names = self.class.multi_db_names
84 | names.each do |multi_db_name|
85 | on_db(multi_db_name) do
86 | migrate_without_db_wrapper(direction)
87 | end
88 | end
89 | else
90 | migrate_without_db_wrapper(direction)
91 | end
92 | end
93 |
94 | def record_on_db(db_name, block)
95 | recorder = ::ActiveRecord::Migration::CommandRecorder.new(DbCharmer::ConnectionFactory.connect(db_name))
96 | old_recorder, @connection = @connection, recorder
97 | block.call
98 | old_recorder.record :on_db, [db_name, @connection]
99 | @connection = old_recorder
100 | end
101 |
102 | def replay_commands_on_db(name, commands)
103 | on_db(name) do
104 | commands.each do |cmd, args|
105 | send(cmd, *args)
106 | end
107 | end
108 | end
109 |
110 | def on_db(db_name, &block)
111 | if @connection.is_a?(::ActiveRecord::Migration::CommandRecorder)
112 | record_on_db(db_name, block)
113 | return
114 | end
115 |
116 | name = db_name.is_a?(Hash) ? db_name[:connection_name] : db_name.inspect
117 | announce "Switching connection to #{name}"
118 | # Switch connection
119 | old_connection, old_proxy = @connection, ::ActiveRecord::Base.db_charmer_connection_proxy
120 | db_name = nil if db_name == :default
121 | ::ActiveRecord::Base.switch_connection_to(db_name, DbCharmer.connections_should_exist?)
122 | # Yield the block
123 | ::ActiveRecord::Base.connection_pool.with_connection do |conn|
124 | @connection = conn
125 | yield
126 | end
127 | ensure
128 | @connection = old_connection
129 | # Switch it back
130 | ::ActiveRecord::Base.verify_active_connections!
131 | announce "Switching connection back"
132 | ::ActiveRecord::Base.switch_connection_to(old_proxy)
133 | end
134 | end
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/test-project/spec/unit/with_remapped_databases_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "DbCharmer#with_remapped_databases" do
4 | before(:all) do
5 | DbCharmer.connections_should_exist = false
6 | end
7 |
8 | let(:logs_connection) { DbCharmer::ConnectionFactory.connect(:logs) }
9 | let(:slave_connection) { DbCharmer::ConnectionFactory.connect(:slave01) }
10 | let(:master_connection) { Avatar.connection }
11 |
12 | before :each do
13 | class User < ActiveRecord::Base
14 | db_magic :connection => :slave01
15 | end
16 | end
17 |
18 | def should_have_connection(model_class, connection)
19 | model_class.connection.object_id.should == connection.object_id
20 | end
21 |
22 | it "should remap the right connection" do
23 | should_have_connection(LogRecord, logs_connection)
24 | DbCharmer.with_remapped_databases(:logs => :slave01) do
25 | should_have_connection(LogRecord, slave_connection)
26 | end
27 | should_have_connection(LogRecord, logs_connection)
28 | end
29 |
30 | it "should not remap other connections" do
31 | should_have_connection(Avatar, master_connection)
32 | should_have_connection(User, slave_connection)
33 | DbCharmer.with_remapped_databases(:logs => :slave01) do
34 | should_have_connection(Avatar, master_connection)
35 | should_have_connection(User, slave_connection)
36 | end
37 | should_have_connection(Avatar, master_connection)
38 | should_have_connection(User, slave_connection)
39 | end
40 |
41 | it "should allow remapping multiple databases" do
42 | should_have_connection(Avatar, master_connection)
43 | should_have_connection(LogRecord, logs_connection)
44 | DbCharmer.with_remapped_databases(:master => :logs, :logs => :slave01) do
45 | should_have_connection(Avatar, logs_connection)
46 | should_have_connection(LogRecord, slave_connection)
47 | end
48 | should_have_connection(Avatar, master_connection)
49 | should_have_connection(LogRecord, logs_connection)
50 | end
51 |
52 | it "should remap the master connection when asked to, but not other connections" do
53 | should_have_connection(Avatar, master_connection)
54 | should_have_connection(User, slave_connection)
55 | should_have_connection(LogRecord, logs_connection)
56 | DbCharmer.with_remapped_databases(:master => :slave01) do
57 | should_have_connection(Avatar, slave_connection)
58 | should_have_connection(User, slave_connection)
59 | should_have_connection(LogRecord, logs_connection)
60 | end
61 | should_have_connection(Avatar, master_connection)
62 | should_have_connection(User, slave_connection)
63 | should_have_connection(LogRecord, logs_connection)
64 | end
65 |
66 | it "should not override connections that are explicitly specified" do
67 | DbCharmer.with_remapped_databases(:logs => :slave01) do
68 | should_have_connection(LogRecord, slave_connection)
69 | should_have_connection(LogRecord.on_db(:master), master_connection)
70 | LogRecord.on_db(:master) do
71 | should_have_connection(LogRecord, master_connection)
72 | end
73 | should_have_connection(LogRecord.on_db(:logs), logs_connection)
74 | LogRecord.on_db(:logs) do
75 | should_have_connection(LogRecord, logs_connection)
76 | end
77 | should_have_connection(LogRecord, slave_connection)
78 | end
79 | end
80 |
81 | it "should successfully run selects on the right database" do
82 | # We need this call to make sure rails would fetch columns info from the logs server before we mess its connection up
83 | LogRecord.all
84 |
85 | # Remap LogRecord connection to slave01 and make sure selects would go there (even though we do not have the table there)
86 | DbCharmer.with_remapped_databases(:logs => :slave01) do
87 | logs_connection.should_not_receive(:select_all)
88 | slave_connection.should_receive(:select_all).and_return([])
89 | stub_columns_for_rails31 slave_connection
90 | LogRecord.all.should be_empty
91 | end
92 | end
93 |
94 | def unhijack!(klass)
95 | if klass.respond_to?(:connection_with_magic)
96 | klass.class_eval <<-END
97 | class << self
98 | undef_method(:connection_with_magic)
99 | alias_method(:connection, :connection_without_magic)
100 | undef_method(:connection_without_magic)
101 |
102 | undef_method(:connection_pool_with_magic)
103 | alias_method(:connection_pool, :connection_pool_without_magic)
104 | undef_method(:connection_pool_without_magic)
105 | end
106 | END
107 | end
108 |
109 | raise "Unable to unhijack #{klass.name}" if klass.respond_to?(:connection_with_magic)
110 | end
111 |
112 | it "should hijack connections only when necessary" do
113 | unhijack!(Category)
114 |
115 | Category.respond_to?(:connection_with_magic).should be_false
116 | DbCharmer.with_remapped_databases(:logs => :slave01) do
117 | Category.respond_to?(:connection_with_magic).should be_false
118 | end
119 | Category.respond_to?(:connection_with_magic).should be_false
120 |
121 | DbCharmer.with_remapped_databases(:master => :slave01) do
122 | Category.respond_to?(:connection_with_magic).should be_true
123 | should_have_connection(Category, slave_connection)
124 | end
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/test-project/spec/unit/multi_db_proxy_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe "ActiveRecord model with db_magic" do
4 | before do
5 | class Blah < ActiveRecord::Base
6 | self.table_name = :posts
7 | db_magic :connection => nil
8 | end
9 | end
10 |
11 | describe "(instance)" do
12 | before do
13 | @blah = Blah.new
14 | end
15 |
16 | describe "in on_db method" do
17 | describe "with a block" do
18 | it "should switch connection to specified one and yield the block" do
19 | Blah.db_charmer_connection_proxy.should be_nil
20 | @blah.on_db(:logs) do
21 | Blah.db_charmer_connection_proxy.should_not be_nil
22 | end
23 | end
24 |
25 | it "should switch connection back after the block finished its work" do
26 | Blah.db_charmer_connection_proxy.should be_nil
27 | @blah.on_db(:logs) {}
28 | Blah.db_charmer_connection_proxy.should be_nil
29 | end
30 |
31 | it "should manage connection level values" do
32 | Blah.db_charmer_connection_level.should == 0
33 | @blah.on_db(:logs) do |m|
34 | m.class.db_charmer_connection_level.should == 1
35 | end
36 | Blah.db_charmer_connection_level.should == 0
37 | end
38 | end
39 |
40 | describe "as a chain call" do
41 | it "should switch connection for all chained calls" do
42 | Blah.db_charmer_connection_proxy.should be_nil
43 | @blah.on_db(:logs).should_not be_nil
44 | end
45 |
46 | it "should switch connection for non-chained calls" do
47 | Blah.db_charmer_connection_proxy.should be_nil
48 | @blah.on_db(:logs).to_s
49 | Blah.db_charmer_connection_proxy.should be_nil
50 | end
51 |
52 | it "should restore connection" do
53 | User.first
54 | User.connection.object_id.should == User.on_master.connection.object_id
55 |
56 | User.on_db(:slave01).first
57 | User.connection.object_id.should == User.on_master.connection.object_id
58 | end
59 |
60 | it "should restore connection after error" do
61 | pending "Disabled in RSpec prior to version 2 because of lack of .any_instance support" unless Object.respond_to?(:any_instance)
62 |
63 | User.on_db(:slave01).first
64 | User.first
65 | ActiveRecord::Base.connection_handler.clear_all_connections!
66 | ActiveRecord::ConnectionAdapters::MysqlAdapter.any_instance.stub(:connect) { raise Mysql::Error, 'Connection error' }
67 | expect { User.on_db(:slave01).first }.to raise_error(Mysql::Error)
68 | ActiveRecord::ConnectionAdapters::MysqlAdapter.any_instance.unstub(:connect)
69 | User.connection.connection_name.should == User.on_master.connection.connection_name
70 | end
71 | end
72 | end
73 | end
74 |
75 | describe "(class)" do
76 | describe "in on_db method" do
77 | describe "with a block" do
78 | it "should switch connection to specified one and yield the block" do
79 | Blah.db_charmer_connection_proxy.should be_nil
80 | Blah.on_db(:logs) do
81 | Blah.db_charmer_connection_proxy.should_not be_nil
82 | end
83 | end
84 |
85 | it "should switch connection back after the block finished its work" do
86 | Blah.db_charmer_connection_proxy.should be_nil
87 | Blah.on_db(:logs) {}
88 | Blah.db_charmer_connection_proxy.should be_nil
89 | end
90 |
91 | it "should manage connection level values" do
92 | Blah.db_charmer_connection_level.should == 0
93 | Blah.on_db(:logs) do |m|
94 | m.db_charmer_connection_level.should == 1
95 | end
96 | Blah.db_charmer_connection_level.should == 0
97 | end
98 | end
99 |
100 | describe "as a chain call" do
101 | it "should switch connection for all chained calls" do
102 | Blah.db_charmer_connection_proxy.should be_nil
103 | Blah.on_db(:logs).should_not be_nil
104 | end
105 |
106 | it "should switch connection for non-chained calls" do
107 | Blah.db_charmer_connection_proxy.should be_nil
108 | Blah.on_db(:logs).to_s
109 | Blah.db_charmer_connection_proxy.should be_nil
110 | end
111 | end
112 | end
113 |
114 | describe "in on_slave method" do
115 | before do
116 | Blah.db_magic :slaves => [ :slave01 ]
117 | end
118 |
119 | it "should use one tof the model's slaves if no slave given" do
120 | Blah.on_slave.db_charmer_connection_proxy.object_id.should == Blah.coerce_to_connection_proxy(:slave01).object_id
121 | end
122 |
123 | it "should use given slave" do
124 | Blah.on_slave(:logs).db_charmer_connection_proxy.object_id.should == Blah.coerce_to_connection_proxy(:logs).object_id
125 | end
126 |
127 | it 'should support block calls' do
128 | Blah.on_slave do |m|
129 | m.db_charmer_connection_proxy.object_id.should == Blah.coerce_to_connection_proxy(:slave01).object_id
130 | end
131 | end
132 | end
133 |
134 | describe "in on_master method" do
135 | before do
136 | Blah.db_magic :slaves => [ :slave01 ]
137 | end
138 |
139 | it "should run queries on the master" do
140 | Blah.on_master.db_charmer_connection_proxy.should be_nil
141 | end
142 | end
143 | end
144 | end
145 |
--------------------------------------------------------------------------------
/lib/db_charmer/rails3/active_record/relation/connection_routing.rb:
--------------------------------------------------------------------------------
1 | module DbCharmer
2 | module ActiveRecord
3 | module Relation
4 | module ConnectionRouting
5 |
6 | # All the methods that could be querying the database
7 | SLAVE_METHODS = [ :calculate, :exists? ]
8 | MASTER_METHODS = [ :delete, :delete_all, :destroy, :destroy_all, :reload, :update, :update_all ]
9 | ALL_METHODS = SLAVE_METHODS + MASTER_METHODS
10 |
11 | DB_CHARMER_ATTRIBUTES = [ :db_charmer_connection, :db_charmer_connection_is_forced, :db_charmer_enable_slaves ]
12 |
13 | # Define the default relation connection + override all the query methods here
14 | def self.included(base)
15 | init_attributes(base)
16 | init_routing(base)
17 | end
18 |
19 | # Define our attributes + spawn methods shit needs to be changed to make sure our accessors are copied over to the new instances
20 | def self.init_attributes(base)
21 | DB_CHARMER_ATTRIBUTES.each do |attr|
22 | base.send(:attr_accessor, attr)
23 | end
24 |
25 | # Override spawn methods
26 | base.alias_method_chain :except, :db_charmer
27 | base.alias_method_chain :only, :db_charmer
28 | end
29 |
30 | # Override all query methods
31 | def self.init_routing(base)
32 | ALL_METHODS.each do |meth|
33 | base.alias_method_chain meth, :db_charmer
34 | end
35 |
36 | # Special case: for normal selects we go to the slave, but for selects with a lock we should use master
37 | base.alias_method_chain :to_a, :db_charmer
38 | end
39 |
40 | # Copy db_charmer attributes in addition to what they're copying
41 | def except_with_db_charmer(*args)
42 | except_without_db_charmer(*args).tap do |result|
43 | copy_db_charmer_options(self, result)
44 | end
45 | end
46 |
47 | # Copy db_charmer attributes in addition to what they're copying
48 | def only_with_db_charmer(*args)
49 | only_without_db_charmer(*args).tap do |result|
50 | copy_db_charmer_options(self, result)
51 | end
52 | end
53 |
54 | # Copy our accessors from one instance to another
55 | def copy_db_charmer_options(src, dst)
56 | DB_CHARMER_ATTRIBUTES.each do |attr|
57 | dst.send("#{attr}=".to_sym, src.send(attr))
58 | end
59 | end
60 |
61 | # Connection switching (changes the default relation connection)
62 | def on_db(con, &block)
63 | if block_given?
64 | @klass.on_db(con, &block)
65 | else
66 | clone.tap do |result|
67 | result.db_charmer_connection = con
68 | result.db_charmer_connection_is_forced = true
69 | end
70 | end
71 | end
72 |
73 | # Make sure we get the right connection here
74 | def connection
75 | @klass.on_db(db_charmer_connection).connection
76 | end
77 |
78 | # Selects preferred destination (master/slave/default) for a query
79 | def select_destination(method, recommendation = :default)
80 | # If this relation was created within a forced connection block (e.g Model.on_db(:foo).relation)
81 | # Then we should use that connection everywhere except cases when a model is slave-enabled
82 | # in those cases DML queries go to the master
83 | if db_charmer_connection_is_forced
84 | return :master if db_charmer_enable_slaves && MASTER_METHODS.member?(method)
85 | return :default
86 | end
87 |
88 | # If this relation is created from a slave-enabled model, let's do the routing if possible
89 | if db_charmer_enable_slaves
90 | return :slave if SLAVE_METHODS.member?(method)
91 | return :master if MASTER_METHODS.member?(method)
92 | else
93 | # Make sure we do not use recommended destination
94 | recommendation = :default
95 | end
96 |
97 | # If nothing else came up, let's use the default or recommended connection
98 | return recommendation
99 | end
100 |
101 | # Switch the model to default relation connection
102 | def switch_connection_for_method(method, recommendation = nil)
103 | # Choose where to send the query
104 | destination ||= select_destination(method, recommendation)
105 |
106 | # What method to use
107 | on_db_method = [ :on_db, db_charmer_connection ]
108 | on_db_method = :on_master if destination == :master
109 | on_db_method = :first_level_on_slave if destination == :slave
110 |
111 | # Perform the query
112 | @klass.send(*on_db_method) do
113 | yield
114 | end
115 | end
116 |
117 | # For normal selects we go to the slave, but for selects with a lock we should use master
118 | def to_a_with_db_charmer(*args, &block)
119 | preferred_destination = :slave
120 | preferred_destination = :master if lock_value
121 |
122 | switch_connection_for_method(:to_a, preferred_destination) do
123 | to_a_without_db_charmer(*args, &block)
124 | end
125 | end
126 |
127 | # Need this to mimick alias_method_chain name generation (exists? => exists_with_db_charmer?)
128 | def self.aliased_method_name(target, with)
129 | aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
130 | "#{aliased_target}_#{with}_db_charmer#{punctuation}"
131 | end
132 |
133 | # Override all the query methods here
134 | ALL_METHODS.each do |method|
135 | class_eval <<-EOF, __FILE__, __LINE__ + 1
136 | def #{aliased_method_name method, :with}(*args, &block)
137 | switch_connection_for_method(:#{method.to_s}) do
138 | #{aliased_method_name method, :without}(*args, &block)
139 | end
140 | end
141 | EOF
142 | end
143 |
144 | end
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/lib/db_charmer/sharding/method/db_block_map.rb:
--------------------------------------------------------------------------------
1 | # This is a more sophisticated sharding method based on a database-backed
2 | # blocks map that holds block-shard associations. It automatically
3 | # creates new blocks for new keys and assigns them to shards.
4 | #
5 | module DbCharmer
6 | module Sharding
7 | module Method
8 | class DbBlockMap
9 | # Sharder name
10 | attr_accessor :name
11 |
12 | # Mapping db connection
13 | attr_accessor :connection, :connection_name
14 |
15 | # Mapping table name
16 | attr_accessor :map_table
17 |
18 | # Shards table name
19 | attr_accessor :shards_table
20 |
21 | # Sharding keys block size
22 | attr_accessor :block_size
23 |
24 | def initialize(config)
25 | @name = config[:name] or raise(ArgumentError, "Missing required :name parameter!")
26 | @connection = DbCharmer::ConnectionFactory.connect(config[:connection], true)
27 | @block_size = (config[:block_size] || 10000).to_i
28 |
29 | @map_table = config[:map_table] or raise(ArgumentError, "Missing required :map_table parameter!")
30 | @shards_table = config[:shards_table] or raise(ArgumentError, "Missing required :shards_table parameter!")
31 |
32 | # Local caches
33 | @shard_info_cache = {}
34 |
35 | @blocks_cache = Rails.cache
36 | @blocks_cache_prefix = config[:blocks_cache_prefix] || "#{@name}_block:"
37 | end
38 |
39 | def shard_for_key(key)
40 | block = block_for_key(key)
41 |
42 | begin
43 | # Auto-allocate new blocks
44 | block ||= allocate_new_block_for_key(key)
45 | rescue ::ActiveRecord::StatementInvalid => e
46 | raise unless e.message.include?('Duplicate entry')
47 | block = block_for_key(key)
48 | end
49 |
50 | raise ArgumentError, "Invalid key value, no shards found for this key and could not create a new block!" unless block
51 |
52 | # Bail if no shard found
53 | shard_id = block['shard_id'].to_i
54 | shard_info = shard_info_by_id(shard_id)
55 | raise ArgumentError, "Invalid shard_id: #{shard_id}" unless shard_info
56 |
57 | # Get config
58 | shard_connection_config(shard_info)
59 | end
60 |
61 | class ShardInfo < ::ActiveRecord::Base
62 | validates_presence_of :db_host
63 | validates_presence_of :db_port
64 | validates_presence_of :db_user
65 | validates_presence_of :db_pass
66 | validates_presence_of :db_name
67 | end
68 |
69 | # Returns a block for a key
70 | def block_for_key(key, cache = true)
71 | # Cleanup the cache if asked to
72 | key_range = [ block_start_for_key(key), block_end_for_key(key) ]
73 | block_cache_key = "%d-%d" % key_range
74 |
75 | if cache
76 | cached_block = get_cached_block(block_cache_key)
77 | return cached_block if cached_block
78 | end
79 |
80 | # Fetch cached value or load from db
81 | block = begin
82 | sql = "SELECT * FROM #{map_table} WHERE start_id = #{key_range.first} AND end_id = #{key_range.last} LIMIT 1"
83 | connection.select_one(sql, 'Find a shard block')
84 | end
85 |
86 | set_cached_block(block_cache_key, block)
87 |
88 | return block
89 | end
90 |
91 | def get_cached_block(block_cache_key)
92 | @blocks_cache.read("#{@blocks_cache_prefix}#{block_cache_key}")
93 | end
94 |
95 | def set_cached_block(block_cache_key, block)
96 | @blocks_cache.write("#{@blocks_cache_prefix}#{block_cache_key}", block)
97 | end
98 |
99 | # Load shard info
100 | def shard_info_by_id(shard_id, cache = true)
101 | # Cleanup the cache if asked to
102 | @shard_info_cache[shard_id] = nil unless cache
103 |
104 | # Either load from cache or from db
105 | @shard_info_cache[shard_id] ||= begin
106 | prepare_shard_model
107 | ShardInfo.find_by_id(shard_id)
108 | end
109 | end
110 |
111 | def allocate_new_block_for_key(key)
112 | # Can't find any shards to use for blocks allocation!
113 | return nil unless shard = least_loaded_shard
114 |
115 | # Figure out block limits
116 | start_id = block_start_for_key(key)
117 | end_id = block_end_for_key(key)
118 |
119 | # Try to insert a new mapping (ignore duplicate key errors)
120 | sql = <<-SQL
121 | INSERT INTO #{map_table}
122 | SET start_id = #{start_id},
123 | end_id = #{end_id},
124 | shard_id = #{shard.id},
125 | block_size = #{block_size},
126 | created_at = NOW(),
127 | updated_at = NOW()
128 | SQL
129 | connection.execute(sql, "Allocate new block")
130 |
131 | # Increment the blocks counter on the shard
132 | ShardInfo.update_counters(shard.id, :blocks_count => +1)
133 |
134 | # Retry block search after creation
135 | block_for_key(key)
136 | end
137 |
138 | def least_loaded_shard
139 | prepare_shard_model
140 |
141 | # Select shard
142 | shard = ShardInfo.all(:conditions => { :enabled => true, :open => true }, :order => 'blocks_count ASC', :limit => 1).first
143 | raise "Can't find any shards to use for blocks allocation!" unless shard
144 | return shard
145 | end
146 |
147 | def block_start_for_key(key)
148 | block_size.to_i * (key.to_i / block_size.to_i)
149 | end
150 |
151 | def block_end_for_key(key)
152 | block_size.to_i + block_start_for_key(key)
153 | end
154 |
155 | # Create configuration (use mapping connection as a template)
156 | def shard_connection_config(shard)
157 | # Format connection name
158 | shard_name = "db_charmer_db_block_map_#{name}_shard_%05d" % shard.id
159 |
160 | # Here we get the mapping connection's configuration
161 | # They do not expose configs so we hack in and get the instance var
162 | # FIXME: Find a better way, maybe move config method to our ar extenstions
163 | connection.instance_variable_get(:@config).clone.merge(
164 | # Name for the connection factory
165 | :connection_name => shard_name,
166 | # Connection params
167 | :host => shard.db_host,
168 | :port => shard.db_port,
169 | :username => shard.db_user,
170 | :password => shard.db_pass,
171 | :database => shard.db_name
172 | )
173 | end
174 |
175 | def create_shard(params)
176 | params = params.symbolize_keys
177 | [ :db_host, :db_port, :db_user, :db_pass, :db_name ].each do |arg|
178 | raise ArgumentError, "Missing required parameter: #{arg}" unless params[arg]
179 | end
180 |
181 | # Prepare model
182 | prepare_shard_model
183 |
184 | # Create the record
185 | ShardInfo.create! do |shard|
186 | shard.db_host = params[:db_host]
187 | shard.db_port = params[:db_port]
188 | shard.db_user = params[:db_user]
189 | shard.db_pass = params[:db_pass]
190 | shard.db_name = params[:db_name]
191 | end
192 | end
193 |
194 | def shard_connections
195 | # Find all shards
196 | prepare_shard_model
197 | shards = ShardInfo.all(:conditions => { :enabled => true })
198 | # Map them to connections
199 | shards.map { |shard| shard_connection_config(shard) }
200 | end
201 |
202 | # Prepare model for working with our shards table
203 | def prepare_shard_model
204 | ShardInfo.table_name = shards_table
205 | ShardInfo.switch_connection_to(connection)
206 | end
207 |
208 | end
209 | end
210 | end
211 | end
212 |
--------------------------------------------------------------------------------