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 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
  - - ! '>='