beaneater 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.yardopts +8 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +399 -0
- data/REF +23 -0
- data/Rakefile +23 -0
- data/TODO +2 -0
- data/beaneater.gemspec +24 -0
- data/examples/demo.rb +96 -0
- data/lib/beaneater.rb +10 -0
- data/lib/beaneater/connection.rb +110 -0
- data/lib/beaneater/errors.rb +73 -0
- data/lib/beaneater/job.rb +2 -0
- data/lib/beaneater/job/collection.rb +91 -0
- data/lib/beaneater/job/record.rb +174 -0
- data/lib/beaneater/pool.rb +141 -0
- data/lib/beaneater/pool_command.rb +71 -0
- data/lib/beaneater/stats.rb +55 -0
- data/lib/beaneater/stats/fast_struct.rb +96 -0
- data/lib/beaneater/stats/stat_struct.rb +39 -0
- data/lib/beaneater/tube.rb +2 -0
- data/lib/beaneater/tube/collection.rb +134 -0
- data/lib/beaneater/tube/record.rb +158 -0
- data/lib/beaneater/version.rb +4 -0
- data/test/beaneater_test.rb +115 -0
- data/test/connection_test.rb +64 -0
- data/test/errors_test.rb +26 -0
- data/test/job_test.rb +213 -0
- data/test/jobs_test.rb +107 -0
- data/test/pool_command_test.rb +68 -0
- data/test/pool_test.rb +154 -0
- data/test/stat_struct_test.rb +41 -0
- data/test/stats_test.rb +42 -0
- data/test/test_helper.rb +21 -0
- data/test/tube_test.rb +164 -0
- data/test/tubes_test.rb +153 -0
- metadata +181 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
module Beaneater
|
2
|
+
# Represents a stats hash with proper underscored keys
|
3
|
+
class StatStruct < FasterOpenStruct
|
4
|
+
# Convert a stats hash into a struct.
|
5
|
+
#
|
6
|
+
# @param [Hash(String => String)] hash Hash Stats hash to convert to struct
|
7
|
+
# @return [Beaneater::StatStruct, nil] Stats struct from hash
|
8
|
+
# @example
|
9
|
+
# s = StatStruct.from_hash(:foo => "bar")
|
10
|
+
# s.foo # => 'bar'
|
11
|
+
#
|
12
|
+
def self.from_hash(hash)
|
13
|
+
return unless hash.is_a?(Hash)
|
14
|
+
underscore_hash = hash.inject({}) { |r, (k, v)| r[k.to_s.gsub(/-/, '_')] = v; r }
|
15
|
+
self.new(underscore_hash)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Access value for stat with specified key.
|
19
|
+
#
|
20
|
+
# @param [String] key Key to fetch from stats.
|
21
|
+
# @return [String,Integer] Value for specified stat key.
|
22
|
+
# @example
|
23
|
+
# @stats['foo'] # => "bar"
|
24
|
+
#
|
25
|
+
def [](key)
|
26
|
+
self.send(key.to_s)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns set of keys within this struct
|
30
|
+
#
|
31
|
+
# @return [Array<String>] Value for specified stat key.
|
32
|
+
# @example
|
33
|
+
# @stats.keys # => ['foo', 'bar', 'baz']
|
34
|
+
#
|
35
|
+
def keys
|
36
|
+
@hash.keys.map { |k| k.to_s }
|
37
|
+
end
|
38
|
+
end # StatStruct
|
39
|
+
end # Beaneater
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Beaneater
|
2
|
+
# Represents collection of tube related commands.
|
3
|
+
class Tubes < PoolCommand
|
4
|
+
|
5
|
+
# Creates new tubes instance.
|
6
|
+
#
|
7
|
+
# @param [Beaneater::Pool] pool The beaneater pool for this tube.
|
8
|
+
# @example
|
9
|
+
# Beaneater::Tubes.new(@pool)
|
10
|
+
#
|
11
|
+
def initialize(pool)
|
12
|
+
@last_used = 'default'
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
# Finds the specified beanstalk tube.
|
17
|
+
#
|
18
|
+
# @param [String] tube_name Name of the beanstalkd tube
|
19
|
+
# @return [Beaneater::Tube] specified tube
|
20
|
+
# @example
|
21
|
+
# @pool.tubes.find('tube2')
|
22
|
+
# @pool.tubes['tube2']
|
23
|
+
# # => <Beaneater::Tube name="tube2">
|
24
|
+
#
|
25
|
+
def find(tube_name)
|
26
|
+
Tube.new(self.pool, tube_name)
|
27
|
+
end
|
28
|
+
alias_method :[], :find
|
29
|
+
|
30
|
+
# Reserves a ready job looking at all watched tubes.
|
31
|
+
#
|
32
|
+
# @param [Integer] timeout Number of seconds before timing out.
|
33
|
+
# @param [Proc] block Callback to perform on the reserved job.
|
34
|
+
# @yield [job] Reserved beaneater job.
|
35
|
+
# @return [Beaneater::Job] Reserved beaneater job.
|
36
|
+
# @example
|
37
|
+
# @conn.tubes.reserve { |job| process(job) }
|
38
|
+
# # => <Beaneater::Job id=5 body="foo">
|
39
|
+
#
|
40
|
+
def reserve(timeout=nil, &block)
|
41
|
+
res = transmit_to_rand(timeout ? "reserve-with-timeout #{timeout}" : 'reserve')
|
42
|
+
job = Job.new(res)
|
43
|
+
block.call(job) if block_given?
|
44
|
+
job
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set specified tube as used.
|
48
|
+
#
|
49
|
+
# @param [String] tube Tube to be used.
|
50
|
+
# @example
|
51
|
+
# @conn.tubes.use("some-tube")
|
52
|
+
#
|
53
|
+
def use(tube)
|
54
|
+
return tube if @last_used == tube
|
55
|
+
res = transmit_to_all("use #{tube}")
|
56
|
+
@last_used = tube
|
57
|
+
rescue BadFormatError
|
58
|
+
raise InvalidTubeName, "Tube cannot be named '#{tube}'"
|
59
|
+
end
|
60
|
+
|
61
|
+
# List of all known beanstalk tubes.
|
62
|
+
#
|
63
|
+
# @return [Array<Beaneater::Tube>] List of all beanstalk tubes.
|
64
|
+
# @example
|
65
|
+
# @pool.tubes.all
|
66
|
+
# # => [<Beaneater::Tube name="tube2">, <Beaneater::Tube name="tube3">]
|
67
|
+
#
|
68
|
+
def all
|
69
|
+
transmit_to_rand('list-tubes')[:body].map { |tube_name| Tube.new(self.pool, tube_name) }
|
70
|
+
end
|
71
|
+
|
72
|
+
# List of watched beanstalk tubes.
|
73
|
+
#
|
74
|
+
# @return [Array<Beaneater::Tube>] List of watched beanstalk tubes.
|
75
|
+
# @example
|
76
|
+
# @pool.tubes.watched
|
77
|
+
# # => [<Beaneater::Tube name="tube2">, <Beaneater::Tube name="tube3">]
|
78
|
+
#
|
79
|
+
def watched
|
80
|
+
transmit_to_rand('list-tubes-watched')[:body].map { |tube_name| Tube.new(self.pool, tube_name) }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Currently used beanstalk tube.
|
84
|
+
#
|
85
|
+
# @return [Beaneater::Tube] Currently used beanstalk tube.
|
86
|
+
# @example
|
87
|
+
# @pool.tubes.used
|
88
|
+
# # => <Beaneater::Tube name="tube2">
|
89
|
+
#
|
90
|
+
def used
|
91
|
+
Tube.new(self.pool, transmit_to_rand('list-tube-used')[:id])
|
92
|
+
end
|
93
|
+
|
94
|
+
# Add specified beanstalkd tubes as watched.
|
95
|
+
#
|
96
|
+
# @param [*String] names Name of tubes to watch
|
97
|
+
# @raise [Beaneater::InvalidTubeName] Tube to watch was invalid.
|
98
|
+
# @example
|
99
|
+
# @pool.tubes.watch('foo', 'bar')
|
100
|
+
#
|
101
|
+
def watch(*names)
|
102
|
+
names.each do |t|
|
103
|
+
transmit_to_all "watch #{t}"
|
104
|
+
end
|
105
|
+
rescue BadFormatError => ex
|
106
|
+
raise InvalidTubeName, "Tube in '#{ex.cmd}' is invalid!"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Add specified beanstalkd tubes as watched and ignores all other tubes.
|
110
|
+
#
|
111
|
+
# @param [*String] names Name of tubes to watch
|
112
|
+
# @raise [Beaneater::InvalidTubeName] Tube to watch was invalid.
|
113
|
+
# @example
|
114
|
+
# @pool.tubes.watch!('foo', 'bar')
|
115
|
+
#
|
116
|
+
def watch!(*names)
|
117
|
+
old_tubes = watched.map(&:name) - names.map(&:to_s)
|
118
|
+
watch(*names)
|
119
|
+
ignore(*old_tubes)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Ignores specified beanstalkd tubes.
|
123
|
+
#
|
124
|
+
# @param [*String] names Name of tubes to ignore
|
125
|
+
# @example
|
126
|
+
# @pool.tubes.ignore('foo', 'bar')
|
127
|
+
#
|
128
|
+
def ignore(*names)
|
129
|
+
names.each do |w|
|
130
|
+
transmit_to_all "ignore #{w}"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end # Tubes
|
134
|
+
end # Beaneater
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module Beaneater
|
2
|
+
# Beanstalk tube which contains jobs which can be inserted, reserved, et al.
|
3
|
+
class Tube < PoolCommand
|
4
|
+
# The default delay for inserted jobs.
|
5
|
+
DEFAULT_DELAY = 0
|
6
|
+
# Default priority for inserted jobs, 0 is the highest.
|
7
|
+
DEFAULT_PRIORITY = 65536
|
8
|
+
# Default time to respond for inserted jobs.
|
9
|
+
DEFAULT_TTR = 120
|
10
|
+
|
11
|
+
# @!attribute name
|
12
|
+
# @return [String] name of the tube
|
13
|
+
attr_reader :name
|
14
|
+
|
15
|
+
# Fetches the specified tube.
|
16
|
+
#
|
17
|
+
# @param [Beaneater::Pool] pool The beaneater pool for this tube.
|
18
|
+
# @param [String] name The name for this tube.
|
19
|
+
# @example
|
20
|
+
# Beaneater::Tube.new(@pool, 'tube-name')
|
21
|
+
#
|
22
|
+
def initialize(pool, name)
|
23
|
+
@name = name.to_s
|
24
|
+
@mutex = Mutex.new
|
25
|
+
super(pool)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Inserts job with specified body onto tube.
|
29
|
+
#
|
30
|
+
# @param [String] body The data to store with this job.
|
31
|
+
# @param [Hash] options The settings associated with this job.
|
32
|
+
# @option options [Integer] pri priority for this job
|
33
|
+
# @option options [Integer] ttr time to respond for this job
|
34
|
+
# @option options [Integer] delay delay for this job
|
35
|
+
# @return [Hash] beanstalkd command response
|
36
|
+
# @example
|
37
|
+
# @tube.put "data", :pri => 1000, :ttr => 10, :delay => 5
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def put(body, options={})
|
41
|
+
safe_use do
|
42
|
+
options = { :pri => DEFAULT_PRIORITY, :delay => DEFAULT_DELAY, :ttr => DEFAULT_TTR }.merge(options)
|
43
|
+
cmd_options = "#{options[:pri]} #{options[:delay]} #{options[:ttr]} #{body.bytesize}"
|
44
|
+
transmit_to_rand("put #{cmd_options}\n#{body}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Peek at next job within this tube in given `state`.
|
49
|
+
#
|
50
|
+
# @param [String] state The job state to peek at (`ready`, `buried`, `delayed`)
|
51
|
+
# @return [Beaneater::Job] The next job within this tube.
|
52
|
+
# @example
|
53
|
+
# @tube.peek(:ready) # => <Beaneater::Job id=5 body=foo>
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def peek(state)
|
57
|
+
safe_use do
|
58
|
+
res = transmit_until_res "peek-#{state}", :status => "FOUND"
|
59
|
+
Job.new(res)
|
60
|
+
end
|
61
|
+
rescue Beaneater::NotFoundError => ex
|
62
|
+
# Return nil if not found
|
63
|
+
end
|
64
|
+
|
65
|
+
# Reserves the next job from tube.
|
66
|
+
#
|
67
|
+
# @param [Integer] timeout Number of seconds before timing out
|
68
|
+
# @param [Proc] block Callback to perform on reserved job
|
69
|
+
# @yield [job] Job that was reserved.
|
70
|
+
# @return [Beaneater::Job] Job that was reserved.
|
71
|
+
# @example
|
72
|
+
# @tube.reserve # => <Beaneater::Job id=5 body=foo>
|
73
|
+
#
|
74
|
+
def reserve(timeout=nil, &block)
|
75
|
+
pool.tubes.watch!(self.name)
|
76
|
+
pool.tubes.reserve(timeout, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Kick specified number of jobs from buried to ready state.
|
80
|
+
#
|
81
|
+
# @param [Integer] bounds The number of jobs to kick.
|
82
|
+
# @return [Hash] Beanstalkd command response
|
83
|
+
# @example
|
84
|
+
# @tube.kick(5)
|
85
|
+
#
|
86
|
+
def kick(bounds=1)
|
87
|
+
safe_use { transmit_to_rand("kick #{bounds}") }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns related stats for this tube.
|
91
|
+
#
|
92
|
+
# @return [Beaneater::StatStruct] Struct of tube related values
|
93
|
+
# @example
|
94
|
+
# @tube.stats.delayed # => 24
|
95
|
+
#
|
96
|
+
def stats
|
97
|
+
res = transmit_to_all("stats-tube #{name}", :merge => true)
|
98
|
+
StatStruct.from_hash(res[:body])
|
99
|
+
end
|
100
|
+
|
101
|
+
# Pause the execution of this tube for specified `delay`.
|
102
|
+
#
|
103
|
+
# @param [Integer] delay Number of seconds to delay tube execution
|
104
|
+
# @return [Hash] Beanstalkd command response
|
105
|
+
# @example
|
106
|
+
# @tube.pause(10)
|
107
|
+
#
|
108
|
+
def pause(delay)
|
109
|
+
transmit_to_all("pause-tube #{name} #{delay}")
|
110
|
+
end
|
111
|
+
|
112
|
+
# Clears all unreserved jobs in all states from the tube
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# @tube.clear
|
116
|
+
#
|
117
|
+
def clear
|
118
|
+
pool.tubes.watch!(self.name)
|
119
|
+
%w(delayed buried ready).each do |state|
|
120
|
+
while job = self.peek(state.to_sym)
|
121
|
+
job.delete
|
122
|
+
end
|
123
|
+
end
|
124
|
+
pool.tubes.ignore(name)
|
125
|
+
rescue Beaneater::UnexpectedResponse
|
126
|
+
# swallow any issues
|
127
|
+
end
|
128
|
+
|
129
|
+
# String representation of tube.
|
130
|
+
#
|
131
|
+
# @return [String] Representation of tube including name.
|
132
|
+
# @example
|
133
|
+
# @tube.to_s # => "#<Beaneater::Tube name=foo>"
|
134
|
+
#
|
135
|
+
def to_s
|
136
|
+
"#<Beaneater::Tube name=#{name.inspect}>"
|
137
|
+
end
|
138
|
+
alias :inspect :to_s
|
139
|
+
|
140
|
+
protected
|
141
|
+
|
142
|
+
# Transmits a beanstalk command that requires this tube to be set as used.
|
143
|
+
#
|
144
|
+
# @param [Proc] block Beanstalk command to transmit.
|
145
|
+
# @return [Object] Result of block passed
|
146
|
+
# @example
|
147
|
+
# safe_use { transmit_to_rand("kick 1") }
|
148
|
+
# # => "Response to kick command"
|
149
|
+
#
|
150
|
+
def safe_use(&block)
|
151
|
+
@mutex.lock
|
152
|
+
tubes.use(self.name)
|
153
|
+
yield
|
154
|
+
ensure
|
155
|
+
@mutex.unlock
|
156
|
+
end
|
157
|
+
end # Tube
|
158
|
+
end # Beaneater
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
|
3
|
+
describe "beanstalk-client" do
|
4
|
+
before do
|
5
|
+
@beanstalk = Beaneater::Pool.new(['127.0.0.1:11300'])
|
6
|
+
@tubes = ['one', 'two', 'three']
|
7
|
+
|
8
|
+
# Put something on each tube so they exist
|
9
|
+
tube_one = @beanstalk.tubes.find('one')
|
10
|
+
tube_one.put('one')
|
11
|
+
|
12
|
+
tube_two = @beanstalk.tubes.find('two')
|
13
|
+
tube_two.put('two')
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "test thread safe one" do
|
17
|
+
before do
|
18
|
+
# Create threads that will execute
|
19
|
+
# A: use one
|
20
|
+
# B: use one
|
21
|
+
# B: put two
|
22
|
+
# A: put one
|
23
|
+
a = Thread.new do
|
24
|
+
tube_one = @beanstalk.tubes.find('one')
|
25
|
+
sleep 4
|
26
|
+
tube_one.put('one')
|
27
|
+
end
|
28
|
+
|
29
|
+
b = Thread.new do
|
30
|
+
sleep 1
|
31
|
+
tube_two = @beanstalk.tubes.find('two')
|
32
|
+
tube_two.put('two')
|
33
|
+
end
|
34
|
+
|
35
|
+
a.join
|
36
|
+
b.join
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should return correct current-jobs-ready for tube one" do
|
40
|
+
one = @beanstalk.tubes.find('one').stats
|
41
|
+
assert_equal 2, one.current_jobs_ready
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return correct current-jobs-ready for tube two" do
|
45
|
+
two = @beanstalk.tubes.find('two').stats
|
46
|
+
assert_equal 2, two.current_jobs_ready
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "test thread safe two" do
|
51
|
+
before do
|
52
|
+
a = Thread.new do
|
53
|
+
tube_one = @beanstalk.tubes.find('one')
|
54
|
+
sleep 4
|
55
|
+
tube_one.put('one')
|
56
|
+
end
|
57
|
+
|
58
|
+
b = Thread.new do
|
59
|
+
tube_two = @beanstalk.tubes.find('two')
|
60
|
+
sleep 1
|
61
|
+
tube_two.put('two')
|
62
|
+
end
|
63
|
+
|
64
|
+
a.join
|
65
|
+
b.join
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should return correct current-jobs-ready for tube one" do
|
69
|
+
one = @beanstalk.tubes.find('one').stats
|
70
|
+
assert_equal 2, one.current_jobs_ready
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should return correct current-jobs-ready for tube two" do
|
74
|
+
two = @beanstalk.tubes.find('two').stats
|
75
|
+
assert_equal 2, two.current_jobs_ready
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "test delete job in reserved state" do
|
80
|
+
before do
|
81
|
+
@tube_three = @beanstalk.tubes.find('three')
|
82
|
+
@tube_three.put('one')
|
83
|
+
@job = @tube_three.reserve
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should be deleted properly" do
|
87
|
+
assert_equal 'one', @job.body
|
88
|
+
assert_equal 'one', @beanstalk.jobs.find(@job.id).body
|
89
|
+
@job.delete
|
90
|
+
assert_nil @beanstalk.jobs.find(@job.id)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "test delete job in buried state" do
|
95
|
+
before do
|
96
|
+
@tube_three = @beanstalk.tubes.find('three')
|
97
|
+
@tube_three.put('two')
|
98
|
+
@job = @tube_three.reserve
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should delete job as expected in buried state" do
|
102
|
+
assert_equal 'two', @job.body
|
103
|
+
@job.bury
|
104
|
+
assert_equal 'two', @tube_three.peek(:buried).body
|
105
|
+
|
106
|
+
@job.delete
|
107
|
+
assert_nil @beanstalk.jobs.find(@job.id)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
after do
|
112
|
+
cleanup_tubes!(@tubes, @beanstalk)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|