activerecord-slave 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +56 -0
- data/.ruby-version +1 -0
- data/.travis.yml +31 -0
- data/Gemfile +4 -0
- data/Guardfile +46 -0
- data/LICENSE.txt +22 -0
- data/README.md +141 -0
- data/Rakefile +6 -0
- data/activerecord-slave.gemspec +40 -0
- data/lib/active_record/slave/config.rb +20 -0
- data/lib/active_record/slave/database_tasks.rb +30 -0
- data/lib/active_record/slave/model.rb +44 -0
- data/lib/active_record/slave/railtie.rb +9 -0
- data/lib/active_record/slave/replication_config.rb +28 -0
- data/lib/active_record/slave/replication_router.rb +21 -0
- data/lib/active_record/slave/version.rb +5 -0
- data/lib/activerecord-slave.rb +26 -0
- data/lib/tasks/activerecord-slave.rake +13 -0
- data/spec/active_record/slave/config_spec.rb +17 -0
- data/spec/active_record/slave/database_tasks_spec.rb +21 -0
- data/spec/active_record/slave/model_spec.rb +98 -0
- data/spec/active_record/slave/replication_config_spec.rb +46 -0
- data/spec/active_record/slave/replication_router_spec.rb +41 -0
- data/spec/active_record/slave/version_spec.rb +5 -0
- data/spec/models.rb +49 -0
- data/spec/schema.rb +26 -0
- data/spec/spec_helper.rb +71 -0
- data/spec/tasks/activerecord-slave_spec.rb +48 -0
- metadata +294 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Slave
|
5
|
+
module Model
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
private_class_method :define_slave_class
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def use_slave(replication_name)
|
14
|
+
replication_config = ActiveRecord::Slave.config.fetch_replication_config replication_name
|
15
|
+
@replication_router = ActiveRecord::Slave::ReplicationRouter.new replication_config
|
16
|
+
|
17
|
+
establish_connection replication_config.master_connection_name
|
18
|
+
|
19
|
+
@slave_class_repository = {}
|
20
|
+
base_class = self
|
21
|
+
replication_config.slave_connection_names.keys.each do |connection_name|
|
22
|
+
@slave_class_repository[connection_name] = define_slave_class base_class, connection_name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def slave_for
|
27
|
+
@slave_class_repository.fetch @replication_router.slave_connection_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def define_slave_class(base_class, connection_name)
|
31
|
+
model = Class.new(base_class) do
|
32
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
33
|
+
def self.name
|
34
|
+
"#{base_class.name}::Slave::#{connection_name}"
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
end
|
38
|
+
model.class_eval { establish_connection(connection_name) }
|
39
|
+
model
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Slave
|
3
|
+
class ReplicationConfig
|
4
|
+
attr_reader :name, :master_connection_name
|
5
|
+
|
6
|
+
def initialize(replication_name)
|
7
|
+
@name = replication_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def validate_config!
|
11
|
+
fail "Nothing register master connection." unless @master_connection_name
|
12
|
+
end
|
13
|
+
|
14
|
+
def register_master(connection_name)
|
15
|
+
@master_connection_name = connection_name
|
16
|
+
end
|
17
|
+
|
18
|
+
def register_slave(connection_name, weight)
|
19
|
+
@slave_connection_registory ||= {}
|
20
|
+
@slave_connection_registory.store connection_name, weight
|
21
|
+
end
|
22
|
+
|
23
|
+
def slave_connection_names
|
24
|
+
@slave_connection_registory
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "pickup"
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Slave
|
5
|
+
class ReplicationRouter
|
6
|
+
def initialize(replication_config)
|
7
|
+
fail "Not ActiveRecord::Slave::ReplicationConfig object." unless replication_config.is_a? ActiveRecord::Slave::ReplicationConfig
|
8
|
+
@replication_config = replication_config
|
9
|
+
end
|
10
|
+
|
11
|
+
def master_connection_name
|
12
|
+
@replication_config.master_connection_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def slave_connection_name
|
16
|
+
slaves = Pickup.new(@replication_config.slave_connection_names)
|
17
|
+
slaves.pick(1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "active_support/lazy_load_hooks"
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
require "active_record/slave/version"
|
6
|
+
require "active_record/slave/config"
|
7
|
+
require "active_record/slave/replication_config"
|
8
|
+
require "active_record/slave/replication_router"
|
9
|
+
require "active_record/slave/model"
|
10
|
+
require "active_record/slave/database_tasks"
|
11
|
+
|
12
|
+
module ActiveRecord
|
13
|
+
module Slave
|
14
|
+
class << self
|
15
|
+
def config
|
16
|
+
@config ||= Config.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def configure(&block)
|
20
|
+
config.instance_eval(&block)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require "active_record/slave/railtie" if defined? Rails
|
@@ -0,0 +1,13 @@
|
|
1
|
+
namespace :active_record do
|
2
|
+
namespace :slave do
|
3
|
+
desc "Create database for replicaition master"
|
4
|
+
task :db_create, %i(replicaition) => %i(environment) do |_, args|
|
5
|
+
ActiveRecord::Slave::DatabaseTasks.create_database args
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Drop database for replicaition master"
|
9
|
+
task :db_drop, %i(replicaition) => %i(environment) do |_, args|
|
10
|
+
ActiveRecord::Slave::DatabaseTasks.drop_database args
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
describe ActiveRecord::Slave::Config do
|
2
|
+
let!(:config) do
|
3
|
+
config = ActiveRecord::Slave::Config.new
|
4
|
+
config.define_replication(:test) do |replication|
|
5
|
+
replication.register_master :test_master
|
6
|
+
replication.register_slave :test_slave_001, 70
|
7
|
+
replication.register_slave :test_slave_002, 30
|
8
|
+
end
|
9
|
+
config
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#fetch_replication_config" do
|
13
|
+
it "returns :test replication config" do
|
14
|
+
expect(config.fetch_replication_config :test).to be_a ActiveRecord::Slave::ReplicationConfig
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
describe ActiveRecord::Slave::DatabaseTasks do
|
2
|
+
let(:args) do
|
3
|
+
{ replicaition: "task" }
|
4
|
+
end
|
5
|
+
|
6
|
+
after(:each) do
|
7
|
+
ActiveRecord::Base.establish_connection(:test)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "Create database" do
|
11
|
+
ActiveRecord::Slave::DatabaseTasks.create_database args
|
12
|
+
expect(ActiveRecord::Base.connection.execute("SHOW DATABASES").include? ["test_task"]).to be true
|
13
|
+
end
|
14
|
+
|
15
|
+
context "Created task replicaition database" do
|
16
|
+
it "Drop database" do
|
17
|
+
ActiveRecord::Slave::DatabaseTasks.drop_database args
|
18
|
+
expect(ActiveRecord::Base.connection.execute("SHOW DATABASES").include? ["test_task"]).to be false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
describe ActiveRecord::Slave::Model do
|
2
|
+
it "connect to master" do
|
3
|
+
expect(User.connection.pool.spec.config[:port]).to eq 21891
|
4
|
+
end
|
5
|
+
|
6
|
+
it "connect to slaves", retry: 3 do
|
7
|
+
slave_ports = 10.times.map { User.slave_for.connection.pool.spec.config[:port] }.uniq
|
8
|
+
expect(slave_ports.count).to eq 2
|
9
|
+
expect(slave_ports).to include 21892
|
10
|
+
expect(slave_ports).to include 21893
|
11
|
+
end
|
12
|
+
|
13
|
+
it "connect to default" do
|
14
|
+
expect(Item.connection.pool.spec.config[:port]).to eq 3306
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "Write to master, Read from slave" do
|
18
|
+
it "returns user object from slave database" do
|
19
|
+
user_from_master = User.create name: "alice"
|
20
|
+
|
21
|
+
user_from_slave = User.slave_for.find user_from_master.id
|
22
|
+
|
23
|
+
expect(user_from_master.id).to eq user_from_slave.id
|
24
|
+
expect(user_from_master.name).to eq user_from_slave.name
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "Assosiations" do
|
29
|
+
let(:user) { User.create name: "alice" }
|
30
|
+
|
31
|
+
context "has_many object" do
|
32
|
+
context "in default database" do
|
33
|
+
let(:item) { Item.create name: "foo", count: 1, user: user }
|
34
|
+
|
35
|
+
it "returns user object, belongs_to" do
|
36
|
+
expect(item.user).to be_a User
|
37
|
+
expect(item.user.id).to eq user.id
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "model connect to default database" do
|
41
|
+
it "returns has_many objects" do
|
42
|
+
expect { item }.to change { user.items.count }.from(0).to(1)
|
43
|
+
expect { Item.create name: "bar", count: 1, user: user }.to change { user.items.count }.from(1).to(2)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns replication model object" do
|
47
|
+
expect(Item.find(item.id).user).to eq user
|
48
|
+
end
|
49
|
+
|
50
|
+
it "returns default database service port" do
|
51
|
+
expect(Item.connection.pool.spec.config[:port]).to eq 3306
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "in replication database" do
|
57
|
+
let(:skill) { Skill.create name: "bar", user: user }
|
58
|
+
|
59
|
+
it "returns user object, belongs_to" do
|
60
|
+
expect(skill.user).to be_a User
|
61
|
+
expect(skill.user.id).to eq user.id
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "model connect to raplication databases" do
|
65
|
+
it "returns has_many objects" do
|
66
|
+
expect { skill }.to change { user.skills.count }.from(0).to(1)
|
67
|
+
expect { Skill.create name: "foobar", user: user }.to change { user.skills.count }.from(1).to(2)
|
68
|
+
expect(user.skills.count).to eq Skill.slave_for.where(user_id: user.id).count
|
69
|
+
end
|
70
|
+
|
71
|
+
it "returns other model object from master" do
|
72
|
+
expect(Skill.find(skill.id).user).to eq user
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns other model object from slaves" do
|
76
|
+
expect(Skill.slave_for.find(skill.id).user).to eq user
|
77
|
+
end
|
78
|
+
|
79
|
+
it "returns master database service port" do
|
80
|
+
expect(Skill.connection.pool.spec.config[:port]).to eq 21891
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns slave database service port" do
|
84
|
+
expect(Skill.slave_for.connection.pool.spec.config[:port]).to eq(21892) | eq(21893)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "Enable DatabaseRewinder, delete records at each after specs" do
|
91
|
+
it "No records in DataBases" do
|
92
|
+
expect(User.all.count).to eq 0
|
93
|
+
expect(Item.all.count).to eq 0
|
94
|
+
expect(Skill.all.count).to eq 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
describe ActiveRecord::Slave::ReplicationConfig do
|
2
|
+
let!(:replication_config) do
|
3
|
+
config = ActiveRecord::Slave::ReplicationConfig.new :test_replication
|
4
|
+
|
5
|
+
config.register_master :test_master
|
6
|
+
|
7
|
+
config.register_slave :test_slave_001, 70
|
8
|
+
config.register_slave :test_slave_002, 30
|
9
|
+
|
10
|
+
config
|
11
|
+
end
|
12
|
+
|
13
|
+
context "not setup config for ReplicationConfig object" do
|
14
|
+
context "#validate_config!" do
|
15
|
+
let!(:not_setup_replication_config) { ActiveRecord::Slave::ReplicationConfig.new :test_replication }
|
16
|
+
|
17
|
+
it "returns raise, nothing register master connection" do
|
18
|
+
expect { not_setup_replication_config.validate_config! }.to raise_error "Nothing register master connection."
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns nil, config is valid" do
|
22
|
+
expect(replication_config.validate_config!).to be nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "#name" do
|
28
|
+
it "returns replication name" do
|
29
|
+
expect(replication_config.name).to eq :test_replication
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context "#master_connection_name" do
|
34
|
+
it "returns master database connection name" do
|
35
|
+
expect(replication_config.master_connection_name).to eq :test_master
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "#slave_connection_names" do
|
40
|
+
it "returns slave database connection names" do
|
41
|
+
expect(replication_config.slave_connection_names).to be_a Hash
|
42
|
+
expect(replication_config.slave_connection_names).to include test_slave_001: 70
|
43
|
+
expect(replication_config.slave_connection_names).to include test_slave_002: 30
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
describe ActiveRecord::Slave::ReplicationRouter do
|
2
|
+
let!(:replication_config) do
|
3
|
+
config = ActiveRecord::Slave::Config.new
|
4
|
+
config.define_replication(:test) do |replication|
|
5
|
+
replication.register_master :test_master
|
6
|
+
replication.register_slave :test_slave_001, 70
|
7
|
+
replication.register_slave :test_slave_002, 30
|
8
|
+
end
|
9
|
+
config.fetch_replication_config :test
|
10
|
+
end
|
11
|
+
|
12
|
+
let!(:replication_router) do
|
13
|
+
ActiveRecord::Slave::ReplicationRouter.new replication_config
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#new" do
|
17
|
+
it "Make instance object" do
|
18
|
+
expect(ActiveRecord::Slave::ReplicationRouter.new replication_config).to be_a ActiveRecord::Slave::ReplicationRouter
|
19
|
+
end
|
20
|
+
|
21
|
+
it "Raise error" do
|
22
|
+
expect { ActiveRecord::Slave::ReplicationRouter.new "test" }.to raise_error "Not ActiveRecord::Slave::ReplicationConfig object."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "#master_connection_name" do
|
27
|
+
it "returns master database conneciton name" do
|
28
|
+
expect(replication_router.master_connection_name).to eq :test_master
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#slave_connection_name" do
|
33
|
+
it "returns slave database conneciton name, by wait lottery confidence 99%", retry: 3 do
|
34
|
+
slave_connection_names = 10000.times.map { replication_router.slave_connection_name }
|
35
|
+
expect(slave_connection_names.count(:test_slave_001)).to be_between 7000 - 60, 7000 + 60
|
36
|
+
expect(slave_connection_names.count(:test_slave_002)).to be_between 3000 - 60, 3000 + 60
|
37
|
+
expect(slave_connection_names).to include :test_slave_001
|
38
|
+
expect(slave_connection_names).to include :test_slave_002
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
base = { "adapter" => "mysql2", "encoding" => "utf8", "pool" => 5, "username" => "root", "password" => "msandbox", "host" => "127.0.0.1" }
|
2
|
+
|
3
|
+
ActiveRecord::Base.configurations = {
|
4
|
+
"test_master" => base.merge("database" => "test_slave", "port" => 21891),
|
5
|
+
"test_slave_001" => base.merge("database" => "test_slave", "port" => 21892),
|
6
|
+
"test_slave_002" => base.merge("database" => "test_slave", "port" => 21893),
|
7
|
+
"test" => base.merge("database" => "test", "port" => 3306, "password" => ""),
|
8
|
+
|
9
|
+
"test_task_master" => base.merge("database" => "test_task", "port" => 21891),
|
10
|
+
"test_task_slave_001" => base.merge("database" => "test_task", "port" => 21892),
|
11
|
+
"test_task_slave_002" => base.merge("database" => "test_task", "port" => 21893)
|
12
|
+
}
|
13
|
+
|
14
|
+
ActiveRecord::Slave.configure do |config|
|
15
|
+
config.define_replication(:user) do |replication|
|
16
|
+
replication.register_master(:test_master)
|
17
|
+
|
18
|
+
replication.register_slave(:test_slave_001, 70)
|
19
|
+
replication.register_slave(:test_slave_002, 30)
|
20
|
+
end
|
21
|
+
|
22
|
+
config.define_replication(:task) do |replication|
|
23
|
+
replication.register_master(:test_task_master)
|
24
|
+
|
25
|
+
replication.register_slave(:test_task_slave_001, 1)
|
26
|
+
replication.register_slave(:test_task_slave_002, 1)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
ActiveRecord::Base.establish_connection(:test)
|
31
|
+
|
32
|
+
class User < ActiveRecord::Base
|
33
|
+
has_many :items
|
34
|
+
has_many :skills
|
35
|
+
|
36
|
+
include ActiveRecord::Slave::Model
|
37
|
+
use_slave :user
|
38
|
+
end
|
39
|
+
|
40
|
+
class Item < ActiveRecord::Base
|
41
|
+
belongs_to :user
|
42
|
+
end
|
43
|
+
|
44
|
+
class Skill < ActiveRecord::Base
|
45
|
+
belongs_to :user
|
46
|
+
|
47
|
+
include ActiveRecord::Slave::Model
|
48
|
+
use_slave :user
|
49
|
+
end
|
data/spec/schema.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
ActiveRecord::Schema.define(version: 0) do
|
2
|
+
create_table "users", force: :cascade do |t|
|
3
|
+
t.string "name", null: false
|
4
|
+
t.datetime "created_at", null: false
|
5
|
+
t.datetime "updated_at", null: false
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
ActiveRecord::Schema.define(version: 1) do
|
10
|
+
create_table "items", force: :cascade do |t|
|
11
|
+
t.integer "user_id", null: false
|
12
|
+
t.string "name", null: false
|
13
|
+
t.integer "count", null: false
|
14
|
+
t.datetime "created_at", null: false
|
15
|
+
t.datetime "updated_at", null: false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveRecord::Schema.define(version: 2) do
|
20
|
+
create_table "skills", force: :cascade do |t|
|
21
|
+
t.integer "user_id", null: false
|
22
|
+
t.string "name", null: false
|
23
|
+
t.datetime "created_at", null: false
|
24
|
+
t.datetime "updated_at", null: false
|
25
|
+
end
|
26
|
+
end
|