cloudist 0.0.2

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