cloudist 0.1.2 → 0.2.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/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
|