rails-sharding 0.1.0

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.
@@ -0,0 +1,32 @@
1
+ module Rails::Sharding
2
+ class Config
3
+ DEFAULT_CONFIGS = {
4
+ # If true one connection will be established per shard (in every shard group)
5
+ # on startup. This only establishes the connection with the database but
6
+ # it does not retrieve a connection yet. This will be done by the ConnectionPool
7
+ # when necessary.
8
+ # If false the user must call Shards::ConnectionHandler.establish_connection(shard_group, shard_name)
9
+ # manually at least once before using each shard.
10
+ establish_all_connections_on_setup: true,
11
+
12
+ # If true the method #using_shard will be mixed in ActiveRecord scopes. Put
13
+ # this to false if you don't want the gem to modify ActiveRecord
14
+ extend_active_record_scope: true,
15
+
16
+ # Specifies where to find the definition of the shards configurations
17
+ shards_config_file: 'config/shards.yml',
18
+
19
+ # Specifies where to find the migrations for each shard group
20
+ shards_migrations_dir: 'db/shards_migrations',
21
+
22
+ # Specifies where to find the schemas for each shard group
23
+ shards_schemas_dir: 'db/shards_schemas'
24
+ }
25
+
26
+ DEFAULT_CONFIGS.each do |config_name, default_value|
27
+ self.cattr_accessor config_name
28
+ self.send(config_name.to_s + '=', default_value)
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,104 @@
1
+ module Rails::Sharding
2
+ class ConnectionHandler
3
+
4
+ # Establishes connections to all shards in all shard groups.
5
+ # Despite the name, this actually only creates a connection pool with zero
6
+ # connections for each shard. The connections will be allocated for each
7
+ # thread when #retrieve_connection or #with_connection are called
8
+ def self.establish_all_connections
9
+ Core.shard_groups.each do |shard_group|
10
+ Core.shard_names(shard_group).each do |shard_name|
11
+ establish_connection(shard_group, shard_name)
12
+ end
13
+ end
14
+ end
15
+
16
+ # Establishes a connection to a single shard in a single shard group
17
+ def self.establish_connection(shard_group, shard_name, environment=nil)
18
+ self.setup unless defined? @@connection_handler
19
+
20
+ unless configurations = (environment.nil? ? Core.configurations : Core.configurations(environment))
21
+ raise Errors::ConfigNotFoundError, "Cannot find configuration for environment '#{environment}' in #{Config.shards_config_file}"
22
+ end
23
+
24
+ unless shard_group_configurations = configurations[shard_group.to_s]
25
+ raise Errors::ConfigNotFoundError, "Cannot find configuration for shard_group '#{shard_group}' in environment '#{environment}' in #{Config.shards_config_file}"
26
+ end
27
+
28
+ resolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(shard_group_configurations)
29
+ begin
30
+ connection_spec = resolver.spec(shard_name.to_sym)
31
+ rescue ActiveRecord::AdapterNotSpecified => e
32
+ raise Errors::ConfigNotFoundError, "Cannot find configuration for shard '#{shard_group}:#{shard_name}' in environment '#{environment}' in #{Config.shards_config_file}"
33
+ end
34
+
35
+ # since rails requires a class to be the connection owner, we trick rails passing
36
+ # an instance of the ConnectionPoolOwner class, that responds to the #name method
37
+ connection_handler.establish_connection(connection_pool_owner(shard_group, shard_name), connection_spec)
38
+ end
39
+
40
+ def self.connection_pool(shard_group, shard_name)
41
+ connection_handler.retrieve_connection_pool(connection_pool_owner(shard_group, shard_name))
42
+ rescue Errors::ConnectionPoolRetrievalError
43
+ # mimicking behavior of rails at:
44
+ # https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#507
45
+ raise ActiveRecord::ConnectionNotEstablished, "No connection pool for shard #{connection_name(shard_group, shard_name)}"
46
+ end
47
+
48
+ def self.retrieve_connection(shard_group, shard_name)
49
+ connection_handler.retrieve_connection(connection_pool_owner(shard_group, shard_name))
50
+ end
51
+
52
+ def self.connected?(shard_group, shard_name)
53
+ connection_handler.connected?(connection_pool_owner(shard_group, shard_name))
54
+ end
55
+
56
+ def self.with_connection(shard_group, shard_name, &block)
57
+ connection_pool(shard_group, shard_name).with_connection(&block)
58
+ end
59
+
60
+ def self.remove_connection(shard_group, shard_name)
61
+ connection_handler.remove_connection(connection_pool_owner(shard_group, shard_name))
62
+ end
63
+
64
+ private
65
+
66
+ def self.connection_handler
67
+ raise Errors::UninitializedError, 'Shards::ConnectionHandler was not setup' unless defined? @@connection_handler
68
+ @@connection_handler
69
+ end
70
+
71
+ def self.setup
72
+ @@connection_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
73
+ @@connection_pool_owners = {}
74
+ end
75
+
76
+ def self.connection_pool_owner(shard_group, shard_name)
77
+ connection_name = self.connection_name(shard_group, shard_name)
78
+ @@connection_pool_owners[connection_name] ||= ConnectionPoolOwner.new(connection_name)
79
+ end
80
+
81
+ # Assembles connection name in the format "shard_group:shard_name"
82
+ def self.connection_name(shard_group, shard_name)
83
+ shard_group.to_s + ':' + shard_name.to_s
84
+ end
85
+
86
+ class ConnectionPoolOwner
87
+ attr_reader :name
88
+
89
+ def initialize(name)
90
+ @name = name
91
+ end
92
+
93
+ # Safeguard in case pool cannot be retrieved for owner. This makes the error clear
94
+ def superclass
95
+ raise Errors::ConnectionPoolRetrievalError, "ConnectionPool could not be retrieved for #{self}. See https://github.com/rails/rails/blob/4-2-stable/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb#607"
96
+ end
97
+
98
+ # in case owner ends up printed by rails in an error message when retrieving connection
99
+ def to_s
100
+ "ConnectionPoolOwner with name #{self.name}"
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,67 @@
1
+ require 'rails/sharding/active_record_extensions'
2
+ require 'rails/sharding/config'
3
+ require 'rails/sharding/connection_handler'
4
+ require 'rails/sharding/errors'
5
+ require 'rails/sharding/shard_thread_registry'
6
+ require 'rails/sharding/shardable_model'
7
+
8
+ module Rails::Sharding
9
+ class Core
10
+
11
+ # Opens a block where all queries will be directed to the selected shard
12
+ def self.using_shard(shard_group, shard_name)
13
+ raise 'Cannot nest using_shard blocks' if ShardThreadRegistry.connecting_to_shard?
14
+
15
+ ShardThreadRegistry.current_shard_group = shard_group
16
+ ShardThreadRegistry.current_shard_name = shard_name
17
+ yield
18
+ ensure
19
+ # Releases connections in case user left some connection in the reserved state
20
+ # (by calling retrieve_connection instead of with_connection). Also, using
21
+ # normal activerecord queries leaves a connection in the reserved state
22
+ ConnectionHandler.connection_pool(*ShardThreadRegistry.current_shard_group_and_name).release_connection
23
+ ShardThreadRegistry.connect_back_to_master!
24
+ end
25
+
26
+ def self.configurations(environment=Rails.env)
27
+ @@db_configs ||= YAML.load_file(Config.shards_config_file)
28
+ @@db_configs[environment]
29
+ rescue Errno::ENOENT => e
30
+ raise Errors::ConfigNotFoundError, Config.shards_config_file.to_s + ' file was not found'
31
+ end
32
+
33
+ def self.reset_configurations_cache
34
+ @@db_configs = nil
35
+ end
36
+
37
+ def self.test_configurations
38
+ self.configurations('test')
39
+ end
40
+
41
+ def self.shard_groups
42
+ self.configurations.keys
43
+ end
44
+
45
+ def self.shard_names(shard_group)
46
+ self.configurations[shard_group.to_s].keys
47
+ end
48
+
49
+ # Method that should be called on a rails initializer
50
+ def self.setup
51
+ if block_given?
52
+ yield Config
53
+ end
54
+
55
+ if Config.establish_all_connections_on_setup
56
+ # Establishes connections with all shards specified in config/shards.yml
57
+ ConnectionHandler.establish_all_connections
58
+ end
59
+
60
+ if Config.extend_active_record_scope
61
+ # includes the #using_shard method to all AR scopes
62
+ ActiveRecordExtensions.extend_active_record_scope
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,13 @@
1
+ module Rails::Sharding
2
+ module Errors
3
+
4
+ class UninitializedError < StandardError; end
5
+
6
+ class WrongUsageError < StandardError; end
7
+
8
+ class ConnectionPoolRetrievalError < StandardError; end
9
+
10
+ class ConfigNotFoundError < StandardError; end
11
+
12
+ end
13
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module Rails::Sharding
3
+
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load "tasks/rails-sharding.rake"
7
+ end
8
+ end
9
+
10
+ end
@@ -0,0 +1,48 @@
1
+ require 'active_support/per_thread_registry'
2
+
3
+ module Rails::Sharding
4
+ class ShardThreadRegistry
5
+ # creates two thread-specific variables
6
+ extend ActiveSupport::PerThreadRegistry
7
+ attr_accessor :_current_shard_group
8
+ attr_accessor :_current_shard_name
9
+
10
+ def self.connecting_to_master?
11
+ current_shard_group.nil? || current_shard_name.nil?
12
+ end
13
+
14
+ def self.connecting_to_shard?
15
+ !connecting_to_master?
16
+ end
17
+
18
+ def self.connect_back_to_master!
19
+ self.current_shard_group = nil
20
+ self.current_shard_name = nil
21
+ end
22
+
23
+ # Returns the current shard group (for the current Thread)
24
+ def self.current_shard_group
25
+ _current_shard_group
26
+ end
27
+
28
+ # Sets the current shard group (for the current Thread)
29
+ def self.current_shard_group=(group)
30
+ self._current_shard_group = group.blank? ? nil : group.to_sym
31
+ end
32
+
33
+ # Returns the current shard name (for the current Thread)
34
+ def self.current_shard_name
35
+ _current_shard_name
36
+ end
37
+
38
+ # Sets the current shard name (for the current Thread)
39
+ def self.current_shard_name=(name)
40
+ self._current_shard_name = name.blank? ? nil : name.to_sym
41
+ end
42
+
43
+ def self.current_shard_group_and_name
44
+ [current_shard_group, current_shard_name]
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,109 @@
1
+ # The purpose of this class is to override ActiveRecord::ConnectionHandling
2
+ # methods that access the DB connection or connection pool in any way.
3
+ #
4
+ # If Rails::Sharding::ShardThreadRegistry.connecting_to_master? is true, we just
5
+ # delegate all calls to the original methods of ActiveRecord::ConnectionHandling,
6
+ # otherwise we access our own ConnectionHandler and retrieve the connection
7
+ # or connection pool to the selected shard
8
+
9
+ module Rails::Sharding
10
+ module ShardableModel
11
+
12
+ def self.included(base)
13
+ # base.extend ClassMethods
14
+ base.extend ClassMethodsOverrides
15
+ end
16
+
17
+ # Module that includes all class methods that will be overriden on the model
18
+ # class when Rails::Sharding::ShardableModel is included
19
+ module ClassMethodsOverrides
20
+ # dinamically saves original methods with prefix "original_" and overrides
21
+ # then with the methods with prefix "sharded_"
22
+ def self.extended(klass)
23
+ self.instance_methods.each do |sharded_method_name|
24
+ method_name = sharded_method_name.to_s.match(/sharded_(.+)/)[1].to_sym
25
+
26
+ klass.singleton_class.instance_eval do
27
+ alias_method "original_#{method_name}".to_sym, method_name
28
+ alias_method method_name, sharded_method_name
29
+ end
30
+ end
31
+ end
32
+
33
+ # @overrides ActiveRecord::ConnectionHandling#connection_pool
34
+ def sharded_connection_pool
35
+ if ShardThreadRegistry.connecting_to_master?
36
+ return original_connection_pool
37
+ else
38
+ return ConnectionHandler.connection_pool(*ShardThreadRegistry.current_shard_group_and_name)
39
+ end
40
+ end
41
+
42
+ # @overrides ActiveRecord::ConnectionHandling#retrieve_connection
43
+ def sharded_retrieve_connection
44
+ if ShardThreadRegistry.connecting_to_master?
45
+ return original_retrieve_connection
46
+ else
47
+ return ConnectionHandler.retrieve_connection(*ShardThreadRegistry.current_shard_group_and_name)
48
+ end
49
+ end
50
+
51
+ # @overrides ActiveRecord::ConnectionHandling#sharded_connected?
52
+ def sharded_connected?
53
+ if ShardThreadRegistry.connecting_to_master?
54
+ return original_connected?
55
+ else
56
+ return ConnectionHandler.connected?(*ShardThreadRegistry.current_shard_group_and_name)
57
+ end
58
+ end
59
+
60
+ # @overrides ActiveRecord::ConnectionHandling#remove_connection (only if no parameters are passed)
61
+ def sharded_remove_connection(klass=nil)
62
+ if ShardThreadRegistry.connecting_to_master? || klass
63
+ return klass ? original_remove_connection(klass) : original_remove_connection
64
+ else
65
+ return ConnectionHandler.remove_connection(*ShardThreadRegistry.current_shard_group_and_name)
66
+ end
67
+ end
68
+
69
+ # @overrides ActiveRecord::ConnectionHandling#establish_connection
70
+ # In the case we are connecting to a shard, ignore spec parameter and use
71
+ # what is in ShardThreadRegistry instead
72
+ def sharded_establish_connection(spec=nil)
73
+ if ShardThreadRegistry.connecting_to_master?
74
+ return original_establish_connection(spec)
75
+ else
76
+ return ConnectionHandler.establish_connection(*ShardThreadRegistry.current_shard_group_and_name)
77
+ end
78
+ end
79
+
80
+ # @overrides ActiveRecord::ConnectionHandling#clear_active_connections!
81
+ def sharded_clear_active_connections!
82
+ if ShardThreadRegistry.connecting_to_master?
83
+ return original_clear_active_connections!
84
+ else
85
+ return ConnectionHandler.connection_handler.clear_active_connections!
86
+ end
87
+ end
88
+
89
+ # @overrides ActiveRecord::ConnectionHandling#clear_reloadable_connections!
90
+ def sharded_clear_reloadable_connections!
91
+ if ShardThreadRegistry.connecting_to_master?
92
+ return original_clear_reloadable_connections!
93
+ else
94
+ return ConnectionHandler.connection_handler.clear_reloadable_connections!
95
+ end
96
+ end
97
+
98
+ # @overrides ActiveRecord::ConnectionHandling#clear_all_connections!
99
+ def sharded_clear_all_connections!
100
+ if ShardThreadRegistry.connecting_to_master?
101
+ return original_clear_all_connections!
102
+ else
103
+ return ConnectionHandler.connection_handler.clear_all_connections!
104
+ end
105
+ end
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ module Rails
2
+ module Sharding
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,309 @@
1
+ require "active_record"
2
+
3
+ shards_namespace = namespace :shards do
4
+ task _make_activerecord_base_shardable: [:environment] do
5
+ # Several resources used (like Migrator, SchemaDumper, schema methods)
6
+ # implicitly use ActiveRecord::Base.connection, so we have to make it
7
+ # shardable so we can call using_shard and switch the connection
8
+ ActiveRecord::Base.include(Rails::Sharding::ShardableModel) unless ActiveRecord::Base.ancestors.include? Rails::Sharding::ShardableModel
9
+ end
10
+
11
+ desc "Creates database shards (options: RAILS_ENV=x SHARD_GROUP=x SHARD=x)"
12
+ task create: [:environment] do
13
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
14
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
15
+
16
+ shards_configurations.each do |shard, configuration|
17
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
18
+ puts "== Creating shard #{shard_group}:#{shard}"
19
+ ActiveRecord::Tasks::DatabaseTasks.create(configuration)
20
+ end
21
+ end
22
+ end
23
+
24
+ desc "Drops database shards (options: RAILS_ENV=x SHARD_GROUP=x SHARD=x)"
25
+ task drop: [:environment] do
26
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
27
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
28
+
29
+ shards_configurations.each do |shard, configuration|
30
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
31
+ puts "== Dropping shard #{shard_group}:#{shard}"
32
+ ActiveRecord::Tasks::DatabaseTasks.drop(configuration)
33
+ end
34
+ end
35
+ end
36
+
37
+ desc "Migrate the database (options: RAILS_ENV=x, SHARD_GROUP=x, VERSION=x, VERBOSE=false, SCOPE=blog)."
38
+ task migrate: [:_make_activerecord_base_shardable] do
39
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
40
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
41
+
42
+ # configures path for migrations of this shard group and creates dir if necessary
43
+ shard_group_migrations_dir = File.join(Rails::Sharding::Config.shards_migrations_dir, shard_group.to_s)
44
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = shard_group_migrations_dir
45
+ FileUtils.mkdir_p(shard_group_migrations_dir)
46
+
47
+ shards_configurations.each do |shard, configuration|
48
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
49
+ puts "== Migrating shard #{shard_group}:#{shard}"
50
+ Rails::Sharding.using_shard(shard_group, shard) do
51
+ ActiveRecord::Tasks::DatabaseTasks.migrate
52
+ end
53
+ end
54
+ end
55
+
56
+ shards_namespace["_dump"].invoke
57
+ end
58
+
59
+ # IMPORTANT: This task won't dump the schema if ActiveRecord::Base.dump_schema_after_migration is set to false
60
+ task :_dump do
61
+ if ActiveRecord::Base.dump_schema_after_migration
62
+ case ActiveRecord::Base.schema_format
63
+ when :ruby
64
+ shards_namespace["schema:dump"].invoke
65
+ when :sql
66
+ raise "sql schema dump not supported by shards"
67
+ else
68
+ raise "unknown schema format #{ActiveRecord::Base.schema_format}"
69
+ end
70
+ end
71
+ # Allow this task to be called as many times as required. An example is the
72
+ # migrate:redo task, which calls other two internally that depend on this one.
73
+ shards_namespace["_dump"].reenable
74
+ end
75
+
76
+ namespace :schema do
77
+ desc "Creates a schema.rb for each shard that is portable against any DB supported by Active Record (options: RAILS_ENV=x, SHARD_GROUP=x, SHARD=x)"
78
+ task dump: [:_make_activerecord_base_shardable] do
79
+ require "active_record/schema_dumper"
80
+
81
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
82
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
83
+
84
+ # configures path for schemas of this shard group and creates dir if necessary
85
+ shard_group_schemas_dir = File.join(Rails::Sharding::Config.shards_schemas_dir, shard_group.to_s)
86
+ FileUtils.mkdir_p(shard_group_schemas_dir)
87
+
88
+ shards_configurations.each do |shard, configuration|
89
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
90
+ puts "== Dumping schema of #{shard_group}:#{shard}"
91
+
92
+ schema_filename = File.join(shard_group_schemas_dir, shard + "_schema.rb")
93
+ File.open(schema_filename, "w:utf-8") do |file|
94
+ Rails::Sharding.using_shard(shard_group, shard) do
95
+ ActiveRecord::SchemaDumper.dump(Rails::Sharding::ConnectionHandler.retrieve_connection(shard_group, shard), file)
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Allow this task to be called as many times as required. An example is the
102
+ # migrate:redo task, which calls other two internally that depend on this one.
103
+ shards_namespace["schema:dump"].reenable
104
+ end
105
+
106
+ desc "Loads schema.rb file into the shards (options: RAILS_ENV=x, SHARD_GROUP=x, SHARD=x)"
107
+ task load: [:_make_activerecord_base_shardable] do
108
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
109
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
110
+
111
+ # configures path for schemas of this shard group
112
+ shard_group_schemas_path = 'db/shards_schemas/' + shard_group.to_s
113
+ Rails.application.paths.add shard_group_schemas_path
114
+ shard_group_schemas_dir = Rails.application.paths[shard_group_schemas_path].to_a.first
115
+
116
+ shards_configurations.each do |shard, configuration|
117
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
118
+ puts "== Loading schema of #{shard_group}:#{shard}"
119
+
120
+ schema_filename = File.join(shard_group_schemas_dir, shard + "_schema.rb")
121
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file(schema_filename)
122
+ Rails::Sharding.using_shard(shard_group, shard) do
123
+ load(schema_filename)
124
+ end
125
+ end
126
+ end
127
+ end
128
+
129
+ task load_if_ruby: ["shards:create", :environment] do
130
+ shards_namespace["schema:load"].invoke if ActiveRecord::Base.schema_format == :ruby
131
+ end
132
+ end
133
+
134
+ namespace :migrate do
135
+ desc 'Rollbacks the shards one migration and re migrate up (options: RAILS_ENV=x, VERSION=x, STEP=x, SHARD_GROUP=x, SHARD=x).'
136
+ task redo: [:environment] do
137
+ if ENV["VERSION"]
138
+ shards_namespace["migrate:down"].invoke
139
+ shards_namespace["migrate:up"].invoke
140
+ else
141
+ shards_namespace["rollback"].invoke
142
+ shards_namespace["migrate"].invoke
143
+ end
144
+ end
145
+
146
+ desc 'Resets your shards using your migrations for the current environment'
147
+ task reset: ["shards:drop", "shards:create", "shards:migrate"]
148
+
149
+ desc 'Runs the "up" for a given migration VERSION.'
150
+ task up: [:_make_activerecord_base_shardable] do
151
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
152
+ raise "VERSION is required" unless version
153
+
154
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
155
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
156
+
157
+ # configures path for migrations of this shard group
158
+ shard_group_migrations_path = 'db/shards_migrations/' + shard_group.to_s
159
+ Rails.application.paths.add shard_group_migrations_path
160
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths[shard_group_migrations_path].to_a
161
+
162
+ shards_configurations.each do |shard, configuration|
163
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
164
+ puts "== Migrating up shard #{shard_group}:#{shard}"
165
+ Rails::Sharding.using_shard(shard_group, shard) do
166
+ ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
167
+ end
168
+ end
169
+ end
170
+
171
+ shards_namespace["_dump"].invoke
172
+ end
173
+
174
+ desc 'Runs the "down" for a given migration VERSION.'
175
+ task down: [:_make_activerecord_base_shardable] do
176
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
177
+ raise "VERSION is required - To go down one migration, run db:rollback" unless version
178
+
179
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
180
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
181
+
182
+ # configures path for migrations of this shard group
183
+ shard_group_migrations_path = 'db/shards_migrations/' + shard_group.to_s
184
+ Rails.application.paths.add shard_group_migrations_path
185
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths[shard_group_migrations_path].to_a
186
+
187
+ shards_configurations.each do |shard, configuration|
188
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
189
+ puts "== Migrating down shard #{shard_group}:#{shard}"
190
+ Rails::Sharding.using_shard(shard_group, shard) do
191
+ ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
192
+ end
193
+ end
194
+ end
195
+
196
+ shards_namespace["_dump"].invoke
197
+ end
198
+ end
199
+
200
+ desc "Rolls the schema back to the previous version (options: RAILS_ENV=x, STEP=x, SHARD_GROUP=x, SHARD=x)."
201
+ task rollback: [:_make_activerecord_base_shardable] do
202
+ step = ENV["STEP"] ? ENV["STEP"].to_i : 1
203
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
204
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
205
+
206
+ # configures path for migrations of this shard group
207
+ shard_group_migrations_path = 'db/shards_migrations/' + shard_group.to_s
208
+ Rails.application.paths.add shard_group_migrations_path
209
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths[shard_group_migrations_path].to_a
210
+
211
+ shards_configurations.each do |shard, configuration|
212
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
213
+ puts "== Rolling back shard #{shard_group}:#{shard}"
214
+ Rails::Sharding.using_shard(shard_group, shard) do
215
+ ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
216
+ end
217
+ end
218
+ end
219
+
220
+ shards_namespace["_dump"].invoke
221
+ end
222
+
223
+ desc "Retrieves the current schema version number"
224
+ task version: [:_make_activerecord_base_shardable] do
225
+ Rails::Sharding.configurations.each do |shard_group, shards_configurations|
226
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
227
+
228
+ shards_configurations.each do |shard, configuration|
229
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
230
+
231
+ Rails::Sharding.using_shard(shard_group, shard) do
232
+ puts "Shard #{shard_group}:#{shard} version: #{ActiveRecord::Migrator.current_version}"
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+
239
+ namespace :test do
240
+ desc "Recreate the test shards from existent schema files (options: SHARD_GROUP=x, SHARD=x)"
241
+ task load_schema: ['shards:test:purge'] do
242
+ Rails::Sharding.test_configurations.each do |shard_group, shards_configurations|
243
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
244
+
245
+ # configures path for schemas of this shard group
246
+ shard_group_schemas_path = 'db/shards_schemas/' + shard_group.to_s
247
+ Rails.application.paths.add shard_group_schemas_path
248
+ shard_group_schemas_dir = Rails.application.paths[shard_group_schemas_path].to_a.first
249
+
250
+ shards_configurations.each do |shard, configuration|
251
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
252
+
253
+ puts "== Loading test schema on shard #{shard_group}:#{shard}"
254
+ begin
255
+ # establishes connection with test shard, saving if it was connected before
256
+ should_reconnect = Rails::Sharding::ConnectionHandler.connection_pool(shard_group, shard).active_connection?
257
+ Rails::Sharding::ConnectionHandler.establish_connection(shard_group, shard, 'test')
258
+
259
+ schema_filename = File.join(shard_group_schemas_dir, shard + "_schema.rb")
260
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file(schema_filename)
261
+ Rails::Sharding.using_shard(shard_group, shard) do
262
+ ActiveRecord::Schema.verbose = false
263
+ load(schema_filename)
264
+ end
265
+ ensure
266
+ if should_reconnect
267
+ # reestablishes connection for RAILS_ENV environment (whatever that is)
268
+ Rails::Sharding::ConnectionHandler.establish_connection(shard_group, shard)
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
274
+
275
+ desc 'Load the test schema into the shards (options: SHARD_GROUP=x, SHARD=x)'
276
+ task prepare: [:environment] do
277
+ unless Rails::Sharding.test_configurations.blank?
278
+ shards_namespace["test:load_schema"].invoke
279
+ end
280
+ end
281
+
282
+ desc "Empty the test shards (drops all tables) (options: SHARD_GROUP=x, SHARD=x)"
283
+ task :purge => [:_make_activerecord_base_shardable] do
284
+ Rails::Sharding.test_configurations.each do |shard_group, shards_configurations|
285
+ next if ENV["SHARD_GROUP"] && ENV["SHARD_GROUP"] != shard_group.to_s
286
+
287
+ shards_configurations.each do |shard, configuration|
288
+ next if ENV["SHARD"] && ENV["SHARD"] != shard.to_s
289
+
290
+ puts "== Purging test shard #{shard_group}:#{shard}"
291
+ begin
292
+ # establishes connection with test shard, saving if it was connected before (rails 4.2 doesn't do this, but should)
293
+ should_reconnect = Rails::Sharding::ConnectionHandler.connection_pool(shard_group, shard).active_connection?
294
+ Rails::Sharding::ConnectionHandler.establish_connection(shard_group, shard, 'test')
295
+
296
+ Rails::Sharding.using_shard(shard_group, shard) do
297
+ ActiveRecord::Tasks::DatabaseTasks.purge(configuration)
298
+ end
299
+ ensure
300
+ if should_reconnect
301
+ # reestablishes connection for RAILS_ENV environment (whatever that is)
302
+ Rails::Sharding::ConnectionHandler.establish_connection(shard_group, shard)
303
+ end
304
+ end
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end