cloudist 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,4 +3,4 @@ module Cloudist
3
3
  class BadPayload < Error; end
4
4
  class EnqueueError < Error; end
5
5
  class StaleHeadersError < BadPayload; end
6
- end
6
+ end
data/lib/cloudist/job.rb CHANGED
@@ -21,7 +21,7 @@ module Cloudist
21
21
 
22
22
  end
23
23
 
24
- def reply(data, headers = {}, options = {})
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
- data.merge!(payload.body) if options[:echo] == true
35
+ # body.merge!(payload.body) if options[:echo] == true
36
36
 
37
- reply_payload = Payload.new(data, headers)
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: #{data.inspect} - Headers: #{published_headers.inspect}")
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)
@@ -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
- (@callbacks[key] ||= []) << Callback.new(blk)
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
@@ -6,15 +6,16 @@ module Cloudist
6
6
 
7
7
  attr_reader :body, :publish_opts, :headers
8
8
 
9
- def initialize(data_hash_or_json, headers = {}, publish_opts = {})
9
+ def initialize(body, headers = {}, publish_opts = {})
10
10
  @publish_opts, @headers = publish_opts, headers
11
11
  @published = false
12
12
 
13
- data_hash_or_json = parse_message(data_hash_or_json) if data_hash_or_json.is_a?(String)
13
+ body = parse_message(body) if body.is_a?(String)
14
14
 
15
- raise Cloudist::BadPayload, "Expected Hash for payload" unless data_hash_or_json.is_a?(Hash)
15
+ # raise Cloudist::BadPayload, "Expected Hash for payload" unless body.is_a?(Hash)
16
16
 
17
- @body = HashWithIndifferentAccess.new(data_hash_or_json)
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.to_json, publish_opts]
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('published_on') || Time.now.utc.to_i
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('event_hash') || create_event_hash
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
@@ -1,5 +1,5 @@
1
1
  module Cloudist
2
- class ReplyQueue < Cloudist::BasicQueue
2
+ class ReplyQueue < Cloudist::Queues::BasicQueue
3
3
  def initialize(queue_name, opts={})
4
4
  opts[:auto_delete] = true
5
5
  opts[:nowait] = false
@@ -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, json_encoded_message, queue_header)
5
+ def initialize(queue, encoded_body, queue_header)
6
6
  @qobj, @queue_header = queue, queue_header
7
7
 
8
- @payload = Cloudist::Payload.new(json_encoded_message.dup, queue_header.headers.dup)
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
@@ -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
@@ -1,24 +1,33 @@
1
1
  module Cloudist
2
2
  class Worker
3
3
 
4
- attr_reader :options
4
+ attr_reader :job, :queue
5
5
 
6
- def initialize(options)
7
- @options = options
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
- def job(queue_name, &block)
15
- q = JobQueue.new(queue_name)
16
- q.subscribe do |request|
17
- j = Job.new(request.payload.dup)
18
- j.instance_eval(&block)
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
- lambda {
6
- Cloudist::Payload.new([1,2,3])
7
- }.should raise_error(Cloudist::BadPayload)
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 == {"bread"=>"white"}
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 == {"bread"=>"white"}
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 == {"bread"=>"white"}
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'}.to_json)
41
- payload.body.should == {"bread"=>"white"}
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'}.to_json, {:published_on => 12345})
46
- payload.parse_custom_headers.should == {:published_on=>12345, :event_hash=>"foo", :content_type=>"application/json", :message_id=>"foo", :ttl=>300}
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
- json, popts = payload.formatted
68
+ body, popts = payload.formatted
69
69
  headers = popts[:headers]
70
70
 
71
- json.should == "{\"bread\":\"white\"}"
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'}, {:reply_type => 'event'})
123
- payload.headers[:reply_type].should == 'event'
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