stalker-be 0.1.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,139 @@
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. Stalker originally is a thin wrapper around the quite raw [Ruby Beanstalk client](http://beanstalk.rubyforge.org/), however, I updated it to use its successor [Beaneater](http://beanstalkd.github.com/beaneater/).
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
+
18
+ Job Status
19
+ ----------
20
+
21
+ The current status of a job can be checked via
22
+
23
+ Stalker.status ‹job_id›
24
+
25
+ The ‹job_id› is a String or Integer. It's returned from Stalker.enqueue.
26
+
27
+
28
+ Working jobs
29
+ ------------
30
+
31
+ In a standalone file, typically jobs.rb or worker.rb:
32
+
33
+ require 'stalker'
34
+ include Stalker
35
+
36
+ job 'email.send' do |args|
37
+ Pony.send(:to => args['to'], :subject => "Hello there")
38
+ end
39
+
40
+ job 'post.cleanup.all' do |args|
41
+ Post.all.each do |post|
42
+ enqueue('post.cleanup', :id => post.id)
43
+ end
44
+ end
45
+
46
+ job 'post.cleanup' do |args|
47
+ Post.find(args['id']).cleanup
48
+ end
49
+
50
+ Running
51
+ -------
52
+
53
+ First, make sure you have Beanstalkd installed and running:
54
+
55
+ $ sudo port install beanstalkd
56
+ $ beanstalkd
57
+
58
+ Stalker - with Bundler:
59
+
60
+ gem 'stalker', github: 'nkoehring/stalker'
61
+
62
+ Stalker - with Bundler:
63
+ $ git clone git://github.com/nkoehring/stalker.git
64
+ $ cd stalker
65
+ $ gem build stalker.gemspec
66
+ $ gem install stalker-<version>.gem
67
+
68
+
69
+ Now run a worker using the stalk binary:
70
+
71
+ $ stalk jobs.rb
72
+ Working 3 jobs: [ email.send post.cleanup.all post.cleanup ]
73
+ Working send.email (email=hello@example.com)
74
+ Finished send.email in 31ms
75
+
76
+ Stalker will log to stdout as it starts working each job, and then again when the job finishes including the ellapsed time in milliseconds.
77
+
78
+ Filter to a list of jobs you wish to run with an argument:
79
+
80
+ $ stalk jobs.rb post.cleanup.all,post.cleanup
81
+ Working 2 jobs: [ post.cleanup.all post.cleanup ]
82
+
83
+ 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:
84
+
85
+ $ for i in 1 2; do stalk jobs.rb email.send > log/urgent-worker.log 2>&1; done
86
+ $ for i in 1 2 3 4; do stalk jobs.rb > log/worker.log 2>&1; done
87
+
88
+
89
+ Error Handling
90
+ -------------
91
+
92
+ 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:
93
+
94
+ error do |e, job, args|
95
+ Exceptional.handle(e)
96
+ end
97
+
98
+ Before filter
99
+ -------------
100
+
101
+ If you wish to run a block of code prior to any job:
102
+
103
+ before do |job|
104
+ puts "About to work #{job}"
105
+ end
106
+
107
+ Tidbits
108
+ -------
109
+
110
+ * 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.
111
+ * Because there are no class definitions associated with jobs, you can queue jobs from anywhere without needing to include your full app's environment.
112
+ * 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/. You can specify multiple beanstalk servers, separated by whitespace or comma, e.g. export BEANSTALK_URL="beanstalk://b1.example.com:11300/, beanstalk://b2.example.com:11300/"
113
+ * The stalk binary is just for convenience, you can also run a worker with a straight Ruby command:
114
+ $ ruby -r jobs -e Stalker.work
115
+
116
+ Running the tests
117
+ -----------------
118
+
119
+ If you wish to hack on Stalker, install these extra gems:
120
+
121
+ $ gem install contest mocha
122
+
123
+ Make sure you have a beanstalkd running, then run the tests:
124
+
125
+ $ ruby test/stalker_test.rb
126
+
127
+ Meta
128
+ ----
129
+
130
+ Created by [Adam Wiggins](https://github.com/adamwiggins)
131
+
132
+ Patches from Jamie Cobbett, Scott Water, Keith Rarick, Mark McGranaghan, Sean Walberg, Adam Pohorecki, Han Kessels
133
+
134
+ Heavily inspired by [Minion](https://github.com/orionz/minion) by Orion Henry
135
+
136
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
137
+
138
+ https://github.com/han/stalker
139
+
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'jeweler'
2
+
3
+ Jeweler::Tasks.new do |s|
4
+ s.name = "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 = "https://github.com/han/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.9.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
+ extend 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,194 @@
1
+ require 'beaneater'
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 = [0, opts[:delay].to_i].max
17
+ ttr = opts[:ttr] || 120
18
+ tube = beanstalk.tubes[job]
19
+ job = tube.put [ job, args ].to_json, pri:pri, delay:delay, ttr:ttr
20
+ job[:id]
21
+ rescue Beaneater::NotConnected => e
22
+ failed_connection(e)
23
+ end
24
+
25
+ def job(j, &block)
26
+ @@handlers ||= {}
27
+ @@handlers[j] = block
28
+ end
29
+
30
+ def status(job_id)
31
+ job = beanstalk.jobs[job_id]
32
+ job.stats unless job.nil?
33
+ end
34
+
35
+ def before(&block)
36
+ @@before_handlers ||= []
37
+ @@before_handlers << block
38
+ end
39
+
40
+ def error(&blk)
41
+ @@error_handler = blk
42
+ end
43
+
44
+ class NoJobsDefined < RuntimeError; end
45
+ class NoSuchJob < RuntimeError; end
46
+
47
+ def prep(jobs=nil)
48
+ raise NoJobsDefined unless defined?(@@handlers)
49
+ @@error_handler = nil unless defined?(@@error_handler)
50
+
51
+ jobs ||= all_jobs
52
+
53
+ jobs.each do |job|
54
+ raise(NoSuchJob, job) unless @@handlers[job]
55
+ end
56
+
57
+ log "Working #{jobs.size} jobs: [ #{jobs.join(' ')} ]"
58
+
59
+ jobs.each { |job| beanstalk.watch(job) }
60
+
61
+ beanstalk.tubes.watched.each do |tube|
62
+ beanstalk.ignore(tube) unless jobs.include?(tube)
63
+ end
64
+ rescue Beaneater::NotConnected => e
65
+ failed_connection(e)
66
+ end
67
+
68
+ def work(jobs=nil)
69
+ prep(jobs)
70
+ loop { work_one_job }
71
+ end
72
+
73
+ class JobTimeout < RuntimeError; end
74
+
75
+ def work_one_job
76
+ job = beanstalk.jobs.reserve
77
+ name, args = JSON.parse job.body
78
+ log_job_begin(name, args)
79
+ handler = @@handlers[name]
80
+ raise(NoSuchJob, name) unless handler
81
+
82
+ begin
83
+ Timeout::timeout(job.ttr - 1) do
84
+ if defined? @@before_handlers and @@before_handlers.respond_to? :each
85
+ @@before_handlers.each do |block|
86
+ block.call(name)
87
+ end
88
+ end
89
+ handler.call(args)
90
+ end
91
+ rescue Timeout::Error
92
+ raise JobTimeout, "#{name} hit #{job.ttr-1}s timeout"
93
+ end
94
+
95
+ job.delete
96
+ log_job_end(name)
97
+ rescue Beaneater::NotConnected => e
98
+ failed_connection(e)
99
+ rescue SystemExit
100
+ raise
101
+ rescue => e
102
+ log_error exception_message(e)
103
+ job.bury rescue nil
104
+ log_job_end(name, 'failed') if @job_begun
105
+ if error_handler
106
+ if error_handler.arity == 1
107
+ error_handler.call(e)
108
+ else
109
+ error_handler.call(e, name, args)
110
+ end
111
+ end
112
+ end
113
+
114
+ def failed_connection(e)
115
+ log_error exception_message(e)
116
+ log_error "*** Failed connection to #{beanstalk_url}"
117
+ log_error "*** Check that beanstalkd is running (or set a different BEANSTALK_URL)"
118
+ exit 1
119
+ end
120
+
121
+ def log_job_begin(name, args)
122
+ args_flat = unless args.empty?
123
+ '(' + args.inject([]) do |accum, (key,value)|
124
+ accum << "#{key}=#{value}"
125
+ end.join(' ') + ')'
126
+ else
127
+ ''
128
+ end
129
+
130
+ log [ "Working", name, args_flat ].join(' ')
131
+ @job_begun = Time.now
132
+ end
133
+
134
+ def log_job_end(name, failed=false)
135
+ ellapsed = Time.now - @job_begun
136
+ ms = (ellapsed.to_f * 1000).to_i
137
+ log "Finished #{name} in #{ms}ms #{failed ? ' (failed)' : ''}"
138
+ end
139
+
140
+ def log(msg)
141
+ puts msg
142
+ end
143
+
144
+ def log_error(msg)
145
+ STDERR.puts msg
146
+ end
147
+
148
+ def beanstalk
149
+ @@beanstalk ||= Beaneater::Pool.new(beanstalk_addresses)
150
+ end
151
+
152
+ def beanstalk_url
153
+ return @@url if defined?(@@url) and @@url
154
+ ENV['BEANSTALK_URL'] || 'beanstalk://localhost/'
155
+ end
156
+
157
+ class BadURL < RuntimeError; end
158
+
159
+ def beanstalk_addresses
160
+ uris = beanstalk_url.split(/[\s,]+/)
161
+ uris.map {|uri| beanstalk_host_and_port(uri)}
162
+ end
163
+
164
+ def beanstalk_host_and_port(uri_string)
165
+ uri = URI.parse(uri_string)
166
+ raise(BadURL, uri_string) if uri.scheme != 'beanstalk'
167
+ "#{uri.host}:#{uri.port || 11300}"
168
+ end
169
+
170
+ def exception_message(e)
171
+ msg = [ "Exception #{e.class} -> #{e.message}" ]
172
+
173
+ base = File.expand_path(Dir.pwd) + '/'
174
+ e.backtrace.each do |t|
175
+ msg << " #{File.expand_path(t).gsub(/#{base}/, '')}"
176
+ end
177
+
178
+ msg.join("\n")
179
+ end
180
+
181
+ def all_jobs
182
+ @@handlers.keys
183
+ end
184
+
185
+ def error_handler
186
+ @@error_handler
187
+ end
188
+
189
+ def clear!
190
+ @@handlers = nil
191
+ @@before_handlers = nil
192
+ @@error_handler = nil
193
+ end
194
+ end
@@ -0,0 +1,124 @@
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 |e, job_name, args|
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 "should be compatible with legacy error handlers" do
46
+ exception = StandardError.new("Oh my, the job has failed!")
47
+ Stalker.error { |e| $handled = e }
48
+ Stalker.job('my.job') { |args| raise exception }
49
+ Stalker.enqueue('my.job', :foo => 123)
50
+ Stalker.prep
51
+ Stalker.work_one_job
52
+ assert_equal exception, $handled
53
+ end
54
+
55
+ test "continue working when error handler not defined" do
56
+ Stalker.job('my.job') { fail }
57
+ Stalker.enqueue('my.job')
58
+ Stalker.prep
59
+ Stalker.work_one_job
60
+ assert_equal false, $handled
61
+ end
62
+
63
+ test "exception raised one second before beanstalk ttr reached" do
64
+ with_an_error_handler
65
+ Stalker.job('my.job') { sleep(3); $handled = "didn't time out" }
66
+ Stalker.enqueue('my.job', {}, :ttr => 2)
67
+ Stalker.prep
68
+ Stalker.work_one_job
69
+ assert_equal Stalker::JobTimeout, $handled
70
+ end
71
+
72
+ test "before filter gets run first" do
73
+ Stalker.before { |name| $flag = "i_was_here" }
74
+ Stalker.job('my.job') { |args| $handled = ($flag == 'i_was_here') }
75
+ Stalker.enqueue('my.job')
76
+ Stalker.prep
77
+ Stalker.work_one_job
78
+ assert_equal true, $handled
79
+ end
80
+
81
+ test "before filter passes the name of the job" do
82
+ Stalker.before { |name| $jobname = name }
83
+ Stalker.job('my.job') { true }
84
+ Stalker.enqueue('my.job')
85
+ Stalker.prep
86
+ Stalker.work_one_job
87
+ assert_equal 'my.job', $jobname
88
+ end
89
+
90
+ test "before filter can pass an instance var" do
91
+ Stalker.before { |name| @foo = "hello" }
92
+ Stalker.job('my.job') { |args| $handled = (@foo == "hello") }
93
+ Stalker.enqueue('my.job')
94
+ Stalker.prep
95
+ Stalker.work_one_job
96
+ assert_equal true, $handled
97
+ end
98
+
99
+ test "before filter invokes error handler when defined" do
100
+ with_an_error_handler
101
+ Stalker.before { |name| fail }
102
+ Stalker.job('my.job') { }
103
+ Stalker.enqueue('my.job', :foo => 123)
104
+ Stalker.prep
105
+ Stalker.work_one_job
106
+ assert $handled
107
+ assert_equal 'my.job', $job_name
108
+ assert_equal({'foo' => 123}, $job_args)
109
+ end
110
+
111
+ test "parse BEANSTALK_URL" do
112
+ ENV['BEANSTALK_URL'] = "beanstalk://localhost:12300"
113
+ assert_equal Stalker.beanstalk_addresses, ["localhost:12300"]
114
+ ENV['BEANSTALK_URL'] = "beanstalk://localhost:12300/, beanstalk://localhost:12301/"
115
+ assert_equal Stalker.beanstalk_addresses, ["localhost:12300","localhost:12301"]
116
+ ENV['BEANSTALK_URL'] = "beanstalk://localhost:12300 beanstalk://localhost:12301"
117
+ assert_equal Stalker.beanstalk_addresses, ["localhost:12300","localhost:12301"]
118
+ ENV['BEANSTALK_URL'] = "beanstalk://localhost:12300, http://localhost:12301"
119
+ assert_raise Stalker::BadURL do
120
+ Stalker.beanstalk_addresses
121
+ end
122
+ end
123
+
124
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stalker-be
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Norman Köhring
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: beaneater
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json_pure
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A job queueing and background workers system using Beanstalkd. Forked
47
+ from original stalker to use beaneater.
48
+ email: n@koehr.in
49
+ executables:
50
+ - stalk
51
+ extensions: []
52
+ extra_rdoc_files:
53
+ - README.md
54
+ files:
55
+ - README.md
56
+ - Rakefile
57
+ - VERSION
58
+ - bin/stalk
59
+ - lib/stalker.rb
60
+ - examples/enqueue.rb
61
+ - examples/jobs.rb
62
+ - test/stalker_test.rb
63
+ homepage: https://github.com/nkoehring/stalker
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.25
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: A job queueing and background workers system using Beanstalkd.
87
+ test_files:
88
+ - examples/enqueue.rb
89
+ - examples/jobs.rb
90
+ - test/stalker_test.rb