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.
@@ -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