qujo 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.