serially 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/README.md +44 -9
- data/lib/generators/serially/install/templates/create_serially_task_runs.rb +3 -1
- data/lib/serially.rb +2 -1
- data/lib/serially/job.rb +1 -1
- data/lib/serially/options.rb +40 -0
- data/lib/serially/serially.rb +11 -27
- data/lib/serially/task.rb +23 -6
- data/lib/serially/task_manager.rb +4 -0
- data/lib/serially/task_run.rb +5 -2
- data/lib/serially/task_run_writer.rb +3 -3
- data/lib/serially/task_runner.rb +7 -5
- data/lib/serially/version.rb +1 -1
- data/serially.gemspec +2 -3
- metadata +9 -10
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NzlmMzhlZjEwYWU1ZjRmMmUyYjA5MGVmZjZiMDQ2NTAyYzQwMzMyNQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YjU1YjBlNGM4NjQ2ZmUxZDVkNTQyZGY2MzVkNTdlYjIyODQ4NTYwZQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
MjFlYjZkOTcxODg5NjBiYjUzOTU0YWZmNjljZjc1NTA2OWZkYTgxNGE1ODhl
|
10
|
+
NmNiYTdhYzI5MzNmYzY5MGE0YmNlMDc4MmNlZjZlYTU2YmU3MTA2MzYwN2Uy
|
11
|
+
ODYwNDQ1YTFmOGUxMDQyMDBkNzZlYjdiYTY5N2M3OTk4NDhhNDI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
OGJhOTg2NDJjMzNjZDljZjY0YWRkNTE5MGE4MmEzMGVjZTM1MmUxMzNjMzA1
|
14
|
+
ZjljNTFhZjJhODg5ZDU5ZjQ5ZmQ4M2ZjYzRkMGI3OWFjZmFmNTcwNDIxOGU3
|
15
|
+
MzdiZDU4MjVlMGQ0OTdlMjRkODMxODhkYjUyOWQ5ZDliYjJiZGQ=
|
data/README.md
CHANGED
@@ -50,13 +50,15 @@ class Post < ActiveRecord::Base
|
|
50
50
|
end
|
51
51
|
|
52
52
|
def draft
|
53
|
+
# each task must return a boolean, signifying whether it has succeeded or not
|
53
54
|
puts "Post #{self.id} drafted"
|
54
55
|
true
|
55
56
|
end
|
56
57
|
|
57
58
|
def review
|
58
59
|
puts "Post #{self.id} reviewed by staff"
|
59
|
-
|
60
|
+
# in addition to a boolean status, task can also return a string and an arbitrary object
|
61
|
+
[true, 'reviewed by staff', {reviewed_by: 'Mike', reviewed_at: Date.today}]
|
60
62
|
end
|
61
63
|
|
62
64
|
def publish
|
@@ -70,7 +72,7 @@ class Post < ActiveRecord::Base
|
|
70
72
|
end
|
71
73
|
end
|
72
74
|
```
|
73
|
-
###
|
75
|
+
### Schedule for a Single Instance
|
74
76
|
|
75
77
|
After creating an instance of a Post, you can run `post.serially.start!` to schedule your Post tasks to run serially. They will run one after the other in the scope of the same `Serially::Job`
|
76
78
|
in the default `serially` queue.
|
@@ -90,7 +92,7 @@ Post 1 not published - bibliography is missing
|
|
90
92
|
Post 2 reviewed by staff
|
91
93
|
Post 2 not published - bibliography is missing
|
92
94
|
```
|
93
|
-
###
|
95
|
+
### Schedule Batch
|
94
96
|
If you want to schedule serially tasks for multiple instances, you can do it in a single call:
|
95
97
|
```ruby
|
96
98
|
Post.start_batch!([post1.id, post2.id, post3.id])
|
@@ -99,7 +101,7 @@ Post.start_batch!([post1.id, post2.id, post3.id])
|
|
99
101
|
### Task Return Values
|
100
102
|
|
101
103
|
* A task should at minimum return a boolean value, signifying whether that task finished successfully or not
|
102
|
-
* A task can also return a string with details of the task completion
|
104
|
+
* A task can also return a string with details of the task completion and an arbitrary object
|
103
105
|
* If a task returns _false_, the execution stops and the next tasks in the chain won't be performed for current instance
|
104
106
|
|
105
107
|
### Inspection
|
@@ -141,7 +143,9 @@ end
|
|
141
143
|
```
|
142
144
|
Jobs for different instances of Post will all be scheduled in 'posts' queue, without any interference to each other.
|
143
145
|
|
144
|
-
###
|
146
|
+
### Callbacks
|
147
|
+
|
148
|
+
#### Blocks
|
145
149
|
In addition to instance methods, you can pass a block as a task callback, and you can mix both syntaxes in your class:
|
146
150
|
|
147
151
|
```ruby
|
@@ -151,21 +155,52 @@ class Post < ActiveRecord::Base
|
|
151
155
|
serially do
|
152
156
|
task :draft
|
153
157
|
task :review do |post|
|
154
|
-
|
158
|
+
# finished successfully
|
155
159
|
true
|
156
160
|
end
|
157
161
|
task :publish do |post|
|
158
|
-
|
159
|
-
true
|
162
|
+
# block can return message and result object in addition to boolean status, just like the instance method
|
163
|
+
[true, 'published ok', {author: 'Mike}]
|
160
164
|
end
|
161
165
|
end
|
162
166
|
|
163
167
|
def draft
|
164
|
-
|
168
|
+
# using instance methods makes sense when your callback is more than 1 or 2 lines of code
|
165
169
|
[false, 'drafting failed']
|
166
170
|
end
|
167
171
|
end
|
168
172
|
```
|
173
|
+
#### On Error Callback
|
174
|
+
You can provide an error handling callback for each task, which will be called if a task fails to finish successfully. If the error handling
|
175
|
+
callback returns `true`, the execution will continue to next task, despite the failure of the previous one, otherwise tasks
|
176
|
+
execution will stop as expected.
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
class Post < ActiveRecord::Base
|
180
|
+
include Serially
|
181
|
+
|
182
|
+
serially do
|
183
|
+
task :draft, on_error: handle_draft_error
|
184
|
+
...
|
185
|
+
end
|
186
|
+
|
187
|
+
def draft
|
188
|
+
# something happened here that caused draft to fail
|
189
|
+
result_obj = {author: 'Mike'}
|
190
|
+
[false, 'drafting failed', result_obj]
|
191
|
+
end
|
192
|
+
|
193
|
+
def handle_draft_error(msg, result_obj)
|
194
|
+
if result_obj[:author] == 'Mike'
|
195
|
+
# let's continue to next task
|
196
|
+
true
|
197
|
+
else
|
198
|
+
# can't continue executing tasks like nothing happened, have to stop
|
199
|
+
false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
169
204
|
|
170
205
|
## Customize Plain Ruby Class Instantiation
|
171
206
|
Before the first task runs, Serially creates an instance of your class, on which your task callbacks are then called. By default, instances of plain ruby classes
|
@@ -7,7 +7,9 @@ class CreateSeriallyTaskRuns < ActiveRecord::Migration
|
|
7
7
|
t.integer :task_order, null: false
|
8
8
|
t.integer :status, default: 0
|
9
9
|
t.datetime :finished_at
|
10
|
-
t.text
|
10
|
+
t.text :result_message
|
11
|
+
t.text :result_object
|
12
|
+
t.boolean :error_handled, default: false
|
11
13
|
t.timestamps null: false
|
12
14
|
end
|
13
15
|
|
data/lib/serially.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require "serially/version"
|
2
2
|
require 'serially/errors'
|
3
|
+
require 'serially/options'
|
3
4
|
require 'serially/serially'
|
4
5
|
require 'serially/base'
|
5
6
|
require 'serially/instance_base'
|
@@ -8,4 +9,4 @@ require 'serially/task_manager'
|
|
8
9
|
require 'serially/task_runner'
|
9
10
|
require 'serially/task_run'
|
10
11
|
require 'serially/task_run_writer'
|
11
|
-
require 'serially/job'
|
12
|
+
require 'serially/job'
|
data/lib/serially/job.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
module Serially
|
2
|
+
|
3
|
+
class Options
|
4
|
+
# this should be overridden in sub-classes
|
5
|
+
def self.allowed
|
6
|
+
[]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.validate(options)
|
10
|
+
invalid_options = {}
|
11
|
+
|
12
|
+
valid_options = options.select{ |k,v| allowed.include?(k) }
|
13
|
+
invalid_keys = options.keys.select{ |k| !allowed.include?(k) }
|
14
|
+
empty_values = valid_options.select{ |k, v| v.blank? }.keys
|
15
|
+
|
16
|
+
invalid_options['Unrecognized Keys'] = invalid_keys if invalid_keys.present?
|
17
|
+
invalid_options['Empty Values'] = empty_values if empty_values.present?
|
18
|
+
|
19
|
+
invalid_options
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class GlobalOptions < Options
|
24
|
+
def self.allowed
|
25
|
+
[:in_queue]
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.default_queue
|
29
|
+
'serially'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class TaskOptions < Options
|
34
|
+
def self.allowed
|
35
|
+
[:on_error]
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/serially/serially.rb
CHANGED
@@ -16,7 +16,7 @@ module Serially
|
|
16
16
|
|
17
17
|
def serially(*args, &block)
|
18
18
|
options = args[0] || {}
|
19
|
-
invalid_options = Serially::
|
19
|
+
invalid_options = Serially::GlobalOptions.validate(options)
|
20
20
|
raise Serially::ConfigurationError.new("Serially received the following invalid options: #{invalid_options}") if invalid_options.present?
|
21
21
|
|
22
22
|
# If TaskManager for current including class doesn't exist, create it
|
@@ -48,20 +48,23 @@ module Serially
|
|
48
48
|
|
49
49
|
# override this to provide a custom way of creating instances of your class
|
50
50
|
def create_instance(*args)
|
51
|
+
instance = nil
|
51
52
|
args = args.flatten
|
52
53
|
if self.is_active_record?
|
53
54
|
if args.count == 1
|
54
|
-
args[0].is_a?(Fixnum) ? self.where(id: args[0]).first : self.where(args[0]).first
|
55
|
+
instance = args[0].is_a?(Fixnum) ? self.where(id: args[0]).first : self.where(args[0]).first
|
56
|
+
raise Serially::ArgumentError.new("Serially: couldn't create ActiveRecord instance with provided arguments: #{args[0]}") if instance.blank?
|
55
57
|
else
|
56
58
|
raise Serially::ArgumentError.new("Serially: default implementation of ::create_instance expects to receive either id or hash")
|
57
59
|
end
|
58
60
|
else
|
59
61
|
begin
|
60
|
-
args.blank? ? new : new(*args)
|
62
|
+
instance = args.blank? ? new : new(*args)
|
61
63
|
rescue StandardError => exc
|
62
64
|
raise Serially::ArgumentError.new("Serially: since no implementation of ::create_instance is provided in #{self}, tried to call new, but failed with provided arguments: #{args}")
|
63
65
|
end
|
64
66
|
end
|
67
|
+
instance
|
65
68
|
end
|
66
69
|
end
|
67
70
|
|
@@ -72,30 +75,11 @@ module Serially
|
|
72
75
|
|
73
76
|
# override this to provide a custom way of fetching id of your class' instance
|
74
77
|
def instance_id
|
75
|
-
self.respond_to?(:id)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
class Options
|
82
|
-
ALLOWED = [:in_queue]
|
83
|
-
|
84
|
-
def self.default_queue
|
85
|
-
'serially'
|
86
|
-
end
|
87
|
-
|
88
|
-
def self.validate(options)
|
89
|
-
invalid_options = {}
|
90
|
-
|
91
|
-
valid_options = options.select{ |k,v| ALLOWED.include?(k) }
|
92
|
-
invalid_keys = options.keys.select{ |k| !ALLOWED.include?(k) }
|
93
|
-
empty_values = valid_options.select{ |k, v| v.blank? }.keys
|
94
|
-
|
95
|
-
invalid_options['Unrecognized Keys'] = invalid_keys if invalid_keys.present?
|
96
|
-
invalid_options['Empty Values'] = empty_values if empty_values.present?
|
97
|
-
|
98
|
-
invalid_options
|
78
|
+
if self.respond_to?(:id)
|
79
|
+
self.id
|
80
|
+
else
|
81
|
+
raise Serially::ArgumentError.new("Serially: default implementation of ::instance_id is not defined for plain Ruby class, please provide one")
|
99
82
|
end
|
100
83
|
end
|
84
|
+
|
101
85
|
end
|
data/lib/serially/task.rb
CHANGED
@@ -34,23 +34,40 @@ module Serially
|
|
34
34
|
|
35
35
|
# <i>args</i> - arguments needed to create an instance of your class. If you don't provide custom implementation for create_instance,
|
36
36
|
# pass instance_id or hash of arguments,
|
37
|
-
def run!(
|
38
|
-
instance = args[0].blank? ? instance = @klass.send(:create_instance) : @klass.send(:create_instance, *args)
|
37
|
+
def run!(instance)
|
39
38
|
if instance
|
40
39
|
if !@run_block && !instance.respond_to?(@name)
|
41
40
|
raise Serially::ConfigurationError.new("Serially task #{@name} in class #{@klass} doesn't have an implementation method or a block to run")
|
42
41
|
end
|
43
42
|
begin
|
44
|
-
status, msg = @run_block ? @run_block.call(instance) : instance.send(@name)
|
43
|
+
status, msg, result_obj = @run_block ? @run_block.call(instance) : instance.send(@name)
|
45
44
|
rescue StandardError => exc
|
46
|
-
return [false, "Serially: task '#{@name}' raised exception: #{exc.message}"]
|
45
|
+
return [false, "Serially: task '#{@name}' raised exception: #{exc.message}", exc]
|
47
46
|
end
|
48
47
|
else
|
49
48
|
return [false, "Serially: instance couldn't be created, task '#{@name}'' not started"]
|
50
49
|
end
|
51
|
-
# returns true (unless status == nil/false/[])
|
52
|
-
|
50
|
+
# returns true (unless status == nil/false/[]), '' (unless msg is a not empty string) and result_obj, which might be nil
|
51
|
+
# if task doesn't return it
|
52
|
+
[status.present?, msg.to_s, result_obj]
|
53
|
+
end
|
54
|
+
|
55
|
+
def on_error!(instance, result_msg, result_obj)
|
56
|
+
if options[:on_error]
|
57
|
+
if !klass.method_defined?(options[:on_error])
|
58
|
+
raise Serially::ConfigurationError.new("Serially: error handler #{options[:on_error]} not found for task #{self.name}")
|
59
|
+
end
|
60
|
+
|
61
|
+
begin
|
62
|
+
status = instance.send(options[:on_error], result_msg, result_obj)
|
63
|
+
rescue StandardError => exc
|
64
|
+
Resque.logger.error("Serially: error handler for task '#{@name}' raised exception: #{exc.message}")
|
65
|
+
status = false
|
66
|
+
end
|
67
|
+
status
|
68
|
+
end
|
53
69
|
end
|
54
70
|
|
55
71
|
end
|
72
|
+
|
56
73
|
end
|
@@ -34,6 +34,10 @@ module Serially
|
|
34
34
|
def add_task(task_name, task_options, &block)
|
35
35
|
raise Serially::ConfigurationError.new("Task #{task_name} is already defined in class #{@klass}") if @tasks.include?(task_name)
|
36
36
|
raise Serially::ConfigurationError.new("Task name #{task_name} defined in class #{@klass} is not a symbol") if !task_name.is_a?(Symbol)
|
37
|
+
|
38
|
+
invalid_options = Serially::TaskOptions.validate(task_options)
|
39
|
+
raise Serially::ConfigurationError.new("Task #{task_name} received the following invalid options: #{invalid_options}") if invalid_options.present?
|
40
|
+
|
37
41
|
@tasks[task_name] = Serially::Task.new(task_name, next_task_order!, task_options, self, &block)
|
38
42
|
end
|
39
43
|
|
data/lib/serially/task_run.rb
CHANGED
@@ -3,6 +3,7 @@ require 'active_record'
|
|
3
3
|
module Serially
|
4
4
|
class TaskRun < ActiveRecord::Base
|
5
5
|
enum status: { pending: 0, started: 1, started_async: 2, finished_ok: 3, finished_error: 4 }
|
6
|
+
serialize :result_object
|
6
7
|
|
7
8
|
self.table_name = 'serially_task_runs'
|
8
9
|
|
@@ -19,7 +20,7 @@ module Serially
|
|
19
20
|
finished_ok? || finished_error?
|
20
21
|
end
|
21
22
|
|
22
|
-
def self.write_task_run(task, item_id, success,
|
23
|
+
def self.write_task_run(task, item_id, success, result_msg, result_obj, error_handled)
|
23
24
|
task_run = TaskRun.where(item_class: task.klass, item_id: item_id, task_name: task.name).first_or_initialize
|
24
25
|
if task_run.finished?
|
25
26
|
Resque.logger.warn("Serially: task '#{task.name}' for #{task.klass}/#{item_id} finished already, not saving this task run")
|
@@ -28,7 +29,9 @@ module Serially
|
|
28
29
|
saved = task_run.tap {|t|
|
29
30
|
t.task_order = task.task_order
|
30
31
|
t.status = success ? TaskRun.statuses[:finished_ok] : TaskRun.statuses[:finished_error]
|
31
|
-
t.result_message =
|
32
|
+
t.result_message = result_msg
|
33
|
+
t.result_object = result_obj
|
34
|
+
t.error_handled = error_handled
|
32
35
|
t.finished_at = DateTime.now
|
33
36
|
}.save
|
34
37
|
saved
|
@@ -1,9 +1,9 @@
|
|
1
1
|
module Serially
|
2
2
|
class TaskRunWriter
|
3
3
|
|
4
|
-
# called by TaskRunner, after each task
|
5
|
-
def update(task,
|
6
|
-
TaskRun.write_task_run(task,
|
4
|
+
# called by TaskRunner, after each task and its error handler have finished running
|
5
|
+
def update(task, instance, success, msg, result_obj, error_handled)
|
6
|
+
TaskRun.write_task_run(task, instance.instance_id, success, msg, result_obj, error_handled)
|
7
7
|
end
|
8
8
|
|
9
9
|
end
|
data/lib/serially/task_runner.rb
CHANGED
@@ -10,6 +10,7 @@ module Serially
|
|
10
10
|
|
11
11
|
def run!(item_class, item_id = nil)
|
12
12
|
last_run = []
|
13
|
+
instance = item_id.blank? ? item_class.send(:create_instance) : item_class.send(:create_instance, item_id)
|
13
14
|
Serially::TaskManager[item_class].each do |task|
|
14
15
|
# if task.async?
|
15
16
|
# started_async_task = SerialTasksManager.begin_task(task)
|
@@ -18,14 +19,15 @@ module Serially
|
|
18
19
|
# else
|
19
20
|
#started_async_task = false
|
20
21
|
|
21
|
-
success, msg = task.run!(
|
22
|
-
|
22
|
+
success, msg, result_obj = task.run!(instance)
|
23
|
+
error_handled = task.on_error!(instance, msg, result_obj) if !success
|
24
|
+
last_run = [task, success, msg, result_obj]
|
23
25
|
|
24
26
|
# write result log to DB
|
25
27
|
changed
|
26
|
-
notify_observers(task,
|
27
|
-
# if task didn't complete successfully, exit
|
28
|
-
break if !success
|
28
|
+
notify_observers(task, instance, success, msg, result_obj, error_handled)
|
29
|
+
# if task didn't complete successfully, and error handler didn't return true, exit
|
30
|
+
break if !success && !error_handled
|
29
31
|
end
|
30
32
|
# if started_async_task
|
31
33
|
# msg = "SerialTasksManager: started async task for #{item_class}/#{item_id}. Worker is exiting..."
|
data/lib/serially/version.rb
CHANGED
data/serially.gemspec
CHANGED
@@ -35,9 +35,8 @@ Gem::Specification.new do |spec|
|
|
35
35
|
spec.add_development_dependency 'sqlite3'
|
36
36
|
|
37
37
|
spec.description = <<desc
|
38
|
-
Have you ever had a class
|
39
|
-
Declare the tasks using a simple DSL in the order you want them to to run.
|
40
|
-
in a queue you specify (or a default one). The next task will run only if previous one finished successfully. All task runs are written to DB and can be inspected (if
|
38
|
+
Have you ever had a class whose instances required a series of background tasks to run serially, strictly one after another? Than Serially is for you.
|
39
|
+
Declare the tasks using a simple DSL in the order you want them to to run. The tasks for each instance will run inside a separate Resque job, in a queue you specify. The next task will run only if the previous one has finished successfully. All task runs are written to DB and can be inspected (if
|
41
40
|
your class is an ActiveRecord object).
|
42
41
|
desc
|
43
42
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: serially
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Polischuk
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-01-
|
11
|
+
date: 2016-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: resque
|
@@ -164,15 +164,13 @@ dependencies:
|
|
164
164
|
- - ! '>='
|
165
165
|
- !ruby/object:Gem::Version
|
166
166
|
version: '0'
|
167
|
-
description: ! 'Have you ever had a class
|
168
|
-
to run serially, strictly one after another? Than Serially is for you.
|
167
|
+
description: ! 'Have you ever had a class whose instances required a series of background
|
168
|
+
tasks to run serially, strictly one after another? Than Serially is for you.
|
169
169
|
|
170
|
-
Declare the tasks using a simple DSL in the order you want them to to run.
|
171
|
-
will
|
172
|
-
|
173
|
-
|
174
|
-
one finished successfully. All task runs are written to DB and can be inspected
|
175
|
-
(if
|
170
|
+
Declare the tasks using a simple DSL in the order you want them to to run. The tasks
|
171
|
+
for each instance will run inside a separate Resque job, in a queue you specify.
|
172
|
+
The next task will run only if the previous one has finished successfully. All task
|
173
|
+
runs are written to DB and can be inspected (if
|
176
174
|
|
177
175
|
your class is an ActiveRecord object).
|
178
176
|
|
@@ -202,6 +200,7 @@ files:
|
|
202
200
|
- lib/serially/errors.rb
|
203
201
|
- lib/serially/instance_base.rb
|
204
202
|
- lib/serially/job.rb
|
203
|
+
- lib/serially/options.rb
|
205
204
|
- lib/serially/serially.rb
|
206
205
|
- lib/serially/task.rb
|
207
206
|
- lib/serially/task_manager.rb
|