quebert 0.0.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.
- 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
|