serially 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,12 @@
1
+ .idea/*
2
+ /.bundle/
3
+ /.yardoc
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ log/*
12
+ .rvmrc
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
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --profile
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in serially.gemspec
4
+ gemspec
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
+ [![Build Status](https://circleci.com/gh/mikemarsian/serially.svg?&style=shield&circle-token=93a8f2925ebdd64032108118ef6e17eb3848d767)](https://circleci.com/gh/mikemarsian/serially)
4
+ [![Code Climate](https://codeclimate.com/github/mikemarsian/serially/badges/gpa.svg)](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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new
6
+
7
+ task :default => :spec
8
+ task :test => :spec
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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/circle.yml ADDED
@@ -0,0 +1,7 @@
1
+ machine:
2
+ pre:
3
+ - rvm use ruby-1.9.3-p551
4
+ - gem install bundler
5
+ database:
6
+ override:
7
+ - echo "no database setup"
@@ -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,12 @@
1
+ module Serially
2
+ class Base
3
+ def initialize(task_manager)
4
+ @task_manager = task_manager
5
+ end
6
+
7
+ # DSL
8
+ def task(name, task_options = {}, &block)
9
+ @task_manager.add_task(name, task_options, &block)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module Serially
2
+
3
+ class ArgumentError < RuntimeError; end
4
+
5
+ class ConfigurationError < RuntimeError; end
6
+
7
+ 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,10 @@
1
+ module Serially
2
+ class TaskRunWriter
3
+
4
+ # called by TaskRunner, after each task has finished running
5
+ def update(task, item_id, success, msg)
6
+ TaskRun.write_task_run(task, item_id, success, msg)
7
+ end
8
+
9
+ end
10
+ 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,3 @@
1
+ module Serially
2
+ VERSION = "0.1.1"
3
+ 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: []