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 +4 -4
- data/README.md +60 -0
- data/lib/burninator/broadcaster.rb +39 -0
- data/lib/burninator/connection.rb +29 -0
- data/lib/burninator/tasks/warm.rake +8 -0
- data/lib/burninator/tasks.rb +7 -0
- data/lib/burninator/warmer.rb +30 -0
- data/lib/burninator.rb +60 -0
- metadata +41 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
!binary "U0hBMQ==":
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2ab5383df34974224a6e7ffef9ef840494605ea
|
|
4
|
+
data.tar.gz: cc519f7bb61e1fff82149a24cc8fcbfd3d43175f
|
|
5
5
|
!binary "U0hBNTEy":
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be185bba1093d74f2a2fbaedc62c3a63a746ceba7b76eef9f7ed65264aa22677423e86731a9823397c64ba3c557b38255825d50daef059c9ccd7afd3a7d50bc6
|
|
7
|
+
data.tar.gz: 1db925daaa85812506d1a9489b9d5b41b8a70cfda531271d30af25b5b4f8cd7a5b4c67a05bbb098ecbb5280d85ece6181151befc9fa537596bb66e434f958d56
|
data/README.md
CHANGED
|
@@ -1 +1,61 @@
|
|
|
1
1
|
# burninator
|
|
2
|
+
|
|
3
|
+
[](https://codeclimate.com/github/jpignata/burninator)
|
|
4
|
+
[](http://badge.fury.io/rb/burninator)
|
|
5
|
+
|
|
6
|
+
### Status: Beta (Caveat Utilitor)
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+

|
|
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,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
|
|
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-
|
|
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:
|
|
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:
|
|
27
|
-
description:
|
|
28
|
-
|
|
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:
|
|
94
|
+
summary: Keep your follower database warm
|
|
61
95
|
test_files: []
|