nutcracker 0.2.4.beta3 → 0.2.4.mac9
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.
- data/README.md +13 -4
- data/Rakefile +1 -1
- data/ext/nutcracker/extconf.rb +3 -0
- data/lib/nutcracker.rb +139 -21
- data/lib/nutcracker/version.rb +1 -1
- metadata +24 -6
data/README.md
CHANGED
@@ -3,15 +3,15 @@
|
|
3
3
|
|
4
4
|
This library wraps Twitter's [Nutcracker](https://github.com/twitter/twemproxy) in a gem package and provides a simple ruby API to the `nutcracker` executable.
|
5
5
|
|
6
|
-
###
|
7
|
-
this is still a
|
6
|
+
### Disclaimer
|
7
|
+
this project is still in its early stages, so things could be a little buggy, if you find one, feel free to [report](https://github.com/kontera-technologies/nutcracker/issues) it.
|
8
8
|
|
9
9
|
## Motivation
|
10
10
|
The main motivation here is to take the advantages of working with Bundler's dependencies management and to be able to embed Twitter's [Nutcracker](https://github.com/twitter/twemproxy) as a dependency to any Ruby project, this allow you to create small-configuration-only-apps tied to specific version of Nutcracker as I show in the example bellow.
|
11
11
|
|
12
12
|
## Plugins
|
13
13
|
- [nutcracker-graphite](https://github.com/kontera-technologies/nutcracker-graphite) - Send cluster stats to Graphite
|
14
|
-
- [nutcracker-
|
14
|
+
- [nutcracker-web](https://github.com/kontera-technologies/nutcracker-web) - Web interface
|
15
15
|
|
16
16
|
### Installation
|
17
17
|
Add this line to your application's Gemfile:
|
@@ -68,9 +68,18 @@ nutcracker.start
|
|
68
68
|
nutcracker.join # wait for server to exit
|
69
69
|
```
|
70
70
|
|
71
|
-
|
71
|
+
you can also attach to a running instance of nutcracker
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
Nutcracker.attach(config_file: 'cluster.yaml', stats_port: 22222)
|
75
|
+
```
|
76
|
+
|
77
|
+
### Building new gems
|
72
78
|
* Set the version @ `lib/nutcracker/version.rb` ( [Available Versions](https://code.google.com/p/twemproxy/downloads/list) )
|
73
79
|
* run the `rake build` command
|
74
80
|
* look for `nutcracker-X.Y.Z` gem under the pkg folder
|
75
81
|
|
82
|
+
### Warranty
|
83
|
+
This software is provided “as is” and without any express or implied warranties, including, without limitation, the implied warranties of merchantability and fitness for a particular purpose.
|
84
|
+
|
76
85
|
> for more details like licensing etc, please look @ [Nutcracker](https://github.com/twitter/twemproxy)
|
data/Rakefile
CHANGED
@@ -18,7 +18,7 @@ task :download do
|
|
18
18
|
File.open("ext/nutcracker/extconf.rb",'w') do |file|
|
19
19
|
file.puts %q{
|
20
20
|
raise "no support for #{RUBY_PLATFORM}" if RUBY_PLATFORM =~ /darwin|mswin|mingw/
|
21
|
-
system
|
21
|
+
system "./configure --prefix=#{File.expand_path('..',__FILE__)}"
|
22
22
|
system 'make'
|
23
23
|
}
|
24
24
|
end
|
data/ext/nutcracker/extconf.rb
CHANGED
data/lib/nutcracker.rb
CHANGED
@@ -2,65 +2,183 @@ require 'nutcracker/version'
|
|
2
2
|
require 'socket'
|
3
3
|
require 'json'
|
4
4
|
require 'yaml'
|
5
|
+
require 'redis'
|
5
6
|
|
6
7
|
module Nutcracker
|
7
|
-
|
8
|
+
# Syntactic sugar for launching the Nutcracker service ( see {Wrapper#initialize} )
|
9
|
+
# @return [Wrapper] Nutcracker process wrapper
|
10
|
+
# @example
|
11
|
+
# Nutcracker.start config_file: 'conf/nutcracker.yaml'
|
8
12
|
def self.start options
|
9
13
|
Nutcracker::Wrapper.new(options).start
|
10
14
|
end
|
11
|
-
|
15
|
+
|
16
|
+
# Connect to a running instance of Nutcracker ( see {Wrapper#initialize} )
|
17
|
+
# @return [Wrapper] Nutcracker process wrapper
|
18
|
+
# @example
|
19
|
+
# Nutcracker.attach :config_file: 'conf/nutcracker.yaml', :stats_port => 22222
|
20
|
+
def self.attach options
|
21
|
+
Nutcracker::Wrapper.new options.merge attached: true
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the Nutcracker executable path that is embeded with the gem
|
12
25
|
def self.executable
|
13
26
|
File.expand_path("../../ext/nutcracker/src/nutcracker", __FILE__)
|
14
27
|
end
|
15
28
|
|
29
|
+
# Returns the version string
|
16
30
|
def self.version
|
17
31
|
Nutcracker::VERSION
|
18
32
|
end
|
19
33
|
|
20
34
|
class Wrapper
|
21
|
-
attr_reader :pid
|
35
|
+
attr_reader :pid
|
22
36
|
|
37
|
+
# Initialize a new Nutcracker process wrappper
|
38
|
+
# @param [Hash] options
|
39
|
+
# @option options [String] :config_file (conf/nutcracker.yaml) path to nutcracker's configuration file
|
40
|
+
# @option options [String] :stats_port (22222) Nutcracker stats listing port
|
41
|
+
# @option options [Array] :args ([]) array with additional command line arguments
|
23
42
|
def initialize options
|
24
|
-
@
|
43
|
+
@options = validate defaults.merge options
|
25
44
|
end
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
45
|
+
|
46
|
+
# launching the Nutcracker service
|
47
|
+
def start *args
|
48
|
+
return if attached? or running?
|
49
|
+
@pid = ::Process.spawn Nutcracker.executable, *command
|
30
50
|
Kernel.at_exit { kill if running? }
|
31
51
|
self
|
32
52
|
end
|
33
|
-
|
53
|
+
|
54
|
+
# Returns the current running status
|
34
55
|
def running?
|
35
|
-
!!(pid and ::Process.getpgid pid rescue false)
|
56
|
+
attached? ? stats.any? : !!(pid and ::Process.getpgid pid rescue false)
|
36
57
|
end
|
37
|
-
|
58
|
+
|
59
|
+
# Returns true if the current instance was initialize with the attached flag
|
60
|
+
def attached?
|
61
|
+
@options[:attached]
|
62
|
+
end
|
63
|
+
|
64
|
+
# Stops the Nutcracker service
|
38
65
|
def stop
|
39
66
|
sig :TERM
|
40
67
|
end
|
41
|
-
|
68
|
+
|
69
|
+
# Kills the Nutcracker service
|
42
70
|
def kill
|
43
71
|
sig :KILL
|
44
72
|
end
|
45
|
-
|
73
|
+
|
74
|
+
# Wait for the process to exit
|
46
75
|
def join
|
47
|
-
running! and ::Process.waitpid2 pid
|
48
|
-
end
|
49
|
-
|
50
|
-
def stats
|
51
|
-
JSON.parse TCPSocket.new('localhost',22222).read rescue {}
|
76
|
+
attached? ? sleep : (running! and ::Process.waitpid2 pid)
|
52
77
|
end
|
53
78
|
|
79
|
+
# Returns Nutcracker's configuration hash
|
54
80
|
def config
|
55
|
-
@config ||= YAML.load_file config_file
|
81
|
+
@config ||= YAML.load_file @options[:config_file]
|
56
82
|
end
|
57
83
|
|
58
|
-
#
|
84
|
+
# Syntactic sugar for initialize plugins
|
59
85
|
def use plugin, *args
|
60
|
-
Nutcracker.const_get(
|
86
|
+
Nutcracker.const_get(plugin.to_s.capitalize).start(self,*args)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns hash with server and node statistics
|
90
|
+
# See example.json @ project root to get details about the structure
|
91
|
+
def overview
|
92
|
+
data = { :clusters => [], :config => config }
|
93
|
+
|
94
|
+
stats.each do |cluster_name, cluster_data|
|
95
|
+
# Setting global server attributes ( like hostname, version etc...)
|
96
|
+
unless cluster_data.is_a? Hash
|
97
|
+
data[cluster_name] = cluster_data
|
98
|
+
next
|
99
|
+
end
|
100
|
+
|
101
|
+
next unless redis? cluster_name # skip memcached clusters
|
102
|
+
|
103
|
+
aliases = node_aliases cluster_name
|
104
|
+
cluster = { nodes: [], name: cluster_name }
|
105
|
+
cluster_data.each do |node, node_value|
|
106
|
+
# Adding node
|
107
|
+
if node_value.kind_of? Hash
|
108
|
+
node_data = cluster_data[node]
|
109
|
+
node = aliases[node] || node
|
110
|
+
url = ( node =~ /redis\:\/\// ) ? node : "redis://#{node}"
|
111
|
+
info = redis_info(url)
|
112
|
+
cluster[:nodes] << {
|
113
|
+
server_url: url, info: info, running: info.any?
|
114
|
+
}.merge(node_data)
|
115
|
+
else # Cluster attribute
|
116
|
+
cluster[node] = node_value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
data[:clusters].push cluster
|
120
|
+
end
|
121
|
+
data
|
122
|
+
end
|
123
|
+
|
124
|
+
# Check if a given cluster name was configure as Redis
|
125
|
+
def redis? cluster
|
126
|
+
config[cluster]["redis"] rescue false
|
127
|
+
end
|
128
|
+
|
129
|
+
# https://github.com/twitter/twemproxy/blob/master/notes/recommendation.md#node-names-for-consistent-hashing
|
130
|
+
def node_aliases cluster
|
131
|
+
Hash[config[cluster]["servers"].map(&:split).each {|o| o[0]=o[0].split(":")[0..1].join(":")}.map(&:reverse)]
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns hash with information about a given Redis
|
135
|
+
def redis_info url
|
136
|
+
begin
|
137
|
+
redis = Redis.connect(url: url)
|
138
|
+
info = redis.info
|
139
|
+
db_size = redis.dbsize
|
140
|
+
max_memory = redis.config(:get, 'maxmemory')['maxmemory'].to_i
|
141
|
+
redis.quit
|
142
|
+
rescue Exception
|
143
|
+
return {}
|
144
|
+
end
|
145
|
+
|
146
|
+
{
|
147
|
+
'connections' => info['connected_clients'].to_i,
|
148
|
+
'used_memory' => info['used_memory'].to_f,
|
149
|
+
'used_memory_rss' => info['used_memory_rss'].to_f,
|
150
|
+
'fragmentation' => info['mem_fragmentation_ratio'].to_f,
|
151
|
+
'expired_keys' => info['expired_keys'].to_i,
|
152
|
+
'evicted_keys' => info['evicted_keys'].to_i,
|
153
|
+
'hits' => info['keyspace_hits'].to_i,
|
154
|
+
'misses' => info['keyspace_misses'].to_i,
|
155
|
+
'keys' => db_size,
|
156
|
+
'max_memory' => max_memory,
|
157
|
+
'hit_ratio' => 0
|
158
|
+
}.tap {|d| d['hit_ratio'] = d['hits'].to_f / (d['hits']+d['misses']).to_f if d['hits'] > 0 }
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns a hash with server statistics
|
162
|
+
def stats
|
163
|
+
JSON.parse TCPSocket.new('127.0.0.1',@options[:stats_port]).read rescue {}
|
61
164
|
end
|
62
165
|
|
63
166
|
private
|
167
|
+
|
168
|
+
def command
|
169
|
+
['-c', @options[:config_file],'-s',@options[:stats_port],*@options[:args]].map(&:to_s)
|
170
|
+
end
|
171
|
+
|
172
|
+
def defaults
|
173
|
+
{ :args => [],
|
174
|
+
:config_file => 'conf/nutcracker.yaml',
|
175
|
+
:stats_port => 22222,
|
176
|
+
:attached => false}
|
177
|
+
end
|
178
|
+
|
179
|
+
def validate options
|
180
|
+
options.tap { File.exists? options[:config_file] or raise "#{options[:config_file]} not found" }
|
181
|
+
end
|
64
182
|
|
65
183
|
def running!
|
66
184
|
running? or raise RuntimeError, "Nutcracker isn't running..."
|
data/lib/nutcracker/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: nutcracker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.4.
|
4
|
+
version: 0.2.4.mac9
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-09-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: minitest
|
@@ -43,11 +43,28 @@ dependencies:
|
|
43
43
|
- - ~>
|
44
44
|
- !ruby/object:Gem::Version
|
45
45
|
version: 0.14.0
|
46
|
-
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Gem wrapper for Twitter's Nutcracker
|
47
63
|
email: eran@kontera.com
|
48
64
|
executables:
|
49
65
|
- nutcracker
|
50
|
-
extensions:
|
66
|
+
extensions:
|
67
|
+
- ext/nutcracker/extconf.rb
|
51
68
|
extra_rdoc_files: []
|
52
69
|
files:
|
53
70
|
- README.md
|
@@ -398,7 +415,8 @@ files:
|
|
398
415
|
homepage: http://www.kontera.com
|
399
416
|
licenses: []
|
400
417
|
post_install_message:
|
401
|
-
rdoc_options:
|
418
|
+
rdoc_options:
|
419
|
+
- --no-private --protected lib/**/*.rb - README.md
|
402
420
|
require_paths:
|
403
421
|
- lib
|
404
422
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -418,5 +436,5 @@ rubyforge_project: ruby-nutcracker
|
|
418
436
|
rubygems_version: 1.8.25
|
419
437
|
signing_key:
|
420
438
|
specification_version: 3
|
421
|
-
summary: Twitter's
|
439
|
+
summary: Gem wrapper for Twitter's Nutcracker
|
422
440
|
test_files: []
|