serially 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Code Climate](https://codeclimate.com/github/mikemarsian/serially/badges/gpa.svg)](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:
|