├── .gitignore ├── .travis.yml ├── Appraisals ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── benchmarks └── benchmark.rb ├── bin ├── console ├── setup └── test ├── fresh_connection.gemspec ├── gemfiles ├── rails52.gemfile ├── rails60.gemfile └── rails61.gemfile ├── lib ├── fresh_connection.rb └── fresh_connection │ ├── abstract_connection_manager.rb │ ├── access_control.rb │ ├── connection_manager.rb │ ├── connection_specification.rb │ ├── connection_specification │ ├── rails_60.rb │ └── rails_61.rb │ ├── executor_hook.rb │ ├── extend.rb │ ├── extend │ ├── adapters │ │ ├── base_adapter.rb │ │ ├── m2_adapter.rb │ │ └── pg_adapter.rb │ ├── ar_base.rb │ ├── ar_connection_handler.rb │ ├── ar_relation.rb │ ├── ar_relation_merger.rb │ ├── ar_resolver.rb │ └── ar_statement_cache.rb │ ├── railtie.rb │ ├── replica_connection_handler.rb │ └── version.rb ├── log └── .gitkeep └── test ├── access_control ├── access_test.rb └── force_master_access_test.rb ├── ar_base ├── ar_abstract_adapter_test.rb ├── master_db_only_test.rb ├── replica_connection_test.rb └── replica_spec_name_test.rb ├── ar_relation └── merge_test.rb ├── config ├── database_postgresql.yml.travis ├── mysql_schema.sql ├── prepare.rb ├── psql_test_master.sql ├── psql_test_replica1.sql ├── psql_test_replica2.sql ├── psql_test_schema_setup1.sql └── psql_test_schema_setup2.sql ├── connection_manager ├── clear_all_connections_test.rb ├── put_aside_test.rb └── replica_connection_test.rb ├── connection_specification └── database_replica_url_test.rb ├── fresh_connection ├── access_to_master_test.rb ├── access_to_replica_test.rb └── master_db_only_model_always_access_to_master_test.rb ├── recovery_test.rb ├── support ├── active_record_logger.rb └── extend_minitest.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .ruby-version 4 | Gemfile.lock 5 | gemfiles/*.lock 6 | log/* 7 | .*.sw[a-z] 8 | /bin/test_local 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | services: 3 | - mysql 4 | - postgresql 5 | before_install: 6 | - gem update --system 7 | - gem --version 8 | rvm: 9 | - 2.6.9 10 | - 2.7.5 11 | - 3.0.3 12 | gemfile: 13 | - gemfiles/rails52.gemfile 14 | - gemfiles/rails60.gemfile 15 | - gemfiles/rails61.gemfile 16 | script: 17 | - "bin/test" 18 | matrix: 19 | fast_finish: true 20 | exclude: 21 | - rvm: 3.0.3 22 | gemfile: gemfiles/rails52.gemfile 23 | 24 | bundler_args: --jobs 3 --retry 3 25 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails52" do 2 | gem 'activerecord', '~> 5.2.0' 3 | gem 'mysql2', '>= 0.4.4', "< 0.6.0" 4 | end 5 | 6 | appraise "rails60" do 7 | gem 'activerecord', '~> 6.0.0' 8 | end 9 | 10 | appraise "rails61" do 11 | gem 'activerecord', '~> 6.1.0' 12 | end 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at tsukasa.oishi@gmail.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in fresh_connection.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tsukasa OISHI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreshConnection 2 | [![Gem Version](https://badge.fury.io/rb/fresh_connection.svg)](http://badge.fury.io/rb/fresh_connection) [![Build Status](https://travis-ci.org/tsukasaoishi/fresh_connection.svg?branch=master)](https://travis-ci.org/tsukasaoishi/fresh_connection) [![Code Climate](https://codeclimate.com/github/tsukasaoishi/fresh_connection/badges/gpa.svg)](https://codeclimate.com/github/tsukasaoishi/fresh_connection) 3 | 4 | **FreshConnection** provides access to one or more configured database replicas. 5 | 6 | For example: 7 | 8 | ```text 9 | Rails ------------ DB Master 10 | | 11 | +---- DB Replica 12 | ``` 13 | 14 | or 15 | 16 | ```text 17 | Rails -------+---- DB Master 18 | | 19 | | +------ DB Replica1 20 | | | 21 | +---- Loadbalancer ---+ 22 | | 23 | +------ DB Replica2 24 | ``` 25 | 26 | FreshConnction connects one or more configured DB replicas, or with multiple replicas behind a DB query load balancer. 27 | 28 | - Read queries go to the DB replica. 29 | - Write queries go to the DB master. 30 | - Within a transaction, all queries go to the DB master. 31 | 32 | ### Failover 33 | FreshConnection assumes that there is a load balancer in front of multi replica servers. 34 | When what happens one of the replicas is unreachable for any reason, FreshConnection will try three retries to access to a replica via a load balancer. 35 | 36 | Removing a trouble replica from a cluster is a work of the load balancer. 37 | FreshConnection expects the load balancer to work during three retries. 38 | 39 | If you would like access to multi replica servers without a load balancer, you should use [EbisuConnection](https://github.com/tsukasaoishi/ebisu_connection). 40 | EbisuConnection has functions of load balancer. 41 | 42 | ## Usage 43 | ### Access to the DB Replica 44 | Read queries are automatically connected to the DB replica. 45 | 46 | ```ruby 47 | Article.where(id: 1) 48 | 49 | Account.count 50 | ``` 51 | 52 | ### Access to the DB Master 53 | If you wish to ensure that queries are directed to the DB master, call `read_master`. 54 | 55 | ```ruby 56 | Article.where(id: 1).read_master 57 | 58 | Account.read_master.count 59 | ``` 60 | 61 | Within transactions, all queries are connected to the DB master. 62 | 63 | ```ruby 64 | Article.transaction do 65 | Article.where(id: 1) 66 | end 67 | ``` 68 | 69 | Create, update and delete queries are connected to the DB master. 70 | 71 | ```ruby 72 | new_article = Article.create(...) 73 | new_article.title = "FreshConnection" 74 | new_article.save 75 | ... 76 | old_article.destroy 77 | ``` 78 | 79 | ## ActiveRecord Versions Supported 80 | 81 | - FreshConnection supports ActiveRecord version 5.2 or later. 82 | - If you are using Rails 5.1, you can use FreshConnection version 3.0.3 or before. 83 | 84 | ### Not Support Multiple Database 85 | I haven't tested it in an environment using MultipleDB in Rails 6. 86 | I plan to enable use with MultipleDB in FreshConnection version 4.0 or later. 87 | 88 | ## Databases Supported 89 | FreshConnection currently supports MySQL and PostgreSQL. 90 | 91 | ## Installation 92 | Add this line to your application's `Gemfile`: 93 | 94 | ```ruby 95 | gem "fresh_connection" 96 | ``` 97 | 98 | And then execute: 99 | 100 | ``` 101 | $ bundle 102 | ``` 103 | 104 | Or install it manually with: 105 | 106 | ``` 107 | $ gem install fresh_connection 108 | ``` 109 | 110 | ## Configuration 111 | 112 | The FreshConnection database replica is configured within the standard Rails 113 | database configuration file, `config/database.yml`, using a `replica:` stanza. 114 | 115 | Below is a sample such configuration file. 116 | 117 | ### `config/database.yml` 118 | 119 | ```yaml 120 | default: &default 121 | adapter: mysql2 122 | encoding: utf8 123 | pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 124 | username: root 125 | password: 126 | 127 | production: 128 | <<: *default 129 | database: blog_production 130 | username: master_db_user 131 | password: <%= ENV['MASTER_DATABASE_PASSWORD'] %> 132 | host: master_db 133 | 134 | replica: 135 | username: replica_db_user 136 | password: <%= ENV['REPLICA_DATABASE_PASSWORD'] %> 137 | host: replica_db 138 | ``` 139 | 140 | `replica` is the configuration used for connecting read-only queries to the database replica. All other connections will use the database master settings. 141 | 142 | **NOTE:** 143 | The 'replica' stanza has a special meaning in Rails6. 144 | In Rails6, use a name other than 'replica', and specify that name using establish_fresh_connection in ApplicationRecord etc. 145 | 146 | ### Multiple DB Replicas 147 | If you want to use multiple configured DB replicas, the configuration can contain multiple `replica` stanzas in the configuration file `config/database.yml`. 148 | 149 | For example: 150 | 151 | ```yaml 152 | default: &default 153 | adapter: mysql2 154 | encoding: utf8 155 | pool: <%%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 156 | username: root 157 | password: 158 | 159 | production: 160 | <<: *default 161 | database: blog_production 162 | username: master_db_user 163 | password: <%= ENV['MASTER_DATABASE_PASSWORD'] %> 164 | host: master_db 165 | 166 | replica: 167 | username: replica_db_user 168 | password: <%= ENV['REPLICA_DATABASE_PASSWORD'] %> 169 | host: replica_db 170 | 171 | admin_replica: 172 | username: admin_replica_db_user 173 | password: <%= ENV['ADMIN_REPLICA_DATABASE_PASSWORD'] %> 174 | host: admin_replica_db 175 | ``` 176 | 177 | The custom replica stanza can then be applied as an argument to the `establish_fresh_connection` method in the models that should use it. For example: 178 | 179 | ```ruby 180 | class AdminUser < ActiveRecord::Base 181 | establish_fresh_connection :admin_replica 182 | end 183 | ``` 184 | 185 | The child (sub) classes of the configured model will inherit the same access as the parent class. Example: 186 | 187 | ```ruby 188 | class AdminBase < ActiveRecord::Base 189 | establish_fresh_connection :admin_replica 190 | end 191 | 192 | class AdminUser < AdminBase 193 | end 194 | 195 | class Benefit < AdminBase 196 | end 197 | 198 | class Customer < ActiveRecord::Base 199 | end 200 | ``` 201 | 202 | The `AdminUser` and `Benefit` models will access the database configured for the `admin_replica` group. 203 | 204 | The `Customer` model will use the default connections: read-only queries will connect to the standard DB replica, and state-changing queries will connect to the DB master. 205 | 206 | 207 | ### Replica Configuration With Environment Variables 208 | 209 | Alternative to using a configuration in the `database.yml` file, it is possible to completely specify the replica access components using environment variables. 210 | 211 | The environment variables corresponding to the `:replica` group are `DATABASE_REPLICA_URL`. 212 | The URL string components is the same as Rails' `DATABASE_URL'. 213 | 214 | #### Multiple Replica Environment Variables 215 | 216 | To specific URLs for multiple replicas, replace the string `REPLICA` in the environment variable name with the replica name, in upper case. See the examples for replicas: `:replica1`, `:replica2`, and `:admin_replica` 217 | 218 | 219 | DATABASE_REPLICA1_URL='mysql://localhost/dbreplica1?pool=5&reconnect=true' 220 | DATABASE_REPLICA2_URL='postgresql://localhost:6432/ro_db?pool=5&reconnect=true' 221 | DATABASE_ADMIN_REPLICA_URL='postgresql://localhost:6432/admin_db?pool=5&reconnect=true' 222 | 223 | 224 | ### Master-only Models 225 | 226 | It is possible to declare that specific models always use the DB master for all connections, using the `master_db_only!` method: 227 | 228 | ```ruby 229 | class CustomerState < ActiveRecord::Base 230 | master_db_only! 231 | end 232 | ``` 233 | 234 | All queries generated by methods on the `CustomerState` model will be directed to the DB master. 235 | 236 | ### Using FreshConnection With Unicorn 237 | 238 | When using FreshConnection with Unicorn (or any other multi-processing web server which restarts processes on the fly), connection management needs special attention during startup: 239 | 240 | ```ruby 241 | before_fork do |server, worker| 242 | ... 243 | ActiveRecord::Base.clear_all_replica_connections! 244 | ... 245 | end 246 | ``` 247 | 248 | ### Replica Connection Manager 249 | The default replica connection manager is `FreshConnection::ConnectionManager`. If an alternative (custom) replica connection manager is desired, this can be done with a simple assignment within a Rails initializer: 250 | 251 | `config/initializers/fresh_connection.rb`: 252 | 253 | ```ruby 254 | FreshConnection.connection_manager = MyOwnReplicaConnection 255 | ``` 256 | 257 | The `MyOwnReplicaConnection` class should inherit from `FreshConnection::AbstractConnectionManager`, which has this interface: 258 | 259 | ```ruby 260 | class MyOwnReplicaConnection < FreshConnection::AbstractConnectionManager 261 | 262 | def replica_connection 263 | # must return an instance of a subclass of ActiveRecord::ConnectionAdapters 264 | # eg: ActiveRecord::ConnectionAdapter::Mysql2Adapter 265 | # or: ActiveRecord::ConnectionAdapter::PostgresqlAdapter 266 | end 267 | 268 | def clear_all_connections! 269 | # called to disconnect all connections 270 | end 271 | 272 | def put_aside! 273 | # called when end of Rails controller action 274 | end 275 | 276 | def recovery? 277 | # called when raising exceptions on access to the DB replica 278 | # access will be retried when this method returns true 279 | end 280 | 281 | end 282 | ``` 283 | 284 | 285 | ## Contributing 286 | 287 | 1. Fork it 288 | 2. Create your feature branch (`git checkout -b my-new-feature`) 289 | 3. Commit your changes (`git commit -am 'Add some feature'`) 290 | 4. Push to the branch (`git push origin my-new-feature`) 291 | 5. Create new Pull Request 292 | 293 | ## Test 294 | 295 | I'm glad that you would like to test! 296 | To run the test suite, both `mysql` and `postgresql` must be installed. 297 | 298 | ### Test Configuration 299 | 300 | First, configure the test servers in `test/config/*.yml` 301 | 302 | Then, run: 303 | 304 | ```bash 305 | ./bin/setup 306 | ``` 307 | 308 | ### Running Tests 309 | 310 | To run the spec suite for all supported versions of rails: 311 | 312 | ```bash 313 | ./bin/test 314 | ``` 315 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rake/testtask' 3 | 4 | desc 'Run tests' 5 | task :test do 6 | Rake::TestTask.new do |t| 7 | t.libs << "test" 8 | t.libs << "lib" 9 | t.test_files = FileList['test/**/*_test.rb'] 10 | t.verbose = true 11 | end 12 | end 13 | 14 | task :default => :test 15 | -------------------------------------------------------------------------------- /benchmarks/benchmark.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'fresh_connection' 3 | require 'benchmark/ips' 4 | require 'active_record' 5 | require 'mysql2' 6 | 7 | class ActiveRecord::Base 8 | establish_connection( 9 | adapter: 'mysql2', 10 | encoding: 'utf8', 11 | database: 'kaeruspoon_development', 12 | pool: 5, 13 | username: 'root', 14 | password: '', 15 | socket: '/tmp/mysql.sock', 16 | replica: { encoding: 'utf8' } 17 | ) 18 | end 19 | 20 | class Article < ActiveRecord::Base 21 | end 22 | 23 | Benchmark.ips do |x| 24 | x.report("find") { Article.take } 25 | end 26 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "fresh_connection" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle config set path '.bundle' 7 | bundle install 8 | bundle exec appraisal install 9 | 10 | # Do any other automated setup that you need to do here 11 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | set -vx 6 | 7 | if [ -v DATABASE_URL ]; then 8 | echo "[specified config]" 9 | bundle exec rake test 10 | else 11 | echo "[mysql]" 12 | DATABASE_URL="mysql2://root@localhost/fresh_connection_test_master" \ 13 | DATABASE_REPLICA1_URL="mysql2://root@localhost/fresh_connection_test_replica1" \ 14 | DATABASE_REPLICA2_URL="mysql2://root@localhost/fresh_connection_test_replica2" \ 15 | DATABASE_FAKE_REPLICA_URL="mysql2://root@localhost/fresh_connection_test_master" \ 16 | bundle exec rake test 17 | 18 | echo "[postgresql]" 19 | DATABASE_URL="postgresql://localhost/fresh_connection_test_master" \ 20 | DATABASE_REPLICA1_URL="postgresql://localhost/fresh_connection_test_replica1" \ 21 | DATABASE_REPLICA2_URL="postgresql://localhost/fresh_connection_test_replica2" \ 22 | DATABASE_FAKE_REPLICA_URL="postgresql://localhost/fresh_connection_test_master" \ 23 | bundle exec rake test 24 | fi 25 | -------------------------------------------------------------------------------- /fresh_connection.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'fresh_connection/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "fresh_connection" 8 | spec.version = FreshConnection::VERSION 9 | spec.authors = ["Tsukasa OISHI"] 10 | spec.email = ["tsukasa.oishi@gmail.com"] 11 | 12 | spec.summary = %q{FreshConnection supports connections with configured replica servers.} 13 | spec.description = %q{https://github.com/tsukasaoishi/fresh_connection} 14 | spec.homepage = "https://github.com/tsukasaoishi/fresh_connection" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency 'activerecord', '>= 5.2.0', '< 7.0' 23 | 24 | spec.add_development_dependency 'mysql2', '>= 0.4.4' 25 | spec.add_development_dependency 'pg', '>= 0.18', '< 2.0' 26 | spec.add_development_dependency "rake" 27 | spec.add_development_dependency 'appraisal' 28 | spec.add_development_dependency "minitest", "~> 5.10.0" 29 | spec.add_development_dependency "minitest-reporters" 30 | spec.add_development_dependency "benchmark-ips" 31 | end 32 | -------------------------------------------------------------------------------- /gemfiles/rails52.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.2.0" 6 | gem "mysql2", ">= 0.4.4", "< 0.6.0" 7 | 8 | gemspec path: "../" 9 | -------------------------------------------------------------------------------- /gemfiles/rails60.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 6.0.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/rails61.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 6.1.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /lib/fresh_connection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | class << self 5 | def connection_manager 6 | if defined?(@connection_manager) 7 | @connection_manager 8 | else 9 | require 'fresh_connection/connection_manager' 10 | ConnectionManager 11 | end 12 | end 13 | 14 | def connection_manager=(mgr) 15 | FreshConnection::ReplicaConnectionHandler.instance.refresh_all 16 | @connection_manager = mgr 17 | end 18 | end 19 | end 20 | 21 | require 'fresh_connection/replica_connection_handler' 22 | require 'fresh_connection/abstract_connection_manager' 23 | require 'fresh_connection/connection_specification' 24 | require 'fresh_connection/extend' 25 | require 'fresh_connection/railtie' if defined?(Rails) 26 | -------------------------------------------------------------------------------- /lib/fresh_connection/abstract_connection_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | class AbstractConnectionManager 5 | attr_reader :spec_name 6 | 7 | def initialize(spec_name = nil) 8 | @spec_name = (spec_name || "replica").to_s 9 | end 10 | 11 | def replica_connection 12 | raise NotImplementedError 13 | end 14 | 15 | def put_aside! 16 | raise NotImplementedError 17 | end 18 | 19 | def clear_all_connections! 20 | raise NotImplementedError 21 | end 22 | 23 | def recovery? 24 | raise NotImplementedError 25 | end 26 | end 27 | end 28 | 29 | -------------------------------------------------------------------------------- /lib/fresh_connection/access_control.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | class AccessControl 5 | class << self 6 | RETRY_LIMIT = 3 7 | private_constant :RETRY_LIMIT 8 | 9 | def manage_access(model:, replica_access:, &block) 10 | return force_master_access(&block) if model.master_db_only? 11 | 12 | retry_count = 0 13 | begin 14 | access(replica_access, &block) 15 | rescue *catch_exceptions 16 | if recovery?(model.replica_spec_name) 17 | retry_count += 1 18 | retry if retry_count < RETRY_LIMIT 19 | end 20 | 21 | raise 22 | end 23 | end 24 | 25 | def replica_access? 26 | access_db == :replica 27 | end 28 | 29 | private 30 | 31 | def force_master_access(&block) 32 | switch_to(:master, &block) 33 | end 34 | 35 | def access(replica_access, &block) 36 | return yield if access_db 37 | 38 | db = replica_access ? :replica : :master 39 | switch_to(db, &block) 40 | end 41 | 42 | def switch_to(new_db) 43 | old_db = access_db 44 | access_to(new_db) 45 | yield 46 | ensure 47 | access_to(old_db) 48 | end 49 | 50 | def access_db 51 | Thread.current[:fresh_connection_access_target] 52 | end 53 | 54 | def access_to(db) 55 | Thread.current[:fresh_connection_access_target] = db 56 | end 57 | 58 | def recovery?(spec_name) 59 | FreshConnection::ReplicaConnectionHandler.instance.recovery?(spec_name) 60 | end 61 | 62 | def catch_exceptions 63 | return @catch_exceptions if defined?(@catch_exceptions) 64 | @catch_exceptions = [ 65 | ActiveRecord::StatementInvalid, 66 | ActiveRecord::ConnectionNotEstablished 67 | ] 68 | 69 | @catch_exceptions << ::Mysql2::Error if defined?(::Mysql2) 70 | 71 | if defined?(::PG) 72 | @catch_exceptions << ::PG::Error 73 | @catch_exceptions << ::PGError if defined?(::PGError) 74 | end 75 | 76 | @catch_exceptions 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/fresh_connection/connection_manager.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'fresh_connection/abstract_connection_manager' 3 | require 'fresh_connection/connection_specification' 4 | 5 | module FreshConnection 6 | class ConnectionManager < AbstractConnectionManager 7 | def initialize(*args) 8 | super 9 | 10 | spec = FreshConnection::ConnectionSpecification.new(spec_name).spec 11 | @pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec) 12 | end 13 | 14 | def replica_connection 15 | @pool.connection 16 | end 17 | 18 | def put_aside! 19 | return unless @pool.active_connection? 20 | 21 | conn = replica_connection 22 | return if conn.transaction_open? 23 | 24 | @pool.release_connection 25 | @pool.remove(conn) 26 | conn.disconnect! 27 | end 28 | 29 | def clear_all_connections! 30 | @pool.disconnect! 31 | end 32 | 33 | def recovery? 34 | c = replica_connection rescue nil 35 | return false if c && c.active? 36 | put_aside! 37 | true 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/fresh_connection/connection_specification.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'active_support' 3 | require 'active_support/core_ext' 4 | 5 | module FreshConnection 6 | class ConnectionSpecification 7 | def initialize(spec_name, modify_spec: nil) 8 | @spec_name = spec_name.to_s 9 | @modify_spec = modify_spec.with_indifferent_access if modify_spec 10 | end 11 | 12 | private 13 | 14 | def build_config 15 | config = base_config.with_indifferent_access 16 | 17 | s_config = replica_config(config) 18 | config = config.merge(s_config) if s_config 19 | 20 | config = config.merge(@modify_spec) if defined?(@modify_spec) 21 | 22 | config 23 | end 24 | 25 | def replica_config(config) 26 | if database_group_url 27 | config_from_url 28 | else 29 | config[@spec_name] 30 | end 31 | end 32 | 33 | def database_group_url 34 | ENV["DATABASE_#{@spec_name.upcase}_URL"] 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/fresh_connection/connection_specification/rails_60.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | class ConnectionSpecification 5 | module Rails60 6 | def spec 7 | ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(config_with_spec_name).spec(@spec_name.to_sym) 8 | end 9 | 10 | private 11 | 12 | def config_with_spec_name 13 | if defined?(ActiveRecord::DatabaseConfigurations) 14 | ActiveRecord::DatabaseConfigurations.new(@spec_name => build_config) 15 | else 16 | { @spec_name => build_config } 17 | end 18 | end 19 | 20 | def config_from_url 21 | ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(database_group_url).to_hash 22 | end 23 | 24 | def base_config 25 | ActiveRecord::Base.connection_pool.spec.config 26 | end 27 | end 28 | end 29 | end 30 | 31 | -------------------------------------------------------------------------------- /lib/fresh_connection/connection_specification/rails_61.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | class ConnectionSpecification 5 | module Rails61 6 | def spec 7 | db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(@spec_name.to_sym, ActiveRecord::Base.connection_pool.db_config.name, build_config) 8 | ActiveRecord::ConnectionAdapters::PoolConfig.new(ActiveRecord::Base, db_config) 9 | end 10 | 11 | private 12 | 13 | def config_from_url 14 | ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.new(database_group_url).to_hash 15 | end 16 | 17 | def base_config 18 | ActiveRecord::Base.connection_pool.db_config.configuration_hash 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/fresh_connection/executor_hook.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | class ExecutorHook 5 | class << self 6 | def run 7 | end 8 | 9 | def complete(*args) 10 | ReplicaConnectionHandler.instance.put_aside! 11 | end 12 | 13 | def install_executor_hooks 14 | ActiveSupport::Executor.register_hook(self) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'active_support' 3 | 4 | ActiveSupport.on_load(:active_record) do 5 | if respond_to?(:connection_handlers) && connection_handlers.empty? 6 | self.connection_handlers = { writing_role => ActiveRecord::Base.default_connection_handler } 7 | end 8 | 9 | require 'fresh_connection/extend/ar_base' 10 | require 'fresh_connection/extend/ar_relation' 11 | require 'fresh_connection/extend/ar_relation_merger' 12 | require 'fresh_connection/extend/ar_statement_cache' 13 | 14 | ActiveRecord::Base.extend FreshConnection::Extend::ArBase 15 | ActiveRecord::Relation.prepend FreshConnection::Extend::ArRelation 16 | ActiveRecord::Relation::Merger.prepend FreshConnection::Extend::ArRelationMerger 17 | ActiveRecord::StatementCache.prepend FreshConnection::Extend::ArStatementCache 18 | 19 | if ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR == 1 20 | require 'fresh_connection/extend/ar_connection_handler' 21 | ActiveRecord::ConnectionAdapters::ConnectionHandler.prepend( 22 | FreshConnection::Extend::ArConnectionHandler 23 | ) 24 | 25 | require 'fresh_connection/connection_specification/rails_61' 26 | FreshConnection::ConnectionSpecification.include( 27 | FreshConnection::ConnectionSpecification::Rails61 28 | ) 29 | else 30 | require 'fresh_connection/extend/ar_resolver' 31 | ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.prepend( 32 | FreshConnection::Extend::ArResolver 33 | ) 34 | 35 | require 'fresh_connection/connection_specification/rails_60' 36 | FreshConnection::ConnectionSpecification.include( 37 | FreshConnection::ConnectionSpecification::Rails60 38 | ) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/adapters/base_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | module Extend 5 | module BaseAdapter 6 | def self.prepended(base) 7 | base.send :attr_writer, :model_class 8 | end 9 | 10 | def log(*args) 11 | args[1] = "[#{__replica_spec_name}] #{args[1]}" if __replica_spec_name 12 | super 13 | end 14 | 15 | def select_all(*, **) 16 | __change_connection { super } 17 | end 18 | 19 | def select_value(*) 20 | __change_connection { super } 21 | end 22 | 23 | private 24 | 25 | def __replica_spec_name 26 | return nil if !defined?(@model_class) || !@model_class 27 | return nil unless FreshConnection::AccessControl.replica_access? 28 | @model_class.replica_spec_name 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/adapters/m2_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'fresh_connection/extend/adapters/base_adapter' 3 | 4 | module FreshConnection 5 | module Extend 6 | module M2Adapter 7 | private 8 | 9 | def __change_connection 10 | return yield unless FreshConnection::AccessControl.replica_access? 11 | 12 | master_connection = @connection 13 | begin 14 | replica_connection = @model_class.replica_connection 15 | @connection = replica_connection.raw_connection 16 | yield 17 | ensure 18 | @connection = master_connection 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/adapters/pg_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'fresh_connection/extend/adapters/base_adapter' 3 | 4 | module FreshConnection 5 | module Extend 6 | module PgAdapter 7 | private 8 | 9 | def __change_connection 10 | return yield unless FreshConnection::AccessControl.replica_access? 11 | 12 | master_connection = @connection 13 | master_statements = @statements 14 | begin 15 | replica_connection = @model_class.replica_connection 16 | @connection = replica_connection.raw_connection 17 | @statements = replica_connection.instance_variable_get(:@statements) 18 | yield 19 | ensure 20 | @connection = master_connection 21 | @statements = master_statements 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/ar_base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'fresh_connection/access_control' 3 | require 'fresh_connection/replica_connection_handler' 4 | 5 | module FreshConnection 6 | module Extend 7 | module ArBase 8 | def read_master 9 | all.read_master 10 | end 11 | 12 | def with_master(&block) 13 | FreshConnection::AccessControl.manage_access( 14 | model: self, 15 | replica_access: false, 16 | &block 17 | ) 18 | end 19 | 20 | def connection 21 | super.tap {|c| c.model_class = self } 22 | end 23 | 24 | def replica_connection 25 | __replica_handler.connection(replica_spec_name) 26 | end 27 | 28 | def clear_all_replica_connections! 29 | __replica_handler.clear_all_connections! 30 | end 31 | 32 | def establish_fresh_connection(spec_name = nil) 33 | spec_name = spec_name.to_s 34 | spec_name = "replica" if spec_name.empty? 35 | @_replica_spec_name = spec_name 36 | 37 | __replica_handler.refresh_connection(replica_spec_name) 38 | end 39 | 40 | def master_db_only! 41 | @_fresh_connection_master_only = true 42 | end 43 | 44 | def master_db_only? 45 | @_fresh_connection_master_only ||= 46 | (self != ActiveRecord::Base && superclass.master_db_only?) 47 | end 48 | 49 | def replica_spec_name 50 | @_replica_spec_name ||= __search_replica_spec_name 51 | end 52 | 53 | private 54 | 55 | def __search_replica_spec_name 56 | if self == ActiveRecord::Base 57 | "replica" 58 | else 59 | superclass.replica_spec_name 60 | end 61 | end 62 | 63 | def __replica_handler 64 | FreshConnection::ReplicaConnectionHandler.instance 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/ar_connection_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | module Extend 5 | module ArConnectionHandler 6 | private def resolve_pool_config(*args) 7 | pool_config = super 8 | 9 | case pool_config.db_config.adapter.to_s 10 | when "mysql", "mysql2" 11 | require 'fresh_connection/extend/adapters/m2_adapter' 12 | __extend_adapter_by_fc(::ActiveRecord::ConnectionAdapters::Mysql2Adapter, M2Adapter) 13 | when "postgresql" 14 | require 'fresh_connection/extend/adapters/pg_adapter' 15 | __extend_adapter_by_fc(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter, PgAdapter) 16 | else 17 | raise NotImplementedError, "This adapter('#{pool_config.db_config.adapter}') is not supported. If you specified the mysql or postgres adapter, it's probably a bug in FreshConnection. Please teach me (https://github.com/tsukasaoishi/fresh_connection/issues/new)" 18 | end 19 | 20 | pool_config 21 | end 22 | 23 | def __extend_adapter_by_fc(klass, extend_adapter) 24 | return if klass.include?(extend_adapter) 25 | klass.prepend BaseAdapter 26 | klass.prepend extend_adapter 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/ar_relation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | module Extend 5 | module ArRelation 6 | def calculate(*) 7 | manage_access { super } 8 | end 9 | 10 | def exists?(*) 11 | manage_access { super } 12 | end 13 | 14 | def pluck(*) 15 | manage_access { super } 16 | end 17 | 18 | def read_master 19 | spawn.read_master! 20 | end 21 | 22 | def read_master! 23 | self.read_master_value = true 24 | self 25 | end 26 | 27 | def read_master_value 28 | @values[:read_master] 29 | end 30 | 31 | def read_master_value=(value) 32 | raise ImmutableRelation if @loaded 33 | @values[:read_master] = value 34 | end 35 | 36 | def manage_access(replica_access: enable_replica_access, &block) 37 | FreshConnection::AccessControl.manage_access( 38 | model: @klass, 39 | replica_access: replica_access, 40 | &block 41 | ) 42 | end 43 | 44 | private 45 | 46 | def exec_queries 47 | manage_access { super } 48 | end 49 | 50 | def enable_replica_access 51 | connection.open_transactions.zero? && !read_master_value 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/ar_relation_merger.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | module Extend 5 | module ArRelationMerger 6 | private 7 | 8 | def merge_single_values 9 | relation.read_master_value = values[:read_master] unless relation.read_master_value 10 | super 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/ar_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | module Extend 5 | module ArResolver 6 | def spec(*args) 7 | specification = super 8 | 9 | case specification.config[:adapter].to_s 10 | when "mysql", "mysql2" 11 | require 'fresh_connection/extend/adapters/m2_adapter' 12 | __extend_adapter_by_fc(::ActiveRecord::ConnectionAdapters::Mysql2Adapter, M2Adapter) 13 | when "postgresql" 14 | require 'fresh_connection/extend/adapters/pg_adapter' 15 | __extend_adapter_by_fc(::ActiveRecord::ConnectionAdapters::PostgreSQLAdapter, PgAdapter) 16 | else 17 | raise NotImplementedError, "This adapter('#{specification.config[:adapter]}') is not supported. If you specified the mysql or postgres adapter, it's probably a bug in FreshConnection. Please teach me (https://github.com/tsukasaoishi/fresh_connection/issues/new)" 18 | end 19 | 20 | specification 21 | end 22 | 23 | def __extend_adapter_by_fc(klass, extend_adapter) 24 | return if klass.include?(extend_adapter) 25 | klass.prepend BaseAdapter 26 | klass.prepend extend_adapter 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/fresh_connection/extend/ar_statement_cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | module Extend 5 | module ArStatementCache 6 | def execute(params, connection, &block) 7 | klass.all.manage_access { super } 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/fresh_connection/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'fresh_connection/executor_hook' 3 | 4 | module FreshConnection 5 | class Railtie < Rails::Railtie 6 | initializer "fresh_connection.configure_rails_initialization" do |app| 7 | ActiveSupport.on_load(:active_record) do 8 | FreshConnection::ExecutorHook.install_executor_hooks 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/fresh_connection/replica_connection_handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'concurrent' 3 | require 'singleton' 4 | 5 | module FreshConnection 6 | class ReplicaConnectionHandler 7 | include Singleton 8 | 9 | def initialize 10 | @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k| 11 | h[k] = Concurrent::Map.new(initial_capacity: 2) 12 | end 13 | end 14 | 15 | def refresh_all 16 | owner_to_pool.clear 17 | end 18 | 19 | def refresh_connection(spec_name) 20 | remove_connection(spec_name.to_s) 21 | end 22 | 23 | def connection(spec_name) 24 | detect_connection_manager(spec_name).replica_connection 25 | end 26 | 27 | def clear_all_connections! 28 | all_connection_managers do |connection_manager| 29 | connection_manager.clear_all_connections! 30 | end 31 | end 32 | 33 | def recovery?(spec_name) 34 | detect_connection_manager(spec_name).recovery? 35 | end 36 | 37 | def put_aside! 38 | all_connection_managers do |connection_manager| 39 | connection_manager.put_aside! 40 | end 41 | end 42 | 43 | private 44 | 45 | def remove_connection(spec_name) 46 | pool = owner_to_pool.delete(spec_name.to_s) 47 | return unless pool 48 | 49 | pool.clear_all_connections! 50 | end 51 | 52 | def all_connection_managers 53 | owner_to_pool.each_value do |connection_manager| 54 | yield(connection_manager) 55 | end 56 | end 57 | 58 | def detect_connection_manager(spec_name) 59 | spec_name = spec_name.to_s 60 | 61 | cm = owner_to_pool[spec_name] 62 | return cm if cm 63 | 64 | refresh_connection(spec_name) 65 | 66 | message_bus = ActiveSupport::Notifications.instrumenter 67 | payload = { 68 | connection_id: object_id, 69 | spec_name: spec_name 70 | } 71 | 72 | message_bus.instrument("!connection.active_record", payload) do 73 | cm = FreshConnection.connection_manager.new(spec_name) 74 | end 75 | 76 | owner_to_pool[spec_name] = cm 77 | end 78 | 79 | def owner_to_pool 80 | @owner_to_pool[Process.pid] 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/fresh_connection/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module FreshConnection 4 | VERSION = "3.1.3" 5 | end 6 | 7 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukasaoishi/fresh_connection/50f6d1562d8dee1f70de9824b6d987835265a8f6/log/.gitkeep -------------------------------------------------------------------------------- /test/access_control/access_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AccessTest < Minitest::Test 4 | def setup 5 | super 6 | @ac = FreshConnection::AccessControl 7 | end 8 | 9 | test "persisted first state(replica)" do 10 | ret = [] 11 | @ac.send(:access, true) do 12 | @ac.send(:access, true) do 13 | ret << @ac.replica_access? 14 | @ac.send(:access, false) do 15 | ret << @ac.replica_access? 16 | end 17 | end 18 | end 19 | 20 | assert ret.all?{|item| item} 21 | end 22 | 23 | test "persisted first state(master)" do 24 | ret = [] 25 | @ac.send(:access, false) do 26 | @ac.send(:access, true) do 27 | ret << @ac.replica_access? 28 | @ac.send(:access, false) do 29 | ret << @ac.replica_access? 30 | end 31 | end 32 | end 33 | 34 | refute ret.all?{|item| item} 35 | end 36 | 37 | test "outside is always master" do 38 | ret = [] 39 | ret << @ac.replica_access? 40 | @ac.send(:access, true) {} 41 | ret << @ac.replica_access? 42 | 43 | refute ret.all?{|item| item} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/access_control/force_master_access_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ForceMasterAccessTest < Minitest::Test 4 | 5 | def setup 6 | super 7 | @ac = FreshConnection::AccessControl 8 | end 9 | 10 | test "forced master state" do 11 | @ac.send(:access, true) do 12 | @ac.send(:force_master_access) do 13 | refute @ac.replica_access? 14 | end 15 | end 16 | end 17 | 18 | test "not effect outside" do 19 | @ac.send(:access, true) do 20 | @ac.send(:force_master_access) {} 21 | assert @ac.replica_access? 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/ar_base/ar_abstract_adapter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AbstractAdapterTest < Minitest::Test 4 | class FakeAddress < ActiveRecord::Base 5 | self.table_name = :addresses 6 | establish_fresh_connection :fake_replica 7 | end 8 | 9 | def setup 10 | ActiveRecord::Base.connection.clear_query_cache 11 | end 12 | 13 | test "cache_query is correct after select once" do 14 | filename = File.join(__dir__, "../../log/sql.log") 15 | 16 | Address.cache do 17 | Address.find(1) 18 | Address.find(1) 19 | last_line = `tail -1 #{filename}` 20 | assert_match(/CACHE/, last_line) 21 | end 22 | end 23 | 24 | test "cache_query is correct after master update" do 25 | old_pref = SecureRandom.hex(3) 26 | a = FakeAddress.create(prefecture: old_pref) 27 | 28 | Address.cache do 29 | new_pref = old_pref + "1" 30 | b = FakeAddress.find(a.id) 31 | b.prefecture = new_pref 32 | b.save! 33 | 34 | address = FakeAddress.find(a.id) 35 | assert_equal new_pref, address.prefecture 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/ar_base/master_db_only_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MasterDbOnlyTest < Minitest::Test 4 | 5 | class User2 < Parent 6 | self.table_name = :users 7 | end 8 | 9 | test "childrend of master_db_only class is master_db_only" do 10 | begin 11 | assert !(User2.master_db_only?) 12 | Parent.master_db_only! 13 | assert User2.master_db_only? 14 | ensure 15 | Parent.instance_variable_set(:@_fresh_connection_master_only, nil) 16 | end 17 | end 18 | 19 | test "not effect other class" do 20 | begin 21 | Parent.master_db_only! 22 | refute Address.master_db_only? 23 | ensure 24 | Parent.instance_variable_set(:@_fresh_connection_master_only, nil) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/ar_base/replica_connection_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ReplicaConnectionTest < Minitest::Test 4 | test "return DB Adapter object" do 5 | case ENV['DB_ADAPTER'] 6 | when 'mysql2' 7 | assert_kind_of ActiveRecord::ConnectionAdapters::Mysql2Adapter, User.replica_connection 8 | when 'postgresql' 9 | assert_kind_of ActiveRecord::ConnectionAdapters::PostgreSQLAdapter, User.replica_connection 10 | end 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /test/ar_base/replica_spec_name_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ReplicaSpecNameTest < Minitest::Test 4 | class Tel2 < Replica2 5 | self.table_name = :tel 6 | end 7 | 8 | test "equal replica_spec_name of establish_fresh_connection" do 9 | assert_equal "replica1", User.replica_spec_name 10 | assert_equal "replica2", Tel.replica_spec_name 11 | end 12 | 13 | test "equal 'replica' when not specific replica_spec_name" do 14 | Tel2.establish_fresh_connection 15 | assert_equal "replica", Tel2.replica_spec_name 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/ar_relation/merge_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MergeTest < Minitest::Test 4 | test "enable merge of read_master" do 5 | name = Address.all.merge(Address.read_master).first.prefecture 6 | assert name.include?("master") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/config/database_postgresql.yml.travis: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | encoding: unicode 4 | database: fresh_connection_test_master 5 | pool: 5 6 | username: postgres 7 | 8 | replica1: 9 | database: fresh_connection_test_replica1 10 | 11 | replica2: 12 | database: fresh_connection_test_replica2 13 | 14 | -------------------------------------------------------------------------------- /test/config/mysql_schema.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 5.6.15, for osx10.9 (x86_64) 2 | -- 3 | -- Host: localhost Database: fresh_connection_test_master 4 | -- ------------------------------------------------------ 5 | -- Server version 5.6.15 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES utf8 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Current Database: `fresh_connection_test_master` 20 | -- 21 | 22 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `fresh_connection_test_master` /*!40100 DEFAULT CHARACTER SET utf8 */; 23 | 24 | SYSTEM echo "Loading master data set" 25 | 26 | USE `fresh_connection_test_master`; 27 | 28 | -- 29 | -- Table structure for table `addresses` 30 | -- 31 | 32 | DROP TABLE IF EXISTS `addresses`; 33 | /*!40101 SET @saved_cs_client = @@character_set_client */; 34 | /*!40101 SET character_set_client = utf8 */; 35 | CREATE TABLE `addresses` ( 36 | `id` int(11) NOT NULL AUTO_INCREMENT, 37 | `user_id` int(11) NOT NULL DEFAULT '0', 38 | `prefecture` varchar(255) NOT NULL DEFAULT '', 39 | `created_at` datetime DEFAULT NULL, 40 | `updated_at` datetime DEFAULT NULL, 41 | PRIMARY KEY (`id`) 42 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 43 | /*!40101 SET character_set_client = @saved_cs_client */; 44 | 45 | -- 46 | -- Dumping data for table `addresses` 47 | -- 48 | 49 | LOCK TABLES `addresses` WRITE; 50 | /*!40000 ALTER TABLE `addresses` DISABLE KEYS */; 51 | INSERT INTO `addresses` VALUES (1,1,'Tokyo (master)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 52 | /*!40000 ALTER TABLE `addresses` ENABLE KEYS */; 53 | UNLOCK TABLES; 54 | 55 | -- 56 | -- Table structure for table `tels` 57 | -- 58 | 59 | DROP TABLE IF EXISTS `tels`; 60 | /*!40101 SET @saved_cs_client = @@character_set_client */; 61 | /*!40101 SET character_set_client = utf8 */; 62 | CREATE TABLE `tels` ( 63 | `id` int(11) NOT NULL AUTO_INCREMENT, 64 | `user_id` int(11) NOT NULL DEFAULT '0', 65 | `number` varchar(255) NOT NULL DEFAULT '', 66 | `created_at` datetime DEFAULT NULL, 67 | `updated_at` datetime DEFAULT NULL, 68 | PRIMARY KEY (`id`) 69 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; 70 | /*!40101 SET character_set_client = @saved_cs_client */; 71 | 72 | -- 73 | -- Dumping data for table `tels` 74 | -- 75 | 76 | LOCK TABLES `tels` WRITE; 77 | /*!40000 ALTER TABLE `tels` DISABLE KEYS */; 78 | INSERT INTO `tels` VALUES (1,1,'03-1111-1111 (master)','2014-04-10 07:24:16','2014-04-10 07:24:16'),(2,1,'03-1111-1112 (master)','2014-04-10 07:24:16','2014-04-10 07:24:16'),(3,1,'03-1111-1113 (master)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 79 | /*!40000 ALTER TABLE `tels` ENABLE KEYS */; 80 | UNLOCK TABLES; 81 | 82 | -- 83 | -- Table structure for table `users` 84 | -- 85 | 86 | DROP TABLE IF EXISTS `users`; 87 | /*!40101 SET @saved_cs_client = @@character_set_client */; 88 | /*!40101 SET character_set_client = utf8 */; 89 | CREATE TABLE `users` ( 90 | `id` int(11) NOT NULL AUTO_INCREMENT, 91 | `name` varchar(255) NOT NULL DEFAULT '', 92 | `created_at` datetime DEFAULT NULL, 93 | `updated_at` datetime DEFAULT NULL, 94 | PRIMARY KEY (`id`) 95 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 96 | /*!40101 SET character_set_client = @saved_cs_client */; 97 | 98 | -- 99 | -- Dumping data for table `users` 100 | -- 101 | 102 | LOCK TABLES `users` WRITE; 103 | /*!40000 ALTER TABLE `users` DISABLE KEYS */; 104 | INSERT INTO `users` VALUES (1,'Tsukasa (master)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 105 | INSERT INTO `users` VALUES (2,'Other','2015-01-16 07:24:16','2014-04-10 07:24:16'); 106 | /*!40000 ALTER TABLE `users` ENABLE KEYS */; 107 | UNLOCK TABLES; 108 | 109 | -- 110 | -- Current Database: `fresh_connection_test_replica1` 111 | -- 112 | 113 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `fresh_connection_test_replica1` /*!40100 DEFAULT CHARACTER SET utf8 */; 114 | 115 | SYSTEM echo "Loading replica1 data set" 116 | 117 | USE `fresh_connection_test_replica1`; 118 | 119 | -- 120 | -- Table structure for table `addresses` 121 | -- 122 | 123 | DROP TABLE IF EXISTS `addresses`; 124 | /*!40101 SET @saved_cs_client = @@character_set_client */; 125 | /*!40101 SET character_set_client = utf8 */; 126 | CREATE TABLE `addresses` ( 127 | `id` int(11) NOT NULL AUTO_INCREMENT, 128 | `user_id` int(11) NOT NULL DEFAULT '0', 129 | `prefecture` varchar(255) NOT NULL DEFAULT '', 130 | `created_at` datetime DEFAULT NULL, 131 | `updated_at` datetime DEFAULT NULL, 132 | PRIMARY KEY (`id`) 133 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 134 | /*!40101 SET character_set_client = @saved_cs_client */; 135 | 136 | -- 137 | -- Dumping data for table `addresses` 138 | -- 139 | 140 | LOCK TABLES `addresses` WRITE; 141 | /*!40000 ALTER TABLE `addresses` DISABLE KEYS */; 142 | INSERT INTO `addresses` VALUES (1,1,'Tokyo (replica1)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 143 | /*!40000 ALTER TABLE `addresses` ENABLE KEYS */; 144 | UNLOCK TABLES; 145 | 146 | -- 147 | -- Table structure for table `tels` 148 | -- 149 | 150 | DROP TABLE IF EXISTS `tels`; 151 | /*!40101 SET @saved_cs_client = @@character_set_client */; 152 | /*!40101 SET character_set_client = utf8 */; 153 | CREATE TABLE `tels` ( 154 | `id` int(11) NOT NULL AUTO_INCREMENT, 155 | `user_id` int(11) NOT NULL DEFAULT '0', 156 | `number` varchar(255) NOT NULL DEFAULT '', 157 | `created_at` datetime DEFAULT NULL, 158 | `updated_at` datetime DEFAULT NULL, 159 | PRIMARY KEY (`id`) 160 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; 161 | /*!40101 SET character_set_client = @saved_cs_client */; 162 | 163 | -- 164 | -- Dumping data for table `tels` 165 | -- 166 | 167 | LOCK TABLES `tels` WRITE; 168 | /*!40000 ALTER TABLE `tels` DISABLE KEYS */; 169 | INSERT INTO `tels` VALUES (1,1,'03-1111-1111 (replica1)','2014-04-10 07:24:16','2014-04-10 07:24:16'),(2,1,'03-1111-1112 (replica1)','2014-04-10 07:24:16','2014-04-10 07:24:16'),(3,1,'03-1111-1113 (replica1)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 170 | /*!40000 ALTER TABLE `tels` ENABLE KEYS */; 171 | UNLOCK TABLES; 172 | 173 | -- 174 | -- Table structure for table `users` 175 | -- 176 | 177 | DROP TABLE IF EXISTS `users`; 178 | /*!40101 SET @saved_cs_client = @@character_set_client */; 179 | /*!40101 SET character_set_client = utf8 */; 180 | CREATE TABLE `users` ( 181 | `id` int(11) NOT NULL AUTO_INCREMENT, 182 | `name` varchar(255) NOT NULL DEFAULT '', 183 | `created_at` datetime DEFAULT NULL, 184 | `updated_at` datetime DEFAULT NULL, 185 | PRIMARY KEY (`id`) 186 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 187 | /*!40101 SET character_set_client = @saved_cs_client */; 188 | 189 | -- 190 | -- Dumping data for table `users` 191 | -- 192 | 193 | LOCK TABLES `users` WRITE; 194 | /*!40000 ALTER TABLE `users` DISABLE KEYS */; 195 | INSERT INTO `users` VALUES (1,'Tsukasa (replica1)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 196 | INSERT INTO `users` VALUES (2,'Other','2015-01-16 07:24:16','2014-04-10 07:24:16'); 197 | INSERT INTO `users` VALUES (3,'Other','2015-01-16 07:24:16','2014-04-10 07:24:16'); 198 | /*!40000 ALTER TABLE `users` ENABLE KEYS */; 199 | UNLOCK TABLES; 200 | 201 | -- 202 | -- Current Database: `fresh_connection_test_replica2` 203 | -- 204 | 205 | CREATE DATABASE /*!32312 IF NOT EXISTS*/ `fresh_connection_test_replica2` /*!40100 DEFAULT CHARACTER SET utf8 */; 206 | 207 | SYSTEM echo "Loading replica2 data set" 208 | USE `fresh_connection_test_replica2`; 209 | 210 | -- 211 | -- Table structure for table `addresses` 212 | -- 213 | 214 | DROP TABLE IF EXISTS `addresses`; 215 | /*!40101 SET @saved_cs_client = @@character_set_client */; 216 | /*!40101 SET character_set_client = utf8 */; 217 | CREATE TABLE `addresses` ( 218 | `id` int(11) NOT NULL AUTO_INCREMENT, 219 | `user_id` int(11) NOT NULL DEFAULT '0', 220 | `prefecture` varchar(255) NOT NULL DEFAULT '', 221 | `created_at` datetime DEFAULT NULL, 222 | `updated_at` datetime DEFAULT NULL, 223 | PRIMARY KEY (`id`) 224 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 225 | /*!40101 SET character_set_client = @saved_cs_client */; 226 | 227 | -- 228 | -- Dumping data for table `addresses` 229 | -- 230 | 231 | LOCK TABLES `addresses` WRITE; 232 | /*!40000 ALTER TABLE `addresses` DISABLE KEYS */; 233 | INSERT INTO `addresses` VALUES (1,1,'Tokyo (replica2)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 234 | /*!40000 ALTER TABLE `addresses` ENABLE KEYS */; 235 | UNLOCK TABLES; 236 | 237 | -- 238 | -- Table structure for table `tels` 239 | -- 240 | 241 | DROP TABLE IF EXISTS `tels`; 242 | /*!40101 SET @saved_cs_client = @@character_set_client */; 243 | /*!40101 SET character_set_client = utf8 */; 244 | CREATE TABLE `tels` ( 245 | `id` int(11) NOT NULL AUTO_INCREMENT, 246 | `user_id` int(11) NOT NULL DEFAULT '0', 247 | `number` varchar(255) NOT NULL DEFAULT '', 248 | `created_at` datetime DEFAULT NULL, 249 | `updated_at` datetime DEFAULT NULL, 250 | PRIMARY KEY (`id`) 251 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; 252 | /*!40101 SET character_set_client = @saved_cs_client */; 253 | 254 | -- 255 | -- Dumping data for table `tels` 256 | -- 257 | 258 | LOCK TABLES `tels` WRITE; 259 | /*!40000 ALTER TABLE `tels` DISABLE KEYS */; 260 | INSERT INTO `tels` VALUES (1,1,'03-1111-1111 (replica2)','2014-04-10 07:24:16','2014-04-10 07:24:16'),(2,1,'03-1111-1112 (replica2)','2014-04-10 07:24:16','2014-04-10 07:24:16'),(3,1,'03-1111-1113 (replica2)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 261 | /*!40000 ALTER TABLE `tels` ENABLE KEYS */; 262 | UNLOCK TABLES; 263 | 264 | -- 265 | -- Table structure for table `users` 266 | -- 267 | 268 | DROP TABLE IF EXISTS `users`; 269 | /*!40101 SET @saved_cs_client = @@character_set_client */; 270 | /*!40101 SET character_set_client = utf8 */; 271 | CREATE TABLE `users` ( 272 | `id` int(11) NOT NULL AUTO_INCREMENT, 273 | `name` varchar(255) NOT NULL DEFAULT '', 274 | `created_at` datetime DEFAULT NULL, 275 | `updated_at` datetime DEFAULT NULL, 276 | PRIMARY KEY (`id`) 277 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 278 | /*!40101 SET character_set_client = @saved_cs_client */; 279 | 280 | -- 281 | -- Dumping data for table `users` 282 | -- 283 | 284 | LOCK TABLES `users` WRITE; 285 | /*!40000 ALTER TABLE `users` DISABLE KEYS */; 286 | INSERT INTO `users` VALUES (1,'Tsukasa (replica2)','2014-04-10 07:24:16','2014-04-10 07:24:16'); 287 | INSERT INTO `users` VALUES (2,'Other','2015-01-16 07:24:16','2014-04-10 07:24:16'); 288 | INSERT INTO `users` VALUES (3,'Other','2015-01-16 07:24:16','2014-04-10 07:24:16'); 289 | /*!40000 ALTER TABLE `users` ENABLE KEYS */; 290 | UNLOCK TABLES; 291 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 292 | 293 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 294 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 295 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 296 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 297 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 298 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 299 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 300 | 301 | -- Dump completed on 2014-04-10 21:36:33 302 | -------------------------------------------------------------------------------- /test/config/prepare.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'erb' 3 | require 'active_record' 4 | require 'active_record/base' 5 | require 'fresh_connection' 6 | 7 | if ActiveRecord::VERSION::MAJOR == 6 && ActiveRecord::VERSION::MINOR == 1 8 | db_config = ActiveRecord::DatabaseConfigurations::ConnectionUrlResolver.new(ENV["DATABASE_URL"]).to_hash 9 | else 10 | db_config = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(ENV["DATABASE_URL"]).to_hash 11 | end 12 | 13 | REPLICA_NAMES = %w( replica1 replica2 fake_replica ) 14 | 15 | case db_config['adapter'] 16 | when 'mysql2' 17 | work = [] 18 | work << "-u#{db_config["username"]}" if db_config["username"] 19 | work << "-p#{db_config["password"]}" if db_config["password"] 20 | work << "-h#{db_config["host"]}" if db_config["host"] 21 | work << "-P#{db_config["port"]}" if db_config["port"] 22 | command = work.join(" ") 23 | 24 | system("mysql #{command} < test/config/mysql_schema.sql") 25 | when 'postgresql' 26 | puts "[postgresql]" 27 | work = [] 28 | work << "-U#{db_config["username"]}" if db_config["username"] 29 | work << "-W#{db_config["password"]}" if db_config["password"] 30 | work << "-h#{db_config["host"]}" if db_config["host"] 31 | work << "-p#{db_config["port"]}" if db_config["port"] 32 | command = work.join(" ") 33 | 34 | 35 | { 36 | fresh_connection_test_master: "psql_test_master.sql", 37 | fresh_connection_test_replica1: "psql_test_replica1.sql", 38 | fresh_connection_test_replica2: "psql_test_replica2.sql" 39 | }.each do |db, file| 40 | if system("psql -l #{command} | grep #{db}") 41 | puts "Dropping database #{db}" 42 | system("dropdb #{command} #{db}") 43 | end 44 | 45 | puts "Creating database #{db}" 46 | system("createdb #{command} #{db}") 47 | system("psql -q #{command} -f test/config/#{file} #{db}") 48 | end 49 | end 50 | 51 | module ActiveRecord 52 | class Base 53 | establish_connection 54 | establish_fresh_connection :replica1 55 | end 56 | end 57 | 58 | class Parent < ActiveRecord::Base 59 | self.abstract_class = true 60 | end 61 | 62 | class Replica2 < ActiveRecord::Base 63 | self.abstract_class = true 64 | establish_fresh_connection :replica2 65 | end 66 | 67 | class User < ActiveRecord::Base 68 | has_one :address 69 | has_many :tels 70 | end 71 | 72 | class Address < ActiveRecord::Base 73 | belongs_to :user 74 | end 75 | 76 | class Tel < Replica2 77 | belongs_to :user 78 | end 79 | 80 | if db_config['adapter'] == "postgresql" 81 | ActiveRecord::Base.connection.execute("select setval('addresses_id_seq',(select max(id) from addresses))") 82 | end 83 | 84 | require "support/extend_minitest" 85 | require "support/active_record_logger" 86 | -------------------------------------------------------------------------------- /test/config/psql_test_master.sql: -------------------------------------------------------------------------------- 1 | -- create the master data set 2 | \echo Setting up master data set 3 | 4 | \ir psql_test_schema_setup1.sql 5 | 6 | COPY addresses (id, user_id, prefecture, created_at, updated_at) FROM stdin; 7 | 1 1 Tokyo (master) 2014-04-10 07:24:16 2014-04-10 07:24:16 8 | \. 9 | 10 | COPY tels (id, user_id, number, created_at, updated_at) FROM stdin; 11 | 1 1 03-1111-1111 (master) 2014-04-10 07:24:16 2014-04-10 07:24:16 12 | 2 1 03-1111-1112 (master) 2014-04-10 07:24:16 2014-04-10 07:24:16 13 | 3 1 03-1111-1113 (master) 2014-04-10 07:24:16 2014-04-10 07:24:16 14 | \. 15 | 16 | COPY users (id, name, created_at, updated_at) FROM stdin; 17 | 1 Tsukasa (master) 2014-04-10 07:24:16 2014-04-10 07:24:16 18 | 2 Other 2014-04-10 07:24:16 2014-04-10 07:24:16 19 | \. 20 | 21 | \ir psql_test_schema_setup2.sql 22 | -------------------------------------------------------------------------------- /test/config/psql_test_replica1.sql: -------------------------------------------------------------------------------- 1 | -- create replica1 data set 2 | \echo Setting up replica1 data set 3 | 4 | \ir psql_test_schema_setup1.sql 5 | 6 | COPY addresses (id, user_id, prefecture, created_at, updated_at) FROM stdin; 7 | 1 1 Tokyo (replica1) 2014-04-10 07:24:16 2014-04-10 07:24:16 8 | \. 9 | 10 | COPY tels (id, user_id, number, created_at, updated_at) FROM stdin; 11 | 1 1 03-1111-1111 (replica1) 2014-04-10 07:24:16 2014-04-10 07:24:16 12 | 2 1 03-1111-1112 (replica1) 2014-04-10 07:24:16 2014-04-10 07:24:16 13 | 3 1 03-1111-1113 (replica1) 2014-04-10 07:24:16 2014-04-10 07:24:16 14 | \. 15 | 16 | COPY users (id, name, created_at, updated_at) FROM stdin; 17 | 1 Tsukasa (replica1) 2014-04-10 07:24:16 2014-04-10 07:24:16 18 | 2 Other 2014-04-10 07:24:16 2014-04-10 07:24:16 19 | 3 Other 2014-04-10 07:24:16 2014-04-10 07:24:16 20 | \. 21 | 22 | \ir psql_test_schema_setup2.sql 23 | 24 | -------------------------------------------------------------------------------- /test/config/psql_test_replica2.sql: -------------------------------------------------------------------------------- 1 | -- setup replica2 data set 2 | 3 | \echo Setting up replica2 data set 4 | 5 | \ir psql_test_schema_setup1.sql 6 | 7 | COPY addresses (id, user_id, prefecture, created_at, updated_at) FROM stdin; 8 | 1 1 Tokyo (replica2) 2014-04-10 07:24:16 2014-04-10 07:24:16 9 | \. 10 | 11 | COPY tels (id, user_id, number, created_at, updated_at) FROM stdin; 12 | 1 1 03-1111-1111 (replica2) 2014-04-10 07:24:16 2014-04-10 07:24:16 13 | 2 1 03-1111-1112 (replica2) 2014-04-10 07:24:16 2014-04-10 07:24:16 14 | 3 1 03-1111-1113 (replica2) 2014-04-10 07:24:16 2014-04-10 07:24:16 15 | \. 16 | 17 | COPY users (id, name, created_at, updated_at) FROM stdin; 18 | 1 Tsukasa (replica2) 2014-04-10 07:24:16 2014-04-10 07:24:16 19 | 2 Other 2014-04-10 07:24:16 2014-04-10 07:24:16 20 | 3 Other 2014-04-10 07:24:16 2014-04-10 07:24:16 21 | \. 22 | 23 | \ir psql_test_schema_setup2.sql 24 | -------------------------------------------------------------------------------- /test/config/psql_test_schema_setup1.sql: -------------------------------------------------------------------------------- 1 | -- create the schema (part 1) 2 | 3 | \set QUIET 1 4 | \timing off 5 | 6 | SET statement_timeout = 0; 7 | SET lock_timeout = 0; 8 | SET client_encoding = 'UTF8'; 9 | SET standard_conforming_strings = on; 10 | SET check_function_bodies = false; 11 | SET client_min_messages = warning; 12 | SET row_security = off; 13 | 14 | 15 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 16 | 17 | COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; 18 | 19 | SET search_path = public, pg_catalog; 20 | 21 | SET default_tablespace = ''; 22 | 23 | SET default_with_oids = false; 24 | 25 | DROP TABLE IF EXISTS addresses CASCADE; 26 | DROP TABLE IF EXISTS tels CASCADE; 27 | DROP TABLE IF EXISTS users CASCADE; 28 | 29 | CREATE TABLE addresses ( 30 | id integer NOT NULL, 31 | user_id integer DEFAULT 0 NOT NULL, 32 | prefecture character varying DEFAULT ''::character varying NOT NULL, 33 | created_at timestamp without time zone NOT NULL, 34 | updated_at timestamp without time zone NOT NULL 35 | ); 36 | 37 | CREATE SEQUENCE addresses_id_seq 38 | START WITH 1 39 | INCREMENT BY 1 40 | NO MINVALUE 41 | NO MAXVALUE 42 | CACHE 1; 43 | 44 | ALTER SEQUENCE addresses_id_seq OWNED BY addresses.id; 45 | 46 | CREATE TABLE tels ( 47 | id integer NOT NULL, 48 | user_id integer DEFAULT 0 NOT NULL, 49 | number character varying DEFAULT ''::character varying NOT NULL, 50 | created_at timestamp without time zone NOT NULL, 51 | updated_at timestamp without time zone NOT NULL 52 | ); 53 | 54 | CREATE SEQUENCE tels_id_seq 55 | START WITH 1 56 | INCREMENT BY 1 57 | NO MINVALUE 58 | NO MAXVALUE 59 | CACHE 1; 60 | 61 | ALTER SEQUENCE tels_id_seq OWNED BY tels.id; 62 | 63 | CREATE TABLE users ( 64 | id integer NOT NULL, 65 | name character varying DEFAULT ''::character varying NOT NULL, 66 | created_at timestamp without time zone NOT NULL, 67 | updated_at timestamp without time zone NOT NULL 68 | ); 69 | 70 | CREATE SEQUENCE users_id_seq 71 | START WITH 1 72 | INCREMENT BY 1 73 | NO MINVALUE 74 | NO MAXVALUE 75 | CACHE 1; 76 | 77 | ALTER SEQUENCE users_id_seq OWNED BY users.id; 78 | 79 | ALTER TABLE ONLY addresses ALTER COLUMN id SET DEFAULT nextval('addresses_id_seq'::regclass); 80 | 81 | ALTER TABLE ONLY tels ALTER COLUMN id SET DEFAULT nextval('tels_id_seq'::regclass); 82 | 83 | ALTER TABLE ONLY users ALTER COLUMN id SET DEFAULT nextval('users_id_seq'::regclass); 84 | -------------------------------------------------------------------------------- /test/config/psql_test_schema_setup2.sql: -------------------------------------------------------------------------------- 1 | -- part2 setup of the test schema, post data load 2 | 3 | \o /dev/null 4 | SELECT pg_catalog.setval('addresses_id_seq', 1, false); 5 | SELECT pg_catalog.setval('tels_id_seq', 1, false); 6 | SELECT pg_catalog.setval('users_id_seq', 1, false); 7 | \o 8 | 9 | ALTER TABLE ONLY addresses 10 | ADD CONSTRAINT addresses_pkey PRIMARY KEY (id); 11 | 12 | ALTER TABLE ONLY tels 13 | ADD CONSTRAINT tels_pkey PRIMARY KEY (id); 14 | 15 | ALTER TABLE ONLY users 16 | ADD CONSTRAINT users_pkey PRIMARY KEY (id); 17 | 18 | REVOKE ALL ON SCHEMA public FROM PUBLIC; 19 | GRANT ALL ON SCHEMA public TO PUBLIC; 20 | -------------------------------------------------------------------------------- /test/connection_manager/clear_all_connections_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ClearAllConnectionTest < Minitest::Test 4 | 5 | def setup 6 | super 7 | @cm = FreshConnection::ConnectionManager.new 8 | end 9 | 10 | def teardown 11 | @cm.clear_all_connections! 12 | end 13 | 14 | test "all connections disconnect" do 15 | threads_num = 5 16 | threads = [] 17 | threads_num.times do |i| 18 | threads << Thread.new do 19 | @cm.replica_connection 20 | end 21 | end 22 | threads.each(&:join) 23 | 24 | connections = @cm.instance_variable_get("@pool").connections.dup 25 | 26 | @cm.clear_all_connections! 27 | assert_empty @cm.instance_variable_get("@pool").connections 28 | connections.each do |c| 29 | refute c.active? 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/connection_manager/put_aside_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PutAsideTest < Minitest::Test 4 | def setup 5 | super 6 | @cm = FreshConnection::ConnectionManager.new 7 | end 8 | 9 | def teardown 10 | @cm.clear_all_connections! 11 | end 12 | 13 | test "current thread connection disconnect" do 14 | current_connection = @cm.replica_connection 15 | assert current_connection.in_use? 16 | @cm.put_aside! 17 | refute current_connection.in_use? 18 | refute current_connection.active? 19 | 20 | connections = @cm.instance_variable_get("@pool").connections 21 | refute connections.include?(current_connection) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/connection_manager/replica_connection_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "fresh_connection/connection_manager" 3 | 4 | class ReplicaConnectionTest < Minitest::Test 5 | def setup 6 | super 7 | @cm = FreshConnection::ConnectionManager.new 8 | end 9 | 10 | def teardown 11 | @cm.clear_all_connections! 12 | end 13 | 14 | test "same connection in one thread" do 15 | c = @cm.replica_connection 16 | assert_equal @cm.replica_connection, c 17 | end 18 | 19 | test "multi connections in several thread" do 20 | threads_num = 5 21 | threads = [] 22 | threads_num.times do |i| 23 | threads << Thread.new do 24 | c = @cm.replica_connection 25 | assert c.in_use? 26 | end 27 | end 28 | threads.each(&:join) 29 | 30 | connections = @cm.instance_variable_get("@pool").connections 31 | assert_equal threads_num, connections.size 32 | before_connection = nil 33 | connections.each do |c| 34 | refute_equal before_connection, c 35 | before_connection = c 36 | end 37 | end 38 | end 39 | 40 | -------------------------------------------------------------------------------- /test/connection_specification/database_replica_url_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class DatabaseReplicaUrlTest < Minitest::Test 4 | def setup 5 | @previous_replica_url = ENV["DATABASE_REPLICA_URL"] 6 | end 7 | 8 | def teardown 9 | ENV["DATABASE_REPLICA_URL"] = @previous_replica_url 10 | ENV["DATABASE_REPLICA_MYSQL2_URL"] = nil 11 | ENV["DATABASE_REPLICA_POSTGRESQL_URL"] = nil 12 | end 13 | 14 | test "use standard config/database.yml spec when DATABASE_REPLICA_URL is not defined" do 15 | test_config = { host: 'test_host', database: 'test_db', adapter: "mysql2" } 16 | ENV["DATABASE_REPLICA_URL"] = nil 17 | 18 | s = FreshConnection::ConnectionSpecification.new(:replica) 19 | s.stub(:base_config, { replica: test_config }) do 20 | config = s.spec.respond_to?(:config) ? s.spec.config : s.spec.db_config.configuration_hash 21 | 22 | assert_equal test_config[:host], config[:host] 23 | assert_equal test_config[:database], config[:database] 24 | assert_equal test_config[:adapter], config[:adapter] 25 | assert_nil config[:username] 26 | assert_nil config[:password] 27 | end 28 | end 29 | 30 | test "use url components when DATABASE_REPLICA_URL is defined" do 31 | test_config = { 32 | adapter: "postgresql", 33 | host: SecureRandom.hex(6), 34 | database: SecureRandom.hex(5), 35 | username: SecureRandom.hex(4), 36 | password: SecureRandom.hex(3), 37 | port: rand(10000) + 1, 38 | prepared_statements: %w(true false).sample, 39 | pool: (rand(100) + 1).to_s, 40 | reconnect: %w(true false).sample 41 | } 42 | 43 | test_replica_url = "#{test_config[:adapter]}://#{test_config[:username]}:#{test_config[:password]}@#{test_config[:host]}:#{test_config[:port]}/#{test_config[:database]}?prepared_statements=#{test_config[:prepared_statements]}&pool=#{test_config[:pool]}&reconnect=#{test_config[:reconnect]}" 44 | ENV["DATABASE_REPLICA_URL"] = test_replica_url 45 | 46 | s = FreshConnection::ConnectionSpecification.new(:replica) 47 | s.stub(:base_config, {}) do 48 | config = s.spec.respond_to?(:config) ? s.spec.config : s.spec.db_config.configuration_hash 49 | 50 | assert_equal test_config[:host], config[:host] 51 | assert_equal test_config[:database], config[:database] 52 | assert_equal test_config[:username], config[:username] 53 | assert_equal test_config[:adapter], config[:adapter] 54 | assert_equal test_config[:password], config[:password] 55 | assert_equal test_config[:port], config[:port] 56 | assert_equal test_config[:prepared_statements], config[:prepared_statements] 57 | assert_equal test_config[:pool], config[:pool] 58 | assert_equal test_config[:reconnect], config[:reconnect] 59 | end 60 | end 61 | 62 | test "use url components when some DATABASE_REPLICA_URL is defined" do 63 | test_config = { 64 | mysql2: { 65 | host: SecureRandom.hex(3), 66 | database: SecureRandom.hex(4), 67 | username: SecureRandom.hex(5), 68 | password: SecureRandom.hex(6), 69 | port: rand(10000) + 1, 70 | pool: (rand(100) + 1).to_s 71 | }, 72 | postgresql: { 73 | host: SecureRandom.hex(7), 74 | database: SecureRandom.hex(8), 75 | username: SecureRandom.hex(9), 76 | password: SecureRandom.hex(10), 77 | port: rand(10000) + 1, 78 | pool: (rand(100) + 1).to_s 79 | } 80 | } 81 | 82 | c = test_config[:mysql2] 83 | test_replica_mysql_url = "mysql2://#{c[:username]}:#{c[:password]}@#{c[:host]}:#{c[:port]}/#{c[:database]}?pool=#{c[:pool]}" 84 | ENV["DATABASE_REPLICA_MYSQL2_URL"] = test_replica_mysql_url 85 | 86 | c = test_config[:postgresql] 87 | test_replica_postgresql_url = "postgresql://#{c[:username]}:#{c[:password]}@#{c[:host]}:#{c[:port]}/#{c[:database]}?pool=#{c[:pool]}" 88 | ENV["DATABASE_REPLICA_POSTGRESQL_URL"] = test_replica_postgresql_url 89 | 90 | %i(mysql2 postgresql).each do |spec_name| 91 | s = FreshConnection::ConnectionSpecification.new("replica_#{spec_name}") 92 | s.stub(:base_config, {}) do 93 | config = s.spec.respond_to?(:config) ? s.spec.config : s.spec.db_config.configuration_hash 94 | tc = test_config[spec_name] 95 | 96 | assert_equal spec_name.to_s, config[:adapter] 97 | assert_equal tc[:host], config[:host] 98 | assert_equal tc[:database], config[:database] 99 | assert_equal tc[:username], config[:username] 100 | assert_equal tc[:password], config[:password] 101 | assert_equal tc[:port], config[:port] 102 | assert_equal tc[:pool], config[:pool] 103 | end 104 | end 105 | end 106 | 107 | test "use merge url components and database.yml configuration" do 108 | test_config = { 109 | adapter: "mysql2", 110 | host: SecureRandom.hex(6), 111 | database: SecureRandom.hex(5), 112 | username: SecureRandom.hex(4), 113 | password: SecureRandom.hex(3), 114 | port: rand(10000) + 1, 115 | reconnect: %w(true false).sample 116 | } 117 | base_config = { 118 | host: "bad host", 119 | database: "bad database", 120 | username: "bad username", 121 | pool: (rand(100) + 1).to_s, 122 | } 123 | 124 | test_replica_url = "#{test_config[:adapter]}://#{test_config[:username]}:#{test_config[:password]}@#{test_config[:host]}:#{test_config[:port]}/#{test_config[:database]}?reconnect=#{test_config[:reconnect]}" 125 | ENV["DATABASE_REPLICA_URL"] = test_replica_url 126 | 127 | s = FreshConnection::ConnectionSpecification.new(:replica) 128 | s.stub(:base_config, base_config) do 129 | config = s.spec.respond_to?(:config) ? s.spec.config : s.spec.db_config.configuration_hash 130 | 131 | tc = base_config.merge!(test_config) 132 | assert_equal tc[:host], config[:host] 133 | assert_equal tc[:database], config[:database] 134 | assert_equal tc[:username], config[:username] 135 | assert_equal tc[:adapter], config[:adapter] 136 | assert_equal tc[:password], config[:password] 137 | assert_equal tc[:port], config[:port] 138 | assert_equal tc[:pool], config[:pool] 139 | assert_equal tc[:reconnect], config[:reconnect] 140 | end 141 | end 142 | end 143 | -------------------------------------------------------------------------------- /test/fresh_connection/access_to_master_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AccessToMasterTest < Minitest::Test 4 | def setup 5 | super 6 | @user = User.where(id: 1).first 7 | end 8 | 9 | test "in transaction" do 10 | User.transaction do 11 | assert_includes @user.name, "replica1" 12 | data = [ 13 | Address.first.user.name, 14 | Address.first.prefecture, 15 | Tel.first.number, 16 | Tel.first.user.name, 17 | @user.address.prefecture, 18 | @user.tels.first.number, 19 | User.joins(:address).where(id: 1).where("addresses.user_id = 1").first.name, 20 | User.where(id: 1).pluck(:name).first, 21 | @user.reload.name 22 | ] 23 | assert data.all?{|n| n.include?("master")} 24 | assert_equal 1, User.where(name: "Other").count 25 | refute User.where(id: 3).exists? 26 | end 27 | end 28 | 29 | test "in with_master" do 30 | User.with_master do 31 | assert_includes @user.name, "replica1" 32 | data = [ 33 | Address.first.user.name, 34 | Address.first.prefecture, 35 | Tel.first.number, 36 | Tel.first.user.name, 37 | @user.address.prefecture, 38 | @user.tels.first.number, 39 | User.joins(:address).where(id: 1).where("addresses.user_id = 1").first.name, 40 | User.where(id: 1).pluck(:name).first, 41 | @user.reload.name 42 | ] 43 | assert data.all?{|n| n.include?("master")} 44 | assert_equal 1, User.where(name: "Other").count 45 | refute User.where(id: 3).exists? 46 | end 47 | end 48 | 49 | test "specify read_master" do 50 | data = [ 51 | Address.read_master.first.prefecture, 52 | Address.includes(:user).read_master.first.user.name, 53 | Tel.read_master.first.number, 54 | Tel.includes(:user).read_master.first.user.name, 55 | @user.tels.read_master.first.number, 56 | User.where(id: 1).includes(:tels).read_master.first.tels.first.number, 57 | User.where(id: 1).includes(:address).read_master.first.address.prefecture, 58 | User.where(id: 1).joins(:address).where("addresses.user_id = 1").read_master.first.name, 59 | User.where(id: 1).read_master.pluck(:name).first 60 | ] 61 | assert data.all?{|n| n.include?("master")} 62 | assert_equal 1, User.where(name: "Other").read_master.count 63 | refute User.read_master.where(id: 3).exists? 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/fresh_connection/access_to_replica_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class AccessToReplicaTest < Minitest::Test 4 | def setup 5 | super 6 | @user = User.where(id: 1).first 7 | end 8 | 9 | test "select from User is to access to replica1" do 10 | data = [ 11 | @user.name, 12 | Address.first.user.name, 13 | Tel.first.user.name, 14 | ] 15 | 16 | assert data.all?{|n| n.include?("replica1")} 17 | end 18 | 19 | test "select from Address is to access to replica1" do 20 | data = [ 21 | Address.first.prefecture, 22 | @user.address.prefecture 23 | ] 24 | 25 | assert data.all?{|n| n.include?("replica1")} 26 | end 27 | 28 | test "select from Address is to access to replica2" do 29 | data = [ 30 | Tel.first.number, 31 | @user.tels.first.number 32 | ] 33 | 34 | assert data.all?{|n| n.include?("replica2")} 35 | end 36 | 37 | test "select with join is to access to replica1" do 38 | name = User.joins(:address).where("addresses.user_id = 1").where(id: 1).first.name 39 | assert_includes name, "replica1" 40 | end 41 | 42 | test "pluck is access to replica1" do 43 | assert_includes User.where(id: 1).pluck(:name).first, "replica" 44 | end 45 | 46 | test "pluck returns empty array when result of condition is empty" do 47 | assert_empty User.limit(0).pluck(:name) 48 | end 49 | 50 | test "count is access to replica" do 51 | assert_equal 2, User.where(name: "Other").count 52 | end 53 | 54 | test "reload is to access to replica1" do 55 | assert_includes @user.reload.name, "replica1" 56 | end 57 | 58 | test "exists? is to access to replica" do 59 | assert User.where(id: 3).exists? 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/fresh_connection/master_db_only_model_always_access_to_master_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class MasterDbOnlyModelAlwaysAccessToMasterTest < Minitest::Test 4 | class Address3 < ActiveRecord::Base 5 | self.table_name = "addresses" 6 | master_db_only! 7 | end 8 | 9 | class Master < ActiveRecord::Base 10 | self.abstract_class = true 11 | master_db_only! 12 | end 13 | 14 | class Tel3 < Master 15 | self.table_name = "tels" 16 | end 17 | 18 | def setup 19 | super 20 | @user = User.where(id: 1).first 21 | end 22 | 23 | test "self is master_db_only model" do 24 | assert_includes Address3.first.prefecture, "master" 25 | end 26 | 27 | test "parent is master_db_only model" do 28 | assert_includes Tel3.first.number, "master" 29 | end 30 | 31 | test "not effect other models" do 32 | assert_includes Address.first.prefecture, "replica1" 33 | assert_includes Tel.first.number, "replica2" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/recovery_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class RecoveryTest < Minitest::Test 4 | test "enable recovery" do 5 | count = 0 6 | raise_exception = lambda {|*args, &block| 7 | if count == 0 8 | count += 1 9 | raise ActiveRecord::StatementInvalid, "hoge" 10 | end 11 | } 12 | 13 | FreshConnection::AccessControl.exception_or_super(:access, raise_exception) do 14 | FreshConnection::AccessControl.stub(:recovery?, true) do 15 | assert User.take 16 | end 17 | end 18 | end 19 | 20 | test "raise exception when retry over" do 21 | raise_exception = lambda {|*args| 22 | raise ActiveRecord::StatementInvalid, "hoge" 23 | } 24 | 25 | FreshConnection::AccessControl.stub(:access, raise_exception) do 26 | FreshConnection::AccessControl.stub(:recovery?, true) do 27 | assert_raises(ActiveRecord::StatementInvalid) do 28 | User.take 29 | end 30 | end 31 | end 32 | end 33 | 34 | test "raise exception when conection active" do 35 | count = 0 36 | raise_exception = lambda {|*args, &block| 37 | if count == 0 38 | count += 1 39 | raise ActiveRecord::StatementInvalid, "hoge" 40 | end 41 | } 42 | 43 | FreshConnection::AccessControl.exception_or_super(:access, raise_exception) do 44 | FreshConnection::AccessControl.stub(:recovery?, false) do 45 | assert_raises(ActiveRecord::StatementInvalid) do 46 | User.take 47 | end 48 | end 49 | end 50 | end 51 | 52 | test "enable recovery (activerecord >= 6.1)" do 53 | count = 0 54 | raise_exception = lambda {|*args, &block| 55 | if count == 0 56 | count += 1 57 | raise ActiveRecord::ConnectionNotEstablished, "hoge" 58 | end 59 | } 60 | 61 | FreshConnection::AccessControl.exception_or_super(:access, raise_exception) do 62 | FreshConnection::AccessControl.stub(:recovery?, true) do 63 | assert User.take 64 | end 65 | end 66 | end 67 | 68 | test "raise exception when retry over (activerecord >= 6.1)" do 69 | raise_exception = lambda {|*args| 70 | raise ActiveRecord::ConnectionNotEstablished, "hoge" 71 | } 72 | 73 | FreshConnection::AccessControl.stub(:access, raise_exception) do 74 | FreshConnection::AccessControl.stub(:recovery?, true) do 75 | assert_raises(ActiveRecord::ConnectionNotEstablished) do 76 | User.take 77 | end 78 | end 79 | end 80 | end 81 | 82 | test "raise exception when conection active (activerecord >= 6.1)" do 83 | count = 0 84 | raise_exception = lambda {|*args, &block| 85 | if count == 0 86 | count += 1 87 | raise ActiveRecord::ConnectionNotEstablished, "hoge" 88 | end 89 | } 90 | 91 | FreshConnection::AccessControl.exception_or_super(:access, raise_exception) do 92 | FreshConnection::AccessControl.stub(:recovery?, false) do 93 | assert_raises(ActiveRecord::ConnectionNotEstablished) do 94 | User.take 95 | end 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /test/support/active_record_logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), '../../log/sql.log')) 4 | -------------------------------------------------------------------------------- /test/support/extend_minitest.rb: -------------------------------------------------------------------------------- 1 | module ExtendMinitest 2 | def test(name, &block) 3 | test_name = "test_#{name.gsub(/\s+/,'_')}" 4 | raise "test '#{name}' is already defined" if method_defined?(test_name) 5 | define_method(test_name, &block) 6 | end 7 | 8 | def setup 9 | ActiveRecord::Base.configurations.clear # reset all configurations before each test 10 | end 11 | end 12 | 13 | unless defined?(Minitest::Test) 14 | Minitest::Test = Minitest::Unit::TestCase 15 | end 16 | 17 | Minitest::Test.extend ExtendMinitest 18 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "minitest/autorun" 2 | 3 | require "minitest/reporters" 4 | Minitest::Reporters.use! 5 | 6 | class Object 7 | def exception_or_super name, callable 8 | new_name = "__minitest_stub__#{name}" 9 | 10 | metaclass = class << self; self; end 11 | 12 | if respond_to? name and not methods.map(&:to_s).include? name.to_s then 13 | metaclass.send :define_method, name do |*args, &blk| 14 | super(*args, &blk) 15 | end 16 | end 17 | 18 | metaclass.send :alias_method, new_name, name 19 | 20 | metaclass.send :define_method, name do |*args, &blk| 21 | callable.call 22 | send new_name, *args, &blk 23 | end 24 | 25 | yield self 26 | ensure 27 | metaclass.send :undef_method, name 28 | metaclass.send :alias_method, name, new_name 29 | metaclass.send :undef_method, new_name 30 | end 31 | end 32 | 33 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 34 | require 'fresh_connection' 35 | 36 | require_relative "config/prepare" 37 | --------------------------------------------------------------------------------