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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- YmVhODFiNzc4NGRkZTAxYTg5MmVjZjg5OWRlMDVlNDUwMmVlNDIyYg==
4
+ NjE5NzA3YmUwMzVmNTljMWZlMjE3MWMzNGEzODE4MjcyOTY5YWU5Zg==
5
5
  data.tar.gz: !binary |-
6
- ZDU5YjNkMDRjYmE1N2VkYzQ1Njg5OWZjMDJlNzgyMjlmZDFiMzU1Yg==
6
+ OGVmOWM3NzViMDdlY2UwNTY0MDdmZTM1MGE1MWMyMTQ5NDc1OGRlOQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- NDRlMDY4Y2NjMTg2OWJlNjVjZDVjNWVmYzRlNDRiOGYyZGEwNzFmYWUwNWRm
10
- YWEwODRjZjcxNTU3ZjExNTYxYWZkYzI4NDRmMjUzZDk3ZWMyN2UzMjE4MDA1
11
- NDU0YmI1MDljNGNkZDlhMTI5YWUzN2ZkMGZiMjYzMjVhMzhhMzk=
9
+ N2E2ZTBiYTM2M2JhNDI1YmRiMmFiMDU5Y2IxMDY0N2IxMzMwMzQ2NGY1YjFi
10
+ M2NmOWI5YWQyZTc3ZGU3MmFkNTk3YWE2YjZiZDgzZTJmMjI3M2Y5YThjMGI1
11
+ OTRjMjcyNWEyNTdlZmI2MmY5NDg5MzI4YWJmNGMwZmU2MTA3MDY=
12
12
  data.tar.gz: !binary |-
13
- NzJjOGNjZmEyZDJmYWQxY2U1Y2I5Yjc5Y2I3NzIxYzE3YjAxNDQ1ZjMzMjdj
14
- MTJlNzRkN2U1MGU4MDM2NWRlMGYzNTlkZWI5N2UyZjc4M2RlODAxYmZmZjFj
15
- YTc5MjExNjdhMzM2NWMyYTliYjNhZDA5N2RlM2Q3MDk1MjFhZTc=
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
- * `completed!` Sets the job status to 'completed'
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
- All routines have such a job object. 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.:
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
@@ -1,32 +1,33 @@
1
- if defined?(::ActiveJob)
2
- module Lev
3
- module ActiveJob
4
- class Base < ::ActiveJob::Base
5
- def self.perform_later(routine_class, *args, &block)
6
- queue_as routine_class.active_job_queue
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
- # To enable tracking of this job's status, create a new BackgroundJob object
10
- # and push it on to the arguments so that in `perform` it can be peeled
11
- # off and handed to the routine instance. The BackgroundJob UUID is returned
12
- # so that callers can track the status.
13
- job = Lev::BackgroundJob.new
14
- job.queued!
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
- super(*args, &block)
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
- job.id
20
- end
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
- def perform(*args, &block)
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
- routine_instance = routine_class.new(Lev::BackgroundJob.new(id: id))
28
- routine_instance.call(*args, &block)
29
- end
30
+ routine_instance.call(*args, &block)
30
31
  end
31
32
  end
32
33
  end
@@ -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
- STATE_COMPLETED = 'completed'
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
- STATE_COMPLETED,
19
+ STATE_SUCCEEDED,
18
20
  STATE_FAILED,
19
21
  STATE_KILLED,
20
22
  STATE_UNKNOWN
21
23
  ].freeze
22
24
 
23
- def initialize(attrs = {})
24
- @id = attrs[:id] || attrs['id'] || SecureRandom.uuid
25
- @status = attrs[:status] || attrs['status'] || STATE_UNKNOWN
26
- @progress = attrs[:progress] || attrs['progress'] || 0
27
- @errors = attrs[:errors] || attrs['errors'] || []
25
+ def self.create
26
+ new(status: STATE_UNQUEUED).tap do |job|
27
+ job.save_standard_values
28
+ end
29
+ end
28
30
 
29
- set({ id: id,
30
- status: status,
31
- progress: progress,
32
- errors: errors })
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
- if job = fetch_and_parse(job_key(id))
39
- attrs.merge!(job)
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
- attrs.merge!(status: STATE_UNKNOWN)
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
- (STATES - [STATE_COMPLETED]).each do |state|
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
- def completed!
69
- set({status: STATE_COMPLETED, progress: 1.0})
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
- instance_variable_get("@#{method_name}") || super
123
+ get_dynamic_variable(method_name) || super
97
124
  end
98
125
 
99
126
  def respond_to?(method_name)
100
- if method_name.match /\?$/
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 = incoming_hash.stringify_keys
112
- incoming_hash = stored.merge(incoming_hash)
113
- incoming_hash.each { |k, v| instance_variable_set("@#{k}", v) }
114
- self.class.store.write(job_key, incoming_hash.to_json)
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"
@@ -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.delete(:params)
193
- self.request = options.delete(:request)
194
- self.caller = options.delete(:caller)
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.new.respond_to?(method_sym)
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
@@ -208,15 +208,13 @@ module Lev
208
208
  result.outputs.send(@express_output)
209
209
  end
210
210
 
211
- if defined?(::ActiveJob)
212
- def perform_later(*args, &block)
213
- # Delegate to a subclass of Lev::Routine::ActiveJob::Base
214
- Lev::ActiveJob::Base.perform_later(self, *args, &block)
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
- def active_job_queue
218
- @active_job_queue || :default
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.completed! if !errors?
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
@@ -1,3 +1,3 @@
1
1
  module Lev
2
- VERSION = "6.0.0"
2
+ VERSION = "7.0.0"
3
3
  end
@@ -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(Lev::BackgroundJob::STATE_QUEUED)
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(Lev::BackgroundJob::STATE_UNKNOWN)
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' => Lev::BackgroundJob::STATE_QUEUED,
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 completed' do
49
- job = Lev::BackgroundJob.new
50
- job.completed!
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
@@ -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
@@ -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::STATE_COMPLETED)
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 completed' do
99
- expect(job).not_to be_completed
100
- job.completed!
101
- expect(job).to be_completed
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: 6.0.0
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: 2015-08-26 00:00:00.000000000 Z
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: '3.0'
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: '3.0'
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: '3.0'
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: '3.0'
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: '3.0'
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: '3.0'
54
+ version: '4.2'
55
55
  - !ruby/object:Gem::Dependency
56
- name: transaction_isolation
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: transaction_retry
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: active_attr
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: hashie
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: bundler
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: :development
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: rake
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: rspec
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: sqlite3
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: debugger
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: activejob
182
+ name: debugger
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
185
  - - ! '>='