afterparty 0.0.21 → 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.
- 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
|
+
[](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
|