cloudist 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,91 @@
1
+ module Cloudist
2
+ class UnknownReplyTo < RuntimeError; end
3
+ class ExpiredMessage < RuntimeError; end
4
+
5
+ class BasicQueue
6
+ attr_reader :queue_name, :opts
7
+ attr_reader :q, :ex, :mq
8
+
9
+ def initialize(queue_name, opts = {})
10
+ opts = {
11
+ :auto_delete => true,
12
+ :durable => false,
13
+ :prefetch => 1
14
+ }.update(opts)
15
+
16
+ @queue_name, @opts = queue_name, opts
17
+ end
18
+
19
+ def setup
20
+ return if @setup == true
21
+
22
+ @mq = MQ.new
23
+ @q = @mq.queue(queue_name, opts)
24
+ # if we don't specify an exchange name it defaults to the queue_name
25
+ @ex = @mq.direct(opts[:exchange_name] || queue_name)
26
+
27
+ q.bind(ex) if ex
28
+
29
+ @setup = true
30
+ end
31
+
32
+ def log
33
+ Cloudist.log
34
+ end
35
+
36
+ def tag
37
+ s = "queue=#{q.name}"
38
+ s += " exchange=#{ex.name}" if ex
39
+ s
40
+ end
41
+
42
+ def subscribe(amqp_opts={}, opts={})
43
+ setup
44
+
45
+ q.subscribe(amqp_opts) do |queue_header, json_encoded_message|
46
+ return if Cloudist.closing?
47
+
48
+ request = Cloudist::Request.new(self, json_encoded_message, queue_header)
49
+
50
+ begin
51
+ raise Cloudist::ExpiredMessage if request.expired?
52
+ yield request if block_given?
53
+ finished = Time.now.utc.to_i
54
+
55
+ rescue Cloudist::ExpiredMessage
56
+ log.info "amqp_message action=timeout #{tag} ttl=#{request.ttl} age=#{request.age} #{request.inspect}"
57
+ request.ack if amqp_opts[:ack]
58
+
59
+ rescue => e
60
+ request.ack if amqp_opts[:ack]
61
+ Cloudist.handle_error(e)
62
+ end
63
+ end
64
+ log.info "amqp_subscribe #{tag}"
65
+ self
66
+ end
67
+
68
+ def publish(payload)
69
+ payload.set_reply_to(queue_name)
70
+ body, headers = payload.formatted
71
+ ex.publish(body, headers)
72
+ payload.publish
73
+ end
74
+
75
+ def publish_to_q(payload)
76
+ body, headers = payload.formatted
77
+ q.publish(body, headers)
78
+ payload.publish
79
+ end
80
+
81
+ def teardown
82
+ @q.unsubscribe
83
+ @mq.close
84
+ log.debug "amqp_unsubscribe #{tag}"
85
+ end
86
+
87
+ def destroy
88
+ teardown
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,11 @@
1
+ class String
2
+ # Returns true iff +other+ appears exactly at the start of +self+.
3
+ def starts_with? other
4
+ self[0, other.length] == other
5
+ end
6
+
7
+ # Returns true iff +other+ appears exactly at the end of +self+.
8
+ def ends_with? other
9
+ self[-other.length, other.length] == other
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module Cloudist
2
+ class Error < RuntimeError; end
3
+ class BadPayload < Error; end
4
+ class EnqueueError < Error; end
5
+ class StaleHeadersError < BadPayload; end
6
+ end
@@ -0,0 +1,54 @@
1
+ module Cloudist
2
+ class Job
3
+ attr_reader :payload
4
+ def initialize(payload)
5
+ @payload = payload
6
+ end
7
+
8
+ def id
9
+ payload.id
10
+ end
11
+
12
+ def data
13
+ payload.body
14
+ end
15
+
16
+ def log
17
+ Cloudist.log
18
+ end
19
+
20
+ def cleanup
21
+
22
+ end
23
+
24
+ def reply(data, headers = {})
25
+ # headers.update(:message_id => payload.headers[:message_id])
26
+ headers = {
27
+ :message_id => payload.headers[:message_id],
28
+ :reply_type => "reply"
29
+ }.update(headers)
30
+
31
+ reply_payload = Payload.new(data, headers)
32
+
33
+ reply_queue = ReplyQueue.new(payload.reply_to)
34
+ reply_queue.setup
35
+ reply_queue.publish_to_q(reply_payload)
36
+
37
+ # log.debug("Replying: #{data.inspect} - Payload: #{reply_payload.inspect}")
38
+ end
39
+
40
+ def event(event_name, data = {})
41
+ data = {} unless data
42
+ reply({:event => event_name}.merge(payload.body), {:reply_type => "event"})
43
+ end
44
+
45
+ def method_missing(meth, *args, &blk)
46
+ if meth.to_s.ends_with?("!")
47
+ event(meth.to_s.gsub(/(!)$/, ''), args.shift)
48
+ else
49
+ super
50
+ end
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ module Cloudist
2
+ class JobQueue < BasicQueue
3
+ attr_reader :prefetch
4
+
5
+ def initialize(queue_name, opts={})
6
+ @prefetch = opts.delete(:prefetch) || 1
7
+ opts[:auto_delete] = false
8
+
9
+ super(queue_name, opts)
10
+ end
11
+
12
+ def setup
13
+ super
14
+ @mq.prefetch(self.prefetch)
15
+ end
16
+
17
+ def subscribe(amqp_opts={}, opts={})
18
+ amqp_opts[:ack] = true
19
+ super(amqp_opts, opts) do |request|
20
+ begin
21
+ yield request if block_given?
22
+ ensure
23
+ request.ack unless amqp_opts[:auto_ack] == false
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Cloudist
2
+ class Listener
3
+
4
+ attr_reader :job_queue_name, :job_id
5
+
6
+ def initialize(job_or_queue_name)
7
+ if job_or_queue_name.is_a?(Cloudist::Job)
8
+ @job_queue_name = Utils.reply_prefix(job_or_queue_name.payload.headers[:master_queue])
9
+ @job_id = job_or_queue_name.id
10
+ elsif job_or_queue_name.is_a?(String)
11
+ @job_queue_name = Utils.reply_prefix(job_or_queue_name)
12
+ @job_id = nil
13
+ else
14
+ raise ArgumentError, "Invalid listener type, accepts job queue name or Cloudist::Job instance"
15
+ end
16
+ end
17
+
18
+ def subscribe(&block)
19
+ reply_queue = Cloudist::ReplyQueue.new(job_queue_name)
20
+ reply_queue.setup(job_id) if job_id
21
+
22
+ reply_queue.subscribe do |request|
23
+ job = Job.new(request.payload)
24
+
25
+ job.instance_eval(&block)
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,128 @@
1
+ module Cloudist
2
+ DEFAULT_TTL = 300
3
+
4
+ class Payload
5
+ include Utils
6
+
7
+ attr_accessor :body, :headers
8
+
9
+ def initialize(data_hash_or_json, headers = {})
10
+ data_hash_or_json = parse_message(data_hash_or_json) if data_hash_or_json.is_a?(String)
11
+
12
+ raise Cloudist::BadPayload, "Expected Hash for payload" unless data_hash_or_json.is_a?(Hash)
13
+
14
+ @body, @headers = HashWithIndifferentAccess.new(data_hash_or_json), headers
15
+ update_headers
16
+ end
17
+
18
+ def formatted
19
+ body, headers = apply_custom_headers
20
+
21
+ # Return message formatted as JSON and headers ready for transport
22
+ [body.to_json, headers]
23
+ end
24
+
25
+ def id
26
+ @id ||= event_hash.to_s
27
+ end
28
+
29
+ def id=(new_id)
30
+ @id = new_id.to_s
31
+ update_headers
32
+ end
33
+
34
+ def frozen?
35
+ headers.frozen?
36
+ end
37
+
38
+ def freeze!
39
+ headers.freeze
40
+ body.freeze
41
+ end
42
+
43
+ def update_headers
44
+ raise StaleHeadersError, "Headers cannot be changed because payload has already been published" if published?
45
+
46
+ headers[:published_on] ||= body.delete('published_on') || Time.now.utc.to_i
47
+ headers[:ttl] ||= body.delete('ttl') || Cloudist::DEFAULT_TTL
48
+
49
+ # this is the event hash that gets transferred through various publish/reply actions
50
+ headers[:event_hash] ||= id
51
+
52
+ # this value should be unique for each published/received message pair
53
+ headers[:message_id] ||= id
54
+
55
+ # We use JSON for message transport exclusively
56
+ headers[:content_type] ||= 'application/json'
57
+
58
+ # some strange behavior with integers makes it better to
59
+ # convert all amqp headers to strings to avoid any problems
60
+ headers.each { |k,v| headers[k] = v.to_s }
61
+ end
62
+
63
+ def apply_custom_headers
64
+ update_headers
65
+ [body, headers]
66
+ end
67
+
68
+ def parse_custom_headers
69
+ return { } unless headers
70
+
71
+ h = headers.dup
72
+
73
+ h[:published_on] = h[:published_on].to_i
74
+
75
+ h[:ttl] = h[:ttl].to_i rescue -1
76
+ h[:ttl] = -1 if h[:ttl] == 0
77
+
78
+ h
79
+ end
80
+
81
+ def set_reply_to(queue_name)
82
+ headers[:reply_to] = reply_name(queue_name)
83
+ set_master_queue_name(queue_name)
84
+ end
85
+
86
+ def set_master_queue_name(queue_name)
87
+ headers[:master_queue] = queue_name
88
+ end
89
+
90
+ def reply_name(queue_name)
91
+ # "#{queue_name}.#{id}"
92
+ Utils.reply_prefix(queue_name)
93
+ end
94
+
95
+ def reply_to
96
+ headers[:reply_to]
97
+ end
98
+
99
+ def event_hash
100
+ @event_hash ||= headers[:event_hash] || body.delete('event_hash') || create_event_hash
101
+ end
102
+
103
+ def create_event_hash
104
+ s = Time.now.to_s + object_id.to_s + rand(100).to_s
105
+ Digest::MD5.hexdigest(s)
106
+ end
107
+
108
+ def parse_message(raw)
109
+ return { } unless raw
110
+ JSON.parse(raw)
111
+ end
112
+
113
+ def [](key)
114
+ body[key]
115
+ end
116
+
117
+ def published?
118
+ @published == true
119
+ end
120
+
121
+ def publish
122
+ return if published?
123
+ @published = true
124
+ freeze!
125
+ end
126
+
127
+ end
128
+ end
@@ -0,0 +1,17 @@
1
+ module Cloudist
2
+ class Publisher
3
+
4
+ class << self
5
+ def enqueue(queue_name, data)
6
+ payload = Cloudist::Payload.new(data)
7
+
8
+ queue = Cloudist::JobQueue.new(queue_name)
9
+ queue.setup
10
+ queue.publish(payload)
11
+
12
+ return Job.new(payload)
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ module Cloudist
2
+ class ReplyQueue < Cloudist::BasicQueue
3
+ def initialize(queue_name, opts={})
4
+ opts[:auto_delete] = true
5
+ super
6
+ end
7
+
8
+ def setup(key = nil)
9
+ @mq = MQ.new
10
+ @q = @mq.queue(queue_name, opts)
11
+ @ex = @mq.direct
12
+ if key
13
+ @q.bind(@ex, :key => key)
14
+ else
15
+ @q.bind(@ex)
16
+ end
17
+ end
18
+
19
+ # def subscribe(amqp_opts={}, opts={})
20
+ # super(amqp_opts, opts) do |request|
21
+ # yield request if block_given?
22
+ # self.destroy
23
+ # end
24
+ # end
25
+ #
26
+ # def teardown
27
+ # @q.delete
28
+ # super
29
+ # end
30
+
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ module Cloudist
2
+ class Request
3
+ attr_reader :queue_header, :qobj, :payload, :start, :headers
4
+
5
+ def initialize(queue, json_encoded_message, queue_header)
6
+ @qobj, @queue_header = queue, queue_header
7
+
8
+ @payload = Cloudist::Payload.new(json_encoded_message.dup, queue_header.properties.dup)
9
+ @headers = @payload.parse_custom_headers
10
+
11
+ @start = Time.now.utc.to_i
12
+ end
13
+
14
+ def q
15
+ qobj.q
16
+ end
17
+
18
+ def ex
19
+ qobj.ex
20
+ end
21
+
22
+ def mq
23
+ qobj.mq
24
+ end
25
+
26
+ def age
27
+ return -1 unless headers[:published_on]
28
+ start - headers[:published_on]
29
+ end
30
+
31
+ def ttl
32
+ headers[:ttl] || -1
33
+ end
34
+
35
+ def expired?
36
+ return false if ttl == -1
37
+ age > ttl
38
+ end
39
+
40
+ def acked?
41
+ @acked == true
42
+ end
43
+
44
+ def ack
45
+ return if acked?
46
+ queue_header.ack
47
+ @acked = true
48
+ end
49
+
50
+ end
51
+ end