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.
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ # test/connection_test.rb
4
+
5
+ require File.expand_path('../test_helper', __FILE__)
6
+
7
+ describe Tuber::Connection do
8
+
9
+ describe 'for #new' do
10
+ before do
11
+ @host = 'localhost'
12
+ @bc = Tuber::Connection.new(@host)
13
+ end
14
+
15
+ it "should store address, host and port" do
16
+ assert_equal 'localhost', @bc.address
17
+ assert_equal 'localhost', @bc.host
18
+ assert_equal 11300, @bc.port
19
+ end
20
+
21
+ it "should init connection" do
22
+ assert_kind_of TCPSocket, @bc.connection
23
+ if @bc.connection.peeraddr[0] == 'AF_INET'
24
+ assert_equal '127.0.0.1', @bc.connection.peeraddr[3]
25
+ else
26
+ assert_equal 'AF_INET6', @bc.connection.peeraddr[0]
27
+ assert_equal '::1', @bc.connection.peeraddr[3]
28
+ end
29
+ assert_equal 11300, @bc.connection.peeraddr[1]
30
+ end
31
+
32
+ it "should raise on invalid connection" do
33
+ assert_raises(Tuber::NotConnected) { Tuber::Connection.new("localhost:8544") }
34
+ end
35
+
36
+ it "should support array connection to single connection" do
37
+ @bc2 = Tuber::Connection.new([@host])
38
+ assert_equal 'localhost', @bc.address
39
+ assert_equal 'localhost', @bc.host
40
+ assert_equal 11300, @bc.port
41
+ end
42
+
43
+ it "should use TUBER_URL over BEANSTALKD_URL from env" do
44
+ ENV['TUBER_URL'] = tuber_address.to_s
45
+ ENV['BEANSTALKD_URL'] = 'unreachable.invalid:99999'
46
+ bc = Tuber::Connection.new(nil)
47
+ assert_equal tuber_address.to_s, bc.address
48
+ ensure
49
+ ENV.delete('TUBER_URL')
50
+ ENV.delete('BEANSTALKD_URL')
51
+ end
52
+
53
+ it "should fall back to BEANSTALKD_URL from env" do
54
+ ENV['BEANSTALKD_URL'] = tuber_address.to_s
55
+ bc = Tuber::Connection.new(nil)
56
+ assert_equal tuber_address.to_s, bc.address
57
+ ensure
58
+ ENV.delete('BEANSTALKD_URL')
59
+ end
60
+ end # new
61
+
62
+ describe 'for timeout configuration' do
63
+ after do
64
+ Tuber.configure do |config|
65
+ config.connect_timeout = nil
66
+ config.resolv_timeout = nil
67
+ config.read_timeout = nil
68
+ config.write_timeout = nil
69
+ end
70
+ end
71
+
72
+ it "should pass connect_timeout and resolv_timeout to TCPSocket.new on Ruby >= 3.0" do
73
+ skip("connect_timeout/resolv_timeout kwargs require Ruby >= 3.0") if RUBY_VERSION < "3.0"
74
+
75
+ Tuber.configure do |config|
76
+ config.connect_timeout = 2
77
+ config.resolv_timeout = 2
78
+ end
79
+
80
+ real_socket = TCPSocket.new('localhost', 11300)
81
+ TCPSocket.expects(:new).with('localhost', 11300, connect_timeout: 2, resolv_timeout: 2).returns(real_socket)
82
+
83
+ bc = Tuber::Connection.new('localhost')
84
+ assert_kind_of TCPSocket, bc.connection
85
+ end
86
+
87
+ it "should ignore connect_timeout and resolv_timeout on Ruby < 3.0" do
88
+ skip("connect_timeout/resolv_timeout kwargs are supported on Ruby >= 3.0") if RUBY_VERSION >= "3.0"
89
+
90
+ Tuber.configure do |config|
91
+ config.connect_timeout = 2
92
+ config.resolv_timeout = 4
93
+ end
94
+
95
+ real_socket = TCPSocket.new('localhost', 11300)
96
+ TCPSocket.expects(:new).with('localhost', 11300).returns(real_socket)
97
+
98
+ bc = Tuber::Connection.new('localhost')
99
+ assert_same real_socket, bc.connection
100
+ end
101
+
102
+ it "should set SO_RCVTIMEO and SO_SNDTIMEO when read/write timeouts are configured" do
103
+ Tuber.configure do |config|
104
+ config.read_timeout = 3
105
+ config.write_timeout = 5
106
+ end
107
+
108
+ bc = Tuber::Connection.new('localhost')
109
+ rcv = bc.connection.getsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO).unpack('l_l_')[0]
110
+ snd = bc.connection.getsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO).unpack('l_l_')[0]
111
+ assert_equal 3, rcv
112
+ assert_equal 5, snd
113
+ end
114
+
115
+ end # timeout configuration
116
+
117
+ describe 'for #transmit' do
118
+ before do
119
+ @host = 'localhost'
120
+ @bc = Tuber::Connection.new(@host)
121
+ end
122
+
123
+ it "should return yaml loaded response" do
124
+ res = @bc.transmit 'stats'
125
+ refute_nil res[:body]['current-connections']
126
+ assert_equal 'OK', res[:status]
127
+ end
128
+
129
+ it "should return id" do
130
+ res = @bc.transmit "put 0 0 100 1\r\nX"
131
+ assert res[:id]
132
+ assert_equal 'INSERTED', res[:status]
133
+ end
134
+
135
+ it "should support dashes in response" do
136
+ res = @bc.transmit "use foo-bar\r\n"
137
+ assert_equal 'USING', res[:status]
138
+ assert_equal 'foo-bar', res[:id]
139
+ end
140
+
141
+ it "should pass crlf through without changing its length" do
142
+ res = @bc.transmit "put 0 0 100 2\r\n\r\n"
143
+ assert_equal 'INSERTED', res[:status]
144
+ end
145
+
146
+ it "should handle *any* byte value without changing length" do
147
+ res = @bc.transmit "put 0 0 100 256\r\n"+(0..255).to_a.pack("c*")
148
+ assert_equal 'INSERTED', res[:status]
149
+ end
150
+
151
+ it "should retry idempotent command with success after one connection failure" do
152
+ TCPSocket.any_instance.expects(:readline).times(2).
153
+ raises(EOFError.new).then.
154
+ returns("USING foo\n")
155
+
156
+ res = @bc.transmit "use foo\r\n"
157
+ assert_equal 'USING', res[:status]
158
+ end
159
+
160
+ it "should not retransmit delete after a connection failure" do
161
+ TCPSocket.any_instance.expects(:readline).times(1).raises(EOFError.new)
162
+
163
+ assert_raises(EOFError) { @bc.transmit "delete 56\r\n" }
164
+ end
165
+
166
+ it "should not retransmit put after a connection failure" do
167
+ TCPSocket.any_instance.expects(:readline).times(1).raises(Errno::ECONNRESET.new)
168
+
169
+ assert_raises(Errno::ECONNRESET) { @bc.transmit "put 0 0 100 4\r\ndata" }
170
+ end
171
+
172
+ it "heals the connection after a non-retransmitted failure" do
173
+ $eof_once = false
174
+ TCPSocket.prepend(Module.new do
175
+ def readline
176
+ if $eof_once == false
177
+ $eof_once = true
178
+ raise EOFError
179
+ end
180
+ super
181
+ end
182
+ end)
183
+
184
+ assert_raises(EOFError) { @bc.transmit "delete 56\r\n" }
185
+
186
+ # The reconnect still happened, so the next command works.
187
+ res = @bc.transmit "use foo"
188
+ assert_equal 'USING', res[:status]
189
+ ensure
190
+ $eof_once = true
191
+ end
192
+
193
+ it "should fail after exceeding retries with DrainingError" do
194
+ TCPSocket.any_instance.expects(:readline).times(3).
195
+ raises(Tuber::UnexpectedResponse.from_status("DRAINING", "delete 56"))
196
+
197
+ assert_raises(Tuber::DrainingError) { @bc.transmit "delete 56\r\n" }
198
+ end
199
+
200
+ it "should fail after exceeding reconnect max retries" do
201
+ # next connection attempts should fail
202
+ TCPSocket.stubs(:new).times(3).raises(Errno::ECONNREFUSED.new)
203
+ TCPSocket.any_instance.stubs(:readline).times(1).raises(EOFError.new)
204
+
205
+ assert_raises(Tuber::NotConnected) { @bc.transmit "delete 56\r\n" }
206
+ end
207
+
208
+ it "tubes_watched are restored after reconnect" do
209
+ client = Tuber.new('127.0.0.1:11300')
210
+ client.tubes.watch! "another"
211
+
212
+ $called = false
213
+ TCPSocket.prepend Module.new {
214
+ def readline
215
+ if !$called
216
+ $called = true
217
+ raise EOFError
218
+ end
219
+
220
+ super
221
+ end
222
+ }
223
+
224
+ assert_equal %w[another], client.tubes.watched.map(&:name)
225
+ ensure
226
+ # Leave the prepended module inert: with $called reset to nil it would
227
+ # fire one more EOFError at an arbitrary later readline, which a
228
+ # non-retransmitted command (delete/put) now surfaces as a failure.
229
+ $called = true
230
+ end
231
+ end # transmit
232
+
233
+ describe 'for idempotent insert response' do
234
+ before do
235
+ require_tuber!
236
+ @host = tuber_address
237
+ @bc = Tuber::Connection.new(@host)
238
+ @idp_key = "testkey_#{Time.now.to_f}"
239
+ end
240
+
241
+ it "should return state for dedup hit" do
242
+ @bc.transmit "use idp_test"
243
+ res1 = @bc.transmit "put 0 0 100 4 idp:#{@idp_key}\r\ndata"
244
+ assert_equal 'INSERTED', res1[:status]
245
+ assert_nil res1[:state]
246
+
247
+ res2 = @bc.transmit "put 0 0 100 4 idp:#{@idp_key}\r\ndata"
248
+ assert_equal 'INSERTED', res2[:status]
249
+ assert_equal res1[:id], res2[:id]
250
+ assert_equal 'READY', res2[:state]
251
+ end
252
+ end # idempotent insert response
253
+
254
+ describe 'for #close' do
255
+ before do
256
+ @host = 'localhost'
257
+ @bc = Tuber::Connection.new(@host)
258
+ end
259
+
260
+ it "should clear connection" do
261
+ assert_kind_of TCPSocket, @bc.connection
262
+ @bc.close
263
+ assert_nil @bc.connection
264
+ assert_raises(Tuber::NotConnected) { @bc.transmit 'stats' }
265
+ end
266
+ end # close
267
+ describe 'for drain and undrain commands' do
268
+ it "should return DRAINING status for drain command" do
269
+ @bc = Tuber::Connection.new('localhost')
270
+ TCPSocket.any_instance.stubs(:write)
271
+ TCPSocket.any_instance.expects(:readline).returns("DRAINING\r\n")
272
+ res = @bc.transmit("drain")
273
+ assert_equal 'DRAINING', res[:status]
274
+ end
275
+
276
+ it "should return NOT_DRAINING status for undrain command" do
277
+ @bc = Tuber::Connection.new('localhost')
278
+ TCPSocket.any_instance.stubs(:write)
279
+ TCPSocket.any_instance.expects(:readline).returns("NOT_DRAINING\r\n")
280
+ res = @bc.transmit("undrain")
281
+ assert_equal 'NOT_DRAINING', res[:status]
282
+ end
283
+
284
+ it "should still raise DrainingError for non-drain commands" do
285
+ @bc = Tuber::Connection.new('localhost')
286
+ TCPSocket.any_instance.stubs(:write)
287
+ TCPSocket.any_instance.expects(:readline).times(3).returns("DRAINING\r\n")
288
+ assert_raises(Tuber::DrainingError) { @bc.transmit("put 0 0 100 4\r\ntest") }
289
+ end
290
+ end # drain and undrain
291
+ end # Tuber::Connection
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # test/errors_test.rb
4
+
5
+ require File.expand_path('../test_helper', __FILE__)
6
+
7
+ describe "Tuber::Errors" do
8
+ it 'should raise proper exception for invalid status NOT_FOUND' do
9
+ @klazz = Tuber::UnexpectedResponse.from_status("NOT_FOUND", "job-stats -1")
10
+ assert_kind_of(Tuber::NotFoundError, @klazz)
11
+ assert_equal 'job-stats -1', @klazz.cmd
12
+ assert_equal 'NOT_FOUND', @klazz.status
13
+ end
14
+
15
+ it 'should raise proper exception for invalid status BAD_FORMAT' do
16
+ @klazz = Tuber::UnexpectedResponse.from_status("BAD_FORMAT", "FAKE")
17
+ assert_kind_of(Tuber::BadFormatError, @klazz)
18
+ assert_equal 'FAKE', @klazz.cmd
19
+ assert_equal 'BAD_FORMAT', @klazz.status
20
+ end
21
+
22
+ it 'should raise proper exception for invalid status DEADLINE_SOON' do
23
+ @klazz = Tuber::UnexpectedResponse.from_status("DEADLINE_SOON", "reserve 0")
24
+ assert_kind_of(Tuber::DeadlineSoonError, @klazz)
25
+ assert_equal 'reserve 0', @klazz.cmd
26
+ assert_equal 'DEADLINE_SOON', @klazz.status
27
+ end
28
+
29
+ it 'should raise proper exception for invalid status EXPECTED_CRLF' do
30
+ @klazz = Tuber::UnexpectedResponse.from_status("EXPECTED_CRLF", "reserve 0")
31
+ assert_kind_of(Tuber::ExpectedCrlfError, @klazz)
32
+ assert_equal 'reserve 0', @klazz.cmd
33
+ assert_equal 'EXPECTED_CRLF', @klazz.status
34
+ end
35
+ end
data/test/job_test.rb ADDED
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ # test/job_test.rb
4
+
5
+ require File.expand_path('../test_helper', __FILE__)
6
+
7
+ describe Tuber::Job do
8
+ before do
9
+ @beanstalk = Tuber.new(tuber_address)
10
+ @tube = @beanstalk.tubes.find 'tube'
11
+ end
12
+
13
+ describe "for #bury" do
14
+ before do
15
+ @tube.clear
16
+ @time = Time.now.to_i
17
+ @tube.put "foo bury #{@time}", :pri => 5
18
+ end
19
+
20
+ it("should be buried with same pri") do
21
+ job = @tube.reserve
22
+ assert_equal "foo bury #{@time}", job.body
23
+ assert_equal 'reserved', job.stats.state
24
+ job.bury
25
+ assert_equal 'buried', job.stats.state
26
+ assert_equal 5, job.stats.pri
27
+ assert_equal "foo bury #{@time}", @tube.peek(:buried).body
28
+ end
29
+
30
+ it("should be released with new pri") do
31
+ job = @tube.reserve
32
+ assert_equal "foo bury #{@time}", job.body
33
+ assert_equal 'reserved', job.stats.state
34
+ job.bury(:pri => 10)
35
+ assert_equal 'buried', job.stats.state
36
+ assert_equal 10, job.stats.pri
37
+ assert_equal "foo bury #{@time}", @tube.peek(:buried).body
38
+ end
39
+
40
+ it "should not bury if not reserved" do
41
+ job = @tube.peek(:ready)
42
+ assert_raises(Tuber::JobNotReserved) { job.bury }
43
+ end
44
+
45
+ it "should not bury if reserved and deleted" do
46
+ job = @tube.reserve
47
+ job.delete
48
+ assert_equal false, job.reserved
49
+ assert_raises(Tuber::NotFoundError) { job.bury }
50
+ end
51
+ end # bury
52
+
53
+ describe "for #release" do
54
+ before do
55
+ @time = Time.now.to_i
56
+ @tube.put "foo release #{@time}", :pri => 5
57
+ end
58
+
59
+ it("should be released with same pri") do
60
+ job = @tube.reserve
61
+ assert_equal "foo release #{@time}", job.body
62
+ assert_equal 'reserved', job.stats.state
63
+ job.release
64
+ assert_equal 'ready', job.stats.state
65
+ assert_equal 5, job.stats.pri
66
+ assert_equal 0, job.stats.delay
67
+ end
68
+
69
+ it("should be released with new pri") do
70
+ job = @tube.reserve
71
+ assert_equal "foo release #{@time}", job.body
72
+ assert_equal 'reserved', job.stats.state
73
+ job.release :pri => 10, :delay => 2
74
+ assert_equal 'delayed', job.stats.state
75
+ assert_equal 10, job.stats.pri
76
+ assert_equal 2, job.stats.delay
77
+ end
78
+
79
+ it "should not released if not reserved" do
80
+ job = @tube.peek(:ready)
81
+ assert_raises(Tuber::JobNotReserved) { job.release }
82
+ end
83
+
84
+ it "should not release if not reserved and buried" do
85
+ job = @tube.reserve
86
+ job.bury
87
+ assert_raises(Tuber::JobNotReserved) { job.release }
88
+ end
89
+ end # release
90
+
91
+ describe "for #delete" do
92
+ before do
93
+ @tube.put 'foo'
94
+ end
95
+
96
+ it("should deletable") do
97
+ job = @tube.peek(:ready)
98
+ assert_equal 'foo', job.body
99
+ job.delete
100
+ assert_nil @tube.peek(:ready)
101
+ end
102
+ end # delete
103
+
104
+ describe "for #touch" do
105
+ before do
106
+ @tube.put 'foo touch', :ttr => 1
107
+ end
108
+
109
+ it("should be toucheable") do
110
+ job = @tube.reserve
111
+ assert_equal 'foo touch', job.body
112
+ job.touch
113
+ assert_equal 1, job.stats.reserves
114
+ job.delete
115
+ end
116
+
117
+ it "should not touch if not reserved" do
118
+ job = @tube.peek(:ready)
119
+ assert_raises(Tuber::JobNotReserved) { job.touch }
120
+ end
121
+
122
+ it "should not touch if not reserved and released" do
123
+ job = @tube.reserve
124
+ job.release
125
+ assert_raises(Tuber::JobNotReserved) { job.touch }
126
+ end
127
+
128
+ it "should not touch if reserved and deleted" do
129
+ job = @tube.reserve
130
+ job.delete
131
+ assert_raises(Tuber::NotFoundError) { job.touch }
132
+ end
133
+ end # touch
134
+
135
+ describe "for #kick" do
136
+ before do
137
+ @tube.put 'foo touch', :ttr => 1
138
+ end
139
+
140
+ it("should be toucheable") do
141
+ job = @tube.reserve
142
+ assert_equal 'foo touch', job.body
143
+ job.bury
144
+ assert_equal 1, @tube.stats.current_jobs_buried
145
+ if @beanstalk.stats.version.to_f > 1.7
146
+ job.kick
147
+ assert_equal 0, @tube.stats.current_jobs_buried
148
+ assert_equal 1, @tube.stats.current_jobs_ready
149
+ end
150
+ end
151
+ end # kick
152
+
153
+ describe "for #stats" do
154
+ before do
155
+ @tube.put 'foo'
156
+ @job = @tube.peek(:ready)
157
+ end
158
+
159
+ it("should have stats") do
160
+ assert_equal 'tube', @job.stats['tube']
161
+ assert_equal 'ready', @job.stats.state
162
+ end
163
+
164
+ it "should return nil for deleted job with no stats" do
165
+ @job.delete
166
+ assert_raises(Tuber::NotFoundError) { @job.stats }
167
+ end
168
+ end # stats
169
+
170
+ describe "for #reserved?" do
171
+ before do
172
+ @tube.put 'foo'
173
+ @job = @tube.peek(:ready)
174
+ end
175
+
176
+ it("should have stats") do
177
+ assert_equal false, @job.reserved?
178
+ job = @tube.reserve
179
+ assert_equal job.id, @job.id
180
+ assert_equal true, @job.reserved?
181
+ @job.delete
182
+ assert_raises(Tuber::NotFoundError) { @job.reserved? }
183
+ end
184
+ end # reserved?
185
+
186
+ describe "for #exists?" do
187
+ before do
188
+ @tube.put 'foo'
189
+ @job = @tube.peek(:ready)
190
+ end
191
+
192
+ it("should exists") { assert @job.exists? }
193
+
194
+ it "should not exist" do
195
+ @job.delete
196
+ assert !@job.exists?
197
+ end
198
+ end # exists?
199
+
200
+ describe "for #tube" do
201
+ before do
202
+ @tube.put 'bar'
203
+ @job = @tube.peek(:ready)
204
+ end
205
+
206
+ it("should have stats") do
207
+ job = @tube.reserve
208
+ assert_equal @tube.name, job.tube
209
+ job.release
210
+ end
211
+ end # tube
212
+
213
+ describe "for #pri" do
214
+ before do
215
+ @tube.put 'bar', :pri => 1
216
+ @job = @tube.peek(:ready)
217
+ end
218
+
219
+ it("should return pri") do
220
+ job = @tube.reserve
221
+ assert_equal 1, job.pri
222
+ job.release
223
+ end
224
+ end # pri
225
+
226
+
227
+ describe "for #ttr" do
228
+ before do
229
+ @tube.put 'bar', :ttr => 5
230
+ @job = @tube.peek(:ready)
231
+ end
232
+
233
+ it("should return ttr") do
234
+ job = @tube.reserve
235
+ assert_equal 5, job.ttr
236
+ job.release
237
+ end
238
+ end # ttr
239
+
240
+ describe "for #delay" do
241
+ before do
242
+ @tube.put 'bar', :delay => 5
243
+ @job = @tube.peek(:delayed)
244
+ end
245
+
246
+ it("should return delay") do
247
+ assert_equal 5, @job.delay
248
+ end
249
+ end # delay
250
+ end # Tuber::Job