ar-octopus 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.mkdn +49 -0
- data/Rakefile +106 -0
- data/VERSION +1 -0
- data/doc/api.textile +98 -0
- data/doc/features.textile +74 -0
- data/doc/libraries.textile +70 -0
- data/doc/shards.yml +76 -0
- data/lib/octopus.rb +30 -0
- data/lib/octopus/controller.rb +2 -0
- data/lib/octopus/migration.rb +41 -0
- data/lib/octopus/model.rb +57 -0
- data/lib/octopus/proxy.rb +195 -0
- data/spec/config/shards.yml +76 -0
- data/spec/database_connection.rb +6 -0
- data/spec/database_models.rb +23 -0
- data/spec/migrations/1_create_users_on_master.rb +9 -0
- data/spec/migrations/2_create_users_on_canada.rb +11 -0
- data/spec/migrations/3_create_users_on_both_shards.rb +11 -0
- data/spec/migrations/4_create_users_on_shards_of_a_group.rb +11 -0
- data/spec/migrations/5_create_users_on_multiples_groups.rb +11 -0
- data/spec/migrations/6_raise_exception_with_invalid_shard_name.rb +11 -0
- data/spec/migrations/7_raise_exception_with_invalid_multiple_shard_names.rb +11 -0
- data/spec/migrations/8_raise_exception_with_invalid_group_name.rb +11 -0
- data/spec/migrations/9_raise_exception_with_multiple_invalid_group_names.rb +11 -0
- data/spec/octopus/controller_spec.rb +7 -0
- data/spec/octopus/migration_spec.rb +79 -0
- data/spec/octopus/model_spec.rb +122 -0
- data/spec/octopus/octopus_spec.rb +15 -0
- data/spec/octopus/proxy_spec.rb +112 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +27 -0
- metadata +123 -0
data/lib/octopus.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Octopus
|
4
|
+
def self.env()
|
5
|
+
if defined?(Rails)
|
6
|
+
Rails.env.to_s
|
7
|
+
else
|
8
|
+
"production"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.config()
|
13
|
+
@@config ||= YAML.load_file(Octopus.directory() + "/config/shards.yml")
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.directory()
|
17
|
+
if defined?(Rails)
|
18
|
+
# Running in a normal Rails application
|
19
|
+
Rails.root.to_s
|
20
|
+
else
|
21
|
+
# Running in a generic Ruby process
|
22
|
+
Dir.pwd
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
require "octopus/proxy"
|
28
|
+
require "octopus/migration"
|
29
|
+
require "octopus/model"
|
30
|
+
require "octopus/controller"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Octopus::Migration
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
class << base
|
5
|
+
def connection
|
6
|
+
ActiveRecord::Base.connection_proxy()
|
7
|
+
end
|
8
|
+
|
9
|
+
def connected?
|
10
|
+
ActiveRecord::Base.connection_proxy().connected?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def using(*args)
|
17
|
+
if args.size == 1
|
18
|
+
self.connection().block = true
|
19
|
+
self.connection().current_shard = args.first
|
20
|
+
else
|
21
|
+
self.connection().current_shard = args
|
22
|
+
end
|
23
|
+
|
24
|
+
return self
|
25
|
+
end
|
26
|
+
|
27
|
+
def using_group(*args)
|
28
|
+
if args.size == 1
|
29
|
+
self.connection().block = true
|
30
|
+
self.connection().current_group = args.first
|
31
|
+
else
|
32
|
+
self.connection().current_group = args
|
33
|
+
end
|
34
|
+
|
35
|
+
return self
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
ActiveRecord::Migration.send(:include, Octopus::Migration)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Octopus::Model
|
2
|
+
def self.included(base)
|
3
|
+
base.extend(ClassMethods)
|
4
|
+
base.send(:include, InstanceMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module InstanceMethods
|
8
|
+
def connection_proxy
|
9
|
+
self.class.connection_proxy
|
10
|
+
end
|
11
|
+
|
12
|
+
def using(shard, &block)
|
13
|
+
class << self
|
14
|
+
def connection_proxy
|
15
|
+
@@connection_proxy ||= Octopus::Proxy.new(Octopus.config())
|
16
|
+
end
|
17
|
+
|
18
|
+
def connection
|
19
|
+
if self.respond_to?(:replicated)
|
20
|
+
self.connection_proxy().set_replicated_model(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
self.connection_proxy()
|
24
|
+
end
|
25
|
+
|
26
|
+
def connected?
|
27
|
+
self.connection_proxy().connected?
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if block_given?
|
32
|
+
older_shard = self.connection_proxy.current_shard
|
33
|
+
self.connection_proxy.block = true
|
34
|
+
self.connection_proxy.current_shard = shard
|
35
|
+
begin
|
36
|
+
yield
|
37
|
+
ensure
|
38
|
+
self.connection_proxy.block = false
|
39
|
+
self.connection_proxy.current_shard = older_shard
|
40
|
+
end
|
41
|
+
else
|
42
|
+
self.connection_proxy.current_shard = shard
|
43
|
+
return self
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module ClassMethods
|
49
|
+
include InstanceMethods
|
50
|
+
|
51
|
+
def replicated_model()
|
52
|
+
self.cattr_accessor :replicated
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
ActiveRecord::Base.send(:include, Octopus::Model)
|
@@ -0,0 +1,195 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
class Octopus::Proxy
|
4
|
+
attr_accessor :shards, :current_shard, :block, :groups, :current_group, :replicated, :slaves_list, :replicated_models
|
5
|
+
|
6
|
+
delegate :increment_open_transactions, :decrement_open_transactions, :to => :select_connection
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@shards = {}
|
10
|
+
@groups = {}
|
11
|
+
@replicated_models = Set.new
|
12
|
+
@block = false
|
13
|
+
@replicated = config[Octopus.env()]["replicated"] || false
|
14
|
+
@shards[:master] = ActiveRecord::Base.connection_pool()
|
15
|
+
@current_shard = :master
|
16
|
+
|
17
|
+
config[Octopus.env()]["shards"].each do |key, value|
|
18
|
+
if value.has_key?("adapter")
|
19
|
+
initialize_adapter(value['adapter'])
|
20
|
+
@shards[key.to_sym] = connection_pool_for(value, "#{value['adapter']}_connection")
|
21
|
+
else
|
22
|
+
@groups[key.to_sym] = []
|
23
|
+
|
24
|
+
value.each do |k, v|
|
25
|
+
raise "You have duplicated shard names!" if @shards.has_key?(k.to_sym)
|
26
|
+
initialize_adapter(v['adapter'])
|
27
|
+
@shards[k.to_sym] = connection_pool_for(v, "#{v['adapter']}_connection")
|
28
|
+
@groups[key.to_sym] << k.to_sym
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
if @replicated
|
34
|
+
@slaves_list = @shards.keys
|
35
|
+
@slaves_list.delete(:master)
|
36
|
+
@slaves_list = @slaves_list.map {|sym| sym.to_s}.sort
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_shard=(shard_symbol)
|
41
|
+
if shard_symbol.is_a?(Array)
|
42
|
+
shard_symbol.each {|symbol| raise "Nonexistent Shard Name: #{symbol}" if @shards[symbol].nil? }
|
43
|
+
else
|
44
|
+
raise "Nonexistent Shard Name: #{shard_symbol}" if @shards[shard_symbol].nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
@current_shard = shard_symbol
|
48
|
+
end
|
49
|
+
|
50
|
+
def current_group=(group_symbol)
|
51
|
+
if group_symbol.is_a?(Array)
|
52
|
+
group_symbol.each {|symbol| raise "Nonexistent Group Name: #{symbol}" if @groups[symbol].nil? }
|
53
|
+
else
|
54
|
+
raise "Nonexistent Group Name: #{group_symbol}" if @groups[group_symbol].nil? && !group_symbol.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
@current_group = group_symbol
|
58
|
+
end
|
59
|
+
|
60
|
+
def select_connection()
|
61
|
+
@shards[shard_name].connection()
|
62
|
+
end
|
63
|
+
|
64
|
+
def shard_name
|
65
|
+
if(current_shard.is_a?(Array))
|
66
|
+
current_shard.first
|
67
|
+
else
|
68
|
+
current_shard
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_replicated_model(model)
|
73
|
+
replicated_models << model.to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
def transaction(start_db_transaction = true, &block)
|
77
|
+
if should_send_queries_to_multiple_shards?
|
78
|
+
self.send_transaction_to_multiple_shards(current_shard, start_db_transaction, &block)
|
79
|
+
self.current_shard = :master
|
80
|
+
elsif should_send_queries_to_multiple_groups?
|
81
|
+
self.send_transaction_to_multiple_groups(start_db_transaction, &block)
|
82
|
+
self.current_group = nil
|
83
|
+
elsif should_send_queries_to_a_group_of_shards?
|
84
|
+
self.send_transaction_to_multiple_shards(@groups[current_group], start_db_transaction, &block)
|
85
|
+
self.current_group = nil
|
86
|
+
else
|
87
|
+
select_connection.transaction(start_db_transaction, &block)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def method_missing(method, *args, &block)
|
92
|
+
if should_clean_connection?(method)
|
93
|
+
conn = select_connection()
|
94
|
+
self.current_shard = :master
|
95
|
+
conn.send(method, *args, &block)
|
96
|
+
elsif should_send_queries_to_replicated_databases?(method)
|
97
|
+
send_queries_to_selected_slave(method, *args, &block)
|
98
|
+
elsif should_send_queries_to_multiple_groups?
|
99
|
+
send_queries_to_multiple_groups(method, *args, &block)
|
100
|
+
elsif should_send_queries_to_multiple_shards?
|
101
|
+
send_queries_to_shards(current_shard, method, *args, &block)
|
102
|
+
elsif should_send_queries_to_a_group_of_shards?
|
103
|
+
send_queries_to_shards(@groups[current_group], method, *args, &block)
|
104
|
+
else
|
105
|
+
select_connection().send(method, *args, &block)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
def connection_pool_for(adapter, config)
|
111
|
+
ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base::ConnectionSpecification.new(adapter, config))
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize_adapter(adapter)
|
115
|
+
begin
|
116
|
+
require 'rubygems'
|
117
|
+
gem "activerecord-#{adapter}-adapter"
|
118
|
+
require "active_record/connection_adapters/#{adapter}_adapter"
|
119
|
+
rescue LoadError
|
120
|
+
begin
|
121
|
+
require "active_record/connection_adapters/#{adapter}_adapter"
|
122
|
+
rescue LoadError
|
123
|
+
raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def should_clean_connection?(method)
|
129
|
+
method.to_s =~ /begin_db_transaction|insert|select/ && !should_send_queries_to_multiple_shards? && !self.current_group && !replicated
|
130
|
+
end
|
131
|
+
|
132
|
+
def should_send_queries_to_multiple_shards?
|
133
|
+
current_shard.is_a?(Array)
|
134
|
+
end
|
135
|
+
|
136
|
+
def should_send_queries_to_multiple_groups?
|
137
|
+
current_group.is_a?(Array)
|
138
|
+
end
|
139
|
+
|
140
|
+
def should_send_queries_to_a_group_of_shards?
|
141
|
+
!current_group.nil?
|
142
|
+
end
|
143
|
+
|
144
|
+
def should_send_queries_to_replicated_databases?(method)
|
145
|
+
@replicated && method.to_s == "select_all"
|
146
|
+
end
|
147
|
+
|
148
|
+
def send_queries_to_multiple_groups(method, *args, &block)
|
149
|
+
method_return = nil
|
150
|
+
|
151
|
+
current_group.each do |group_symbol|
|
152
|
+
method_return = self.send_queries_to_shards(@groups[group_symbol], method, *args, &block)
|
153
|
+
end
|
154
|
+
|
155
|
+
return method_return
|
156
|
+
end
|
157
|
+
|
158
|
+
def send_queries_to_shards(shard_array, method, *args, &block)
|
159
|
+
method_return = nil
|
160
|
+
|
161
|
+
shard_array.each do |shard_symbol|
|
162
|
+
method_return = @shards[shard_symbol].connection().send(method, *args, &block)
|
163
|
+
end
|
164
|
+
|
165
|
+
return method_return
|
166
|
+
end
|
167
|
+
|
168
|
+
def send_queries_to_selected_slave(method, *args, &block)
|
169
|
+
#TODO: ugly code, needs refactor
|
170
|
+
if args.last =~ /#{replicated_models.to_a.join('|')}/
|
171
|
+
old_shard = self.current_shard
|
172
|
+
self.current_shard = slaves_list.shift.to_sym
|
173
|
+
slaves_list << self.current_shard
|
174
|
+
else
|
175
|
+
old_shard = self.current_shard
|
176
|
+
self.current_shard = :master
|
177
|
+
end
|
178
|
+
|
179
|
+
sql = select_connection().send(method, *args, &block)
|
180
|
+
self.current_shard = old_shard
|
181
|
+
return sql
|
182
|
+
end
|
183
|
+
|
184
|
+
def send_transaction_to_multiple_shards(shard_array, start_db_transaction, &block)
|
185
|
+
shard_array.each do |shard_symbol|
|
186
|
+
@shards[shard_symbol].connection().transaction(start_db_transaction, &block)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def send_transaction_to_multiple_groups(start_db_transaction, &block)
|
191
|
+
current_group.each do |group_symbol|
|
192
|
+
self.send_transaction_to_multiple_shards(@groups[group_symbol], start_db_transaction, &block)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
production:
|
2
|
+
shards:
|
3
|
+
alone_shard:
|
4
|
+
adapter: mysql
|
5
|
+
host: localhost
|
6
|
+
database: octopus_shard5
|
7
|
+
|
8
|
+
postgresql_shard:
|
9
|
+
adapter: postgresql
|
10
|
+
username: postgres
|
11
|
+
password:
|
12
|
+
database: octopus_shard1
|
13
|
+
encoding: unicode
|
14
|
+
|
15
|
+
history_shards:
|
16
|
+
aug2009:
|
17
|
+
adapter: mysql
|
18
|
+
host: localhost
|
19
|
+
database: octopus_shard2
|
20
|
+
aug2010:
|
21
|
+
adapter: mysql
|
22
|
+
host: localhost
|
23
|
+
database: octopus_shard3
|
24
|
+
aug2011:
|
25
|
+
adapter: mysql
|
26
|
+
host: localhost
|
27
|
+
database: octopus_shard4
|
28
|
+
|
29
|
+
country_shards:
|
30
|
+
canada:
|
31
|
+
adapter: mysql
|
32
|
+
host: localhost
|
33
|
+
database: octopus_shard2
|
34
|
+
brazil:
|
35
|
+
adapter: mysql
|
36
|
+
host: localhost
|
37
|
+
database: octopus_shard3
|
38
|
+
russia:
|
39
|
+
adapter: mysql
|
40
|
+
host: localhost
|
41
|
+
database: octopus_shard4
|
42
|
+
|
43
|
+
|
44
|
+
production_raise_error:
|
45
|
+
shards:
|
46
|
+
history_shards:
|
47
|
+
duplicated_shard_name:
|
48
|
+
adapter: mysql
|
49
|
+
host: localhost
|
50
|
+
database: octopus_shard5
|
51
|
+
|
52
|
+
country_shards:
|
53
|
+
duplicated_shard_name:
|
54
|
+
adapter: mysql
|
55
|
+
host: localhost
|
56
|
+
database: octopus_shard4
|
57
|
+
|
58
|
+
production_replicated:
|
59
|
+
replicated: true
|
60
|
+
shards:
|
61
|
+
slave1:
|
62
|
+
adapter: mysql
|
63
|
+
host: localhost
|
64
|
+
database: octopus_shard2
|
65
|
+
slave2:
|
66
|
+
adapter: mysql
|
67
|
+
host: localhost
|
68
|
+
database: octopus_shard3
|
69
|
+
slave3:
|
70
|
+
adapter: mysql
|
71
|
+
host: localhost
|
72
|
+
database: octopus_shard4
|
73
|
+
slave4:
|
74
|
+
adapter: mysql
|
75
|
+
host: localhost
|
76
|
+
database: octopus_shard5
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#The user class is just sharded, not replicated
|
2
|
+
class User < ActiveRecord::Base
|
3
|
+
def awesome_queries
|
4
|
+
using(:canada) do
|
5
|
+
User.create(:name => "teste")
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
#The client class isn't replicated
|
11
|
+
class Client < ActiveRecord::Base
|
12
|
+
has_many :items
|
13
|
+
end
|
14
|
+
|
15
|
+
#This class is replicated
|
16
|
+
class Cat < ActiveRecord::Base
|
17
|
+
replicated_model()
|
18
|
+
end
|
19
|
+
|
20
|
+
#This items belongs to a client
|
21
|
+
class Item < ActiveRecord::Base
|
22
|
+
belongs_to :client
|
23
|
+
end
|