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 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: