psyho-stalker 0.7.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/README.md ADDED
@@ -0,0 +1,120 @@
1
+ Stalker - a job queueing DSL for Beanstalk
2
+ ==========================================
3
+
4
+ [Beanstalkd](http://kr.github.com/beanstalkd/) is a fast, lightweight queueing backend inspired by mmemcached. The [Ruby Beanstalk client](http://beanstalk.rubyforge.org/) is a bit raw, however, so Stalker provides a thin wrapper to make job queueing from your Ruby app easy and fun.
5
+
6
+ Queueing jobs
7
+ -------------
8
+
9
+ From anywhere in your app:
10
+
11
+ require 'stalker'
12
+
13
+ Stalker.enqueue('email.send', :to => 'joe@example.com')
14
+ Stalker.enqueue('post.cleanup.all')
15
+ Stalker.enqueue('post.cleanup', :id => post.id)
16
+
17
+ Working jobs
18
+ ------------
19
+
20
+ In a standalone file, typically jobs.rb or worker.rb:
21
+
22
+ require 'stalker'
23
+ include Stalker
24
+
25
+ job 'email.send' do |args|
26
+ Pony.send(:to => args['to'], :subject => "Hello there")
27
+ end
28
+
29
+ job 'post.cleanup.all' do |args|
30
+ Post.all.each do |post|
31
+ enqueue('post.cleanup', :id => post.all)
32
+ end
33
+ end
34
+
35
+ job 'post.cleanup' do |args|
36
+ Post.find(args['id']).cleanup
37
+ end
38
+
39
+ Running
40
+ -------
41
+
42
+ First, make sure you have Beanstalkd installed and running:
43
+
44
+ $ sudo port install beanstalkd
45
+ $ beanstalkd
46
+
47
+ Stalker:
48
+
49
+ $ sudo gem install stalker
50
+
51
+ Now run a worker using the stalk binary:
52
+
53
+ $ stalk jobs.rb
54
+ Working 3 jobs: [ email.send post.cleanup.all post.cleanup ]
55
+ Working send.email (email=hello@example.com)
56
+ Finished send.email in 31ms
57
+
58
+ Stalker will log to stdout as it starts working each job, and then again when the job finishes including the ellapsed time in milliseconds.
59
+
60
+ Filter to a list of jobs you wish to run with an argument:
61
+
62
+ $ stalk jobs.rb post.cleanup.all,post.cleanup
63
+ Working 2 jobs: [ post.cleanup.all post.cleanup ]
64
+
65
+ In a production environment you may run one or more high-priority workers (limited to short/urgent jobs) and any number of regular workers (working all jobs). For example, two workers working just the email.send job, and four running all jobs:
66
+
67
+ $ for i in 1 2; do stalk jobs.rb email.send > log/urgent-worker.log 2>&1; end
68
+ $ for i in 1 2 3 4; do stalk jobs.rb > log/worker.log 2>&1; end
69
+
70
+ Error Handling
71
+ -------------
72
+
73
+ If you include an `error` block in your jobs definition, that block will be invoked when a worker encounters an error. You might use this to report errors to an external monitoring service:
74
+
75
+ error do |job_name, args, e|
76
+ Exceptional.handle(e)
77
+ end
78
+
79
+ Before filter
80
+ -------------
81
+
82
+ If you wish to run a block of code prior to any job:
83
+
84
+ before do |job|
85
+ puts "About to work #{job}"
86
+ end
87
+
88
+ Tidbits
89
+ -------
90
+
91
+ * Jobs are serialized as JSON, so you should stick to strings, integers, arrays, and hashes as arguments to jobs. e.g. don't pass full Ruby objects - use something like an ActiveRecord/MongoMapper/CouchRest id instead.
92
+ * Because there are no class definitions associated with jobs, you can queue jobs from anywhere without needing to include your full app's environment.
93
+ * If you need to change the location of your Beanstalk from the default (localhost:11300), set BEANSTALK_URL in your environment, e.g. export BEANSTALK_URL=beanstalk://example.com:11300/
94
+ * The stalk binary is just for convenience, you can also run a worker with a straight Ruby command:
95
+ $ ruby -r jobs -e Stalker.work
96
+
97
+ Running the tests
98
+ -----------------
99
+
100
+ If you wish to hack on Stalker, install these extra gems:
101
+
102
+ $ gem install contest mocha turn
103
+
104
+ Run the tests:
105
+
106
+ $ turn
107
+
108
+ Meta
109
+ ----
110
+
111
+ Created by Adam Wiggins
112
+
113
+ Patches from Jamie Cobbett, Scott Water, Keith Rarick, Mark McGranaghan, Sean Walberg
114
+
115
+ Heavily inspired by [Minion](http://github.com/orionz/minion) by Orion Henry
116
+
117
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
118
+
119
+ http://github.com/adamwiggins/stalker
120
+
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'jeweler'
2
+
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "psyho-stalker"
5
+ s.summary = "A job queueing and background workers system using Beanstalkd."
6
+ s.description = "A job queueing and background workers system using Beanstalkd. Inspired by the Minion gem."
7
+ s.author = "Adam Wiggins"
8
+ s.email = "adam@heroku.com"
9
+ s.homepage = "http://github.com/adamwiggins/stalker"
10
+ s.executables = [ "stalk" ]
11
+ s.rubyforge_project = "stalker"
12
+
13
+ s.add_dependency 'beanstalk-client'
14
+ s.add_dependency 'json_pure'
15
+
16
+ s.files = FileList["[A-Z]*", "{bin,lib}/**/*"]
17
+ end
18
+
19
+ Jeweler::GemcutterTasks.new
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.7.0
data/bin/stalk ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ STDERR.sync = STDOUT.sync = true
4
+
5
+ require File.expand_path('../../lib/stalker', __FILE__)
6
+ include Stalker
7
+
8
+ usage = "stalk <jobs.rb> [<job>[,<job>,..]]"
9
+ file = ARGV.shift or abort usage
10
+ jobs = ARGV.shift.split(',') rescue nil
11
+
12
+ file = "./#{file}" unless file.match(/^[\/.]/)
13
+
14
+ require file
15
+
16
+ trap('INT') do
17
+ puts "\rExiting"
18
+ exit
19
+ end
20
+
21
+ Stalker.work jobs
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'stalker'
3
+
4
+ Stalker.enqueue('send.email', :email => 'hello@example.com')
5
+ Stalker.enqueue('cleanup.strays')
data/examples/jobs.rb ADDED
@@ -0,0 +1,11 @@
1
+ job 'send.email' do |args|
2
+ log "Sending email to #{args['email']}"
3
+ end
4
+
5
+ job 'transform.image' do |args|
6
+ log "Image transform"
7
+ end
8
+
9
+ job 'cleanup.strays' do |args|
10
+ log "Cleaning up"
11
+ end
data/lib/stalker.rb ADDED
@@ -0,0 +1,177 @@
1
+ require 'beanstalk-client'
2
+ require 'json'
3
+ require 'uri'
4
+ require 'timeout'
5
+
6
+ module Stalker
7
+ extend self
8
+
9
+ def connect(url)
10
+ @@url = url
11
+ beanstalk
12
+ end
13
+
14
+ def enqueue(job, args={}, opts={})
15
+ pri = opts[:pri] || 65536
16
+ delay = opts[:delay] || 0
17
+ ttr = opts[:ttr] || 120
18
+ beanstalk.use job
19
+ beanstalk.put [ job, args ].to_json, pri, delay, ttr
20
+ rescue Beanstalk::NotConnected => e
21
+ failed_connection(e)
22
+ end
23
+
24
+ def job(j, &block)
25
+ @@handlers ||= {}
26
+ @@handlers[j] = block
27
+ end
28
+
29
+ def before(&block)
30
+ @@before_handlers ||= []
31
+ @@before_handlers << block
32
+ end
33
+
34
+ def error(&blk)
35
+ @@error_handler = blk
36
+ end
37
+
38
+ class NoJobsDefined < RuntimeError; end
39
+ class NoSuchJob < RuntimeError; end
40
+
41
+ def prep(jobs=nil)
42
+ raise NoJobsDefined unless defined?(@@handlers)
43
+ @@error_handler = nil unless defined?(@@error_handler)
44
+
45
+ jobs ||= all_jobs
46
+
47
+ jobs.each do |job|
48
+ raise(NoSuchJob, job) unless @@handlers[job]
49
+ end
50
+
51
+ log "Working #{jobs.size} jobs: [ #{jobs.join(' ')} ]"
52
+
53
+ jobs.each { |job| beanstalk.watch(job) }
54
+
55
+ beanstalk.list_tubes_watched.each do |server, tubes|
56
+ tubes.each { |tube| beanstalk.ignore(tube) unless jobs.include?(tube) }
57
+ end
58
+ rescue Beanstalk::NotConnected => e
59
+ failed_connection(e)
60
+ end
61
+
62
+ def work(jobs=nil)
63
+ prep(jobs)
64
+ loop { work_one_job }
65
+ end
66
+
67
+ class JobTimeout < RuntimeError; end
68
+
69
+ def work_one_job
70
+ job = beanstalk.reserve
71
+ name, args = JSON.parse job.body
72
+ log_job_begin(name, args)
73
+ handler = @@handlers[name]
74
+ raise(NoSuchJob, name) unless handler
75
+
76
+ begin
77
+ Timeout::timeout(job.ttr - 1) do
78
+ if defined? @@before_handlers and @@before_handlers.respond_to? :each
79
+ @@before_handlers.each do |block|
80
+ block.call(name)
81
+ end
82
+ end
83
+ handler.call(args)
84
+ end
85
+ rescue Timeout::Error
86
+ raise JobTimeout, "#{name} hit #{job.ttr-1}s timeout"
87
+ end
88
+
89
+ job.delete
90
+ log_job_end(name)
91
+ rescue Beanstalk::NotConnected => e
92
+ failed_connection(e)
93
+ rescue SystemExit
94
+ raise
95
+ rescue => e
96
+ log_error exception_message(e)
97
+ job.bury rescue nil
98
+ log_job_end(name, 'failed')
99
+ error_handler.call(name, args, e) if error_handler
100
+ end
101
+
102
+ def failed_connection(e)
103
+ log_error exception_message(e)
104
+ log_error "*** Failed connection to #{beanstalk_url}"
105
+ log_error "*** Check that beanstalkd is running (or set a different BEANSTALK_URL)"
106
+ exit 1
107
+ end
108
+
109
+ def log_job_begin(name, args)
110
+ args_flat = unless args.empty?
111
+ '(' + args.inject([]) do |accum, (key,value)|
112
+ accum << "#{key}=#{value}"
113
+ end.join(' ') + ')'
114
+ else
115
+ ''
116
+ end
117
+
118
+ log [ "Working", name, args_flat ].join(' ')
119
+ @job_begun = Time.now
120
+ end
121
+
122
+ def log_job_end(name, failed=false)
123
+ ellapsed = Time.now - @job_begun
124
+ ms = (ellapsed.to_f * 1000).to_i
125
+ log "Finished #{name} in #{ms}ms #{failed ? ' (failed)' : ''}"
126
+ end
127
+
128
+ def log(msg)
129
+ puts msg
130
+ end
131
+
132
+ def log_error(msg)
133
+ STDERR.puts msg
134
+ end
135
+
136
+ def beanstalk
137
+ @@beanstalk ||= Beanstalk::Pool.new([ beanstalk_host_and_port ])
138
+ end
139
+
140
+ def beanstalk_url
141
+ return @@url if defined?(@@url) and @@url
142
+ ENV['BEANSTALK_URL'] || 'beanstalk://localhost/'
143
+ end
144
+
145
+ class BadURL < RuntimeError; end
146
+
147
+ def beanstalk_host_and_port
148
+ uri = URI.parse(beanstalk_url)
149
+ raise(BadURL, beanstalk_url) if uri.scheme != 'beanstalk'
150
+ return "#{uri.host}:#{uri.port || 11300}"
151
+ end
152
+
153
+ def exception_message(e)
154
+ msg = [ "Exception #{e.class} -> #{e.message}" ]
155
+
156
+ base = File.expand_path(Dir.pwd) + '/'
157
+ e.backtrace.each do |t|
158
+ msg << " #{File.expand_path(t).gsub(/#{base}/, '')}"
159
+ end
160
+
161
+ msg.join("\n")
162
+ end
163
+
164
+ def all_jobs
165
+ @@handlers.keys
166
+ end
167
+
168
+ def error_handler
169
+ @@error_handler
170
+ end
171
+
172
+ def clear!
173
+ @@handlers = nil
174
+ @@before_handlers = nil
175
+ @@error_handler = nil
176
+ end
177
+ end
@@ -0,0 +1,101 @@
1
+ require File.expand_path('../../lib/stalker', __FILE__)
2
+ require 'contest'
3
+ require 'mocha'
4
+
5
+ module Stalker
6
+ def log(msg); end
7
+ def log_error(msg); end
8
+ end
9
+
10
+ class StalkerTest < Test::Unit::TestCase
11
+ setup do
12
+ Stalker.clear!
13
+ $result = -1
14
+ $handled = false
15
+ end
16
+
17
+ def with_an_error_handler
18
+ Stalker.error do |job_name, args, e|
19
+ $handled = e.class
20
+ $job_name = job_name
21
+ $job_args = args
22
+ end
23
+ end
24
+
25
+ test "enqueue and work a job" do
26
+ val = rand(999999)
27
+ Stalker.job('my.job') { |args| $result = args['val'] }
28
+ Stalker.enqueue('my.job', :val => val)
29
+ Stalker.prep
30
+ Stalker.work_one_job
31
+ assert_equal val, $result
32
+ end
33
+
34
+ test "invoke error handler when defined" do
35
+ with_an_error_handler
36
+ Stalker.job('my.job') { |args| fail }
37
+ Stalker.enqueue('my.job', :foo => 123)
38
+ Stalker.prep
39
+ Stalker.work_one_job
40
+ assert $handled
41
+ assert_equal 'my.job', $job_name
42
+ assert_equal({'foo' => 123}, $job_args)
43
+ end
44
+
45
+ test "continue working when error handler not defined" do
46
+ Stalker.job('my.job') { fail }
47
+ Stalker.enqueue('my.job')
48
+ Stalker.prep
49
+ Stalker.work_one_job
50
+ assert_equal false, $handled
51
+ end
52
+
53
+ test "exception raised one second before beanstalk ttr reached" do
54
+ with_an_error_handler
55
+ Stalker.job('my.job') { sleep(3); $handled = "didn't time out" }
56
+ Stalker.enqueue('my.job', {}, :ttr => 2)
57
+ Stalker.prep
58
+ Stalker.work_one_job
59
+ assert_equal Stalker::JobTimeout, $handled
60
+ end
61
+
62
+ test "before filter gets run first" do
63
+ Stalker.before { |name| $flag = "i_was_here" }
64
+ Stalker.job('my.job') { |args| $handled = ($flag == 'i_was_here') }
65
+ Stalker.enqueue('my.job')
66
+ Stalker.prep
67
+ Stalker.work_one_job
68
+ assert_equal true, $handled
69
+ end
70
+
71
+ test "before filter passes the name of the job" do
72
+ Stalker.before { |name| $jobname = name }
73
+ Stalker.job('my.job') { true }
74
+ Stalker.enqueue('my.job')
75
+ Stalker.prep
76
+ Stalker.work_one_job
77
+ assert_equal 'my.job', $jobname
78
+ end
79
+
80
+ test "before filter can pass an instance var" do
81
+ Stalker.before { |name| @foo = "hello" }
82
+ Stalker.job('my.job') { |args| $handled = (@foo == "hello") }
83
+ Stalker.enqueue('my.job')
84
+ Stalker.prep
85
+ Stalker.work_one_job
86
+ assert_equal true, $handled
87
+ end
88
+
89
+ test "before filter invokes error handler when defined" do
90
+ with_an_error_handler
91
+ Stalker.before { |name| fail }
92
+ Stalker.job('my.job') { }
93
+ Stalker.enqueue('my.job', :foo => 123)
94
+ Stalker.prep
95
+ Stalker.work_one_job
96
+ assert $handled
97
+ assert_equal 'my.job', $job_name
98
+ assert_equal({'foo' => 123}, $job_args)
99
+ end
100
+
101
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: psyho-stalker
3
+ version: !ruby/object:Gem::Version
4
+ hash: 3
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 7
9
+ - 0
10
+ version: 0.7.0
11
+ platform: ruby
12
+ authors:
13
+ - Adam Wiggins
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-16 00:00:00 +01:00
19
+ default_executable: stalk
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: beanstalk-client
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: json_pure
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: A job queueing and background workers system using Beanstalkd. Inspired by the Minion gem.
50
+ email: adam@heroku.com
51
+ executables:
52
+ - stalk
53
+ extensions: []
54
+
55
+ extra_rdoc_files:
56
+ - README.md
57
+ files:
58
+ - README.md
59
+ - Rakefile
60
+ - VERSION
61
+ - bin/stalk
62
+ - lib/stalker.rb
63
+ - examples/enqueue.rb
64
+ - examples/jobs.rb
65
+ - test/stalker_test.rb
66
+ has_rdoc: true
67
+ homepage: http://github.com/adamwiggins/stalker
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options: []
72
+
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ hash: 3
81
+ segments:
82
+ - 0
83
+ version: "0"
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 3
90
+ segments:
91
+ - 0
92
+ version: "0"
93
+ requirements: []
94
+
95
+ rubyforge_project: stalker
96
+ rubygems_version: 1.3.7
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: A job queueing and background workers system using Beanstalkd.
100
+ test_files:
101
+ - examples/enqueue.rb
102
+ - examples/jobs.rb
103
+ - test/stalker_test.rb