afterparty 0.0.21 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.travis.yml +3 -1
- data/Gemfile +3 -1
- data/README.md +57 -13
- data/afterparty.gemspec +1 -0
- data/afterparty_test.sqlite3 +0 -0
- data/app/assets/javascripts/afterparty.js.coffee +14 -0
- data/app/assets/stylesheets/afterparty.css.sass +91 -0
- data/app/controllers/afterparty/dashboard_controller.rb +44 -0
- data/app/views/afterparty/dashboard/index.html.haml +85 -0
- data/config/routes.rb +9 -0
- data/lib/afterparty.rb +51 -7
- data/lib/afterparty/afterparty_job.rb +31 -0
- data/lib/afterparty/engine.rb +7 -0
- data/lib/afterparty/job_container.rb +36 -0
- data/lib/afterparty/jobs.rb +41 -0
- data/lib/afterparty/queue.rb +53 -0
- data/lib/afterparty/queue_helpers.rb +90 -15
- data/lib/afterparty/version.rb +1 -1
- data/lib/afterparty/worker.rb +46 -0
- data/lib/generators/afterparty_generator.rb +9 -0
- data/lib/generators/templates/initializer.rb +6 -0
- data/lib/generators/templates/jobs_migration.rb +21 -0
- data/lib/tasks/tasks.rake +25 -0
- data/spec/afterparty_job_spec.rb +17 -0
- data/spec/database.yml +3 -0
- data/spec/generators/afterparty_generator_spec.rb +10 -0
- data/spec/queue_functional_spec.rb +112 -0
- data/spec/queue_helpers_spec.rb +56 -0
- data/spec/schema.rb +14 -0
- data/spec/spec_helper.rb +15 -1
- metadata +44 -5
- data/lib/afterparty/redis_queue.rb +0 -57
- data/spec/redis_queue_spec.rb +0 -98
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d17a9d413c53f3fbac4701c2c6b3019141b94f3
|
4
|
+
data.tar.gz: de82e598bc016faa80d7fa4879ca0084f372588c
|
5
5
|
!binary "U0hBNTEy":
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: abb7fba4469a92003e3a439ae9514088e5f9312547216a75386856af95cd3e6ecda7d7e0b4259556fb778bc67a4d3db0b10618a3c01a13dcd15545901ac49472
|
7
|
+
data.tar.gz: 7d475a34daa110341d5401cb08a6f1186add9a584aaffe4f0551e5337d8633e4431aca2c9b66d2c91d45e87827975d9e6f9712aa7c2ba64038925f2fbfd5b45a
|
data/.rspec
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Afterparty
|
2
2
|
|
3
|
-
|
3
|
+
[![Build Status](https://travis-ci.org/hstove/afterparty.png?branch=master)](https://travis-ci.org/hstove/afterparty)
|
4
4
|
|
5
|
-
|
5
|
+
A Rails 3 & 4 compatible queue with support for executing jobs in the future and persistence with ActiveRecord.
|
6
6
|
|
7
|
-
|
7
|
+
## Installation
|
8
8
|
|
9
9
|
Add this line to your application's Gemfile:
|
10
10
|
|
@@ -15,16 +15,11 @@ gem 'afterparty'
|
|
15
15
|
And then execute:
|
16
16
|
|
17
17
|
$ bundle
|
18
|
+
$ rails g afterparty
|
19
|
+
$ rake db:migrate
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
$ gem install afterparty
|
22
|
-
|
23
|
-
In your desired application environment, like `application.rb`:
|
24
|
-
|
25
|
-
~~~Ruby
|
26
|
-
config.queue = Afterparty::RedisQueue.new
|
27
|
-
~~~
|
21
|
+
This will create an initializer in `config/initializers/afterparty.rb`. It initializes a queue at
|
22
|
+
`Rails.configuration.queue` for you to pass jobs to.
|
28
23
|
|
29
24
|
## Usage
|
30
25
|
|
@@ -41,11 +36,60 @@ end
|
|
41
36
|
Then add it to the queue at any time.
|
42
37
|
|
43
38
|
~~~Ruby
|
44
|
-
Rails.queue << Job.new
|
39
|
+
Rails.configuration.queue << Job.new
|
45
40
|
~~~
|
46
41
|
|
47
42
|
If your job responds to an `execute_at` method, the queue will wait to process that job until the specified time.
|
48
43
|
|
44
|
+
### Running jobs
|
45
|
+
|
46
|
+
You can start a worker in a separate process for executing jobs by calling `rake jobs:work`.
|
47
|
+
|
48
|
+
### Helper jobs
|
49
|
+
|
50
|
+
Afterparty provides helper job wrappers for executing arbitrary methods or mailers.
|
51
|
+
|
52
|
+
~~~Ruby
|
53
|
+
# pass an object, method, and arguments
|
54
|
+
|
55
|
+
mailer_job = Afterparty::MailerJob.new UserMailer, :welcome, @user
|
56
|
+
mailer_job.execute_at = Time.now + 20.minutes
|
57
|
+
Rails.configuration.queue << mailer_job
|
58
|
+
|
59
|
+
job = Afterparty::BasicJob.new @user, :reset_password
|
60
|
+
Rails.configuration.queue << mailer_job
|
61
|
+
~~~
|
62
|
+
|
63
|
+
### Dashboard
|
64
|
+
|
65
|
+
This gem provides a handy dashboard for inspecting, debugging, and re-running jobs.
|
66
|
+
|
67
|
+
Visit [http://localhost:3000/afterparty/](http://localhost:3000/afterparty/) and login with
|
68
|
+
`admin` and `password`. You can change the authentication strategy in `config/initializers/afterparty.rb` to something like this:
|
69
|
+
|
70
|
+
~~~Ruby
|
71
|
+
Rails.configuration.queue.config_login do |username, password|
|
72
|
+
user = User.authenticate(username, password)
|
73
|
+
!user.nil? && user.is_admin?
|
74
|
+
end
|
75
|
+
~~~
|
76
|
+
|
77
|
+
### Unicorn configuration
|
78
|
+
|
79
|
+
If you're using Unicorn as your application server, you can run a worker thread asynchronously by adding a few lines to your `unicorn.rb`:
|
80
|
+
|
81
|
+
~~~Ruby
|
82
|
+
|
83
|
+
@jobs_pid = nil
|
84
|
+
|
85
|
+
before_fork do |server, worker|
|
86
|
+
@jobs_pid ||= spawn("bundle exec rake jobs:work")
|
87
|
+
|
88
|
+
# ... the rest of your configuration
|
89
|
+
~~~
|
90
|
+
|
91
|
+
This has the advantage of, for example, staying within Heroku's free tier by not running a worker dyno.
|
92
|
+
|
49
93
|
## Contributing
|
50
94
|
|
51
95
|
1. Fork it
|
data/afterparty.gemspec
CHANGED
Binary file
|
@@ -0,0 +1,14 @@
|
|
1
|
+
$(document).ready ->
|
2
|
+
# $('.debug').click (e) ->
|
3
|
+
# $el = $(e.target)
|
4
|
+
# $tr = $el.parents('tr')
|
5
|
+
# $tr.next().toggle()
|
6
|
+
# $tr.toggleClass('debugged')
|
7
|
+
# false
|
8
|
+
$('.job-row').click (e) ->
|
9
|
+
$el = $(e.target)
|
10
|
+
return true if $el.hasClass('job-action')
|
11
|
+
$tr = $el.parents('tr')
|
12
|
+
$tr.next().toggle()
|
13
|
+
$tr.toggleClass('debugged')
|
14
|
+
false
|
@@ -0,0 +1,91 @@
|
|
1
|
+
body
|
2
|
+
:font-family helvetica
|
3
|
+
:background-color whitesmoke
|
4
|
+
:color lighten(black,30%)
|
5
|
+
:margin 0 20px
|
6
|
+
|
7
|
+
a:visited, a
|
8
|
+
:color rgb(96, 89, 180)
|
9
|
+
:text-decoration none
|
10
|
+
|
11
|
+
span
|
12
|
+
:font-size 12px
|
13
|
+
|
14
|
+
.job-table
|
15
|
+
:border-spacing 0
|
16
|
+
:max-width 100%
|
17
|
+
:margin 0px auto
|
18
|
+
thead tr:first-child
|
19
|
+
th
|
20
|
+
:border-width 1px 1px 1px 0
|
21
|
+
&:first-child
|
22
|
+
:border-width 1px 1px 1px 1px
|
23
|
+
th:first-child
|
24
|
+
:border-top-left-radius 2px
|
25
|
+
th:last-child
|
26
|
+
:border-top-right-radius 2px
|
27
|
+
tbody tr:last-child
|
28
|
+
td:first-child
|
29
|
+
:border-bottom-left-radius 2px
|
30
|
+
td:last-child
|
31
|
+
:border-bottom-right-radius 2px
|
32
|
+
tr
|
33
|
+
td, th
|
34
|
+
:border 1px solid #ccc
|
35
|
+
:border-width 0 1px 1px 0
|
36
|
+
:background-color lighten(whitesmoke, 5%)
|
37
|
+
:padding 12px 5px
|
38
|
+
:text-align left
|
39
|
+
:vertical-align middle
|
40
|
+
:word-wrap break-word
|
41
|
+
&:nth-child(2)
|
42
|
+
:width 70%
|
43
|
+
&:first-child
|
44
|
+
:border-width 0 1px 1px 1px
|
45
|
+
&:first-child, &:last-child
|
46
|
+
:width 15% !important
|
47
|
+
&.debugged
|
48
|
+
td
|
49
|
+
:background-color rgba(255,255,0,0.25)
|
50
|
+
&.job-row
|
51
|
+
:cursor pointer
|
52
|
+
h3
|
53
|
+
:margin 0
|
54
|
+
&:hover
|
55
|
+
td
|
56
|
+
:background-color rgba(0,0,0,0.15)
|
57
|
+
.description
|
58
|
+
:color white
|
59
|
+
.debug-row
|
60
|
+
:display none
|
61
|
+
:max-width 100%
|
62
|
+
td pre
|
63
|
+
:word-wrap break-word
|
64
|
+
:white-space pre-wrap
|
65
|
+
|
66
|
+
.distance-past, .distance-future
|
67
|
+
:font-size 12px
|
68
|
+
.distance-future
|
69
|
+
:color darken(rgb(0,255,0), 20%)
|
70
|
+
.distance-past
|
71
|
+
:color rgba(255,0,0,0.6)
|
72
|
+
|
73
|
+
.description
|
74
|
+
:font-size 12px
|
75
|
+
:color #bbb
|
76
|
+
:margin-left 10px
|
77
|
+
|
78
|
+
.error-description
|
79
|
+
:color lighten(red, 10%)
|
80
|
+
|
81
|
+
.job-action
|
82
|
+
:text-decoration underline
|
83
|
+
:font-size 12px
|
84
|
+
:margin 0 2px
|
85
|
+
|
86
|
+
.job_id
|
87
|
+
:color #bbb
|
88
|
+
:font-weight 700
|
89
|
+
|
90
|
+
.notice
|
91
|
+
:color lighten(green, 10%)
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Afterparty
|
2
|
+
class DashboardController < ApplicationController
|
3
|
+
before_filter :authenticate
|
4
|
+
layout false
|
5
|
+
before_filter :find_job, only: [:run, :destroy, :run_again]
|
6
|
+
|
7
|
+
def index
|
8
|
+
@queues = Afterparty.queues
|
9
|
+
if params[:completed]
|
10
|
+
@jobs = AfterpartyJob.completed.limit(20)
|
11
|
+
else
|
12
|
+
@jobs = queue.jobs
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def run
|
17
|
+
queue.run @job
|
18
|
+
flash[:notice] = "You successfully completed job ##{@job.id}."
|
19
|
+
redirect_to afterparty_engine.dashboard_path(completed: true)
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy
|
23
|
+
@job.destroy
|
24
|
+
flash[:notice] = "You have successfully destroyed job ##{@job.id}."
|
25
|
+
redirect_to afterparty_engine.dashboard_path
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def queue
|
31
|
+
Rails.configuration.queue
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_job
|
35
|
+
@job = AfterpartyJob.find(params[:id])
|
36
|
+
end
|
37
|
+
|
38
|
+
def authenticate
|
39
|
+
authenticate_or_request_with_http_basic do |username, password|
|
40
|
+
queue.authenticate(username, password)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
!!!
|
2
|
+
%html
|
3
|
+
|
4
|
+
%head
|
5
|
+
%title== #{Rails.application.class.parent_name} Job Queue
|
6
|
+
%meta{"http-equiv"=>"Content-Type", :content=>"text/html; charset=utf-8"}
|
7
|
+
/ %meta{name: "viewport", content: "width=device-width, initial-scale=1.0"}
|
8
|
+
= stylesheet_link_tag "afterparty"
|
9
|
+
= javascript_include_tag "jquery", "afterparty"
|
10
|
+
= csrf_meta_tag
|
11
|
+
= favicon_link_tag
|
12
|
+
= yield(:head)
|
13
|
+
|
14
|
+
%body
|
15
|
+
%h1
|
16
|
+
Viewing
|
17
|
+
- if params[:completed]
|
18
|
+
= pluralize @jobs.size, "completed job"
|
19
|
+
%h3= link_to "View Job Queue", afterparty_engine.dashboard_path
|
20
|
+
- else
|
21
|
+
= pluralize @jobs.size, "job"
|
22
|
+
%h3= link_to "View Completed Jobs", afterparty_engine.dashboard_path(completed: true)
|
23
|
+
- if notice
|
24
|
+
%p.notice= notice
|
25
|
+
%table.job-table
|
26
|
+
%thead
|
27
|
+
%tr
|
28
|
+
%th Execute At
|
29
|
+
%tbody
|
30
|
+
- if @jobs.empty?
|
31
|
+
%tr
|
32
|
+
%td{colspan: 3}
|
33
|
+
%em No jobs to show...
|
34
|
+
- else
|
35
|
+
- @jobs.each do |job_container|
|
36
|
+
- job = job_container.reify
|
37
|
+
%tr.job-row
|
38
|
+
%td
|
39
|
+
%h3
|
40
|
+
- if job
|
41
|
+
= link_to job.class.to_s, "#", class: 'debug'
|
42
|
+
- if job.respond_to? :description
|
43
|
+
%span.description= job.description.html_safe
|
44
|
+
- elsif job_container.has_error
|
45
|
+
%sp.error-description= job_container.error_message
|
46
|
+
- else
|
47
|
+
%em Error marshaling job
|
48
|
+
- if job_container.completed_at
|
49
|
+
%span.distance-future= "Completed #{time_ago_in_words(job_container.completed_at)} ago"
|
50
|
+
- else
|
51
|
+
- distance = time_ago_in_words(job_container.execute_at)
|
52
|
+
- if job_container.execute_at > Time.now
|
53
|
+
%span.distance-future= "Scheduled to execute in #{distance}"
|
54
|
+
- else
|
55
|
+
%span.distance-past= "Scheduled to execute #{distance} ago"
|
56
|
+
%span.job_id== ##{job_container.id} #{job_container.queue}
|
57
|
+
- if params[:completed]
|
58
|
+
= link_to "run again", afterparty_engine.run_job_path(id: job_container.id), class: 'job-action'
|
59
|
+
- else
|
60
|
+
= link_to "run", afterparty_engine.run_job_path(id: job_container.id), class: 'job-action'
|
61
|
+
= link_to "delete", afterparty_engine.destroy_job_path(id: job_container.id), class: 'job-action'
|
62
|
+
%tr.debug-row
|
63
|
+
%td
|
64
|
+
- if job
|
65
|
+
= debug job
|
66
|
+
- else
|
67
|
+
%p
|
68
|
+
%strong YAML dump:
|
69
|
+
= job_container.job_dump
|
70
|
+
- if job_container.has_error
|
71
|
+
%p
|
72
|
+
%strong Error Message:
|
73
|
+
= job_container.error_message
|
74
|
+
- if job_container.error_backtrace
|
75
|
+
%p
|
76
|
+
%strong Error Backtrace:
|
77
|
+
= job_container.error_backtrace.gsub("\n","<br>")
|
78
|
+
- if job_container.completed_at
|
79
|
+
%p
|
80
|
+
%strong Completed At:
|
81
|
+
= job_container.completed_at.strftime("%B %d, %Y at %l:%I %P")
|
82
|
+
|
83
|
+
%p
|
84
|
+
Current Time:
|
85
|
+
= Time.now.strftime("%B %d, %Y at %l:%M %P")
|
data/config/routes.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Afterparty::Engine.routes.draw do
|
2
|
+
get "/" => "afterparty/dashboard#index", as: :dashboard
|
3
|
+
get "/run" => "afterparty/dashboard#run", as: :run_job
|
4
|
+
get "/delete" => "afterparty/dashboard#destroy", as: :destroy_job
|
5
|
+
end
|
6
|
+
|
7
|
+
Rails.application.routes.draw do
|
8
|
+
mount Afterparty::Engine, at: "afterparty", as: "afterparty_engine"
|
9
|
+
end
|
data/lib/afterparty.rb
CHANGED
@@ -1,17 +1,61 @@
|
|
1
1
|
require 'logger'
|
2
2
|
require 'afterparty/queue_helpers'
|
3
|
-
require '
|
4
|
-
require 'redis'
|
3
|
+
require 'yaml'
|
5
4
|
Dir[File.expand_path('../afterparty/*', __FILE__)].each { |f| require f }
|
6
5
|
|
7
6
|
|
8
7
|
module Afterparty
|
9
|
-
|
8
|
+
def self.clear namespace=:default
|
9
|
+
redis_call namespace, :del
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.redis_call namespace, command, *args
|
13
|
+
@@redis.send(command, redis_queue_name(namespace), *args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.redis_queue_name namespace=:default
|
17
|
+
"afterparty_#{namespace}_queue"
|
18
|
+
end
|
10
19
|
|
11
|
-
def self.
|
12
|
-
@@redis
|
20
|
+
def self.queues
|
21
|
+
# @@redis.smembers "afterparty_queues"
|
13
22
|
end
|
14
|
-
|
15
|
-
|
23
|
+
|
24
|
+
# return timestamp of :execute_at or current time
|
25
|
+
def self.queue_time job
|
26
|
+
time = job_valid?(job) ? job.execute_at : DateTime.now
|
27
|
+
end
|
28
|
+
|
29
|
+
# returns true if job has an :execute_at value
|
30
|
+
def self.job_valid? job
|
31
|
+
job.respond_to?(:execute_at) && !job.execute_at.nil?
|
16
32
|
end
|
33
|
+
|
34
|
+
def self.load(raw)
|
35
|
+
begin
|
36
|
+
# postgres converts it to utf-8
|
37
|
+
# raw.encode!("ascii")
|
38
|
+
begin
|
39
|
+
# job = Marshal.load(raw)
|
40
|
+
# job = Marshal.load(job) if String === job
|
41
|
+
return YAML.load(raw)
|
42
|
+
rescue ArgumentError => e
|
43
|
+
# lots of yaml load errors are because something that hasn't been
|
44
|
+
# required. recursively require on these errors
|
45
|
+
# Invoke the autoloader and try again if object's class is undefined
|
46
|
+
if e.message =~ /undefined class\/module (.*)$/
|
47
|
+
# puts "autoloading #{$1}"
|
48
|
+
$1.constantize rescue return nil
|
49
|
+
end
|
50
|
+
return load(raw)
|
51
|
+
end
|
52
|
+
rescue Exception => e
|
53
|
+
puts e
|
54
|
+
puts "Exception while unmarshaling a job:"
|
55
|
+
puts e.message
|
56
|
+
puts e.backtrace
|
57
|
+
return nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
17
61
|
end
|