lev 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/README.md +4 -2
- data/lib/lev.rb +8 -1
- data/lib/lev/active_job.rb +25 -24
- data/lib/lev/background_job.rb +90 -43
- data/lib/lev/handler.rb +28 -28
- data/lib/lev/no_background_job.rb +1 -1
- data/lib/lev/routine.rb +13 -9
- data/lib/lev/version.rb +1 -1
- data/spec/background_job_spec.rb +82 -7
- data/spec/routine_spec.rb +37 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/statused_routines_spec.rb +5 -5
- metadata +19 -19
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NjE5NzA3YmUwMzVmNTljMWZlMjE3MWMzNGEzODE4MjcyOTY5YWU5Zg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
OGVmOWM3NzViMDdlY2UwNTY0MDdmZTM1MGE1MWMyMTQ5NDc1OGRlOQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
N2E2ZTBiYTM2M2JhNDI1YmRiMmFiMDU5Y2IxMDY0N2IxMzMwMzQ2NGY1YjFi
|
10
|
+
M2NmOWI5YWQyZTc3ZGU3MmFkNTk3YWE2YjZiZDgzZTJmMjI3M2Y5YThjMGI1
|
11
|
+
OTRjMjcyNWEyNTdlZmI2MmY5NDg5MzI4YWJmNGMwZmU2MTA3MDY=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZDY5MGZiZGMwNzgxZDExZGE0Njk1ZTRiMDU0YzExYjNjZDRjNWI4NzJkZTdl
|
14
|
+
M2RjODNkNzUyMDYyNzUxNjYzZjhiYWNkZGEyOTJjY2RiMzFjOTY0MjFjNmEx
|
15
|
+
MGYzYzY5YzUzYjRkMDRkNDUyZmRhYjRjYTY4YjA0ZjNjMmVmZjE=
|
data/README.md
CHANGED
@@ -438,13 +438,15 @@ Routines have a `job` object and can call the following methods:
|
|
438
438
|
a counter towards a total, e.g. `set_progress(67,212)`.
|
439
439
|
* `queued!` Sets the job status to 'queued'
|
440
440
|
* `working!` Sets the job status to 'working'
|
441
|
-
* `
|
441
|
+
* `succeeded!` Sets the job status to 'succeeded'
|
442
442
|
* `failed!` Sets the job status to 'failed'
|
443
443
|
* `killed!` Sets the job status to 'killed'
|
444
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
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.
|
446
446
|
|
447
|
-
|
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.
|
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.:
|
448
450
|
|
449
451
|
```ruby
|
450
452
|
Lev.configure do |config|
|
data/lib/lev.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "action_view"
|
2
|
+
require "active_job"
|
2
3
|
require "transaction_isolation"
|
3
4
|
require "transaction_retry"
|
4
5
|
require "active_attr"
|
@@ -24,7 +25,6 @@ require "lev/form_builder"
|
|
24
25
|
require "lev/delegate_to_routine"
|
25
26
|
require "lev/transaction_isolation"
|
26
27
|
|
27
|
-
require 'lev/active_job'
|
28
28
|
require 'lev/memory_store'
|
29
29
|
require 'lev/background_job'
|
30
30
|
require 'lev/no_background_job'
|
@@ -47,12 +47,17 @@ module Lev
|
|
47
47
|
|
48
48
|
def configure
|
49
49
|
yield configuration
|
50
|
+
after_initialize
|
50
51
|
end
|
51
52
|
|
52
53
|
def configuration
|
53
54
|
@configuration ||= Configuration.new
|
54
55
|
end
|
55
56
|
|
57
|
+
def after_initialize
|
58
|
+
require 'lev/active_job'
|
59
|
+
end
|
60
|
+
|
56
61
|
class Configuration
|
57
62
|
# This HTML class is added to form fields that caused errors
|
58
63
|
attr_accessor :form_error_class
|
@@ -61,6 +66,7 @@ module Lev
|
|
61
66
|
attr_accessor :raise_fatal_errors
|
62
67
|
attr_accessor :job_store
|
63
68
|
attr_accessor :job_store_namespace
|
69
|
+
attr_accessor :job_class
|
64
70
|
|
65
71
|
def initialize
|
66
72
|
@form_error_class = 'error'
|
@@ -69,6 +75,7 @@ module Lev
|
|
69
75
|
@raise_fatal_errors = false
|
70
76
|
@job_store = Lev::MemoryStore.new
|
71
77
|
@job_store_namespace = "lev_job"
|
78
|
+
@job_class = ::ActiveJob::Base
|
72
79
|
super
|
73
80
|
end
|
74
81
|
end
|
data/lib/lev/active_job.rb
CHANGED
@@ -1,32 +1,33 @@
|
|
1
|
-
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
args.push(routine_class.to_s)
|
1
|
+
module Lev
|
2
|
+
module ActiveJob
|
3
|
+
class Base < Lev.configuration.job_class
|
4
|
+
def self.perform_later(routine_class, *args, &block)
|
5
|
+
queue_as routine_class.active_job_queue
|
6
|
+
args.push(routine_class.to_s)
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
args.push(job.id)
|
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)
|
16
14
|
|
17
|
-
|
15
|
+
# In theory we'd mark as queued right after the call to super, but this messes
|
16
|
+
# up when the activejob adapter runs the job right away
|
17
|
+
job.queued!
|
18
|
+
super(*args, &block)
|
18
19
|
|
19
|
-
|
20
|
-
|
20
|
+
job.id
|
21
|
+
end
|
22
|
+
|
23
|
+
def perform(*args, &block)
|
24
|
+
# Pop arguments added by perform_later
|
25
|
+
id = args.pop
|
26
|
+
routine_class = Kernel.const_get(args.pop)
|
21
27
|
|
22
|
-
|
23
|
-
# Pop arguments added by perform_later
|
24
|
-
id = args.pop
|
25
|
-
routine_class = Kernel.const_get(args.pop)
|
28
|
+
routine_instance = routine_class.new(Lev::BackgroundJob.find!(id))
|
26
29
|
|
27
|
-
|
28
|
-
routine_instance.call(*args, &block)
|
29
|
-
end
|
30
|
+
routine_instance.call(*args, &block)
|
30
31
|
end
|
31
32
|
end
|
32
33
|
end
|
data/lib/lev/background_job.rb
CHANGED
@@ -4,69 +4,87 @@ module Lev
|
|
4
4
|
class BackgroundJob
|
5
5
|
attr_reader :id, :status, :progress, :errors
|
6
6
|
|
7
|
+
STATE_UNQUEUED = 'unqueued'
|
7
8
|
STATE_QUEUED = 'queued'
|
8
9
|
STATE_WORKING = 'working'
|
9
|
-
|
10
|
+
STATE_SUCCEEDED = 'succeeded'
|
10
11
|
STATE_FAILED = 'failed'
|
11
12
|
STATE_KILLED = 'killed'
|
12
13
|
STATE_UNKNOWN = 'unknown'
|
13
14
|
|
14
15
|
STATES = [
|
16
|
+
STATE_UNQUEUED,
|
15
17
|
STATE_QUEUED,
|
16
18
|
STATE_WORKING,
|
17
|
-
|
19
|
+
STATE_SUCCEEDED,
|
18
20
|
STATE_FAILED,
|
19
21
|
STATE_KILLED,
|
20
22
|
STATE_UNKNOWN
|
21
23
|
].freeze
|
22
24
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
25
|
+
def self.create
|
26
|
+
new(status: STATE_UNQUEUED).tap do |job|
|
27
|
+
job.save_standard_values
|
28
|
+
end
|
29
|
+
end
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
33
38
|
end
|
34
39
|
|
40
|
+
# Finds the job with the specified ID and returns it. If no such ID
|
41
|
+
# exists in the store, returns nil.
|
35
42
|
def self.find(id)
|
43
|
+
raise(ArgumentError, "`id` cannot be nil") if id.nil?
|
44
|
+
|
36
45
|
attrs = { id: id }
|
37
46
|
|
38
|
-
|
39
|
-
|
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)
|
40
52
|
else
|
41
|
-
|
53
|
+
nil
|
42
54
|
end
|
43
|
-
|
44
|
-
new(attrs)
|
45
55
|
end
|
46
56
|
|
47
57
|
def self.all
|
48
|
-
job_ids.map { |id| find(id) }
|
58
|
+
job_ids.map { |id| find!(id) }
|
49
59
|
end
|
50
60
|
|
51
61
|
def set_progress(at, out_of = nil)
|
52
62
|
progress = compute_fractional_progress(at, out_of)
|
53
|
-
|
54
|
-
data_to_set = { progress: progress }
|
55
|
-
data_to_set[:status] = STATE_COMPLETED if 1.0 == progress
|
56
|
-
|
57
|
-
set(data_to_set)
|
58
|
-
|
59
|
-
progress
|
63
|
+
set(progress: progress)
|
60
64
|
end
|
61
65
|
|
62
|
-
|
66
|
+
STATES.each do |state|
|
63
67
|
define_method("#{state}!") do
|
64
68
|
set(status: state)
|
65
69
|
end
|
70
|
+
|
71
|
+
define_method("#{state}?") do
|
72
|
+
status == state
|
73
|
+
end
|
66
74
|
end
|
67
75
|
|
68
|
-
|
69
|
-
|
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?
|
70
88
|
end
|
71
89
|
|
72
90
|
def add_error(error, options = { })
|
@@ -92,29 +110,64 @@ module Lev
|
|
92
110
|
end
|
93
111
|
end
|
94
112
|
|
113
|
+
def save_standard_values
|
114
|
+
set({
|
115
|
+
id: id,
|
116
|
+
status: status,
|
117
|
+
progress: progress,
|
118
|
+
errors: errors
|
119
|
+
})
|
120
|
+
end
|
121
|
+
|
95
122
|
def method_missing(method_name, *args)
|
96
|
-
|
123
|
+
get_dynamic_variable(method_name) || super
|
97
124
|
end
|
98
125
|
|
99
126
|
def respond_to?(method_name)
|
100
|
-
|
101
|
-
super
|
102
|
-
else
|
103
|
-
instance_variable_get("@#{method_name}").present? || super
|
104
|
-
end
|
127
|
+
has_dynamic_variable?(method_name) || super
|
105
128
|
end
|
106
129
|
|
107
130
|
protected
|
131
|
+
|
108
132
|
RESERVED_KEYS = [:id, :status, :progress, :errors]
|
109
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
|
+
|
110
149
|
def set(incoming_hash)
|
111
|
-
incoming_hash
|
112
|
-
|
113
|
-
|
114
|
-
self.class.store.write(job_key,
|
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)
|
115
154
|
track_job_id
|
116
155
|
end
|
117
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
|
+
|
118
171
|
def self.store
|
119
172
|
Lev.configuration.job_store
|
120
173
|
end
|
@@ -157,12 +210,6 @@ module Lev
|
|
157
210
|
set(key => new_value)
|
158
211
|
end
|
159
212
|
|
160
|
-
STATES.each do |state|
|
161
|
-
define_method("#{state}?") do
|
162
|
-
status == state
|
163
|
-
end
|
164
|
-
end
|
165
|
-
|
166
213
|
def compute_fractional_progress(at, out_of)
|
167
214
|
if at.nil?
|
168
215
|
raise IllegalArgument, "Must specify at least `at` argument to `progress` call"
|
data/lib/lev/handler.rb
CHANGED
@@ -14,28 +14,28 @@ module Lev
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
# Common methods for all handlers. Handlers are extensions of Routines
|
18
|
-
# and are responsible for taking input data from a form or other widget and
|
17
|
+
# Common methods for all handlers. Handlers are extensions of Routines
|
18
|
+
# and are responsible for taking input data from a form or other widget and
|
19
19
|
# doing something with it. See Lev::Routine for more information.
|
20
20
|
#
|
21
21
|
# All handlers must:
|
22
22
|
# 2) call "lev_handler"
|
23
|
-
# 3) implement the 'handle' method which takes no arguments and does the
|
23
|
+
# 3) implement the 'handle' method which takes no arguments and does the
|
24
24
|
# work the handler is charged with
|
25
|
-
# 4) implement the 'authorized?' method which returns true iff the
|
25
|
+
# 4) implement the 'authorized?' method which returns true iff the
|
26
26
|
# caller is authorized to do what the handler is charged with
|
27
27
|
#
|
28
28
|
# Handlers may:
|
29
29
|
# 1) implement the 'setup' method which runs before 'authorized?' and 'handle'.
|
30
|
-
# This method can do anything, and will likely include setting up some
|
30
|
+
# This method can do anything, and will likely include setting up some
|
31
31
|
# instance objects based on the params.
|
32
32
|
# 2) Call the class method "paramify" to declare, cast, and validate parts of
|
33
33
|
# the params hash. The first argument to paramify is the key in params
|
34
34
|
# which points to a hash of params to be paramified. If this first argument
|
35
|
-
# is unspecified (or specified as `:paramify`, a reserved symbol), the entire
|
36
|
-
# params hash will be paramified. The block passed to paramify looks just
|
35
|
+
# is unspecified (or specified as `:paramify`, a reserved symbol), the entire
|
36
|
+
# params hash will be paramified. The block passed to paramify looks just
|
37
37
|
# like the guts of an ActiveAttr model.
|
38
|
-
#
|
38
|
+
#
|
39
39
|
# When the incoming params includes :search => {:type, :terms, :num_results}
|
40
40
|
# the Handler class would look like:
|
41
41
|
#
|
@@ -53,9 +53,9 @@ module Lev
|
|
53
53
|
#
|
54
54
|
# attribute :num_results, type: Integer
|
55
55
|
# validates :num_results, numericality: { only_integer: true,
|
56
|
-
# greater_than_or_equal_to: 0 }
|
56
|
+
# greater_than_or_equal_to: 0 }
|
57
57
|
# end
|
58
|
-
#
|
58
|
+
#
|
59
59
|
# def handle
|
60
60
|
# # By this time, if there were any errors the handler would have
|
61
61
|
# # already populated the errors object and returned.
|
@@ -81,23 +81,23 @@ module Lev
|
|
81
81
|
#
|
82
82
|
# These methods are available iff these data were supplied in the call
|
83
83
|
# to the handler (not all handlers need all of this). However, note that
|
84
|
-
# the Lev::HandleWith module supplies an easy way to call Handlers from
|
84
|
+
# the Lev::HandleWith module supplies an easy way to call Handlers from
|
85
85
|
# controllers -- when this way is used, all of the methods above are available.
|
86
86
|
#
|
87
|
-
# Handler 'handle' methods don't return anything; they just set values in
|
87
|
+
# Handler 'handle' methods don't return anything; they just set values in
|
88
88
|
# the errors and results objects. The documentation for each handler
|
89
89
|
# should explain what the results will be and any nonstandard data required
|
90
90
|
# to be passed in in the options.
|
91
91
|
#
|
92
|
-
# In addition to the class- and instance-level "call" methods provided by
|
92
|
+
# In addition to the class- and instance-level "call" methods provided by
|
93
93
|
# Lev::Routine, Handlers have a class-level "handle" method (an alias of
|
94
94
|
# the class-level "call" method). The convention for handlers is that the
|
95
95
|
# call methods take a hash of options/inputs. The instance-level handle
|
96
96
|
# method doesn't take any arguments since the arguments have been stored
|
97
97
|
# as instance variables by the time the instance-level handle method is called.
|
98
|
-
#
|
98
|
+
#
|
99
99
|
# Example:
|
100
|
-
#
|
100
|
+
#
|
101
101
|
# class MyHandler
|
102
102
|
# lev_handler
|
103
103
|
# protected
|
@@ -119,7 +119,7 @@ module Lev
|
|
119
119
|
end
|
120
120
|
|
121
121
|
module ClassMethods
|
122
|
-
|
122
|
+
|
123
123
|
def handle(options={})
|
124
124
|
call(options)
|
125
125
|
end
|
@@ -140,14 +140,14 @@ module Lev
|
|
140
140
|
end
|
141
141
|
|
142
142
|
# Attach a name to this dynamic class
|
143
|
-
const_set("#{group.to_s.capitalize}Paramifier",
|
143
|
+
const_set("#{group.to_s.capitalize}Paramifier",
|
144
144
|
paramify_classes[group])
|
145
145
|
|
146
146
|
paramify_classes[group].class_eval(&block)
|
147
147
|
paramify_classes[group].group = group
|
148
148
|
end
|
149
149
|
|
150
|
-
# Define the "#{group}_params" method to get the paramifier
|
150
|
+
# Define the "#{group}_params" method to get the paramifier
|
151
151
|
# instance wrapping the params. Choose the subset of params
|
152
152
|
# based on the group, choosing all params if the default group
|
153
153
|
# is used.
|
@@ -155,7 +155,7 @@ module Lev
|
|
155
155
|
define_method method_name.to_sym do
|
156
156
|
if !instance_variable_get(variable_sym)
|
157
157
|
params_subset = group == :paramify ? params : params[group]
|
158
|
-
instance_variable_set(variable_sym,
|
158
|
+
instance_variable_set(variable_sym,
|
159
159
|
self.class.paramify_classes[group].new(params_subset))
|
160
160
|
end
|
161
161
|
instance_variable_get(variable_sym)
|
@@ -187,12 +187,12 @@ module Lev
|
|
187
187
|
attr_accessor :auth_error_details
|
188
188
|
|
189
189
|
# This is a method required by Lev::Routine. It enforces the steps common
|
190
|
-
# to all handlers.
|
190
|
+
# to all handlers.
|
191
191
|
def exec(options)
|
192
|
-
self.params = options
|
193
|
-
self.request = options
|
194
|
-
self.caller = options
|
195
|
-
self.options = options
|
192
|
+
self.params = options[:params]
|
193
|
+
self.request = options[:request]
|
194
|
+
self.caller = options[:caller]
|
195
|
+
self.options = options.except(:params, :request, :caller)
|
196
196
|
|
197
197
|
setup
|
198
198
|
raise Lev.configuration.security_transgression_error, auth_error_details unless authorized?
|
@@ -203,19 +203,19 @@ module Lev
|
|
203
203
|
# Default setup implementation -- a no-op
|
204
204
|
def setup; end
|
205
205
|
|
206
|
-
# Default authorized? implementation. It returns true so that every
|
206
|
+
# Default authorized? implementation. It returns true so that every
|
207
207
|
# handler realization has to make a conscious decision about who is authorized
|
208
|
-
# to call the handler. To help the common error of forgetting to override this
|
208
|
+
# to call the handler. To help the common error of forgetting to override this
|
209
209
|
# method in a handler instance, we provide an error message when this default
|
210
210
|
# implementation is called.
|
211
211
|
def authorized?
|
212
|
-
self.auth_error_details =
|
212
|
+
self.auth_error_details =
|
213
213
|
"Access to handlers is prevented by default. You need to override the " +
|
214
214
|
"'authorized?' in this handler to explicitly grant access."
|
215
215
|
false
|
216
216
|
end
|
217
217
|
|
218
|
-
|
218
|
+
|
219
219
|
|
220
220
|
# Helper method to validate paramified params and to transfer any errors
|
221
221
|
# into the handler.
|
@@ -14,7 +14,7 @@ module Lev
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def self.method_missing(method_sym, *args, &block)
|
17
|
-
if Lev::BackgroundJob.
|
17
|
+
if Lev::BackgroundJob.method_defined?(method_sym)
|
18
18
|
raise NameError,
|
19
19
|
"'#{method_sym}' is Lev::BackgroundJob query method, and those cannot be called on NoBackgroundJob"
|
20
20
|
else
|
data/lib/lev/routine.rb
CHANGED
@@ -208,15 +208,13 @@ module Lev
|
|
208
208
|
result.outputs.send(@express_output)
|
209
209
|
end
|
210
210
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
end
|
211
|
+
def perform_later(*args, &block)
|
212
|
+
# Delegate to a subclass of Lev::Routine::ActiveJob::Base
|
213
|
+
Lev::ActiveJob::Base.perform_later(self, *args, &block)
|
214
|
+
end
|
216
215
|
|
217
|
-
|
218
|
-
|
219
|
-
end
|
216
|
+
def active_job_queue
|
217
|
+
@active_job_queue || :default
|
220
218
|
end
|
221
219
|
|
222
220
|
# Called at a routine's class level to foretell which other routines will
|
@@ -271,6 +269,8 @@ module Lev
|
|
271
269
|
|
272
270
|
begin
|
273
271
|
in_transaction do
|
272
|
+
reset_result! if transaction_run_by?(self)
|
273
|
+
|
274
274
|
catch :fatal_errors_encountered do
|
275
275
|
if self.class.delegates_to
|
276
276
|
run(self.class.delegates_to, *args, &block)
|
@@ -297,7 +297,7 @@ module Lev
|
|
297
297
|
raise e
|
298
298
|
end
|
299
299
|
|
300
|
-
job.
|
300
|
+
job.succeeded! if !errors?
|
301
301
|
|
302
302
|
result
|
303
303
|
end
|
@@ -449,6 +449,10 @@ module Lev
|
|
449
449
|
Errors.new(job, topmost_runner.class.raise_fatal_errors?))
|
450
450
|
end
|
451
451
|
|
452
|
+
def reset_result!
|
453
|
+
@result = nil
|
454
|
+
end
|
455
|
+
|
452
456
|
def outputs
|
453
457
|
result.outputs
|
454
458
|
end
|
data/lib/lev/version.rb
CHANGED
data/spec/background_job_spec.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'lev/active_job'
|
2
3
|
|
3
4
|
describe Lev::BackgroundJob do
|
4
5
|
|
@@ -19,13 +20,13 @@ describe Lev::BackgroundJob do
|
|
19
20
|
|
20
21
|
it 'behaves as a nice ruby object' do
|
21
22
|
expect(job.id).to eq('123abc')
|
22
|
-
expect(job.status).to eq(
|
23
|
+
expect(job.status).to eq(described_class::STATE_QUEUED)
|
23
24
|
expect(job.progress).to eq(0.0)
|
24
25
|
end
|
25
26
|
|
26
27
|
it 'is unknown when not found' do
|
27
|
-
foo = described_class.find('noooooo')
|
28
|
-
expect(foo.status).to eq(
|
28
|
+
foo = described_class.find!('noooooo')
|
29
|
+
expect(foo.status).to eq(described_class::STATE_UNKNOWN)
|
29
30
|
end
|
30
31
|
|
31
32
|
it 'uses as_json' do
|
@@ -33,7 +34,7 @@ describe Lev::BackgroundJob do
|
|
33
34
|
|
34
35
|
expect(json).to eq({
|
35
36
|
'id' => '123abc',
|
36
|
-
'status' =>
|
37
|
+
'status' => described_class::STATE_QUEUED,
|
37
38
|
'progress' => 0.0,
|
38
39
|
'errors' => []
|
39
40
|
})
|
@@ -43,12 +44,86 @@ describe Lev::BackgroundJob do
|
|
43
44
|
|
44
45
|
expect(json['foo']).to eq('bar')
|
45
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
|
46
91
|
end
|
47
92
|
|
48
|
-
it 'sets progress to 100% when
|
49
|
-
job =
|
50
|
-
job.
|
93
|
+
it 'sets progress to 100% when succeeded' do
|
94
|
+
job = described_class.new
|
95
|
+
job.succeeded!
|
51
96
|
expect(job.progress).to eq 1
|
52
97
|
end
|
53
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
|
+
|
54
129
|
end
|
data/spec/routine_spec.rb
CHANGED
@@ -116,4 +116,41 @@ describe Lev::Routine do
|
|
116
116
|
end
|
117
117
|
end
|
118
118
|
|
119
|
+
it 'does not mess up results on a transaction retry' do
|
120
|
+
# To get the transaction to retry, we need to raise an exception the first time
|
121
|
+
# through execution, after an output has been set in a nested routine and
|
122
|
+
# translated to the parent routine
|
123
|
+
|
124
|
+
stub_const 'NestedRoutine', Class.new
|
125
|
+
NestedRoutine.class_eval {
|
126
|
+
lev_routine
|
127
|
+
def exec
|
128
|
+
outputs[:test] = 1
|
129
|
+
end
|
130
|
+
}
|
131
|
+
|
132
|
+
stub_const 'MainRoutine', Class.new
|
133
|
+
MainRoutine.class_eval {
|
134
|
+
lev_routine
|
135
|
+
uses_routine NestedRoutine,
|
136
|
+
translations: {outputs: {type: :verbatim}}
|
137
|
+
|
138
|
+
def exec
|
139
|
+
run(NestedRoutine)
|
140
|
+
|
141
|
+
@times_called ||= 0
|
142
|
+
@times_called += 1
|
143
|
+
raise(::ActiveRecord::TransactionIsolationConflict, 'hi') if @times_called == 1
|
144
|
+
end
|
145
|
+
}
|
146
|
+
|
147
|
+
# In reality, the Lev routine is the top-level transaction, but rspec has its own
|
148
|
+
# transactions at the top, so we have to fake that the Lev routine transaction
|
149
|
+
# is at the top.
|
150
|
+
allow(ActiveRecord::Base).to receive(:tr_in_nested_transaction?) { false }
|
151
|
+
|
152
|
+
results = MainRoutine.call
|
153
|
+
expect(results.outputs.test).to eq 1
|
154
|
+
end
|
155
|
+
|
119
156
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -25,6 +25,9 @@ end
|
|
25
25
|
require 'lev'
|
26
26
|
require 'debugger'
|
27
27
|
|
28
|
+
require 'transaction_retry'
|
29
|
+
TransactionRetry.apply_activerecord_patch
|
30
|
+
|
28
31
|
Dir[(File.expand_path('../support', __FILE__)) + ("/**/*.rb")].each { |f| require f }
|
29
32
|
|
30
33
|
ActiveRecord::Base.establish_connection(
|
@@ -32,7 +32,7 @@ RSpec.describe 'Statused Routines' do
|
|
32
32
|
it 'completes the job object on completion, returning other data' do
|
33
33
|
id = StatusedRoutine.perform_later
|
34
34
|
job = Lev::BackgroundJob.find(id)
|
35
|
-
expect(job.status).to eq(Lev::BackgroundJob::
|
35
|
+
expect(job.status).to eq(Lev::BackgroundJob::STATE_SUCCEEDED)
|
36
36
|
expect(job.progress).to eq(1.0)
|
37
37
|
end
|
38
38
|
end
|
@@ -95,10 +95,10 @@ RSpec.describe 'Statused Routines' do
|
|
95
95
|
expect(job).to be_working
|
96
96
|
end
|
97
97
|
|
98
|
-
it 'is
|
99
|
-
expect(job).not_to
|
100
|
-
job.
|
101
|
-
expect(job).to
|
98
|
+
it 'is succeeded' do
|
99
|
+
expect(job).not_to be_succeeded
|
100
|
+
job.succeeded!
|
101
|
+
expect(job).to be_succeeded
|
102
102
|
end
|
103
103
|
|
104
104
|
it 'is failed' do
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lev
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 7.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- JP Slavinsky
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -16,44 +16,44 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ! '>='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
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
|
-
version: '
|
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
|
-
version: '
|
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
|
-
version: '
|
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
|
-
version: '
|
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
|
-
version: '
|
54
|
+
version: '4.2'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: activejob
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
59
|
- - ! '>='
|
@@ -67,7 +67,7 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: transaction_isolation
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - ! '>='
|
@@ -81,7 +81,7 @@ dependencies:
|
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
84
|
+
name: transaction_retry
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
87
|
- - ! '>='
|
@@ -95,7 +95,7 @@ dependencies:
|
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
98
|
+
name: active_attr
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ! '>='
|
@@ -109,13 +109,13 @@ dependencies:
|
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
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
|
-
type: :
|
118
|
+
type: :runtime
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
@@ -123,7 +123,7 @@ dependencies:
|
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: bundler
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - ! '>='
|
@@ -137,7 +137,7 @@ dependencies:
|
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
140
|
+
name: rake
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - ! '>='
|
@@ -151,7 +151,7 @@ dependencies:
|
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
|
-
name:
|
154
|
+
name: rspec
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - ! '>='
|
@@ -165,7 +165,7 @@ dependencies:
|
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
167
|
- !ruby/object:Gem::Dependency
|
168
|
-
name:
|
168
|
+
name: sqlite3
|
169
169
|
requirement: !ruby/object:Gem::Requirement
|
170
170
|
requirements:
|
171
171
|
- - ! '>='
|
@@ -179,7 +179,7 @@ dependencies:
|
|
179
179
|
- !ruby/object:Gem::Version
|
180
180
|
version: '0'
|
181
181
|
- !ruby/object:Gem::Dependency
|
182
|
-
name:
|
182
|
+
name: debugger
|
183
183
|
requirement: !ruby/object:Gem::Requirement
|
184
184
|
requirements:
|
185
185
|
- - ! '>='
|