beaneater 0.1.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,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