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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +61 -0
- data/LICENSE.txt +20 -0
- data/README.md +113 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/examples/queue_message.rb +19 -0
- data/examples/sandwich_client.rb +17 -0
- data/examples/sandwich_worker.rb +21 -0
- data/lib/cloudist.rb +141 -0
- data/lib/cloudist/basic_queue.rb +91 -0
- data/lib/cloudist/core_ext/string.rb +11 -0
- data/lib/cloudist/errors.rb +6 -0
- data/lib/cloudist/job.rb +54 -0
- data/lib/cloudist/job_queue.rb +28 -0
- data/lib/cloudist/listener.rb +30 -0
- data/lib/cloudist/payload.rb +128 -0
- data/lib/cloudist/publisher.rb +17 -0
- data/lib/cloudist/reply_queue.rb +32 -0
- data/lib/cloudist/request.rb +51 -0
- data/lib/cloudist/utils.rb +36 -0
- data/lib/cloudist/worker.rb +24 -0
- data/spec/cloudist/basic_queue_spec.rb +33 -0
- data/spec/cloudist/job_spec.rb +23 -0
- data/spec/cloudist/payload_spec.rb +126 -0
- data/spec/cloudist/request_spec.rb +41 -0
- data/spec/cloudist_spec.rb +24 -0
- data/spec/core_ext/string_spec.rb +16 -0
- data/spec/spec_helper.rb +15 -0
- metadata +270 -0
@@ -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
|
data/lib/cloudist/job.rb
ADDED
@@ -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
|