kestrel-client 0.2.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.
- 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
|