derrick 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e0285870ebaaa8e6ba6bc7af6fb5a7b492e7d80c
4
+ data.tar.gz: 749a8f109082a389b2f604da95ee1071f0502f82
5
+ SHA512:
6
+ metadata.gz: 524f95f92f4ab8f4be1563266abb9bba1499c93cb3557806f6b819030c0729b94361b753319a6d127f6ef14344b2444e223a87831b26e8e3705869c3bd881706
7
+ data.tar.gz: c1b2df39d3d9d99ed81b80b464623089862fedb93c0adba932fa27549a33a98020a1695a5ed82cfed718eb53da7b7a248cf802cc9eb1a55177d746d818ac03cb
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in derrick.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jean Boussier
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Derrick
2
+
3
+ Inspect Redis databases and print statistics about the keys
4
+
5
+ ## Installation
6
+
7
+ ```shell
8
+ $ gem install derrick
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```shell
14
+ $ derrick inspect redis://127.0.0.1:6379/5
15
+ ```
16
+
17
+ It will print something like this:
18
+
19
+ ```
20
+ Pattern Count Exp Type
21
+ shop:*:name 10000 100% string
22
+ shop:*:id 10000 0% string
23
+ shop:*:versions 2 0% string: 50%,hash: 50%
24
+ sorted_set:*:product_types 1 0% zset
25
+ ```
26
+
27
+ You can also configure the concurrency level and batch size:
28
+
29
+ ```shell
30
+ $ derrick inspect --concurrency 4 --batch-size 100000 redis://127.0.0.1:6379/5
31
+ ```
32
+
33
+ ## Contributing
34
+
35
+ 1. Fork it ( https://github.com/[my-github-username]/derrick/fork )
36
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
37
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
38
+ 4. Push to the branch (`git push origin my-new-feature`)
39
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/bin/derrick ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'derrick'
7
+ require 'derrick/cli'
8
+
9
+ Derrick::CLI.run(ARGV)
data/derrick.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'derrick/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'derrick'
8
+ spec.version = Derrick::VERSION
9
+ spec.authors = ['Jean Boussier']
10
+ spec.email = ['jean.boussier@gmail.com']
11
+ spec.summary = %q{Inspect Redis databases and print statistics about the keys}
12
+ spec.homepage = ''
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files`.split("\n")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.6'
21
+ spec.add_development_dependency 'rake'
22
+
23
+ spec.add_dependency 'redis'
24
+ end
@@ -0,0 +1,147 @@
1
+ require 'optparse'
2
+ require 'redis'
3
+
4
+ require 'byebug'
5
+
6
+ module Derrick
7
+ class CLI
8
+ class ProgressDisplay
9
+
10
+ def initialize(progress, output)
11
+ @progress = progress
12
+ @output = output
13
+ @last_output_size = 0
14
+ end
15
+
16
+ def render
17
+ clear
18
+ message = "collected: #{@progress.collected}/#{@progress.total} fetched: #{@progress.fetched}/#{@progress.total}"
19
+ @last_output_size = message.size
20
+ @output.print message
21
+ end
22
+
23
+ def clear
24
+ @output.print "\b" * @last_output_size
25
+ end
26
+
27
+ def start
28
+ @thread = Thread.new do
29
+ loop do
30
+ render
31
+ sleep 1
32
+ end
33
+ end
34
+ end
35
+
36
+ def stop
37
+ @thread.kill
38
+ clear
39
+ end
40
+
41
+ end
42
+
43
+ class Context
44
+ attr_accessor :concurrency, :batch_size
45
+
46
+ def initialize
47
+ @concurrency = 2
48
+ @batch_size = 10_000
49
+ end
50
+ end
51
+
52
+ class AggregateFormatter
53
+ def initialize(aggregate)
54
+ @aggregate = Hash[aggregate.sort_by { |p, a| -a.count }]
55
+ end
56
+
57
+ def each
58
+ yield render_header
59
+ @aggregate.each { |n, s| yield render_line(n, s) }
60
+ end
61
+
62
+ def render_header
63
+ [
64
+ 'Pattern'.ljust(key_size),
65
+ 'Count'.rjust(6),
66
+ 'Exp'.rjust(4),
67
+ 'Type',
68
+ ].join(' ')
69
+ end
70
+
71
+ def render_line(name, stats)
72
+ [
73
+ name.ljust(key_size),
74
+ stats.count.to_s.rjust(6),
75
+ "#{(stats.expirable_ratio * 100).round}%".rjust(4),
76
+ types_summary(stats)
77
+ ].join(' ')
78
+ end
79
+
80
+ def types_summary(stats)
81
+ if stats.types_count.size == 1
82
+ stats.types_count.keys.first
83
+ else
84
+ stats.types_ratio.map do |type, ratio|
85
+ "#{type}: #{(ratio * 100).round}%"
86
+ end.join(',')
87
+ end
88
+ end
89
+
90
+ def key_size
91
+ @key_size ||= @aggregate.keys.map(&:size).max
92
+ end
93
+ end
94
+
95
+ def self.run(args)
96
+ new(args).run!
97
+ end
98
+
99
+ def initialize(args)
100
+ @context = Context.new
101
+ @command, *@arguments = parser.parse(args)
102
+ end
103
+
104
+ def run!
105
+ abort! unless @command
106
+ public_send("command_#{@command}", *@arguments)
107
+ end
108
+
109
+ def command_inspect(database_url=nil)
110
+ inspector = Derrick::Inspector.new(Redis.new(url: database_url), @context)
111
+
112
+ progress_display = ProgressDisplay.new(inspector.progress, STDERR)
113
+ progress_display.start
114
+
115
+ aggregate = inspector.report
116
+ progress_display.stop
117
+
118
+ AggregateFormatter.new(aggregate).each do |line|
119
+ puts line
120
+ end
121
+ end
122
+
123
+ protected
124
+
125
+ def abort!(message=parser.help)
126
+ puts message
127
+ exit 1
128
+ end
129
+
130
+ def parser
131
+ OptionParser.new do |opts|
132
+ opts.banner = "Inpect Redis databases to compute statistics on keys"
133
+ opts.separator ""
134
+ opts.separator "Usage: derrick inspect [options] DATABASE_URL"
135
+ opts.separator ""
136
+ opts.separator "Main options:"
137
+ opts.on('-c', '--concurrency CONCURRENCY') do |concurrency|
138
+ @context.concurrency = Integer(concurrency)
139
+ end
140
+ opts.on('-b', '--batch-size BATCH_SIZE') do |batch_size|
141
+ @context.batch_size = Integer(batch_size)
142
+ end
143
+ end
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,182 @@
1
+ require 'thread'
2
+
3
+ Thread.abort_on_exception = true
4
+
5
+ module Derrick
6
+ class Collector
7
+ def initialize(redis, queue, progress, context)
8
+ @redis = redis
9
+ @queue = queue
10
+ @progress = progress
11
+ @context = context
12
+ end
13
+
14
+ def run
15
+ collect_keys
16
+ @context.concurrency.times { @queue.push(:stop) }
17
+ end
18
+
19
+ def collect_keys
20
+ cursor = '0'
21
+ loop do
22
+ cursor, keys = @redis.scan(cursor, count: @context.batch_size)
23
+ @queue.push(keys)
24
+ @progress.increment_collected(keys.size)
25
+ return if cursor == '0'
26
+ end
27
+ end
28
+ end
29
+
30
+ Key = Struct.new(:name, :type, :ttl)
31
+
32
+ class Fetcher
33
+
34
+ def initialize(redis, input, output, progress)
35
+ @redis = redis
36
+ @input = input
37
+ @output = output
38
+ @progress = progress
39
+ end
40
+
41
+ def run
42
+ while (keys = @input.pop) != :stop
43
+ @output.push(stats(keys))
44
+ end
45
+ @output.push(:stop)
46
+ end
47
+
48
+ def stats(keys)
49
+ types = @redis.pipelined do
50
+ keys.each do |key|
51
+ @redis.type(key)
52
+ end
53
+ end
54
+
55
+ ttls = @redis.pipelined do
56
+ keys.each do |key|
57
+ @redis.ttl(key)
58
+ end
59
+ end
60
+
61
+ @progress.increment_fetched(keys.size)
62
+
63
+ keys.map.with_index do |key, index|
64
+ Key.new(key, types[index], ttls[index])
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ class Pattern
71
+ attr_reader :pattern, :count, :expirable_count, :persisted_count, :types_count
72
+
73
+ def initialize
74
+ @count = 0
75
+ @expirable_count = 0
76
+ @persisted_count = 0
77
+ @types_count = Hash.new(0)
78
+ end
79
+
80
+ def expirable_ratio
81
+ return 1 if count == 0
82
+ expirable_count.to_f / count
83
+ end
84
+
85
+ def types_ratio
86
+ Hash[@types_count.map do |type, sub_count|
87
+ [type, sub_count.to_f / count]
88
+ end]
89
+ end
90
+
91
+ def aggregate(key)
92
+ @count += 1
93
+
94
+ if key.ttl == -1
95
+ @persisted_count += 1
96
+ else
97
+ @expirable_count += 1
98
+ end
99
+
100
+ @types_count[key.type] += 1
101
+ end
102
+
103
+ end
104
+
105
+ class Progress
106
+
107
+ attr_reader :total, :collected, :fetched
108
+
109
+ def initialize(total)
110
+ @total = total
111
+ @mutex = Mutex.new
112
+
113
+ @collected = 0
114
+ @fetched = 0
115
+ end
116
+
117
+ def increment_collected(count)
118
+ @mutex.synchronize { @collected += count }
119
+ end
120
+
121
+ def increment_fetched(count)
122
+ @mutex.synchronize { @fetched += count }
123
+ end
124
+
125
+ end
126
+
127
+ class Aggregator
128
+ attr_reader :patterns
129
+ def initialize(queue, context)
130
+ @queue = queue
131
+ @patterns = {}
132
+ @context = context
133
+ end
134
+
135
+ def run
136
+ fetcher_count = @context.concurrency
137
+ loop do
138
+ keys = @queue.pop
139
+ if keys == :stop
140
+ fetcher_count -= 1
141
+ break if fetcher_count == 0
142
+ else
143
+ keys.each { |k| aggregate(k) }
144
+ end
145
+ end
146
+ self
147
+ end
148
+
149
+ def aggregate(key)
150
+ pattern = pattern_from(key)
151
+ pattern.aggregate(key)
152
+ end
153
+
154
+ def pattern_from(key)
155
+ canonical_key = key.name.gsub(/(^|:)(\d+|[0-9a-f]{32,40})($|:)/, '\1*\3')
156
+ @patterns[canonical_key] ||= Pattern.new
157
+ end
158
+
159
+ end
160
+
161
+ class Inspector
162
+ attr_reader :redis, :progress
163
+
164
+ def initialize(redis, context)
165
+ @redis = redis
166
+ @context = context
167
+ @progress = Progress.new(@redis.dbsize)
168
+ end
169
+
170
+ def report
171
+ keys_queue = Queue.new
172
+ stats_queue = Queue.new
173
+ Thread.new { Collector.new(@redis, keys_queue, @progress, @context).run }
174
+ @context.concurrency.times do
175
+ Thread.new { Fetcher.new(@redis, keys_queue, stats_queue, @progress).run }
176
+ end
177
+ aggregator = Aggregator.new(stats_queue, @context)
178
+ aggregator.run
179
+ aggregator.patterns
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,3 @@
1
+ module Derrick
2
+ VERSION = "0.0.1"
3
+ end
data/lib/derrick.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'derrick/version'
2
+ require 'derrick/inspector'
3
+
4
+ module Derrick
5
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: derrick
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jean Boussier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: redis
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email:
57
+ - jean.boussier@gmail.com
58
+ executables:
59
+ - derrick
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - ".gitignore"
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - bin/derrick
69
+ - derrick.gemspec
70
+ - lib/derrick.rb
71
+ - lib/derrick/cli.rb
72
+ - lib/derrick/inspector.rb
73
+ - lib/derrick/version.rb
74
+ homepage: ''
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.2.2
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Inspect Redis databases and print statistics about the keys
98
+ test_files: []