lev 7.0.0 → 7.0.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.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- NjE5NzA3YmUwMzVmNTljMWZlMjE3MWMzNGEzODE4MjcyOTY5YWU5Zg==
5
- data.tar.gz: !binary |-
6
- OGVmOWM3NzViMDdlY2UwNTY0MDdmZTM1MGE1MWMyMTQ5NDc1OGRlOQ==
2
+ SHA1:
3
+ metadata.gz: ef0f435c1b9d31f93f019ec4ed9a39a7f5168fd2
4
+ data.tar.gz: 7b20edd8fc30967cae1a66e409d42d5ec45a54dd
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- N2E2ZTBiYTM2M2JhNDI1YmRiMmFiMDU5Y2IxMDY0N2IxMzMwMzQ2NGY1YjFi
10
- M2NmOWI5YWQyZTc3ZGU3MmFkNTk3YWE2YjZiZDgzZTJmMjI3M2Y5YThjMGI1
11
- OTRjMjcyNWEyNTdlZmI2MmY5NDg5MzI4YWJmNGMwZmU2MTA3MDY=
12
- data.tar.gz: !binary |-
13
- ZDY5MGZiZGMwNzgxZDExZGE0Njk1ZTRiMDU0YzExYjNjZDRjNWI4NzJkZTdl
14
- M2RjODNkNzUyMDYyNzUxNjYzZjhiYWNkZGEyOTJjY2RiMzFjOTY0MjFjNmEx
15
- MGYzYzY5YzUzYjRkMDRkNDUyZmRhYjRjYTY4YjA0ZjNjMmVmZjE=
6
+ metadata.gz: c423328fc9d60a3583776a51d778c0b13252da9f3f93594a3765ed445893e12207b1fd11ac97394a64e8ca6443c478f8d2789596bf01bef40726a1644e2030de
7
+ data.tar.gz: 0fbf4737caa577039695222f8bf272d7c9f99aaeca9ac57116ce9be8fb139ce5ece6280c1f8fe4b2448b22d88c16a3400baf4efbf761a7011b8fb98e2f21dec9
data/README.md CHANGED
@@ -432,48 +432,30 @@ end
432
432
 
433
433
  Routines run as ActiveJobs can also publish their status somewhere it can be listened to (e.g. to Redis).
434
434
 
435
- Routines have a `job` object and can call the following methods:
435
+ Routines have a `status` object and can call the following methods:
436
436
 
437
437
  * `set_progress(at, out_of = nil)` sets the current progress; can either pass a float between 0.0 and 1.0 or
438
438
  a counter towards a total, e.g. `set_progress(67,212)`.
439
- * `queued!` Sets the job status to 'queued'
440
- * `working!` Sets the job status to 'working'
441
- * `succeeded!` Sets the job status to 'succeeded'
442
- * `failed!` Sets the job status to 'failed'
443
- * `killed!` Sets the job status to 'killed'
444
- * `save(hash)` Takes a hash of key value pairs and writes those keys and values to the job status; there are several reserved keys which cannot be used (and which will blow up if you try to use them)
445
- * `add_error(is_fatal, error)` takes a boolean and a Lev `Error` object and adds its data to an array of `errors` in the job status hash.
439
+ * `queued!` Sets the state to 'queued'
440
+ * `started!` Sets the state to 'working'
441
+ * `succeeded!` Sets the state to 'succeeded'
442
+ * `failed!` Sets the state to 'failed'
443
+ * `killed!` Sets the state to 'killed'
444
+ * `save(hash)` Takes a hash of key value pairs and writes those keys and values to the status; there are several reserved keys which cannot be used (and which will blow up if you try to use them)
445
+ * `add_error(error)` takes a Lev `Error` object and adds its data to an array of `errors` in the job status hash.
446
446
 
447
- Routine job objects also have query methods to check if a job is in a given state, e.g. `queued?`. `completed?` and `incomplete` convenience methods are provided as well. A job is complete if it is failed or succeeded; incomplete if neither. All job routines start in an `unqueued` state and will only stay there if queueing had a problem. Scope-like class methods (e.g. `BackgroundJob.queued`) are provided to return all jobs in a given state.
447
+ Routine status objects also have query methods to check if a status is in a given state, e.g. `queued?`. `completed?` and `incomplete` convenience methods are provided as well. A status is complete if it is failed or succeeded; incomplete if neither. All job routines start in an `unqueued` state and will only stay there if queueing had a problem. Scope-like class methods are provided to return all statuses in a given state.
448
448
 
449
- For plain vanilla routines not run as an active job, the job calls are no-ops. When a routine is invoked with `perform_later`, the job object actually records the jobs to a store of your choice. The store is configured in the Lev configuration block, e.g.:
449
+ For plain vanilla routines not run as an active job, the status calls are no-ops. When a routine is invoked with `perform_later`, the status object is created/found using two configuration options that must be set (if you care about the status):
450
450
 
451
451
  ```ruby
452
452
  Lev.configure do |config|
453
- config.job_store = whatever
453
+ config.create_status_proc = ->(*) { Jobba::Status.create! }
454
+ config.find_status_proc = ->(id) { Jobba::Status.find!(id) }
454
455
  end
455
456
  ```
456
457
 
457
- The store needs to respond to the following methods:
458
-
459
- 1. fetch(key)
460
- 2. write(key, value)
461
-
462
- The default store is essentially a hash (implemented in `Lev::MemoryStore`). Any `ActiveSupport::Cache::Store` will work.
463
-
464
- A routine's job can be retrieved with `Lev::BackgroundJob.get(uuid_here)`. This just returns a simple hash. Notable keys are
465
-
466
- * `'progress'` which returns the progress as a number between 0.0 and 1.0
467
- * `'status'` which returns one of the status strings shown above
468
- * `'errors'` which (if present) is an error of error hashes
469
- * `'id'` the UUID of the routine / job
470
-
471
- Other routine-specific keys (set with a `save` call) are also present.
472
-
473
- **Notes:**
474
-
475
- 1. Don't try to write a job store that uses the ActiveRecord database, as the database changes would only be seen when the routine completes and its transaction is committed.
476
- 2. Job killing hasn't been implemented yet, but shouldn't be too bad. For routines run in a transaction (which are frankly the only ones you'd want to kill), we can likely kill them by raising an exception or similar to cause a rollback (will need to have good tests to prove that).
458
+ See the [Jobba README](https://github.com/openstax/jobba) for full details on the status objects.
477
459
 
478
460
  ## Handlers
479
461
 
data/lib/lev.rb CHANGED
@@ -26,12 +26,19 @@ require "lev/delegate_to_routine"
26
26
  require "lev/transaction_isolation"
27
27
 
28
28
  require 'lev/memory_store'
29
- require 'lev/background_job'
30
- require 'lev/no_background_job'
29
+ require 'lev/null_status'
31
30
 
32
31
  module Lev
33
32
  class << self
34
33
 
34
+ def create_status
35
+ configuration.create_status_proc.call
36
+ end
37
+
38
+ def find_status(id)
39
+ configuration.find_status_proc.call(id)
40
+ end
41
+
35
42
  ###########################################################################
36
43
  #
37
44
  # Configuration machinery.
@@ -64,8 +71,8 @@ module Lev
64
71
  attr_accessor :security_transgression_error
65
72
  attr_accessor :illegal_argument_error
66
73
  attr_accessor :raise_fatal_errors
67
- attr_accessor :job_store
68
- attr_accessor :job_store_namespace
74
+ attr_accessor :create_status_proc
75
+ attr_accessor :find_status_proc
69
76
  attr_accessor :job_class
70
77
 
71
78
  def initialize
@@ -73,8 +80,8 @@ module Lev
73
80
  @security_transgression_error = Lev::SecurityTransgression
74
81
  @illegal_argument_error = Lev::IllegalArgument
75
82
  @raise_fatal_errors = false
76
- @job_store = Lev::MemoryStore.new
77
- @job_store_namespace = "lev_job"
83
+ @create_status_proc = ->(*) { NullStatus.new }
84
+ @find_status_proc = ->(*) { NullStatus.new }
78
85
  @job_class = ::ActiveJob::Base
79
86
  super
80
87
  end
@@ -5,19 +5,18 @@ module Lev
5
5
  queue_as routine_class.active_job_queue
6
6
  args.push(routine_class.to_s)
7
7
 
8
- # To enable tracking of this job's status, create a new BackgroundJob object
9
- # and push it on to the arguments so that in `perform` it can be peeled
10
- # off and handed to the routine instance. The BackgroundJob UUID is returned
11
- # so that callers can track the status.
12
- job = Lev::BackgroundJob.create
13
- args.push(job.id)
8
+ # Create a new status object and push its ID on to the arguments so that
9
+ # in `perform` it can be used to retrieve the status when the routine is
10
+ # initialized.
11
+ status = Lev::create_status
12
+ args.push(status.id)
14
13
 
15
14
  # In theory we'd mark as queued right after the call to super, but this messes
16
15
  # up when the activejob adapter runs the job right away
17
- job.queued!
16
+ status.queued!
18
17
  super(*args, &block)
19
18
 
20
- job.id
19
+ status.id
21
20
  end
22
21
 
23
22
  def perform(*args, &block)
@@ -25,7 +24,7 @@ module Lev
25
24
  id = args.pop
26
25
  routine_class = Kernel.const_get(args.pop)
27
26
 
28
- routine_instance = routine_class.new(Lev::BackgroundJob.find!(id))
27
+ routine_instance = routine_class.new(Lev::find_status(id))
29
28
 
30
29
  routine_instance.call(*args, &block)
31
30
  end
@@ -24,10 +24,6 @@ module Lev
24
24
  ErrorTranslator.translate(self)
25
25
  end
26
26
 
27
- def to_s
28
- inspect
29
- end
30
-
31
27
  end
32
28
 
33
- end
29
+ end
@@ -4,8 +4,8 @@ module Lev
4
4
  #
5
5
  class Errors < Array
6
6
 
7
- def initialize(routine_job = nil, raise_fatal_errors = false)
8
- @routine_job = routine_job || NoBackgroundJob.new
7
+ def initialize(routine_status = nil, raise_fatal_errors = false)
8
+ @routine_status = routine_status || NullStatus.new
9
9
  @raise_fatal_errors = raise_fatal_errors
10
10
  end
11
11
 
@@ -16,13 +16,13 @@ module Lev
16
16
  return if ignored_error_procs.any?{|proc| proc.call(error)}
17
17
  self.push(error)
18
18
 
19
- routine_job.add_error(error, is_fatal: fail)
19
+ routine_status.add_error(error)
20
20
 
21
21
  if fail
22
- routine_job.failed!
22
+ routine_status.failed!
23
23
 
24
24
  if raise_fatal_errors
25
- # Use special FatalError type so Routine doesn't re-add job errors
25
+ # Use special FatalError type so Routine doesn't re-add status errors
26
26
  raise Lev::FatalError, args.to_a.map { |i| i.join(' ') }.join(' - ')
27
27
  else
28
28
  throw :fatal_errors_encountered
@@ -55,7 +55,7 @@ module Lev
55
55
 
56
56
  protected
57
57
 
58
- attr_reader :routine_job
58
+ attr_reader :routine_status
59
59
  attr_reader :raise_fatal_errors
60
60
 
61
61
  def ignored_error_procs
@@ -0,0 +1,34 @@
1
+ class Lev::NullStatus
2
+ attr_reader :id
3
+
4
+ def initialize(id=nil)
5
+ @id = id || "null-status:#{SecureRandom.uuid}"
6
+ @kill_requested = false
7
+ end
8
+
9
+ def request_kill!
10
+ @kill_requested = true
11
+ end
12
+
13
+ def kill_requested?
14
+ @kill_requested
15
+ end
16
+
17
+ def method_missing(*args, &block)
18
+ nil
19
+ end
20
+
21
+ # Provide null object pattern methods for status setter methods called from
22
+ # within routines; routines should not be using other query methods to check
23
+ # their own status (they should know it), with the exception of `kill_requested?`
24
+
25
+ def set_progress(*); end
26
+ def save(*); end
27
+ def add_error(*); end
28
+
29
+ def queued!; end
30
+ def started!; end
31
+ def succeeded!; end
32
+ def failed!; end
33
+ def killed!; end
34
+ end
@@ -265,7 +265,7 @@ module Lev
265
265
  def call(*args, &block)
266
266
  @after_transaction_blocks = []
267
267
 
268
- job.working!
268
+ status.started!
269
269
 
270
270
  begin
271
271
  in_transaction do
@@ -284,20 +284,20 @@ module Lev
284
284
  block.call
285
285
  end
286
286
  rescue Exception => e
287
- # Let exceptions escape but make sure to note the error in the job
287
+ # Let exceptions escape but make sure to note the error in the status
288
288
  # if not already done
289
289
  if !e.is_a?(Lev::FatalError)
290
290
  error = Error.new(code: :exception,
291
291
  message: e.message,
292
292
  data: e.backtrace.first)
293
- job.add_error(error, is_fatal: true)
294
- job.failed!
293
+ status.add_error(error)
294
+ status.failed!
295
295
  end
296
296
 
297
297
  raise e
298
298
  end
299
299
 
300
- job.succeeded! if !errors?
300
+ status.succeeded! if !errors?
301
301
 
302
302
  result
303
303
  end
@@ -430,23 +430,20 @@ module Lev
430
430
 
431
431
  # Note that the parent may neglect to call super, leading to this method never being called.
432
432
  # Do not perform any initialization here that cannot be safely skipped
433
- def initialize(job = nil)
434
- # If someone cares about the job, they'll pass it in; otherwise all
435
- # job updates go into the bit bucket.
436
- @job = job
433
+ def initialize(status = nil)
434
+ # If someone cares about the status, they'll pass it in; otherwise all
435
+ # status updates go into the bit bucket.
436
+ @status = status || Lev::NullStatus.new
437
437
  end
438
438
 
439
439
  protected
440
440
 
441
441
  attr_writer :runner
442
-
443
- def job
444
- @job ||= Lev::NoBackgroundJob.new
445
- end
442
+ attr_reader :status
446
443
 
447
444
  def result
448
445
  @result ||= Result.new(Outputs.new,
449
- Errors.new(job, topmost_runner.class.raise_fatal_errors?))
446
+ Errors.new(status, topmost_runner.class.raise_fatal_errors?))
450
447
  end
451
448
 
452
449
  def reset_result!
@@ -1,3 +1,3 @@
1
1
  module Lev
2
- VERSION = "7.0.0"
2
+ VERSION = "7.0.1"
3
3
  end
@@ -1,6 +1,9 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe 'ActiveJob routines' do
4
+
5
+ before { Lev::Jobba.use_jobba }
6
+
4
7
  context 'default configuration' do
5
8
  class LaterRoutine
6
9
  lev_routine active_job_queue: :something_else
@@ -24,22 +27,6 @@ RSpec.describe 'ActiveJob routines' do
24
27
 
25
28
  expect(queue_name).to eq('something_else')
26
29
  end
27
-
28
- it 'stores all the UUIDs of queued jobs' do
29
- Lev.configuration.job_store.clear
30
-
31
- job_id1 = LaterRoutine.perform_later
32
-
33
- expect(Lev::BackgroundJob.send(:job_ids)).to eq([job_id1])
34
- end
35
- end
36
-
37
- it 'does not duplicate BackgroundJobs in `all`' do
38
- # Previous track_job_id implementation changed string objects in job_ids
39
- # resulting in duplicate objects in `all`
40
- Lev.configuration.job_store.clear
41
- LaterRoutine.perform_later
42
- expect(Lev::BackgroundJob.all.count).to eq 1
43
30
  end
44
31
 
45
32
  context 'exception raised' do
@@ -56,17 +43,17 @@ RSpec.describe 'ActiveJob routines' do
56
43
  end
57
44
 
58
45
  it 'lets exception escape, job is failed and has error details' do
59
- Lev.configuration.job_store.clear
46
+ Jobba.all.delete_all!
60
47
 
61
48
  expect{
62
49
  ExceptionalRoutine.perform_later
63
50
  }.to raise_error(TypeError)
64
51
 
65
- job = Lev::BackgroundJob.all.first
52
+ status = Jobba.all.run.to_a.first
66
53
 
67
- expect(job.status).to eq Lev::BackgroundJob::STATE_FAILED
54
+ expect(status).to be_failed
68
55
 
69
- error = job.errors.first
56
+ error = status.errors.first
70
57
 
71
58
  expect(error["code"]).to eq "exception"
72
59
  expect(error["message"]).to eq "howdy there"
@@ -21,9 +21,8 @@ RSpec.configure do |config|
21
21
  config.order = 'random'
22
22
  end
23
23
 
24
-
25
24
  require 'lev'
26
- require 'debugger'
25
+ require 'byebug'
27
26
 
28
27
  require 'transaction_retry'
29
28
  TransactionRetry.apply_activerecord_patch
@@ -5,166 +5,44 @@ class StatusedRoutine
5
5
 
6
6
  protected
7
7
  def exec
8
- job.set_progress(9, 10)
8
+ status.set_progress(9, 10)
9
+ status.save({'hi' => 'there'})
10
+ fatal_error(code: 'blah', message: 'hi')
9
11
  end
10
12
  end
11
13
 
12
14
  RSpec.describe 'Statused Routines' do
13
- subject(:job) { Lev::BackgroundJob.new }
15
+
16
+ before { Lev::Jobba.use_jobba }
14
17
 
15
18
  context 'in a routine' do
16
19
  it 'queues the job object on queue' do
17
20
  id = StatusedRoutine.perform_later
18
- job = Lev::BackgroundJob.find(id)
21
+ status = Jobba::Status.find(id)
19
22
 
20
- expect(job.status).to eq(Lev::BackgroundJob::STATE_QUEUED)
23
+ expect(status).to be_queued
21
24
  end
22
25
 
23
26
  context 'inline activejob mode' do
24
27
  before { ::ActiveJob::Base.queue_adapter = :inline }
25
28
  after { ::ActiveJob::Base.queue_adapter = :test }
26
29
 
27
- it 'sets job to working when called' do
28
- expect_any_instance_of(Lev::BackgroundJob).to receive(:working!)
30
+ it 'sets job to started when called' do
31
+ expect_any_instance_of(Jobba::Status).to receive(:started!)
29
32
  StatusedRoutine.perform_later
30
33
  end
31
34
 
32
- it 'completes the job object on completion, returning other data' do
35
+ it 'completes the status object on completion, returning other data' do
33
36
  id = StatusedRoutine.perform_later
34
- job = Lev::BackgroundJob.find(id)
35
- expect(job.status).to eq(Lev::BackgroundJob::STATE_SUCCEEDED)
36
- expect(job.progress).to eq(1.0)
37
+ status = Jobba::Status.find(id)
38
+ expect(status).to be_failed
39
+ expect(status.progress).to eq(0.9)
40
+ expect(status.errors).to contain_exactly(
41
+ a_hash_including({'code' => 'blah', 'message' => 'hi'})
42
+ )
43
+ expect(status.data).to eq ({'hi' => 'there'})
37
44
  end
38
45
  end
39
46
  end
40
47
 
41
- describe '#save' do
42
- it 'saves the hash given and writes them to the job' do
43
- job.save(something: 'else')
44
- expect(job.something).to eq('else')
45
- end
46
- end
47
-
48
- describe '#add_error' do
49
- it 'adds the error object data to the job object' do
50
- errors = Lev::Error.new(code: 'bad', message: 'awful')
51
- job.add_error(errors)
52
- expect(job.errors).to eq([{ is_fatal: false,
53
- code: 'bad',
54
- message: 'awful',
55
- data: nil }])
56
- end
57
- end
58
-
59
- describe '#save' do
60
- it 'prevents the use of reserved keys' do
61
- expect {
62
- job.save(progress: 'blocked')
63
- }.to raise_error(Lev::IllegalArgument)
64
-
65
- expect {
66
- job.save(id: 'blocked')
67
- }.to raise_error(Lev::IllegalArgument)
68
-
69
- expect {
70
- job.save(status: 'blocked')
71
- }.to raise_error(Lev::IllegalArgument)
72
-
73
- expect {
74
- job.save(errors: 'blocked')
75
- }.to raise_error(Lev::IllegalArgument)
76
- end
77
-
78
- it 'saves the hash given and writes them to the job' do
79
- job.save(something: 'else')
80
- expect(job).to respond_to(:something)
81
- expect(job.something).to eq('else')
82
- end
83
- end
84
-
85
- describe 'dynamic job setters/getters' do
86
- it 'is queued' do
87
- expect(job).not_to be_queued
88
- job.queued!
89
- expect(job).to be_queued
90
- end
91
-
92
- it 'is working' do
93
- expect(job).not_to be_working
94
- job.working!
95
- expect(job).to be_working
96
- end
97
-
98
- it 'is succeeded' do
99
- expect(job).not_to be_succeeded
100
- job.succeeded!
101
- expect(job).to be_succeeded
102
- end
103
-
104
- it 'is failed' do
105
- expect(job).not_to be_failed
106
- job.failed!
107
- expect(job).to be_failed
108
- end
109
-
110
- it 'is killed' do
111
- expect(job).not_to be_killed
112
- job.killed!
113
- expect(job).to be_killed
114
- end
115
-
116
- it 'is unknown' do
117
- expect(job).to be_unknown
118
- end
119
- end
120
-
121
- describe '#set_progress' do
122
- it 'sets the progress key on the job object' do
123
- job.set_progress(8, 10)
124
- expect(job.progress).to eq(0.8)
125
- end
126
-
127
- context 'when `out_of` is supplied' do
128
- it 'requires a positive `at` float or integer' do
129
- expect {
130
- job.set_progress(nil, 1)
131
- }.to raise_error(Lev::IllegalArgument)
132
-
133
- expect {
134
- job.set_progress(-1, 1)
135
- }.to raise_error(Lev::IllegalArgument)
136
-
137
- expect {
138
- job.set_progress(2, 5)
139
- }.not_to raise_error
140
- end
141
-
142
- it 'requires `out_of` to be greater than `at`' do
143
- expect {
144
- job.set_progress(15, 8)
145
- }.to raise_error(Lev::IllegalArgument)
146
-
147
- expect {
148
- job.set_progress(5, 10)
149
- }.not_to raise_error
150
- end
151
- end
152
-
153
- context 'without out_of specified' do
154
- it 'requires `at` to be a float between 0.0 and 1.0' do
155
- expect {
156
- job.set_progress(1.1)
157
- }.to raise_error(Lev::IllegalArgument)
158
-
159
- expect {
160
- job.set_progress(-1)
161
- }.to raise_error(Lev::IllegalArgument)
162
-
163
- expect {
164
- job.set_progress(0.78)
165
- }.not_to raise_error
166
- end
167
- end
168
-
169
- end
170
48
  end
@@ -0,0 +1,13 @@
1
+ require 'jobba'
2
+
3
+ module Lev::Jobba
4
+
5
+ def self.use_jobba
6
+ Lev.configure do |config|
7
+ config.create_status_proc = ->(*) { Jobba::Status.create! }
8
+ config.find_status_proc = ->(id) { Jobba::Status.find!(id) }
9
+ end
10
+ true
11
+ end
12
+
13
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lev
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 7.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Slavinsky
@@ -14,196 +14,210 @@ dependencies:
14
14
  name: activemodel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '4.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '4.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '4.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: actionpack
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
47
  version: '4.2'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '4.2'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: activejob
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: transaction_isolation
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ! '>='
73
+ - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ! '>='
80
+ - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: transaction_retry
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ! '>='
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ! '>='
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: active_attr
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ! '>='
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
103
  version: '0'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ! '>='
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: hashie
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ! '>='
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ! '>='
122
+ - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: bundler
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ! '>='
129
+ - - ">="
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ! '>='
136
+ - - ">="
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: rake
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ! '>='
143
+ - - ">="
144
144
  - !ruby/object:Gem::Version
145
145
  version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - ! '>='
150
+ - - ">="
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: rspec
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - ! '>='
157
+ - - ">="
158
158
  - !ruby/object:Gem::Version
159
159
  version: '0'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
- - - ! '>='
164
+ - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: sqlite3
169
169
  requirement: !ruby/object:Gem::Requirement
170
170
  requirements:
171
- - - ! '>='
171
+ - - ">="
172
172
  - !ruby/object:Gem::Version
173
173
  version: '0'
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
- - - ! '>='
178
+ - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
181
  - !ruby/object:Gem::Dependency
182
- name: debugger
182
+ name: byebug
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
- - - ! '>='
185
+ - - ">="
186
186
  - !ruby/object:Gem::Version
187
187
  version: '0'
188
188
  type: :development
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
- - - ! '>='
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: jobba
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
193
207
  - !ruby/object:Gem::Version
194
208
  version: '0'
195
209
  - !ruby/object:Gem::Dependency
196
210
  name: rails
197
211
  requirement: !ruby/object:Gem::Requirement
198
212
  requirements:
199
- - - ! '>='
213
+ - - ">="
200
214
  - !ruby/object:Gem::Version
201
215
  version: '0'
202
216
  type: :development
203
217
  prerelease: false
204
218
  version_requirements: !ruby/object:Gem::Requirement
205
219
  requirements:
206
- - - ! '>='
220
+ - - ">="
207
221
  - !ruby/object:Gem::Version
208
222
  version: '0'
209
223
  description: Ride the rails but don't touch them.
@@ -218,7 +232,6 @@ files:
218
232
  - Rakefile
219
233
  - lib/lev.rb
220
234
  - lib/lev/active_job.rb
221
- - lib/lev/background_job.rb
222
235
  - lib/lev/better_active_model_errors.rb
223
236
  - lib/lev/delegate_to_routine.rb
224
237
  - lib/lev/error.rb
@@ -231,7 +244,7 @@ files:
231
244
  - lib/lev/handler.rb
232
245
  - lib/lev/handler_helper.rb
233
246
  - lib/lev/memory_store.rb
234
- - lib/lev/no_background_job.rb
247
+ - lib/lev/null_status.rb
235
248
  - lib/lev/object.rb
236
249
  - lib/lev/outputs.rb
237
250
  - lib/lev/routine.rb
@@ -240,7 +253,6 @@ files:
240
253
  - lib/lev/utilities.rb
241
254
  - lib/lev/version.rb
242
255
  - spec/active_job_routines_spec.rb
243
- - spec/background_job_spec.rb
244
256
  - spec/create_sprocket_spec.rb
245
257
  - spec/deep_merge_spec.rb
246
258
  - spec/delegates_to_spec.rb
@@ -254,6 +266,7 @@ files:
254
266
  - spec/support/create_sprocket.rb
255
267
  - spec/support/delegated_routine.rb
256
268
  - spec/support/delegating_routine.rb
269
+ - spec/support/jobba.rb
257
270
  - spec/support/paramify_handler_a.rb
258
271
  - spec/support/paramify_handler_b.rb
259
272
  - spec/support/sprocket.rb
@@ -269,23 +282,22 @@ require_paths:
269
282
  - lib
270
283
  required_ruby_version: !ruby/object:Gem::Requirement
271
284
  requirements:
272
- - - ! '>='
285
+ - - ">="
273
286
  - !ruby/object:Gem::Version
274
287
  version: '0'
275
288
  required_rubygems_version: !ruby/object:Gem::Requirement
276
289
  requirements:
277
- - - ! '>='
290
+ - - ">="
278
291
  - !ruby/object:Gem::Version
279
292
  version: '0'
280
293
  requirements: []
281
294
  rubyforge_project:
282
- rubygems_version: 2.4.6
295
+ rubygems_version: 2.4.8
283
296
  signing_key:
284
297
  specification_version: 4
285
298
  summary: Ride the rails but don't touch them.
286
299
  test_files:
287
300
  - spec/active_job_routines_spec.rb
288
- - spec/background_job_spec.rb
289
301
  - spec/create_sprocket_spec.rb
290
302
  - spec/deep_merge_spec.rb
291
303
  - spec/delegates_to_spec.rb
@@ -299,6 +311,7 @@ test_files:
299
311
  - spec/support/create_sprocket.rb
300
312
  - spec/support/delegated_routine.rb
301
313
  - spec/support/delegating_routine.rb
314
+ - spec/support/jobba.rb
302
315
  - spec/support/paramify_handler_a.rb
303
316
  - spec/support/paramify_handler_b.rb
304
317
  - spec/support/sprocket.rb
@@ -1,229 +0,0 @@
1
- require 'json'
2
-
3
- module Lev
4
- class BackgroundJob
5
- attr_reader :id, :status, :progress, :errors
6
-
7
- STATE_UNQUEUED = 'unqueued'
8
- STATE_QUEUED = 'queued'
9
- STATE_WORKING = 'working'
10
- STATE_SUCCEEDED = 'succeeded'
11
- STATE_FAILED = 'failed'
12
- STATE_KILLED = 'killed'
13
- STATE_UNKNOWN = 'unknown'
14
-
15
- STATES = [
16
- STATE_UNQUEUED,
17
- STATE_QUEUED,
18
- STATE_WORKING,
19
- STATE_SUCCEEDED,
20
- STATE_FAILED,
21
- STATE_KILLED,
22
- STATE_UNKNOWN
23
- ].freeze
24
-
25
- def self.create
26
- new(status: STATE_UNQUEUED).tap do |job|
27
- job.save_standard_values
28
- end
29
- end
30
-
31
- # Finds the job with the specified ID and returns it. If no such ID
32
- # exists in the store, returns a job with 'unknown' status and sets it
33
- # in the store
34
- def self.find!(id)
35
- find(id) || new({id: id}).tap do |job|
36
- job.save_standard_values
37
- end
38
- end
39
-
40
- # Finds the job with the specified ID and returns it. If no such ID
41
- # exists in the store, returns nil.
42
- def self.find(id)
43
- raise(ArgumentError, "`id` cannot be nil") if id.nil?
44
-
45
- attrs = { id: id }
46
-
47
- existing_job_attrs = fetch_and_parse(job_key(id))
48
-
49
- if existing_job_attrs.present?
50
- attrs.merge!(existing_job_attrs)
51
- new(attrs)
52
- else
53
- nil
54
- end
55
- end
56
-
57
- def self.all
58
- job_ids.map { |id| find!(id) }
59
- end
60
-
61
- def set_progress(at, out_of = nil)
62
- progress = compute_fractional_progress(at, out_of)
63
- set(progress: progress)
64
- end
65
-
66
- STATES.each do |state|
67
- define_method("#{state}!") do
68
- set(status: state)
69
- end
70
-
71
- define_method("#{state}?") do
72
- status == state
73
- end
74
- end
75
-
76
- (STATES + %w(completed incomplete)).each do |state|
77
- define_singleton_method("#{state}") do
78
- all.select{|job| job.send("#{state}?")}
79
- end
80
- end
81
-
82
- def completed?
83
- failed? || succeeded?
84
- end
85
-
86
- def incomplete?
87
- !completed?
88
- end
89
-
90
- def add_error(error, options = { })
91
- options = { is_fatal: false }.merge(options)
92
- @errors << { is_fatal: options[:is_fatal],
93
- code: error.code,
94
- message: error.message,
95
- data: error.data }
96
- set(errors: @errors)
97
- end
98
-
99
- # Rails compatibility
100
- # returns a Hash of all key-value pairs that have been #set()
101
- def as_json(options = {})
102
- stored
103
- end
104
-
105
- def save(incoming_hash)
106
- if reserved = incoming_hash.select { |k, _| RESERVED_KEYS.include?(k) }.first
107
- raise IllegalArgument, "Cannot set reserved key: #{reserved[0]}"
108
- else
109
- set(incoming_hash)
110
- end
111
- end
112
-
113
- def save_standard_values
114
- set({
115
- id: id,
116
- status: status,
117
- progress: progress,
118
- errors: errors
119
- })
120
- end
121
-
122
- def method_missing(method_name, *args)
123
- get_dynamic_variable(method_name) || super
124
- end
125
-
126
- def respond_to?(method_name)
127
- has_dynamic_variable?(method_name) || super
128
- end
129
-
130
- protected
131
-
132
- RESERVED_KEYS = [:id, :status, :progress, :errors]
133
-
134
- def initialize(attrs = {})
135
- attrs = attrs.stringify_keys
136
-
137
- @id = attrs['id'] || SecureRandom.uuid
138
- @status = attrs['status'] || STATE_UNKNOWN
139
- @progress = attrs['progress'] || 0
140
- @errors = attrs['errors'] || []
141
-
142
- attrs.each do |attr, value|
143
- if !instance_variable_defined?("@#{attr}")
144
- instance_variable_set("@#{attr}", attrs[attr])
145
- end
146
- end
147
- end
148
-
149
- def set(incoming_hash)
150
- apply_consistency_rules!(incoming_hash)
151
- new_hash = stored.merge(incoming_hash)
152
- new_hash.each { |k, v| instance_variable_set("@#{k}", v) }
153
- self.class.store.write(job_key, new_hash.to_json)
154
- track_job_id
155
- end
156
-
157
- def apply_consistency_rules!(hash)
158
- hash.stringify_keys!
159
- hash['progress'] = 1.0 if hash['status'] == 'succeeded'
160
- end
161
-
162
- def get_dynamic_variable(name)
163
- return nil if !has_dynamic_variable?(name)
164
- instance_variable_get("@#{name}")
165
- end
166
-
167
- def has_dynamic_variable?(name)
168
- !name.match(/\?|\!/) && instance_variable_defined?("@#{name}")
169
- end
170
-
171
- def self.store
172
- Lev.configuration.job_store
173
- end
174
-
175
- def self.fetch_and_parse(job_key)
176
- fetched = store.fetch(job_key)
177
- return nil if fetched.nil?
178
- JSON.parse(fetched).stringify_keys!
179
- end
180
-
181
- def self.job_ids
182
- store.fetch(job_key('lev_job_ids')) || []
183
- end
184
-
185
- def stored
186
- self.class.fetch_and_parse(job_key) || {}
187
- end
188
-
189
- def track_job_id
190
- ids = self.class.job_ids
191
- return if ids.include?(@id)
192
- ids << @id
193
- self.class.store.write(self.class.job_key('lev_job_ids'), ids)
194
- end
195
-
196
- def job_key
197
- self.class.job_key(@id)
198
- end
199
-
200
- def self.job_key(id)
201
- "#{Lev.configuration.job_store_namespace}:#{id}"
202
- end
203
-
204
- def has_reserved_keys?(hash)
205
- (hash.keys.collect(&:to_sym) & RESERVED_KEYS).any?
206
- end
207
-
208
- def push(key, new_item)
209
- new_value = (send(key) || []).push(new_item)
210
- set(key => new_value)
211
- end
212
-
213
- def compute_fractional_progress(at, out_of)
214
- if at.nil?
215
- raise IllegalArgument, "Must specify at least `at` argument to `progress` call"
216
- elsif at < 0
217
- raise IllegalArgument, "progress cannot be negative (at=#{at})"
218
- elsif out_of && out_of < at
219
- raise IllegalArgument, "`out_of` must be greater than `at` in `progress` calls"
220
- elsif out_of.nil? && (at < 0 || at > 1)
221
- raise IllegalArgument, "If `out_of` not specified, `at` must be in the range [0.0, 1.0]"
222
- end
223
-
224
- at.to_f / (out_of || 1).to_f
225
- end
226
-
227
- end
228
- end
229
-
@@ -1,26 +0,0 @@
1
- module Lev
2
- class NoBackgroundJob
3
-
4
- # Provide null object pattern methods for background jobs; routines should
5
- # not be checking their own status (they should know it), and outside callers
6
- # should not be checking status unless the background job is a real one.
7
-
8
- def set_progress(*); end
9
- def save(*); end
10
- def add_error(*); end
11
-
12
- Lev::BackgroundJob::STATES.each do |state|
13
- define_method("#{state}!") do; end
14
- end
15
-
16
- def self.method_missing(method_sym, *args, &block)
17
- if Lev::BackgroundJob.method_defined?(method_sym)
18
- raise NameError,
19
- "'#{method_sym}' is Lev::BackgroundJob query method, and those cannot be called on NoBackgroundJob"
20
- else
21
- super
22
- end
23
- end
24
-
25
- end
26
- end
@@ -1,129 +0,0 @@
1
- require 'spec_helper'
2
- require 'lev/active_job'
3
-
4
- describe Lev::BackgroundJob do
5
-
6
- context 'delayed routine' do
7
- class DelayedRoutine
8
- lev_routine
9
- protected
10
- def exec; end
11
- end
12
-
13
- subject(:job) { described_class.all.last }
14
-
15
- before do
16
- Lev.configuration.job_store.clear
17
- allow(SecureRandom).to receive(:uuid) { '123abc' }
18
- DelayedRoutine.perform_later
19
- end
20
-
21
- it 'behaves as a nice ruby object' do
22
- expect(job.id).to eq('123abc')
23
- expect(job.status).to eq(described_class::STATE_QUEUED)
24
- expect(job.progress).to eq(0.0)
25
- end
26
-
27
- it 'is unknown when not found' do
28
- foo = described_class.find!('noooooo')
29
- expect(foo.status).to eq(described_class::STATE_UNKNOWN)
30
- end
31
-
32
- it 'uses as_json' do
33
- json = job.as_json
34
-
35
- expect(json).to eq({
36
- 'id' => '123abc',
37
- 'status' => described_class::STATE_QUEUED,
38
- 'progress' => 0.0,
39
- 'errors' => []
40
- })
41
-
42
- job.save(foo: :bar)
43
- json = job.as_json
44
-
45
- expect(json['foo']).to eq('bar')
46
- end
47
-
48
- it 'generates attributes for custom variables' do
49
- job.save(foo: 'bar')
50
-
51
- reloaded_job = Lev::BackgroundJob.find(job.id)
52
-
53
- expect(reloaded_job.respond_to?(:foo)).to be true
54
- expect(reloaded_job.foo).to eq('bar')
55
- end
56
-
57
- it 'has scopes' do
58
- expect(described_class.incomplete.collect(&:id)).to include(job.id)
59
-
60
- job.queued!
61
- expect(described_class.incomplete.collect(&:id)).to include(job.id)
62
- expect(described_class.queued.collect(&:id)).to include(job.id)
63
-
64
- job.working!
65
- expect(described_class.incomplete.collect(&:id)).to include(job.id)
66
- expect(described_class.working.collect(&:id)).to include(job.id)
67
-
68
- job.failed!
69
- expect(described_class.incomplete.collect(&:id)).not_to include(job.id)
70
- expect(described_class.failed.collect(&:id)).to include(job.id)
71
-
72
- job.killed!
73
- expect(described_class.incomplete.collect(&:id)).to include(job.id)
74
- expect(described_class.killed.collect(&:id)).to include(job.id)
75
-
76
- job.unknown!
77
- expect(described_class.incomplete.collect(&:id)).to include(job.id)
78
- expect(described_class.unknown.collect(&:id)).to include(job.id)
79
-
80
- job.succeeded!
81
- expect(described_class.succeeded.collect(&:id)).to include(job.id)
82
- expect(described_class.incomplete.collect(&:id)).not_to include(job.id)
83
- expect(described_class.succeeded.collect(&:id)).to include(job.id)
84
- end
85
-
86
- it 'has the unqueued scope' do
87
- expect(described_class.unqueued.collect(&:id)).to eq []
88
- unqueued_job = Lev::BackgroundJob.create
89
- expect(described_class.unqueued.collect(&:id)).to include(unqueued_job.id)
90
- end
91
- end
92
-
93
- it 'sets progress to 100% when succeeded' do
94
- job = described_class.new
95
- job.succeeded!
96
- expect(job.progress).to eq 1
97
- end
98
-
99
- describe '.find!' do
100
- let!(:job) { described_class.create }
101
-
102
- it 'does not write to store when job exists' do
103
- expect(described_class.store).to_not receive(:write)
104
- found_job = described_class.find!(job.id)
105
- expect(found_job.as_json).to eq(job.as_json)
106
- end
107
-
108
- it 'finds jobs that are not in the store' do
109
- found_job = described_class.find!('not-a-real-id')
110
- expect(found_job.as_json).to include('status' => 'unknown')
111
- end
112
- end
113
-
114
- describe '.find' do
115
- let!(:job) { described_class.create }
116
-
117
- it 'finds jobs that are in the store' do
118
- expect(described_class.store).to_not receive(:write)
119
- found_job = described_class.find(job.id)
120
- expect(found_job.as_json).to eq(job.as_json)
121
- end
122
-
123
- it 'returns nil for jobs not in the store' do
124
- found_job = described_class.find('not-a-real-id')
125
- expect(found_job).to be_nil
126
- end
127
- end
128
-
129
- end