lev 6.0.0 → 7.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
- - ! '>='
|