jobba 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +377 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/jobba.gemspec +30 -0
- data/lib/jobba/clause.rb +44 -0
- data/lib/jobba/clause_factory.rb +82 -0
- data/lib/jobba/common.rb +17 -0
- data/lib/jobba/configuration.rb +10 -0
- data/lib/jobba/exceptions.rb +5 -0
- data/lib/jobba/query.rb +84 -0
- data/lib/jobba/state.rb +59 -0
- data/lib/jobba/status.rb +304 -0
- data/lib/jobba/statuses.rb +74 -0
- data/lib/jobba/time.rb +21 -0
- data/lib/jobba/utils.rb +25 -0
- data/lib/jobba/version.rb +3 -0
- data/lib/jobba.rb +44 -0
- metadata +167 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5188a37939c0f9b306f0d98e01dfb9abf6ef3a31
|
4
|
+
data.tar.gz: 9cc2a58ff46d55c13aa25bec9e98b395916fd13b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8f6d7780ac4361ba25a5f73f4a1df1746c6954e947497fcb6e1f97d6b9d5c360d891835fd12cb2f09f52c557d9abc304a881b5fd3e40131c3ad7521a812d1a6a
|
7
|
+
data.tar.gz: e7fec3fbfd1e73496816907efbc28723daa1d148180b1cda7e71ec47ec176a5cbf4280be40491b751ed59a86a7fe35d7736a146b902dfe3a9a85851c90fcf8b9
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
jobba
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.3
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Rice University
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,377 @@
|
|
1
|
+
# jobba
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/openstax/jobba.svg?branch=master)](https://travis-ci.org/openstax/jobba)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/openstax/jobba/badges/gpa.svg)](https://codeclimate.com/github/openstax/jobba)
|
5
|
+
|
6
|
+
Redis-based background job status tracking.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
# Gemfile
|
12
|
+
gem 'jobba'
|
13
|
+
```
|
14
|
+
|
15
|
+
or
|
16
|
+
|
17
|
+
```
|
18
|
+
$> gem install jobba
|
19
|
+
```
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
|
23
|
+
To configure Jobba, put the following code in your applications
|
24
|
+
initialization logic (eg. in the config/initializers in a Rails app):
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
Jobba.configure do |config|
|
28
|
+
# Whatever options should be passed to `Redis.new` (see https://github.com/redis/redis-rb)
|
29
|
+
config.redis_options = { url: "redis://:p4ssw0rd@10.0.1.1:6380/15" }
|
30
|
+
# top-level redis prefix
|
31
|
+
config.namespace = "jobba"
|
32
|
+
end
|
33
|
+
```
|
34
|
+
|
35
|
+
## Getting status objects
|
36
|
+
|
37
|
+
If you know you need a new `Status`, call `create!`:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
Jobba::Status.create!
|
41
|
+
```
|
42
|
+
|
43
|
+
If you are looking for a status:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
Jobba::Status.find(id)
|
47
|
+
```
|
48
|
+
|
49
|
+
which will return `nil` if no such `Status` is found. If you always want a `Status` object back,
|
50
|
+
call:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
Jobba::Status.find!(id)
|
54
|
+
```
|
55
|
+
|
56
|
+
The results of `find!` will always start in an `unknown` state.
|
57
|
+
|
58
|
+
## Basic Use with ActiveJob
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
class MyJob < ::ActiveJob::Base
|
62
|
+
def self.perform_later(an_arg:, another_arg:)
|
63
|
+
status = Jobba::Status.create!
|
64
|
+
args.push(status.id)
|
65
|
+
|
66
|
+
# In theory we'd mark as queued right after the call to super, but this messes
|
67
|
+
# up when the activejob adapter runs the job right away
|
68
|
+
status.queued!
|
69
|
+
super(*args, &block)
|
70
|
+
|
71
|
+
# return the Status ID in case it needs to be noted elsewhere
|
72
|
+
status.id
|
73
|
+
end
|
74
|
+
|
75
|
+
def perform(*args, &block)
|
76
|
+
# Pop the ID argument added by perform_later and get a Status
|
77
|
+
status = Jobba::Status.find!(args.pop)
|
78
|
+
status.working!
|
79
|
+
|
80
|
+
# ... do stuff ...
|
81
|
+
|
82
|
+
status.succeeded!
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
## Change States
|
88
|
+
|
89
|
+
One of the main functions of Jobba is to let a job advance its status through a series of states:
|
90
|
+
|
91
|
+
* `unqueued`
|
92
|
+
* `queued`
|
93
|
+
* `working`
|
94
|
+
* `succeeded`
|
95
|
+
* `failed`
|
96
|
+
* `killed`
|
97
|
+
* `unknown`
|
98
|
+
|
99
|
+
Put a `Status` into one of these states by calling `that_state!`, e.g.
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
my_state.working!
|
103
|
+
```
|
104
|
+
|
105
|
+
The `unqueued` state is entered when a `Status` is first created. The `unknown` state is entered when `find!(id)` is called but the `id` is not known. You can re-enter these states with the `!` methods, but note that the `recorded_at` timestamp will not be updated.
|
106
|
+
|
107
|
+
The **first time a state is entered**, a timestamp is recorded for that state. Not all timestamp names match the state names:
|
108
|
+
|
109
|
+
| State | Timestamp |
|
110
|
+
|-------|-----------|
|
111
|
+
|unqueued | recorded_at |
|
112
|
+
|queued | queued_at |
|
113
|
+
|working | started_at |
|
114
|
+
|succeeded | succeeded_at |
|
115
|
+
|failed | failed_at |
|
116
|
+
|killed | killed_at |
|
117
|
+
|unknown | recorded_at |
|
118
|
+
|
119
|
+
There is also a special timestamp for when a kill is requested, `kill_requested_at`. More about this later.
|
120
|
+
|
121
|
+
The order of states is not enforced, and you do not have to use all states. However, note that you'll only be able to query for states you use (Jobba doesn't automatically travel through states you skip) and if you're using an unusual order your time-based queries will have to reflect that order.
|
122
|
+
|
123
|
+
## Mark Progress
|
124
|
+
|
125
|
+
If you want to have a way to track the progress of a job, you can call:
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
my_status.set_progress(0.7) # 70% complete
|
129
|
+
my_status.set_progress(7,10) # 70% complete
|
130
|
+
my_status.set_progress(14,20) # 70% complete
|
131
|
+
```
|
132
|
+
|
133
|
+
This is useful if you need to show a progress bar on your client, for example.
|
134
|
+
|
135
|
+
## Recording Job Errors
|
136
|
+
|
137
|
+
...
|
138
|
+
|
139
|
+
## Saving Job-specific Data
|
140
|
+
|
141
|
+
Jobba provides a `data` field in all `Status` objects that you can use for storing job-specific data. Note that the data must be in a format that can be serialized to JSON. Recommend sticking with basic data types, arrays, primitives, hashes, etc.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
my_status.save({a: 'blah', b: [1,2,3]})
|
145
|
+
my_status.save("some string")
|
146
|
+
```
|
147
|
+
|
148
|
+
## Setting Job Name and Arguments
|
149
|
+
|
150
|
+
If you want to be able to query for all statuses for a certain kind of job, you can set the job's name in the status:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
my_status.set_job_name("MySpecialJob")
|
154
|
+
```
|
155
|
+
|
156
|
+
If you want to be able to query for all statuses that take a certain argument as input, you can add job arguments to a status:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
my_status.add_job_arg(arg_name, arg)
|
160
|
+
```
|
161
|
+
|
162
|
+
where `arg_name` is what the argument is called in your job (e.g. `"input_1"`) and `arg` is a way to identify the argument (e.g. `"gid://app/Person/72").
|
163
|
+
|
164
|
+
You probably will only want to track complex arguments, e.g. models in your application. E.g. you could have a `Book` model and a `PublishBook` background job and you may want to see all of the `PublishBook` jobs that have status for the `Book` with ID `53`.
|
165
|
+
|
166
|
+
## Killing Jobs
|
167
|
+
|
168
|
+
While Jobba can't really kill jobs (it doesn't control your job-running library), it has a facility for marking that you'd like a job to be killed.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
a_status.request_kill!
|
172
|
+
```
|
173
|
+
|
174
|
+
Then a job itself can occassionally come up for air and check
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
my_status.kill_requested?
|
178
|
+
```
|
179
|
+
|
180
|
+
and if that returns `true`, it can attempt to gracefully terminate itself.
|
181
|
+
|
182
|
+
Note that when a kill is requested, the job will continue to be in some other state (e.g. `working`) until it is in fact killed, at which point the job should call:
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
my_status.killed!
|
186
|
+
```
|
187
|
+
|
188
|
+
to change the state to `killed`.
|
189
|
+
|
190
|
+
## Status Objects
|
191
|
+
|
192
|
+
When you get hold of a `Status`, via `create!`, `find`, `find!`, or as the result of a query, it will have the following attributes (some of which may be nil):
|
193
|
+
|
194
|
+
| Attribute | Description |
|
195
|
+
|-----------|-------------|
|
196
|
+
| `id` | A Jobba-created UUID |
|
197
|
+
| `state` | one of the states above |
|
198
|
+
| `progress` | a float between 0.0 and 1.0 |
|
199
|
+
| `errors` | TBD |
|
200
|
+
| `data` | job-specific data |
|
201
|
+
| `job_name` | The name of the job |
|
202
|
+
| `job_args` | An hash of job arguments, {arg_name: arg, ...} |
|
203
|
+
| `recorded_at` | Ruby `Time` timestamp |
|
204
|
+
| `queued_at` | Ruby `Time` timestamp |
|
205
|
+
| `started_at` | Ruby `Time` timestamp |
|
206
|
+
| `succeeded_at` | Ruby `Time` timestamp |
|
207
|
+
| `failed_at` | Ruby `Time` timestamp |
|
208
|
+
| `killed_at` | Ruby `Time` timestamp |
|
209
|
+
| `recorded_at` | Ruby `Time` timestamp |
|
210
|
+
| `kill_requested_at` | Ruby `Time` timestamp |
|
211
|
+
|
212
|
+
A `Status` object also methods to check if it is in certain states:
|
213
|
+
|
214
|
+
* `reload!`
|
215
|
+
* `unqueued?`
|
216
|
+
* `queued?`
|
217
|
+
* `working?`
|
218
|
+
* `succeeded?`
|
219
|
+
* `failed?`
|
220
|
+
* `killed?`
|
221
|
+
* `unknown?`
|
222
|
+
|
223
|
+
And two conveience methods for checking groups of states:
|
224
|
+
|
225
|
+
* `completed?`
|
226
|
+
* `incomplete?`
|
227
|
+
|
228
|
+
You can also call `reload!` on a `Status` to have it reset its state to what is stored in redis.
|
229
|
+
|
230
|
+
## Deleting Job Statuses
|
231
|
+
|
232
|
+
Once jobs are completed or otherwise no longer interesting, it'd be nice to clear them out of redis. You can do this with:
|
233
|
+
|
234
|
+
```ruby
|
235
|
+
my_status.delete # freaks out if `my_status` isn't complete
|
236
|
+
my_status.delete! # always deletes
|
237
|
+
```
|
238
|
+
|
239
|
+
## Querying for Statuses
|
240
|
+
|
241
|
+
Jobba has an activerecord-like query interface for finding Status objects.
|
242
|
+
|
243
|
+
### Basic Query Examples
|
244
|
+
|
245
|
+
**State**
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
Jobba.where(state: :unqueued)
|
249
|
+
Jobba.where(state: :queued)
|
250
|
+
Jobba.where(state: :working)
|
251
|
+
Jobba.where(state: :succeeded)
|
252
|
+
Jobba.where(state: :failed)
|
253
|
+
Jobba.where(state: :killed)
|
254
|
+
Jobba.where(state: :unknown)
|
255
|
+
```
|
256
|
+
|
257
|
+
Two convenience "state" queries have been added:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
Jobba.where(state: :completed) # includes succeeded, failed
|
261
|
+
Jobba.where(state: :incomplete) # includes unqueued, queued, working, killed
|
262
|
+
```
|
263
|
+
|
264
|
+
You can query combinations of states too:
|
265
|
+
|
266
|
+
```ruby
|
267
|
+
Jobba.where(state: [:queued, :working])
|
268
|
+
```
|
269
|
+
|
270
|
+
**State Timestamp**
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
Jobba.where(recorded_at: {after: time_1})
|
274
|
+
Jobba.where(queued_at: [time_1, nil])
|
275
|
+
Jobba.where(started_at: {before: time_2})
|
276
|
+
Jobba.where(started_at: [nil, time_2])
|
277
|
+
Jobba.where(succeeded_at: {after: time_1, before: time_2})
|
278
|
+
Jobba.where(failed_at: [time_1, time_2])
|
279
|
+
```
|
280
|
+
|
281
|
+
Note that you cannot query on `kill_requested_at`. The time arguments can be Ruby `Time` objects or a number of microseconds since the epoch represented as a float, integer, or string.
|
282
|
+
|
283
|
+
Note that, in operations having to do with time, this gem ignores anything beyond microseconds.
|
284
|
+
|
285
|
+
**Job Name**
|
286
|
+
|
287
|
+
(requires having called the optional `set_job_name` method)
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
Jobba.where(job_name: "MySpecialBackgroundJob")
|
291
|
+
Jobba.where(job_name: ["MySpecialBackgroundJob", "MyOtherJob"])
|
292
|
+
```
|
293
|
+
|
294
|
+
**Job Arguments**
|
295
|
+
|
296
|
+
(requires having called the optional `add_job_arg` method)
|
297
|
+
|
298
|
+
```ruby
|
299
|
+
Jobba.where(job_arg: "gid://app/MyModel/42")
|
300
|
+
Jobba.where(job_arg: "gid://app/Person/86")
|
301
|
+
```
|
302
|
+
|
303
|
+
### Query Chaining
|
304
|
+
|
305
|
+
Queries can be chained! (intersects the results of each `where` clause)
|
306
|
+
|
307
|
+
```ruby
|
308
|
+
Jobba.where(state: :queued).where(recorded_at: {after: some_time})
|
309
|
+
Jobba.where(job_name: "MyTroublesomeJob").where(state: :failed)
|
310
|
+
```
|
311
|
+
|
312
|
+
### Operations on Queries
|
313
|
+
|
314
|
+
When you have a query you can run the following methods on it. These act like what you'd expect for a Ruby array.
|
315
|
+
|
316
|
+
* `first`
|
317
|
+
* `any?`
|
318
|
+
* `none?`
|
319
|
+
* `all?`
|
320
|
+
* `each`
|
321
|
+
* `each_with_index`
|
322
|
+
* `map`
|
323
|
+
* `collect`
|
324
|
+
* `select`
|
325
|
+
* `empty?`
|
326
|
+
* `count`
|
327
|
+
|
328
|
+
`empty?` and `count` are performed in redis without bringing back all query results to Ruby.
|
329
|
+
|
330
|
+
You can also call two special methods directly on `Jobba`:
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
Jobba.all # returns all statuses
|
334
|
+
Jobba.count # returns count of all statuses
|
335
|
+
```
|
336
|
+
|
337
|
+
## Statuses Object
|
338
|
+
|
339
|
+
Calling `all` on a query returns a `Statuses` object, which is just a collection of `Status` objects. It has a few methods for doing bulk operations on all `Status` objects in it.
|
340
|
+
|
341
|
+
* `delete`
|
342
|
+
* `delete!`
|
343
|
+
* `request_kill!`
|
344
|
+
|
345
|
+
These work like describe above for individual `Status` objects.
|
346
|
+
|
347
|
+
There is also a not-very-tested `multi` operation that takes a block and executes the block inside a Redis `multi` call.
|
348
|
+
|
349
|
+
```ruby
|
350
|
+
my_statuses.multi do |status, redis|
|
351
|
+
# do stuff on `status` using the `redis` connection
|
352
|
+
end
|
353
|
+
```
|
354
|
+
|
355
|
+
## Notes
|
356
|
+
|
357
|
+
### Times
|
358
|
+
|
359
|
+
Note that, in operations having to do with time, this gem ignores anything beyond microseconds.
|
360
|
+
|
361
|
+
### Efficiency
|
362
|
+
|
363
|
+
Jobba strives to do all of its operations as efficiently as possible using built-in redis operations. If you find a place where the efficiency can be improved, please submit an issue or a pull request.
|
364
|
+
|
365
|
+
## TODO
|
366
|
+
|
367
|
+
1. Provide job min, max, and average durations.
|
368
|
+
2. Implement `add_error`.
|
369
|
+
8. Specs that test scale.
|
370
|
+
9. Make sure we're calling `multi` or `pipelined` everywhere we can.
|
371
|
+
11. Make sure we're consistent on completed/complete incompleted/incomplete.
|
372
|
+
12. Should more `Statuses` operations return `Statuses`, e.g. `each`, so that they can be chained?
|
373
|
+
|
374
|
+
|
375
|
+
|
376
|
+
|
377
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "jobba"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/jobba.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'jobba/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "jobba"
|
8
|
+
spec.version = Jobba::VERSION
|
9
|
+
spec.authors = ["JP Slavinsky"]
|
10
|
+
spec.email = ["jps@kindlinglabs.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Redis-based background job status tracking.}
|
13
|
+
spec.description = %q{Redis-based background job status tracking.}
|
14
|
+
spec.homepage = "https://github.com/openstax/jobba"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_runtime_dependency "redis", "~> 3.2"
|
23
|
+
spec.add_runtime_dependency "redis-namespace"
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec"
|
28
|
+
spec.add_development_dependency "byebug"
|
29
|
+
spec.add_development_dependency "fakeredis"
|
30
|
+
end
|
data/lib/jobba/clause.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
class Jobba::Clause
|
2
|
+
attr_reader :keys, :min, :max
|
3
|
+
|
4
|
+
include Jobba::Common
|
5
|
+
|
6
|
+
# if `keys` or `suffixes` is an array, all entries will be included in the resulting set
|
7
|
+
def initialize(prefix: nil, suffixes: nil, keys: nil, min: nil, max: nil)
|
8
|
+
if keys.nil? && prefix.nil? && suffixes.nil?
|
9
|
+
raise ArgumentError, "Either `keys` or both `prefix` and `suffix` must be specified."
|
10
|
+
end
|
11
|
+
|
12
|
+
if (prefix.nil? && !suffixes.nil?) || (!prefix.nil? && suffixes.nil?)
|
13
|
+
raise ArgumentError, "When `prefix` is given, so must `suffix` be, and vice versa."
|
14
|
+
end
|
15
|
+
|
16
|
+
if keys
|
17
|
+
@keys = [keys].flatten
|
18
|
+
else
|
19
|
+
prefix = "#{prefix}:" unless prefix[-1] == ":"
|
20
|
+
@keys = [suffixes].flatten.collect{|suffix| prefix + suffix}
|
21
|
+
end
|
22
|
+
|
23
|
+
@min = min
|
24
|
+
@max = max
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_new_set
|
28
|
+
new_key = "temp:#{SecureRandom.hex(10)}"
|
29
|
+
|
30
|
+
# Make a copy of the data into new_key then filter values if indicated
|
31
|
+
# (always making a copy gets normal sets into a sorted set key OR if
|
32
|
+
# already sorted gives us a safe place to filter out values without
|
33
|
+
# perturbing the original sorted set).
|
34
|
+
|
35
|
+
if !keys.empty?
|
36
|
+
redis.zunionstore(new_key, keys)
|
37
|
+
redis.zremrangebyscore(new_key, '-inf', "(#{min}") unless min.nil?
|
38
|
+
redis.zremrangebyscore(new_key, "(#{max}", '+inf') unless max.nil?
|
39
|
+
end
|
40
|
+
|
41
|
+
new_key
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'jobba/clause'
|
2
|
+
|
3
|
+
class Jobba::ClauseFactory
|
4
|
+
|
5
|
+
def self.new_clause(key, value)
|
6
|
+
if value.nil?
|
7
|
+
raise ArgumentError, "Nil search criteria are not currently " \
|
8
|
+
"accepted in a Jobba `where` call"
|
9
|
+
end
|
10
|
+
|
11
|
+
case key.to_sym
|
12
|
+
when :state
|
13
|
+
state_clause(value)
|
14
|
+
when :job_name
|
15
|
+
Jobba::Clause.new(prefix: "job_name", suffixes: value)
|
16
|
+
when :job_arg
|
17
|
+
Jobba::Clause.new(prefix: "job_arg", suffixes: value)
|
18
|
+
when /.*_at/
|
19
|
+
timestamp_clause(key, value)
|
20
|
+
else
|
21
|
+
raise ArgumentError, "#{key} is not a valid key in a Jobba `where` call"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
def self.timestamp_clause(timestamp_name, options)
|
28
|
+
validate_timestamp_name!(timestamp_name)
|
29
|
+
|
30
|
+
min, max =
|
31
|
+
case options
|
32
|
+
when Array
|
33
|
+
if options.length != 2
|
34
|
+
raise ArgumentError, "Wrong number of array entries for '#{timestamp_name}'."
|
35
|
+
end
|
36
|
+
|
37
|
+
[options[0], options[1]]
|
38
|
+
when Hash
|
39
|
+
[options[:after], options[:before]]
|
40
|
+
else
|
41
|
+
raise ArgumentError,
|
42
|
+
"#{option_value} is not a valid value for a " +
|
43
|
+
"#{option_key} key in a Jobba `where` call"
|
44
|
+
end
|
45
|
+
|
46
|
+
min = Jobba::Utils.time_to_usec_int(min)
|
47
|
+
max = Jobba::Utils.time_to_usec_int(max)
|
48
|
+
|
49
|
+
Jobba::Clause.new(keys: timestamp_name, min: min, max: max)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.state_clause(state)
|
53
|
+
state = [state].flatten.collect { |ss|
|
54
|
+
case ss
|
55
|
+
when :completed
|
56
|
+
Jobba::State::COMPLETED.collect(&:name)
|
57
|
+
when :incomplete
|
58
|
+
Jobba::State::INCOMPLETE.collect(&:name)
|
59
|
+
else
|
60
|
+
ss
|
61
|
+
end
|
62
|
+
}.uniq
|
63
|
+
|
64
|
+
validate_state_name!(state)
|
65
|
+
Jobba::Clause.new(keys: state)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.validate_state_name!(state_name)
|
69
|
+
[state_name].flatten.each do |name|
|
70
|
+
if Jobba::State::ALL.none?{|state| state.name == name.to_s}
|
71
|
+
raise ArgumentError, "'#{name}' is not a valid state name."
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.validate_timestamp_name!(timestamp_name)
|
77
|
+
if Jobba::State::ALL.none?{|state| state.timestamp_name == timestamp_name.to_s}
|
78
|
+
raise ArgumentError, "'#{timestamp_name}' is not a valid timestamp."
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
data/lib/jobba/common.rb
ADDED