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