jobba 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .byebug*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ jobba
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.2.3
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.2.3
5
+ cache: bundler
6
+ bundler_args: --retry=6
7
+ script:
8
+ - bundle exec rake
9
+ services:
10
+ - redis-server
11
+ notifications:
12
+ email: false
13
+ env:
14
+ - USE_REAL_REDIS=false
15
+ - USE_REAL_REDIS=true
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in jobba.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
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
@@ -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
@@ -0,0 +1,17 @@
1
+ module Jobba::Common
2
+
3
+ def redis
4
+ Jobba.redis
5
+ end
6
+
7
+ module ClassMethods
8
+ def redis
9
+ Jobba.redis
10
+ end
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ end
16
+
17
+ end
@@ -0,0 +1,10 @@
1
+ class Jobba::Configuration
2
+
3
+ attr_accessor :redis_options
4
+ attr_accessor :namespace
5
+
6
+ def initialize
7
+ @redis_options = {}
8
+ @namespace = "jobba"
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module Jobba
2
+
3
+ class NotCompletedError < StandardError; end
4
+
5
+ end