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