qujo 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,265 @@
1
+ # Qujo
2
+
3
+ The basic idea behind Qujo came from a simple idea:
4
+
5
+ > Store information about jobs in my application domain database, rather than in Redis
6
+
7
+ Qujo wraps a Resque worker with a model class and some basic intelligence around
8
+ executing jobs and storing their output. It also provides a logger for Jobs, retry
9
+ functionality, Rails integration, Bootstrap integration, polling javascript for
10
+ displaying the running jobs and workers in a page within your app.
11
+
12
+ ## Databases
13
+
14
+ Currently, Qujo only works for Mongoid. It doesn't lend well to ActiveRecord-based
15
+ models, as it talks to the database a lot (probably more than it should).
16
+
17
+ ## Queue support
18
+
19
+ I've started some work seeing if I can get Qujo to work with Sidekiq, but I'm not sure
20
+ it's worth the effort. Sidekiq stands much better on it's own, having solved some of the
21
+ problems that I experienced with Resque. Though you still have to look somewhere
22
+ else for errors and stack traces.
23
+
24
+ ## Setup
25
+
26
+ Add Qujo to your Gemfile
27
+
28
+ ```
29
+ gem 'qujo'
30
+ ```
31
+
32
+ ### Create an initializer for Rails
33
+
34
+ ```Ruby
35
+ # config/initializers/qujo.rb
36
+ require 'qujo/queue/resque' # we're using Resque
37
+ require 'qujo/database/mongoid' # we're using Mongoid
38
+
39
+ Qujo.configure do |config|
40
+ # use Yell for our logger
41
+ config.logger = Yell.new do |l|
42
+ l.level = [:info, :warn, :error, :fatal]
43
+ l.adapter :file, File.join(Rails.root, "log", "qujo.log")
44
+ end
45
+ end
46
+ ```
47
+
48
+ ### Create a Job model
49
+
50
+ The following Job model is the parent of all Jobs that do the actual work.
51
+
52
+ ```Ruby
53
+ # app/models/job.rb
54
+ class Job
55
+ include Qujo::Database::Mongoid
56
+
57
+ include Qujo::Queue::Resque
58
+
59
+ include Qujo::Concerns::Common
60
+ include Qujo::Concerns::Logging
61
+ include Qujo::Concerns::Status
62
+
63
+ # any custom functionality shared for all jobs
64
+
65
+ end
66
+ ```
67
+
68
+ ### Mount the Qujo engine
69
+
70
+ ```Ruby
71
+ # config/routes.rb
72
+ MyApp::Application.routes.draw do
73
+ # ... normal routes
74
+
75
+ mount Qujo::Engine => "/"
76
+ end
77
+ ```
78
+
79
+ After this is configured, you can view the jobs at `localhost:3000/jobs`.
80
+
81
+ ### Bootstrap
82
+
83
+ If you're using bootstrap, there is a Qujo template to integrate some status
84
+ information into your bootstrap navigation.
85
+
86
+ ```
87
+ <header class="navbar navbar-fixed-top">
88
+ <nav class="navbar-inner">
89
+ <div class="container">
90
+ <!-- ... -->
91
+ <ul class="nav pull-right">
92
+ <!-- ... -->
93
+ <li>
94
+ <%= render 'qujo/common/bootstrap', jobs: {header: ["Jobs::Class::One", "Jobs::Class::Two"], background: ["Jobs::Class::Three"]} %>
95
+ </li>
96
+ </ul>
97
+ <!-- ... -->
98
+
99
+ ```
100
+
101
+ The data you pass to jobs should be a hash of keys and arrays. The keys are the header names, the arrays are a list
102
+ of strings of Job class names.
103
+
104
+ When this data is configured, you can kick off jobs from the UI, just by clicking on their link in the dropdown.
105
+
106
+ Color codes:
107
+
108
+ * black
109
+
110
+ > making request to server (normally it will blink black every 2 seconds)
111
+
112
+ * gray
113
+
114
+ > normal - things are good
115
+
116
+ * red
117
+
118
+ > errors - job with error, resque has failed jobs, or can't talk to resque (workers aren't running)
119
+
120
+ * orange
121
+
122
+ > after 10 minutes, the page will stop polling, just to lower the traffic to the server and not fill logs with polling
123
+
124
+ ### Create your first Job
125
+
126
+ This job inherits from the previous Job parent class and defines a work method.
127
+
128
+ ```Ruby
129
+ # app/models/jobs/hello/world.rb
130
+ class Jobs::Hello::World < Job
131
+ # the work method is where the work goes
132
+ def work
133
+ info "Hello world!"
134
+ end
135
+ end
136
+ ```
137
+
138
+ ### Integrate with your other models
139
+
140
+ Imagine that you have a model in your app that tracks purchases. You want to let
141
+ the customer know that the purchase has succeeded.
142
+
143
+ Create a Job class named `Jobs::Purchase::Notify`
144
+
145
+ ```Ruby
146
+ # app/models/jobs/purchase/notify.rb
147
+ class Jobs::Purchase::Notify < Job
148
+ def work
149
+ # notify the customer
150
+ purchase = model # model is a helper to grab the associated model
151
+ purchase.do_notify
152
+ end
153
+ end
154
+ ```
155
+
156
+ Now if you want to queue that job to run, all you need to do (from the controller, or another job)
157
+
158
+ ```Ruby
159
+ purchase.enqueue(:notify, optionshash)
160
+ ```
161
+
162
+ Qujo will take care of the rest.
163
+
164
+ How Qujo does this, it figures out the job class like so: `Jobs::<Model>::<Action>`
165
+
166
+ In the above example, `notify` is the action, so it translates this to `Jobs::Purchase::Notify`,
167
+ creates an instance, stores it in the database and tells Resque to enqueue it.
168
+
169
+ ### Passing options to jobs
170
+
171
+ ```Ruby
172
+ purchase.enqueue(:notify, optionshash)
173
+ ```
174
+
175
+ In the above, `optionshash` is just a hash of any data that you want to pass into the Job.
176
+ It is accessible from within the job with the `data` helper.
177
+
178
+ Keep in mind, because of the way that Resque stores (marshalls and unmarshalls) data in
179
+ Redis, all keys will be converted to strings.
180
+
181
+ ```Ruby
182
+ # either
183
+ purchase.enqueue(:notify, :blarg => true)
184
+
185
+ # or this
186
+ purchase.enqueue(:notify, blarg: true)
187
+
188
+ # or this
189
+ purchase.enqueue(:notify, {"blarg" => true})
190
+ # or any other hash
191
+
192
+ # will result in:
193
+ data = {
194
+ "blarg" => true
195
+ }
196
+ ```
197
+
198
+ ### The views
199
+
200
+ By default, Qujo provides some views and routes to handle querying the job models.
201
+
202
+ ## Some of the other things that Qujo provides
203
+
204
+ ### Wait
205
+
206
+ A method to have a job wait while the return value is true. I've considered inversing this,
207
+ as it probably makes more sense to wait while false.
208
+
209
+ ```Ruby
210
+ # app/models/jobs/purchase/notify.rb
211
+ class Jobs::Purchase::Notify < Job
212
+ def work
213
+ # notify the customer
214
+ purchase = model # model is a helper to grab the associated model
215
+
216
+ # this could potentially be calling another job that sets
217
+ # email#sent to true once it has completed successfully
218
+ email = purchase.do_notify
219
+
220
+ # email#sent returns true once the email has been delivered
221
+ wait do
222
+ ! email.sent
223
+ end
224
+ end
225
+ end
226
+ ```
227
+
228
+ The `wait` method supports a few options
229
+
230
+ ```Yaml
231
+ interval: integer - the length of time to sleep in seconds for each loop (default: 3)
232
+ maximum: integer - the maximum time in seconds to wait (default: 600)
233
+ ```
234
+
235
+ Keep in mind, that right now jobs are not re-entrant, so when a job is waiting, the worker
236
+ is doing nothing. If you have a lot of jobs that wait, you could end up having all of your
237
+ workers waiting for something to complete.
238
+
239
+ I have a plan to change this functionality to work around the re-entrancy problem.
240
+
241
+ ### Parent jobs
242
+
243
+ Qujo can also store parent jobs, when jobs need to run a set of things in order. The API for
244
+ this is still being worked out.
245
+
246
+ ### Workflow
247
+
248
+ Some basic workflow handling. A job transitions through the following states:
249
+
250
+ ```
251
+ new -> working -> complete
252
+ -> error -> retry => working ...
253
+ ```
254
+
255
+ ## TODO
256
+
257
+ [ ] Smarter workflows, use `workflow` gem
258
+ [ ] Job re-entrancy.
259
+ When a job waits, just schedule it 5 seconds from now and pick up where it left off.
260
+ [ ] Sidekiq support?
261
+ [ ] ActiveRecord support?
262
+
263
+ ## License
264
+
265
+ MIT-LICENSE
data/Rakefile CHANGED
@@ -16,7 +16,7 @@ RDoc::Task.new(:rdoc) do |rdoc|
16
16
  rdoc.rdoc_dir = 'rdoc'
17
17
  rdoc.title = 'Qujo'
18
18
  rdoc.options << '--line-numbers'
19
- rdoc.rdoc_files.include('README.rdoc')
19
+ rdoc.rdoc_files.include('README.md')
20
20
  rdoc.rdoc_files.include('lib/**/*.rb')
21
21
  end
22
22
 
@@ -0,0 +1,15 @@
1
+ div.log {
2
+
3
+ td.logline {
4
+ font-size: 12px;
5
+ font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
6
+ font-weight: bold;
7
+ vertical-align: top;
8
+ }
9
+
10
+ td.logline.severity {
11
+ width: 60px;
12
+ vertical-align: top;
13
+ }
14
+
15
+ }
@@ -5,12 +5,8 @@ module Qujo
5
5
 
6
6
  included do
7
7
  extend ClassMethods
8
- attr_accessor :model, :parent
9
8
 
10
9
  def run
11
- #self.load_model
12
- #self.load_parent
13
-
14
10
  self.status = :working
15
11
  self.save!
16
12
 
@@ -43,34 +39,6 @@ module Qujo
43
39
  raise "wait timeout count=#{count} interval=#{interval} maximum=#{maximum}" if ((count * interval) >= maximum)
44
40
  end
45
41
 
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
42
  def model
75
43
  @model ||= begin
76
44
  if data && data["model"] && data["model"]["id"] && data["model"]["class"]
@@ -2,7 +2,7 @@ module Qujo
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 1
5
- TINY = 2
5
+ TINY = 3
6
6
  TAG = nil
7
7
  STRING = [MAJOR, MINOR, TINY, TAG].compact.join('.')
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qujo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-08-25 00:00:00.000000000 Z
12
+ date: 2013-08-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -78,6 +78,7 @@ files:
78
78
  - app/helpers/qujo/application_helper.rb
79
79
  - app/assets/javascripts/qujo/application.js
80
80
  - app/assets/javascripts/qujo/qujo.js
81
+ - app/assets/stylesheets/qujo/qujo.css.scss
81
82
  - app/assets/stylesheets/qujo/application.css
82
83
  - config/routes.rb
83
84
  - lib/qujo/queue/resque.rb
@@ -95,7 +96,7 @@ files:
95
96
  - lib/tasks/qujo_tasks.rake
96
97
  - MIT-LICENSE
97
98
  - Rakefile
98
- - README.rdoc
99
+ - README.md
99
100
  - test/qujo_test.rb
100
101
  - test/dummy/config/locales/en.yml
101
102
  - test/dummy/config/boot.rb
@@ -140,7 +141,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
141
  version: '0'
141
142
  segments:
142
143
  - 0
143
- hash: -1828845946222427002
144
+ hash: 3602785166820259427
144
145
  required_rubygems_version: !ruby/object:Gem::Requirement
145
146
  none: false
146
147
  requirements:
@@ -149,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
149
150
  version: '0'
150
151
  segments:
151
152
  - 0
152
- hash: -1828845946222427002
153
+ hash: 3602785166820259427
153
154
  requirements: []
154
155
  rubyforge_project:
155
156
  rubygems_version: 1.8.25
@@ -1,3 +0,0 @@
1
- = Qujo
2
-
3
- This project rocks and uses MIT-LICENSE.