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.
@@ -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