quebert 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/Gemfile +14 -0
- data/LICENSE +20 -0
- data/README.rdoc +113 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/quebert.rb +29 -0
- data/lib/quebert/async_sender.rb +99 -0
- data/lib/quebert/backend.rb +11 -0
- data/lib/quebert/backend/beanstalk.rb +35 -0
- data/lib/quebert/backend/in_process.rb +14 -0
- data/lib/quebert/backend/sync.rb +11 -0
- data/lib/quebert/configuration.rb +16 -0
- data/lib/quebert/consumer.rb +9 -0
- data/lib/quebert/consumer/base.rb +21 -0
- data/lib/quebert/consumer/beanstalk.rb +30 -0
- data/lib/quebert/daemonizing.rb +144 -0
- data/lib/quebert/job.rb +62 -0
- data/lib/quebert/support.rb +28 -0
- data/lib/quebert/worker.rb +29 -0
- data/quebert.gemspec +92 -0
- data/spec/async_sender_spec.rb +85 -0
- data/spec/backend_spec.rb +70 -0
- data/spec/configuration_spec.rb +18 -0
- data/spec/consumer_spec.rb +65 -0
- data/spec/job_spec.rb +52 -0
- data/spec/jobs.rb +29 -0
- data/spec/quebert_spec.rb +8 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/worker_spec.rb +27 -0
- metadata +164 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
module Quebert
|
2
|
+
# The basic glue between a job and the specific queue implementation. This
|
3
|
+
# handles exceptions that may be thrown by the Job and how the Job should
|
4
|
+
# be put back on the queue, if at all.
|
5
|
+
module Consumer
|
6
|
+
autoload :Base, 'quebert/consumer/base'
|
7
|
+
autoload :Beanstalk, 'quebert/consumer/beanstalk'
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Quebert
|
2
|
+
module Consumer
|
3
|
+
# The most Consumer. Doesn't even accept the queue as an argument because there's nothing
|
4
|
+
# a job can do to be rescheduled, etc.
|
5
|
+
class Base
|
6
|
+
attr_reader :job
|
7
|
+
|
8
|
+
def initialize(job)
|
9
|
+
@job = job
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
begin
|
14
|
+
job.perform(*job.args)
|
15
|
+
rescue Job::Action
|
16
|
+
# Nothing to do chief!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Quebert
|
2
|
+
module Consumer
|
3
|
+
# Handle interactions between a job and a Beanstalk queue.
|
4
|
+
class Beanstalk < Base
|
5
|
+
attr_reader :beanstalk_job, :queue, :job
|
6
|
+
|
7
|
+
def initialize(beanstalk_job, queue)
|
8
|
+
@beanstalk_job, @queue = beanstalk_job, queue
|
9
|
+
@job = Job.from_json(beanstalk_job.body)
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform
|
13
|
+
begin
|
14
|
+
result = job.perform(*job.args)
|
15
|
+
beanstalk_job.delete
|
16
|
+
result
|
17
|
+
rescue Job::Delete
|
18
|
+
beanstalk_job.delete
|
19
|
+
rescue Job::Release
|
20
|
+
beanstalk_job.release
|
21
|
+
rescue Job::Bury
|
22
|
+
beanstalk_job.bury
|
23
|
+
rescue Exception => e
|
24
|
+
beanstalk_job.bury
|
25
|
+
raise e
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'etc'
|
2
|
+
require 'daemons'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Process
|
6
|
+
# Returns +true+ the process identied by +pid+ is running.
|
7
|
+
def running?(pid)
|
8
|
+
Process.getpgid(pid) != -1
|
9
|
+
rescue Errno::EPERM
|
10
|
+
true
|
11
|
+
rescue Errno::ESRCH
|
12
|
+
false
|
13
|
+
end
|
14
|
+
module_function :running?
|
15
|
+
end
|
16
|
+
|
17
|
+
module Quebert
|
18
|
+
module Daemonizable
|
19
|
+
attr_accessor :pid_file, :log_file
|
20
|
+
|
21
|
+
PidFileExist = Class.new(RuntimeError)
|
22
|
+
|
23
|
+
def self.included(base)
|
24
|
+
base.extend ClassMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
def daemonize
|
28
|
+
raise ArgumentError, 'You must specify a pid_file to daemonize' unless pid_file
|
29
|
+
|
30
|
+
remove_stale_pid_file
|
31
|
+
|
32
|
+
pwd = Dir.pwd # Current directory is changed during daemonization, so store it
|
33
|
+
# HACK we need to create the directory before daemonization to prevent a bug under 1.9
|
34
|
+
# ignoring all signals when the directory is created after daemonization.
|
35
|
+
FileUtils.mkdir_p File.dirname(pid_file)
|
36
|
+
# Daemonize.daemonize(File.expand_path(@log_file), "quebert worker")
|
37
|
+
Daemonize.daemonize(File.expand_path(@log_file), "quebert")
|
38
|
+
Dir.chdir(pwd)
|
39
|
+
write_pid_file
|
40
|
+
end
|
41
|
+
|
42
|
+
def pid
|
43
|
+
File.exist?(pid_file) ? open(pid_file).read.to_i : nil
|
44
|
+
end
|
45
|
+
|
46
|
+
# Register a proc to be called to restart the server.
|
47
|
+
def on_restart(&block)
|
48
|
+
@on_restart = block
|
49
|
+
end
|
50
|
+
|
51
|
+
# Restart the server.
|
52
|
+
def restart
|
53
|
+
if @on_restart
|
54
|
+
log '>> Restarting ...'
|
55
|
+
stop
|
56
|
+
remove_pid_file
|
57
|
+
@on_restart.call
|
58
|
+
exit!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module ClassMethods
|
63
|
+
# Send a QUIT or INT (if timeout is +0+) signal the process which
|
64
|
+
# PID is stored in +pid_file+.
|
65
|
+
# If the process is still running after +timeout+, KILL signal is
|
66
|
+
# sent.
|
67
|
+
def kill(pid_file, timeout=60)
|
68
|
+
if timeout == 0
|
69
|
+
send_signal('INT', pid_file, timeout)
|
70
|
+
else
|
71
|
+
send_signal('QUIT', pid_file, timeout)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Restart the server by sending HUP signal.
|
76
|
+
def restart(pid_file)
|
77
|
+
send_signal('HUP', pid_file)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Send a +signal+ to the process which PID is stored in +pid_file+.
|
81
|
+
def send_signal(signal, pid_file, timeout=60)
|
82
|
+
if pid = read_pid_file(pid_file)
|
83
|
+
Logging.log "Sending #{signal} signal to process #{pid} ... "
|
84
|
+
Process.kill(signal, pid)
|
85
|
+
Timeout.timeout(timeout) do
|
86
|
+
sleep 0.1 while Process.running?(pid)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
Logging.log "Can't stop process, no PID found in #{pid_file}"
|
90
|
+
end
|
91
|
+
rescue Timeout::Error
|
92
|
+
Logging.log "Timeout!"
|
93
|
+
force_kill pid_file
|
94
|
+
rescue Interrupt
|
95
|
+
force_kill pid_file
|
96
|
+
rescue Errno::ESRCH # No such process
|
97
|
+
Logging.log "process not found!"
|
98
|
+
force_kill pid_file
|
99
|
+
end
|
100
|
+
|
101
|
+
def force_kill(pid_file)
|
102
|
+
if pid = read_pid_file(pid_file)
|
103
|
+
Logging.log "Sending KILL signal to process #{pid} ... "
|
104
|
+
Process.kill("KILL", pid)
|
105
|
+
File.delete(pid_file) if File.exist?(pid_file)
|
106
|
+
else
|
107
|
+
Logging.log "Can't stop process, no PID found in #{pid_file}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def read_pid_file(file)
|
112
|
+
if File.file?(file) && pid = File.read(file)
|
113
|
+
pid.to_i
|
114
|
+
else
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
def remove_pid_file
|
122
|
+
File.delete(pid_file) if pid_file && File.exists?(pid_file)
|
123
|
+
end
|
124
|
+
|
125
|
+
def write_pid_file
|
126
|
+
log ">> Writing PID to #{pid_file}"
|
127
|
+
open(pid_file,"w") { |f| f.write(Process.pid) }
|
128
|
+
File.chmod(0644, pid_file)
|
129
|
+
end
|
130
|
+
|
131
|
+
# If PID file is stale, remove it.
|
132
|
+
def remove_stale_pid_file
|
133
|
+
if File.exist?(pid_file)
|
134
|
+
if pid && Process.running?(pid)
|
135
|
+
raise PidFileExist, "#{pid_file} already exists, seems like it's already running (process ID: #{pid}). " +
|
136
|
+
"Stop the process or delete #{pid_file}."
|
137
|
+
else
|
138
|
+
log ">> Deleting stale PID file #{pid_file}"
|
139
|
+
remove_pid_file
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
data/lib/quebert/job.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Quebert
|
4
|
+
class Job
|
5
|
+
attr_reader :args
|
6
|
+
|
7
|
+
NotImplemented = Class.new(StandardError)
|
8
|
+
|
9
|
+
Action = Class.new(Exception)
|
10
|
+
|
11
|
+
Bury = Class.new(Action)
|
12
|
+
Delete = Class.new(Action)
|
13
|
+
Release = Class.new(Action)
|
14
|
+
|
15
|
+
def initialize(args=[])
|
16
|
+
@args = args.dup.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def perform(*args)
|
20
|
+
raise NotImplemented
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.enqueue(*args)
|
24
|
+
backend.put(self, *args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_json
|
28
|
+
self.class.to_json(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.to_json(job, *args)
|
32
|
+
args, job = job.args, job.class if job.respond_to?(:args)
|
33
|
+
JSON.generate('job' => job.name, 'args' => args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.from_json(json)
|
37
|
+
if data = JSON.parse(json)
|
38
|
+
Support.constantize(data['job']).new(data['args'])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.backend=(backend)
|
43
|
+
@backend = backend
|
44
|
+
end
|
45
|
+
def self.backend
|
46
|
+
@backend || Quebert.configuration.backend
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
def delete!
|
51
|
+
raise Delete
|
52
|
+
end
|
53
|
+
|
54
|
+
def release!
|
55
|
+
raise Release
|
56
|
+
end
|
57
|
+
|
58
|
+
def bury!
|
59
|
+
raise Bury
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Quebert
|
2
|
+
module Support
|
3
|
+
# Borrowed from Rails ActiveSupport
|
4
|
+
def self.constantize(camel_cased_word) #:nodoc:
|
5
|
+
names = camel_cased_word.split('::')
|
6
|
+
names.shift if names.empty? || names.first.empty?
|
7
|
+
|
8
|
+
constant = Object
|
9
|
+
names.each do |name|
|
10
|
+
constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
|
11
|
+
end
|
12
|
+
constant
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.symbolize_keys(hash)
|
16
|
+
map_keys(hash, :to_sym)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.stringify_keys(hash)
|
20
|
+
map_keys(hash, :to_s)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def self.map_keys(hash, meth)
|
25
|
+
hash.inject({}){|h, (k,v)| h[k.send(meth)] = v; h; }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Quebert
|
4
|
+
class Worker
|
5
|
+
attr_accessor :exception_handler, :log_file, :backend
|
6
|
+
|
7
|
+
include Quebert::Daemonizable
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
yield self if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Start the worker backend and intercept exceptions if a handler is provided
|
14
|
+
def start
|
15
|
+
while job = backend.reserve do
|
16
|
+
begin
|
17
|
+
job.perform
|
18
|
+
rescue Exception => e
|
19
|
+
exception_handler ? exception_handler.call(e) : raise(e)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
def log(message)
|
26
|
+
puts message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/quebert.gemspec
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{quebert}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Brad Gessler"]
|
12
|
+
s.date = %q{2010-10-03}
|
13
|
+
s.description = %q{A worker queue framework built around beanstalkd}
|
14
|
+
s.email = %q{brad@bradgessler.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"Gemfile",
|
23
|
+
"LICENSE",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"lib/quebert.rb",
|
28
|
+
"lib/quebert/async_sender.rb",
|
29
|
+
"lib/quebert/backend.rb",
|
30
|
+
"lib/quebert/backend/beanstalk.rb",
|
31
|
+
"lib/quebert/backend/in_process.rb",
|
32
|
+
"lib/quebert/backend/sync.rb",
|
33
|
+
"lib/quebert/configuration.rb",
|
34
|
+
"lib/quebert/consumer.rb",
|
35
|
+
"lib/quebert/consumer/base.rb",
|
36
|
+
"lib/quebert/consumer/beanstalk.rb",
|
37
|
+
"lib/quebert/daemonizing.rb",
|
38
|
+
"lib/quebert/job.rb",
|
39
|
+
"lib/quebert/support.rb",
|
40
|
+
"lib/quebert/worker.rb",
|
41
|
+
"quebert.gemspec",
|
42
|
+
"spec/async_sender_spec.rb",
|
43
|
+
"spec/backend_spec.rb",
|
44
|
+
"spec/configuration_spec.rb",
|
45
|
+
"spec/consumer_spec.rb",
|
46
|
+
"spec/job_spec.rb",
|
47
|
+
"spec/jobs.rb",
|
48
|
+
"spec/quebert_spec.rb",
|
49
|
+
"spec/spec.opts",
|
50
|
+
"spec/spec_helper.rb",
|
51
|
+
"spec/worker_spec.rb"
|
52
|
+
]
|
53
|
+
s.homepage = %q{http://github.com/bradgessler/quebert}
|
54
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
55
|
+
s.require_paths = ["lib"]
|
56
|
+
s.rubygems_version = %q{1.3.7}
|
57
|
+
s.summary = %q{A worker queue framework built around beanstalkd}
|
58
|
+
s.test_files = [
|
59
|
+
"spec/async_sender_spec.rb",
|
60
|
+
"spec/backend_spec.rb",
|
61
|
+
"spec/configuration_spec.rb",
|
62
|
+
"spec/consumer_spec.rb",
|
63
|
+
"spec/job_spec.rb",
|
64
|
+
"spec/jobs.rb",
|
65
|
+
"spec/quebert_spec.rb",
|
66
|
+
"spec/spec_helper.rb",
|
67
|
+
"spec/worker_spec.rb"
|
68
|
+
]
|
69
|
+
|
70
|
+
if s.respond_to? :specification_version then
|
71
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
72
|
+
s.specification_version = 3
|
73
|
+
|
74
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
75
|
+
s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
|
76
|
+
s.add_runtime_dependency(%q<json>, [">= 0"])
|
77
|
+
s.add_runtime_dependency(%q<daemons>, [">= 0"])
|
78
|
+
s.add_runtime_dependency(%q<beanstalk-client>, [">= 0"])
|
79
|
+
else
|
80
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
81
|
+
s.add_dependency(%q<json>, [">= 0"])
|
82
|
+
s.add_dependency(%q<daemons>, [">= 0"])
|
83
|
+
s.add_dependency(%q<beanstalk-client>, [">= 0"])
|
84
|
+
end
|
85
|
+
else
|
86
|
+
s.add_dependency(%q<rspec>, [">= 1.2.9"])
|
87
|
+
s.add_dependency(%q<json>, [">= 0"])
|
88
|
+
s.add_dependency(%q<daemons>, [">= 0"])
|
89
|
+
s.add_dependency(%q<beanstalk-client>, [">= 0"])
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
describe AsyncSender::Class do
|
5
|
+
|
6
|
+
before(:all) do
|
7
|
+
@q = Backend::InProcess.new
|
8
|
+
Quebert::AsyncSender::Object::ObjectJob.backend = @q
|
9
|
+
Quebert::AsyncSender::Instance::InstanceJob.backend = @q
|
10
|
+
end
|
11
|
+
|
12
|
+
class Greeter
|
13
|
+
include AsyncSender::Class
|
14
|
+
|
15
|
+
def initialize(name)
|
16
|
+
@name = name
|
17
|
+
end
|
18
|
+
|
19
|
+
def hi(desc)
|
20
|
+
"hi #{@name}, you look #{desc}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.hi(name)
|
24
|
+
"hi #{name}!"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should async send class methods" do
|
29
|
+
Greeter.async_send(:hi, 'Jeannette')
|
30
|
+
@q.reserve.perform.should eql(Greeter.send(:hi, 'Jeannette'))
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should async send instance methods" do
|
34
|
+
Greeter.new("brad").async_send(:hi, 'stunning')
|
35
|
+
@q.reserve.perform.should eql(Greeter.new("brad").send(:hi, 'stunning'))
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe AsyncSender::ActiveRecord do
|
41
|
+
|
42
|
+
ActiveRecord::Base.establish_connection({
|
43
|
+
:adapter => 'sqlite3',
|
44
|
+
:database => ':memory:'
|
45
|
+
})
|
46
|
+
|
47
|
+
ActiveRecord::Schema.define do
|
48
|
+
create_table "users", :force => true do |t|
|
49
|
+
t.column "first_name", :text
|
50
|
+
t.column "last_name", :text
|
51
|
+
t.column "email", :text
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class User < ActiveRecord::Base
|
56
|
+
include Quebert::AsyncSender::ActiveRecord
|
57
|
+
|
58
|
+
def name
|
59
|
+
"#{first_name} #{last_name}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.email(address)
|
63
|
+
address
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
before(:all) do
|
68
|
+
@q = Backend::InProcess.new
|
69
|
+
Quebert::AsyncSender::ActiveRecord::RecordJob.backend = @q
|
70
|
+
Quebert::AsyncSender::Object::ObjectJob.backend = @q
|
71
|
+
|
72
|
+
@user = User.create!(:first_name => 'Brad', :last_name => 'Gessler', :email => 'brad@bradgessler.com')
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should async_send instance method" do
|
76
|
+
User.first.async_send(:name)
|
77
|
+
@q.reserve.perform.should eql(User.first.name)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should async_send class method" do
|
81
|
+
email = "brad@bradgessler.com"
|
82
|
+
User.async_send(:email, email)
|
83
|
+
@q.reserve.perform.should eql(email)
|
84
|
+
end
|
85
|
+
end
|