tuber 0.0.1 → 0.6.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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +68 -0
- data/.gitignore +17 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +134 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +27 -0
- data/README.md +430 -8
- data/Rakefile +35 -0
- data/examples/demo.rb +97 -0
- data/lib/tuber/configuration.rb +49 -0
- data/lib/tuber/connection.rb +500 -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 +257 -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 +125 -2
- data/test/connection_test.rb +627 -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 +342 -0
- data/tuber.gemspec +33 -0
- metadata +111 -6
data/README.md
CHANGED
|
@@ -1,14 +1,436 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tuber
|
|
2
2
|
|
|
3
|
-
Ruby client for [Tuber](https://github.com/tuberq/tuber),
|
|
3
|
+
The Ruby client for [Tuber](https://github.com/tuberq/tuber), the simple, fast job queue server. One binary, zero dependencies — with unique/idempotent jobs, concurrency keys, job group pipelines, weighted tubes and batch operations built in.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Tuber (the server and this gem) is wire-compatible with [beanstalkd](https://github.com/beanstalkd/beanstalkd): point this client at a stock beanstalkd and everything except the Tuber-only extensions works unchanged. The gem is a fork of [beaneater](https://github.com/beanstalkd/beaneater), so it is also a drop-in beaneater replacement — see [Migrating from beaneater](#migrating-from-beaneater).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Quick Start
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
```ruby
|
|
10
|
+
@tuber = Tuber.new('localhost:11300')
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
@tube = @tuber.tubes["my-tube"]
|
|
13
|
+
@tube.put '{"key": "foo"}', pri: 5
|
|
14
|
+
@tube.put '{"key": "bar"}', delay: 3
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
while @tube.peek(:ready)
|
|
17
|
+
job = @tube.reserve
|
|
18
|
+
puts job.body
|
|
19
|
+
job.delete
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@tuber.close
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
Install the [tuber server](https://github.com/tuberq/tuber) (a single binary; `brew install tuberq/tuber/tuber`, Docker image `ghcr.io/tuberq/tuber`, or a [release binary](https://github.com/tuberq/tuber/releases)) — or use an existing beanstalkd — then add the gem:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
# Gemfile
|
|
31
|
+
gem 'tuber'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Migrating from beaneater
|
|
35
|
+
|
|
36
|
+
Tuber forked from beaneater 1.1.4. The rename is the only breaking change — every
|
|
37
|
+
method, option and return value is unchanged. Two things to update:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require 'beaneater' # before
|
|
41
|
+
require 'tuber' # after
|
|
42
|
+
|
|
43
|
+
@beanstalk = Beaneater.new('localhost:11300') # before
|
|
44
|
+
@beanstalk = Tuber.new('localhost:11300') # after
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That includes the nested constants (`Beaneater::Job` → `Tuber::Job`), the error
|
|
48
|
+
classes (`Beaneater::NotConnected` → `Tuber::NotConnected`) and the configuration
|
|
49
|
+
block (`Beaneater.configure` → `Tuber.configure`). In most codebases a
|
|
50
|
+
case-sensitive `Beaneater` → `Tuber` and `beaneater` → `tuber` replacement is the
|
|
51
|
+
whole migration.
|
|
52
|
+
|
|
53
|
+
The `BEANSTALKD_URL` environment variable is still honoured (it's a beanstalkd
|
|
54
|
+
convention, not a beaneater one), though `TUBER_URL` now takes precedence.
|
|
55
|
+
Likewise `config.beanstalkd_url` remains as an alias for `config.tuber_url`.
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### Configuration
|
|
60
|
+
|
|
61
|
+
To setup advanced options for tuber, you can pass configuration options using:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Tuber.configure do |config|
|
|
65
|
+
# config.default_put_delay = 0
|
|
66
|
+
# config.default_put_pri = 65536
|
|
67
|
+
# config.default_put_ttr = 120
|
|
68
|
+
# config.job_parser = lambda { |body| body }
|
|
69
|
+
# config.job_serializer = lambda { |body| body }
|
|
70
|
+
# config.tuber_url = 'localhost:11300'
|
|
71
|
+
# config.connect_timeout = nil
|
|
72
|
+
# config.resolv_timeout = nil
|
|
73
|
+
# config.read_timeout = nil
|
|
74
|
+
# config.write_timeout = nil
|
|
75
|
+
# config.connect_retries = 0
|
|
76
|
+
# config.connect_retry_interval = 1
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The above options are all defaults, so only include a configuration block if you need to make changes.
|
|
81
|
+
|
|
82
|
+
`connect_timeout` and `resolv_timeout` are passed through to `TCPSocket.new` on Ruby 3.0 and newer (ignored on Ruby < 3.0 for compatibility). `read_timeout` and `write_timeout` apply socket read and write timeouts via `setsockopt`.
|
|
83
|
+
|
|
84
|
+
### Connection
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
@tuber = Tuber.new('10.0.1.5:11300')
|
|
88
|
+
|
|
89
|
+
# Or use ENV['TUBER_URL'] (or ENV['BEANSTALKD_URL'])
|
|
90
|
+
@tuber = Tuber.new
|
|
91
|
+
|
|
92
|
+
@tuber.close
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Surviving a server restart
|
|
96
|
+
|
|
97
|
+
An established connection heals itself: when a command notices the socket has
|
|
98
|
+
died, the connection reconnects, replays its tube state, and re-sends the
|
|
99
|
+
command (except for the non-idempotent verbs — `put`, `delete`, `release`,
|
|
100
|
+
`bury`, `touch`, `kick` — where a blind re-send could duplicate a job or act on
|
|
101
|
+
a reservation that died with the old socket, so the original error is raised
|
|
102
|
+
for the caller to decide).
|
|
103
|
+
|
|
104
|
+
That healing gets three connect attempts a second apart. A server restart
|
|
105
|
+
typically takes longer, so give the client a budget that outlasts it:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Tuber.configure do |config|
|
|
109
|
+
config.connect_retries = 15 # extra attempts, 0 = a single attempt
|
|
110
|
+
config.connect_retry_interval = 1 # seconds between them
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`connect_retries` covers both a cold start (`Tuber.new` while the server is
|
|
115
|
+
down) and reconnects on a client you already hold. It defaults to `0` — a
|
|
116
|
+
single attempt — which is the historical behaviour.
|
|
117
|
+
|
|
118
|
+
When a command does surface `Tuber::NotConnected`, heal the client you have
|
|
119
|
+
rather than building a new one:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
begin
|
|
123
|
+
job = @tuber.tubes.reserve
|
|
124
|
+
rescue Tuber::NotConnected
|
|
125
|
+
@tuber.reconnect! # watched tubes, their weights, the used tube and the
|
|
126
|
+
retry # reserve mode all come back with it
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A fresh `Tuber.new` starts with no tube state, so a worker that reconnects that
|
|
131
|
+
way has to re-watch and re-weight every tube by hand — and will silently fall
|
|
132
|
+
back to FIFO if it forgets the reserve mode. `reconnect!` replays all of it.
|
|
133
|
+
It takes the same budget as the config, overridable per call:
|
|
134
|
+
`@tuber.reconnect!(tries: 30, retry_interval: 2)`.
|
|
135
|
+
|
|
136
|
+
### Tubes
|
|
137
|
+
|
|
138
|
+
Tubes are named work queues. Jobs are `put` into the used tube and `reserve`d from watched tubes. Each tube has a _ready_, _delayed_, and _buried_ queue.
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
@tube = @tuber.tubes.find("some-tube")
|
|
142
|
+
|
|
143
|
+
# Watch tubes for reserving jobs
|
|
144
|
+
@tuber.tubes.watch!('some-tube') # watch only these tubes
|
|
145
|
+
@tuber.tubes.watch('another-tube') # append to watch list
|
|
146
|
+
@tuber.tubes.ignore('some-tube') # stop watching
|
|
147
|
+
|
|
148
|
+
# List tubes
|
|
149
|
+
@tuber.tubes.all # => [<Tube name='foo'>, <Tube name='bar'>]
|
|
150
|
+
@tuber.tubes.used # => <Tube name='bar'>
|
|
151
|
+
@tuber.tubes.watched # => [<Tube name='foo'>]
|
|
152
|
+
|
|
153
|
+
# Manage tubes
|
|
154
|
+
@tube.pause(3) # pause for 3 seconds
|
|
155
|
+
@tube.clear # delete all jobs
|
|
156
|
+
@tube.flush # delete all jobs, returns count
|
|
157
|
+
@tube.flush_buried # delete only buried jobs (Tuber only), returns count
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Each client manages two separate concerns: **use**/**using** controls where `put` places jobs, and **watch**/**watching** controls where `reserve` takes jobs from. These are fully orthogonal.
|
|
161
|
+
|
|
162
|
+
### Jobs
|
|
163
|
+
|
|
164
|
+
A job has a body (string) and metadata. The typical lifecycle:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
put reserve delete
|
|
168
|
+
-----> [READY] ---------> [RESERVED] --------> *poof*
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Jobs are in one of three states:
|
|
172
|
+
|
|
173
|
+
| State | Description |
|
|
174
|
+
| ------- | ----------- |
|
|
175
|
+
| ready | Waiting to be reserved and processed. |
|
|
176
|
+
| delayed | Waiting to become ready after a delay. |
|
|
177
|
+
| buried | Held aside after failure, waiting to be kicked. |
|
|
178
|
+
|
|
179
|
+
#### Inserting jobs
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
@tube.put "job-data-here"
|
|
183
|
+
@tube.put({foo: 'bar'}.to_json)
|
|
184
|
+
@tube.put "job-data-here", pri: 1000, delay: 50, ttr: 200
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- **pri** — integer < 2^32, lower values run first (default: 65536)
|
|
188
|
+
- **delay** — seconds to wait before the job becomes ready (default: 0)
|
|
189
|
+
- **ttr** — time to run, seconds a worker has to finish the job (default: 120)
|
|
190
|
+
|
|
191
|
+
#### Reserving and processing jobs
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
job = @tuber.tubes.reserve # blocks until a job is available
|
|
195
|
+
job = @tuber.tubes.reserve(5) # wait up to 5 seconds
|
|
196
|
+
|
|
197
|
+
puts job.body
|
|
198
|
+
puts job.tube
|
|
199
|
+
puts job.stats.state # => 'reserved'
|
|
200
|
+
|
|
201
|
+
job.touch # extend ttr
|
|
202
|
+
job.delete # success
|
|
203
|
+
job.release delay: 5 # retry later
|
|
204
|
+
job.bury # set aside for inspection
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Peeking and kicking
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
@tuber.jobs.find(123) # peek at a specific job
|
|
211
|
+
@tube.peek(:ready) # peek at next ready job
|
|
212
|
+
@tube.peek(:buried)
|
|
213
|
+
@tube.peek(:delayed)
|
|
214
|
+
|
|
215
|
+
@tuber.tubes['some-tube'].kick(3) # kick 3 buried jobs back to ready
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
#### Automatic processing
|
|
219
|
+
|
|
220
|
+
Register handlers for tubes and let `process!` loop over incoming jobs:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
@tuber.jobs.register('some-tube', retry_on: [SomeError]) do |job|
|
|
224
|
+
do_something(job)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
@tuber.jobs.register('other-tube') do |job|
|
|
228
|
+
do_something_else(job)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
@tuber.jobs.process!
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The loop reserves a job, calls the matching handler, then: deletes on success, releases on `retry_on` errors, and buries on other exceptions. Raise `AbortProcessingError` to stop the loop.
|
|
235
|
+
|
|
236
|
+
### Job Dependencies (Tuber only)
|
|
237
|
+
|
|
238
|
+
When using [Tuber](https://github.com/tuberq/tuber), you can group related jobs and chain dependent work using `group:` and `after:` options on `put`. After-jobs are held until every job in the group they depend on has been deleted:
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# Fan-out: enqueue grouped work
|
|
242
|
+
@tube.put "import-row-1", group: "import"
|
|
243
|
+
@tube.put "import-row-2", group: "import"
|
|
244
|
+
|
|
245
|
+
# Fan-in: this job waits until all "import" jobs are deleted
|
|
246
|
+
@tube.put "send-summary", after: "import"
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Chain stages together by combining `after:` and `group:` on the same job to build a simple DAG pipeline:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
@tube.put "row-1", group: "extract"
|
|
253
|
+
@tube.put "row-2", group: "extract"
|
|
254
|
+
@tube.put "transform", after: "extract", group: "transform"
|
|
255
|
+
@tube.put "load", after: "transform"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Here `transform` waits for the extract group to finish, then becomes part of the `transform` group. `load` waits for `transform` to complete.
|
|
259
|
+
|
|
260
|
+
Buried jobs block group completion — kick them to let the group finish. Group names are global and can span multiple tubes.
|
|
261
|
+
|
|
262
|
+
### Unique Jobs / Idempotency (Tuber only)
|
|
263
|
+
|
|
264
|
+
Prevent duplicate jobs with the `idempotency:` option. If a job with the same key already exists in the tube, the original job is returned instead of creating a duplicate:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
@tube.put "send-report", idempotency: "daily-report"
|
|
268
|
+
# => <Tuber::Job id=1 body="send-report">
|
|
269
|
+
|
|
270
|
+
@tube.put "send-report", idempotency: "daily-report"
|
|
271
|
+
# => <Tuber::Job id=1 body="send-report"> (same job, no duplicate created)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The key is scoped to the tube and cleared when the job is deleted, so the same key can be reused afterwards.
|
|
275
|
+
|
|
276
|
+
Add a cooldown TTL to keep deduplicating for N seconds after deletion — useful for preventing rapid resubmission:
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
@tube.put "send-report", idempotency: "daily-report", idempotency_ttl: 300
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Concurrency Keys (Tuber only)
|
|
283
|
+
|
|
284
|
+
Limit parallel processing of related jobs. When a job with a concurrency key is reserved, other ready jobs sharing the same key are hidden from `reserve` until the reservation ends:
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# Only one job per user can be processed at a time
|
|
288
|
+
@tube.put "process-user-42", concurrency: "user-42"
|
|
289
|
+
@tube.put "process-user-42-again", concurrency: "user-42"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The second job won't be reserved until the first is deleted, released, or buried. Set a higher limit to allow N concurrent reservations:
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
# Allow up to 3 concurrent API jobs
|
|
296
|
+
@tube.put "api-call-1", concurrency: "api", concurrency_limit: 3
|
|
297
|
+
@tube.put "api-call-2", concurrency: "api", concurrency_limit: 3
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Weighted Tubes (Tuber only)
|
|
301
|
+
|
|
302
|
+
By default, `reserve` picks the highest-priority job across all watched tubes. Switch to weighted mode to select tubes randomly in proportion to their weight:
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
@tuber.tubes.watch('email')
|
|
306
|
+
@tuber.tubes.watch('notifications', weight: 2)
|
|
307
|
+
@tuber.tubes.watch('batch-jobs', weight: 6)
|
|
308
|
+
|
|
309
|
+
@tuber.tubes.reserve_mode(:weighted)
|
|
310
|
+
|
|
311
|
+
job = @tuber.tubes.reserve # batch-jobs selected 6x as often as email
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Tubes default to weight 1. Switch back with `reserve_mode(:fifo)`.
|
|
315
|
+
|
|
316
|
+
Weights and reserve mode are per-connection server state. The client remembers
|
|
317
|
+
both and replays them if the connection has to be re-established, so a worker
|
|
318
|
+
that heals across a server blip keeps reserving by weight.
|
|
319
|
+
|
|
320
|
+
### Batch Reserve (Tuber only)
|
|
321
|
+
|
|
322
|
+
Reserve multiple jobs atomically in a single call:
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
jobs = @tuber.tubes.reserve_batch(10) # up to 10 jobs
|
|
326
|
+
|
|
327
|
+
jobs.each do |job|
|
|
328
|
+
process(job)
|
|
329
|
+
job.delete
|
|
330
|
+
end
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
By default `reserve_batch` is non-blocking — it returns whatever is ready
|
|
334
|
+
immediately, possibly an empty array. Pass a timeout (in seconds) to long-poll
|
|
335
|
+
instead: the call blocks until the first job arrives, then drains everything
|
|
336
|
+
ready up to `count`, or returns an empty array when the timeout elapses. This
|
|
337
|
+
avoids hot-looping a worker on empty polls.
|
|
338
|
+
|
|
339
|
+
```ruby
|
|
340
|
+
jobs = @tuber.tubes.reserve_batch(10, 30) # block up to 30s for the first job
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
While blocked, a positive-timeout batch reserve may raise
|
|
344
|
+
`Tuber::DeadlineSoonError` if one of the connection's already-reserved jobs
|
|
345
|
+
is about to hit its TTR — service that job, then reserve again.
|
|
346
|
+
|
|
347
|
+
### Batch Touch (Tuber 0.12.0+)
|
|
348
|
+
|
|
349
|
+
A batch reserve starts the TTR clock on every job at the same instant, but a
|
|
350
|
+
worker processes them serially — so the tail of a large batch can expire and
|
|
351
|
+
return to the queue while the worker is still busy. `touch_all` extends the TTR
|
|
352
|
+
of every job the connection currently holds in a single command:
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
jobs = @tuber.tubes.reserve_batch(10)
|
|
356
|
+
|
|
357
|
+
jobs.each do |job|
|
|
358
|
+
process(job)
|
|
359
|
+
job.delete
|
|
360
|
+
@tuber.jobs.touch_all # heartbeat whatever is still held
|
|
361
|
+
end
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
No ids are sent: the server tracks the reserved set per connection, so jobs
|
|
365
|
+
already deleted, released, buried or lost to a TTR timeout are simply absent.
|
|
366
|
+
Each job keeps its own TTR — deadlines are extended individually, not levelled
|
|
367
|
+
onto a common value.
|
|
368
|
+
|
|
369
|
+
The return value is how many jobs the connection *actually* still holds. A count
|
|
370
|
+
lower than expected means jobs hit their TTR and went back to the queue while the
|
|
371
|
+
worker was busy — otherwise invisible, since nothing notifies a worker that it
|
|
372
|
+
lost a job.
|
|
373
|
+
|
|
374
|
+
### Stats
|
|
375
|
+
|
|
376
|
+
```ruby
|
|
377
|
+
@tuber.stats # server-wide stats
|
|
378
|
+
@tuber.tubes['some-tube'].stats # tube stats
|
|
379
|
+
@tuber.jobs[some_job_id].stats # job stats
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Configuration
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
Tuber.configure do |config|
|
|
386
|
+
config.default_put_delay = 0
|
|
387
|
+
config.default_put_pri = 65536
|
|
388
|
+
config.default_put_ttr = 120
|
|
389
|
+
config.job_parser = lambda { |body| body }
|
|
390
|
+
config.job_serializer = lambda { |body| body }
|
|
391
|
+
config.tuber_url = 'localhost:11300'
|
|
392
|
+
|
|
393
|
+
# Connect attempts; see "Surviving a server restart" above.
|
|
394
|
+
config.connect_retries = 0
|
|
395
|
+
config.connect_retry_interval = 1
|
|
396
|
+
end
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
The `job_serializer` is applied to every `put` body — useful for automatic JSON encoding:
|
|
400
|
+
|
|
401
|
+
```ruby
|
|
402
|
+
Tuber.configure do |config|
|
|
403
|
+
config.job_serializer = lambda { |body| JSON.dump(body) }
|
|
404
|
+
end
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Error Handling
|
|
408
|
+
|
|
409
|
+
| Error | Description |
|
|
410
|
+
| ----------------------------- | ----------- |
|
|
411
|
+
| Tuber::NotConnected | Cannot connect to the server. |
|
|
412
|
+
| Tuber::InvalidTubeName | Tube name is not valid. |
|
|
413
|
+
| Tuber::NotFoundError | Job or tube not found. |
|
|
414
|
+
| Tuber::TimedOutError | Reserve timed out. |
|
|
415
|
+
| Tuber::JobNotReserved | Action requires a reserved job. |
|
|
416
|
+
|
|
417
|
+
See the [Tuber protocol](https://github.com/tuberq/tuber/blob/main/docs/protocol.md) (a superset of the [beanstalk protocol](https://github.com/beanstalkd/beanstalkd/blob/master/doc/protocol.txt)) for additional error types.
|
|
418
|
+
|
|
419
|
+
## Resources
|
|
420
|
+
|
|
421
|
+
* [Tuber](https://github.com/tuberq/tuber)
|
|
422
|
+
* [Tuber protocol](https://github.com/tuberq/tuber/blob/main/docs/protocol.md)
|
|
423
|
+
* [Tuber on RubyGems](https://rubygems.org/gems/tuber)
|
|
424
|
+
* [Beanstalkd](https://github.com/beanstalkd/beanstalkd) and the [beanstalk protocol](https://github.com/beanstalkd/beanstalkd/blob/master/doc/protocol.txt)
|
|
425
|
+
* [Backburner](https://github.com/nesquena/backburner) — Ruby job queue for Rails/Sinatra
|
|
426
|
+
|
|
427
|
+
## Contributors
|
|
428
|
+
|
|
429
|
+
Tuber is maintained by [Dan Milne](https://github.com/dkam), and builds on the work of
|
|
430
|
+
everyone who wrote beaneater, from which it is forked:
|
|
431
|
+
|
|
432
|
+
- [Nico Taing](https://github.com/Nico-Taing) - Creator and co-maintainer of beaneater
|
|
433
|
+
- [Nathan Esquenazi](https://github.com/nesquena) - Contributor and co-maintainer
|
|
434
|
+
- [Keith Rarick](https://github.com/kr) - Much code inspired and adapted from beanstalk-client
|
|
435
|
+
- [Vidar Hokstad](https://github.com/vidarh) - Replaced telnet with correct TCP socket handling
|
|
436
|
+
- [Andreas Loupasakis](https://github.com/alup) - Improve test coverage, improve job configuration
|
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/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,49 @@
|
|
|
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
|
+
# How hard a client tries to connect — "how long should this ride out a
|
|
19
|
+
# server restart", in one knob.
|
|
20
|
+
#
|
|
21
|
+
# An established connection has always retried when it notices a dropped
|
|
22
|
+
# socket (Tuber::Connection::MAX_RETRIES attempts), but the *initial*
|
|
23
|
+
# connect got exactly one: a cold start during an outage — a web boot, a
|
|
24
|
+
# fresh Tuber.new, a daemon rebuilding its client after an error — failed
|
|
25
|
+
# where a held connection would have healed.
|
|
26
|
+
#
|
|
27
|
+
# Defaults to that single attempt, so nothing changes until you raise it.
|
|
28
|
+
# Raising it covers the initial connect and lifts the reconnect budget of
|
|
29
|
+
# connections already established along with it.
|
|
30
|
+
attr_accessor :connect_retries # extra connect attempts, 0 = one shot
|
|
31
|
+
attr_accessor :connect_retry_interval # seconds between connect attempts
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@default_put_delay = 0
|
|
35
|
+
@default_put_pri = 65536
|
|
36
|
+
@default_put_ttr = 120
|
|
37
|
+
@job_parser = lambda { |body| body }
|
|
38
|
+
@job_serializer = lambda { |body| body }
|
|
39
|
+
@tuber_url = nil
|
|
40
|
+
@connect_timeout = nil
|
|
41
|
+
@resolv_timeout = nil
|
|
42
|
+
@read_timeout = nil
|
|
43
|
+
@write_timeout = nil
|
|
44
|
+
@connect_retries = 0
|
|
45
|
+
# Matches Tuber::Connection::DEFAULT_RETRY_INTERVAL, which is not loaded yet.
|
|
46
|
+
@connect_retry_interval = 1
|
|
47
|
+
end
|
|
48
|
+
end # Configuration
|
|
49
|
+
end # Tuber
|