stateful_jobs 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +147 -0
  5. data/Rakefile +2 -0
  6. data/app/assets/javascripts/stateful_jobs.js.coffee +65 -0
  7. data/lib/stateful_jobs.rb +14 -0
  8. data/lib/stateful_jobs/controller.rb +41 -0
  9. data/lib/stateful_jobs/engine.rb +11 -0
  10. data/lib/stateful_jobs/job.rb +9 -0
  11. data/lib/stateful_jobs/job/base.rb +22 -0
  12. data/lib/stateful_jobs/job/dispatcher.rb +31 -0
  13. data/lib/stateful_jobs/model.rb +36 -0
  14. data/lib/stateful_jobs/routing.rb +59 -0
  15. data/lib/stateful_jobs/version.rb +3 -0
  16. data/lib/stateful_jobs/view_helpers.rb +29 -0
  17. data/spec/controllers/test_runs_controller_spec.rb +47 -0
  18. data/spec/dummy/Rakefile +7 -0
  19. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  20. data/spec/dummy/app/assets/javascripts/test.js +2 -0
  21. data/spec/dummy/app/assets/javascripts/tests.js +2 -0
  22. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  23. data/spec/dummy/app/assets/stylesheets/test.css +4 -0
  24. data/spec/dummy/app/assets/stylesheets/tests.css +4 -0
  25. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  26. data/spec/dummy/app/controllers/test_runs_controller.rb +7 -0
  27. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  28. data/spec/dummy/app/helpers/tests_helper.rb +2 -0
  29. data/spec/dummy/app/jobs/execute_job.rb +6 -0
  30. data/spec/dummy/app/mailers/.gitkeep +0 -0
  31. data/spec/dummy/app/models/.gitkeep +0 -0
  32. data/spec/dummy/app/models/test_run.rb +10 -0
  33. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  34. data/spec/dummy/app/views/test/index.html.erb +2 -0
  35. data/spec/dummy/config.ru +4 -0
  36. data/spec/dummy/config/application.rb +59 -0
  37. data/spec/dummy/config/boot.rb +10 -0
  38. data/spec/dummy/config/database.yml +25 -0
  39. data/spec/dummy/config/environment.rb +5 -0
  40. data/spec/dummy/config/environments/development.rb +37 -0
  41. data/spec/dummy/config/environments/production.rb +67 -0
  42. data/spec/dummy/config/environments/test.rb +37 -0
  43. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  44. data/spec/dummy/config/initializers/inflections.rb +15 -0
  45. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  46. data/spec/dummy/config/initializers/resque.rb +1 -0
  47. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  48. data/spec/dummy/config/initializers/session_store.rb +8 -0
  49. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  50. data/spec/dummy/config/locales/en.yml +5 -0
  51. data/spec/dummy/config/routes.rb +61 -0
  52. data/spec/dummy/db/migrate/20130228143735_create_test_runs.rb +10 -0
  53. data/spec/dummy/db/schema.rb +23 -0
  54. data/spec/dummy/lib/assets/.gitkeep +0 -0
  55. data/spec/dummy/log/.gitkeep +0 -0
  56. data/spec/dummy/public/404.html +26 -0
  57. data/spec/dummy/public/422.html +26 -0
  58. data/spec/dummy/public/500.html +25 -0
  59. data/spec/dummy/public/favicon.ico +0 -0
  60. data/spec/dummy/script/rails +6 -0
  61. data/spec/lib/stateful_jobs_job_dispatcher_spec.rb +126 -0
  62. data/spec/lib/stateful_jobs_job_spec.rb +23 -0
  63. data/spec/models/test_run_spec.rb +77 -0
  64. data/spec/spec_helper.rb +35 -0
  65. data/stateful_jobs.gemspec +36 -0
  66. metadata +246 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ tmp
16
+
17
+ *.sqlite3
18
+ *.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in stateful_jobs.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Kjell Schlitt
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # StatefulJobs
2
+
3
+ StatefulJobs is a Resque based library which allows you to integrate responsive background jobs in a very easy way.
4
+ StatefulJobs wraps an ActiveRecord Model around a set of jobs and adds a polling mechanism to your frontend to get your users noticed about the state of their tasks.
5
+
6
+ Very useful for:
7
+ * background jobs which provide its state to the frontend
8
+ * background jobs which need user interaction between several steps
9
+ * a set of jobs which share process information
10
+
11
+ All these jobs can either be implemented as a separate Class or inline with just a handy Proc.
12
+
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```
19
+ $ gem 'stateful_jobs'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```
25
+ $ bundle
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```
31
+ $ gem install stateful_jobs
32
+ ```
33
+
34
+ Add the provided jQuery Plugin to your application.js:
35
+ ```
36
+ //= require stateful_jobs
37
+ ```
38
+
39
+ Or place it manually wherever your application can load it.
40
+
41
+
42
+ ## Usage
43
+
44
+ ### Model
45
+
46
+ ```
47
+ $ rails g model import current_job:string current_state:string some:string other:string attributes:integer
48
+ ```
49
+
50
+ Define your Jobs:
51
+
52
+ ```ruby
53
+ class Import < ActiveRecord::Base
54
+ include StatefulJobs::Model
55
+
56
+ # as a proc:
57
+ stateful_job :extract do |model|
58
+ puts "processing #{model}..."
59
+ # do some expensive work here...
60
+
61
+ true | false
62
+ end
63
+
64
+ # or as a separate class:
65
+ stateful_job :execute, ImportExecutionJob
66
+ end
67
+
68
+ class ImportExecutionJob < StatefulJobs::Job::Base
69
+ def perform
70
+ puts "processing #{@model}..."
71
+ # do some expensive work here...
72
+
73
+ true | false
74
+ end
75
+ end
76
+ ```
77
+
78
+ Your Model now has been equiped with following methods: `extract!`, `execute!` which automatically enqueue your job on resque.
79
+ While a job is processed its state is set to `running`. Finished job's states are set depending on their return value. A sucessfully performed job's (returns true) state becomes `done`. If a Job returns false, its state is set to `failed`. Errors raising an Exception result into an `error` state.
80
+
81
+
82
+ ### Controller
83
+
84
+ ```ruby
85
+ class ImportsController < ActiveRecord::Base
86
+ include StatefulJobs::Controller
87
+
88
+ stateful_jobs :import
89
+ end
90
+ ```
91
+
92
+ This adds a `state` member action to your controller returning the current job/state of your Model as JSON.
93
+ Additionally each state gets his own action which is called for every state change of your job. It gets the current state passed as a `current_state` parameter.
94
+
95
+ Your can sum up all these state actions into one centralized callback action if you want:
96
+
97
+ ```ruby
98
+ class ImportsController < ActiveRecord::Base
99
+ include StatefulJobs::Controller
100
+
101
+ stateful_jobs :import, action: :state_changed
102
+ end
103
+ ```
104
+
105
+ `state_changed` now gets called with current_job and current_state as parameters on every state change.
106
+
107
+
108
+ ### Routes
109
+
110
+ ```ruby
111
+ RailsApp::Application.routes.draw do
112
+ stateful_jobs :imports
113
+ end
114
+ ```
115
+
116
+ For a complete set of restful routes just use `stateful_jobs_resources :imports`.
117
+
118
+ ### View
119
+
120
+ ```erb
121
+ <%= stateful_job :imports, @import, :div, class: 'spinner', interval: 3000 do %>
122
+ spinner
123
+ <% end %>
124
+ ```
125
+
126
+ Adds the followng to your html:
127
+
128
+ ```html
129
+ <div class="spinner" id="import_1" data-id="1" data-current-job="current-job" data-current-state="current-state">
130
+ spinner
131
+ </div>
132
+ <script type="test/javascript">
133
+ $('#import_1').statefulJobs({'interval': 3000})
134
+ </script>
135
+ ```
136
+
137
+ The plugin now asks the server for a new state every 3 seconds. On state change the according state's action is invoked via ajax. If you want to be redirected instead of an ajax call, ajax can be disabled with `ajax: false` flag.
138
+ While a job is running, the css class `running` is applied to your spinner's div.
139
+
140
+
141
+ ## Contributing
142
+
143
+ 1. Fork it
144
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
145
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
146
+ 4. Push to the branch (`git push origin my-new-feature`)
147
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,65 @@
1
+ $.fn.extend
2
+ statefulJobs: (url, options = {}) ->
3
+ settings =
4
+ interval: 5000
5
+ final: 'done'
6
+ ajax: true
7
+ callback: null
8
+ action: null
9
+
10
+ settings = $.extend settings, options
11
+
12
+ return @each () ->
13
+ new StatefulJobs url, settings, $(this)
14
+
15
+ class @StatefulJobs
16
+
17
+ @spinners = {}
18
+
19
+ constructor: (@url, @options, @element) ->
20
+ @currentJob = @element.data('current-job')
21
+ @currentState = @element.data('current-state')
22
+ @element.removeData('current-job')
23
+ @element.removeData('current-state')
24
+ @options = $.extend @element.data(), @options
25
+
26
+ @timer = null
27
+
28
+ StatefulJobs.spinners[@options.id] = @
29
+
30
+ @start() unless @current == @options.final
31
+
32
+ getState: () =>
33
+ $.getJSON("#{@url}/#{@options.id}/state", @gotState)
34
+
35
+ gotState: (s) =>
36
+ if @currentJob != s.current_job or @currentState != s.current_state
37
+ @currentJob = s.current_job
38
+ @currentState = s.current_state
39
+ @element.removeClass('active')
40
+
41
+ if @options.callback
42
+ @options.callback(@)
43
+ else
44
+ if @options.action
45
+ url = "#{@url}/#{@options.id}/#{@options.action}?current_job=#{@currentJob}&current_state=#{@currentState}"
46
+ else
47
+ url = "#{@url}/#{@options.id}/#{@currentJob}?current_state=#{@currentState}"
48
+
49
+ if @options.ajax
50
+ $.getScript url
51
+ else
52
+ location.href = url
53
+ else
54
+ setTimeout @getState, @options.interval
55
+
56
+ start: ->
57
+ @element.addClass('active')
58
+ @timer = setTimeout @getState, @options.interval
59
+
60
+ stop: ->
61
+ @element.removeClass('active')
62
+ clearTimeout @timer
63
+
64
+ @get: (id) ->
65
+ StatefulJobs.spinners[id]
@@ -0,0 +1,14 @@
1
+ require 'resque'
2
+
3
+ require 'stateful_jobs/version'
4
+
5
+ require 'stateful_jobs/engine' if defined?(Rails)
6
+
7
+ require 'stateful_jobs/controller'
8
+ require 'stateful_jobs/model'
9
+ require 'stateful_jobs/job'
10
+ require 'stateful_jobs/job/base'
11
+ require 'stateful_jobs/job/dispatcher'
12
+
13
+ module StatefulJobs
14
+ end
@@ -0,0 +1,41 @@
1
+ module StatefulJobs
2
+
3
+ module Controller
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class << self
8
+ attr_accessor :stateful_jobs_class, :stateful_jobs_options
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def stateful_jobs klass, options = {}
14
+ self.stateful_jobs_class = klass.to_s.camelcase.constantize
15
+ self.stateful_jobs_options = options
16
+
17
+ if options[:action]
18
+ define_method options[:action] do
19
+ end
20
+ else
21
+ if self.stateful_jobs_class.stateful_jobs.is_a? Hash
22
+ self.stateful_jobs_class.stateful_jobs.keys.each do |job|
23
+ define_method job do
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def state
32
+ render json: stateful_jobs_class.where(id: params[:id]).limit(1).select('current_job, current_state').first.to_json
33
+ end
34
+
35
+ def stateful_jobs_class
36
+ self.class.stateful_jobs_class
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,11 @@
1
+ require 'stateful_jobs/view_helpers'
2
+ require 'stateful_jobs/routing.rb'
3
+
4
+ module StatefulJobs
5
+ class Engine < Rails::Engine
6
+ initializer "stateful_jobs.view_helpers" do
7
+ ActionView::Base.send :include, ViewHelpers
8
+ ActionDispatch::Routing::Mapper.send :include, Routing::Mapper
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module StatefulJobs
2
+ module Job
3
+
4
+ def self.enqueue model, job
5
+ Resque.enqueue Dispatcher, model.class.to_s, model.id, job
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ module StatefulJobs
2
+ module Job
3
+
4
+ class Base
5
+
6
+ attr_reader :model
7
+
8
+ def initialize m
9
+ @model = m
10
+ end
11
+
12
+ def self.perform m
13
+ self.new(m).perform
14
+ end
15
+
16
+ def perform
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ module StatefulJobs
2
+ module Job
3
+
4
+ class Dispatcher
5
+ @queue = :stateful_jobs
6
+
7
+ def self.perform model, id, job_name
8
+ begin
9
+ model_class = const_get(model)
10
+ model = model_class.find id
11
+
12
+ job = model_class.stateful_job_for(job_name)
13
+
14
+ model.current_job = job_name
15
+ model.current_state = 'running'
16
+ model.save validate: false
17
+
18
+ if job.is_a?(Class)? job.perform(model) : job.call(model)
19
+ model_class.where(id: id).update_all current_state: 'done', updated_at: Time.now
20
+ else
21
+ model_class.where(id: id).update_all current_state: 'failed', updated_at: Time.now
22
+ end
23
+ rescue Exception => e
24
+ model_class.where(id: id).update_all current_state: 'error', updated_at: Time.now
25
+ raise e
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ module StatefulJobs
2
+
3
+ module Model
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class << self
9
+ attr_accessor :stateful_jobs
10
+ end
11
+ end
12
+
13
+ def stateful_jobs
14
+ self.class.stateful_jobs.keys
15
+ end
16
+
17
+ module ClassMethods
18
+ def stateful_job name, klass = nil, &block
19
+ @stateful_jobs ||= {}
20
+ name = name.to_sym
21
+
22
+ @stateful_jobs[name] = (block_given?)? block : klass
23
+
24
+ define_method "#{name}!" do
25
+ StatefulJobs::Job.enqueue self, name
26
+ end
27
+ end
28
+
29
+ def stateful_job_for j
30
+ @stateful_jobs[j.to_sym]
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,59 @@
1
+ module StatefulJobs
2
+ module Routing
3
+ module Mapper
4
+
5
+ def stateful_jobs_resource *resources, &block
6
+ options = resources.extract_options!.dup
7
+
8
+ path_name = resources.first
9
+
10
+ if options[:controller]
11
+ controller = "#{options[:controller].camelcase}Controller".constantize
12
+ else
13
+ controller = "#{resources.first.to_s.pluralize.camelcase}Controller".constantize
14
+ end
15
+
16
+ create_stateful_jobs_routes path_name, controller
17
+
18
+ resources(*resources, &block)
19
+ end
20
+
21
+ def stateful_jobs_resources *resources, &block
22
+ options = resources.extract_options!.dup
23
+
24
+ path_name = resources.first
25
+
26
+ if options[:controller]
27
+ controller = "#{options[:controller].camelcase}Controller".constantize
28
+ else
29
+ controller = "#{resources.first.to_s.camelcase}Controller".constantize
30
+ end
31
+
32
+ create_stateful_jobs_routes path_name, controller
33
+
34
+ resources(*resources, &block)
35
+ end
36
+
37
+ def stateful_jobs controller
38
+ path_name = controller
39
+ controller = "#{controller.to_s.camelcase}Controller".constantize
40
+ create_stateful_jobs_routes path_name, controller
41
+ end
42
+
43
+ private
44
+
45
+ def create_stateful_jobs_routes path_name, controller
46
+ get "#{path_name}/:id/state", to: "#{path_name}#state", as: "#{path_name}_state".to_sym
47
+
48
+ if controller.stateful_jobs_options[:action]
49
+ get "#{path_name}/:id/#{controller.stateful_jobs_options[:action]}", to: "#{path_name}##{controller.stateful_jobs_options[:action]}", as: "#{path_name}_#{stateful_jobs_options[:action]}".to_sym
50
+ else
51
+ controller.stateful_jobs_class.stateful_jobs.keys.each do |job|
52
+ get "#{path_name}/:id/#{job}", to: "#{path_name}##{job}", as: "#{path_name}_#{job}".to_sym
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end