serially 0.1.1
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 +15 -0
- data/.gitignore +12 -0
- data/.pryrc +49 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +204 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/circle.yml +7 -0
- data/lib/generators/serially/install/install_generator.rb +21 -0
- data/lib/generators/serially/install/templates/create_serially_task_runs.rb +15 -0
- data/lib/serially/base.rb +12 -0
- data/lib/serially/errors.rb +7 -0
- data/lib/serially/instance_base.rb +20 -0
- data/lib/serially/serially.rb +68 -0
- data/lib/serially/task.rb +55 -0
- data/lib/serially/task_manager.rb +45 -0
- data/lib/serially/task_run.rb +42 -0
- data/lib/serially/task_run_writer.rb +10 -0
- data/lib/serially/task_runner.rb +43 -0
- data/lib/serially/version.rb +3 -0
- data/lib/serially/worker.rb +37 -0
- data/lib/serially.rb +11 -0
- data/serially.gemspec +43 -0
- metadata +240 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NDBkOWU0MjdmOTI2YmUxNDM3YzJiOTY1MWEyNzRlODc1YmQzYmNjZA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
NmE3MjZkYTRiNWFiZWU3N2I5NGY2ZTRkZDliMmQ0ZmFlZWY5ZmVmMg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ODk0MjVkODJmMGYwZDRkNjJjMDE5YmI1NzA1YTQwZWQxYjRmOGY5NmVjMzE2
|
10
|
+
N2U5NTQxZTU3Y2RmODM0M2RlYTJhNmVmODM4MTk0NTcwYTMwMGZkMjhmZmEy
|
11
|
+
ZWIwNjQ0NDM2Mzg5NDcwNjQ4MmI0YmZjZmRlMDAxZWNmODMzMjQ=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZmNjYWQ1M2Y0MzAxMDJhZmM3NTI3MTU2NWUzOTc3ODM1Yzg1YWU0ZTcyMmZh
|
14
|
+
ZTkyNTcyNzgwNWM5ZjE1ZDA4MjIxYjEwNGFlYWUwOGNjZTY5MDFlY2Q1ZGYx
|
15
|
+
MTRhODQyNjg5NzE5ODVhZjhhZDA5MDlkMjQyZjcxZWQ3M2Q4OWU=
|
data/.gitignore
ADDED
data/.pryrc
ADDED
@@ -0,0 +1,49 @@
|
|
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
|
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Mike Polischuk
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
# Serially
|
2
|
+
|
3
|
+
[](https://circleci.com/gh/mikemarsian/serially)
|
4
|
+
[](https://codeclimate.com/github/mikemarsian/serially)
|
5
|
+
|
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
|
+
All background jobs are scheduled using resque in a queue called `serially`, and Serially makes sure that for every instance of your class, only one task runs at a time.
|
8
|
+
Different instances of the same class do not interfere with each other and their tasks can run in parallel.
|
9
|
+
Serially works for both plain ruby classes and ActiveRecord models. In case of the latter, all task runs results are written to `serially_tasks` table which you can interrogate pragmatically using `Serially::TaskRun` model.
|
10
|
+
|
11
|
+
See [this rails demo app][1] that showcases how Serially gem can be used.
|
12
|
+
|
13
|
+
Note: this gem is in active development and currently is not intended to run in production.
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'serially'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install serially
|
30
|
+
|
31
|
+
## Optional ActiveRecord Setup
|
32
|
+
|
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 your task runs.
|
34
|
+
|
35
|
+
$ rails generate serially:install
|
36
|
+
$ rake db:migrate
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
```ruby
|
40
|
+
class Post < ActiveRecord::Base
|
41
|
+
include Serially
|
42
|
+
|
43
|
+
serially do
|
44
|
+
task :draft
|
45
|
+
task :review
|
46
|
+
task :publish
|
47
|
+
task :promote
|
48
|
+
end
|
49
|
+
|
50
|
+
def draft
|
51
|
+
puts "Post #{self.id} drafted"
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
def review
|
56
|
+
puts "Post #{self.id} reviewed by staff"
|
57
|
+
[true, 'reviewed by staff']
|
58
|
+
end
|
59
|
+
|
60
|
+
def publish
|
61
|
+
puts "Post #{self.id} not published - bibliography is missing"
|
62
|
+
[false, 'bibliography is missing']
|
63
|
+
end
|
64
|
+
|
65
|
+
def promote
|
66
|
+
puts "Post #{self.id} promoted"
|
67
|
+
true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
```
|
71
|
+
|
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::Worker` job.
|
73
|
+
An example run:
|
74
|
+
```ruby
|
75
|
+
post1 = Post.create(title: 'Critique of Pure Reason', author: 'Immanuel Kant') #=> <Post id: 1, title: 'Critique of Pure Reason'...>
|
76
|
+
post2 = Post.create(title: 'The Social Contract', author: 'Jean-Jacques Rousseau') #=> <Post id: 2, title: 'The Social Contract'...>
|
77
|
+
post1.serially.start!
|
78
|
+
post2.serially.start!
|
79
|
+
```
|
80
|
+
The resulting resque log may look something like this:
|
81
|
+
```
|
82
|
+
Post 1 drafted
|
83
|
+
Post 1 reviewed by staff
|
84
|
+
Post 2 drafted
|
85
|
+
Post 1 not published - bibliography is missing
|
86
|
+
Post 2 reviewed by staff
|
87
|
+
Post 2 not published - bibliography is missing
|
88
|
+
```
|
89
|
+
|
90
|
+
### Task Return Values
|
91
|
+
|
92
|
+
* A task should at minimum return a boolean value, signifying whether that task finished successfully or not
|
93
|
+
* A task can also return a string with details of the task completion
|
94
|
+
* If a task returns _false_, the execution stops and the next tasks in the chain won't be performed for current instance
|
95
|
+
|
96
|
+
### Inspecting Task Runs
|
97
|
+
|
98
|
+
You can inspect task runs results using the provided `Serially::TaskRun` model and its associated `serially_task_runs` table.
|
99
|
+
Running `Serially::TaskRun.all` for the above example, will show something like this:
|
100
|
+
```
|
101
|
+
+----+------------+---------+-----------+----------------+----------------------+---------------------+
|
102
|
+
| id | item_class | item_id | task_name | status | result_message | finished_at |
|
103
|
+
+----+------------+---------+-----------+----------------+----------------------+---------------------+
|
104
|
+
| 1 | Post | 1 | draft | finished_ok | | 2015-12-31 09:17:17 |
|
105
|
+
| 2 | Post | 1 | review | finished_ok | reviewed by staff | 2015-12-31 09:17:17 |
|
106
|
+
| 3 | Post | 2 | draft | finished_ok | | 2015-12-31 09:17:17 |
|
107
|
+
| 4 | Post | 1 | publish | finished_error | bibliography missing | 2015-12-31 09:17:17 |
|
108
|
+
| 5 | Post | 2 | review | finished_ok | | 2015-12-31 09:17:17 |
|
109
|
+
| 6 | Post | 2 | publish | finished_error | bibliography missing | 2015-12-31 09:17:17 |
|
110
|
+
+----+------------+---------+-----------+----------------+----------------------+---------------------+
|
111
|
+
```
|
112
|
+
Notice that the _promote_ task didn't run at all, since the _publish_ task that ran before it returned _false_ for both posts.
|
113
|
+
|
114
|
+
|
115
|
+
### Blocks
|
116
|
+
In addition to instance methods, you can pass a block as a task callback, and you can mix both syntaxes in your class:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
class Post < ActiveRecord::Base
|
120
|
+
include Serially
|
121
|
+
|
122
|
+
serially do
|
123
|
+
task :draft
|
124
|
+
task :review do |post|
|
125
|
+
puts "Reviewing #{post.id}"
|
126
|
+
end
|
127
|
+
task :publish do |post|
|
128
|
+
puts "Publishing #{post.id}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def draft
|
133
|
+
puts "Drafting #{self.id}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
```
|
137
|
+
|
138
|
+
## Customize Plain Ruby Class Instantiation
|
139
|
+
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
|
140
|
+
are created using simple `new`. If your class has a custom `initialize` method that you want to be called when creating instance of your class, it's easy to achieve. All you need to do is to implement
|
141
|
+
`instance_id` method that can return any number of arguments, which will be passed as-is to 'initialize`.
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
class Post
|
145
|
+
include Serially
|
146
|
+
|
147
|
+
attr_accessor :title
|
148
|
+
|
149
|
+
def initialize(title)
|
150
|
+
@title = title
|
151
|
+
end
|
152
|
+
|
153
|
+
def instance_id
|
154
|
+
@title
|
155
|
+
end
|
156
|
+
|
157
|
+
|
158
|
+
serially do
|
159
|
+
# ...
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
class Post
|
164
|
+
include Serially
|
165
|
+
|
166
|
+
attr_accessor :title
|
167
|
+
attr_accessor :author
|
168
|
+
|
169
|
+
def initialize(title, author)
|
170
|
+
@title = title
|
171
|
+
@author = author
|
172
|
+
end
|
173
|
+
|
174
|
+
def instance_id
|
175
|
+
[@title, @author]
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
serially do
|
180
|
+
# ...
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
### ActiveRecord Model Instantiation
|
186
|
+
For ActiveRecord objects, `instance_id` will return the DB id as expected, and overwriting this method isn't recommended.
|
187
|
+
|
188
|
+
|
189
|
+
## Development
|
190
|
+
|
191
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
192
|
+
|
193
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
194
|
+
|
195
|
+
## Contributing
|
196
|
+
|
197
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mikemarsian/serially.
|
198
|
+
|
199
|
+
|
200
|
+
## License
|
201
|
+
|
202
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
203
|
+
|
204
|
+
[1]: https://github.com/mikemarsian/serially-demo
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "serially"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
12
|
+
|
13
|
+
# require "irb"
|
14
|
+
# IRB.start
|
data/bin/setup
ADDED
data/circle.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
|
4
|
+
module Serially
|
5
|
+
module Generators
|
6
|
+
class InstallGenerator < ::Rails::Generators::Base
|
7
|
+
include Rails::Generators::Migration
|
8
|
+
source_root File.expand_path('../templates', __FILE__)
|
9
|
+
desc "Add the migrations for Serially"
|
10
|
+
|
11
|
+
def self.next_migration_number(path)
|
12
|
+
next_migration_number = current_migration_number(path) + 1
|
13
|
+
ActiveRecord::Migration.next_migration_number(next_migration_number)
|
14
|
+
end
|
15
|
+
|
16
|
+
def copy_migrations
|
17
|
+
migration_template "create_serially_task_runs.rb", "db/migrate/create_serially_task_runs.rb"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreateSeriallyTaskRuns < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :serially_task_runs do |t|
|
4
|
+
t.string :item_class, null: false
|
5
|
+
t.string :item_id, null: false
|
6
|
+
t.string :task_name, null: false
|
7
|
+
t.integer :status, default: 0
|
8
|
+
t.datetime :finished_at
|
9
|
+
t.text :result_message
|
10
|
+
t.timestamps null: false
|
11
|
+
end
|
12
|
+
|
13
|
+
add_index :serially_task_runs, [:item_class, :item_id]
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Serially
|
2
|
+
class InstanceBase
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def initialize(instance)
|
6
|
+
@instance = instance
|
7
|
+
@task_manager = Serially::TaskManager[instance.class]
|
8
|
+
end
|
9
|
+
|
10
|
+
# delegate instance methods to task_manager
|
11
|
+
def_delegator :@task_manager, :tasks
|
12
|
+
|
13
|
+
def start!
|
14
|
+
Serially::Worker.enqueue(@instance.class, @instance.instance_id)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Serially
|
2
|
+
|
3
|
+
def self.included(receiver)
|
4
|
+
receiver.extend Serially::ClassMethods
|
5
|
+
# remove any task_manager that might have been inherited - inclusion takes precedence
|
6
|
+
Serially::TaskManager[receiver] = nil
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# make sure inheritance works with Serially
|
12
|
+
def inherited(subclass)
|
13
|
+
Serially::TaskManager[subclass] = Serially::TaskManager[self].clone_for(subclass)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def serially(*args, &block)
|
18
|
+
options = args[0] || {}
|
19
|
+
|
20
|
+
# If TaskManager for current including class doesn't exist, create it
|
21
|
+
task_manager = Serially::TaskManager.new(self, options)
|
22
|
+
Serially::TaskManager[self] ||= task_manager
|
23
|
+
|
24
|
+
# create a new base, and resolve DSL
|
25
|
+
@serially = Serially::Base.new(task_manager)
|
26
|
+
if block
|
27
|
+
@serially.instance_eval(&block)
|
28
|
+
else
|
29
|
+
raise Serially::ConfigurationError.new("Serially is defined without a block of tasks definitions in class #{self}")
|
30
|
+
end
|
31
|
+
|
32
|
+
# return Serially::Base
|
33
|
+
@serially
|
34
|
+
end
|
35
|
+
|
36
|
+
def is_active_record?
|
37
|
+
self < ActiveRecord::Base
|
38
|
+
end
|
39
|
+
|
40
|
+
# override this to provide a custom way of creating instances of your class
|
41
|
+
def create_instance(*args)
|
42
|
+
args = args.flatten
|
43
|
+
if self.is_active_record?
|
44
|
+
if args.count == 1
|
45
|
+
args[0].is_a?(Fixnum) ? self.where(id: args[0]).first : self.where(args[0]).first
|
46
|
+
else
|
47
|
+
raise Serially::ArgumentError.new("Serially: default implementation of ::create_instance expects to receive either id or hash")
|
48
|
+
end
|
49
|
+
else
|
50
|
+
begin
|
51
|
+
args.blank? ? new : new(*args)
|
52
|
+
rescue StandardError => exc
|
53
|
+
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}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# this is the entry point for all instance-level access to Serially
|
60
|
+
def serially
|
61
|
+
@serially ||= Serially::InstanceBase.new(self)
|
62
|
+
end
|
63
|
+
|
64
|
+
# override this to provide a custom way of fetching id of your class' instance
|
65
|
+
def instance_id
|
66
|
+
self.respond_to?(:id) ? self.id : self.object_id
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Serially
|
2
|
+
class Task
|
3
|
+
|
4
|
+
attr_accessor :name, :klass, :options, :run_block
|
5
|
+
|
6
|
+
def initialize(task_name, options, task_manager, &run_block)
|
7
|
+
@name = task_name.to_sym
|
8
|
+
@klass = task_manager.klass
|
9
|
+
@options = options
|
10
|
+
@run_block = run_block
|
11
|
+
@task_manager = task_manager
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(task)
|
15
|
+
if task.is_a? Symbol
|
16
|
+
name == task
|
17
|
+
else
|
18
|
+
name == task.name && self.klass == task.klass
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def <=>(task)
|
23
|
+
if task.is_a? Symbol
|
24
|
+
name <=> task
|
25
|
+
else
|
26
|
+
name <=> task.name
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
name.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
# <i>args</i> - arguments needed to create an instance of your class. If you don't provide custom implementation for create_instance,
|
35
|
+
# pass instance_id or hash of arguments,
|
36
|
+
def run!(*args)
|
37
|
+
instance = args[0].blank? ? instance = @klass.send(:create_instance) : @klass.send(:create_instance, *args)
|
38
|
+
if instance
|
39
|
+
if !@run_block && !instance.respond_to?(@name)
|
40
|
+
raise Serially::ConfigurationError.new("Serially task #{@name} in class #{@klass} doesn't have an implementation method or a block to run")
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
status, msg = @run_block ? @run_block.call(instance) : instance.send(@name)
|
44
|
+
rescue StandardError => exc
|
45
|
+
return [false, "Serially: task '#{@name}' raised exception: #{exc.message}"]
|
46
|
+
end
|
47
|
+
else
|
48
|
+
return [false, "Serially: instance couldn't be created, task '#{@name}'' not started"]
|
49
|
+
end
|
50
|
+
# returns true (unless status == nil/false/[]) and '' (unless msg is a not empty string)
|
51
|
+
[status.present?, msg.to_s]
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Serially
|
2
|
+
class TaskManager
|
3
|
+
|
4
|
+
# the following two methods provide the storage of all task managers
|
5
|
+
def self.[](klass)
|
6
|
+
(@task_managers ||= {})[klass.to_s]
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.[]=(klass, task_manager)
|
10
|
+
(@task_managers ||= {})[klass.to_s] = task_manager
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :tasks, :options, :klass
|
14
|
+
|
15
|
+
def initialize(klass, options = {})
|
16
|
+
@klass = klass
|
17
|
+
@options = options
|
18
|
+
# Hash is ordered since Ruby 1.9
|
19
|
+
@tasks = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def clone_for(new_klass)
|
23
|
+
new_mgr = TaskManager.new(new_klass, self.options)
|
24
|
+
self.each { |task| new_mgr.add_task(task.name, task.options, &task.run_block) }
|
25
|
+
new_mgr
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def add_task(task_name, task_options, &block)
|
30
|
+
raise Serially::ConfigurationError.new("Task #{task_name} is already defined in class #{@klass}") if @tasks.include?(task_name)
|
31
|
+
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)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Allow iterating over tasks
|
36
|
+
def each
|
37
|
+
return enum_for(:each) unless block_given? # return Enumerator
|
38
|
+
|
39
|
+
@tasks.values.each do |task|
|
40
|
+
yield task
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Serially
|
4
|
+
class TaskRun < ActiveRecord::Base
|
5
|
+
enum status: { pending: 0, started: 1, started_async: 2, finished_ok: 3, finished_error: 4 }
|
6
|
+
|
7
|
+
self.table_name = 'serially_task_runs'
|
8
|
+
|
9
|
+
validates :item_class, :item_id, :task_name, presence: true
|
10
|
+
validates :task_name, uniqueness: { scope: [:item_class, :item_id] }
|
11
|
+
|
12
|
+
def self.create_from_hash!(args = {})
|
13
|
+
task_run = TaskRun.new do |t|
|
14
|
+
t.item_class = args[:item_class] if args[:item_class].present?
|
15
|
+
t.item_id = args[:item_id] if args[:item_id].present?
|
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
|
21
|
+
end
|
22
|
+
|
23
|
+
def finished?
|
24
|
+
finished_ok? || finished_error?
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.write_task_run(task, item_id, success, msg)
|
28
|
+
task_run = TaskRun.where(item_class: task.klass, item_id: item_id, task_name: task.name).first_or_initialize
|
29
|
+
if task_run.finished?
|
30
|
+
Resque.logger.warn("Serially: task '#{task.name}' for #{task.klass}/#{item_id} finished already, not saving this task run")
|
31
|
+
false
|
32
|
+
else
|
33
|
+
saved = task_run.tap {|t|
|
34
|
+
t.status = success ? TaskRun.statuses[:finished_ok] : TaskRun.statuses[:finished_error]
|
35
|
+
t.result_message = msg
|
36
|
+
t.finished_at = DateTime.now
|
37
|
+
}.save
|
38
|
+
saved
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
module Serially
|
4
|
+
class TaskRunner
|
5
|
+
include Observable
|
6
|
+
|
7
|
+
def initialize(task_run_observer = nil)
|
8
|
+
add_observer(task_run_observer) if task_run_observer
|
9
|
+
end
|
10
|
+
|
11
|
+
def run!(item_class, item_id = nil)
|
12
|
+
last_run = []
|
13
|
+
Serially::TaskManager[item_class].each do |task|
|
14
|
+
# if task.async?
|
15
|
+
# started_async_task = SerialTasksManager.begin_task(task)
|
16
|
+
# # if started async task successfully, exit the loop, otherwise go to next task
|
17
|
+
# break if started_async_task
|
18
|
+
# else
|
19
|
+
#started_async_task = false
|
20
|
+
|
21
|
+
success, msg = task.run!(item_id)
|
22
|
+
last_run = [task, success, msg]
|
23
|
+
|
24
|
+
# write result log to DB
|
25
|
+
changed
|
26
|
+
notify_observers(task, item_id, success, msg)
|
27
|
+
# if task didn't complete successfully, exit
|
28
|
+
break if !success
|
29
|
+
end
|
30
|
+
# if started_async_task
|
31
|
+
# msg = "SerialTasksManager: started async task for #{item_class}/#{item_id}. Worker is exiting..."
|
32
|
+
# else
|
33
|
+
# msg = "SerialTasksManager: no available tasks found for #{item_class}/#{item_id}. Worker is exiting..."
|
34
|
+
# end
|
35
|
+
|
36
|
+
# If we are here, it means that no more tasks were found
|
37
|
+
success = last_run[1]
|
38
|
+
msg = success ? "Serially: finished all tasks for #{item_class}/#{item_id}. Serially::Worker is exiting..." :
|
39
|
+
"Serially: task '#{last_run[0]}' for #{item_class}/#{item_id} finished with success: #{last_run[1]}, message: #{last_run[2]}"
|
40
|
+
msg
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'resque'
|
3
|
+
require 'resque-lonely_job'
|
4
|
+
|
5
|
+
module Serially
|
6
|
+
class Worker
|
7
|
+
# LonelyJob ensures that only one job a time runs per item_class/item_id (such as Invoice/34599), which
|
8
|
+
# effectively means that for every invoice there is only one job a time, which ensures invoice jobs are processed serially
|
9
|
+
extend Resque::Plugins::LonelyJob
|
10
|
+
|
11
|
+
@queue = 'serially'
|
12
|
+
def self.queue
|
13
|
+
@queue
|
14
|
+
end
|
15
|
+
|
16
|
+
# this ensures that for item_class=Invoice, and item_id=34500, only one job will run at a time
|
17
|
+
def self.redis_key(item_class, item_id, *args)
|
18
|
+
"serially:#{item_class}_#{item_id}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.perform(item_class, item_id)
|
22
|
+
Resque.logger.info("Serially: starting running tasks for #{item_class}/#{item_id}...")
|
23
|
+
item_class = item_class.constantize if item_class.is_a?(String)
|
24
|
+
writer = TaskRunWriter.new if item_class.is_active_record?
|
25
|
+
result_str = TaskRunner.new(writer).run!(item_class, item_id)
|
26
|
+
Resque.logger.info(result_str)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.enqueue(item_class, item_id)
|
30
|
+
Resque.enqueue(Serially::Worker, item_class.to_s, item_id)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.enqueue_batch(item_class, items)
|
34
|
+
items.each {|item| enqueue(item_class.to_s, item.instance_id)}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/serially.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "serially/version"
|
2
|
+
require 'serially/errors'
|
3
|
+
require 'serially/serially'
|
4
|
+
require 'serially/base'
|
5
|
+
require 'serially/instance_base'
|
6
|
+
require 'serially/task'
|
7
|
+
require 'serially/task_manager'
|
8
|
+
require 'serially/task_runner'
|
9
|
+
require 'serially/task_run'
|
10
|
+
require 'serially/task_run_writer'
|
11
|
+
require 'serially/worker'
|
data/serially.gemspec
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'serially/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "serially"
|
8
|
+
spec.version = Serially::VERSION
|
9
|
+
spec.authors = ["Mike Polischuk"]
|
10
|
+
spec.email = ["mike@polischuk.net"]
|
11
|
+
|
12
|
+
spec.summary = %q{Allows any plain ruby class or ActiveRecord model to define a series of background tasks that will be run serially, strictly one
|
13
|
+
after another, as a single, long-running resque job}
|
14
|
+
spec.homepage = "http://github.com/mikemarsian/serially"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "bin"
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency 'resque', '~> 1.2'
|
24
|
+
spec.add_dependency 'resque-lonely_job'
|
25
|
+
spec.add_dependency 'activerecord', '~> 4.2'
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", '~> 3.0'
|
30
|
+
spec.add_development_dependency "rspec_junit_formatter", '0.2.2'
|
31
|
+
spec.add_development_dependency "pry"
|
32
|
+
spec.add_development_dependency "pry-debugger"
|
33
|
+
spec.add_development_dependency 'database_cleaner'
|
34
|
+
spec.add_development_dependency 'sqlite3'
|
35
|
+
|
36
|
+
spec.description = <<desc
|
37
|
+
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.
|
38
|
+
All background jobs are scheduled using resque and Serially makes sure that for every instance of your class, only one task runs at a time.
|
39
|
+
Different instances of the same class do not interfere with each other and their tasks can run in parallel.
|
40
|
+
Serially works for both plain ruby classes and ActiveRecord models. In case of the latter, all task runs results are written to serially_tasks table which you can interrogate pragmatically using Serially::TaskRun model.
|
41
|
+
desc
|
42
|
+
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: serially
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mike Polischuk
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: resque
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: resque-lonely_job
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '4.2'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '4.2'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '10.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rspec_junit_formatter
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.2.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.2.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry-debugger
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ! '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: database_cleaner
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ! '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ! '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: sqlite3
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ! '>='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ! '>='
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
167
|
+
description: ! 'Have you ever had a class that required a series of background tasks
|
168
|
+
to run serially, strictly one after another? Than Serially is for you.
|
169
|
+
|
170
|
+
All background jobs are scheduled using resque and Serially makes sure that for
|
171
|
+
every instance of your class, only one task runs at a time.
|
172
|
+
|
173
|
+
Different instances of the same class do not interfere with each other and their
|
174
|
+
tasks can run in parallel.
|
175
|
+
|
176
|
+
Serially works for both plain ruby classes and ActiveRecord models. In case of the
|
177
|
+
latter, all task runs results are written to serially_tasks table which you can
|
178
|
+
interrogate pragmatically using Serially::TaskRun model.
|
179
|
+
|
180
|
+
'
|
181
|
+
email:
|
182
|
+
- mike@polischuk.net
|
183
|
+
executables:
|
184
|
+
- console
|
185
|
+
- setup
|
186
|
+
extensions: []
|
187
|
+
extra_rdoc_files: []
|
188
|
+
files:
|
189
|
+
- .gitignore
|
190
|
+
- .pryrc
|
191
|
+
- .rspec
|
192
|
+
- Gemfile
|
193
|
+
- LICENSE.txt
|
194
|
+
- README.md
|
195
|
+
- Rakefile
|
196
|
+
- bin/console
|
197
|
+
- bin/setup
|
198
|
+
- circle.yml
|
199
|
+
- lib/generators/serially/install/install_generator.rb
|
200
|
+
- lib/generators/serially/install/templates/create_serially_task_runs.rb
|
201
|
+
- lib/serially.rb
|
202
|
+
- lib/serially/base.rb
|
203
|
+
- lib/serially/errors.rb
|
204
|
+
- lib/serially/instance_base.rb
|
205
|
+
- lib/serially/serially.rb
|
206
|
+
- lib/serially/task.rb
|
207
|
+
- lib/serially/task_manager.rb
|
208
|
+
- lib/serially/task_run.rb
|
209
|
+
- lib/serially/task_run_writer.rb
|
210
|
+
- lib/serially/task_runner.rb
|
211
|
+
- lib/serially/version.rb
|
212
|
+
- lib/serially/worker.rb
|
213
|
+
- serially.gemspec
|
214
|
+
homepage: http://github.com/mikemarsian/serially
|
215
|
+
licenses:
|
216
|
+
- MIT
|
217
|
+
metadata: {}
|
218
|
+
post_install_message:
|
219
|
+
rdoc_options: []
|
220
|
+
require_paths:
|
221
|
+
- lib
|
222
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
223
|
+
requirements:
|
224
|
+
- - ! '>='
|
225
|
+
- !ruby/object:Gem::Version
|
226
|
+
version: '0'
|
227
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
228
|
+
requirements:
|
229
|
+
- - ! '>='
|
230
|
+
- !ruby/object:Gem::Version
|
231
|
+
version: '0'
|
232
|
+
requirements: []
|
233
|
+
rubyforge_project:
|
234
|
+
rubygems_version: 2.4.3
|
235
|
+
signing_key:
|
236
|
+
specification_version: 4
|
237
|
+
summary: Allows any plain ruby class or ActiveRecord model to define a series of background
|
238
|
+
tasks that will be run serially, strictly one after another, as a single, long-running
|
239
|
+
resque job
|
240
|
+
test_files: []
|