famoseagle-sweat_shop 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-01-13
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Amos Elliston
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,65 @@
1
+ # SweatShop
2
+
3
+ SweatShop provides an api to background resource intensive tasks. Much of the api design was copied from Workling, with a few tweaks.
4
+ Currently, it runs kestrel, but it can support any number of queues.
5
+
6
+ ## Installing
7
+
8
+ gem install sweat_shop
9
+ freeze in your rails directory
10
+ cd vendor/gems/sweat_shop
11
+ rake setup
12
+
13
+ ## Writing workers
14
+
15
+ Put `email_worker.rb` into app/workers and sublcass `SweatShop::Worker`:
16
+
17
+ class EmailWorker
18
+ def send_mail(to)
19
+ user = User.find_by_id(to)
20
+ Mailer.deliver_welcome(to)
21
+ end
22
+ end
23
+
24
+ Then, anywhere in your app you can execute:
25
+
26
+ EmailWorker.async_send_mail(1)
27
+
28
+ The `async` signifies that this task will be placed on a queue to be serviced by the EmailWorker possibly on another machine. You can also
29
+ call:
30
+
31
+ EmailWorker.send_mail(1)
32
+
33
+ That will do the work immediately, without placing the task on the queue. You can also define a `queue_group` at the top of the file
34
+ which will allow you to split workers out into logical groups. This is important if you have various machines serving different
35
+ queues.
36
+
37
+ ## Running the queue
38
+
39
+ SweatShop has been tested with Kestrel, but it will also work with Starling. You can install and start kestrel following the instructions here:
40
+
41
+ http://github.com/robey/kestrel/tree/master
42
+
43
+ config/sweatshop.yml specifies the machine address of the queue (default localhost:22133).
44
+
45
+ ## Running the workers
46
+
47
+ Assuming you ran `rake setup` in Rails, you can type:
48
+
49
+ script/sweatshop
50
+
51
+ By default, the script will run all workers defined in the app/workers dir. Every task will be processed on each queue using a round-robin algorithm. You can also add the `-d` flag which will put the worker in daemon mode. The daemon also takes other params. Add a `-h` for more details.
52
+
53
+ script/sweatshop -d
54
+ script/sweatshop -d stop
55
+
56
+ If you would like to run SweatShop as a daemon on a linux machine, use the initd.sh script provided in the sweat_shop/script dir.
57
+
58
+ # REQUIREMENTS
59
+
60
+ i_can_daemonize
61
+ memcache (for kestrel)
62
+
63
+ # LICENSE
64
+
65
+ Copyright (c) 2009 Amos Elliston, Geni.com; Published under The MIT License, see License
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rcov/rcovtask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |s|
9
+ s.name = "sweat_shop"
10
+ s.summary = %Q{SweatShop is a simple asynchronous worker queue build on top of rabbitmq/ampq}
11
+ s.email = "amos@geni.com"
12
+ s.homepage = "http://github.com/famoseagle/sweat-shop"
13
+ s.description = "TODO"
14
+ s.authors = ["Amos Elliston"]
15
+ s.files = FileList["[A-Z]*", "{lib,test,config}/**/*"]
16
+ end
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
20
+
21
+ Rake::TestTask.new
22
+
23
+ Rake::RDocTask.new do |rdoc|
24
+ rdoc.rdoc_dir = 'rdoc'
25
+ rdoc.title = 'new_project'
26
+ rdoc.options << '--line-numbers' << '--inline-source'
27
+ rdoc.rdoc_files.include('README*')
28
+ rdoc.rdoc_files.include('lib/**/*.rb')
29
+ end
30
+
31
+ Rcov::RcovTask.new do |t|
32
+ t.libs << 'test'
33
+ t.test_files = FileList['test/**/*_test.rb']
34
+ t.verbose = true
35
+ end
36
+
37
+ task :default => :test
38
+
39
+ task :setup do
40
+ require File.dirname(__FILE__) + '/install'
41
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 0
3
+ :major: 0
4
+ :minor: 3
@@ -0,0 +1,4 @@
1
+ # default options
2
+ servers:
3
+ - localhost:22133
4
+ enable: true
@@ -0,0 +1,12 @@
1
+ development:
2
+ servers:
3
+ - localhost:22133
4
+ enable: true
5
+ test:
6
+ servers:
7
+ - localhost:22133
8
+ enable: true
9
+ production:
10
+ servers:
11
+ - localhost:22133
12
+ enable: true
data/lib/kestrel.rb ADDED
@@ -0,0 +1,29 @@
1
+ class Kestrel
2
+ attr_reader :client, :servers
3
+
4
+ def initialize(opts)
5
+ @servers = opts[:servers]
6
+ @client = opts[:client] || MemCache.new(@servers)
7
+ end
8
+
9
+ def queue_size(queue)
10
+ size = 0
11
+ stats = client.stats
12
+ servers.each do |server|
13
+ size += stats[server]["queue_#{queue}_items"].to_i
14
+ end
15
+ size
16
+ end
17
+
18
+ def enqueue(queue, data)
19
+ client.set(queue, data)
20
+ end
21
+
22
+ def dequeue(queue)
23
+ client.get("#{queue}/open")
24
+ end
25
+
26
+ def confirm(queue)
27
+ client.get("#{queue}/close")
28
+ end
29
+ end
data/lib/sweat_shop.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'rubygems'
2
+ require 'digest'
3
+ require 'yaml'
4
+
5
+ $:.unshift(File.dirname(__FILE__))
6
+ require 'kestrel'
7
+ require 'sweat_shop/worker'
8
+
9
+ module SweatShop
10
+ extend self
11
+
12
+ def workers
13
+ @workers ||= []
14
+ end
15
+
16
+ def workers=(workers)
17
+ @workers = workers
18
+ end
19
+
20
+ def workers_in_group(groups)
21
+ groups = [groups] unless groups.is_a?(Array)
22
+ if groups.include?(:all)
23
+ workers
24
+ else
25
+ workers.select do |worker|
26
+ groups.include?(worker.queue_group)
27
+ end
28
+ end
29
+ end
30
+
31
+ def do_tasks(workers)
32
+ loop do
33
+ wait = true
34
+ workers.each do |worker|
35
+ if task = worker.dequeue
36
+ worker.do_task(task)
37
+ wait = false
38
+ end
39
+ end
40
+ exit if stop?
41
+ sleep 1 if wait
42
+ end
43
+ end
44
+
45
+ def do_all_tasks
46
+ do_tasks(
47
+ workers_in_group(:all)
48
+ )
49
+ end
50
+
51
+ def do_default_tasks
52
+ do_tasks(
53
+ workers_in_group(:default)
54
+ )
55
+ end
56
+
57
+ def stop
58
+ @stop = true
59
+ end
60
+
61
+ def stop?
62
+ @stop
63
+ end
64
+
65
+ def config
66
+ @config ||= begin
67
+ defaults = YAML.load_file(File.dirname(__FILE__) + '/../config/defaults.yml')
68
+ if defined?(RAILS_ROOT)
69
+ file = RAILS_ROOT + '/config/sweatshop.yml'
70
+ if File.exist?(file)
71
+ YAML.load_file(file)[RAILS_ENV || 'development']
72
+ else
73
+ defaults['enable'] = false
74
+ defaults
75
+ end
76
+ else
77
+ defaults
78
+ end
79
+ end
80
+ end
81
+
82
+ def queue_sizes
83
+ workers.inject([]) do |all, worker|
84
+ all << [worker, worker.queue_size]
85
+ all
86
+ end
87
+ end
88
+
89
+ def pp_sizes
90
+ max_width = workers.collect{|w| w.to_s.size}.max
91
+ puts '-' * (max_width + 10)
92
+ puts queue_sizes.collect{|p| sprintf("%-#{max_width}s %2s", p.first, p.last)}.join("\n")
93
+ puts '-' * (max_width + 10)
94
+ end
95
+
96
+ def queue
97
+ @queue ||= Kestrel.new(:servers => config['servers'])
98
+ end
99
+
100
+ def queue=(queue)
101
+ @queue = queue
102
+ end
103
+ end
104
+
105
+ if defined?(RAILS_ROOT)
106
+ Dir.glob(RAILS_ROOT + '/app/workers/*.rb').each{|worker| require worker }
107
+ end
@@ -0,0 +1,5 @@
1
+ def metaclass; class << self; self; end; end
2
+ def meta_eval(&blk); metaclass.instance_eval(&blk); end
3
+ def meta_def(name, &blk)
4
+ meta_eval { define_method name, &blk }
5
+ end
@@ -0,0 +1,67 @@
1
+ require File.dirname(__FILE__) + '/../sweat_shop'
2
+ require 'i_can_daemonize'
3
+
4
+ module SweatShop
5
+ class Sweatd
6
+ include ICanDaemonize
7
+ queues = []
8
+ groups = []
9
+ rails_root = nil
10
+
11
+ arg '--workers=Worker,Worker', 'Workers to service (Default is all)' do |value|
12
+ queues = value.split(',')
13
+ end
14
+
15
+ arg '--groups=GROUP,GROUP', 'Groups of queues to service' do |value|
16
+ groups = value.split(',').collect{|g| g.to_sym}
17
+ end
18
+
19
+ arg '--worker-file=WORKERFILE', 'Worker file to load' do |value|
20
+ require value
21
+ end
22
+
23
+ arg '--worker-dir=WORKERDIR', 'Directory containing workers' do |value|
24
+ Dir.glob(value + '*.rb').each{|worker| require worker}
25
+ end
26
+
27
+ arg '--rails=DIR', 'Pass in RAILS_ROOT to run this daemon in a rails environment' do |value|
28
+ rails_root = value
29
+ end
30
+
31
+ sig(:term, :int) do
32
+ puts "Shutting down sweatd..."
33
+ SweatShop.stop
34
+ end
35
+
36
+ before do
37
+ if rails_root
38
+ puts "Loading Rails..."
39
+ require rails_root + '/config/environment'
40
+ end
41
+ end
42
+
43
+ daemonize(:kill_timeout => 20) do
44
+ workers = []
45
+
46
+ if groups.any?
47
+ workers += SweatShop.workers_in_group(groups)
48
+ end
49
+
50
+ if queues.any?
51
+ workers += queues.collect{|q| Object.module_eval(q)}
52
+ end
53
+
54
+ if workers.any?
55
+ worker_str = workers.join(',')
56
+ puts "Starting #{worker_str}..."
57
+ $0 = "Sweatd: #{worker_str}"
58
+ SweatShop.do_tasks(workers)
59
+ else
60
+ puts "Starting all workers..."
61
+ $0 = 'Sweatd: all'
62
+ SweatShop.do_all_tasks
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,130 @@
1
+ require File.dirname(__FILE__) + '/metaid'
2
+
3
+ module SweatShop
4
+ class Worker
5
+ @@logger = nil
6
+
7
+ def self.inherited(subclass)
8
+ self.workers << subclass
9
+ end
10
+
11
+ def self.method_missing(method, *args, &block)
12
+ if method.to_s =~ /^async_(.*)/ and config['enable']
13
+ method = $1
14
+ expected_args = instance.method(method).arity
15
+ if expected_args != args.size
16
+ raise ArgumentError.new("#{method} expects #{expected_args} arguments")
17
+ end
18
+
19
+ uid = ::Digest::MD5.hexdigest("#{name}:#{method}:#{args}:#{Time.now.to_f}")
20
+ task = {:args => args, :method => method, :uid => uid, :queued_at => Time.now.to_i}
21
+
22
+ log("Putting #{uid} on #{queue_name}")
23
+ enqueue(task)
24
+
25
+ uid
26
+ elsif instance.respond_to?(method)
27
+ instance.send(method, *args)
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ def self.instance
34
+ @instance ||= new
35
+ end
36
+
37
+ def self.config
38
+ SweatShop.config
39
+ end
40
+
41
+ def self.queue_name
42
+ @queue_name ||= self.to_s
43
+ end
44
+
45
+ def self.queue_size
46
+ queue.queue_size(queue_name)
47
+ end
48
+
49
+ def self.enqueue(task)
50
+ queue.enqueue(queue_name, task)
51
+ end
52
+
53
+ def self.dequeue
54
+ queue.dequeue(queue_name)
55
+ end
56
+
57
+ def self.confirm
58
+ queue.confirm(queue_name)
59
+ end
60
+
61
+ def self.do_tasks
62
+ while task = dequeue
63
+ do_task(task)
64
+ end
65
+ end
66
+
67
+ def self.do_task(task)
68
+ call_before_task(task)
69
+
70
+ queued_at = task[:queued_at] ? "(queued #{Time.at(task[:queued_at]).strftime('%Y/%m/%d %H:%M:%S')})" : ''
71
+ log("Dequeuing #{queue_name}::#{task[:method]} #{queued_at}")
72
+ task[:result] = instance.send(task[:method], *task[:args])
73
+
74
+ call_after_task(task)
75
+ confirm
76
+ end
77
+
78
+ def self.call_before_task(task)
79
+ superclass.call_before_task(task) if superclass.respond_to?(:call_before_task)
80
+ before_task.call(task) if before_task
81
+ end
82
+
83
+ def self.call_after_task(task)
84
+ superclass.call_after_task(task) if superclass.respond_to?(:call_after_task)
85
+ after_task.call(task) if after_task
86
+ end
87
+
88
+ def self.workers
89
+ SweatShop.workers
90
+ end
91
+
92
+ def self.log(msg)
93
+ return if logger == :silent
94
+ logger ? logger.debug(msg) : puts(msg)
95
+ end
96
+
97
+ def self.logger
98
+ @@logger
99
+ end
100
+
101
+ def self.logger=(logger)
102
+ @@logger = logger
103
+ end
104
+
105
+ def self.before_task(&block)
106
+ if block
107
+ @before_task = block
108
+ else
109
+ @before_task
110
+ end
111
+ end
112
+
113
+ def self.after_task(&block)
114
+ if block
115
+ @after_task = block
116
+ else
117
+ @after_task
118
+ end
119
+ end
120
+
121
+ def self.queue
122
+ SweatShop.queue
123
+ end
124
+
125
+ def self.queue_group(group=nil)
126
+ group ? meta_def(:_queue_group){ group } : _queue_group
127
+ end
128
+ queue_group :default
129
+ end
130
+ end
@@ -0,0 +1,16 @@
1
+ # hack for functional tests
2
+ require File.dirname(__FILE__) + '/../../../memcache/lib/memcache_extended'
3
+ require File.dirname(__FILE__) + '/../../../memcache/lib/memcache_util'
4
+
5
+ class HelloWorker < SweatShop::Worker
6
+ TEST_FILE = File.dirname(__FILE__) + '/test.txt' unless defined?(TEST_FILE)
7
+
8
+ def hello(name)
9
+ puts name
10
+ "Hi, #{name}"
11
+ end
12
+
13
+ after_task do |task|
14
+ File.open(TEST_FILE, 'w'){|f| f << task[:result]}
15
+ end
16
+ end
@@ -0,0 +1,35 @@
1
+ require File.dirname(__FILE__) + '/../lib/sweat_shop'
2
+ require File.dirname(__FILE__) + '/test_helper'
3
+ require File.dirname(__FILE__) + '/hello_worker'
4
+
5
+ class WorkerTest < Test::Unit::TestCase
6
+
7
+ def setup
8
+ File.delete(HelloWorker::TEST_FILE) if File.exist?(HelloWorker::TEST_FILE)
9
+ end
10
+
11
+ def teardown
12
+ File.delete(HelloWorker::TEST_FILE) if File.exist?(HelloWorker::TEST_FILE)
13
+ end
14
+
15
+ # remove 'x' and start kestrel to run
16
+ test "daemon" do
17
+ begin
18
+ SweatShop.queue = nil
19
+ SweatShop::Worker.logger = :silent
20
+
21
+ worker = File.expand_path(File.dirname(__FILE__) + '/hello_worker')
22
+ sweatd = "#{File.dirname(__FILE__)}/../lib/sweat_shop/sweatd.rb"
23
+ uid = HelloWorker.async_hello('Amos')
24
+
25
+ `ruby #{sweatd} --worker-file #{worker} start`
26
+ `ruby #{sweatd} stop`
27
+
28
+ File.delete('sweatd.log') if File.exist?('sweatd.log')
29
+ assert_equal 'Hi, Amos', File.read(HelloWorker::TEST_FILE)
30
+ rescue MemCache::MemCacheError => e
31
+ puts "\n\n*** Start kestrel on localhost to run all functional tests. ***\n\n"
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,14 @@
1
+ require 'test/unit'
2
+
3
+ class << Test::Unit::TestCase
4
+ def test(name, &block)
5
+ test_name = "test_#{name.gsub(/[\s\W]/,'_')}"
6
+ raise ArgumentError, "#{test_name} is already defined" if self.instance_methods.include? test_name
7
+ define_method test_name, &block
8
+ end
9
+
10
+ def xtest(name, &block)
11
+ # no-op, an empty test method is defined to prevent "no tests in testcase" errors when all tests are disabled
12
+ define_method(:test_disabled) { assert true }
13
+ end
14
+ end
@@ -0,0 +1,67 @@
1
+ require File.dirname(__FILE__) + '/../../../memcache/lib/memcache_mock'
2
+ require File.dirname(__FILE__) + '/test_helper'
3
+ require File.dirname(__FILE__) + '/../lib/sweat_shop'
4
+
5
+ class SweatShopTest < Test::Unit::TestCase
6
+ SweatShop.workers = []
7
+ SweatShop.queue = Kestrel.new(:client => MemCacheMock.new)
8
+
9
+ class HelloWorker < SweatShop::Worker
10
+ def hello(name)
11
+ "Hi, #{name}"
12
+ end
13
+ end
14
+
15
+ class GroupedWorker < SweatShop::Worker
16
+ queue_group :foo
17
+ end
18
+
19
+ test "group workers" do
20
+ assert_equal [HelloWorker, GroupedWorker], SweatShop.workers_in_group(:all)
21
+ assert_equal [HelloWorker], SweatShop.workers_in_group(:default)
22
+ assert_equal [GroupedWorker], SweatShop.workers_in_group(:foo)
23
+ end
24
+
25
+ test "synch call" do
26
+ worker = HelloWorker.new
27
+ assert_equal "Hi, Amos", worker.hello('Amos')
28
+ end
29
+
30
+ test "uid" do
31
+ SweatShop::Worker.logger = :silent
32
+ uid = HelloWorker.async_hello('Amos')
33
+ assert_not_nil uid
34
+ end
35
+
36
+ test "before task" do
37
+ HelloWorker.before_task do
38
+ "hello"
39
+ end
40
+ assert_equal "hello", HelloWorker.before_task.call
41
+ end
42
+
43
+ test "after task" do
44
+ HelloWorker.after_task do
45
+ "goodbye"
46
+ end
47
+ assert_equal "goodbye", HelloWorker.after_task.call
48
+ end
49
+
50
+ test "chainable before tasks" do
51
+ MESSAGES = []
52
+ class BaseWorker < SweatShop::Worker
53
+ before_task do |task|
54
+ MESSAGES << 'base'
55
+ end
56
+ end
57
+ class SubWorker < BaseWorker
58
+ before_task do |task|
59
+ MESSAGES << 'sub'
60
+ end
61
+ end
62
+ SubWorker.call_before_task('foo')
63
+ assert_equal ['base', 'sub'], MESSAGES
64
+ SweatShop.workers.delete(BaseWorker)
65
+ SweatShop.workers.delete(SubWorker)
66
+ end
67
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: famoseagle-sweat_shop
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Amos Elliston
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-18 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: TODO
17
+ email: amos@geni.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - History.txt
26
+ - LICENSE
27
+ - Rakefile
28
+ - README.markdown
29
+ - VERSION.yml
30
+ - lib/kestrel.rb
31
+ - lib/sweat_shop
32
+ - lib/sweat_shop/metaid.rb
33
+ - lib/sweat_shop/sweatd.rb
34
+ - lib/sweat_shop/worker.rb
35
+ - lib/sweat_shop.rb
36
+ - test/hello_worker.rb
37
+ - test/test_functional_worker.rb
38
+ - test/test_helper.rb
39
+ - test/test_sweatshop.rb
40
+ - config/defaults.yml
41
+ - config/sweatshop.yml
42
+ has_rdoc: true
43
+ homepage: http://github.com/famoseagle/sweat-shop
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --inline-source
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.2.0
66
+ signing_key:
67
+ specification_version: 2
68
+ summary: SweatShop is a simple asynchronous worker queue build on top of rabbitmq/ampq
69
+ test_files: []
70
+