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