qujo 0.1.0

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.
Files changed (61) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +38 -0
  4. data/app/assets/javascripts/qujo/application.js +15 -0
  5. data/app/assets/javascripts/qujo/qujo.js +124 -0
  6. data/app/assets/stylesheets/qujo/application.css +13 -0
  7. data/app/controllers/qujo/application_controller.rb +3 -0
  8. data/app/controllers/qujo/jobs_controller.rb +175 -0
  9. data/app/helpers/qujo/application_helper.rb +4 -0
  10. data/app/views/qujo/common/_bootstrap.html.erb +29 -0
  11. data/app/views/qujo/jobs/_form.html.erb +14 -0
  12. data/app/views/qujo/jobs/edit.html.erb +9 -0
  13. data/app/views/qujo/jobs/index.html.erb +54 -0
  14. data/app/views/qujo/jobs/new.html.erb +5 -0
  15. data/app/views/qujo/jobs/resque.html.erb +1 -0
  16. data/app/views/qujo/jobs/show.html.erb +46 -0
  17. data/config/routes.rb +13 -0
  18. data/lib/qujo.rb +34 -0
  19. data/lib/qujo/concerns/common.rb +117 -0
  20. data/lib/qujo/concerns/exceptions.rb +27 -0
  21. data/lib/qujo/concerns/logging.rb +36 -0
  22. data/lib/qujo/concerns/model.rb +36 -0
  23. data/lib/qujo/concerns/status.rb +27 -0
  24. data/lib/qujo/database/mongoid.rb +25 -0
  25. data/lib/qujo/engine.rb +6 -0
  26. data/lib/qujo/queue/resque.rb +29 -0
  27. data/lib/qujo/queue/resque/job_worker.rb +25 -0
  28. data/lib/qujo/queue/resque/schedule_worker.rb +30 -0
  29. data/lib/qujo/version.rb +9 -0
  30. data/lib/tasks/qujo_tasks.rake +4 -0
  31. data/test/dummy/README.rdoc +261 -0
  32. data/test/dummy/Rakefile +7 -0
  33. data/test/dummy/app/assets/javascripts/application.js +15 -0
  34. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  35. data/test/dummy/app/controllers/application_controller.rb +3 -0
  36. data/test/dummy/app/helpers/application_helper.rb +2 -0
  37. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/test/dummy/config.ru +4 -0
  39. data/test/dummy/config/application.rb +65 -0
  40. data/test/dummy/config/boot.rb +10 -0
  41. data/test/dummy/config/environment.rb +5 -0
  42. data/test/dummy/config/environments/development.rb +31 -0
  43. data/test/dummy/config/environments/production.rb +64 -0
  44. data/test/dummy/config/environments/test.rb +35 -0
  45. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  46. data/test/dummy/config/initializers/inflections.rb +15 -0
  47. data/test/dummy/config/initializers/mime_types.rb +5 -0
  48. data/test/dummy/config/initializers/secret_token.rb +7 -0
  49. data/test/dummy/config/initializers/session_store.rb +8 -0
  50. data/test/dummy/config/initializers/wrap_parameters.rb +10 -0
  51. data/test/dummy/config/locales/en.yml +5 -0
  52. data/test/dummy/config/routes.rb +4 -0
  53. data/test/dummy/public/404.html +26 -0
  54. data/test/dummy/public/422.html +26 -0
  55. data/test/dummy/public/500.html +25 -0
  56. data/test/dummy/public/favicon.ico +0 -0
  57. data/test/dummy/script/rails +6 -0
  58. data/test/integration/navigation_test.rb +9 -0
  59. data/test/qujo_test.rb +7 -0
  60. data/test/test_helper.rb +15 -0
  61. metadata +189 -0
@@ -0,0 +1,9 @@
1
+ <% content_for :subnav_buttons do %>
2
+ <%= link_to 'Show', @job, :class => "btn" %>
3
+ <% end %>
4
+ <% content_for :subnav_pills do %>
5
+ <%= link_to 'Back', jobs_path, :class => "btn" %>
6
+ <% end %>
7
+
8
+ <%= render 'form' %>
9
+
@@ -0,0 +1,54 @@
1
+ <div>
2
+ <div class="container">
3
+ <div class="pull-left">
4
+ <ul class="nav nav-pills">
5
+ <li class="<%= "active" if request.path == jobs_path %>"><%= link_to 'Recent', jobs_path, class: "pill" %></li>
6
+ <li class="<%= "active" if request.path == errors_jobs_path %>"><%= link_to 'Errors', errors_jobs_path, class: "pill" %></li>
7
+ <li class="<%= "active" if request.path == all_jobs_path %>"><%= link_to 'All', all_jobs_path, class: "pill" %></li>
8
+ </ul>
9
+ </div>
10
+ <div class="buttons pull-right">
11
+ <%= link_to "#", class: "btn btn-primary job_accept_all" do %>
12
+ <i class="icon-ok-sign icon-white"></i> Accept All
13
+ <% end %>
14
+ </div>
15
+ </div>
16
+ </div>
17
+
18
+ <table class="table table-condensed table-striped tablesorter">
19
+ <thead>
20
+ <tr>
21
+ <th style="width: 100px;">Status</th>
22
+ <th style="width: 100px;">Class</th>
23
+ <th>Message</th>
24
+ <th>Created</th>
25
+ <th class="links"></th>
26
+ </tr>
27
+ </thead>
28
+ <tbody>
29
+ <% @jobs.each do |job| %>
30
+ <tr>
31
+ <td><span class="<%= "text-error" if job.status == :error %>"><%= job.status %></span></td>
32
+ <td><%= job.class.name %></td>
33
+ <td><%= link_to((job.message||"none")[0..50], job_path(job)) %></td>
34
+ <td><%= job.created_at %></td>
35
+ <td class="links">
36
+ <% if [:new, :working, :waiting].include?(job.status) %>
37
+ <%= link_to "#", data: {id: job.id.to_s||"nil"}, :title => "Cancel", :class => "btn btn-mini job_cancel" do %>
38
+ <i class="icon-remove-sign"></i>
39
+ <% end %>
40
+ <% end %>
41
+ <% unless job.accepted_at %>
42
+ <%= link_to "#", data: {id: job.id.to_s||"nil"}, :title => "Accept", :class => "btn btn-mini job_accept" do %>
43
+ <i class="icon-ok-sign"></i>
44
+ <% end %>
45
+ <% end %>
46
+ <%= link_to "#", :title => "Refresh", :class => "btn btn-mini job_retry", data: {id: job.id.to_s } do %>
47
+ <i class="icon-refresh"></i>
48
+ <% end %>
49
+ </td>
50
+ </tr>
51
+ <% end %>
52
+ </tbody>
53
+ </table>
54
+
@@ -0,0 +1,5 @@
1
+ <h1>New job</h1>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <%= link_to 'Back', jobs_path %>
@@ -0,0 +1 @@
1
+ <iframe height='600' src='/resque' style='border: none' width='100%'></iframe>
@@ -0,0 +1,46 @@
1
+ <% content_for :subnav_buttons do %>
2
+ <% end %>
3
+ <% content_for :subnav_pills do %>
4
+ <%= link_to 'Back', jobs_path, class: "btn" %>
5
+ <% end %>
6
+
7
+ <table class="table table-condensed table-bordered fields-horizontal">
8
+ <tr>
9
+ <th>ID</th>
10
+ <td><%= @job.id %></td>
11
+ </tr>
12
+ <tr>
13
+ <th>Class</th>
14
+ <td><%= @job.class.name %></td>
15
+ </tr>
16
+ <tr>
17
+ <th>Status</th>
18
+ <td><%= @job.status %></td>
19
+ </tr>
20
+ <tr>
21
+ <th>Model</th>
22
+ <td>
23
+ <% if @job.model %>
24
+ <%= link_to "#{@job.model.display} (#{@job.model.class})", "#" %>
25
+ <% end %>
26
+ </td>
27
+ </tr>
28
+ <tr>
29
+ <th>Data</th>
30
+ <td>
31
+ <% if @job.data %>
32
+ <pre class="pre-scrollable"><%= @job.data.to_yaml %></pre>
33
+ <% end %></td>
34
+ </tr>
35
+ <tr>
36
+ <th>Message</th>
37
+ <td><%= @job.message %></td>
38
+ </tr>
39
+ <tr>
40
+ <th>Created</th>
41
+ <td><%= @job.created_at %></td>
42
+ </tr>
43
+ </table>
44
+
45
+ <%= render "common/log", log: @job.log, trace: @job.trace %>
46
+
@@ -0,0 +1,13 @@
1
+ Qujo::Engine.routes.draw do
2
+ resources :jobs do
3
+ post "accept", on: :member
4
+ post "refresh", on: :member
5
+ get "resque", on: :collection if defined?(Resque)
6
+ get "status", on: :collection
7
+ get "errors", on: :collection
8
+ get "all", on: :collection
9
+ end
10
+ if defined?(Resque)
11
+ mount Resque::Server.new, :at => "/resque"
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ require 'qujo/version'
2
+
3
+ require 'qujo/concerns/common'
4
+ require 'qujo/concerns/logging'
5
+ require 'qujo/concerns/model'
6
+ require 'qujo/concerns/status'
7
+
8
+ if defined?(Rails)
9
+ require "qujo/engine"
10
+ end
11
+
12
+ module Qujo
13
+ class << self
14
+ def configure
15
+ @cfg ||= OpenStruct.new({
16
+ :ui_status => true,
17
+ :logger => Rails.logger,
18
+ :models => "Jobs"
19
+ })
20
+ yield @cfg if block_given?
21
+ true
22
+ end
23
+
24
+ def config
25
+ @cfg
26
+ end
27
+
28
+ def logger
29
+ config.logger
30
+ end
31
+ end
32
+ end
33
+
34
+ Qujo.configure
@@ -0,0 +1,117 @@
1
+ module Qujo
2
+ module Concerns
3
+ module Common
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ extend ClassMethods
8
+ attr_accessor :model, :parent
9
+
10
+ def run
11
+ #self.load_model
12
+ #self.load_parent
13
+
14
+ self.status = :working
15
+ self.save!
16
+
17
+ self.work
18
+
19
+ self.status = :complete
20
+ self.save!
21
+
22
+ self.accept
23
+ rescue => e
24
+ error "JOB#RUN: error running job, attempting to save"
25
+ error " #{e.message} at #{e.backtrace.first}"
26
+ self.status = :error
27
+ self.message = e.message
28
+ self.trace = e.backtrace
29
+ self.save!
30
+ end
31
+
32
+ # wait while return value from block is true
33
+ def wait(options = {}, &block)
34
+ return unless block_given?
35
+ o = {interval: 3, maximum: 600}.merge(options)
36
+ interval = o[:interval]
37
+ maximum = o[:maximum]
38
+ count = 0
39
+ while ((count * interval) < maximum) && yield do
40
+ sleep interval
41
+ count += 1
42
+ end
43
+ raise "wait timeout count=#{count} interval=#{interval} maximum=#{maximum}" if ((count * interval) >= maximum)
44
+ end
45
+
46
+ #def model
47
+ # @model ||= begin
48
+ # if data && data["id"] && data["class"]
49
+ # begin
50
+ # c = data["class"].constantize
51
+ # c.find(data["id"]) # rescue nil #TODO: make this smarter
52
+ # #rescue Mongoid::Errors::DocumentNotFound => e
53
+ # # logger.error "document not found"
54
+ # # nil
55
+ # end
56
+ # end
57
+ # end
58
+ #end
59
+ #
60
+ #def parent
61
+ # @parent ||= begin
62
+ # if data["parent"] && data["parent"]["type"] && data["parent"]["id"]
63
+ # begin
64
+ # c = data["parent"]["type"].constantize
65
+ # c.find(data["parent"]["id"])
66
+ # #rescue Mongoid::Errors::DocumentNotFound => e
67
+ # # logger.error "document not found"
68
+ # # nil
69
+ # end
70
+ # end
71
+ # end
72
+ #end
73
+
74
+ def model
75
+ @model ||= begin
76
+ if data && data["model"] && data["model"]["id"] && data["model"]["class"]
77
+ i = data["model"]["id"]
78
+ c = data["model"]["class"].constantize
79
+ c.find(i)
80
+ end
81
+ rescue => e
82
+ logger.error "could not load model: #{e.message} at #{e.backtrace.first}"
83
+ nil
84
+ end
85
+ end
86
+
87
+ def parent
88
+ @parent ||= begin
89
+ if data && data["parent"] && data["parent"]["id"] && data["parent"]["class"]
90
+ i = data["parent"]["id"]
91
+ c = data["parent"]["class"].constantize
92
+ c.find(i)
93
+ end
94
+ rescue => e
95
+ logger.error "could not load parent: #{e.message} at #{e.backtrace.first}"
96
+ nil
97
+ end
98
+ end
99
+ end
100
+
101
+ module ClassMethods
102
+ def errors?
103
+ where(status: :error).count > 0
104
+ end
105
+
106
+ def inherited(child)
107
+ child.instance_eval do
108
+ def model_name
109
+ ::Job.model_name
110
+ end
111
+ end
112
+ super
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,27 @@
1
+ module Qujo
2
+ module Exceptions
3
+
4
+ class ResqueNotLoaded < StandardError
5
+ def initialize(msg = "Resque is undefined")
6
+ super
7
+ end
8
+ end
9
+ class ResqueSchedulerNotLoaded < StandardError
10
+ def initialize(msg = "Resque::Scheduler is undefined")
11
+ super
12
+ end
13
+ end
14
+
15
+ class MongoidUndefined < StandardError
16
+ def initialize(msg = "Mongoid is undefined")
17
+ super
18
+ end
19
+ end
20
+
21
+ class JobNameUndefined < StandardError
22
+ def initialize(msg = "Job name must match Jobs::ModelName::ActionName or be passed as the :job option")
23
+ super
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ module Qujo
2
+ module Concerns
3
+ module Logging
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def logger
8
+ @logger ||= Qujo.logger
9
+ end
10
+
11
+ def debug(msg)
12
+ _log(:debug, msg) if Rails.env.development?
13
+ end
14
+
15
+ def info(msg)
16
+ _log(:info, msg)
17
+ end
18
+
19
+ def warn(msg)
20
+ _log(:warn, msg)
21
+ end
22
+
23
+ def error(msg)
24
+ _log(:error, msg)
25
+ end
26
+
27
+ protected
28
+
29
+ def _log(sev, msg)
30
+ logger.send(sev, msg)
31
+ self.log << {severity: sev, message: msg}
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Qujo
2
+ module Concerns
3
+ module Model
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def enqueue(action, options={})
8
+ name = options[:job] || "#{Qujo.config.models}::#{self.class.name.capitalize}::#{action.capitalize}"
9
+ raise Qujo::Exceptions::JobNameUndefined unless name
10
+
11
+ begin
12
+ klass = name.constantize
13
+ rescue => e
14
+ raise Qujo::Exceptions::JobNameUndefined, e.message
15
+ end
16
+
17
+
18
+ data = {}
19
+ data = data.merge(model: {id: self.id.to_s, class: self.class.name})
20
+
21
+ parent = options.delete(:parent)
22
+ if parent && parent.is_a?(::Job)
23
+ data = data.merge(parent: {id: parent.id.to_s, class: parent.class.name})
24
+ end
25
+
26
+ data = data.merge(options)
27
+
28
+ job = klass.create(data: data)
29
+
30
+ logger.info "** ENQUEUE: JOB=#{j.id} NAME=#{n} DATA=#{d}" if self.respond_to?(:logger)
31
+ job.enqueue
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ module Qujo
2
+ module Concerns
3
+ module Status
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def accept
8
+ self.accepted_at = Time.now
9
+ self.save!
10
+ end
11
+
12
+ def cancel
13
+ self.status = :cancelled
14
+ self.save!
15
+ end
16
+
17
+ def retry
18
+ self.accepted_at = nil
19
+ self.status = :retry
20
+ self.log << "--- retry ---"
21
+ self.message = nil
22
+ self.save
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ module Qujo
2
+ module Database
3
+ module Mongoid
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ raise Qujo::Exceptions::DatabaseUndefined, "Mongoid is undefined" unless defined?(Mongoid)
8
+ include ::Mongoid::Document
9
+ include ::Mongoid::Timestamps
10
+ include ::Mongoid::Symbolize
11
+
12
+ field :data, type: Hash
13
+ symbolize :status, in: [:new, :working, :waiting, :complete, :error, :retry, :cancelled], default: "new"
14
+ field :message, type: String
15
+ field :log, type: Array, default: []
16
+ field :trace, type: Array, default: []
17
+ field :accepted_at, type: DateTime
18
+
19
+ default_scope where(accepted_at: nil)
20
+ scope :active, -> { where(:status.in => [:new, :working, :waiting, :retry, :error], accepted_at: nil) }
21
+ scope :errors, -> { where(:status.in => [:error]) }
22
+ end
23
+ end
24
+ end
25
+ end