jobba 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +50 -7
- data/lib/jobba/clause_factory.rb +0 -2
- data/lib/jobba/query.rb +1 -1
- data/lib/jobba/state.rb +10 -0
- data/lib/jobba/status.rb +159 -80
- data/lib/jobba/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28b15024f2860e2b7cc60d8a70d0f39f1bd9a112
|
4
|
+
data.tar.gz: e2498a809fd6583a2fa56077c78f2ed04b094a12
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2f49f2e17aa3b5cc43422aabe408a908afcd17d80973d9859ad6144e3949465fbb5a4ef2afca680ae2f0b4f8eb57478aeec296278810e8da5575764dfddf1ef8
|
7
|
+
data.tar.gz: 04c416209f2864a203e5517da500c8fa1258535757a56d1a17bac38debc4504e54f2888a36c06778741134e10d54d9eba0780ff15018e975235deb290d726888
|
data/README.md
CHANGED
@@ -123,6 +123,14 @@ There is also a special timestamp for when a kill is requested, `kill_requested_
|
|
123
123
|
|
124
124
|
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.
|
125
125
|
|
126
|
+
### Restarts
|
127
|
+
|
128
|
+
Generally-speaking, you should only enter any state once. Jobba only records the timestamp the first time you enter a state.
|
129
|
+
|
130
|
+
The expection to this rule is that if call `started!` a second time, Jobba will note this as a restart. The current values in the status will be archived and your status will look like a `started` status, with the exception that the `attempt` count will be incremented. A restarted status can then enter `succeeded`, `failed`, or `killed` states and those timestamps will be stored. `job_name` and `job_args` survive the restart.
|
131
|
+
|
132
|
+
The `attempt` field is zero-indexed, so the first attempt is attempt `0`.
|
133
|
+
|
126
134
|
## Mark Progress
|
127
135
|
|
128
136
|
If you want to have a way to track the progress of a job, you can call:
|
@@ -137,7 +145,18 @@ This is useful if you need to show a progress bar on your client, for example.
|
|
137
145
|
|
138
146
|
## Recording Job Errors
|
139
147
|
|
140
|
-
|
148
|
+
The status can keep track of a list of errors. Errors can be anything, as long as they are JSON-friendly.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
my_status.add_error("oh nooo!!")
|
152
|
+
my_status.add_error(msg: "oh nooo!!", data: 42)
|
153
|
+
```
|
154
|
+
|
155
|
+
Errors are available from an `errors` attribute
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
my_status.errors # => ["oh nooo!!", {"msg" => "oh nooo!!", "data" => 42}]
|
159
|
+
```
|
141
160
|
|
142
161
|
## Saving Job-specific Data
|
143
162
|
|
@@ -148,7 +167,9 @@ my_status.save({a: 'blah', b: [1,2,3]})
|
|
148
167
|
my_status.save("some string")
|
149
168
|
```
|
150
169
|
|
151
|
-
|
170
|
+
## Normalization of Saved Data and Errors
|
171
|
+
|
172
|
+
Note that if you `save` or `add_error` contains a hash with symbol keys, those keys will be converted to strings. In fact, any argument passed in to these methods will be converted to JSON and parsed back again so that the `data` and `errors` attributes returns the same thing regardless of if they are retrieved immediately after being set or after being loaded from Redis.
|
152
173
|
|
153
174
|
## Setting Job Name and Arguments
|
154
175
|
|
@@ -158,16 +179,24 @@ If you want to be able to query for all statuses for a certain kind of job, you
|
|
158
179
|
my_status.set_job_name("MySpecialJob")
|
159
180
|
```
|
160
181
|
|
161
|
-
If you want to be able to query for all statuses that take a certain argument as input, you can
|
182
|
+
If you want to be able to query for all statuses that take a certain argument as input, you can set job arguments on a status:
|
162
183
|
|
163
184
|
```ruby
|
164
|
-
my_status.
|
185
|
+
my_status.set_job_args(arg_1_name: arg_2, arg_2_name: arg_2)
|
165
186
|
```
|
166
187
|
|
167
|
-
where
|
188
|
+
where the keys are what the argument is called in your job (e.g. `"input_1"`) and the values are a way to identify the argument (e.g. `"gid://app/Person/72"`). The values must currently be strings.
|
168
189
|
|
169
190
|
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`.
|
170
191
|
|
192
|
+
Note that you can set job args with names that are either symbols or strings, but you can only read the args back by the string form of their name, e.g.
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
my_status.set_job_args(foo: "bar")
|
196
|
+
my_status.job_args['foo'] # => "bar"
|
197
|
+
my_status.job_args[:foo] # => nil
|
198
|
+
```
|
199
|
+
|
171
200
|
## Killing Jobs
|
172
201
|
|
173
202
|
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.
|
@@ -201,7 +230,7 @@ When you get hold of a `Status`, via `create!`, `find`, `find!`, or as the resul
|
|
201
230
|
| `id` | A Jobba-created UUID |
|
202
231
|
| `state` | one of the states above |
|
203
232
|
| `progress` | a float between 0.0 and 1.0 |
|
204
|
-
| `errors` |
|
233
|
+
| `errors` | an array of errors |
|
205
234
|
| `data` | job-specific data |
|
206
235
|
| `job_name` | The name of the job |
|
207
236
|
| `job_args` | An hash of job arguments, {arg_name: arg, ...} |
|
@@ -414,11 +443,25 @@ Note that, in operations having to do with time, this gem ignores anything beyon
|
|
414
443
|
|
415
444
|
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.
|
416
445
|
|
446
|
+
### Write from one; Read from many
|
447
|
+
|
448
|
+
Jobba assumes that any job is being run at one time by only one worker. Jobba makes no accomodations for multiple processes updating a Status at the same time; multiple processes reading of a Status are fine of course.
|
449
|
+
|
450
|
+
## Development
|
451
|
+
|
452
|
+
By default, this gem uses `fakeredis` instead of real Redis. This is great most of the time, but occassionally `fakeredis` doesn't work exactly like real Redis. If you want to use real Redis, just set the `USE_REAL_REDIS` environment variable to `true`, e.g.
|
453
|
+
|
454
|
+
```
|
455
|
+
$> USE_REAL_REDIS=true rspec
|
456
|
+
```
|
457
|
+
|
458
|
+
Travis runs the specs with both `fakeredis` and real Redis.
|
459
|
+
|
417
460
|
## TODO
|
418
461
|
|
419
462
|
1. Provide job min, max, and average durations.
|
420
|
-
2. Implement `add_error`.
|
421
463
|
8. Specs that test scale.
|
464
|
+
9. Move redis code in `set_job_args`, `set_job_name`, and `save` into `set` to match rest of code.
|
422
465
|
|
423
466
|
|
424
467
|
|
data/lib/jobba/clause_factory.rb
CHANGED
data/lib/jobba/query.rb
CHANGED
data/lib/jobba/state.rb
CHANGED
@@ -5,6 +5,11 @@ class Jobba::State
|
|
5
5
|
def initialize(name, timestamp_name)
|
6
6
|
@name = name
|
7
7
|
@timestamp_name = timestamp_name
|
8
|
+
@timestamp_name_key = timestamp_name.to_sym
|
9
|
+
end
|
10
|
+
|
11
|
+
def timestamp_name_key
|
12
|
+
@timestamp_name_key
|
8
13
|
end
|
9
14
|
|
10
15
|
def self.from_name(state_name)
|
@@ -51,6 +56,11 @@ class Jobba::State
|
|
51
56
|
UNKNOWN
|
52
57
|
].freeze
|
53
58
|
|
59
|
+
ALL_TIMESTAMP_SYMBOLS = ALL.collect{|state| state.timestamp_name_key}.freeze
|
60
|
+
|
61
|
+
def to_json
|
62
|
+
name.to_json
|
63
|
+
end
|
54
64
|
end
|
55
65
|
|
56
66
|
|
data/lib/jobba/status.rb
CHANGED
@@ -20,21 +20,21 @@ module Jobba
|
|
20
20
|
# Finds the job with the specified ID and returns it. If no such ID
|
21
21
|
# exists in the store, returns nil.
|
22
22
|
def self.find(id)
|
23
|
-
if (hash = raw_redis_hash(id))
|
24
|
-
new(raw: hash)
|
25
|
-
else
|
23
|
+
if (hash = raw_redis_hash(id)).empty?
|
26
24
|
nil
|
25
|
+
else
|
26
|
+
new(raw: hash)
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.local_attrs
|
31
|
-
%w(id state progress errors data kill_requested_at job_name job_args) +
|
31
|
+
%w(id state progress errors data kill_requested_at job_name job_args attempt) +
|
32
32
|
State::ALL.collect(&:timestamp_name)
|
33
33
|
end
|
34
34
|
|
35
35
|
def reload!
|
36
36
|
@json_encoded_attrs = self.class.raw_redis_hash(id)
|
37
|
-
|
37
|
+
clear_attrs
|
38
38
|
self
|
39
39
|
end
|
40
40
|
|
@@ -56,11 +56,12 @@ module Jobba
|
|
56
56
|
|
57
57
|
State::ENTERABLE.each do |state|
|
58
58
|
define_method("#{state.name}!") do
|
59
|
-
return self if state == self.state
|
60
|
-
|
61
59
|
redis.multi do
|
62
|
-
|
63
|
-
|
60
|
+
if state == State::STARTED && did_start?
|
61
|
+
restart!
|
62
|
+
elsif state != self.state
|
63
|
+
move_to_state!(state)
|
64
|
+
end
|
64
65
|
end
|
65
66
|
|
66
67
|
self
|
@@ -81,6 +82,10 @@ module Jobba
|
|
81
82
|
!completed?
|
82
83
|
end
|
83
84
|
|
85
|
+
def did_start?
|
86
|
+
!self.started_at.nil?
|
87
|
+
end
|
88
|
+
|
84
89
|
def request_kill!
|
85
90
|
time, usec_int = now
|
86
91
|
if redis.hsetnx(job_key, :kill_requested_at, usec_int)
|
@@ -99,37 +104,40 @@ module Jobba
|
|
99
104
|
|
100
105
|
def set_job_name(job_name)
|
101
106
|
raise ArgumentError, "`job_name` must not be blank" if job_name.nil? || job_name.empty?
|
102
|
-
raise StandardError, "`job_name` can only be set once" if !self.job_name.nil?
|
103
107
|
|
104
108
|
redis.multi do
|
109
|
+
redis.srem(job_name_key, id)
|
105
110
|
set(job_name: job_name)
|
106
111
|
redis.sadd(job_name_key, id)
|
107
112
|
end
|
108
113
|
end
|
109
114
|
|
110
|
-
def
|
111
|
-
raise ArgumentError, "`
|
112
|
-
|
115
|
+
def set_job_args(args_hash={})
|
116
|
+
raise ArgumentError, "All values in the hash passed to `set_job_args` must be strings" \
|
117
|
+
if args_hash.values.any?{|val| !val.is_a?(String)}
|
118
|
+
|
119
|
+
args_hash = normalize_for_json(args_hash)
|
113
120
|
|
114
121
|
redis.multi do
|
115
|
-
|
116
|
-
|
117
|
-
|
122
|
+
delete_self_from_job_args_set!
|
123
|
+
set(job_args: args_hash)
|
124
|
+
add_self_to_job_args_set!
|
118
125
|
end
|
119
126
|
end
|
120
127
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
# set(errors: @errors)
|
128
|
-
# end
|
128
|
+
def add_error(error)
|
129
|
+
raise ArgumentError, "The argument to `add_error` cannot be nil" if error.nil?
|
130
|
+
|
131
|
+
errors.push(normalize_for_json(error))
|
132
|
+
set(errors: errors)
|
133
|
+
end
|
129
134
|
|
130
135
|
def save(data)
|
131
|
-
|
132
|
-
|
136
|
+
set(data: normalize_for_json(data))
|
137
|
+
end
|
138
|
+
|
139
|
+
def normalize_for_json(input)
|
140
|
+
JSON.parse(input.to_json, quirks_mode: true)
|
133
141
|
end
|
134
142
|
|
135
143
|
def delete
|
@@ -141,21 +149,12 @@ module Jobba
|
|
141
149
|
end
|
142
150
|
|
143
151
|
def delete!
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
State::ALL.each do |state|
|
148
|
-
redis.srem(state.name, id)
|
149
|
-
redis.zrem(state.timestamp_name, id)
|
150
|
-
end
|
151
|
-
|
152
|
-
redis.srem(job_name_key, id)
|
152
|
+
delete_in_redis!
|
153
|
+
delete_locally!
|
154
|
+
end
|
153
155
|
|
154
|
-
|
155
|
-
|
156
|
-
redis.srem(job_arg_key(arg), id)
|
157
|
-
end
|
158
|
-
end
|
156
|
+
def prior_attempts
|
157
|
+
[*0..attempt-1].collect{|ii| self.class.find!("#{id}:#{ii}")}
|
159
158
|
end
|
160
159
|
|
161
160
|
protected
|
@@ -165,28 +164,40 @@ module Jobba
|
|
165
164
|
end
|
166
165
|
|
167
166
|
def self.raw_redis_hash(id)
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
167
|
+
redis.hgetall(job_key(id))
|
168
|
+
end
|
169
|
+
|
170
|
+
def restart!
|
171
|
+
# Identify the values we want the restarted status to have, archive
|
172
|
+
# the attempt (clears out redis and local for this status), then
|
173
|
+
# set the restarted values
|
174
|
+
|
175
|
+
restarted_values = {
|
176
|
+
id: id,
|
177
|
+
attempt: attempt+1,
|
178
|
+
recorded_at: recorded_at,
|
179
|
+
queued_at: queued_at,
|
180
|
+
progress: 0,
|
181
|
+
errors: [],
|
182
|
+
job_name: job_name,
|
183
|
+
job_args: job_args
|
184
|
+
}
|
172
185
|
|
173
|
-
|
186
|
+
archive_attempt!
|
174
187
|
|
175
|
-
|
176
|
-
|
188
|
+
set(restarted_values)
|
189
|
+
move_to_state!(State::STARTED)
|
177
190
|
end
|
178
191
|
|
179
|
-
def
|
180
|
-
|
192
|
+
def archive_attempt!
|
193
|
+
archived_job_key = job_key(attempt)
|
194
|
+
redis.rename(job_key, archived_job_key)
|
195
|
+
redis.hset(archived_job_key, :id, "#{id}:#{attempt}".to_json)
|
196
|
+
delete_locally!
|
181
197
|
end
|
182
198
|
|
183
|
-
def
|
184
|
-
|
185
|
-
set(state: state.name, state.timestamp_name => usec_int)
|
186
|
-
self.state = state
|
187
|
-
self.send("#{state.timestamp_name}=",time)
|
188
|
-
redis.zadd(state.timestamp_name, usec_int, id)
|
189
|
-
redis.sadd(state.name, id)
|
199
|
+
def move_to_state!(state)
|
200
|
+
set(state: state, state.timestamp_name_key => Jobba::Time.now)
|
190
201
|
end
|
191
202
|
|
192
203
|
def initialize(attrs = {})
|
@@ -195,20 +206,25 @@ module Jobba
|
|
195
206
|
@json_encoded_attrs = attrs[:raw]
|
196
207
|
|
197
208
|
if @json_encoded_attrs.nil? || @json_encoded_attrs.empty?
|
198
|
-
|
199
|
-
@
|
200
|
-
@
|
201
|
-
@
|
202
|
-
@
|
209
|
+
|
210
|
+
@id = attrs[:id] || SecureRandom.uuid
|
211
|
+
@state = attrs[:state] || State::UNKNOWN
|
212
|
+
@progress = attrs[:progress] || 0
|
213
|
+
@errors = attrs[:errors] || []
|
214
|
+
@data = attrs[:data] || {}
|
215
|
+
@attempt = attrs[:attempt] || 0
|
216
|
+
@job_args = attrs[:job_args] || {}
|
217
|
+
@job_name = attrs[:job_name]
|
203
218
|
|
204
219
|
if attrs[:persist]
|
205
220
|
redis.multi do
|
206
221
|
set({
|
207
222
|
id: id,
|
208
223
|
progress: progress,
|
209
|
-
errors: errors
|
224
|
+
errors: errors,
|
225
|
+
attempt: attempt
|
210
226
|
})
|
211
|
-
|
227
|
+
move_to_state!(state)
|
212
228
|
end
|
213
229
|
end
|
214
230
|
end
|
@@ -224,59 +240,122 @@ module Jobba
|
|
224
240
|
when /.*_at/
|
225
241
|
attribute.nil? ? nil : Jobba::Utils.time_from_usec_int(attribute.to_i)
|
226
242
|
when 'job_args'
|
227
|
-
|
243
|
+
attribute || {}
|
228
244
|
else
|
229
245
|
attribute
|
230
246
|
end
|
231
247
|
end
|
232
248
|
|
233
249
|
def set(incoming_hash)
|
250
|
+
# in case the ID isn't set but is in the hash, set locally so other
|
251
|
+
# commands can use it
|
252
|
+
self.id = incoming_hash[:id] || id
|
253
|
+
|
234
254
|
apply_consistency_rules!(incoming_hash)
|
235
|
-
|
255
|
+
|
236
256
|
set_hash_in_redis(incoming_hash)
|
257
|
+
set_state_in_redis(incoming_hash)
|
258
|
+
set_state_timestamps_in_redis(incoming_hash)
|
259
|
+
|
260
|
+
set_hash_locally(incoming_hash)
|
237
261
|
end
|
238
262
|
|
239
263
|
def apply_consistency_rules!(hash)
|
240
264
|
hash[:progress] = 1.0 if hash[:state] == State::SUCCEEDED
|
241
265
|
end
|
242
266
|
|
267
|
+
def set_hash_in_redis(hash)
|
268
|
+
redis_key_value_array =
|
269
|
+
hash.to_a.flat_map do |kv_array|
|
270
|
+
key = kv_array[0]
|
271
|
+
value = kv_array[1]
|
272
|
+
value = Jobba::Utils.time_to_usec_int(value) if value.is_a?(::Time)
|
273
|
+
|
274
|
+
[key, value.to_json]
|
275
|
+
end
|
276
|
+
|
277
|
+
Jobba.redis.hmset(job_key, *redis_key_value_array)
|
278
|
+
end
|
279
|
+
|
280
|
+
def set_state_in_redis(hash)
|
281
|
+
return unless hash[:state]
|
282
|
+
redis.srem(state.name, id) unless state.nil? # leave old state if set
|
283
|
+
redis.sadd(hash[:state].name, id) # enter new state
|
284
|
+
end
|
285
|
+
|
286
|
+
def set_state_timestamps_in_redis(hash)
|
287
|
+
timestamp_names = hash.keys & State::ALL_TIMESTAMP_SYMBOLS
|
288
|
+
|
289
|
+
timestamp_names.each do |timestamp_name|
|
290
|
+
usec_int = Utils.time_to_usec_int(hash[timestamp_name])
|
291
|
+
redis.zadd(timestamp_name, usec_int, id)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
243
295
|
def set_hash_locally(hash)
|
244
296
|
hash.each{ |key, value| self.send("#{key}=", value) }
|
245
297
|
end
|
246
298
|
|
247
|
-
def
|
248
|
-
|
249
|
-
|
299
|
+
def delete_in_redis!
|
300
|
+
redis.multi do
|
301
|
+
redis.del(job_key)
|
250
302
|
|
251
|
-
|
303
|
+
State::ALL.each do |state|
|
304
|
+
redis.srem(state.name, id)
|
305
|
+
redis.zrem(state.timestamp_name, id)
|
306
|
+
end
|
307
|
+
|
308
|
+
redis.srem(job_name_key, id)
|
309
|
+
|
310
|
+
delete_self_from_job_args_set!
|
311
|
+
end
|
312
|
+
|
313
|
+
prior_attempts.each(&:delete!)
|
252
314
|
end
|
253
315
|
|
254
|
-
def
|
255
|
-
|
316
|
+
def delete_locally!
|
317
|
+
clear_attrs
|
318
|
+
@json_encoded_attrs = nil
|
256
319
|
end
|
257
320
|
|
258
|
-
def
|
259
|
-
self.
|
321
|
+
def delete_self_from_job_args_set!
|
322
|
+
self.job_args.values.each do |arg|
|
323
|
+
redis.srem(job_arg_key(arg), id)
|
324
|
+
end
|
260
325
|
end
|
261
326
|
|
262
|
-
def
|
263
|
-
|
264
|
-
|
327
|
+
def add_self_to_job_args_set!
|
328
|
+
self.job_args.values.each do |arg|
|
329
|
+
redis.sadd(job_arg_key(arg), id)
|
330
|
+
end
|
265
331
|
end
|
266
332
|
|
267
|
-
def
|
268
|
-
self.class.
|
333
|
+
def clear_attrs
|
334
|
+
self.class.local_attrs.each{|aa| send("#{aa}=",nil)}
|
335
|
+
end
|
336
|
+
|
337
|
+
def job_name_key
|
338
|
+
"job_name:#{job_name}"
|
269
339
|
end
|
270
340
|
|
271
|
-
def
|
341
|
+
def job_key(attempt=nil)
|
342
|
+
self.class.job_key(id, attempt)
|
343
|
+
end
|
344
|
+
|
345
|
+
def self.job_key(id, attempt=nil)
|
272
346
|
raise(ArgumentError, "`id` cannot be nil") if id.nil?
|
273
|
-
"
|
347
|
+
attempt.nil? ? "id:#{id}" : "id:#{id}:#{attempt}"
|
274
348
|
end
|
275
349
|
|
276
350
|
def job_arg_key(arg)
|
277
351
|
"job_arg:#{arg}"
|
278
352
|
end
|
279
353
|
|
354
|
+
def job_errors_key(id)
|
355
|
+
raise(ArgumentError, "`id` cannot be nil") if id.nil?
|
356
|
+
"#{id}:errors"
|
357
|
+
end
|
358
|
+
|
280
359
|
def compute_fractional_progress(at, out_of)
|
281
360
|
if at.nil?
|
282
361
|
raise ArgumentError, "Must specify at least `at` argument to `progress` call"
|
data/lib/jobba/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jobba
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- JP Slavinsky
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis
|