serially 0.3.1 → 0.4.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 +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
|