ar-octopus 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,2 @@
1
+ class Octopus::Controller
2
+ end
@@ -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,6 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'logger'
4
+ require "pg"
5
+
6
+ ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "octopus_shard1", :username => "root", :password => "")
@@ -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
@@ -0,0 +1,9 @@
1
+ class CreateUsersOnMaster < ActiveRecord::Migration
2
+ def self.up
3
+ User.create!(:name => "Master")
4
+ end
5
+
6
+ def self.down
7
+ User.delete_all()
8
+ end
9
+ end