tuber 0.0.1 → 0.5.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +68 -0
- data/.gitignore +17 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +112 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +27 -0
- data/README.md +379 -8
- data/Rakefile +35 -0
- data/TODO +1 -0
- data/examples/demo.rb +97 -0
- data/lib/tuber/configuration.rb +31 -0
- data/lib/tuber/connection.rb +366 -0
- data/lib/tuber/errors.rb +78 -0
- data/lib/tuber/job/collection.rb +160 -0
- data/lib/tuber/job/record.rb +226 -0
- data/lib/tuber/job.rb +4 -0
- data/lib/tuber/stats/fast_struct.rb +98 -0
- data/lib/tuber/stats/stat_struct.rb +52 -0
- data/lib/tuber/stats.rb +81 -0
- data/lib/tuber/tube/collection.rb +253 -0
- data/lib/tuber/tube/record.rb +231 -0
- data/lib/tuber/tube.rb +4 -0
- data/lib/tuber/version.rb +6 -0
- data/lib/tuber.rb +104 -2
- data/test/connection_test.rb +291 -0
- data/test/errors_test.rb +35 -0
- data/test/job_test.rb +250 -0
- data/test/jobs_test.rb +152 -0
- data/test/prompt_regexp_test.rb +59 -0
- data/test/stat_struct_test.rb +58 -0
- data/test/stats_test.rb +45 -0
- data/test/test_helper.rb +74 -0
- data/test/tube_test.rb +299 -0
- data/test/tuber_test.rb +112 -0
- data/test/tubes_test.rb +329 -0
- data/tuber.gemspec +33 -0
- metadata +112 -6
data/Rakefile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require 'rake/testtask'
|
|
5
|
+
require 'yard'
|
|
6
|
+
require 'redcarpet'
|
|
7
|
+
|
|
8
|
+
# rake test
|
|
9
|
+
Rake::TestTask.new do |t|
|
|
10
|
+
t.libs.push "lib"
|
|
11
|
+
t.test_files = FileList[File.expand_path('../test/**/*_test.rb', __FILE__)] -
|
|
12
|
+
FileList[File.expand_path('../test/**/tuber_test.rb', __FILE__)]
|
|
13
|
+
t.verbose = true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# rake test:integration
|
|
17
|
+
Rake::TestTask.new("test:integration") do |t|
|
|
18
|
+
t.libs.push "lib"
|
|
19
|
+
t.test_files = FileList[File.expand_path('../test/**/tuber_test.rb', __FILE__)]
|
|
20
|
+
t.verbose = true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# rake test:full
|
|
24
|
+
Rake::TestTask.new("test:full") do |t|
|
|
25
|
+
t.libs.push "lib"
|
|
26
|
+
t.test_files = FileList[File.expand_path('../test/**/*_test.rb', __FILE__)]
|
|
27
|
+
t.verbose = true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
YARD::Rake::YardocTask.new do |t|
|
|
31
|
+
t.files = ['lib/tuber/**/*.rb']
|
|
32
|
+
t.options = []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
task :default => 'test:full'
|
data/TODO
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- Remove connection from pool if it's not responding and be able to add more connections and reattempt later
|
data/examples/demo.rb
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'term/ansicolor'
|
|
4
|
+
class String; include Term::ANSIColor; end
|
|
5
|
+
def step(msg); "\n[STEP] #{msg}...".yellow; end
|
|
6
|
+
$:.unshift("../lib")
|
|
7
|
+
require 'tuber'
|
|
8
|
+
|
|
9
|
+
# Establish a pool of beanstalks
|
|
10
|
+
puts step("Connecting to Beanstalk")
|
|
11
|
+
bc = Tuber.new('localhost')
|
|
12
|
+
puts bc
|
|
13
|
+
|
|
14
|
+
# Print out key stats
|
|
15
|
+
puts step("Print Stats")
|
|
16
|
+
p bc.stats.keys
|
|
17
|
+
p [bc.stats.total_connections, bc.stats[:total_connections], bc.stats['total_connections']]
|
|
18
|
+
|
|
19
|
+
# find tube
|
|
20
|
+
puts step("Find tube")
|
|
21
|
+
tube = bc.tubes.find('tube2')
|
|
22
|
+
puts tube
|
|
23
|
+
|
|
24
|
+
# Put job onto tube
|
|
25
|
+
puts step("Put job")
|
|
26
|
+
response = tube.put "foo bar", :pri => 1000, :ttr => 10, :delay => 0
|
|
27
|
+
puts response
|
|
28
|
+
|
|
29
|
+
# peek tube
|
|
30
|
+
puts step("Peek tube")
|
|
31
|
+
p tube.peek :ready
|
|
32
|
+
|
|
33
|
+
# watch tube
|
|
34
|
+
bc.tubes.watch!('tube2')
|
|
35
|
+
|
|
36
|
+
# Check tube stats
|
|
37
|
+
puts step("Get tube stats")
|
|
38
|
+
p tube.stats.keys
|
|
39
|
+
p tube.stats.name
|
|
40
|
+
p tube.stats.current_jobs_ready
|
|
41
|
+
|
|
42
|
+
# Reserve job from tube
|
|
43
|
+
puts step("Reserve job")
|
|
44
|
+
p job = bc.tubes.reserve
|
|
45
|
+
jid = job.id
|
|
46
|
+
|
|
47
|
+
# pause tube
|
|
48
|
+
puts step("Pause tube")
|
|
49
|
+
p tube.pause(1)
|
|
50
|
+
|
|
51
|
+
# Register jobs
|
|
52
|
+
puts step("Register jobs for tubes")
|
|
53
|
+
bc.jobs.register('tube_test', :retry_on => [Timeout::Error]) do |job|
|
|
54
|
+
p 'tube_test'
|
|
55
|
+
p job
|
|
56
|
+
raise Tuber::AbortProcessingError
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
bc.jobs.register('tube_test2', :retry_on => [Timeout::Error]) do |job|
|
|
60
|
+
p 'tube_test2'
|
|
61
|
+
p job
|
|
62
|
+
raise Tuber::AbortProcessingError
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
p bc.jobs.processors
|
|
66
|
+
|
|
67
|
+
response = bc.tubes.find('tube_test').put "foo register", :pri => 1000, :ttr => 10, :delay => 0
|
|
68
|
+
response = bc.tubes.find('tube_test2').put "foo baz", :pri => 1000, :ttr => 10, :delay => 0
|
|
69
|
+
|
|
70
|
+
# Process jobs
|
|
71
|
+
puts step("Process jobs")
|
|
72
|
+
2.times { bc.jobs.process! }
|
|
73
|
+
|
|
74
|
+
# Get job from id (peek job)
|
|
75
|
+
puts step("Get job from id")
|
|
76
|
+
p bc.jobs.find(jid)
|
|
77
|
+
p bc.jobs.peek(jid)
|
|
78
|
+
|
|
79
|
+
# Check job stats
|
|
80
|
+
puts step("Get job stats")
|
|
81
|
+
p job.stats.keys
|
|
82
|
+
p job.stats.tube
|
|
83
|
+
p job.stats.state
|
|
84
|
+
|
|
85
|
+
# bury job
|
|
86
|
+
puts step("Bury job")
|
|
87
|
+
p job.bury
|
|
88
|
+
|
|
89
|
+
# delete job
|
|
90
|
+
puts step("Delete job")
|
|
91
|
+
p job.delete
|
|
92
|
+
|
|
93
|
+
# list tubes
|
|
94
|
+
puts step("List tubes")
|
|
95
|
+
p bc.tubes.watched
|
|
96
|
+
p bc.tubes.used
|
|
97
|
+
p bc.tubes.all
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Tuber
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :default_put_delay # default delay value to put a job
|
|
6
|
+
attr_accessor :default_put_pri # default priority value to put a job
|
|
7
|
+
attr_accessor :default_put_ttr # default ttr value to put a job
|
|
8
|
+
attr_accessor :job_parser # default job_parser to parse job body
|
|
9
|
+
attr_accessor :job_serializer # default serializer for job body
|
|
10
|
+
attr_accessor :tuber_url # default server url
|
|
11
|
+
alias_method :beanstalkd_url, :tuber_url # compatibility with beaneater configs
|
|
12
|
+
alias_method :beanstalkd_url=, :tuber_url=
|
|
13
|
+
attr_accessor :connect_timeout # TCP connect timeout in seconds
|
|
14
|
+
attr_accessor :resolv_timeout # DNS resolve timeout in seconds
|
|
15
|
+
attr_accessor :read_timeout # socket read timeout in seconds
|
|
16
|
+
attr_accessor :write_timeout # socket write timeout in seconds
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@default_put_delay = 0
|
|
20
|
+
@default_put_pri = 65536
|
|
21
|
+
@default_put_ttr = 120
|
|
22
|
+
@job_parser = lambda { |body| body }
|
|
23
|
+
@job_serializer = lambda { |body| body }
|
|
24
|
+
@tuber_url = nil
|
|
25
|
+
@connect_timeout = nil
|
|
26
|
+
@resolv_timeout = nil
|
|
27
|
+
@read_timeout = nil
|
|
28
|
+
@write_timeout = nil
|
|
29
|
+
end
|
|
30
|
+
end # Configuration
|
|
31
|
+
end # Tuber
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'socket'
|
|
5
|
+
|
|
6
|
+
class Tuber
|
|
7
|
+
# Represents a connection to a beanstalkd instance.
|
|
8
|
+
class Connection
|
|
9
|
+
|
|
10
|
+
# Default number of retries to send a command to a connection
|
|
11
|
+
MAX_RETRIES = 3
|
|
12
|
+
|
|
13
|
+
# Default retry interval
|
|
14
|
+
DEFAULT_RETRY_INTERVAL = 1
|
|
15
|
+
|
|
16
|
+
# @!attribute address
|
|
17
|
+
# @return [String] returns Beanstalkd server address
|
|
18
|
+
# @example
|
|
19
|
+
# @conn.address # => "localhost:11300"
|
|
20
|
+
# @!attribute host
|
|
21
|
+
# @return [String] returns Beanstalkd server host
|
|
22
|
+
# @example
|
|
23
|
+
# @conn.host # => "localhost"
|
|
24
|
+
# @!attribute port
|
|
25
|
+
# @return [Integer] returns Beanstalkd server port
|
|
26
|
+
# @example
|
|
27
|
+
# @conn.port # => "11300"
|
|
28
|
+
# @!attribute connection
|
|
29
|
+
# @return [Net::TCPSocket] returns connection object
|
|
30
|
+
attr_reader :address, :host, :port, :connection
|
|
31
|
+
|
|
32
|
+
# @!attribute tubes_watched
|
|
33
|
+
# @returns [Array<String>] returns currently watched tube names
|
|
34
|
+
# @!attribute tube_used
|
|
35
|
+
# @returns [String] returns currently used tube name
|
|
36
|
+
attr_accessor :tubes_watched, :tube_used
|
|
37
|
+
|
|
38
|
+
# Default port value for beanstalk connection
|
|
39
|
+
DEFAULT_PORT = 11300
|
|
40
|
+
|
|
41
|
+
# Commands that must not be retransmitted after a dropped connection.
|
|
42
|
+
# The socket dying between write and readline leaves the first send's
|
|
43
|
+
# fate unknown: a re-sent put can insert a duplicate job, and a re-sent
|
|
44
|
+
# delete/release/bury/touch acts on a job whose reservation died with
|
|
45
|
+
# the old socket — the server answers NOT_FOUND for work that actually
|
|
46
|
+
# succeeded. For these verbs the connection is healed but the original
|
|
47
|
+
# error is re-raised so the caller decides. Everything else (reserve,
|
|
48
|
+
# watch, stats, peek, ...) converges to the same state on a re-send and
|
|
49
|
+
# keeps the transparent retry.
|
|
50
|
+
NON_IDEMPOTENT_COMMANDS = %w[put delete delete-batch release bury touch touch-all kick kick-job].freeze
|
|
51
|
+
|
|
52
|
+
# Initializes new connection.
|
|
53
|
+
#
|
|
54
|
+
# @param [String] address beanstalkd instance address.
|
|
55
|
+
# @example
|
|
56
|
+
# Tuber::Connection.new('127.0.0.1')
|
|
57
|
+
# Tuber::Connection.new('127.0.0.1:11300')
|
|
58
|
+
#
|
|
59
|
+
# ENV['TUBER_URL'] = '127.0.0.1:11300'
|
|
60
|
+
# @b = Tuber.new
|
|
61
|
+
# @b.connection.host # => '127.0.0.1'
|
|
62
|
+
# @b.connection.port # => '11300'
|
|
63
|
+
#
|
|
64
|
+
def initialize(address)
|
|
65
|
+
@address = address || _host_from_env || Tuber.configuration.tuber_url
|
|
66
|
+
@mutex = Mutex.new
|
|
67
|
+
@tube_used = 'default'
|
|
68
|
+
@tubes_watched = ['default']
|
|
69
|
+
|
|
70
|
+
establish_connection
|
|
71
|
+
rescue
|
|
72
|
+
_raise_not_connected!
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Send commands to beanstalkd server via connection.
|
|
76
|
+
#
|
|
77
|
+
# @param [Hash{String => String, Number}>] options Retained for compatibility
|
|
78
|
+
# @param [String] command Beanstalkd command
|
|
79
|
+
# @return [Array<Hash{String => String, Number}>] Beanstalkd command response
|
|
80
|
+
# @example
|
|
81
|
+
# @conn = Tuber::Connection.new
|
|
82
|
+
# @conn.transmit('bury 123')
|
|
83
|
+
# @conn.transmit('stats')
|
|
84
|
+
#
|
|
85
|
+
def transmit(command, **options)
|
|
86
|
+
verb = command.to_s[/\A\S+/]
|
|
87
|
+
retransmit = !NON_IDEMPOTENT_COMMANDS.include?(verb)
|
|
88
|
+
_with_retry(retransmit: retransmit, **options.slice(:retry_interval, :init)) do
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
_raise_not_connected! unless connection
|
|
91
|
+
|
|
92
|
+
command = command.dup.force_encoding('ASCII-8BIT') if command.respond_to?(:force_encoding)
|
|
93
|
+
connection.write(command.to_s + "\r\n")
|
|
94
|
+
res = connection.readline
|
|
95
|
+
parse_response(command, res)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Reserves a batch of jobs atomically.
|
|
101
|
+
#
|
|
102
|
+
# Without a +timeout+ (or with +timeout+ of 0) the command is non-blocking:
|
|
103
|
+
# it returns whatever is ready immediately, possibly an empty array. With a
|
|
104
|
+
# positive +timeout+ it long-polls, blocking until the first job arrives (up
|
|
105
|
+
# to +timeout+ seconds) and then draining everything ready, up to +count+.
|
|
106
|
+
#
|
|
107
|
+
# @param [Integer] count Maximum number of jobs to reserve
|
|
108
|
+
# @param [Integer] timeout Seconds to long-poll for the first job (nil = non-blocking)
|
|
109
|
+
# @return [Array<Hash>] Array of job hashes with :status, :id, :body keys
|
|
110
|
+
# @raise [Tuber::DeadlineSoonError] A reserved job's TTR is about to expire
|
|
111
|
+
#
|
|
112
|
+
def reserve_batch(count, timeout = nil)
|
|
113
|
+
_with_retry do
|
|
114
|
+
@mutex.synchronize do
|
|
115
|
+
_raise_not_connected! unless connection
|
|
116
|
+
|
|
117
|
+
cmd = timeout ? "reserve-batch #{count} #{timeout}" : "reserve-batch #{count}"
|
|
118
|
+
connection.write(cmd + "\r\n")
|
|
119
|
+
|
|
120
|
+
header = connection.readline.chomp
|
|
121
|
+
status, actual_count_str = header.split(/\s/, 2)
|
|
122
|
+
|
|
123
|
+
raise UnexpectedResponse.from_status(status, cmd) unless status == "RESERVED_BATCH"
|
|
124
|
+
|
|
125
|
+
actual_count = actual_count_str.to_i
|
|
126
|
+
jobs = []
|
|
127
|
+
actual_count.times do
|
|
128
|
+
line = connection.readline.chomp
|
|
129
|
+
_, job_id, bytes_str = line.split(/\s/)
|
|
130
|
+
bytes = bytes_str.to_i
|
|
131
|
+
body = connection.read(bytes)
|
|
132
|
+
crlf = connection.read(2)
|
|
133
|
+
raise ExpectedCrlfError.new("EXPECTED_CRLF", cmd) unless crlf == "\r\n"
|
|
134
|
+
|
|
135
|
+
body = config.job_parser.call(body)
|
|
136
|
+
jobs << { status: "RESERVED", id: job_id, body: body }
|
|
137
|
+
end
|
|
138
|
+
jobs
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Deletes a batch of jobs atomically.
|
|
144
|
+
#
|
|
145
|
+
# @param [Array<Integer, String>] ids Job IDs to delete
|
|
146
|
+
# @return [Hash] with :deleted and :not_found counts
|
|
147
|
+
#
|
|
148
|
+
def delete_batch(ids)
|
|
149
|
+
_with_retry(retransmit: false) do
|
|
150
|
+
@mutex.synchronize do
|
|
151
|
+
_raise_not_connected! unless connection
|
|
152
|
+
|
|
153
|
+
cmd = "delete-batch #{ids.join(' ')}"
|
|
154
|
+
connection.write(cmd + "\r\n")
|
|
155
|
+
|
|
156
|
+
res = connection.readline.chomp
|
|
157
|
+
status, deleted, not_found = res.split(/\s/)
|
|
158
|
+
|
|
159
|
+
raise UnexpectedResponse.from_status(status, cmd) unless status == "DELETED_BATCH"
|
|
160
|
+
|
|
161
|
+
{ deleted: deleted.to_i, not_found: not_found.to_i }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Close connection with beanstalkd server.
|
|
167
|
+
#
|
|
168
|
+
# @example
|
|
169
|
+
# @conn.close
|
|
170
|
+
#
|
|
171
|
+
def close
|
|
172
|
+
if @connection
|
|
173
|
+
@connection.close
|
|
174
|
+
@connection = nil
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns string representation of job.
|
|
179
|
+
#
|
|
180
|
+
# @example
|
|
181
|
+
# @conn.inspect
|
|
182
|
+
#
|
|
183
|
+
def to_s
|
|
184
|
+
"#<Tuber::Connection host=#{host.inspect} port=#{port.inspect}>"
|
|
185
|
+
end
|
|
186
|
+
alias :inspect :to_s
|
|
187
|
+
|
|
188
|
+
def add_to_watched(tube_name)
|
|
189
|
+
@tubes_watched << tube_name
|
|
190
|
+
@tubes_watched.uniq
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def remove_from_watched(tube_name)
|
|
194
|
+
@tubes_watched.delete(tube_name)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
protected
|
|
198
|
+
|
|
199
|
+
# Establish a connection based on beanstalk address.
|
|
200
|
+
#
|
|
201
|
+
# @return [Net::TCPSocket] connection for specified address.
|
|
202
|
+
# @raise [Tuber::NotConnected] Could not connect to specified beanstalkd instance.
|
|
203
|
+
# @example
|
|
204
|
+
# establish_connection('localhost:3005')
|
|
205
|
+
#
|
|
206
|
+
def establish_connection
|
|
207
|
+
@address = address.first if address.is_a?(Array)
|
|
208
|
+
match = address.split(':')
|
|
209
|
+
@host, @port = match[0], Integer(match[1] || DEFAULT_PORT)
|
|
210
|
+
|
|
211
|
+
tcp_opts = { connect_timeout: config.connect_timeout, resolv_timeout: config.resolv_timeout }.compact
|
|
212
|
+
|
|
213
|
+
socket = if RUBY_VERSION >= "3.0" && tcp_opts.any?
|
|
214
|
+
TCPSocket.new(@host, @port, **tcp_opts)
|
|
215
|
+
else
|
|
216
|
+
TCPSocket.new(@host, @port)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
begin
|
|
220
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, _timeval_for(config.read_timeout)) if config.read_timeout
|
|
221
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, _timeval_for(config.write_timeout)) if config.write_timeout
|
|
222
|
+
@connection = socket
|
|
223
|
+
rescue
|
|
224
|
+
socket.close rescue nil
|
|
225
|
+
raise
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Parses the response and returns the useful beanstalk response.
|
|
230
|
+
# Will read the body if one is indicated by the status.
|
|
231
|
+
#
|
|
232
|
+
# @param [String] cmd Beanstalk command transmitted
|
|
233
|
+
# @param [String] res Telnet command response
|
|
234
|
+
# @return [Array<Hash{String => String, Number}>] Beanstalk response with `status`, `id`, `body`
|
|
235
|
+
# @raise [Tuber::UnexpectedResponse] Response from beanstalk command was an error status
|
|
236
|
+
# @example
|
|
237
|
+
# parse_response("delete 56", "DELETED 56\nFOO")
|
|
238
|
+
# # => { :body => "FOO", :status => "DELETED", :id => 56 }
|
|
239
|
+
#
|
|
240
|
+
def parse_response(cmd, res)
|
|
241
|
+
status = res.chomp
|
|
242
|
+
body_values = status.split(/\s/)
|
|
243
|
+
status = body_values[0]
|
|
244
|
+
if status == "DRAINING" && cmd.strip.start_with?("drain")
|
|
245
|
+
return { status: status }
|
|
246
|
+
end
|
|
247
|
+
raise UnexpectedResponse.from_status(status, cmd) if UnexpectedResponse::ERROR_STATES.include?(status)
|
|
248
|
+
body = nil
|
|
249
|
+
if status == 'FLUSHED'
|
|
250
|
+
return { status: status, id: body_values[1] }
|
|
251
|
+
end
|
|
252
|
+
if ['OK','FOUND', 'RESERVED'].include?(status)
|
|
253
|
+
bytes_size = body_values[-1].to_i
|
|
254
|
+
raw_body = connection.read(bytes_size)
|
|
255
|
+
body = if status == 'OK'
|
|
256
|
+
psych_v4_valid_body = raw_body.gsub(/^(.*?): (.*)$/) { "#{$1}: #{$2.gsub(/[\:\-\~]/, '_')}" }
|
|
257
|
+
YAML.load(psych_v4_valid_body)
|
|
258
|
+
else
|
|
259
|
+
config.job_parser.call(raw_body)
|
|
260
|
+
end
|
|
261
|
+
crlf = connection.read(2) # \r\n
|
|
262
|
+
raise ExpectedCrlfError.new('EXPECTED_CRLF', cmd) if crlf != "\r\n"
|
|
263
|
+
end
|
|
264
|
+
id = body_values[1]
|
|
265
|
+
response = { :status => status }
|
|
266
|
+
response[:id] = id if id
|
|
267
|
+
response[:body] = body if body
|
|
268
|
+
response[:state] = body_values[2] if status == 'INSERTED' && body_values[2]
|
|
269
|
+
response
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Returns configuration options for tuber
|
|
273
|
+
#
|
|
274
|
+
# @return [Tuber::Configuration] configuration object
|
|
275
|
+
def config
|
|
276
|
+
Tuber.configuration
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
private
|
|
280
|
+
|
|
281
|
+
def _initialize_tubes
|
|
282
|
+
if @tubes_watched != ['default']
|
|
283
|
+
tubes_watched.each do |t|
|
|
284
|
+
transmit("watch #{t}", init: false)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
transmit("ignore default", init: false)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
transmit("use #{tube_used}", init: false) if @tube_used != 'default'
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Wrapper method for capturing certain failures and retry the payload block
|
|
294
|
+
#
|
|
295
|
+
# @param [Proc] block The command to execute.
|
|
296
|
+
# @param [Integer] retry_interval The time to wait before the next retry
|
|
297
|
+
# @param [Boolean] retransmit Whether the command is safe to re-send after
|
|
298
|
+
# a reconnect. When false the connection is still healed (reconnect +
|
|
299
|
+
# tube re-init) but the original connection error is re-raised.
|
|
300
|
+
# @param [Integer] tries The maximum number of tries in draining mode
|
|
301
|
+
# @return [Object] Result of the block passed
|
|
302
|
+
#
|
|
303
|
+
def _with_retry(retry_interval: DEFAULT_RETRY_INTERVAL, init: true, tries: MAX_RETRIES, retransmit: true, &block)
|
|
304
|
+
yield
|
|
305
|
+
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE,
|
|
306
|
+
Errno::ECONNREFUSED => ex
|
|
307
|
+
_reconnect(ex, retry_interval)
|
|
308
|
+
_initialize_tubes if init
|
|
309
|
+
raise ex unless retransmit
|
|
310
|
+
retry
|
|
311
|
+
rescue Tuber::DrainingError
|
|
312
|
+
tries -= 1
|
|
313
|
+
if tries.zero?
|
|
314
|
+
close
|
|
315
|
+
raise
|
|
316
|
+
end
|
|
317
|
+
sleep(retry_interval)
|
|
318
|
+
retry
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# Tries to re-establish connection to the beanstalkd
|
|
322
|
+
#
|
|
323
|
+
# @param [Exception] original_exception The exception caused the retry
|
|
324
|
+
# @param [Integer] retry_interval The time to wait before the next reconnect
|
|
325
|
+
# @param [Integer] tries The maximum number of attempts to reconnect
|
|
326
|
+
def _reconnect(original_exception, retry_interval, tries=MAX_RETRIES)
|
|
327
|
+
close
|
|
328
|
+
establish_connection
|
|
329
|
+
rescue Errno::ECONNREFUSED
|
|
330
|
+
tries -= 1
|
|
331
|
+
if tries.zero?
|
|
332
|
+
_raise_not_connected!
|
|
333
|
+
end
|
|
334
|
+
sleep(retry_interval || DEFAULT_RETRY_INTERVAL)
|
|
335
|
+
retry
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# The host provided by the TUBER_URL environment variable (or BEANSTALKD_URL,
|
|
339
|
+
# honoured for compatibility with beanstalkd tooling), if available.
|
|
340
|
+
#
|
|
341
|
+
# @return [String] A server host address
|
|
342
|
+
# @example
|
|
343
|
+
# ENV['TUBER_URL'] = "localhost:1212"
|
|
344
|
+
# # => 'localhost:1212'
|
|
345
|
+
#
|
|
346
|
+
def _host_from_env
|
|
347
|
+
url = ENV['TUBER_URL'] || ENV['BEANSTALKD_URL']
|
|
348
|
+
url.respond_to?(:length) && url.length > 0 && url.strip
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Packs a timeout value (in seconds) into a struct timeval binary string.
|
|
352
|
+
# Supports fractional seconds (e.g., 0.5 => 500000 usec).
|
|
353
|
+
def _timeval_for(timeout)
|
|
354
|
+
sec = timeout.to_i
|
|
355
|
+
usec = ((timeout.to_f - sec) * 1_000_000).to_i
|
|
356
|
+
[sec, usec].pack('l_l_')
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Raises an error to be triggered when the connection has failed
|
|
360
|
+
# @raise [Tuber::NotConnected] Beanstalkd is no longer connected
|
|
361
|
+
def _raise_not_connected!
|
|
362
|
+
raise Tuber::NotConnected, "Connection to beanstalk '#{@host}:#{@port}' is closed!"
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
end # Connection
|
|
366
|
+
end # Tuber
|
data/lib/tuber/errors.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Tuber
|
|
4
|
+
# Raises when a beanstalkd instance is no longer accessible.
|
|
5
|
+
class NotConnected < RuntimeError; end
|
|
6
|
+
# Raises when the tube name specified is invalid.
|
|
7
|
+
class InvalidTubeName < RuntimeError; end
|
|
8
|
+
# Raises when a job has not been reserved properly.
|
|
9
|
+
class JobNotReserved < RuntimeError; end
|
|
10
|
+
|
|
11
|
+
# Abstract class for errors that occur when a command does not complete successfully.
|
|
12
|
+
class UnexpectedResponse < RuntimeError
|
|
13
|
+
# Set of status states that are considered errors
|
|
14
|
+
ERROR_STATES = %w(OUT_OF_MEMORY INTERNAL_ERROR
|
|
15
|
+
BAD_FORMAT UNKNOWN_COMMAND JOB_TOO_BIG DRAINING
|
|
16
|
+
TIMED_OUT DEADLINE_SOON NOT_FOUND NOT_IGNORED EXPECTED_CRLF)
|
|
17
|
+
|
|
18
|
+
# @!attribute status
|
|
19
|
+
# @return [String] returns beanstalkd response status
|
|
20
|
+
# @example @ex.status # => "NOT_FOUND"
|
|
21
|
+
# @!attribute cmd
|
|
22
|
+
# @return [String] returns beanstalkd request command
|
|
23
|
+
# @example @ex.cmd # => "stats-job 23"
|
|
24
|
+
attr_reader :status, :cmd
|
|
25
|
+
|
|
26
|
+
# Initialize unexpected response error
|
|
27
|
+
#
|
|
28
|
+
# @param [Tuber::UnexpectedResponse] status Unexpected response object
|
|
29
|
+
# @param [String] cmd Beanstalkd request command
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# Tuber::UnexpectedResponse.new(NotFoundError, 'bury 123')
|
|
33
|
+
#
|
|
34
|
+
def initialize(status, cmd)
|
|
35
|
+
@status, @cmd = status, cmd
|
|
36
|
+
super("Response failed with: #{status}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Translate beanstalkd error status to ruby Exeception
|
|
40
|
+
#
|
|
41
|
+
# @param [String] status Beanstalkd error status
|
|
42
|
+
# @param [String] cmd Beanstalkd request command
|
|
43
|
+
#
|
|
44
|
+
# @return [Tuber::UnexpectedResponse] Exception for the status provided
|
|
45
|
+
# @example
|
|
46
|
+
# Tuber::UnexpectedResponse.new('NOT_FOUND', 'bury 123')
|
|
47
|
+
#
|
|
48
|
+
def self.from_status(status, cmd)
|
|
49
|
+
error_klazz_name = status.split('_').map { |w| w.capitalize }.join
|
|
50
|
+
error_klazz_name << "Error" unless error_klazz_name =~ /Error$/
|
|
51
|
+
error_klazz = Tuber.const_get(error_klazz_name)
|
|
52
|
+
error_klazz.new(status, cmd)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Raises when the beanstalkd instance runs out of memory
|
|
57
|
+
class OutOfMemoryError < UnexpectedResponse; end
|
|
58
|
+
# Raises when the beanstalkd instance is draining and new jobs cannot be inserted
|
|
59
|
+
class DrainingError < UnexpectedResponse; end
|
|
60
|
+
# Raises when the job or tube cannot be found
|
|
61
|
+
class NotFoundError < UnexpectedResponse; end
|
|
62
|
+
# Raises when the job reserved is going to be released within a second.
|
|
63
|
+
class DeadlineSoonError < UnexpectedResponse; end
|
|
64
|
+
# Raises when a beanstalkd has an internal error.
|
|
65
|
+
class InternalError < UnexpectedResponse; end
|
|
66
|
+
# Raises when a command was not properly formatted.
|
|
67
|
+
class BadFormatError < UnexpectedResponse; end
|
|
68
|
+
# Raises when a command was sent that is unknown.
|
|
69
|
+
class UnknownCommandError < UnexpectedResponse; end
|
|
70
|
+
# Raises when command does not have proper CRLF suffix.
|
|
71
|
+
class ExpectedCrlfError < UnexpectedResponse; end
|
|
72
|
+
# Raises when the body of a job was too large.
|
|
73
|
+
class JobTooBigError < UnexpectedResponse; end
|
|
74
|
+
# Raises when a job was attempted to be reserved but the timeout occurred.
|
|
75
|
+
class TimedOutError < UnexpectedResponse; end
|
|
76
|
+
# Raises when a tube could not be ignored because it is the last watched tube.
|
|
77
|
+
class NotIgnoredError < UnexpectedResponse; end
|
|
78
|
+
end
|