active_record_shards 4.0.0.beta9 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 924d664ed8711d98af55e7b349f19e9d477536670a8e9b441b5e56c52f0d519b
4
- data.tar.gz: 4bd2af92d712cbbeaebca5e07e3912950bd8a1281cfde44b981785c9e36bf73b
3
+ metadata.gz: 07a2ae4e9b397abcc455655266c9d66c76af160e56ea963b401a130589f3f48c
4
+ data.tar.gz: 45993e917fb0585637c72f89d6bee77f805e0192efb6ca3f476e86929d9192ab
5
5
  SHA512:
6
- metadata.gz: 7be602e84729dc05224c35a5631c7f11d0988ff18fad48f8aafc38afb84b7a71f523ad8e7c60ae5d0937d531002f70419413caf737ba1698597abba1e749ef3d
7
- data.tar.gz: 67c2f918198c1872fd2373c2bee5a2c16d49f80b2fef6a4f81fabb15bc627db722684169b81658742a370265262b722993857d8e731cc08bc0eed49d410a151f
6
+ metadata.gz: 16309006259fadb5b3851ea470d8859618c2df4d10555e5ecb50c257d538ac3f36c69490a33547bc7e53e6a2962d5985623176021b5f0866f9a35973138000cc
7
+ data.tar.gz: d23f2ae22180be953ba4a8bbb23000f73901ecf7950cae95858d371eec24bf01f2e7d6ee0c7225e8bbe2f76490261baae4059193718ae3cb17ffd78ab338f852
data/README.md CHANGED
@@ -1,11 +1,20 @@
1
- [![Build Status](https://secure.travis-ci.org/zendesk/active_record_shards.png)](http://travis-ci.org/zendesk/active_record_shards)
1
+ [![Build Status](https://github.com/zendesk/active_record_shards/workflows/CI/badge.svg)](https://github.com/zendesk/active_record_shards/actions?query=workflow%3ACI)
2
2
 
3
3
  # ActiveRecord Shards
4
4
 
5
- ActiveRecord Shards is an extension for ActiveRecord that provides support for sharded database and slaves. Basically it is just a nice way to
5
+ ActiveRecord Shards is an extension for ActiveRecord that provides support for sharded database and replicas. Basically it is just a nice way to
6
6
  switch between database connections. We've made the implementation very small, and have tried not to reinvent any wheels already present in ActiveRecord.
7
7
 
8
- ActiveRecord Shards has been used and tested on Rails 5.0 and has in some form or another been used in production on a large Rails app for several years.
8
+ ActiveRecord Shards has been used and tested on Rails 5.x and 6.0, and has in some form or another been used in production on large Rails apps for several years.
9
+
10
+ - [Installation](#installation)
11
+ - [Configuration](#configuration)
12
+ - [Migrations](#migrations)
13
+ - [Example](#example)
14
+ - [Shared Model](#create-a-table-for-the-shared-not-sharded-model)
15
+ - [Sharded Model](#create-a-table-for-the-sharded-model)
16
+ - [Usage](#usage)
17
+ - [Debugging](#debugging)
9
18
 
10
19
  ## Installation
11
20
 
@@ -15,83 +24,143 @@ and make sure to require 'active\_record\_shards' in some way.
15
24
 
16
25
  ## Configuration
17
26
 
18
- Add the slave and shard configuration to config/database.yml:
19
-
20
- production:
21
- adapter: mysql
22
- encoding: utf8
23
- database: my_app_main
24
- pool: 5
25
- host: db1
26
- username: root
27
- password:
28
- slave:
29
- host: db1_slave
30
- shards:
31
- 1:
32
- host: db_shard1
33
- database: my_app_shard
34
- slave:
35
- host: db_shard1_slave
36
- 2:
37
- host: db_shard2
38
- database: my_app_shard
39
- slave:
40
- host: db_shard2_slave
27
+ Add the replica and shard configuration to config/database.yml:
28
+
29
+ ```yaml
30
+ production:
31
+ adapter: mysql
32
+ encoding: utf8
33
+ database: my_app_main
34
+ pool: 5
35
+ host: db1
36
+ username: root
37
+ password:
38
+ replica:
39
+ host: db1_replica
40
+ shards:
41
+ 1:
42
+ host: db_shard1
43
+ database: my_app_shard
44
+ replica:
45
+ host: db_shard1_replica
46
+ 2:
47
+ host: db_shard2
48
+ database: my_app_shard
49
+ replica:
50
+ host: db_shard2_replica
51
+ ```
41
52
 
42
53
  basically connections inherit configuration from the parent configuration file.
43
54
 
44
- ## Usage
55
+ ## Migrations
45
56
 
46
- Normally you have some models that live on a shared database, and you might need to query this data in order to know what shard to switch to.
47
- All the models that live on the sharded database must inherit from ActiveRecordShards::ShardedModel:
57
+ ActiveRecord Shards also patches migrations to support running migrations on a shared (not sharded) or a sharded database.
58
+ Each migration class has to specify a shard spec indicating where to run the migration.
59
+
60
+ Valid shard specs:
61
+
62
+ * `:none` - Run this migration on the shared database, not any shards
63
+ * `:all` - Run this migration on all of the shards, not the shared database
64
+
65
+ #### Example
66
+
67
+ ###### Create a table for the shared (not sharded) model
48
68
 
49
- class Account < ActiveRecord::Base
50
- has_many :projects
69
+ ```ruby
70
+ class CreateAccounts < ActiveRecord::Migration
71
+ shard :none
72
+
73
+ def change
74
+ create_table :accounts do |t|
75
+ # This is NOT necessary for the gem to work, we just use it in the examples below demonstrating one way to switch shards
76
+ t.integer :shard_id, null: false
77
+
78
+ t.string :name
51
79
  end
80
+ end
81
+ end
82
+ ```
83
+
84
+ ###### Create a table for the sharded model
52
85
 
53
- class Project < ActiveRecordShards::ShardedModel
54
- belongs_to :account
86
+ ```ruby
87
+ class CreateProjects < ActiveRecord::Migration
88
+ shard :all
89
+
90
+ def change
91
+ create_table :projects do |t|
92
+ t.references :account
93
+ t.string :name
55
94
  end
95
+ end
96
+ end
97
+ ```
98
+
99
+ ## Usage
100
+
101
+ Normally you have some models that live on a shared database, and you might need to query this data in order to know what shard to switch to.
102
+ All the models that live on the shared database must be marked as not\_sharded:
103
+
104
+ ```ruby
105
+ class Account < ActiveRecord::Base
106
+ not_sharded
107
+
108
+ has_many :projects
109
+ end
110
+
111
+ class Project < ActiveRecord::Base
112
+ belongs_to :account
113
+ end
114
+ ```
56
115
 
57
116
  So in this setup the accounts live on the shared database, but the projects are sharded. If accounts have a shard\_id column, you could lookup the account
58
117
  in a rack middleware and switch to the right shard:
59
118
 
60
- class AccountMiddleware
61
- def initialize(app)
62
- @app = app
63
- end
64
-
65
- def call(env)
66
- account = lookup_account(env)
119
+ ```ruby
120
+ class AccountMiddleware
121
+ def initialize(app)
122
+ @app = app
123
+ end
67
124
 
68
- if account
69
- ActiveRecord::Base.on_shard(account.shard_id) do
70
- @app.call(env)
71
- end
72
- else
73
- @app.call(env)
74
- end
75
- end
125
+ def call(env)
126
+ account = lookup_account(env)
76
127
 
77
- def lookup_account(env)
78
- ...
128
+ if account
129
+ ActiveRecord::Base.on_shard(account.shard_id) do
130
+ @app.call(env)
79
131
  end
132
+ else
133
+ @app.call(env)
80
134
  end
135
+ end
136
+
137
+ def lookup_account(env)
138
+ # ...
139
+ end
140
+ end
141
+ ```
81
142
 
82
- You can switch to the slave databases at any point by wrapping your code in an on\_slave block:
143
+ You can switch to the replica databases at any point by wrapping your code in an on\_replica block:
83
144
 
84
- ActiveRecord::Base.on_slave do
85
- Account.find_by_big_expensive_query
86
- end
145
+ ```ruby
146
+ ActiveRecord::Base.on_replica do
147
+ Account.find_by_big_expensive_query
148
+ end
149
+ ```
150
+
151
+ This will perform the query on the replica, and mark the returned instances as read-only. There is also a shortcut for this:
152
+
153
+ ```ruby
154
+ Account.on_replica.find_by_big_expensive_query
155
+ ```
87
156
 
88
- This will perform the query on the slave, and mark the returned instances as read only. There is also a shortcut for this:
157
+ If you do not want instances returned from replicas to be marked as read-only, this can be disabled globally:
89
158
 
90
- Account.on_slave.find_by_big_expensive_query
159
+ `ActiveRecordShards.disable_replica_readonly_records = true`
91
160
 
92
161
  ## Debugging
93
162
 
94
- Show if a query went to master or slave in the logs:
163
+ Show if a query went to primary or replica in the logs:
95
164
 
96
165
  ```Ruby
97
166
  require 'active_record_shards/sql_comments'
@@ -1,40 +1,42 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ActiveRecordShards
3
4
  module AssociationCollectionConnectionSelection
4
- def on_slave_if(condition)
5
- condition ? on_slave : self
5
+ def on_replica_if(condition)
6
+ condition ? on_replica : self
6
7
  end
7
8
 
8
- def on_slave_unless(condition)
9
- on_slave_if(!condition)
9
+ def on_replica_unless(condition)
10
+ on_replica_if(!condition)
10
11
  end
11
12
 
12
- def on_master_if(condition)
13
- condition ? on_master : self
13
+ def on_primary_if(condition)
14
+ condition ? on_primary : self
14
15
  end
15
16
 
16
- def on_master_unless(condition)
17
- on_master_if(!condition)
17
+ def on_primary_unless(condition)
18
+ on_primary_if(!condition)
18
19
  end
19
20
 
20
- def on_slave
21
- MasterSlaveProxy.new(self, :slave)
21
+ def on_replica
22
+ PrimaryReplicaProxy.new(self, :replica)
22
23
  end
23
24
 
24
- def on_master
25
- MasterSlaveProxy.new(self, :master)
25
+ def on_primary
26
+ PrimaryReplicaProxy.new(self, :primary)
26
27
  end
27
28
 
28
- class MasterSlaveProxy
29
+ class PrimaryReplicaProxy
29
30
  def initialize(association_collection, which)
30
31
  @association_collection = association_collection
31
32
  @which = which
32
33
  end
33
34
 
34
- def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissing
35
+ def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
35
36
  reflection = @association_collection.proxy_association.reflection
36
37
  reflection.klass.on_cx_switch_block(@which) { @association_collection.send(method, *args, &block) }
37
38
  end
39
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
38
40
  end
39
41
  end
40
42
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'active_support/core_ext'
3
4
 
4
5
  module ActiveRecordShards
@@ -6,7 +7,7 @@ module ActiveRecordShards
6
7
  module_function
7
8
 
8
9
  def explode(conf)
9
- conf = conf.deep_dup
10
+ conf = conf.to_h.deep_dup
10
11
 
11
12
  conf.to_a.each do |env_name, env_config|
12
13
  next unless shards = env_config.delete('shards')
@@ -23,9 +24,9 @@ module ActiveRecordShards
23
24
  end
24
25
 
25
26
  conf.to_a.each do |env_name, env_config|
26
- if slave_conf = env_config.delete('slave')
27
- expand_child!(env_config, slave_conf)
28
- conf["#{env_name}_slave"] = slave_conf
27
+ if replica_conf = env_config.delete('replica')
28
+ expand_child!(env_config, replica_conf)
29
+ conf["#{env_name}_replica"] = replica_conf
29
30
  end
30
31
  end
31
32
 
@@ -34,7 +35,7 @@ module ActiveRecordShards
34
35
 
35
36
  def expand_child!(parent, child)
36
37
  parent.each do |key, value|
37
- unless ['slave', 'shards'].include?(key) || value.is_a?(Hash)
38
+ unless ['replica', 'shards'].include?(key) || value.is_a?(Hash)
38
39
  child[key] ||= value
39
40
  end
40
41
  end
@@ -1,5 +1,15 @@
1
1
  module ActiveRecordShards
2
- module BaseConfig
2
+ module ConnectionSwitcher
3
+ def connection_specification_name
4
+ name = current_shard_selection.resolve_connection_name(sharded: is_sharded?, configurations: configurations)
5
+
6
+ unless configurations[name] || name == "primary"
7
+ raise ActiveRecord::AdapterNotSpecified, "No database defined by #{name} in your database config. (configurations: #{configurations.to_h.keys.inspect})"
8
+ end
9
+
10
+ name
11
+ end
12
+
3
13
  private
4
14
 
5
15
  def ensure_shard_connection
@@ -0,0 +1,30 @@
1
+ module ActiveRecordShards
2
+ module ConnectionSwitcher
3
+ def connection_specification_name
4
+ name = current_shard_selection.resolve_connection_name(sharded: is_sharded?, configurations: configurations)
5
+
6
+ @_ars_connection_specification_names ||= {}
7
+ unless @_ars_connection_specification_names.include?(name)
8
+ unless configurations[name] || name == "primary"
9
+ raise ActiveRecord::AdapterNotSpecified, "No database defined by #{name} in your database config. (configurations: #{configurations.to_h.keys.inspect})"
10
+ end
11
+
12
+ @_ars_connection_specification_names[name] = true
13
+ end
14
+
15
+ name
16
+ end
17
+
18
+ private
19
+
20
+ def ensure_shard_connection
21
+ # See if we've connected before. If not, call `#establish_connection`
22
+ # so that ActiveRecord can resolve connection_specification_name to an
23
+ # ARS connection.
24
+ spec_name = connection_specification_name
25
+
26
+ pool = connection_handler.retrieve_connection_pool(spec_name)
27
+ connection_handler.establish_connection(spec_name.to_sym) if pool.nil?
28
+ end
29
+ end
30
+ end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'active_record_shards/shard_support'
3
4
 
4
5
  module ActiveRecordShards
5
6
  module ConnectionSwitcher
6
- SHARD_NAMES_CONFIG_KEY = 'shard_names'.freeze
7
+ SHARD_NAMES_CONFIG_KEY = 'shard_names'
7
8
 
8
9
  def self.extended(base)
9
10
  base.singleton_class.send(:alias_method, :load_schema_without_default_shard!, :load_schema!)
@@ -13,17 +14,26 @@ module ActiveRecordShards
13
14
  base.singleton_class.send(:alias_method, :table_exists?, :table_exists_with_default_shard?)
14
15
  end
15
16
 
17
+ def default_shard=(new_default_shard)
18
+ ActiveRecordShards::ShardSelection.default_shard = new_default_shard
19
+ switch_connection(shard: new_default_shard)
20
+ end
21
+
22
+ def on_primary_db(&block)
23
+ on_shard(nil, &block)
24
+ end
25
+
16
26
  def on_shard(shard)
17
- old_selection = current_shard_selection
18
- switch_connection(ShardSelection.new(shard)) if supports_sharding?
27
+ old_options = current_shard_selection.options
28
+ switch_connection(shard: shard) if supports_sharding?
19
29
  yield
20
30
  ensure
21
- switch_connection(old_selection)
31
+ switch_connection(old_options)
22
32
  end
23
33
 
24
- def on_first_shard
34
+ def on_first_shard(&block)
25
35
  shard_name = shard_names.first
26
- on_shard(shard_name) { yield }
36
+ on_shard(shard_name, &block)
27
37
  end
28
38
 
29
39
  def shards
@@ -31,85 +41,144 @@ module ActiveRecordShards
31
41
  end
32
42
 
33
43
  def on_all_shards
34
- old_selection = current_shard_selection
44
+ old_options = current_shard_selection.options
35
45
  if supports_sharding?
36
46
  shard_names.map do |shard|
37
- switch_connection(ShardSelection.new(shard))
47
+ switch_connection(shard: shard)
38
48
  yield(shard)
39
49
  end
40
50
  else
41
51
  [yield]
42
52
  end
43
53
  ensure
44
- switch_connection(old_selection)
54
+ switch_connection(old_options)
55
+ end
56
+
57
+ def on_replica_if(condition, &block)
58
+ condition ? on_replica(&block) : yield
45
59
  end
46
60
 
47
- def connection_config
48
- super.merge(current_shard_selection.connection_config.merge(sharded: is_sharded?))
61
+ def on_replica_unless(condition, &block)
62
+ on_replica_if(!condition, &block)
63
+ end
64
+
65
+ def on_primary_if(condition, &block)
66
+ condition ? on_primary(&block) : yield
67
+ end
68
+
69
+ def on_primary_unless(condition, &block)
70
+ on_primary_if(!condition, &block)
71
+ end
72
+
73
+ def on_primary_or_replica(which, &block)
74
+ if block_given?
75
+ on_cx_switch_block(which, &block)
76
+ else
77
+ PrimaryReplicaProxy.new(self, which)
78
+ end
79
+ end
80
+
81
+ # Executes queries using the replica database. Fails over to primary if no replica is found.
82
+ # if you want to execute a block of code on the replica you can go:
83
+ # Account.on_replica do
84
+ # Account.first
85
+ # end
86
+ # the first account will be found on the replica DB
87
+ #
88
+ # For one-liners you can simply do
89
+ # Account.on_replica.first
90
+ def on_replica(&block)
91
+ on_primary_or_replica(:replica, &block)
92
+ end
93
+
94
+ def on_primary(&block)
95
+ on_primary_or_replica(:primary, &block)
96
+ end
97
+
98
+ def on_cx_switch_block(which, force: false, construct_ro_scope: nil, &block)
99
+ @disallow_replica ||= 0
100
+ @disallow_replica += 1 if which == :primary
101
+
102
+ switch_to_replica = force || @disallow_replica.zero?
103
+ old_options = current_shard_selection.options
104
+
105
+ switch_connection(replica: switch_to_replica)
106
+
107
+ # we avoid_readonly_scope to prevent some stack overflow problems, like when
108
+ # .columns calls .with_scope which calls .columns and onward, endlessly.
109
+ if self == ActiveRecord::Base || !switch_to_replica || construct_ro_scope == false || ActiveRecordShards.disable_replica_readonly_records == true
110
+ yield
111
+ else
112
+ readonly.scoping(&block)
113
+ end
114
+ ensure
115
+ @disallow_replica -= 1 if which == :primary
116
+ switch_connection(old_options) if old_options
49
117
  end
50
118
 
51
119
  def supports_sharding?
52
120
  shard_names.any?
53
121
  end
54
122
 
55
- def current_shard_selection
56
- Thread.current[:shard_selection] ||= NoShardSelection.new
123
+ def on_replica?
124
+ current_shard_selection.on_replica?
57
125
  end
58
126
 
59
- def current_shard_selection=(shard_selection)
60
- Thread.current[:shard_selection] = shard_selection
127
+ def current_shard_selection
128
+ Thread.current[:shard_selection] ||= ShardSelection.new
61
129
  end
62
130
 
63
131
  def current_shard_id
64
132
  current_shard_selection.shard
65
133
  end
66
134
 
67
- def on_shard?
68
- current_shard_selection.on_shard?
69
- end
70
-
71
135
  def shard_names
72
- unless config = configurations[shard_env]
73
- raise "Did not find #{shard_env} in configurations, did you forget to add it to your database config? (configurations: #{configurations.inspect})"
74
- end
75
- unless config[SHARD_NAMES_CONFIG_KEY]
76
- raise "No shards configured for #{shard_env}"
77
- end
78
- unless config[SHARD_NAMES_CONFIG_KEY].all? { |shard_name| shard_name.is_a?(Integer) }
79
- raise "All shard names must be integers: #{config[SHARD_NAMES_CONFIG_KEY].inspect}."
136
+ unless config_for_env.fetch(SHARD_NAMES_CONFIG_KEY, []).all? { |shard_name| shard_name.is_a?(Integer) }
137
+ raise "All shard names must be integers: #{config_for_env[SHARD_NAMES_CONFIG_KEY].inspect}."
80
138
  end
81
- config[SHARD_NAMES_CONFIG_KEY]
82
- end
83
139
 
84
- def table_exists_with_default_shard?
85
- with_default_shard { table_exists_without_default_shard? }
140
+ config_for_env[SHARD_NAMES_CONFIG_KEY] || []
86
141
  end
87
142
 
88
143
  private
89
144
 
90
- def switch_connection(selection)
91
- if selection.is_a?(ShardSelection)
145
+ def config_for_env
146
+ @_ars_config_for_env ||= {}
147
+ @_ars_config_for_env[shard_env] ||= begin
92
148
  unless config = configurations[shard_env]
93
- raise "Did not find #{shard_env} in configurations, did you forget to add it to your database config? (configurations: #{configurations.inspect})"
149
+ raise "Did not find #{shard_env} in configurations, did you forget to add it to your database config? (configurations: #{configurations.to_h.keys.inspect})"
94
150
  end
95
- unless config['shard_names'].include?(selection.shard)
96
- raise "Did not find shard #{selection.shard} in configurations"
151
+
152
+ config
153
+ end
154
+ end
155
+ alias_method :check_config_for_env, :config_for_env
156
+
157
+ def switch_connection(options)
158
+ if options.any?
159
+ if options.key?(:replica)
160
+ current_shard_selection.on_replica = options[:replica]
161
+ end
162
+
163
+ if options.key?(:shard)
164
+ check_config_for_env
165
+
166
+ current_shard_selection.shard = options[:shard]
97
167
  end
98
- self.current_shard_selection = selection
99
168
 
100
169
  ensure_shard_connection
101
- else
102
- self.current_shard_selection = selection
103
170
  end
104
171
  end
105
172
 
106
173
  def shard_env
107
- ActiveRecordShards.rails_env
174
+ ActiveRecordShards.app_env
108
175
  end
109
176
 
110
- def with_default_shard
111
- if is_sharded? && !on_shard? && table_name != ActiveRecord::SchemaMigration.table_name
112
- on_first_shard { yield }
177
+ # Make these few schema related methods available before having switched to
178
+ # a shard.
179
+ def with_default_shard(&block)
180
+ if is_sharded? && current_shard_id.nil? && table_name != ActiveRecord::SchemaMigration.table_name
181
+ on_first_shard(&block)
113
182
  else
114
183
  yield
115
184
  end
@@ -118,14 +187,30 @@ module ActiveRecordShards
118
187
  def load_schema_with_default_shard!
119
188
  with_default_shard { load_schema_without_default_shard! }
120
189
  end
190
+
191
+ def table_exists_with_default_shard?
192
+ with_default_shard { table_exists_without_default_shard? }
193
+ end
194
+
195
+ class PrimaryReplicaProxy
196
+ def initialize(target, which)
197
+ @target = target
198
+ @which = which
199
+ end
200
+
201
+ def method_missing(method, *args, &block) # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing
202
+ @target.on_primary_or_replica(@which) { @target.send(method, *args, &block) }
203
+ end
204
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
205
+ end
121
206
  end
122
207
  end
123
208
 
124
209
  case "#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"
125
- when '5.0'
126
- require 'active_record_shards/connection_switcher_5_0'
127
210
  when '5.1', '5.2'
128
- require 'active_record_shards/connection_switcher_5_1'
211
+ require 'active_record_shards/connection_switcher-5-1'
212
+ when '6.0'
213
+ require 'active_record_shards/connection_switcher-6-0'
129
214
  else
130
215
  raise "ActiveRecordShards is not compatible with #{ActiveRecord::VERSION::STRING}"
131
216
  end