quebert 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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