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
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'afterparty/queue_helpers'
|
3
|
+
class AfterpartyJob < ::ActiveRecord::Base
|
4
|
+
# include Afterparty::QueueHelpers
|
5
|
+
|
6
|
+
validates_presence_of :job_dump, :execute_at, :queue
|
7
|
+
|
8
|
+
scope :incomplete, -> { where(completed: false).order("execute_at") }
|
9
|
+
scope :valid, -> { incomplete.where(execute_at: 10.years.ago..DateTime.now) }
|
10
|
+
scope :completed, -> { where(completed: true).order("execute_at desc") }
|
11
|
+
|
12
|
+
def self.make_with_job job, queue=:default
|
13
|
+
afterparty_job = AfterpartyJob.new
|
14
|
+
afterparty_job.job_dump = job.to_yaml
|
15
|
+
afterparty_job.execute_at = Afterparty.queue_time(job)
|
16
|
+
afterparty_job.queue = queue
|
17
|
+
afterparty_job.completed = false
|
18
|
+
afterparty_job.save
|
19
|
+
afterparty_job
|
20
|
+
end
|
21
|
+
|
22
|
+
def reify
|
23
|
+
Afterparty.load(job_dump)
|
24
|
+
end
|
25
|
+
|
26
|
+
def execute
|
27
|
+
if (j = reify)
|
28
|
+
j.run
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'iconv'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Afterparty
|
5
|
+
class JobContainer
|
6
|
+
attr_accessor :job, :raw, :execute_at, :job_id, :queue_name
|
7
|
+
|
8
|
+
#intialized from redis's WITHSCORES function
|
9
|
+
def initialize _raw, timestamp
|
10
|
+
@execute_at = Time.at(timestamp)
|
11
|
+
begin
|
12
|
+
@job = Afterparty.load(_raw)
|
13
|
+
@job_id = job.afterparty_job_id if @job.respond_to? :afterparty_job_id
|
14
|
+
@queue_name = job.afterparty_queue if @job.respond_to? :afterparty_queue
|
15
|
+
rescue Exception => e
|
16
|
+
ap "Error during load: #{e.message}"
|
17
|
+
@job = nil
|
18
|
+
end
|
19
|
+
@raw = _raw
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def job_class
|
24
|
+
if @job
|
25
|
+
@job.class
|
26
|
+
else
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def raw_string
|
32
|
+
ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
|
33
|
+
ic.iconv(@raw.dup + ' ')[0..-2]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Afterparty
|
2
|
+
class MailerJob
|
3
|
+
attr_accessor :execute_at, :mail, :clazz, :method, :args
|
4
|
+
def initialize clazz, method, *args
|
5
|
+
# @mail = UserMailer.welcome_email(User.find(1))
|
6
|
+
@clazz = UserMailer
|
7
|
+
@method = method
|
8
|
+
@args = args
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
@mail = @clazz.send @method, *@args
|
13
|
+
@mail.deliver
|
14
|
+
end
|
15
|
+
|
16
|
+
def description
|
17
|
+
desc = "Mailer: #{(@clazz || "nil")}."
|
18
|
+
desc << "Method: #{(@method || nil)}."
|
19
|
+
desc << "Args: #{(@args || nil)}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class BasicJob
|
24
|
+
attr_accessor :object, :method, :args
|
25
|
+
def initialize object, method, *args
|
26
|
+
@object = object
|
27
|
+
@method = method
|
28
|
+
@args = args
|
29
|
+
end
|
30
|
+
|
31
|
+
def run
|
32
|
+
@object.send(:method, *@args)
|
33
|
+
end
|
34
|
+
|
35
|
+
def description
|
36
|
+
desc = "Object: #{(@object || "nil")}."
|
37
|
+
desc << "Method: #{(@method || nil)}."
|
38
|
+
desc << "Args: #{(@args || nil)}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Afterparty
|
2
|
+
class Queue
|
3
|
+
attr_accessor :options, :temp_namespace, :login_block
|
4
|
+
include Afterparty::QueueHelpers
|
5
|
+
|
6
|
+
def initialize options={}, consumer_options={}
|
7
|
+
# @consumer = ThreadedQueueConsumer.new(self, consumer_options).start
|
8
|
+
@options = options
|
9
|
+
@options[:namespace] ||= "default"
|
10
|
+
# Afterparty.add_queue @options[:namespace]
|
11
|
+
@options[:sleep] ||= 5
|
12
|
+
@mutex = Mutex.new
|
13
|
+
@options[:logger] ||= Logger.new($stderr)
|
14
|
+
end
|
15
|
+
|
16
|
+
def push job
|
17
|
+
# @mutex.synchronize do
|
18
|
+
return nil if job.nil?
|
19
|
+
queue_name = @temp_namespace || @options[:namespace]
|
20
|
+
AfterpartyJob.make_with_job job, queue_name
|
21
|
+
# end
|
22
|
+
end
|
23
|
+
alias :<< :push
|
24
|
+
alias :eng :push
|
25
|
+
|
26
|
+
def pop
|
27
|
+
# @mutex.synchronize do
|
28
|
+
while true do
|
29
|
+
unless (_job = AfterpartyJob.valid.first).nil?
|
30
|
+
ap "poppin job"
|
31
|
+
_job.completed = true
|
32
|
+
_job.save
|
33
|
+
return _job
|
34
|
+
end
|
35
|
+
sleep(@options[:sleep])
|
36
|
+
end
|
37
|
+
# end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class TestQueue < Queue
|
42
|
+
attr_accessor :completed_jobs
|
43
|
+
|
44
|
+
def initialize opts={}, consumer_opts={}
|
45
|
+
super
|
46
|
+
@completed_jobs = []
|
47
|
+
@exceptions = []
|
48
|
+
end
|
49
|
+
def handle_exception job, exception
|
50
|
+
@exceptions << [job, exception]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -4,16 +4,18 @@ module Afterparty
|
|
4
4
|
@temp_namespace = namespace
|
5
5
|
end
|
6
6
|
|
7
|
-
def redis_queue_name
|
8
|
-
|
7
|
+
def redis_queue_name
|
8
|
+
puts (a = Afterparty.redis_queue_name(@temp_namespace || @options[:namespace]))
|
9
|
+
a
|
9
10
|
end
|
10
11
|
|
11
12
|
def clear
|
12
|
-
redis_call :del
|
13
|
+
# redis_call :del
|
14
|
+
AfterpartyJob.destroy_all
|
13
15
|
end
|
14
16
|
|
15
17
|
def redis_call command, *args
|
16
|
-
result = Afterparty.
|
18
|
+
result = Afterparty.redis_call (@temp_namespace || @options[:namespace]), command, *args
|
17
19
|
@temp_namespace = nil
|
18
20
|
result
|
19
21
|
end
|
@@ -23,37 +25,110 @@ module Afterparty
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def jobs
|
26
|
-
_jobs = redis_call(:zrange, 0, -1)
|
27
|
-
_jobs.each_with_index do |job, i|
|
28
|
-
|
29
|
-
end
|
30
|
-
_jobs
|
28
|
+
# _jobs = redis_call(:zrange, 0, -1)
|
29
|
+
# _jobs.each_with_index do |job, i|
|
30
|
+
# _jobs[i] = Marshal.load(job)
|
31
|
+
# end
|
32
|
+
# _jobs
|
33
|
+
AfterpartyJob.incomplete
|
31
34
|
end
|
32
35
|
|
33
36
|
def jobs_with_scores
|
34
|
-
redis_call
|
37
|
+
hash_from_scores(redis_call(:zrange, 0, -1, {withscores: true}))
|
35
38
|
end
|
36
39
|
|
37
40
|
def valid_jobs
|
38
|
-
redis_call :zrangebyscore, 0, Time.now.to_i
|
41
|
+
# redis_call :zrangebyscore, 0, Time.now.to_i
|
42
|
+
AfterpartyJob.valid
|
43
|
+
end
|
44
|
+
|
45
|
+
def next_valid_job
|
46
|
+
# valid_jobs.first
|
47
|
+
AfterpartyJob.valid.first
|
39
48
|
end
|
40
49
|
|
41
50
|
def jobs_empty?
|
42
|
-
count = total_jobs_count
|
43
|
-
# ap count
|
44
|
-
count == 0
|
51
|
+
# count = total_jobs_count
|
52
|
+
# # ap count
|
53
|
+
# count == 0
|
54
|
+
AfterpartyJob.valid.empty?
|
45
55
|
end
|
46
56
|
|
47
57
|
def total_jobs_count
|
48
|
-
redis_call(:zcount, "-inf", "+inf")
|
58
|
+
# redis_call(:zcount, "-inf", "+inf")
|
59
|
+
AfterpartyJob.incomplete.count
|
49
60
|
end
|
50
61
|
|
51
62
|
def redis
|
52
63
|
@@redis
|
53
64
|
end
|
54
65
|
|
66
|
+
def last_completed
|
67
|
+
# @temp_namespace = "completed"
|
68
|
+
# redis_call(:zrange, -1, -1).first
|
69
|
+
AfterpartyJob.completed.first
|
70
|
+
end
|
71
|
+
|
72
|
+
def completed
|
73
|
+
# @temp_namespace = "completed"
|
74
|
+
# redis_call(:zrange, -20, -1).reverse
|
75
|
+
AfterpartyJob.completed
|
76
|
+
end
|
77
|
+
|
78
|
+
def completed_with_scores
|
79
|
+
@temp_namespace = "completed"
|
80
|
+
hash_from_scores(redis_call(:zrange, -20, -1, withscores: true)).reverse
|
81
|
+
end
|
82
|
+
|
83
|
+
def run(job)
|
84
|
+
real_job = job.reify
|
85
|
+
if real_job
|
86
|
+
job.execute
|
87
|
+
else
|
88
|
+
job.has_error = true
|
89
|
+
job.error_message = "Error marshaling job."
|
90
|
+
end
|
91
|
+
job.completed = true
|
92
|
+
job.completed_at = DateTime.now
|
93
|
+
job.save
|
94
|
+
rescue Exception => exception
|
95
|
+
handle_exception job, exception
|
96
|
+
end
|
97
|
+
|
98
|
+
def handle_exception(job, exception)
|
99
|
+
job.completed = true
|
100
|
+
job.completed_at = DateTime.now
|
101
|
+
job.has_error = true
|
102
|
+
job.error_message = exception.message
|
103
|
+
job.error_backtrace = exception.backtrace.join("\n")
|
104
|
+
job.save
|
105
|
+
logger_message = "Job Error: #{job.inspect}\n#{exception.message}"
|
106
|
+
logger_message << "\n#{exception.backtrace.join("\n")}"
|
107
|
+
@options[:logger].error logger_message
|
108
|
+
end
|
109
|
+
|
110
|
+
# &block takes a 'username' and 'password'
|
111
|
+
# argument. return true or false
|
112
|
+
def config_login &block
|
113
|
+
@login_block = block
|
114
|
+
end
|
115
|
+
|
116
|
+
def authenticate username, password
|
117
|
+
raise 'Must set queue.config_login to use dashboard' if @login_block.nil?
|
118
|
+
@login_block.call(username, password)
|
119
|
+
end
|
120
|
+
|
121
|
+
|
55
122
|
private
|
56
123
|
|
124
|
+
def hash_from_scores raw
|
125
|
+
arr = []
|
126
|
+
raw.each do |group|
|
127
|
+
arr << Afterparty::JobContainer.new(group[0], group[1])
|
128
|
+
end
|
129
|
+
arr
|
130
|
+
end
|
131
|
+
|
57
132
|
# returns true if job has an :execute_at value
|
58
133
|
def job_valid? job
|
59
134
|
job.respond_to?(:execute_at) && !job.execute_at.nil?
|
data/lib/afterparty/version.rb
CHANGED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Afterparty
|
2
|
+
class Worker
|
3
|
+
include QueueHelpers
|
4
|
+
|
5
|
+
def initialize options = {}
|
6
|
+
@options = options
|
7
|
+
@options[:adapter] ||= :redis
|
8
|
+
@options[:namespace] ||= :default
|
9
|
+
@options[:sleep] ||= 10
|
10
|
+
@options[:logger] ||= Logger.new($stderr)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def consume
|
15
|
+
@stopped = false
|
16
|
+
# puts "starting worker with namespace [#{@options[:namespace]}]."
|
17
|
+
@thread = Thread.new {
|
18
|
+
consume_sync
|
19
|
+
}
|
20
|
+
@thread
|
21
|
+
end
|
22
|
+
|
23
|
+
def consume_next
|
24
|
+
if (job = next_valid_job)
|
25
|
+
run job
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def consume_sync
|
30
|
+
while !@stopped
|
31
|
+
job = next_valid_job
|
32
|
+
if job
|
33
|
+
puts "Executing job: #{job.id}"
|
34
|
+
run job
|
35
|
+
else
|
36
|
+
sleep(@options[:sleep])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def stop
|
42
|
+
@stopped = true
|
43
|
+
@thread.join(0) if @thread
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
class AfterpartyGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
def install
|
6
|
+
copy_file "jobs_migration.rb", "db/migrate/#{Time.now.strftime('%Y%m%d%H%M%S')}_create_afterparty_jobs.rb"
|
7
|
+
copy_file "initializer.rb", "config/initializers/afterparty.rb"
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class CreateAfterpartyJobs < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :afterparty_jobs do |t|
|
4
|
+
t.text :job_dump
|
5
|
+
t.string :queue
|
6
|
+
t.datetime :execute_at
|
7
|
+
t.boolean :completed
|
8
|
+
t.boolean :has_error
|
9
|
+
t.text :error_message
|
10
|
+
t.text :error_backtrace
|
11
|
+
t.datetime :completed_at
|
12
|
+
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.down
|
18
|
+
drop_table :afterparty_jobs
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
namespace :jobs do
|
2
|
+
require 'mail'
|
3
|
+
|
4
|
+
desc "Start a new worker"
|
5
|
+
task work: :environment do
|
6
|
+
worker = Afterparty::Worker.new
|
7
|
+
worker.consume_sync
|
8
|
+
end
|
9
|
+
|
10
|
+
# desc "Clear all jobs"
|
11
|
+
# task clear: :environment do
|
12
|
+
# Rails.configuration.queue.clear
|
13
|
+
# end
|
14
|
+
|
15
|
+
desc "List Jobs"
|
16
|
+
task list: :environment do
|
17
|
+
jobs = Rails.configuration.queue.jobs
|
18
|
+
puts "#{jobs.size} total jobs."
|
19
|
+
jobs.each do |time, job|
|
20
|
+
puts time
|
21
|
+
puts job
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe AfterpartyJob do
|
3
|
+
before :each do
|
4
|
+
AfterpartyJob.destroy_all
|
5
|
+
end
|
6
|
+
|
7
|
+
it "makes a job correctly" do
|
8
|
+
tester = test_job
|
9
|
+
tester.execute_at = Time.now + 10
|
10
|
+
job = AfterpartyJob.make_with_job tester
|
11
|
+
job.reload
|
12
|
+
(reloaded = job.reify).class.should == tester.class
|
13
|
+
reloaded.execute_at.should == tester.execute_at
|
14
|
+
job.execute_at.should == reloaded.execute_at
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|