beaneater 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,174 @@
1
+ module Beaneater
2
+ # Represents job related commands.
3
+ class Job
4
+
5
+ # @!attribute id
6
+ # @return [Integer] returns Job id
7
+ # @!attribute body
8
+ # @return [String] returns Job body
9
+ # @!attribute connection
10
+ # @return [Beaneater::Connection] returns Connection which has retrieved job
11
+ # @!attribute reserved
12
+ # @return [Boolean] returns If job is being reserved
13
+ attr_reader :id, :body, :connection, :reserved
14
+
15
+
16
+ # Initialize new connection
17
+ #
18
+ # @param [Hash] res result from beanstalkd response
19
+ def initialize(res)
20
+ @id = res[:id]
21
+ @body = res[:body]
22
+ @connection = res[:connection]
23
+ @reserved = res[:status] == 'RESERVED'
24
+ end
25
+
26
+ # Send command to bury job
27
+ #
28
+ # @param [Hash] options Settings to bury job
29
+ # @option options [Integer] pri Assign new priority to job
30
+ #
31
+ # @example
32
+ # @beaneater_connection.bury({:pri => 100})
33
+ #
34
+ # @api public
35
+ def bury(options={})
36
+ options = { :pri => stats.pri }.merge(options)
37
+ with_reserved("bury #{id} #{options[:pri]}") do
38
+ @reserved = false
39
+ end
40
+ end
41
+
42
+ # Send command to release job
43
+ #
44
+ # @param [Hash] options Settings to release job
45
+ # @option options [Integer] pri Assign new priority to job
46
+ # @option options [Integer] pri Assign new delay to job
47
+ #
48
+ # @example
49
+ # @beaneater_connection.jobs.find(123).release(:pri => 10, :delay => 5)
50
+ #
51
+ # @api public
52
+ def release(options={})
53
+ options = { :pri => stats.pri, :delay => stats.delay }.merge(options)
54
+ with_reserved("release #{id} #{options[:pri]} #{options[:delay]}") do
55
+ @reserved = false
56
+ end
57
+ end
58
+
59
+ # Send command to touch job
60
+ #
61
+ # @example
62
+ # @beaneater_connection.jobs.find(123).touch
63
+ #
64
+ # @api public
65
+ def touch
66
+ with_reserved("touch #{id}")
67
+ end
68
+
69
+ # Send command to delete job
70
+ #
71
+ # @example
72
+ # @beaneater_connection.jobs.find(123).delete
73
+ #
74
+ # @api public
75
+ def delete
76
+ transmit("delete #{id}") { @reserved = false }
77
+ end
78
+
79
+ # Send command to kick job
80
+ #
81
+ # @example
82
+ # @beaneater_connection.jobs.find(123).kick
83
+ #
84
+ # @api public
85
+ def kick
86
+ transmit("kick-job #{id}")
87
+ end
88
+
89
+ # Send command to get stats about job
90
+ #
91
+ # @example
92
+ # @beaneater_connection.jobs.find(123).stats
93
+ #
94
+ # @api public
95
+ def stats
96
+ res = transmit("stats-job #{id}")
97
+ StatStruct.from_hash(res[:body])
98
+ end
99
+
100
+ # Check if job is being reserved
101
+ #
102
+ # @example
103
+ # @beaneater_connection.jobs.find(123).reserved?
104
+ #
105
+ # @api public
106
+ def reserved?
107
+ @reserved || self.stats.state == "reserved"
108
+ end
109
+
110
+ # Check if job exists
111
+ #
112
+ # @example
113
+ # @beaneater_connection.jobs.find(123).exists?
114
+ #
115
+ # @api public
116
+ def exists?
117
+ !!self.stats
118
+ rescue Beaneater::NotFoundError
119
+ false
120
+ end
121
+
122
+ # Returns the name of the tube this job is in
123
+ #
124
+ # @example
125
+ # @beaneater_connection.jobs.find(123).tube
126
+ #
127
+ # @api public
128
+ def tube
129
+ self.stats && self.stats.tube
130
+ end
131
+
132
+ # Returns string representation of job
133
+ #
134
+ # @example
135
+ # @beaneater_connection.jobs.find(123).to_s
136
+ # @beaneater_connection.jobs.find(123).inspect
137
+ #
138
+ # @api public
139
+ def to_s
140
+ "#<Beaneater::Job id=#{id} body=#{body.inspect}>"
141
+ end
142
+ alias :inspect :to_s
143
+
144
+ protected
145
+
146
+ # Transmit command to beanstalkd instances and fetch response.
147
+ #
148
+ # @param [String] cmd Beanstalkd command to send.
149
+ # @return [Hash] Beanstalkd response for the command.
150
+ # @example
151
+ # transmit('stats')
152
+ # transmit('stats') { 'success' }
153
+ #
154
+ def transmit(cmd, &block)
155
+ res = connection.transmit(cmd)
156
+ yield if block_given?
157
+ res
158
+ end
159
+
160
+ # Transmits a command which requires the job to be reserved.
161
+ #
162
+ # @param [String] cmd Beanstalkd command to send.
163
+ # @return [Hash] Beanstalkd response for the command.
164
+ # @raise [Beaneater::JobNotReserved] Command cannot execute since job is not reserved.
165
+ # @example
166
+ # with_reserved("bury 26") { @reserved = false }
167
+ #
168
+ def with_reserved(cmd, &block)
169
+ raise JobNotReserved unless reserved?
170
+ transmit(cmd, &block)
171
+ end
172
+
173
+ end # Job
174
+ end # Beaneater
@@ -0,0 +1,141 @@
1
+ # Simple ruby client for beanstalkd.
2
+ module Beaneater
3
+ # Represents collection of connections.
4
+ class Pool
5
+ # Default number of retries to send a command to a connection
6
+ MAX_RETRIES = 3
7
+
8
+ # @!attribute connections
9
+ # @return [Array<Beaneater::Connection>] returns Collection of connections
10
+ attr_reader :connections
11
+
12
+ # Initialize new connection
13
+ #
14
+ # @param [Array] hosts Array of beanstalkd server host
15
+ #
16
+ # @example
17
+ # Beaneater::Pool.new(['localhost:11300', '127.0.0.1:11300'])
18
+ #
19
+ # ENV['BEANSTALKD_URL'] = 'localhost:11300,127.0.0.1:11300'
20
+ # @bp = Beaneater::Pool.new
21
+ # @bp.connections.first.host # => 'localhost'
22
+ # @bp.connections.last.host # => '127.0.0.1'
23
+ def initialize(hosts=nil)
24
+ hosts = hosts || host_from_env
25
+ @connections = Array(hosts).map { |h| Connection.new(h) }
26
+ end
27
+
28
+ # Returns Beaneater::Stats object
29
+ #
30
+ # @api public
31
+ def stats
32
+ @stats ||= Stats.new(self)
33
+ end
34
+
35
+ # Returns Beaneater::Jobs object
36
+ #
37
+ # @api public
38
+ def jobs
39
+ @jobs ||= Jobs.new(self)
40
+ end
41
+
42
+ # Returns Beaneater::Tubes object
43
+ #
44
+ # @api public
45
+ def tubes
46
+ @tubes ||= Tubes.new(self)
47
+ end
48
+
49
+ # Send command to every beanstalkd servers set in pool
50
+ #
51
+ # @param [String] command Beanstalkd command
52
+ # @param [Hash] options telnet connections options
53
+ # @param [Proc] block Block passed in telnet connection object
54
+ #
55
+ # @example
56
+ # @pool.transmit_to_all("stats")
57
+ def transmit_to_all(command, options={}, &block)
58
+ connections.map do |conn|
59
+ safe_transmit { conn.transmit(command, options, &block) }
60
+ end
61
+ end
62
+
63
+ # Send command to a random beanstalkd servers set in pool
64
+ #
65
+ # @param [String] command Beanstalkd command
66
+ # @param [Hash] options telnet connections options
67
+ # @param [Proc] block Block passed in telnet connection object
68
+ #
69
+ # @example
70
+ # @pool.transmit_to_rand("stats", :match => /\n/)
71
+ def transmit_to_rand(command, options={}, &block)
72
+ safe_transmit do
73
+ conn = connections.respond_to?(:sample) ? connections.sample : connections.choice
74
+ conn.transmit(command, options, &block)
75
+ end
76
+ end
77
+
78
+ # Send command to each beanstalkd servers until getting response expected
79
+ #
80
+ # @param [String] command Beanstalkd command
81
+ # @param [Hash] options telnet connections options
82
+ # @param [Proc] block Block passed in telnet connection object
83
+ #
84
+ # @example
85
+ # @pool.transmit_until_res('peek-ready', :status => "FOUND", &block)
86
+ def transmit_until_res(command, options={}, &block)
87
+ status_expected = options.delete(:status)
88
+ connections.each do |conn|
89
+ res = safe_transmit { conn.transmit(command, options, &block) }
90
+ return res if res[:status] == status_expected
91
+ end && nil
92
+ end
93
+
94
+ # Closes all connections within pool
95
+ def close
96
+ while @connections.any?
97
+ conn = @connections.pop
98
+ conn.close
99
+ end
100
+ end
101
+
102
+ protected
103
+
104
+ # Transmit command to beanstalk connections safely handling failed connections
105
+ #
106
+ # @param [Proc] block The command to execute.
107
+ # @return [Object] Result of the block passed
108
+ # @raise [Beaneater::DrainingError,Beaneater::NotConnected] Could not connect to Beanstalk client
109
+ # @example
110
+ # safe_transmit { conn.transmit('foo') }
111
+ # # => "result of foo command from beanstalk"
112
+ #
113
+ def safe_transmit(&block)
114
+ retries = 1
115
+ begin
116
+ yield
117
+ rescue DrainingError, EOFError, Errno::ECONNRESET, Errno::EPIPE => ex
118
+ # TODO remove faulty connections from pool?
119
+ # https://github.com/kr/beanstalk-client-ruby/blob/master/lib/beanstalk-client/connection.rb#L405-410
120
+ if retries < MAX_RETRIES
121
+ retries += 1
122
+ retry
123
+ else # finished retrying, fail out
124
+ ex.is_a?(DrainingError) ? raise(ex) : raise(NotConnected, "Could not connect!")
125
+ end
126
+ end
127
+ end # transmit_call
128
+
129
+ # The hosts provided by BEANSTALKD_URL environment variable, if available.
130
+ #
131
+ # @return [Array] Set of beanstalkd host addresses
132
+ # @example
133
+ # ENV['BEANSTALKD_URL'] = "localhost:1212,localhost:2424"
134
+ # # => ['localhost:1212', 'localhost:2424']
135
+ #
136
+ def host_from_env
137
+ ENV['BEANSTALKD_URL'].respond_to?(:length) && ENV['BEANSTALKD_URL'].length > 0 && ENV['BEANSTALKD_URL'].split(',').map(&:strip)
138
+ end
139
+
140
+ end # Pool
141
+ end # Beaneater
@@ -0,0 +1,71 @@
1
+ require 'set'
2
+
3
+ module Beaneater
4
+ # Represents collection of pool related commands.
5
+ class PoolCommand
6
+ # @!attribute pool
7
+ # @return [Beaneater::Pool] returns Pool object
8
+ attr_reader :pool
9
+
10
+ # Initialize new connection
11
+ #
12
+ # @param [Beaneater::Pool] pool Pool object
13
+ def initialize(pool)
14
+ @pool = pool
15
+ end
16
+
17
+ # Delegate to Pool#transmit_to_all and if needed will merge responses from beanstalkd
18
+ #
19
+ # @param [String] body Beanstalkd command
20
+ # @param [Hash] options telnet connections options
21
+ # @option options [Boolean] merge Ask for merging responses or not
22
+ # @param [Proc] block Block passed in telnet connection object
23
+ #
24
+ # @example
25
+ # @pool.transmit_to_all("stats")
26
+ def transmit_to_all(body, options={}, &block)
27
+ merge = options.delete(:merge)
28
+ res = pool.transmit_to_all(body, options, &block)
29
+ if merge
30
+ res = { :status => res.first[:status], :body => sum_hashes(res.map { |r| r[:body] }) }
31
+ end
32
+ res
33
+ end
34
+
35
+ # Delegate missing methods to pool
36
+ # @api public
37
+ def method_missing(name, *args, &block)
38
+ if pool.respond_to?(name)
39
+ pool.send(name, *args, &block)
40
+ else # not a known pool command
41
+ super
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ # Selects hashes from collection and then merges the individual key values
48
+ #
49
+ # @param [Array<Hash>] hs Collection of hash responses returned from beanstalkd
50
+ # @return [Hash] Merged responses combining values from all the hash bodies
51
+ # @example
52
+ # self.sum_hashes([{ :foo => 1, :bar => 5 }, { :foo => 2, :bar => 3 }])
53
+ # => { :foo => 3, :bar => 8 }
54
+ #
55
+ def sum_hashes(hs)
56
+ hs.select { |h| h.is_a?(Hash) }.
57
+ inject({}) { |a,b| a.merge(b) { |k,o,n| combine_stats(k, o, n) } }
58
+ end
59
+
60
+ # Combine two values for given key
61
+ #
62
+ # @param [String] k key name within response hash
63
+ # @return [Set,Integer] combined value for stat
64
+ # @example
65
+ # self.combine_stats('total_connections', 4, 5) # => 9
66
+ #
67
+ def combine_stats(k, a, b)
68
+ ['name', 'version', 'pid'].include?(k) ? Set[a] + Set[b] : a + b
69
+ end
70
+ end # PoolCommand
71
+ end # Beaneater
@@ -0,0 +1,55 @@
1
+ require 'beaneater/stats/fast_struct'
2
+ require 'beaneater/stats/stat_struct'
3
+
4
+ module Beaneater
5
+ # Represents stats related to the beanstalkd pool.
6
+ class Stats < PoolCommand
7
+ # Returns keys for stats data
8
+ #
9
+ # @return [Array<String>] Set of keys for stats.
10
+ # @example
11
+ # @bp.stats.keys # => ["version", "total_connections"]
12
+ #
13
+ def keys
14
+ data.keys
15
+ end
16
+
17
+ # Returns value for specified key.
18
+ #
19
+ # @param [String,Symbol] key Name of key to retrieve
20
+ # @return [String,Integer] Value of specified key
21
+ # @example
22
+ # @bp.stats['total_connections'] # => 4
23
+ #
24
+ def [](key)
25
+ data[key]
26
+ end
27
+
28
+ # Defines a cached method for looking up data for specified key
29
+ # Protects against infinite loops by checking stacktrace
30
+ # @api public
31
+ def method_missing(name, *args, &block)
32
+ if caller.first !~ /`(method_missing|data')/ && data.keys.include?(name.to_s)
33
+ self.class.class_eval <<-CODE, __FILE__, __LINE__
34
+ def #{name}; data[#{name.inspect}]; end
35
+ CODE
36
+ data[name.to_s]
37
+ else # no key matches or caught infinite loop
38
+ super
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ # Returns struct based on stats data merged from all connections.
45
+ #
46
+ # @return [Beaneater::StatStruct] the combined stats for all beanstalk connections in the pool
47
+ # @example
48
+ # self.data # => { 'version' : 1.7, 'total_connections' : 23 }
49
+ # self.data.total_connections # => 23
50
+ #
51
+ def data
52
+ StatStruct.from_hash(transmit_to_all('stats', :merge => true)[:body])
53
+ end
54
+ end # Stats
55
+ end # Beaneater
@@ -0,0 +1,96 @@
1
+ module Beaneater
2
+ #
3
+ # Borrowed from:
4
+ # https://github.com/dolzenko/faster_open_struct/blob/master/lib/faster_open_struct.rb
5
+ #
6
+ # Up to 40 (!) times more memory efficient version of OpenStruct
7
+ #
8
+ # Differences from Ruby MRI OpenStruct:
9
+ #
10
+ # 1. Doesn't `dup` passed initialization hash (NOTE: only reference to hash is stored)
11
+ #
12
+ # 2. Doesn't convert hash keys to symbols (by default string keys are used,
13
+ # with fallback to symbol keys)
14
+ #
15
+ # 3. Creates methods on the fly on `OpenStruct` class, instead of singleton class.
16
+ # Uses `module_eval` with string to avoid holding scope references for every method.
17
+ #
18
+ # 4. Refactored, crud clean, spec covered :)
19
+ #
20
+ # @private
21
+ class FasterOpenStruct
22
+ # Undefine particularly nasty interfering methods on Ruby 1.8
23
+ undef :type if method_defined?(:type)
24
+ undef :id if method_defined?(:id)
25
+
26
+ def initialize(hash = nil)
27
+ @hash = hash || {}
28
+ @initialized_empty = hash == nil
29
+ end
30
+
31
+ def method_missing(method_name_sym, *args)
32
+ if method_name_sym.to_s[-1] == ?=
33
+ if args.size != 1
34
+ raise ArgumentError, "wrong number of arguments (#{args.size} for 1)", caller(1)
35
+ end
36
+
37
+ if self.frozen?
38
+ raise TypeError, "can't modify frozen #{self.class}", caller(1)
39
+ end
40
+
41
+ __new_ostruct_member__(method_name_sym.to_s.chomp("="))
42
+ send(method_name_sym, args[0])
43
+ elsif args.size == 0
44
+ __new_ostruct_member__(method_name_sym)
45
+ send(method_name_sym)
46
+ else
47
+ raise NoMethodError, "undefined method `#{method_name_sym}' for #{self}", caller(1)
48
+ end
49
+ end
50
+
51
+ def __new_ostruct_member__(method_name_sym)
52
+ self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
53
+ def #{ method_name_sym }
54
+ @hash.fetch("#{ method_name_sym }", @hash[:#{ method_name_sym }]) # read by default from string key, then try symbol
55
+ # if string key doesn't exist
56
+ end
57
+ END_EVAL
58
+
59
+ unless method_name_sym.to_s[-1] == ?? # can't define writer for predicate method
60
+ self.class.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
61
+ def #{ method_name_sym }=(val)
62
+ if @hash.key?("#{ method_name_sym }") || @initialized_empty # write by default to string key (when it is present
63
+ # in initialization hash or initialization hash
64
+ # wasn't provided)
65
+ @hash["#{ method_name_sym }"] = val # if it doesn't exist - write to symbol key
66
+ else
67
+ @hash[:#{ method_name_sym }] = val
68
+ end
69
+ end
70
+ END_EVAL
71
+ end
72
+ end
73
+
74
+ def empty?
75
+ @hash.empty?
76
+ end
77
+
78
+ #
79
+ # Compare this object and +other+ for equality.
80
+ #
81
+ def ==(other)
82
+ return false unless other.is_a?(self.class)
83
+ @hash == other.instance_variable_get(:@hash)
84
+ end
85
+
86
+ #
87
+ # Returns a string containing a detailed summary of the keys and values.
88
+ #
89
+ def inspect
90
+ str = "#<#{ self.class }"
91
+ str << " #{ @hash.map { |k, v| "#{ k }=#{ v.inspect }" }.join(", ") }" unless @hash.empty?
92
+ str << ">"
93
+ end
94
+ alias :to_s :inspect
95
+ end # FasterOpenStruct
96
+ end # Beaneater