active_record_shards 4.0.0.beta9 → 5.0.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.
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