kestrel-client 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.md +16 -0
- data/Rakefile +30 -0
- data/VERSION +1 -0
- data/kestrel-client.gemspec +71 -0
- data/lib/kestrel-client.rb +1 -0
- data/lib/kestrel.rb +10 -0
- data/lib/kestrel/client.rb +99 -0
- data/lib/kestrel/client/blocking.rb +31 -0
- data/lib/kestrel/client/envelope.rb +25 -0
- data/lib/kestrel/client/json.rb +16 -0
- data/lib/kestrel/client/proxy.rb +15 -0
- data/lib/kestrel/client/unmarshal.rb +21 -0
- data/lib/kestrel/config.rb +48 -0
- data/spec/kestrel/client/blocking_spec.rb +72 -0
- data/spec/kestrel/client/envelope_spec.rb +34 -0
- data/spec/kestrel/client/json_spec.rb +42 -0
- data/spec/kestrel/client/unmarshal_spec.rb +61 -0
- data/spec/kestrel/client_spec.rb +107 -0
- data/spec/kestrel/config/kestrel.yml +42 -0
- data/spec/kestrel/config_spec.rb +75 -0
- data/spec/spec.opts +7 -0
- data/spec/spec_helper.rb +15 -0
- metadata +93 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg/*
|
data/README.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# kestrel-client: Talk to Kestrel queue server from Ruby
|
2
|
+
|
3
|
+
Copyright 2010 Twitter, Inc.
|
4
|
+
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
you may not use this file except in compliance with the License.
|
7
|
+
You may obtain a copy of the License at
|
8
|
+
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
See the License for the specific language governing permissions and
|
15
|
+
limitations under the License.
|
16
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
ROOT_DIR = File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'rubygems' rescue nil
|
4
|
+
require 'rake'
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
|
7
|
+
task :default => :spec
|
8
|
+
|
9
|
+
desc "Run all specs in spec directory."
|
10
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
11
|
+
t.spec_opts = ['--options', "\"#{ROOT_DIR}/spec/spec.opts\""]
|
12
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
13
|
+
end
|
14
|
+
|
15
|
+
# gemification with jeweler
|
16
|
+
begin
|
17
|
+
require 'jeweler'
|
18
|
+
Jeweler::Tasks.new do |gemspec|
|
19
|
+
gemspec.name = "kestrel-client"
|
20
|
+
gemspec.summary = "Ruby Kestrel client"
|
21
|
+
gemspec.description = "Ruby client for the Kestrel queue server"
|
22
|
+
gemspec.email = "rael@twitter.com"
|
23
|
+
gemspec.homepage = "http://github.com/freels/kestrel-client"
|
24
|
+
gemspec.authors = ["Matt Freels", "Rael Dornfest"]
|
25
|
+
gemspec.add_dependency 'memcached'
|
26
|
+
end
|
27
|
+
Jeweler::GemcutterTasks.new
|
28
|
+
rescue LoadError
|
29
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
30
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{kestrel-client}
|
8
|
+
s.version = "0.2.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Matt Freels", "Rael Dornfest"]
|
12
|
+
s.date = %q{2010-02-11}
|
13
|
+
s.description = %q{Ruby client for the Kestrel queue server}
|
14
|
+
s.email = %q{rael@twitter.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"README.md",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"kestrel-client.gemspec",
|
24
|
+
"lib/kestrel-client.rb",
|
25
|
+
"lib/kestrel.rb",
|
26
|
+
"lib/kestrel/client.rb",
|
27
|
+
"lib/kestrel/client/blocking.rb",
|
28
|
+
"lib/kestrel/client/envelope.rb",
|
29
|
+
"lib/kestrel/client/json.rb",
|
30
|
+
"lib/kestrel/client/proxy.rb",
|
31
|
+
"lib/kestrel/client/unmarshal.rb",
|
32
|
+
"lib/kestrel/config.rb",
|
33
|
+
"spec/kestrel/client/blocking_spec.rb",
|
34
|
+
"spec/kestrel/client/envelope_spec.rb",
|
35
|
+
"spec/kestrel/client/json_spec.rb",
|
36
|
+
"spec/kestrel/client/unmarshal_spec.rb",
|
37
|
+
"spec/kestrel/client_spec.rb",
|
38
|
+
"spec/kestrel/config/kestrel.yml",
|
39
|
+
"spec/kestrel/config_spec.rb",
|
40
|
+
"spec/spec.opts",
|
41
|
+
"spec/spec_helper.rb"
|
42
|
+
]
|
43
|
+
s.homepage = %q{http://github.com/freels/kestrel-client}
|
44
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
45
|
+
s.require_paths = ["lib"]
|
46
|
+
s.rubygems_version = %q{1.3.5}
|
47
|
+
s.summary = %q{Ruby Kestrel client}
|
48
|
+
s.test_files = [
|
49
|
+
"spec/kestrel/client/blocking_spec.rb",
|
50
|
+
"spec/kestrel/client/envelope_spec.rb",
|
51
|
+
"spec/kestrel/client/json_spec.rb",
|
52
|
+
"spec/kestrel/client/unmarshal_spec.rb",
|
53
|
+
"spec/kestrel/client_spec.rb",
|
54
|
+
"spec/kestrel/config_spec.rb",
|
55
|
+
"spec/spec_helper.rb"
|
56
|
+
]
|
57
|
+
|
58
|
+
if s.respond_to? :specification_version then
|
59
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
60
|
+
s.specification_version = 3
|
61
|
+
|
62
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
63
|
+
s.add_runtime_dependency(%q<memcached>, [">= 0"])
|
64
|
+
else
|
65
|
+
s.add_dependency(%q<memcached>, [">= 0"])
|
66
|
+
end
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<memcached>, [">= 0"])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'kestrel'
|
data/lib/kestrel.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module Kestrel
|
2
|
+
class Client < Memcached::Rails
|
3
|
+
class Timeout < Timeout::Error; end
|
4
|
+
|
5
|
+
QUEUE_STAT_NAMES = %w{items bytes total_items logsize expired_items mem_items mem_bytes age discarded}
|
6
|
+
|
7
|
+
def flush(queue)
|
8
|
+
count = 0
|
9
|
+
while sizeof(queue) > 0
|
10
|
+
while get(queue, true)
|
11
|
+
count += 1
|
12
|
+
end
|
13
|
+
end
|
14
|
+
count
|
15
|
+
end
|
16
|
+
|
17
|
+
def peek(queue)
|
18
|
+
val = get(queue)
|
19
|
+
set(queue, val)
|
20
|
+
val
|
21
|
+
end
|
22
|
+
|
23
|
+
def sizeof(queue)
|
24
|
+
stat_info = stat(queue)
|
25
|
+
stat_info ? stat_info['items'] : 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def available_queues
|
29
|
+
stats['queues'].keys.sort
|
30
|
+
end
|
31
|
+
|
32
|
+
def stats
|
33
|
+
merge_stats(servers.map { |server| stats_for_server(server) })
|
34
|
+
end
|
35
|
+
|
36
|
+
def stat(queue)
|
37
|
+
stats['queues'][queue]
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def stats_for_server(server)
|
43
|
+
server_name, port = server.split(/:/)
|
44
|
+
socket = TCPSocket.new(server_name, port)
|
45
|
+
socket.puts "STATS"
|
46
|
+
|
47
|
+
stats = Hash.new
|
48
|
+
stats['queues'] = Hash.new
|
49
|
+
while line = socket.readline
|
50
|
+
if line =~ /^STAT queue_(\S+?)_(#{QUEUE_STAT_NAMES.join("|")}) (\S+)/
|
51
|
+
queue_name, queue_stat_name, queue_stat_value = $1, $2, deserialize_stat_value($3)
|
52
|
+
stats['queues'][queue_name] ||= Hash.new
|
53
|
+
stats['queues'][queue_name][queue_stat_name] = queue_stat_value
|
54
|
+
elsif line =~ /^STAT (\w+) (\S+)/
|
55
|
+
stat_name, stat_value = $1, deserialize_stat_value($2)
|
56
|
+
stats[stat_name] = stat_value
|
57
|
+
elsif line =~ /^END/
|
58
|
+
socket.close
|
59
|
+
break
|
60
|
+
elsif defined?(RAILS_DEFAULT_LOGGER)
|
61
|
+
RAILS_DEFAULT_LOGGER.debug("KestrelClient#stats_for_server: Ignoring #{line}")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
stats
|
66
|
+
end
|
67
|
+
|
68
|
+
def merge_stats(all_stats)
|
69
|
+
result = Hash.new
|
70
|
+
|
71
|
+
all_stats.each do |stats|
|
72
|
+
stats.each do |stat_name, stat_value|
|
73
|
+
if result.has_key?(stat_name)
|
74
|
+
if stat_value.kind_of?(Hash)
|
75
|
+
result[stat_name] = merge_stats([result[stat_name], stat_value])
|
76
|
+
else
|
77
|
+
result[stat_name] += stat_value
|
78
|
+
end
|
79
|
+
else
|
80
|
+
result[stat_name] = stat_value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
result
|
86
|
+
end
|
87
|
+
|
88
|
+
def deserialize_stat_value(value)
|
89
|
+
case value
|
90
|
+
when /^\d+\.\d+$/:
|
91
|
+
value.to_f
|
92
|
+
when /^\d+$/:
|
93
|
+
value.to_i
|
94
|
+
else
|
95
|
+
value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Kestrel
|
2
|
+
class Client
|
3
|
+
class Blocking < Proxy
|
4
|
+
DEFAULT_EXPIRY = 0
|
5
|
+
WAIT_TIME_BEFORE_RETRY = 0.25
|
6
|
+
|
7
|
+
def get(*args)
|
8
|
+
while !(response = client.get(*args))
|
9
|
+
sleep WAIT_TIME_BEFORE_RETRY
|
10
|
+
end
|
11
|
+
response
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_without_blocking(*args)
|
15
|
+
client.get(*args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def set(key, value, expiry = DEFAULT_EXPIRY, raw = false)
|
19
|
+
@retried = false
|
20
|
+
begin
|
21
|
+
client.set(key, value, expiry, raw)
|
22
|
+
rescue Memcached::Error => e
|
23
|
+
raise if @retried
|
24
|
+
sleep(WAIT_TIME_BEFORE_RETRY)
|
25
|
+
@retried = true
|
26
|
+
retry
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Kestrel
|
2
|
+
class Client
|
3
|
+
class Envelope < Proxy
|
4
|
+
attr_accessor :envelope_class
|
5
|
+
|
6
|
+
def initialize(envelope_class, client)
|
7
|
+
@envelope_class = envelope_class
|
8
|
+
super(client)
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(*args)
|
12
|
+
response = client.get(*args)
|
13
|
+
if response.respond_to?(:unwrap)
|
14
|
+
response.unwrap
|
15
|
+
else
|
16
|
+
response
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def set(key, value, *args)
|
21
|
+
client.set(key, envelope_class.new(value), *args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Kestrel
|
4
|
+
class Client
|
5
|
+
class Json < Proxy
|
6
|
+
def get(*args)
|
7
|
+
response = client.get(*args)
|
8
|
+
if response.is_a?(String)
|
9
|
+
HashWithIndifferentAccess.new(JSON.parse(response)) rescue response
|
10
|
+
else
|
11
|
+
response
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Kestrel
|
2
|
+
class Client
|
3
|
+
class Unmarshal < Proxy
|
4
|
+
def get(keys, raw = false)
|
5
|
+
response = client.get(keys, true)
|
6
|
+
return response if raw
|
7
|
+
if is_marshaled?(response)
|
8
|
+
Marshal.load_with_constantize(response, loaded_constants = [])
|
9
|
+
else
|
10
|
+
response
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def is_marshaled?(object)
|
15
|
+
object.to_s[0] == Marshal::MAJOR_VERSION && object.to_s[1] == Marshal::MINOR_VERSION
|
16
|
+
rescue Exception
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Kestrel
|
4
|
+
module Config
|
5
|
+
class ConfigNotLoaded < StandardError; end
|
6
|
+
|
7
|
+
extend self
|
8
|
+
|
9
|
+
attr_accessor :environment, :config
|
10
|
+
|
11
|
+
def load(config_file)
|
12
|
+
self.config = YAML.load_file(config_file)
|
13
|
+
end
|
14
|
+
|
15
|
+
def environment
|
16
|
+
@environment ||= 'development'
|
17
|
+
end
|
18
|
+
|
19
|
+
def config
|
20
|
+
@config or raise ConfigNotLoaded
|
21
|
+
end
|
22
|
+
|
23
|
+
def namespace(namespace)
|
24
|
+
client_args_from config[namespace.to_s][environment.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
def default
|
28
|
+
client_args_from config[environment.to_s]
|
29
|
+
end
|
30
|
+
|
31
|
+
def new_client(space = nil)
|
32
|
+
Client.new *(space ? namespace(space) : default)
|
33
|
+
end
|
34
|
+
|
35
|
+
alias method_missing namespace
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def client_args_from(config)
|
40
|
+
sanitized = config.inject({}) do |sanitized, (key, val)|
|
41
|
+
sanitized[key.to_sym] = val; sanitized
|
42
|
+
end
|
43
|
+
servers = sanitized.delete(:servers)
|
44
|
+
|
45
|
+
[servers, sanitized]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe "Kestrel::Client::Blocking" do
|
4
|
+
describe "Instance Methods" do
|
5
|
+
before do
|
6
|
+
Kestrel::Config.load TEST_CONFIG_FILE
|
7
|
+
@raw_kestrel_client = Kestrel::Client.new(*Kestrel::Config.default)
|
8
|
+
@kestrel = Kestrel::Client::Blocking.new(@raw_kestrel_client)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#set" do
|
12
|
+
before do
|
13
|
+
@queue = "some_queue"
|
14
|
+
@value = "some_value"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "blocks on a set until the set works" do
|
18
|
+
@queue = "some_queue"
|
19
|
+
@value = "some_value"
|
20
|
+
mock(@raw_kestrel_client)\
|
21
|
+
.set(@queue, @value, anything, anything) { raise Memcached::Error }.then\
|
22
|
+
.set(@queue, @value, anything, anything) { :mcguffin }
|
23
|
+
mock(@kestrel).sleep(Kestrel::Client::Blocking::WAIT_TIME_BEFORE_RETRY).once
|
24
|
+
@kestrel.set(@queue, @value).should == :mcguffin
|
25
|
+
end
|
26
|
+
|
27
|
+
it "raises if two sets in a row fail" do
|
28
|
+
mock(@raw_kestrel_client)\
|
29
|
+
.set(@queue, @value, anything, anything) { raise Memcached::Error }.then\
|
30
|
+
.set(@queue, @value, anything, anything) { raise Memcached::Error }
|
31
|
+
mock(@kestrel).sleep(Kestrel::Client::Blocking::WAIT_TIME_BEFORE_RETRY).once
|
32
|
+
lambda { @kestrel.set(@queue, @value) }.should raise_error(Memcached::Error)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "passes along the default expiry time if none is given" do
|
36
|
+
@queue = "some_queue"
|
37
|
+
@value = "some_value"
|
38
|
+
mock(@raw_kestrel_client).set(@queue, @value, Kestrel::Client::Blocking::DEFAULT_EXPIRY, anything)
|
39
|
+
@kestrel.set(@queue, @value)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "passes along the given expiry time if one is passed in" do
|
43
|
+
@queue = "some_queue"
|
44
|
+
@value = "some_value"
|
45
|
+
mock(@raw_kestrel_client).set(@queue, @value, 60, anything)
|
46
|
+
@kestrel.set(@queue, @value, 60)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#get" do
|
51
|
+
before do
|
52
|
+
@queue = "some_queue"
|
53
|
+
end
|
54
|
+
|
55
|
+
it "blocks on a get until the get works" do
|
56
|
+
mock(@raw_kestrel_client)\
|
57
|
+
.get(@queue) { nil }.then\
|
58
|
+
.get(@queue) { :mcguffin }
|
59
|
+
mock(@kestrel).sleep(Kestrel::Client::Blocking::WAIT_TIME_BEFORE_RETRY).once
|
60
|
+
@kestrel.get(@queue).should == :mcguffin
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#get_without_blocking" do
|
64
|
+
it "does not block" do
|
65
|
+
mock(@raw_kestrel_client).get(@queue) { nil }
|
66
|
+
mock(@kestrel).sleep(Kestrel::Client::Blocking::WAIT_TIME_BEFORE_RETRY).never
|
67
|
+
@kestrel.get_without_blocking(@queue).should be_nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
class Envelope; end
|
4
|
+
|
5
|
+
describe Kestrel::Client::Envelope do
|
6
|
+
describe "Instance Methods" do
|
7
|
+
before do
|
8
|
+
Kestrel::Config.load TEST_CONFIG_FILE
|
9
|
+
@raw_kestrel_client = Kestrel::Client.new(*Kestrel::Config.default)
|
10
|
+
@kestrel = Kestrel::Client::Envelope.new(Envelope, @raw_kestrel_client)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#get and #set" do
|
14
|
+
describe "envelopes" do
|
15
|
+
it "creates an envelope on a set" do
|
16
|
+
mock(Envelope).new(:mcguffin)
|
17
|
+
@kestrel.set('a_queue', :mcguffin)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "unwraps an envelope on a get" do
|
21
|
+
envelope = Envelope.new
|
22
|
+
mock(@raw_kestrel_client).get('a_queue') { envelope }
|
23
|
+
mock(envelope).unwrap { :mcguffin }
|
24
|
+
@kestrel.get('a_queue').should == :mcguffin
|
25
|
+
end
|
26
|
+
|
27
|
+
it "does not unwrap a nil get" do
|
28
|
+
mock(@raw_kestrel_client).get('a_queue') { nil }
|
29
|
+
@kestrel.get('a_queue').should be_nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
require 'kestrel/client/json'
|
4
|
+
|
5
|
+
describe Kestrel::Client::Json do
|
6
|
+
describe "Instance Methods" do
|
7
|
+
before do
|
8
|
+
Kestrel::Config.load TEST_CONFIG_FILE
|
9
|
+
@raw_kestrel_client = Kestrel::Client.new(*Kestrel::Config.default)
|
10
|
+
@kestrel = Kestrel::Client::Json.new(@raw_kestrel_client)
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#get" do
|
14
|
+
it "parses json" do
|
15
|
+
mock(@raw_kestrel_client).get('a_queue') { '{"a": 1, "b": [{"c": 2}]}' }
|
16
|
+
@kestrel.get('a_queue').should == {"a" => 1, "b" => ["c" => 2]}
|
17
|
+
end
|
18
|
+
|
19
|
+
it "uses a HashWithIndifferentAccess" do
|
20
|
+
mock(@raw_kestrel_client).get('a_queue') { '{"a": 1, "b": [{"c": 2}]}' }
|
21
|
+
@kestrel.get('a_queue').class.should == HashWithIndifferentAccess
|
22
|
+
end
|
23
|
+
|
24
|
+
it "passes through non-strings" do
|
25
|
+
mock(@raw_kestrel_client).get('a_queue') { {:key => "value"} }
|
26
|
+
@kestrel.get('a_queue').should == {:key => "value"}
|
27
|
+
end
|
28
|
+
|
29
|
+
it "passes through strings that are not json" do
|
30
|
+
mock(@raw_kestrel_client).get('a_queue') { "I am not JSON" }
|
31
|
+
@kestrel.get('a_queue').should == "I am not JSON"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class HashWithIndifferentAccess < Hash
|
38
|
+
def initialize(hash = {})
|
39
|
+
super()
|
40
|
+
merge!(hash)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe Kestrel::Client::Unmarshal do
|
4
|
+
describe "Instance Methods" do
|
5
|
+
before do
|
6
|
+
Kestrel::Config.load TEST_CONFIG_FILE
|
7
|
+
@raw_kestrel_client = Kestrel::Client.new(*Kestrel::Config.default)
|
8
|
+
@kestrel = Kestrel::Client::Unmarshal.new(@raw_kestrel_client)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#get" do
|
12
|
+
it "unmarshals marshaled objects" do
|
13
|
+
test_object = {:a => 1, :b => [1, 2, 3]}
|
14
|
+
mock(@raw_kestrel_client).get('a_queue', true) { Marshal.dump(test_object) }
|
15
|
+
@kestrel.get('a_queue').should == test_object
|
16
|
+
end
|
17
|
+
|
18
|
+
it "does not unmarshal when raw is true" do
|
19
|
+
test_object = {:a => 1, :b => [1, 2, 3]}
|
20
|
+
mock(@raw_kestrel_client).get('a_queue', true) { Marshal.dump(test_object) }
|
21
|
+
@kestrel.get('a_queue', true).should == Marshal.dump(test_object)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "pasess through objects" do
|
25
|
+
test_object = Object.new
|
26
|
+
mock(@raw_kestrel_client).get('a_queue', true) { test_object }
|
27
|
+
@kestrel.get('a_queue').should == test_object
|
28
|
+
end
|
29
|
+
|
30
|
+
it "passes through strings" do
|
31
|
+
mock(@raw_kestrel_client).get('a_queue', true) { "I am not marshaled" }
|
32
|
+
@kestrel.get('a_queue').should == "I am not marshaled"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#isMarshaled" do
|
37
|
+
it "should foo" do
|
38
|
+
@kestrel.is_marshaled?("foo").should be_false
|
39
|
+
@kestrel.is_marshaled?(Marshal.dump("foo")).should be_true
|
40
|
+
|
41
|
+
@kestrel.is_marshaled?({}).should be_false
|
42
|
+
@kestrel.is_marshaled?(Marshal.dump({})).should be_true
|
43
|
+
|
44
|
+
@kestrel.is_marshaled?(BadObject.new).should be_false
|
45
|
+
@kestrel.is_marshaled?(Marshal.dump(BadObject.new)).should be_true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class BadObject
|
52
|
+
def to_s
|
53
|
+
raise Exception
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
module Marshal
|
58
|
+
def self.load_with_constantize(source, loaded_constants = [])
|
59
|
+
self.load(source)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Kestrel::Client do
|
4
|
+
describe "Instance Methods" do
|
5
|
+
before do
|
6
|
+
Kestrel::Config.load TEST_CONFIG_FILE
|
7
|
+
@kestrel = Kestrel::Client.new(*Kestrel::Config.default)
|
8
|
+
stub(@kestrel).with_timing(anything) { |_, block| block.call }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#get and #set" do
|
12
|
+
it "basic operation" do
|
13
|
+
@kestrel.flush(queue = "test_queue")
|
14
|
+
@kestrel.set(queue, value = "russell's reserve")
|
15
|
+
@kestrel.get(queue).should == value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#flush" do
|
20
|
+
before do
|
21
|
+
@queue = "some_random_queue_#{Time.now.to_i}_#{rand(10000)}"
|
22
|
+
end
|
23
|
+
|
24
|
+
it "counts the number of items flushed and passes each of them to a given block" do
|
25
|
+
%w{A B C}.each { |item| @kestrel.set(@queue, item) }
|
26
|
+
@kestrel.flush(@queue).should == 3
|
27
|
+
end
|
28
|
+
|
29
|
+
it "does not attempt to Marshal load the data being flushed" do
|
30
|
+
@kestrel.set(@queue, "some_stuff", 0, true)
|
31
|
+
mock(Marshal).load.never
|
32
|
+
@kestrel.flush(@queue).should == 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#peek" do
|
37
|
+
it "should return first item from the queue and reenqueue" do
|
38
|
+
@queue = "some_random_queue_#{Time.now.to_i}_#{rand(10000)}"
|
39
|
+
@kestrel.set(@queue, "item_1")
|
40
|
+
@kestrel.set(@queue, "item_2")
|
41
|
+
@kestrel.peek(@queue).should == "item_1"
|
42
|
+
@kestrel.sizeof(@queue).should == 2
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#stats" do
|
47
|
+
it "retrieves stats" do
|
48
|
+
stats = @kestrel.stats
|
49
|
+
%w{uptime time version curr_items total_items bytes curr_connections total_connections
|
50
|
+
cmd_get cmd_set get_hits get_misses bytes_read bytes_written queues}.each do |stat|
|
51
|
+
stats[stat].should_not be_nil
|
52
|
+
end
|
53
|
+
|
54
|
+
@kestrel.set("test-queue-name", 97)
|
55
|
+
stats['queues']["test-queue-name"].should_not be_nil
|
56
|
+
Kestrel::Client::QUEUE_STAT_NAMES.each do |queue_stat|
|
57
|
+
stats['queues']['test-queue-name'][queue_stat].should_not be_nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
it "merge in stats from all the servers" do
|
62
|
+
server = @kestrel.servers.first
|
63
|
+
stub(@kestrel).servers { [server] }
|
64
|
+
stats_for_one_server = @kestrel.stats
|
65
|
+
|
66
|
+
server = @kestrel.servers.first
|
67
|
+
stub(@kestrel).servers { [server] * 2 }
|
68
|
+
stats_for_two_servers = @kestrel.stats
|
69
|
+
|
70
|
+
stats_for_two_servers['bytes'].should == 2*stats_for_one_server['bytes']
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#stat" do
|
75
|
+
it "get stats for single queue" do
|
76
|
+
@kestrel.set(queue = "test-queue-name", 97)
|
77
|
+
all_stats = @kestrel.stats
|
78
|
+
@kestrel.stat(queue).should == all_stats['queues'][queue]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#sizeof" do
|
83
|
+
before do
|
84
|
+
@queue = "some_random_queue_#{Time.now.to_i}_#{rand(10000)}"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "reports the size of the queue" do
|
88
|
+
100.times { @kestrel.set(@queue, true) }
|
89
|
+
@kestrel.sizeof(@queue).should == 100
|
90
|
+
end
|
91
|
+
|
92
|
+
it "reports the size of a non-existant queue as 0" do
|
93
|
+
queue = "some_random_queue_#{Time.now.to_i}_#{rand(10000)}"
|
94
|
+
@kestrel.sizeof(queue).should == 0
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#available_queues" do
|
99
|
+
it "returns all the queue names" do
|
100
|
+
@kestrel.set("test-queue-name1", 97)
|
101
|
+
@kestrel.set("test-queue-name2", 155)
|
102
|
+
@kestrel.available_queues.should include('test-queue-name1')
|
103
|
+
@kestrel.available_queues.should include('test-queue-name2')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
defaults: &defaults
|
2
|
+
distribution: :random
|
3
|
+
timeout: 10
|
4
|
+
connect_timeout: 2
|
5
|
+
server_failure_limit: 10
|
6
|
+
auto_eject_hosts: false
|
7
|
+
|
8
|
+
production: &production
|
9
|
+
<<: *defaults
|
10
|
+
auto_eject_hosts: true
|
11
|
+
server_failure_limit: 4
|
12
|
+
servers:
|
13
|
+
- localhost:22133
|
14
|
+
|
15
|
+
staging:
|
16
|
+
<<: *production
|
17
|
+
servers:
|
18
|
+
- localhost:22133
|
19
|
+
|
20
|
+
development: &development
|
21
|
+
<<: *defaults
|
22
|
+
show_backtraces: true
|
23
|
+
servers:
|
24
|
+
- localhost:22133
|
25
|
+
|
26
|
+
benchmark:
|
27
|
+
<<: *development
|
28
|
+
|
29
|
+
test:
|
30
|
+
<<: *development
|
31
|
+
|
32
|
+
replication_lag:
|
33
|
+
<<: *development
|
34
|
+
|
35
|
+
selenium:
|
36
|
+
<<: *development
|
37
|
+
|
38
|
+
foo_space:
|
39
|
+
production:
|
40
|
+
<<: *production
|
41
|
+
development:
|
42
|
+
<<: *development
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Kestrel::Config do
|
4
|
+
before do
|
5
|
+
Kestrel::Config.environment = nil
|
6
|
+
Kestrel::Config.load TEST_CONFIG_FILE
|
7
|
+
|
8
|
+
# to sniff namespace foo_space
|
9
|
+
Kestrel::Config.config['foo_space']['development']['connect_timeout'] = 8
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "load" do
|
13
|
+
it "loads a yaml file" do
|
14
|
+
Kestrel::Config.config = nil
|
15
|
+
lambda { Kestrel::Config.default }.should raise_error(Kestrel::Config::ConfigNotLoaded)
|
16
|
+
|
17
|
+
Kestrel::Config.load TEST_CONFIG_FILE
|
18
|
+
lambda { Kestrel::Config.default }.should_not raise_error(Kestrel::Config::ConfigNotLoaded)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
shared_examples_for "config getters" do
|
23
|
+
it "returns a tuple of [servers, options]" do
|
24
|
+
config = @configurer.call
|
25
|
+
config.should be_a(Array)
|
26
|
+
|
27
|
+
[String, Array].should include(config.first.class)
|
28
|
+
config.last.should be_a(Hash)
|
29
|
+
|
30
|
+
config.last.keys.map{|k| k.class }.uniq.should == [Symbol]
|
31
|
+
end
|
32
|
+
|
33
|
+
it "defaults to development enviroment" do
|
34
|
+
@configurer.call.last[:server_failure_limit].should == 10 # development options should pull 10 from defaults
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns config for the specified environment" do
|
38
|
+
Kestrel::Config.environment = :production
|
39
|
+
@configurer.call.last[:server_failure_limit].should == 4 # production is set to 4
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "namespace" do
|
44
|
+
before { @configurer = lambda { Kestrel::Config.namespace(:foo_space) } }
|
45
|
+
|
46
|
+
it_should_behave_like "config getters"
|
47
|
+
|
48
|
+
it "returns args for Kestrel::Client.new for the appropriate namespace" do
|
49
|
+
Kestrel::Config.foo_space.last[:connect_timeout].should == 8
|
50
|
+
end
|
51
|
+
|
52
|
+
it "is aliased to method_missing" do
|
53
|
+
Kestrel::Config.foo_space.should == Kestrel::Config.namespace(:foo_space)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "default" do
|
58
|
+
before { @configurer = lambda { Kestrel::Config.default } }
|
59
|
+
|
60
|
+
it_should_behave_like "config getters"
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "new_client" do
|
64
|
+
it "returns a Kestrel::Client instance" do
|
65
|
+
client = Kestrel::Config.new_client
|
66
|
+
client.should be_a(Kestrel::Client)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "can take a namespace" do
|
70
|
+
client = Kestrel::Config.new_client(:foo_space)
|
71
|
+
client.should be_a(Kestrel::Client)
|
72
|
+
client.options[:connect_timeout].should == 8
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'spec'
|
3
|
+
|
4
|
+
spec_dir = File.dirname(__FILE__)
|
5
|
+
|
6
|
+
# make sure we load local libs rather than gems first
|
7
|
+
$: << File.expand_path("#{spec_dir}/../lib")
|
8
|
+
|
9
|
+
require 'kestrel'
|
10
|
+
|
11
|
+
TEST_CONFIG_FILE = File.expand_path("#{spec_dir}/kestrel/config/kestrel.yml")
|
12
|
+
|
13
|
+
Spec::Runner.configure do |config|
|
14
|
+
config.mock_with :rr
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kestrel-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Freels
|
8
|
+
- Rael Dornfest
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2010-02-11 00:00:00 -08:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: memcached
|
18
|
+
type: :runtime
|
19
|
+
version_requirement:
|
20
|
+
version_requirements: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
version:
|
26
|
+
description: Ruby client for the Kestrel queue server
|
27
|
+
email: rael@twitter.com
|
28
|
+
executables: []
|
29
|
+
|
30
|
+
extensions: []
|
31
|
+
|
32
|
+
extra_rdoc_files:
|
33
|
+
- README.md
|
34
|
+
files:
|
35
|
+
- .gitignore
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- VERSION
|
39
|
+
- kestrel-client.gemspec
|
40
|
+
- lib/kestrel-client.rb
|
41
|
+
- lib/kestrel.rb
|
42
|
+
- lib/kestrel/client.rb
|
43
|
+
- lib/kestrel/client/blocking.rb
|
44
|
+
- lib/kestrel/client/envelope.rb
|
45
|
+
- lib/kestrel/client/json.rb
|
46
|
+
- lib/kestrel/client/proxy.rb
|
47
|
+
- lib/kestrel/client/unmarshal.rb
|
48
|
+
- lib/kestrel/config.rb
|
49
|
+
- spec/kestrel/client/blocking_spec.rb
|
50
|
+
- spec/kestrel/client/envelope_spec.rb
|
51
|
+
- spec/kestrel/client/json_spec.rb
|
52
|
+
- spec/kestrel/client/unmarshal_spec.rb
|
53
|
+
- spec/kestrel/client_spec.rb
|
54
|
+
- spec/kestrel/config/kestrel.yml
|
55
|
+
- spec/kestrel/config_spec.rb
|
56
|
+
- spec/spec.opts
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
has_rdoc: true
|
59
|
+
homepage: http://github.com/freels/kestrel-client
|
60
|
+
licenses: []
|
61
|
+
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options:
|
64
|
+
- --charset=UTF-8
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: "0"
|
72
|
+
version:
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: "0"
|
78
|
+
version:
|
79
|
+
requirements: []
|
80
|
+
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.3.5
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: Ruby Kestrel client
|
86
|
+
test_files:
|
87
|
+
- spec/kestrel/client/blocking_spec.rb
|
88
|
+
- spec/kestrel/client/envelope_spec.rb
|
89
|
+
- spec/kestrel/client/json_spec.rb
|
90
|
+
- spec/kestrel/client/unmarshal_spec.rb
|
91
|
+
- spec/kestrel/client_spec.rb
|
92
|
+
- spec/kestrel/config_spec.rb
|
93
|
+
- spec/spec_helper.rb
|