siberite-client 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/LICENSE +14 -0
- data/README.md +63 -0
- data/Rakefile +18 -0
- data/lib/siberite-client.rb +1 -0
- data/lib/siberite.rb +9 -0
- data/lib/siberite/client.rb +202 -0
- data/lib/siberite/client/blocking.rb +36 -0
- data/lib/siberite/client/envelope.rb +25 -0
- data/lib/siberite/client/json.rb +16 -0
- data/lib/siberite/client/namespace.rb +29 -0
- data/lib/siberite/client/partitioning.rb +35 -0
- data/lib/siberite/client/proxy.rb +15 -0
- data/lib/siberite/client/stats_helper.rb +122 -0
- data/lib/siberite/client/transactional.rb +125 -0
- data/lib/siberite/client/unmarshal.rb +32 -0
- data/lib/siberite/client/version.rb +3 -0
- data/lib/siberite/config.rb +48 -0
- data/siberite-client.gemspec +35 -0
- data/spec/siberite/client/blocking_spec.rb +29 -0
- data/spec/siberite/client/envelope_spec.rb +45 -0
- data/spec/siberite/client/json_spec.rb +40 -0
- data/spec/siberite/client/namespace_spec.rb +54 -0
- data/spec/siberite/client/partitioning_spec.rb +33 -0
- data/spec/siberite/client/transactional_spec.rb +273 -0
- data/spec/siberite/client/unmarshal_spec.rb +68 -0
- data/spec/siberite/client_spec.rb +175 -0
- data/spec/siberite/config/siberite.yml +42 -0
- data/spec/siberite/config_spec.rb +72 -0
- data/spec/siberite_benchmark.rb +26 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +33 -0
- metadata +205 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
module Siberite::Client::StatsHelper
|
2
|
+
STATS_TIMEOUT = 3
|
3
|
+
QUEUE_STAT_NAMES = %w{items bytes total_items logsize expired_items mem_items mem_bytes age discarded waiters open_transactions}
|
4
|
+
|
5
|
+
def sizeof(queue)
|
6
|
+
stat_info = stat(queue)
|
7
|
+
stat_info ? stat_info['items'] : 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def available_queues
|
11
|
+
stats['queues'].keys.sort
|
12
|
+
end
|
13
|
+
|
14
|
+
def stats
|
15
|
+
alive, dead = 0, 0
|
16
|
+
|
17
|
+
results = servers.map do |server|
|
18
|
+
begin
|
19
|
+
result = stats_for_server(server)
|
20
|
+
alive += 1
|
21
|
+
result
|
22
|
+
rescue Exception
|
23
|
+
dead += 1
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end.compact
|
27
|
+
|
28
|
+
stats = merge_stats(results)
|
29
|
+
stats['alive_servers'] = alive
|
30
|
+
stats['dead_servers'] = dead
|
31
|
+
stats
|
32
|
+
end
|
33
|
+
|
34
|
+
def stat(queue)
|
35
|
+
stats['queues'][queue]
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def stats_for_server(server)
|
41
|
+
server_name, port = server.split(/:/)
|
42
|
+
socket = nil
|
43
|
+
with_timeout STATS_TIMEOUT do
|
44
|
+
socket = TCPSocket.new(server_name, port)
|
45
|
+
end
|
46
|
+
socket.puts "STATS"
|
47
|
+
|
48
|
+
stats = Hash.new
|
49
|
+
stats['queues'] = Hash.new
|
50
|
+
while line = socket.readline
|
51
|
+
if line =~ /^STAT queue_(\S+?)_(#{QUEUE_STAT_NAMES.join("|")}) (\S+)/
|
52
|
+
queue_name, queue_stat_name, queue_stat_value = $1, $2, deserialize_stat_value($3)
|
53
|
+
stats['queues'][queue_name] ||= Hash.new
|
54
|
+
stats['queues'][queue_name][queue_stat_name] = queue_stat_value
|
55
|
+
elsif line =~ /^STAT (\w+) (\S+)/
|
56
|
+
stat_name, stat_value = $1, deserialize_stat_value($2)
|
57
|
+
stats[stat_name] = stat_value
|
58
|
+
elsif line =~ /^END/
|
59
|
+
socket.close
|
60
|
+
break
|
61
|
+
elsif defined?(RAILS_DEFAULT_LOGGER)
|
62
|
+
RAILS_DEFAULT_LOGGER.debug("SiberiteClient#stats_for_server: Ignoring #{line}")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
stats
|
67
|
+
ensure
|
68
|
+
socket.close if socket && !socket.closed?
|
69
|
+
end
|
70
|
+
|
71
|
+
def merge_stats(all_stats)
|
72
|
+
result = Hash.new
|
73
|
+
|
74
|
+
all_stats.each do |stats|
|
75
|
+
stats.each do |stat_name, stat_value|
|
76
|
+
if result.has_key?(stat_name)
|
77
|
+
if stat_value.kind_of?(Hash)
|
78
|
+
result[stat_name] = merge_stats([result[stat_name], stat_value])
|
79
|
+
else
|
80
|
+
result[stat_name] += stat_value
|
81
|
+
end
|
82
|
+
else
|
83
|
+
result[stat_name] = stat_value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
def deserialize_stat_value(value)
|
92
|
+
case value
|
93
|
+
when /^\d+\.\d+$/
|
94
|
+
value.to_f
|
95
|
+
when /^\d+$/
|
96
|
+
value.to_i
|
97
|
+
else
|
98
|
+
value
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
begin
|
103
|
+
require "system_timer"
|
104
|
+
|
105
|
+
def with_timeout(seconds, &block)
|
106
|
+
SystemTimer.timeout_after(seconds, &block)
|
107
|
+
end
|
108
|
+
|
109
|
+
rescue LoadError
|
110
|
+
if ! defined?(RUBY_ENGINE)
|
111
|
+
# MRI 1.8, all other interpreters define RUBY_ENGINE, JRuby and
|
112
|
+
# Rubinius should have no issues with timeout.
|
113
|
+
warn "WARNING: using the built-in Timeout class which is known to have issues when used for opening connections. Install the SystemTimer gem if you want to make sure the Siberite client will not hang."
|
114
|
+
end
|
115
|
+
|
116
|
+
require "timeout"
|
117
|
+
|
118
|
+
def with_timeout(seconds, &block)
|
119
|
+
Timeout.timeout(seconds, &block)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
class Siberite::Client::Transactional < Siberite::Client::Proxy
|
2
|
+
|
3
|
+
# Raised when a caller attempts to use this proxy across
|
4
|
+
# multiple queues.
|
5
|
+
class MultipleQueueException < StandardError; end
|
6
|
+
|
7
|
+
# Raised when a caller attempts to retry a job if
|
8
|
+
# there is no current open transaction
|
9
|
+
class NoOpenTransaction < StandardError; end
|
10
|
+
|
11
|
+
# Raised when a retry fails when max retries is exceeded
|
12
|
+
class RetriesExceeded < StandardError
|
13
|
+
def initialize(job)
|
14
|
+
super "Max retries of #{job.retries} exceeded for item: #{job.job.inspect}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class RetryableJob < Struct.new(:retries, :job); end
|
19
|
+
|
20
|
+
# Number of times to retry a job before giving up
|
21
|
+
DEFAULT_RETRIES = 10
|
22
|
+
|
23
|
+
# Pct. of the time during 'normal' processing we check the error queue first
|
24
|
+
ERROR_PROCESSING_RATE = 0.1
|
25
|
+
|
26
|
+
# ==== Parameters
|
27
|
+
# client<Siberite::Client>:: Client
|
28
|
+
# max_retries<Integer>:: Number of times to retry a job before
|
29
|
+
# giving up. Defaults to DEFAULT_RETRIES
|
30
|
+
# error_rate<Float>:: Pct. of the time during 'normal'
|
31
|
+
# processing we check the error queue
|
32
|
+
# first. Defaults to ERROR_PROCESSING_RATE
|
33
|
+
# per_server<Integer>:: Number of gets to execute against a
|
34
|
+
# single server, before changing
|
35
|
+
# servers. Defaults to MAX_PER_SERVER
|
36
|
+
#
|
37
|
+
def initialize(client, max_retries = nil, error_rate = nil)
|
38
|
+
@max_retries = max_retries || DEFAULT_RETRIES
|
39
|
+
@error_rate = error_rate || ERROR_PROCESSING_RATE
|
40
|
+
@counter = 0 # Command counter
|
41
|
+
super(client)
|
42
|
+
end
|
43
|
+
|
44
|
+
attr_reader :current_queue
|
45
|
+
|
46
|
+
# Returns job from the +key+ queue 1 - ERROR_PROCESSING_RATE
|
47
|
+
# pct. of the time. Every so often, checks the error queue for
|
48
|
+
# jobs and returns a retryable job.
|
49
|
+
#
|
50
|
+
# ==== Returns
|
51
|
+
# Job, possibly retryable, or nil
|
52
|
+
#
|
53
|
+
# ==== Raises
|
54
|
+
# MultipleQueueException
|
55
|
+
#
|
56
|
+
def get(key, opts = {})
|
57
|
+
raise MultipleQueueException if current_queue && key != current_queue
|
58
|
+
|
59
|
+
close_last_transaction
|
60
|
+
|
61
|
+
if read_from_error_queue?
|
62
|
+
queue = key + "_errors"
|
63
|
+
job = client.get_from_last(queue, opts.merge(:open => true))
|
64
|
+
else
|
65
|
+
queue = key
|
66
|
+
job = client.get(queue, opts.merge(:open => true))
|
67
|
+
end
|
68
|
+
|
69
|
+
if job
|
70
|
+
@job = job.is_a?(RetryableJob) ? job : RetryableJob.new(0, job)
|
71
|
+
@last_read_queue = queue
|
72
|
+
@current_queue = key
|
73
|
+
@job.job
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def current_try
|
78
|
+
@job.retries + 1
|
79
|
+
end
|
80
|
+
|
81
|
+
def close_last_transaction #:nodoc:
|
82
|
+
return unless @last_read_queue
|
83
|
+
|
84
|
+
client.get_from_last(@last_read_queue, :close => true)
|
85
|
+
@last_read_queue = @current_queue = @job = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# Enqueues the current job on the error queue for later
|
89
|
+
# retry. If the job has been retried DEFAULT_RETRIES times,
|
90
|
+
# gives up entirely.
|
91
|
+
#
|
92
|
+
# ==== Parameters
|
93
|
+
# item (optional):: if specified, the job set to the error
|
94
|
+
# queue with the given payload instead of what
|
95
|
+
# was originally fetched.
|
96
|
+
#
|
97
|
+
# ==== Returns
|
98
|
+
# Boolean:: true if the job is enqueued in the retry queue, false otherwise
|
99
|
+
#
|
100
|
+
# ==== Raises
|
101
|
+
# NoOpenTransaction
|
102
|
+
#
|
103
|
+
def retry(item = nil)
|
104
|
+
raise NoOpenTransaction unless @last_read_queue
|
105
|
+
|
106
|
+
job = item ? RetryableJob.new(@job.retries, item) : @job.dup
|
107
|
+
|
108
|
+
job.retries += 1
|
109
|
+
|
110
|
+
if job.retries < @max_retries
|
111
|
+
client.set(current_queue + "_errors", job)
|
112
|
+
else
|
113
|
+
raise RetriesExceeded.new(job)
|
114
|
+
end
|
115
|
+
|
116
|
+
ensure
|
117
|
+
close_last_transaction
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def read_from_error_queue?
|
123
|
+
rand < @error_rate
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Siberite
|
2
|
+
class Client
|
3
|
+
class Unmarshal < Proxy
|
4
|
+
def get(key, opts = {})
|
5
|
+
response = client.get(key, opts.merge(:raw => true))
|
6
|
+
return response if opts[:raw]
|
7
|
+
|
8
|
+
if is_marshaled?(response)
|
9
|
+
Marshal.load(response)
|
10
|
+
else
|
11
|
+
response
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
if RUBY_VERSION.respond_to?(:getbyte)
|
16
|
+
def is_marshaled?(object)
|
17
|
+
o = object.to_s
|
18
|
+
o.getbyte(0) == Marshal::MAJOR_VERSION && o.getbyte(1) == Marshal::MINOR_VERSION
|
19
|
+
rescue Exception
|
20
|
+
false
|
21
|
+
end
|
22
|
+
else
|
23
|
+
def is_marshaled?(object)
|
24
|
+
o = object.to_s
|
25
|
+
o[0] == Marshal::MAJOR_VERSION && o[1] == Marshal::MINOR_VERSION
|
26
|
+
rescue Exception
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Siberite
|
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,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'siberite/client/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "siberite-client"
|
8
|
+
s.version = Siberite::VERSION
|
9
|
+
|
10
|
+
s.authors = ["Matt Freels", "Rael Dornfest", "Anton Bogdanovich"]
|
11
|
+
s.summary = "Ruby Siberite client"
|
12
|
+
s.description = "Ruby client for the Siberite queue server"
|
13
|
+
s.email = "anton@bogdanovich.co"
|
14
|
+
s.license = "Apache-2"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = `git ls-files -z`.split("\x0")
|
19
|
+
s.homepage = "http://github.com/bogdanovich/siberite-ruby"
|
20
|
+
|
21
|
+
s.add_dependency(%q<memcached>, [">= 0.19.6"])
|
22
|
+
|
23
|
+
s.files = `git ls-files -z`.split("\x0")
|
24
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
25
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
|
28
|
+
s.add_development_dependency "pry", "~> 0.10"
|
29
|
+
s.add_development_dependency "pry-byebug", "~> 2.0"
|
30
|
+
s.add_development_dependency "rspec", "~> 3.2"
|
31
|
+
s.add_development_dependency "rr", "~> 1.1"
|
32
|
+
s.add_development_dependency "activesupport", "> 3.1"
|
33
|
+
s.add_development_dependency "bundler", "~> 1.7"
|
34
|
+
s.add_development_dependency "rake", "~> 10.0"
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Siberite::Client::Blocking" do
|
4
|
+
describe "Instance Methods" do
|
5
|
+
before do
|
6
|
+
@raw_client = Siberite::Client.new(*Siberite::Config.default)
|
7
|
+
@client = Siberite::Client::Blocking.new(@raw_client)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#get" do
|
11
|
+
before do
|
12
|
+
@queue = "some_queue"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "blocks on a get until the get works" do
|
16
|
+
mock(@raw_client).
|
17
|
+
get(@queue) { nil }.times(5).then.get(@queue) { :mcguffin }
|
18
|
+
@client.get(@queue).should == :mcguffin
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#get_without_blocking" do
|
22
|
+
it "does not block" do
|
23
|
+
mock(@raw_client).get(@queue) { nil }
|
24
|
+
@client.get_without_blocking(@queue).should be_nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Envelope
|
4
|
+
class << self; attr_accessor :unwraps end
|
5
|
+
|
6
|
+
def initialize(item); @item = item end
|
7
|
+
def unwrap; self.class.unwraps += 1; @item end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe Siberite::Client::Envelope do
|
11
|
+
describe "Instance Methods" do
|
12
|
+
before do
|
13
|
+
Envelope.unwraps = 0
|
14
|
+
@raw_client = Siberite::Client.new(*Siberite::Config.default)
|
15
|
+
@client = Siberite::Client::Envelope.new(Envelope, @raw_client)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#get and #set" do
|
19
|
+
describe "envelopes" do
|
20
|
+
it "integrates" do
|
21
|
+
@client.set("a_queue", :mcguffin)
|
22
|
+
@client.get("a_queue").should == :mcguffin
|
23
|
+
Envelope.unwraps.should == 1
|
24
|
+
end
|
25
|
+
|
26
|
+
it "creates an envelope on a set" do
|
27
|
+
mock(Envelope).new(:mcguffin)
|
28
|
+
@client.set('a_queue', :mcguffin)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "unwraps an envelope on a get" do
|
32
|
+
envelope = Envelope.new(:mcguffin)
|
33
|
+
mock(@raw_client).get('a_queue') { envelope }
|
34
|
+
mock.proxy(envelope).unwrap
|
35
|
+
@client.get('a_queue').should == :mcguffin
|
36
|
+
end
|
37
|
+
|
38
|
+
it "does not unwrap a nil get" do
|
39
|
+
mock(@raw_client).get('a_queue') { nil }
|
40
|
+
@client.get('a_queue').should be_nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|