netinlet-sweat_shop 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/LICENSE +20 -0
- data/README.markdown +105 -0
- data/Rakefile +42 -0
- data/VERSION.yml +4 -0
- data/bin/sweatd +3 -0
- data/config/defaults.yml +10 -0
- data/config/sweatshop.yml +18 -0
- data/lib/message_queue/base.rb +17 -0
- data/lib/message_queue/kestrel.rb +32 -0
- data/lib/message_queue/rabbit.rb +72 -0
- data/lib/serializers/json_serializer.rb +39 -0
- data/lib/serializers/marshal_serializer.rb +15 -0
- data/lib/serializers/serializer.rb +108 -0
- data/lib/serializers/yaml_serializer.rb +22 -0
- data/lib/sweat_shop.rb +149 -0
- data/lib/sweat_shop/daemoned.rb +405 -0
- data/lib/sweat_shop/metaid.rb +5 -0
- data/lib/sweat_shop/sweatd.rb +76 -0
- data/lib/sweat_shop/worker.rb +152 -0
- data/test/hello_worker.rb +13 -0
- data/test/test_functional_worker.rb +35 -0
- data/test/test_helper.rb +14 -0
- data/test/test_json_serializer.rb +50 -0
- data/test/test_marshal_serializer.rb +42 -0
- data/test/test_serializer.rb +95 -0
- data/test/test_sweatshop.rb +65 -0
- data/test/test_yaml_serializer.rb +44 -0
- metadata +97 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/daemoned'
|
2
|
+
|
3
|
+
module SweatShop
|
4
|
+
class Sweatd
|
5
|
+
include Daemoned
|
6
|
+
queues = []
|
7
|
+
groups = []
|
8
|
+
rails_root = nil
|
9
|
+
start_cmd = "ruby #{__FILE__} start #{ARGV.reject{|a| a == 'start'}.join(' ')}"
|
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
|
+
sig(:hup) do
|
37
|
+
puts "Received HUP"
|
38
|
+
SweatShop.stop
|
39
|
+
remove_pid!
|
40
|
+
puts "Restarting sweatd with #{start_cmd}..."
|
41
|
+
`#{start_cmd}`
|
42
|
+
end
|
43
|
+
|
44
|
+
before do
|
45
|
+
if rails_root
|
46
|
+
puts "Loading Rails..."
|
47
|
+
require rails_root + '/config/environment'
|
48
|
+
end
|
49
|
+
require File.dirname(__FILE__) + '/../sweat_shop'
|
50
|
+
end
|
51
|
+
|
52
|
+
daemonize(:kill_timeout => 20) do
|
53
|
+
workers = []
|
54
|
+
|
55
|
+
if groups.any?
|
56
|
+
workers += SweatShop.workers_in_group(groups)
|
57
|
+
end
|
58
|
+
|
59
|
+
if queues.any?
|
60
|
+
workers += queues.collect{|q| Object.module_eval(q)}
|
61
|
+
end
|
62
|
+
|
63
|
+
if workers.any?
|
64
|
+
worker_str = workers.join(',')
|
65
|
+
puts "Starting #{worker_str}..."
|
66
|
+
$0 = "Sweatd: #{worker_str}"
|
67
|
+
SweatShop.do_tasks(workers)
|
68
|
+
else
|
69
|
+
puts "Starting all workers..."
|
70
|
+
$0 = 'Sweatd: all'
|
71
|
+
SweatShop.do_all_tasks
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/metaid'
|
2
|
+
require File.dirname(__FILE__) + '/../serializers/serializer'
|
3
|
+
|
4
|
+
module SweatShop
|
5
|
+
class Worker
|
6
|
+
def self.inherited(subclass)
|
7
|
+
self.workers << subclass
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.method_missing(method, *args, &block)
|
11
|
+
if method.to_s =~ /^async_(.*)/
|
12
|
+
method = $1
|
13
|
+
expected_args = instance.method(method).arity
|
14
|
+
if expected_args != args.size
|
15
|
+
raise ArgumentError.new("#{method} expects #{expected_args} arguments")
|
16
|
+
end
|
17
|
+
#return instance.send(method, *args) unless true == config['enable']
|
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) if config['enable']
|
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.serialize_with(serializer_name)
|
34
|
+
@serializer = SweatShop::Serializer.serializers[serializer_name]
|
35
|
+
raise RuntimeError.new("No Such Serializer ['#{serializer_name}']") if @serializer.nil?
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.serializer
|
39
|
+
@serializer ||= SweatShop::Serializer.default
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.instance
|
43
|
+
@instance ||= new
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.config
|
47
|
+
SweatShop.config
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.queue_name
|
51
|
+
@queue_name ||= self.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.delete_queue
|
55
|
+
queue.delete(queue_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.queue_size
|
59
|
+
queue.queue_size(queue_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.enqueue(task)
|
63
|
+
queue.enqueue(queue_name, task, serializer)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.dequeue
|
67
|
+
queue.dequeue(queue_name, serializer)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.confirm
|
71
|
+
queue.confirm(queue_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.subscribe
|
75
|
+
queue.subscribe(queue_name, serializer) do |task|
|
76
|
+
do_task(task)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.do_tasks
|
81
|
+
while task = dequeue
|
82
|
+
do_task(task)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.do_task(task)
|
87
|
+
call_before_task(task)
|
88
|
+
|
89
|
+
queued_at = task[:queued_at] ? "(queued #{Time.at(task[:queued_at]).strftime('%Y/%m/%d %H:%M:%S')})" : ''
|
90
|
+
log("Dequeuing #{queue_name}::#{task[:method]} #{queued_at}")
|
91
|
+
task[:result] = instance.send(task[:method], *task[:args])
|
92
|
+
|
93
|
+
call_after_task(task)
|
94
|
+
confirm
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.call_before_task(task)
|
98
|
+
superclass.call_before_task(task) if superclass.respond_to?(:call_before_task)
|
99
|
+
before_task.call(task) if before_task
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.call_after_task(task)
|
103
|
+
superclass.call_after_task(task) if superclass.respond_to?(:call_after_task)
|
104
|
+
after_task.call(task) if after_task
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.queue
|
108
|
+
SweatShop.queue(queue_group.to_s)
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.workers
|
112
|
+
SweatShop.workers
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.config
|
116
|
+
SweatShop.config
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.log(msg)
|
120
|
+
SweatShop.log(msg)
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.before_task(&block)
|
124
|
+
if block
|
125
|
+
@before_task = block
|
126
|
+
else
|
127
|
+
@before_task
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.after_task(&block)
|
132
|
+
if block
|
133
|
+
@after_task = block
|
134
|
+
else
|
135
|
+
@after_task
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.stop
|
140
|
+
instance.stop
|
141
|
+
end
|
142
|
+
|
143
|
+
# called before we exit -- subclass can implement this method
|
144
|
+
def stop; end;
|
145
|
+
|
146
|
+
|
147
|
+
def self.queue_group(group=nil)
|
148
|
+
group ? meta_def(:_queue_group){ group } : _queue_group
|
149
|
+
end
|
150
|
+
queue_group :default
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/sweat_shop'
|
2
|
+
class HelloWorker < SweatShop::Worker
|
3
|
+
TEST_FILE = File.dirname(__FILE__) + '/test.txt' unless defined?(TEST_FILE)
|
4
|
+
|
5
|
+
def hello(name)
|
6
|
+
puts name
|
7
|
+
"Hi, #{name}"
|
8
|
+
end
|
9
|
+
|
10
|
+
after_task do |task|
|
11
|
+
File.open(TEST_FILE, 'w'){|f| f << task[:result]}
|
12
|
+
end
|
13
|
+
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
|
+
class WorkerTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
File.delete(HelloWorker::TEST_FILE) if File.exist?(HelloWorker::TEST_FILE)
|
8
|
+
end
|
9
|
+
|
10
|
+
def teardown
|
11
|
+
File.delete(HelloWorker::TEST_FILE) if File.exist?(HelloWorker::TEST_FILE)
|
12
|
+
end
|
13
|
+
|
14
|
+
test "daemon" do
|
15
|
+
begin
|
16
|
+
SweatShop.queue = nil
|
17
|
+
SweatShop.logger = :silent
|
18
|
+
|
19
|
+
worker = File.expand_path(File.dirname(__FILE__) + '/hello_worker')
|
20
|
+
sweatd = "#{File.dirname(__FILE__)}/../lib/sweat_shop/sweatd.rb"
|
21
|
+
uid = HelloWorker.async_hello('Amos')
|
22
|
+
|
23
|
+
`ruby #{sweatd} --worker-file #{worker} start`
|
24
|
+
`ruby #{sweatd} stop`
|
25
|
+
|
26
|
+
File.delete('sweatd.log') if File.exist?('sweatd.log')
|
27
|
+
assert_equal 'Hi, Amos', File.read(HelloWorker::TEST_FILE)
|
28
|
+
rescue Exception => e
|
29
|
+
puts e.message
|
30
|
+
puts e.backtrace.join("\n")
|
31
|
+
fail "\n\n*** Functional test failed, is the rabbit server running on localhost? ***\n"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -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,50 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/sweat_shop'
|
3
|
+
|
4
|
+
class JsonSerializerTest < Test::Unit::TestCase
|
5
|
+
class UnderTest
|
6
|
+
attr_accessor :name, :address, :city, :state, :zip, :id
|
7
|
+
def ==(other)
|
8
|
+
![:name, :address, :city, :state, :zip, :id].map{|a| return self.send(a) == other.send(a)}.include?(false)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.json_create(params)
|
12
|
+
obj = new
|
13
|
+
for key, value in params
|
14
|
+
next if key == 'json_class'
|
15
|
+
obj.instance_variable_set "@#{key}", value
|
16
|
+
end
|
17
|
+
obj
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@under_test = UnderTest.new
|
23
|
+
@under_test.id = 87
|
24
|
+
@under_test.name = "JsonTest"
|
25
|
+
@under_test.address = "555 Rock Ridge Road"
|
26
|
+
@under_test.city = "Rock Ridge"
|
27
|
+
@under_test.state = "Texas"
|
28
|
+
@under_test.zip = "90210"
|
29
|
+
end
|
30
|
+
|
31
|
+
test "should properly serialize a simple data structure" do
|
32
|
+
dump = SweatShop::Serializers::JsonSerializer.serialize([{:foo => "bar"}, 23, 87, %w{doug cathy connor tommy}])
|
33
|
+
assert_equal '[{"foo":"bar"},23,87,["doug","cathy","connor","tommy"]]', dump
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
test "should be able to serialize an arbitrary class" do
|
38
|
+
dump = SweatShop::Serializers::JsonSerializer.serialize(@under_test)
|
39
|
+
assert_equal @under_test, JSON.parse(dump) # deserialize the dump because hash keys are getting set in different order from time to time & making tests fail
|
40
|
+
end
|
41
|
+
|
42
|
+
test "should be able to deserialize a simple data structure" do
|
43
|
+
assert_equal [{"foo" => "bar"}, 23, 87, %w{doug cathy connor tommy}], SweatShop::Serializers::JsonSerializer.deserialize('[{"foo":"bar"},23,87,["doug","cathy","connor","tommy"]]')
|
44
|
+
end
|
45
|
+
|
46
|
+
test "should be able to deserialize an arbitrary class" do
|
47
|
+
assert_equal @under_test, SweatShop::Serializers::JsonSerializer.deserialize('{"city":"Rock Ridge","name":"JsonTest","zip":"90210","json_class":"JsonSerializerTest::UnderTest","id":87,"address":"555 Rock Ridge Road","state":"Texas"}')
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/sweat_shop'
|
3
|
+
|
4
|
+
class MarshalSerializerTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
class UnderTest
|
7
|
+
attr_accessor :name, :address, :city, :state, :zip, :id
|
8
|
+
def ==(other)
|
9
|
+
![:name, :address, :city, :state, :zip, :id].map{|a| return self.send(a) == other.send(a)}.include?(false)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup
|
15
|
+
@under_test = UnderTest.new
|
16
|
+
@under_test.id = 87
|
17
|
+
@under_test.name = "MarshalTest"
|
18
|
+
@under_test.address = "555 Rock Ridge Road"
|
19
|
+
@under_test.city = "Rock Ridge"
|
20
|
+
@under_test.state = "Texas"
|
21
|
+
@under_test.zip = "90210"
|
22
|
+
end
|
23
|
+
|
24
|
+
test "should properly serialize a simple data structure" do
|
25
|
+
dump = SweatShop::Serializers::MarshalSerializer.serialize([{:foo => "bar"}, 23, 87, %w{doug cathy connor tommy}])
|
26
|
+
assert_equal Marshal.dump([{:foo => "bar"}, 23, 87, %w{doug cathy connor tommy}]), dump
|
27
|
+
end
|
28
|
+
|
29
|
+
test "should be able to serialize an arbitrary class" do
|
30
|
+
dump = SweatShop::Serializers::MarshalSerializer.serialize(@under_test)
|
31
|
+
assert_equal Marshal.dump(@under_test), dump
|
32
|
+
end
|
33
|
+
|
34
|
+
test "should be able to deserialize a simple data structure" do
|
35
|
+
assert_equal [{:foo => "bar"}, 23, 87, %w{doug cathy connor tommy}], SweatShop::Serializers::MarshalSerializer.deserialize(Marshal.dump([{:foo => "bar"}, 23, 87, %w{doug cathy connor tommy}]))
|
36
|
+
end
|
37
|
+
|
38
|
+
test "should be able to deserialize an arbitrary class" do
|
39
|
+
assert_equal @under_test, SweatShop::Serializers::MarshalSerializer.deserialize(Marshal.dump(@under_test))
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/sweat_shop'
|
3
|
+
|
4
|
+
class SerializerTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
# no serializer specified
|
7
|
+
class DefaultSerialWorker < SweatShop::Worker
|
8
|
+
def hello(name)
|
9
|
+
"Hi, #{name}"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Marshall specified serializer
|
14
|
+
class MarshalWorker < SweatShop::Worker
|
15
|
+
serialize_with :marshal
|
16
|
+
def hello(name)
|
17
|
+
"Hi, #{name}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# JSON specified serializer
|
22
|
+
class JsonWorker < SweatShop::Worker
|
23
|
+
serialize_with :json
|
24
|
+
def hello(name)
|
25
|
+
"Hi, #{name}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# YAML specified serializer
|
30
|
+
class YamlWorker < SweatShop::Worker
|
31
|
+
serialize_with :yaml
|
32
|
+
def hello(name)
|
33
|
+
"Hi, #{name}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class NoNameSerializer < SweatShop::Serializer
|
38
|
+
def self.serialize(payload)
|
39
|
+
"x"
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.deserialize(payload)
|
43
|
+
"y"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class NamedSerializer < SweatShop::Serializer
|
48
|
+
serializer_name :silly
|
49
|
+
def self.serialize(payload)
|
50
|
+
"x"
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.deserialize(payload)
|
54
|
+
"y"
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
test "should use default serializer if none is specified" do
|
60
|
+
assert_equal SweatShop::Serializers::MarshalSerializer, DefaultSerialWorker.serializer
|
61
|
+
end
|
62
|
+
|
63
|
+
test "should mark marshal as the default serializer out of the box" do
|
64
|
+
assert_equal SweatShop::Serializer.default, SweatShop::Serializers::MarshalSerializer
|
65
|
+
end
|
66
|
+
test "should be able to specify the default serializer" do
|
67
|
+
SweatShop::Serializer.default = :json
|
68
|
+
assert_equal SweatShop::Serializer.default, SweatShop::Serializers::JsonSerializer
|
69
|
+
SweatShop::Serializer.default = :marshal # set it back so tests don't break
|
70
|
+
end
|
71
|
+
|
72
|
+
test "should be able to specify json as the serializer on a per class basis" do
|
73
|
+
assert_equal JsonWorker.serializer, SweatShop::Serializers::JsonSerializer
|
74
|
+
end
|
75
|
+
|
76
|
+
test "should be able to specify marshal as the serializer on a per class basis" do
|
77
|
+
assert_equal MarshalWorker.serializer, SweatShop::Serializers::MarshalSerializer
|
78
|
+
end
|
79
|
+
|
80
|
+
test "should be able to specify yaml as the serializer on a per class basis" do
|
81
|
+
assert_equal YamlWorker.serializer, SweatShop::Serializers::YamlSerializer
|
82
|
+
end
|
83
|
+
|
84
|
+
test "should provide a default short name based on class name" do
|
85
|
+
assert_equal :no_name, NoNameSerializer.get_name
|
86
|
+
end
|
87
|
+
|
88
|
+
test "should accept a custom short name" do
|
89
|
+
assert_equal :silly, NamedSerializer.get_name
|
90
|
+
end
|
91
|
+
|
92
|
+
test "should register all available serializers" do
|
93
|
+
assert_equal ["json", "marshal", "no_name", "silly", "yaml"], SweatShop::Serializer.serializers.keys.map{|k| k.to_s}.sort
|
94
|
+
end
|
95
|
+
end
|