cloudist 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -2
- data/VERSION +1 -1
- data/cloudist.gemspec +13 -5
- data/examples/extending_values.rb +44 -0
- data/examples/sandwich_client.rb +18 -4
- data/examples/sandwich_worker.rb +12 -18
- data/examples/sandwich_worker_with_class.rb +37 -0
- data/lib/cloudist.rb +101 -17
- data/lib/cloudist/callbacks/error_callback.rb +14 -0
- data/lib/cloudist/core_ext/object.rb +81 -0
- data/lib/cloudist/errors.rb +1 -1
- data/lib/cloudist/job.rb +19 -4
- data/lib/cloudist/listener.rb +14 -2
- data/lib/cloudist/payload.rb +21 -11
- data/lib/cloudist/queues/basic_queue.rb +101 -0
- data/lib/cloudist/{job_queue.rb → queues/job_queue.rb} +2 -2
- data/lib/cloudist/{reply_queue.rb → queues/reply_queue.rb} +1 -1
- data/lib/cloudist/request.rb +2 -2
- data/lib/cloudist/utils.rb +16 -0
- data/lib/cloudist/worker.rb +20 -11
- data/spec/cloudist/basic_queue_spec.rb +2 -2
- data/spec/cloudist/payload_spec.rb +23 -18
- data/spec/cloudist/request_spec.rb +2 -2
- data/spec/cloudist/utils_spec.rb +19 -0
- data/spec/cloudist_spec.rb +42 -11
- metadata +61 -53
- data/lib/cloudist/basic_queue.rb +0 -92
data/lib/cloudist/errors.rb
CHANGED
data/lib/cloudist/job.rb
CHANGED
@@ -21,7 +21,7 @@ module Cloudist
|
|
21
21
|
|
22
22
|
end
|
23
23
|
|
24
|
-
def reply(
|
24
|
+
def reply(body, headers = {}, options = {})
|
25
25
|
options = {
|
26
26
|
:echo => false
|
27
27
|
}.update(options)
|
@@ -32,15 +32,15 @@ module Cloudist
|
|
32
32
|
}.update(headers)
|
33
33
|
|
34
34
|
# Echo the payload back
|
35
|
-
|
35
|
+
# body.merge!(payload.body) if options[:echo] == true
|
36
36
|
|
37
|
-
reply_payload = Payload.new(
|
37
|
+
reply_payload = Payload.new(body, headers)
|
38
38
|
|
39
39
|
reply_queue = ReplyQueue.new(payload.reply_to)
|
40
40
|
reply_queue.setup
|
41
41
|
published_headers = reply_queue.publish_to_q(reply_payload)
|
42
42
|
|
43
|
-
log.debug("Replying: #{
|
43
|
+
log.debug("Replying: #{body.inspect} HEADERS: #{headers.inspect}")
|
44
44
|
end
|
45
45
|
|
46
46
|
# Sends a progress update
|
@@ -55,6 +55,21 @@ module Cloudist
|
|
55
55
|
reply(event_data, {:event => event_name, :message_type => 'event'}, options)
|
56
56
|
end
|
57
57
|
|
58
|
+
def safely(&blk)
|
59
|
+
# begin
|
60
|
+
yield
|
61
|
+
rescue Exception => e
|
62
|
+
handle_error(e)
|
63
|
+
# end
|
64
|
+
# result
|
65
|
+
end
|
66
|
+
|
67
|
+
# This will transfer the Exception object to the client
|
68
|
+
def handle_error(e)
|
69
|
+
# reply({:exception_class => e.class.name, :message => e.message, :backtrace => e.backtrace}, {:message_type => 'error'})
|
70
|
+
reply({:exception => e}, {:message_type => 'error'})
|
71
|
+
end
|
72
|
+
|
58
73
|
def method_missing(meth, *args, &blk)
|
59
74
|
if meth.to_s.ends_with?("!")
|
60
75
|
event(meth.to_s.gsub(/(!)$/, ''), args.shift)
|
data/lib/cloudist/listener.rb
CHANGED
@@ -4,7 +4,7 @@ module Cloudist
|
|
4
4
|
|
5
5
|
attr_reader :job_queue_name, :job_id, :callbacks
|
6
6
|
|
7
|
-
@@valid_callbacks = ["event", "progress", "reply", "update"]
|
7
|
+
@@valid_callbacks = ["event", "progress", "reply", "update", "error"]
|
8
8
|
|
9
9
|
def initialize(job_or_queue_name)
|
10
10
|
@callbacks = {}
|
@@ -38,6 +38,13 @@ module Cloudist
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
+
if callbacks.has_key?('error')
|
42
|
+
callbacks['error'].each do |c|
|
43
|
+
# c.call(payload)
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
41
48
|
if callbacks.has_key?(key)
|
42
49
|
callbacks_to_call = callbacks[key]
|
43
50
|
callbacks_to_call.each do |c|
|
@@ -57,7 +64,12 @@ module Cloudist
|
|
57
64
|
# callback should in format of "event:started" or "progress"
|
58
65
|
key = [meth.to_s, args.shift].compact.join(':')
|
59
66
|
|
60
|
-
|
67
|
+
case meth.to_sym
|
68
|
+
when :error
|
69
|
+
(@callbacks[key] ||= []) << ErrorCallback.new(blk)
|
70
|
+
else
|
71
|
+
(@callbacks[key] ||= []) << Callback.new(blk)
|
72
|
+
end
|
61
73
|
else
|
62
74
|
super
|
63
75
|
end
|
data/lib/cloudist/payload.rb
CHANGED
@@ -6,15 +6,16 @@ module Cloudist
|
|
6
6
|
|
7
7
|
attr_reader :body, :publish_opts, :headers
|
8
8
|
|
9
|
-
def initialize(
|
9
|
+
def initialize(body, headers = {}, publish_opts = {})
|
10
10
|
@publish_opts, @headers = publish_opts, headers
|
11
11
|
@published = false
|
12
12
|
|
13
|
-
|
13
|
+
body = parse_message(body) if body.is_a?(String)
|
14
14
|
|
15
|
-
raise Cloudist::BadPayload, "Expected Hash for payload" unless
|
15
|
+
# raise Cloudist::BadPayload, "Expected Hash for payload" unless body.is_a?(Hash)
|
16
16
|
|
17
|
-
@body =
|
17
|
+
@body = body
|
18
|
+
# HashWithIndifferentAccess.new(body)
|
18
19
|
update_headers
|
19
20
|
end
|
20
21
|
|
@@ -22,7 +23,7 @@ module Cloudist
|
|
22
23
|
def formatted
|
23
24
|
update_headers
|
24
25
|
|
25
|
-
[body
|
26
|
+
[encode_message(body), publish_opts]
|
26
27
|
end
|
27
28
|
|
28
29
|
def id
|
@@ -50,8 +51,8 @@ module Cloudist
|
|
50
51
|
|
51
52
|
def extract_custom_headers
|
52
53
|
raise StaleHeadersError, "Headers cannot be changed because payload has already been published" if published?
|
53
|
-
headers[:published_on] ||= body.delete(
|
54
|
-
headers[:ttl] ||= body.delete('ttl') || Cloudist::DEFAULT_TTL
|
54
|
+
headers[:published_on] ||= body.is_a?(Hash) && body.delete(:published_on) || Time.now.utc.to_i
|
55
|
+
headers[:ttl] ||= body.is_a?(Hash) && body.delete('ttl') || Cloudist::DEFAULT_TTL
|
55
56
|
|
56
57
|
# this is the event hash that gets transferred through various publish/reply actions
|
57
58
|
headers[:event_hash] ||= id
|
@@ -60,7 +61,7 @@ module Cloudist
|
|
60
61
|
headers[:message_id] ||= id
|
61
62
|
|
62
63
|
# We use JSON for message transport exclusively
|
63
|
-
headers[:content_type] ||= 'application/json'
|
64
|
+
# headers[:content_type] ||= 'application/json'
|
64
65
|
|
65
66
|
# headers[:headers][:message_type] = 'event'
|
66
67
|
# ||= body.delete('message_type') || 'reply'
|
@@ -110,7 +111,7 @@ module Cloudist
|
|
110
111
|
end
|
111
112
|
|
112
113
|
def event_hash
|
113
|
-
@event_hash ||= headers[:event_hash] || body.delete(
|
114
|
+
@event_hash ||= headers[:event_hash] || body.is_a?(Hash) && body.delete(:event_hash) || create_event_hash
|
114
115
|
end
|
115
116
|
|
116
117
|
def create_event_hash
|
@@ -119,8 +120,9 @@ module Cloudist
|
|
119
120
|
end
|
120
121
|
|
121
122
|
def parse_message(raw)
|
122
|
-
return { } unless raw
|
123
|
-
decode_json(raw)
|
123
|
+
# return { } unless raw
|
124
|
+
# decode_json(raw)
|
125
|
+
decode_message(raw)
|
124
126
|
end
|
125
127
|
|
126
128
|
def [](key)
|
@@ -137,5 +139,13 @@ module Cloudist
|
|
137
139
|
freeze!
|
138
140
|
end
|
139
141
|
|
142
|
+
def method_missing(meth, *args, &blk)
|
143
|
+
if body.is_a?(Hash) && body.has_key?(meth)
|
144
|
+
return body[meth]
|
145
|
+
else
|
146
|
+
super
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
140
150
|
end
|
141
151
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Cloudist
|
2
|
+
class UnknownReplyTo < RuntimeError; end
|
3
|
+
class ExpiredMessage < RuntimeError; end
|
4
|
+
|
5
|
+
module Queues
|
6
|
+
class BasicQueue
|
7
|
+
attr_reader :queue_name, :opts
|
8
|
+
attr_reader :q, :ex, :mq
|
9
|
+
|
10
|
+
def initialize(queue_name, opts = {})
|
11
|
+
opts = {
|
12
|
+
:auto_delete => true,
|
13
|
+
:durable => false,
|
14
|
+
:prefetch => 1
|
15
|
+
}.update(opts)
|
16
|
+
|
17
|
+
@queue_name, @opts = queue_name, opts
|
18
|
+
end
|
19
|
+
|
20
|
+
def setup
|
21
|
+
return if @setup == true
|
22
|
+
|
23
|
+
@mq = MQ.new
|
24
|
+
@q = @mq.queue(queue_name, opts)
|
25
|
+
# if we don't specify an exchange name it defaults to the queue_name
|
26
|
+
@ex = @mq.direct(opts[:exchange_name] || queue_name)
|
27
|
+
|
28
|
+
q.bind(ex) if ex
|
29
|
+
|
30
|
+
@setup = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def log
|
34
|
+
Cloudist.log
|
35
|
+
end
|
36
|
+
|
37
|
+
def tag
|
38
|
+
s = "queue=#{q.name}"
|
39
|
+
s += " exchange=#{ex.name}" if ex
|
40
|
+
s
|
41
|
+
end
|
42
|
+
|
43
|
+
def subscribe(amqp_opts={}, opts={})
|
44
|
+
setup
|
45
|
+
print_status
|
46
|
+
q.subscribe(amqp_opts) do |queue_header, json_encoded_message|
|
47
|
+
next if Cloudist.closing?
|
48
|
+
|
49
|
+
request = Cloudist::Request.new(self, json_encoded_message, queue_header)
|
50
|
+
|
51
|
+
begin
|
52
|
+
raise Cloudist::ExpiredMessage if request.expired?
|
53
|
+
yield request if block_given?
|
54
|
+
# finished = Time.now.utc.to_i
|
55
|
+
# log.debug("Finished Job in #{finished - request.start} seconds")
|
56
|
+
|
57
|
+
rescue Cloudist::ExpiredMessage
|
58
|
+
log.error "AMQP Message Timeout: #{tag} ttl=#{request.ttl} age=#{request.age}"
|
59
|
+
request.ack if amqp_opts[:ack]
|
60
|
+
|
61
|
+
rescue => e
|
62
|
+
request.ack if amqp_opts[:ack]
|
63
|
+
Cloudist.handle_error(e)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
log.info "AMQP Subscribed: #{tag}"
|
67
|
+
self
|
68
|
+
end
|
69
|
+
|
70
|
+
def print_status
|
71
|
+
q.status{ |num_messages, num_consumers|
|
72
|
+
log.info("STATUS: #{q.name}: JOBS: #{num_messages} WORKERS: #{num_consumers+1}")
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def publish(payload)
|
77
|
+
payload.set_reply_to(queue_name)
|
78
|
+
body, headers = payload.formatted
|
79
|
+
ex.publish(body, headers)
|
80
|
+
payload.publish
|
81
|
+
end
|
82
|
+
|
83
|
+
def publish_to_q(payload)
|
84
|
+
body, headers = payload.formatted
|
85
|
+
q.publish(body, headers)
|
86
|
+
payload.publish
|
87
|
+
return headers
|
88
|
+
end
|
89
|
+
|
90
|
+
def teardown
|
91
|
+
@q.unsubscribe
|
92
|
+
@mq.close
|
93
|
+
log.debug "AMQP Unsubscribed: #{tag}"
|
94
|
+
end
|
95
|
+
|
96
|
+
def destroy
|
97
|
+
teardown
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Cloudist
|
2
|
-
class JobQueue < BasicQueue
|
2
|
+
class JobQueue < Cloudist::Queues::BasicQueue
|
3
3
|
attr_reader :prefetch
|
4
4
|
|
5
5
|
def initialize(queue_name, opts={})
|
@@ -20,7 +20,7 @@ module Cloudist
|
|
20
20
|
begin
|
21
21
|
yield request if block_given?
|
22
22
|
ensure
|
23
|
-
request.ack unless amqp_opts[:auto_ack] == false
|
23
|
+
request.ack unless amqp_opts[:auto_ack] == false || Cloudist.closing?
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
data/lib/cloudist/request.rb
CHANGED
@@ -2,10 +2,10 @@ module Cloudist
|
|
2
2
|
class Request
|
3
3
|
attr_reader :queue_header, :qobj, :payload, :start, :headers
|
4
4
|
|
5
|
-
def initialize(queue,
|
5
|
+
def initialize(queue, encoded_body, queue_header)
|
6
6
|
@qobj, @queue_header = queue, queue_header
|
7
7
|
|
8
|
-
@payload = Cloudist::Payload.new(
|
8
|
+
@payload = Cloudist::Payload.new(encoded_body, queue_header.headers.dup)
|
9
9
|
@headers = @payload.parse_custom_headers
|
10
10
|
|
11
11
|
@start = Time.now.utc.to_i
|
data/lib/cloudist/utils.rb
CHANGED
@@ -6,6 +6,14 @@ module Cloudist
|
|
6
6
|
"temp.reply.#{name}"
|
7
7
|
end
|
8
8
|
|
9
|
+
def log_prefix(name)
|
10
|
+
"temp.log.#{name}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def stats_prefix(name)
|
14
|
+
"temp.stats.#{name}"
|
15
|
+
end
|
16
|
+
|
9
17
|
def generate_queue(exchange_name, second_name=nil)
|
10
18
|
second_name ||= $$
|
11
19
|
"#{generate_name_for_instance(exchange_name)}.#{second_name}"
|
@@ -33,6 +41,14 @@ module Cloudist
|
|
33
41
|
"%04x%04x%04x%04x%04x%06x%06x" % values
|
34
42
|
end
|
35
43
|
|
44
|
+
def encode_message(object)
|
45
|
+
Marshal.dump(object).to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
def decode_message(string)
|
49
|
+
Marshal.load(string)
|
50
|
+
end
|
51
|
+
|
36
52
|
def decode_json(string)
|
37
53
|
if defined? ActiveSupport::JSON
|
38
54
|
ActiveSupport::JSON.decode string
|
data/lib/cloudist/worker.rb
CHANGED
@@ -1,24 +1,33 @@
|
|
1
1
|
module Cloudist
|
2
2
|
class Worker
|
3
3
|
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :job, :queue
|
5
5
|
|
6
|
-
def initialize(
|
7
|
-
@
|
6
|
+
def initialize(job, queue)
|
7
|
+
@job, @queue = job, queue
|
8
|
+
end
|
9
|
+
|
10
|
+
def data
|
11
|
+
job.data
|
12
|
+
end
|
13
|
+
|
14
|
+
def headers
|
15
|
+
job.headers
|
16
|
+
end
|
17
|
+
|
18
|
+
def process
|
19
|
+
raise NotImplementedError, "Your worker class must subclass this method"
|
8
20
|
end
|
9
21
|
|
10
22
|
def log
|
11
23
|
Cloudist.log
|
12
24
|
end
|
13
25
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
j.cleanup
|
20
|
-
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class GenericWorker < Worker
|
29
|
+
def process(&block)
|
30
|
+
instance_eval(&block)
|
21
31
|
end
|
22
|
-
|
23
32
|
end
|
24
33
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
2
|
|
3
3
|
describe "Cloudist" do
|
4
|
-
describe "BasicQueue" do
|
4
|
+
describe "Cloudist::Queues::BasicQueue" do
|
5
5
|
before(:each) do
|
6
6
|
overload_amqp
|
7
7
|
reset_broker
|
@@ -16,7 +16,7 @@ describe "Cloudist" do
|
|
16
16
|
@queue.expects(:bind).with(@exchange)
|
17
17
|
# @mq.expects(:queue).with("make.sandwich")
|
18
18
|
|
19
|
-
bq = Cloudist::BasicQueue.new("make.sandwich")
|
19
|
+
bq = Cloudist::Queues::BasicQueue.new("make.sandwich")
|
20
20
|
bq.stubs(:q).returns(@queue)
|
21
21
|
bq.stubs(:mq).returns(@mq)
|
22
22
|
bq.stubs(:ex).returns(@exchange)
|
@@ -1,11 +1,11 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
|
2
2
|
|
3
3
|
describe Cloudist::Payload do
|
4
|
-
it "should raise bad payload error unless data is a hash" do
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
4
|
+
# it "should raise bad payload error unless data is a hash" do
|
5
|
+
# lambda {
|
6
|
+
# Cloudist::Payload.new([1,2,3])
|
7
|
+
# }.should raise_error(Cloudist::BadPayload)
|
8
|
+
# end
|
9
9
|
|
10
10
|
it "should accept a hash for data" do
|
11
11
|
lambda {
|
@@ -15,10 +15,10 @@ describe Cloudist::Payload do
|
|
15
15
|
|
16
16
|
it "should prepare headers" do
|
17
17
|
payload = Cloudist::Payload.new({:bread => 'white'})
|
18
|
-
payload.body.should == {
|
18
|
+
payload.body.should == {:bread => "white"}
|
19
19
|
payload.headers.has_key?(:ttl).should be_true
|
20
|
-
payload.headers.has_key?(:content_type).should be_true
|
21
|
-
payload.headers[:content_type].should == "application/json"
|
20
|
+
# payload.headers.has_key?(:content_type).should be_true
|
21
|
+
# payload.headers[:content_type].should == "application/json"
|
22
22
|
payload.headers.has_key?(:published_on).should be_true
|
23
23
|
payload.headers.has_key?(:event_hash).should be_true
|
24
24
|
payload.headers.has_key?(:message_id).should be_true
|
@@ -26,24 +26,24 @@ describe Cloudist::Payload do
|
|
26
26
|
|
27
27
|
it "should extract published_on from data" do
|
28
28
|
payload = Cloudist::Payload.new({:bread => 'white', :published_on => 12345678})
|
29
|
-
payload.body.should == {
|
29
|
+
payload.body.should == {:bread=>"white"}
|
30
30
|
payload.headers[:published_on].should == "12345678"
|
31
31
|
end
|
32
32
|
|
33
33
|
it "should extract custom event hash from data" do
|
34
34
|
payload = Cloudist::Payload.new({:bread => 'white', :event_hash => 'foo'})
|
35
|
-
payload.body.should == {
|
35
|
+
payload.body.should == {:bread=>"white"}
|
36
36
|
payload.headers[:event_hash].should == "foo"
|
37
37
|
end
|
38
38
|
|
39
39
|
it "should parse JSON message" do
|
40
|
-
payload = Cloudist::Payload.new({:bread => 'white', :event_hash => 'foo'}
|
41
|
-
payload.body.should == {
|
40
|
+
payload = Cloudist::Payload.new(Marshal.dump({:bread => 'white', :event_hash => 'foo'}))
|
41
|
+
payload.body.should == {:bread => "white"}
|
42
42
|
end
|
43
43
|
|
44
44
|
it "should parse custom headers" do
|
45
|
-
payload = Cloudist::Payload.new({:bread => 'white', :event_hash => 'foo'}
|
46
|
-
payload.parse_custom_headers.should == {:published_on=>12345, :event_hash=>"foo", :
|
45
|
+
payload = Cloudist::Payload.new(Marshal.dump({:bread => 'white', :event_hash => 'foo'}), {:published_on => 12345})
|
46
|
+
payload.parse_custom_headers.should == {:published_on=>12345, :event_hash=>"foo", :message_id=>"foo", :ttl=>300}
|
47
47
|
end
|
48
48
|
|
49
49
|
it "should create a unique event hash" do
|
@@ -65,10 +65,10 @@ describe Cloudist::Payload do
|
|
65
65
|
|
66
66
|
it "should format payload for sending" do
|
67
67
|
payload = Cloudist::Payload.new({:bread => 'white'}, {:event_hash => 'foo', :message_type => 'reply'})
|
68
|
-
|
68
|
+
body, popts = payload.formatted
|
69
69
|
headers = popts[:headers]
|
70
70
|
|
71
|
-
|
71
|
+
body.should == Marshal.dump({:bread => 'white'})
|
72
72
|
headers[:ttl].should == "300"
|
73
73
|
headers[:message_type].should == 'reply'
|
74
74
|
end
|
@@ -119,8 +119,13 @@ describe Cloudist::Payload do
|
|
119
119
|
end
|
120
120
|
|
121
121
|
it "should allow custom headers to be set" do
|
122
|
-
payload = Cloudist::Payload.new({:bread => 'white'}, {:
|
123
|
-
payload.headers[:
|
122
|
+
payload = Cloudist::Payload.new({:bread => 'white'}, {:message_type => 'event'})
|
123
|
+
payload.headers[:message_type].should == 'event'
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should be able to transport an error" do
|
127
|
+
e = ArgumentError.new("FAILED")
|
128
|
+
payload = Cloudist::Payload.new(e, {:message_type => 'error'})
|
124
129
|
end
|
125
130
|
|
126
131
|
end
|