famoseagle-sweat_shop 0.3.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/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
+