burninator 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
- metadata.gz: cf37a9aa94004a0c632b93436c807ea11dae991c
4
- data.tar.gz: 30e66ae4a8eb2a498a8760504d2e6483072c9397
3
+ metadata.gz: d2ab5383df34974224a6e7ffef9ef840494605ea
4
+ data.tar.gz: cc519f7bb61e1fff82149a24cc8fcbfd3d43175f
5
5
  !binary "U0hBNTEy":
6
- metadata.gz: fec7cfdc0d066bd538aa1a9f58f8fa3b60339a77de520cdeb7173e1ef5e436dfff574030b7c53acf6eada7e15b8e8a7f9c23798d94c7f1835ca620b9ccd14188
7
- data.tar.gz: 24bfdce22414d1964e3f41b5dbe251a46af01ea14a2908521dd750bc4ec4f471267becdd189378642c5a472cfb7aa74d2f133adc91c3940ec446532a67fa5d02
6
+ metadata.gz: be185bba1093d74f2a2fbaedc62c3a63a746ceba7b76eef9f7ed65264aa22677423e86731a9823397c64ba3c557b38255825d50daef059c9ccd7afd3a7d50bc6
7
+ data.tar.gz: 1db925daaa85812506d1a9489b9d5b41b8a70cfda531271d30af25b5b4f8cd7a5b4c67a05bbb098ecbb5280d85ece6181151befc9fa537596bb66e434f958d56
data/README.md CHANGED
@@ -1 +1,61 @@
1
1
  # burninator
2
+
3
+ [![Code Climate](https://codeclimate.com/github/jpignata/burninator.png)](https://codeclimate.com/github/jpignata/burninator)
4
+ [![Gem Version](https://badge.fury.io/rb/burninator.png)](http://badge.fury.io/rb/burninator)
5
+
6
+ ### Status: Beta (Caveat Utilitor)
7
+
8
+ ## Summary
9
+
10
+ ![burninator](http://25.media.tumblr.com/tumblr_li2bl6oSh01qh5zi3o1_500.jpg)
11
+
12
+ Warm a standby database with some percentage of real production query traffic.
13
+
14
+ It's common for Heroku customers to have a standby database follower in the
15
+ event of a primary failure, however if you cutover to that follower and its
16
+ caches are cold you're likely in for a rough time until its SQL and page
17
+ caches warm up.
18
+
19
+ Burninator uses a Redis pub/sub channel to broadcast some percentage of
20
+ query traffic (by default 5%) from Rails application servers to a central
21
+ warming process that will run queries against the follower. It uses the
22
+ ActiveSupport notifications instrumentation API to listen for queries. These
23
+ queries are broadcast through the channel to the warming process which
24
+ will run them onto the standby database.
25
+
26
+ Since you're standby is seeing some percentage of real production query
27
+ traffic, its caches should keep warm and ready for failover.
28
+
29
+ ## Installation
30
+
31
+ Assuming you're using Heroku:
32
+
33
+ In your Gemfile:
34
+
35
+ ```ruby
36
+ gem "burninator"
37
+ ```
38
+
39
+ In an initializer:
40
+
41
+ ```ruby
42
+ burninator = Burninator.new(redis: $redis, percentage: 25)
43
+ burninator.broadcast
44
+ ```
45
+
46
+ In your Procfile:
47
+
48
+ ```ruby
49
+ burninator: rake burninator:warm
50
+ ```
51
+
52
+ Deploy and start burninating:
53
+
54
+ ```sh
55
+ $ heroku config:add WARM_TARGET_URL="postgres://..."
56
+ $ heroku scale burninator=1
57
+ ```
58
+
59
+ ## License
60
+
61
+ Please see LICENSE.
@@ -0,0 +1,39 @@
1
+ require "securerandom"
2
+ require "active_support/core_ext/string"
3
+ require "active_support/notifications"
4
+ require "redis"
5
+
6
+ class Burninator
7
+ class Broadcaster
8
+ KEY = "sql.active_record"
9
+
10
+ def initialize(redis, channel, percentage)
11
+ @redis = redis
12
+ @channel = channel
13
+ @percentage = percentage
14
+ end
15
+
16
+ def run
17
+ @subscriber ||= ActiveSupport::Notifications.subscribe(KEY) do |*args|
18
+ event = ActiveSupport::Notifications::Event.new(*args)
19
+ sql = event.payload[:sql].squish
20
+
21
+ if publish?(sql)
22
+ @redis.publish(@channel, Marshal.dump(event.payload))
23
+ end
24
+ end
25
+ end
26
+
27
+ def stop
28
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
29
+ end
30
+
31
+ private
32
+
33
+ def publish?(sql)
34
+ return false unless sql =~ /\Aselect /i
35
+
36
+ SecureRandom.random_number(100 / @percentage) == 0
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,29 @@
1
+ require "active_record"
2
+
3
+ class Burninator
4
+ class Connection
5
+ def initialize(url, warm_target = nil)
6
+ @warm_target = warm_target
7
+ connect(url)
8
+ end
9
+
10
+ def execute(query, binds = [])
11
+ query = "/* BURNINATOR */ " + query
12
+ connection.exec_query(query, nil, binds)
13
+ end
14
+
15
+ private
16
+
17
+ def warm_target
18
+ @warm_target ||= Class.new(ActiveRecord::Base)
19
+ end
20
+
21
+ def connect(url)
22
+ warm_target.establish_connection(url)
23
+ end
24
+
25
+ def connection
26
+ @connection ||= warm_target.connection
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ task :environment
2
+
3
+ namespace :burninator do
4
+ desc "Warm follower database"
5
+ task warm: :environment do
6
+ Burninator.new.warm
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ class Burninator::Tasks < Rails::Railtie
2
+ rake_tasks do
3
+ Dir[File.join(File.dirname(__FILE__), "tasks/*.rake")].each do |rake_task|
4
+ load rake_task
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ class Burninator
2
+ class Warmer
3
+ def initialize(redis, channel, connection)
4
+ @redis = redis
5
+ @channel = channel
6
+ @connection = connection
7
+ end
8
+
9
+ def run
10
+ @redis.subscribe(@channel) do |on|
11
+ on.message do |_, serialized|
12
+ event = Marshal.load(serialized)
13
+ process(event)
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def process(event)
21
+ query = event[:sql]
22
+ binds = event[:binds]
23
+
24
+ @connection.execute(query, binds)
25
+ rescue ActiveRecord::StatementInvalid => e
26
+ Rails.logger.error("Error running #{query}")
27
+ Rails.logger.error("#{e.class}: #{e.message}")
28
+ end
29
+ end
30
+ end
data/lib/burninator.rb ADDED
@@ -0,0 +1,60 @@
1
+ require "burninator/broadcaster"
2
+ require "burninator/warmer"
3
+ require "burninator/connection"
4
+ require "burninator/tasks"
5
+
6
+ class Burninator
7
+ DEFAULT_PERCENTAGE = 5
8
+
9
+ def initialize(options = {})
10
+ @redis = options[:redis]
11
+ @percentage = options.fetch(:percentage, DEFAULT_PERCENTAGE)
12
+ end
13
+
14
+ def warm
15
+ trap_signals
16
+
17
+ Burninator::Warmer.new(redis, channel, database).run
18
+ end
19
+
20
+ def broadcast
21
+ Burninator::Broadcaster.new(redis, channel, @percentage).run
22
+ end
23
+
24
+ def channel
25
+ ["burninator", database_id].join(":")
26
+ end
27
+
28
+ private
29
+
30
+ def trap_signals
31
+ trap(:INT) { abort }
32
+ trap(:TERM) { abort }
33
+ end
34
+
35
+ def redis
36
+ @redis ||= Redis.new(:url => redis_url)
37
+ end
38
+
39
+ def database
40
+ Connection.new(warm_target_url)
41
+ end
42
+
43
+ def database_id
44
+ Digest::SHA1.hexdigest(warm_target_url)
45
+ end
46
+
47
+ def warm_target_url
48
+ ENV.fetch("WARM_TARGET_URL")
49
+ rescue KeyError
50
+ raise ArgumentError,
51
+ "To use burninator, set WARM_TARGET_URL in your environment. See https://github.com/jpignata/burninator for more details."
52
+ end
53
+
54
+ def redis_url
55
+ ENV.fetch("REDIS_URL")
56
+ rescue KeyError
57
+ raise ArgumentError,
58
+ "To use burninator, set REDIS_URL in your environment. See https://github.com/jpignata/burninator for more details."
59
+ end
60
+ end
metadata CHANGED
@@ -1,37 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: burninator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Pignata
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-04-14 00:00:00.000000000 Z
11
+ date: 2013-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 3.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 3.2.0
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rspec
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ~>
18
32
  - !ruby/object:Gem::Version
19
- version: 2.13.0
33
+ version: 3.0.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: mocha
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: 0.13.3
20
48
  type: :development
21
49
  prerelease: false
22
50
  version_requirements: !ruby/object:Gem::Requirement
23
51
  requirements:
24
52
  - - ~>
25
53
  - !ruby/object:Gem::Version
26
- version: 2.13.0
27
- description: Uses a pub/sub channel for broadcasting SELECT queries for replay onto
28
- follower databases
54
+ version: 0.13.3
55
+ description: Plays SELECT queries to your Rails application on a follower database
56
+ to keep its caches warm.
29
57
  email:
30
58
  - john@pignata.com
31
59
  executables: []
32
60
  extensions: []
33
61
  extra_rdoc_files: []
34
62
  files:
63
+ - lib/burninator/broadcaster.rb
64
+ - lib/burninator/connection.rb
65
+ - lib/burninator/tasks/warm.rake
66
+ - lib/burninator/tasks.rb
67
+ - lib/burninator/warmer.rb
68
+ - lib/burninator.rb
35
69
  - README.md
36
70
  - LICENSE
37
71
  homepage: http://github.com/jpignata/burninator
@@ -57,5 +91,5 @@ rubyforge_project:
57
91
  rubygems_version: 2.0.0
58
92
  signing_key:
59
93
  specification_version: 4
60
- summary: Run queries to a Rails application on your standby databases
94
+ summary: Keep your follower database warm
61
95
  test_files: []