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 +120 -0
- data/Rakefile +19 -0
- data/VERSION +1 -0
- data/bin/stalk +21 -0
- data/examples/enqueue.rb +5 -0
- data/examples/jobs.rb +11 -0
- data/lib/stalker.rb +177 -0
- data/test/stalker_test.rb +101 -0
- metadata +103 -0
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
|
data/examples/enqueue.rb
ADDED
data/examples/jobs.rb
ADDED
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
|