serially 0.1.2 → 0.1.3
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/.pryrc +1 -49
- data/README.md +38 -15
- data/lib/generators/serially/install/templates/create_serially_task_runs.rb +1 -0
- data/lib/serially/errors.rb +2 -0
- data/lib/serially/instance_base.rb +6 -2
- data/lib/serially/{worker.rb → job.rb} +4 -9
- data/lib/serially/serially.rb +28 -2
- data/lib/serially/task.rb +3 -2
- data/lib/serially/task_manager.rb +16 -2
- data/lib/serially/task_run.rb +5 -9
- data/lib/serially/task_runner.rb +1 -1
- data/lib/serially/version.rb +1 -1
- data/lib/serially.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
M2ZlZTI1ZjYzZmYwMTBhZDQ5ZDZhM2U3MTMzYjVmYmQwYzUwZTdjMQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YmIzNTJmZGZhMGY1ZGZhMzY5YmUzNTZjNmI3MDRkZTU1Zjg1ZmI5YQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
YWZhYzgzMTlhY2FkMzdlMDAyMWMyYTQ1ZjIzYjhkOWZmYzE4MjZiYTRjNzRk
|
10
|
+
MWM2YjI5N2NiMjBmNTNmOTAwZGQxZDc4YTkxNWQ4NTg2YWM3NTFjY2YxNzcy
|
11
|
+
OTg1N2U0MDFjMDYwYzA1YTJhYTkxOTdiNTU0ZTIyYzBjOWQ0ZGM=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
NjhjMTM3NTMwNTRhODQ3NTcxYmFkOTE5MWVjODg4ZGRlZjc3OGUyYjY2MWRj
|
14
|
+
MjA2ZGYwNDMwYzRjM2MyOTlmZjg2Y2M0NGRlN2FlOTI0YzkwNmI5NDdmZjhm
|
15
|
+
MWVkOGI0Y2YxYTM3ZjRhNjcxNGIxMjVmNGM0ZTY1MTNlNjA4MjU=
|
data/.pryrc
CHANGED
@@ -1,49 +1 @@
|
|
1
|
-
require './spec/active_record_helper'
|
2
|
-
|
3
|
-
class SimpleClass
|
4
|
-
include Serially
|
5
|
-
|
6
|
-
serially do
|
7
|
-
task :enrich
|
8
|
-
task :validate
|
9
|
-
task :refund
|
10
|
-
task :archive
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
class SimpleSubClass < SimpleClass
|
15
|
-
include Serially
|
16
|
-
|
17
|
-
serially do
|
18
|
-
task :zip
|
19
|
-
task :send
|
20
|
-
task :acknowledge
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
class SimpleModel < ActiveRecord::Base
|
25
|
-
include Serially
|
26
|
-
|
27
|
-
self.table_name = 'simple_items'
|
28
|
-
|
29
|
-
serially do
|
30
|
-
task :model_step1
|
31
|
-
task :model_step2
|
32
|
-
task :model_step3
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def create_simple
|
37
|
-
simple = SimpleClass.new
|
38
|
-
simple
|
39
|
-
end
|
40
|
-
|
41
|
-
def create_sub
|
42
|
-
simple = SimpleSubClass.new
|
43
|
-
simple
|
44
|
-
end
|
45
|
-
|
46
|
-
def create_model
|
47
|
-
simple = SimpleModel.create(title: 'IAmSimple')
|
48
|
-
simple
|
49
|
-
end
|
1
|
+
require './spec/active_record_helper'
|
data/README.md
CHANGED
@@ -4,13 +4,11 @@
|
|
4
4
|
[](https://codeclimate.com/github/mikemarsian/serially)
|
5
5
|
|
6
6
|
Have you ever had a class that required a series of background tasks to run serially, strictly one after another? Than Serially is for you.
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
Declare the tasks using a simple DSL in the order you want them to to run. Serially will wrap them in a single job, and schedule it using Resque
|
8
|
+
in `serially` queue (the queue is customizable). The next task will start only if previous one finished successfully. All task runs are written to DB and can be inspected (if
|
9
|
+
your class is an ActiveRecord object).
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
Note: this gem is in active development and currently is not intended to run in production.
|
11
|
+
Check [this demo app][1] to see how Serially may be used in a Rails app.
|
14
12
|
|
15
13
|
## Installation
|
16
14
|
|
@@ -30,7 +28,7 @@ Or install it yourself as:
|
|
30
28
|
|
31
29
|
## Optional ActiveRecord Setup
|
32
30
|
|
33
|
-
If you use ActiveRecord, you can generate a migration that creates `serially_task_runs` table, which would be used to write the results of all
|
31
|
+
If you use ActiveRecord, you can generate a migration that creates `serially_task_runs` table, which would be used to write the results of all task runs.
|
34
32
|
|
35
33
|
$ rails generate serially:install
|
36
34
|
$ rake db:migrate
|
@@ -69,7 +67,7 @@ class Post < ActiveRecord::Base
|
|
69
67
|
end
|
70
68
|
```
|
71
69
|
|
72
|
-
After creating 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::
|
70
|
+
After creating 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`.
|
73
71
|
An example run:
|
74
72
|
```ruby
|
75
73
|
post1 = Post.create(title: 'Critique of Pure Reason', author: 'Immanuel Kant') #=> <Post id: 1, title: 'Critique of Pure Reason'...>
|
@@ -93,10 +91,18 @@ Post 2 not published - bibliography is missing
|
|
93
91
|
* A task can also return a string with details of the task completion
|
94
92
|
* If a task returns _false_, the execution stops and the next tasks in the chain won't be performed for current instance
|
95
93
|
|
96
|
-
###
|
97
|
-
|
98
|
-
|
99
|
-
|
94
|
+
### Inspection
|
95
|
+
The easiest way to inspect the task run results, is using `serially.task_runs` instance method (which is supported for ActiveRecord classes only):
|
96
|
+
```ruby
|
97
|
+
post1.serially.task_runs # => returns ActiveRecord::Relation of all task runs for post1, ordered by their order of running
|
98
|
+
post1.serially.task_runs.finished # => returns Relation of all tasks runs that finished (successfully or not) for post1
|
99
|
+
post1.serially.task_runs.finished_ok # => returns Relation of all tasks runs that finished successfully for post1
|
100
|
+
post1.serially.task_runs.finished_error # => returns Relation of all tasks runs that finished with error for post1
|
101
|
+
post1.serially.task_runs.finished.last.task_name # => returns the name of the last finished task for post1
|
102
|
+
post1.serially.task_runs.count # => all the usual ActiveRecord queries can be used
|
103
|
+
```
|
104
|
+
You can also inspect task runs results using the `Serially::TaskRun` model directly. Calling `Serially::TaskRun.all`
|
105
|
+
for the previous task runs example, will show something like this:
|
100
106
|
```
|
101
107
|
+----+------------+---------+-----------+----------------+----------------------+---------------------+
|
102
108
|
| id | item_class | item_id | task_name | status | result_message | finished_at |
|
@@ -111,6 +117,18 @@ Running `Serially::TaskRun.all` for the above example, will show something like
|
|
111
117
|
```
|
112
118
|
Notice that the _promote_ task didn't run at all, since the _publish_ task that ran before it returned _false_ for both posts.
|
113
119
|
|
120
|
+
### Configuration
|
121
|
+
You can specify in which Resque queue the task-containing `Serially::Job` will be scheduled:
|
122
|
+
```ruby
|
123
|
+
class Post
|
124
|
+
include Serially
|
125
|
+
|
126
|
+
serially in_queue: 'posts' do
|
127
|
+
...
|
128
|
+
end
|
129
|
+
end
|
130
|
+
```
|
131
|
+
`Serially::Job`'s of different instances of Post will all be scheduled in 'posts' queue, without any interference to each other.
|
114
132
|
|
115
133
|
### Blocks
|
116
134
|
In addition to instance methods, you can pass a block as a task callback, and you can mix both syntaxes in your class:
|
@@ -123,14 +141,17 @@ class Post < ActiveRecord::Base
|
|
123
141
|
task :draft
|
124
142
|
task :review do |post|
|
125
143
|
puts "Reviewing #{post.id}"
|
144
|
+
true
|
126
145
|
end
|
127
146
|
task :publish do |post|
|
128
147
|
puts "Publishing #{post.id}"
|
148
|
+
true
|
129
149
|
end
|
130
150
|
end
|
131
151
|
|
132
152
|
def draft
|
133
153
|
puts "Drafting #{self.id}"
|
154
|
+
[false, 'drafting failed']
|
134
155
|
end
|
135
156
|
end
|
136
157
|
```
|
@@ -156,11 +177,11 @@ class Post
|
|
156
177
|
|
157
178
|
|
158
179
|
serially do
|
159
|
-
|
180
|
+
...
|
160
181
|
end
|
161
182
|
end
|
162
183
|
|
163
|
-
class
|
184
|
+
class PostWithAuthor
|
164
185
|
include Serially
|
165
186
|
|
166
187
|
attr_accessor :title
|
@@ -177,7 +198,7 @@ class Post
|
|
177
198
|
|
178
199
|
|
179
200
|
serially do
|
180
|
-
|
201
|
+
...
|
181
202
|
end
|
182
203
|
end
|
183
204
|
```
|
@@ -199,6 +220,8 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/mikema
|
|
199
220
|
|
200
221
|
## License
|
201
222
|
|
223
|
+
Copyright (c) 2015-2016 Mike Polischuk
|
224
|
+
|
202
225
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
203
226
|
|
204
227
|
[1]: https://github.com/mikemarsian/serially-demo
|
@@ -4,6 +4,7 @@ class CreateSeriallyTaskRuns < ActiveRecord::Migration
|
|
4
4
|
t.string :item_class, null: false
|
5
5
|
t.string :item_id, null: false
|
6
6
|
t.string :task_name, null: false
|
7
|
+
t.integer :task_order, null: false
|
7
8
|
t.integer :status, default: 0
|
8
9
|
t.datetime :finished_at
|
9
10
|
t.text :result_message
|
data/lib/serially/errors.rb
CHANGED
@@ -11,10 +11,14 @@ module Serially
|
|
11
11
|
def_delegator :@task_manager, :tasks
|
12
12
|
|
13
13
|
def start!
|
14
|
-
Serially::
|
14
|
+
Serially::Job.enqueue(@instance.class, @instance.instance_id, @task_manager.queue)
|
15
|
+
end
|
16
|
+
|
17
|
+
def task_runs
|
18
|
+
raise NotSupportedError.new('Serially: task_runs query is supported only for ActiveRecord classes') unless @instance.class.is_active_record?
|
19
|
+
Serially::TaskRun.where(item_class: @instance.class.to_s, item_id: @instance.id).order('task_order ASC')
|
15
20
|
end
|
16
21
|
|
17
|
-
private
|
18
22
|
|
19
23
|
end
|
20
24
|
end
|
@@ -3,14 +3,13 @@ require 'resque'
|
|
3
3
|
require 'resque-lonely_job'
|
4
4
|
|
5
5
|
module Serially
|
6
|
-
class
|
6
|
+
class Job
|
7
7
|
# LonelyJob ensures that only one job a time runs per item_class/item_id (such as Invoice/34599), which
|
8
8
|
# effectively means that for every invoice there is only one job a time, which ensures invoice jobs are processed serially
|
9
9
|
extend Resque::Plugins::LonelyJob
|
10
10
|
|
11
|
-
@queue = 'serially'
|
12
11
|
def self.queue
|
13
|
-
|
12
|
+
Serially::Options.default_queue
|
14
13
|
end
|
15
14
|
|
16
15
|
# this ensures that for item_class=Invoice, and item_id=34500, only one job will run at a time
|
@@ -26,12 +25,8 @@ module Serially
|
|
26
25
|
Resque.logger.info(result_str)
|
27
26
|
end
|
28
27
|
|
29
|
-
def self.enqueue(item_class, item_id)
|
30
|
-
Resque.
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.enqueue_batch(item_class, items)
|
34
|
-
items.each {|item| enqueue(item_class.to_s, item.instance_id)}
|
28
|
+
def self.enqueue(item_class, item_id, enqueue_to = nil)
|
29
|
+
Resque.enqueue_to(enqueue_to || self.queue, Serially::Job, item_class.to_s, item_id)
|
35
30
|
end
|
36
31
|
end
|
37
32
|
end
|
data/lib/serially/serially.rb
CHANGED
@@ -16,10 +16,12 @@ module Serially
|
|
16
16
|
|
17
17
|
def serially(*args, &block)
|
18
18
|
options = args[0] || {}
|
19
|
+
invalid_options = Serially::Options.validate(options)
|
20
|
+
raise Serially::ConfigurationError.new("Serially received the following invalid options: #{invalid_options}") if invalid_options.present?
|
19
21
|
|
20
22
|
# If TaskManager for current including class doesn't exist, create it
|
21
|
-
|
22
|
-
Serially::TaskManager[self]
|
23
|
+
Serially::TaskManager[self] ||= Serially::TaskManager.new(self, options)
|
24
|
+
task_manager = Serially::TaskManager[self]
|
23
25
|
|
24
26
|
# create a new base, and resolve DSL
|
25
27
|
@serially = Serially::Base.new(task_manager)
|
@@ -65,4 +67,28 @@ module Serially
|
|
65
67
|
def instance_id
|
66
68
|
self.respond_to?(:id) ? self.id : self.object_id
|
67
69
|
end
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
class Options
|
75
|
+
ALLOWED = [:in_queue]
|
76
|
+
|
77
|
+
def self.default_queue
|
78
|
+
'serially'
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.validate(options)
|
82
|
+
invalid_options = {}
|
83
|
+
|
84
|
+
valid_options = options.select{ |k,v| ALLOWED.include?(k) }
|
85
|
+
invalid_keys = options.keys.select{ |k| !ALLOWED.include?(k) }
|
86
|
+
empty_values = valid_options.select{ |k, v| v.blank? }.keys
|
87
|
+
|
88
|
+
invalid_options['Unrecognized Keys'] = invalid_keys if invalid_keys.present?
|
89
|
+
invalid_options['Empty Values'] = empty_values if empty_values.present?
|
90
|
+
|
91
|
+
invalid_options
|
92
|
+
end
|
93
|
+
end
|
68
94
|
end
|
data/lib/serially/task.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
module Serially
|
2
2
|
class Task
|
3
3
|
|
4
|
-
attr_accessor :name, :klass, :options, :run_block
|
4
|
+
attr_accessor :name, :task_order, :klass, :options, :run_block
|
5
5
|
|
6
|
-
def initialize(task_name, options, task_manager, &run_block)
|
6
|
+
def initialize(task_name, task_order, options, task_manager, &run_block)
|
7
7
|
@name = task_name.to_sym
|
8
|
+
@task_order = task_order
|
8
9
|
@klass = task_manager.klass
|
9
10
|
@options = options
|
10
11
|
@run_block = run_block
|
@@ -10,13 +10,18 @@ module Serially
|
|
10
10
|
(@task_managers ||= {})[klass.to_s] = task_manager
|
11
11
|
end
|
12
12
|
|
13
|
-
attr_accessor :tasks, :options, :klass
|
13
|
+
attr_accessor :tasks, :options, :klass, :queue
|
14
14
|
|
15
15
|
def initialize(klass, options = {})
|
16
16
|
@klass = klass
|
17
17
|
@options = options
|
18
18
|
# Hash is ordered since Ruby 1.9
|
19
19
|
@tasks = {}
|
20
|
+
@last_task_order = 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def queue
|
24
|
+
@options[:in_queue]
|
20
25
|
end
|
21
26
|
|
22
27
|
def clone_for(new_klass)
|
@@ -29,7 +34,7 @@ module Serially
|
|
29
34
|
def add_task(task_name, task_options, &block)
|
30
35
|
raise Serially::ConfigurationError.new("Task #{task_name} is already defined in class #{@klass}") if @tasks.include?(task_name)
|
31
36
|
raise Serially::ConfigurationError.new("Task name #{task_name} defined in class #{@klass} is not a symbol") if !task_name.is_a?(Symbol)
|
32
|
-
@tasks[task_name] = Serially::Task.new(task_name, task_options, self, &block)
|
37
|
+
@tasks[task_name] = Serially::Task.new(task_name, next_task_order!, task_options, self, &block)
|
33
38
|
end
|
34
39
|
|
35
40
|
# Allow iterating over tasks
|
@@ -41,5 +46,14 @@ module Serially
|
|
41
46
|
end
|
42
47
|
end
|
43
48
|
|
49
|
+
private
|
50
|
+
|
51
|
+
# returns next task order, and advances the counter
|
52
|
+
def next_task_order!
|
53
|
+
current_order = @last_task_order
|
54
|
+
@last_task_order += 1
|
55
|
+
current_order
|
56
|
+
end
|
57
|
+
|
44
58
|
end
|
45
59
|
end
|
data/lib/serially/task_run.rb
CHANGED
@@ -9,15 +9,10 @@ module Serially
|
|
9
9
|
validates :item_class, :item_id, :task_name, presence: true
|
10
10
|
validates :task_name, uniqueness: { scope: [:item_class, :item_id] }
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
t.status = args[:status] if args[:status].present?
|
17
|
-
t.task_name = args[:task_name] if args[:task_name].present?
|
18
|
-
end
|
19
|
-
task_run.save!
|
20
|
-
task_run
|
12
|
+
scope :finished, -> { where(status: finished_statuses) }
|
13
|
+
|
14
|
+
def self.finished_statuses
|
15
|
+
[TaskRun.statuses[:finished_ok], TaskRun.statuses[:finished_error]]
|
21
16
|
end
|
22
17
|
|
23
18
|
def finished?
|
@@ -31,6 +26,7 @@ module Serially
|
|
31
26
|
false
|
32
27
|
else
|
33
28
|
saved = task_run.tap {|t|
|
29
|
+
t.task_order = task.task_order
|
34
30
|
t.status = success ? TaskRun.statuses[:finished_ok] : TaskRun.statuses[:finished_error]
|
35
31
|
t.result_message = msg
|
36
32
|
t.finished_at = DateTime.now
|
data/lib/serially/task_runner.rb
CHANGED
@@ -35,7 +35,7 @@ module Serially
|
|
35
35
|
|
36
36
|
# If we are here, it means that no more tasks were found
|
37
37
|
success = last_run[1]
|
38
|
-
msg = success ? "Serially: finished all tasks for #{item_class}/#{item_id}. Serially::
|
38
|
+
msg = success ? "Serially: finished all tasks for #{item_class}/#{item_id}. Serially::Job is stopping..." :
|
39
39
|
"Serially: task '#{last_run[0]}' for #{item_class}/#{item_id} finished with success: #{last_run[1]}, message: #{last_run[2]}"
|
40
40
|
msg
|
41
41
|
end
|
data/lib/serially/version.rb
CHANGED
data/lib/serially.rb
CHANGED
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.1.
|
4
|
+
version: 0.1.3
|
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-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: resque
|
@@ -202,6 +202,7 @@ files:
|
|
202
202
|
- lib/serially/base.rb
|
203
203
|
- lib/serially/errors.rb
|
204
204
|
- lib/serially/instance_base.rb
|
205
|
+
- lib/serially/job.rb
|
205
206
|
- lib/serially/serially.rb
|
206
207
|
- lib/serially/task.rb
|
207
208
|
- lib/serially/task_manager.rb
|
@@ -209,7 +210,6 @@ files:
|
|
209
210
|
- lib/serially/task_run_writer.rb
|
210
211
|
- lib/serially/task_runner.rb
|
211
212
|
- lib/serially/version.rb
|
212
|
-
- lib/serially/worker.rb
|
213
213
|
- serially.gemspec
|
214
214
|
homepage: http://github.com/mikemarsian/serially
|
215
215
|
licenses:
|