knockoff 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e0025a70e9bc0d93706b972c83a1bf27ed62a836
4
+ data.tar.gz: 7eada472ea6751980a6109d047f7600cce5590a3
5
+ SHA512:
6
+ metadata.gz: 7743e8ea20c39af558a3425d4b2e565dad7b9f38967833eeaece006874aba0662d364b8383b87c699448750feca896eced1e4bd54de7b9831c55f82cc2fcbcad
7
+ data.tar.gz: 6d1a25ea1b2578529e59b22ac11668a1d13355b2857000a83a169f91eb28cc2e3bcdff94356e0c5544df9de81503fb100acffc27503bae3168b38d41a9f04e22
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ notifications:
3
+ email: false
4
+ rvm:
5
+ - 2.2.4
6
+ - 2.3.1
7
+ - ruby-head
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in knockoff.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Scott Ringwelski
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.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Knockoff (WIP)
2
+
3
+ [![Build Status](https://travis-ci.org/sgringwe/knockoff.svg?branch=master)](https://travis-ci.org/sgringwe/knockoff)
4
+
5
+ A gem for easily using read replicas. Heavily based off of https://github.com/kenn/slavery and https://github.com/kickstarter/replica_pools gem.
6
+
7
+ ## Library Goals
8
+
9
+ * Minimal ActiveRecord monkey-patching
10
+ * Easy run-time configuration using ENV variables
11
+ * Opt-in usage of replicas
12
+ * No need to change code when adding/removing replicas
13
+ * Be thread safe
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'knockoff'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ Or install it yourself as:
28
+
29
+ $ gem install knockoff
30
+
31
+ ## Usage
32
+
33
+ TODO
34
+
35
+ ### Usage Notes
36
+
37
+ * Do not use prepared statements with this gem
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
44
+
45
+ ## Contributing
46
+
47
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/knockoff.
48
+
49
+
50
+ ## License
51
+
52
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
53
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "knockoff"
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
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ set -e # halt script on error
3
+
4
+ sudo apt-get autoremove sqlite3
5
+ sudo apt-get install python-software-properties
6
+ sudo apt-add-repository -y ppa:travis-ci/sqlite3
7
+ sudo apt-get -y update
8
+ sudo apt-cache show sqlite3
9
+ sudo apt-get install sqlite3=3.7.15.1-1~travis1
10
+ sudo sqlite3 -version
11
+ sudo psql --version
12
+ sudo mysql --version
13
+ gem update bundler
data/knockoff.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'knockoff/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "knockoff"
8
+ spec.version = Knockoff::VERSION
9
+ spec.authors = ["Scott Ringwelski"]
10
+ spec.email = ["me@sgringwe.com"]
11
+
12
+ spec.summary = %q{A gem for easily using read replicas}
13
+ spec.homepage = "https://github.com/sgringwe/knockoff"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency 'activerecord', '>= 4.0.0'
22
+ spec.add_runtime_dependency 'request_store_rails', '>= 1.0.0'
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.10"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec"
27
+ spec.add_development_dependency 'sqlite3'
28
+ end
data/lib/knockoff.rb ADDED
@@ -0,0 +1,52 @@
1
+ require 'active_record'
2
+ require 'request_store_rails'
3
+ require 'knockoff/version'
4
+ require 'knockoff/base'
5
+ require 'knockoff/config'
6
+ require 'knockoff/error'
7
+ require 'knockoff/replica_connection_pool'
8
+ require 'knockoff/active_record/base'
9
+ require 'knockoff/active_record/relation'
10
+
11
+ module Knockoff
12
+ class << self
13
+ attr_accessor :enabled
14
+ attr_writer :spec_key
15
+
16
+ def spec_key
17
+ case @spec_key
18
+ when String then @spec_key
19
+ when NilClass then @spec_key = "#{ActiveRecord::ConnectionHandling::RAILS_ENV.call}_replica"
20
+ end
21
+ end
22
+
23
+ def on_replica(&block)
24
+ Base.new(:replica).run(&block)
25
+ end
26
+
27
+ def on_primary(&block)
28
+ Base.new(:primary).run(&block)
29
+ end
30
+
31
+ def replica_pool
32
+ @replica_pool ||= ReplicaConnectionPool.new(config.replica_uris)
33
+ end
34
+
35
+ def config
36
+ @config ||= Config.new
37
+ end
38
+
39
+ def base_transaction_depth
40
+ @base_transaction_depth ||= begin
41
+ testcase = ActiveSupport::TestCase
42
+ if defined?(testcase) &&
43
+ testcase.respond_to?(:use_transactional_fixtures) &&
44
+ testcase.try(:use_transactional_fixtures)
45
+ 1
46
+ else
47
+ 0
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ class Base
3
+ class << self
4
+ alias_method :original_connection, :connection
5
+
6
+ def connection
7
+ case RequestLocals.fetch(:knockoff) { nil }
8
+ when :replica
9
+ # Attempts to use a random replica connection, but otherwise falls back to primary
10
+ Knockoff.replica_pool.random_replica_connection.original_connection
11
+ when :primary, NilClass
12
+ original_connection
13
+ else
14
+ raise Knockoff::Error, "Invalid target: #{RequestLocals.fetch(:knockoff)}"
15
+ end
16
+ end
17
+
18
+ # Generate scope at top level e.g. User.on_replica
19
+ def on_replica
20
+ # Why where(nil)?
21
+ # http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
22
+ context = where(nil)
23
+ context.knockoff_target = :replica
24
+ context
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ class Relation
3
+ attr_accessor :knockoff_target
4
+
5
+ # Supports queries like User.on_replica.to_a
6
+ alias_method :exec_queries_without_knockoff, :exec_queries
7
+
8
+ def exec_queries
9
+ if knockoff_target == :replica
10
+ Knockoff.on_replica { exec_queries_without_knockoff }
11
+ else
12
+ exec_queries_without_knockoff
13
+ end
14
+ end
15
+
16
+
17
+ # Supports queries like User.on_replica.count
18
+ alias_method :calculate_without_knockoff, :calculate
19
+
20
+ def calculate(*args)
21
+ if knockoff_target == :replica
22
+ Knockoff.on_replica { calculate_without_knockoff(*args) }
23
+ else
24
+ calculate_without_knockoff(*args)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ module Knockoff
2
+ class Base
3
+ def initialize(target)
4
+ @target = decide_with(target)
5
+ end
6
+
7
+ def run(&block)
8
+ run_on @target, &block
9
+ end
10
+
11
+ private
12
+
13
+ def decide_with(target)
14
+ raise Knockoff::Error.new('on_slave cannot be used inside transaction block!') if inside_transaction?
15
+
16
+ if Knockoff.enabled
17
+ target
18
+ else
19
+ :primary
20
+ end
21
+ end
22
+
23
+ def inside_transaction?
24
+ open_transactions = run_on(:primary) { ActiveRecord::Base.connection.open_transactions }
25
+ open_transactions > Knockoff.base_transaction_depth
26
+ end
27
+
28
+ def run_on(target)
29
+ backup = RequestLocals.fetch(:knockoff) { nil } # Save for recursive nested calls
30
+ RequestLocals.store[:knockoff] = target
31
+ yield
32
+ ensure
33
+ RequestLocals.store[:knockoff] = backup
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,46 @@
1
+ module Knockoff
2
+ class Config
3
+ # The current environment. Normally set to Rails.env, but
4
+ # will default to 'development' outside of Rails apps.
5
+ attr_accessor :environment
6
+
7
+ # An array of URIs to use for the replica pool.
8
+ # TODO: Add support for inheriting from database.yml
9
+ attr_accessor :replica_uris
10
+
11
+ def initialize
12
+ @environment = 'development'
13
+ set_replica_uris
14
+ end
15
+
16
+ def replica_env_keys
17
+ if ENV['KNOCKOFF_REPLICA_ENVS'].nil?
18
+ []
19
+ else
20
+ ENV['KNOCKOFF_REPLICA_ENVS'].split(',').map(&:strip)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def set_replica_uris
27
+ @replica_uris ||= parse_knockoff_replica_envs_to_uris
28
+ end
29
+
30
+ def parse_knockoff_replica_envs_to_uris
31
+ # As a basic prevention of crashes, attempt to parse each DB uri
32
+ # and don't add the uri to the final list if it can't be parsed
33
+ replica_env_keys.map do |env_key|
34
+ begin
35
+ URI.parse(ENV[env_key])
36
+ rescue URI::InvalidURIError
37
+ Rails.logger.info "LOG NOTIFIER: Invalid URL specified in follower_env_keys. Not including URI, which may result in no followers used." # URI is purposely not printed to logs
38
+ # Return a 'nil' which will be removed from
39
+ # configs with `compact`, resulting in no configs and no followers,
40
+ # therefore disabled since this env will not be in environments list.
41
+ nil
42
+ end
43
+ end.compact
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ module Knockoff
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ module Knockoff
2
+ class ReplicaConnectionPool
3
+ attr_reader :pool
4
+
5
+ def initialize(uris)
6
+ @pool = Concurrent::Hash.new
7
+
8
+ uris.each_with_index do |uri, index|
9
+ @pool["replica_#{index}"] = connection_class(index, uri)
10
+ end
11
+ end
12
+
13
+ def random_replica_connection
14
+ @pool[@pool.keys.sample]
15
+ end
16
+
17
+ # Based off of code from replica_pools gem
18
+ # generates a unique ActiveRecord::Base subclass for a single replica
19
+ def connection_class(replica_index, uri)
20
+ class_name = "Replica#{replica_index}"
21
+
22
+ # TODO: Hardcoding the uri string feels meh. Either set the database config
23
+ # or reference ENV instead
24
+ Knockoff.module_eval %Q{
25
+ class #{class_name} < ActiveRecord::Base
26
+ self.abstract_class = true
27
+ establish_connection '#{uri}'
28
+ end
29
+ }, __FILE__, __LINE__
30
+
31
+ Knockoff.const_get(class_name)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module Knockoff
2
+ VERSION = '0.1.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: knockoff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Scott Ringwelski
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-11-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 4.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: request_store_rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 1.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.10'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.10'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: sqlite3
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description:
98
+ email:
99
+ - me@sgringwe.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - LICENSE.txt
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - ci/install_modern_sqlite.sh
114
+ - knockoff.gemspec
115
+ - lib/knockoff.rb
116
+ - lib/knockoff/active_record/base.rb
117
+ - lib/knockoff/active_record/relation.rb
118
+ - lib/knockoff/base.rb
119
+ - lib/knockoff/config.rb
120
+ - lib/knockoff/error.rb
121
+ - lib/knockoff/replica_connection_pool.rb
122
+ - lib/knockoff/version.rb
123
+ homepage: https://github.com/sgringwe/knockoff
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 2.4.8
144
+ signing_key:
145
+ specification_version: 4
146
+ summary: A gem for easily using read replicas
147
+ test_files: []