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.
@@ -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