siberite-client 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ module Siberite
2
+ class Client
3
+ class Proxy
4
+ attr_reader :client
5
+
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ def method_missing(method, *args, &block)
11
+ client.send(method, *args, &block)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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,3 @@
1
+ module Siberite
2
+ VERSION = "0.8.0"
3
+ 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