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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NzdmYzRhYTIwNDM3YmE3ODRhOTU3YzM0Y2MwNzg2YjlmYWRkNzU4ZA==
4
+ M2ZlZTI1ZjYzZmYwMTBhZDQ5ZDZhM2U3MTMzYjVmYmQwYzUwZTdjMQ==
5
5
  data.tar.gz: !binary |-
6
- OWNlNTI3MGMxN2ZiMjE5MGQwMTA5ZWE3MWFkNjQyM2JiY2IwY2Q3Mg==
6
+ YmIzNTJmZGZhMGY1ZGZhMzY5YmUzNTZjNmI3MDRkZTU1Zjg1ZmI5YQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ZDBkMTUyNTY3ZjA5MDlmMGVkYzUyYzRmYzgxNTUxOTdmNzFjZTNiMDg3Y2Fj
10
- NTZhMmViZjRhMDE1M2M4YzViNWVlY2Q1Nzg0OGZiOGE1YTBhNTc4NjljZWE5
11
- MGI3Y2ViYzEzOTEzM2FhNWEwNmQ2YmYxODc3MTY3N2IwM2MyOGI=
9
+ YWZhYzgzMTlhY2FkMzdlMDAyMWMyYTQ1ZjIzYjhkOWZmYzE4MjZiYTRjNzRk
10
+ MWM2YjI5N2NiMjBmNTNmOTAwZGQxZDc4YTkxNWQ4NTg2YWM3NTFjY2YxNzcy
11
+ OTg1N2U0MDFjMDYwYzA1YTJhYTkxOTdiNTU0ZTIyYzBjOWQ0ZGM=
12
12
  data.tar.gz: !binary |-
13
- NmJmMjJiOGM3MDQ5ZmExYjZhZjk1N2E5YzczMDQwYTBmMjZkZDYyZmI2MjQ3
14
- YjgwZDc0NTQ2NjJlNzczYzM5ODlmNjhjNWZlZmI3Mzc2ODc1MjRmZTA2Y2Rh
15
- NjVmY2Y5Y2VkYjUzYzNkNjBiZmMxZWRlNzIxOGYwZmYwN2I3N2U=
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
- 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.
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
- 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.
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 your task runs.
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::Worker` job.
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
- ### 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:
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 Post
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
@@ -4,4 +4,6 @@ module Serially
4
4
 
5
5
  class ConfigurationError < RuntimeError; end
6
6
 
7
+ class NotSupportedError < RuntimeError; end
8
+
7
9
  end
@@ -11,10 +11,14 @@ module Serially
11
11
  def_delegator :@task_manager, :tasks
12
12
 
13
13
  def start!
14
- Serially::Worker.enqueue(@instance.class, @instance.instance_id)
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 Worker
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
- @queue
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.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)}
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
@@ -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
- task_manager = Serially::TaskManager.new(self, options)
22
- Serially::TaskManager[self] ||= task_manager
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
@@ -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
- 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
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
@@ -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::Worker is exiting..." :
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
@@ -1,3 +1,3 @@
1
1
  module Serially
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
data/lib/serially.rb CHANGED
@@ -8,4 +8,4 @@ require 'serially/task_manager'
8
8
  require 'serially/task_runner'
9
9
  require 'serially/task_run'
10
10
  require 'serially/task_run_writer'
11
- require 'serially/worker'
11
+ require 'serially/job'
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.2
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-01 00:00:00.000000000 Z
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: