siberite-client 0.8.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.
- 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
|