cloud-queues 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Andrew Regner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ cloud-queues
2
+ ================
3
+
4
+ Basic (unoffical) ruby interface into Rackspace Cloud Queues
@@ -0,0 +1,11 @@
1
+ require 'excon'
2
+
3
+ require 'json'
4
+ require 'uri'
5
+ require 'securerandom'
6
+
7
+ require_relative "cloud-queues/version"
8
+ require_relative "cloud-queues/client"
9
+ require_relative "cloud-queues/queue"
10
+ require_relative "cloud-queues/message"
11
+ require_relative "cloud-queues/claim"
@@ -0,0 +1,62 @@
1
+ module CloudQueues
2
+ class Claim
3
+ include Enumerable
4
+
5
+ attr_accessor :default_ttl
6
+
7
+ attr_reader :id
8
+
9
+ def initialize(queue, id, msgs)
10
+ @client = queue.client
11
+ @queue = queue.name
12
+ @id = id
13
+
14
+ # request the messages if we don't already have them
15
+ @messages = msgs || messages
16
+
17
+ @default_ttl = 43200 # 12 hours, server max
18
+ end
19
+
20
+ def queue
21
+ Queue.new(@client, @queue)
22
+ end
23
+
24
+ def each(&block)
25
+ @messages.each(&block)
26
+ end
27
+
28
+ def [](index)
29
+ @messages[index] rescue messages[index]
30
+ end
31
+
32
+ def messages
33
+ msgs = refresh["messages"]
34
+ @messages = msgs.map { |message| Message.new(queue, message) }
35
+ end
36
+
37
+ def update(options = {})
38
+ options = options.select { |opt| %w[ttl grace].include?(opt.to_s) }
39
+ options[:ttl] ||= @default_ttl unless options["ttl"]
40
+ @client.request(method: :patch, path: path, body: options, expects: 204) && true
41
+ end
42
+
43
+ def delete
44
+ @client.request(method: :delete, path: path, expects: 204) && true
45
+ end
46
+ alias_method :release, :delete
47
+
48
+ def path
49
+ "/queues/#{@queue}/claims/#{@id}"
50
+ end
51
+
52
+ def age; refresh["age"]; end
53
+ def ttl; refresh["ttl"]; end
54
+
55
+ private
56
+
57
+ def refresh
58
+ @client.request(method: :get, path: path).body
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,167 @@
1
+ module CloudQueues
2
+ class Client
3
+
4
+ attr_accessor :client_id
5
+ attr_accessor :token
6
+ attr_accessor :tenant
7
+
8
+ def initialize(options = {})
9
+ [:username, :api_key].each do |arg|
10
+ raise ArgumentError.new "#{arg} is a required argument." unless options[arg]
11
+ end if options[:token].nil? and options[:tenant].nil?
12
+
13
+ @client_id = SecureRandom.uuid
14
+
15
+ options.each_pair {|k, v| instance_variable_set("@#{k}".to_sym, v) }
16
+ authenticate!
17
+ end
18
+
19
+ def create(name)
20
+ request(method: :put, path: "/queues/#{name}", expects: 201)
21
+ Queue.new(self, name)
22
+ end
23
+
24
+ def get(name)
25
+ request(method: :head, path: "/queues/#{name}", expects: 204)
26
+ Queue.new(self, name)
27
+ end
28
+
29
+ def queues
30
+ response = request_all("queues", method: :get, path: "/queues", expects: [200, 204])
31
+
32
+ return [] if response.status == 204
33
+
34
+ response.body["queues"].map do |queue|
35
+ Queue.new(self, queue["name"])
36
+ end
37
+ end
38
+
39
+ def authenticate!
40
+ @client = Excon.new("https://identity.api.rackspacecloud.com")
41
+ @base_path = nil
42
+
43
+ if @token.nil?
44
+ request = {auth: {"RAX-KSKEY:apiKeyCredentials" => {username: @username, apiKey: @api_key}}}
45
+ response = request(method: :post, path: "/v2.0/tokens", body: request)
46
+ @token = response.body["access"]["token"]["id"]
47
+ else
48
+ # try the current token
49
+ request = {auth: {tenantId: @tenant, token: {id: @token}}}
50
+ response = request(method: :post, path: "/v2.0/tokens", body: request)
51
+ end
52
+
53
+ url_type = @internal ? "internalURL" : "publicURL"
54
+ queues = response.body["access"]["serviceCatalog"].select{|service| service["name"] == "cloudQueues" }
55
+ endpoints = queues[0]["endpoints"]
56
+
57
+ url = if @region.nil?
58
+ # pick the first region
59
+ # TODO when cloud queues goes GA, change this to response.body["access"]["user"]["RAX-AUTH:defaultRegion"]
60
+ endpoints[0][url_type].split('/')
61
+ else
62
+ endpoint = endpoints.select { |endpoint| endpoint["region"] == @region.to_s.upcase }
63
+ raise ArgumentError.new "Region #{@region.to_s.upcase} does not exist!" if endpoint.count == 0
64
+ endpoint[0][url_type].split('/')
65
+ end
66
+
67
+ host = url[0..2].join('/')
68
+ @base_path = "/" + url[3..-1].join('/')
69
+ @tenant = url[-1]
70
+
71
+ @client = Excon.new(host)
72
+ end
73
+
74
+ def request(options = {}, second_try = false)
75
+ if options[:body] and options[:body].class != String
76
+ options[:body] = options[:body].to_json
77
+ end
78
+
79
+ options[:path] = "#{@base_path}#{options[:path]}" unless options[:path].start_with?(@base_path)
80
+
81
+ options[:headers] ||= {}
82
+ options[:headers]["Content-Type"] = "application/json" if options[:body]
83
+ options[:headers]["Accept"] = "application/json"
84
+ options[:headers]["Client-ID"] = @client_id
85
+ options[:headers]["X-Auth-Token"] = @token if @token
86
+ options[:headers]["X-Project-ID"] = @tenant
87
+
88
+ options[:tcp_nodelay] = true if options[:tcp_nodelay].nil?
89
+ options[:expects] ||= 200
90
+
91
+ puts options if @debug
92
+
93
+ begin
94
+ response = @client.request(options)
95
+ rescue Excon::Errors::SocketError => e
96
+ raise unless e.message.include?("EOFError") or second_try
97
+
98
+ # this happens when the server closes the keep-alive socket and
99
+ # Excon doesn't realize it yet.
100
+ @client.reset
101
+ return request(options, true)
102
+ rescue Excon::Errors::Unauthorized => e
103
+ raise if second_try or @token.nil?
104
+
105
+ # Our @token probably expired, re-auth and try again
106
+ @token = nil
107
+ authenticate!
108
+ @client.reset # for good measure
109
+ return request(options, true)
110
+ end
111
+
112
+ begin
113
+ response.body = JSON.load(response.body) if (response.get_header("Content-Type") || "").include?("application/json")
114
+ rescue
115
+ # the api seems to like to give a json content type for a "204 no content" response
116
+ end
117
+
118
+ return response
119
+ end
120
+
121
+ def request_all(collection_key, options = {})
122
+ begin
123
+ absolute_limit = options[:query][:limit]
124
+ limit = [absolute_limit, 10].min
125
+ options[:query][:limit] = limit if options[:query][:limit]
126
+ rescue
127
+ absolute_limit = Float::INFINITY
128
+ limit = 10
129
+ end
130
+
131
+ response = request(options)
132
+
133
+ if collection_key and response.status != 204
134
+ # the next href link will have the query represented in it
135
+ options.delete :query
136
+
137
+ collection = response.body[collection_key]
138
+
139
+ while response.body[collection_key].count >= limit and collection.count < absolute_limit
140
+ next_link = response.body["links"].select{|l| l["rel"] == "next" }[0]["href"]
141
+ options[:path] = set_query_from(options[:path], next_link)
142
+
143
+ response = request(options)
144
+
145
+ break if response.status == 204
146
+ collection += response.body[collection_key]
147
+ end
148
+
149
+ response.body[collection_key] = collection
150
+ end
151
+
152
+ return response
153
+ end
154
+
155
+ private
156
+
157
+ # I would just like to comment that this is only necessary because the API does not return the correct
158
+ # href for the next page. The tenant id (account number) is missing from between the /v1 and /queues.
159
+ def set_query_from(original, new_uri)
160
+ original = URI.parse(original)
161
+ new_query = URI.parse(new_uri).query
162
+ original.query = new_query
163
+ return original.to_s
164
+ end
165
+
166
+ end
167
+ end
@@ -0,0 +1,51 @@
1
+ module CloudQueues
2
+ class Message
3
+
4
+ attr_reader :id, :body
5
+
6
+ def initialize(queue, message)
7
+ @client = queue.client
8
+ @queue = queue.name
9
+
10
+ href = URI.parse(message["href"])
11
+ @id = href.path.split('/')[-1]
12
+ @claim = href.query.match(/(^|&)claim_id=([^&]+)/)[2] rescue nil
13
+ @body = message["body"]
14
+ @age = message["age"]
15
+ @ttl = message["ttl"]
16
+ end
17
+
18
+ def delete!
19
+ @client.request(method: :delete, path: path, expects: 204) && true
20
+ end
21
+
22
+ def queue
23
+ Queue.new(@client, @queue)
24
+ end
25
+
26
+ def claim
27
+ @claim.nil? ? nil : Claim.new(queue, @claim, nil)
28
+ end
29
+
30
+ def to_hash
31
+ {ttl: @ttl, body: @body}
32
+ end
33
+
34
+ def age; @age; end
35
+ def ttl; @ttl; end
36
+
37
+ # beware using this path method. it could return a path with a query argument
38
+ # at the end. this is to ensure the claim_id is provided whenever operations
39
+ # against this message are performed, however it could end up causing string
40
+ # formatting problems depending on how it's used.
41
+ def path
42
+ query = @claim ? "?claim_id=#{@claim}" : ""
43
+ "/queues/#{@queue}/messages/#{@id}#{query}"
44
+ end
45
+
46
+ def [](key)
47
+ @body[key]
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,144 @@
1
+ module CloudQueues
2
+ class Queue
3
+
4
+ attr_accessor :default_ttl
5
+
6
+ attr_reader :name, :client
7
+
8
+ def initialize(client, name)
9
+ @client = client
10
+ @name = name
11
+
12
+ # TODO maybe make these defaults on the client or something, this isn't going
13
+ # to always work out the way we want
14
+ @default_ttl = 1209600 # 14 days, server max
15
+
16
+ @default_claim_ttl = 43200 # 12 hours, server max
17
+ @default_claim_grace = 300 # 5 minutes, arbitrary
18
+ end
19
+
20
+ def messages(options = {})
21
+ if options[:ids]
22
+ raise ArgumentError.new "Only 20 or less message IDs may be specified" if options[:ids].count > 20
23
+ allowed_query = %w[ids claim_id]
24
+ else
25
+ allowed_query = %w[marker limit echo include_claimed]
26
+ end
27
+
28
+ options = options.select { |opt| allowed_query.include?(opt.to_s) }
29
+
30
+ # Excon likes to CGI.escape values in a query hash, so this has to be handled manually
31
+ if options[:ids]
32
+ query = "?ids=#{options.delete(:ids).join(',')}"
33
+ options.each_pair do |key, value|
34
+ query << "&#{key}=#{CGI.escape(value)}"
35
+ end
36
+ options = query
37
+ end
38
+
39
+ response = @client.request_all(options.class == String ? nil : "messages",
40
+ method: :get, path: "#{path}/messages", expects: [200, 204], query: options)
41
+ return [] if response.status == 204
42
+ response.body.class == Hash ? process_messages(response.body["messages"]) : process_messages(response.body)
43
+ end
44
+
45
+ def get(id, options = {})
46
+ options = options[:claim_id] ? {claim_id: options[:claim_id]} : {}
47
+ msgs = @client.request(method: :get, path: "#{path}/messages/#{id}", query: options)
48
+ process_messages([msgs.body])[0]
49
+ end
50
+
51
+ def put(*msgs)
52
+ raise ArgumentError.new("Only 10 or less messages may be given at once") if msgs.count > 10
53
+
54
+ msgs = msgs.map do |message|
55
+ begin
56
+ if message.class == Message
57
+ message.to_hash
58
+ elsif message[:body] or message["body"]
59
+ {
60
+ ttl: message[:ttl] || message["ttl"] || @default_ttl,
61
+ body: message[:body] || message["body"],
62
+ }
63
+ else
64
+ { ttl: @default_ttl, body: message }
65
+ end
66
+ rescue
67
+ { ttl: @default_ttl, body: message }
68
+ end
69
+ end
70
+
71
+ # TODO this should probably do something with a body["partial"] == true response
72
+ resources = @client.request(method: :post, path: "#{path}/messages", body: msgs, expects: 201).body["resources"]
73
+ resources.map { |resource| URI.parse(resource).path.split('/')[-1] }
74
+ end
75
+
76
+ def claim(options = {})
77
+ query = options[:limit] ? {limit: options[:limit]} : {}
78
+ body = {
79
+ ttl: options[:ttl] || options["ttl"] || @default_claim_ttl,
80
+ grace: options[:grace] || options["grace"] || @default_claim_grace,
81
+ }
82
+ response = @client.request(method: :post, path: "#{path}/claims", body: body, query: query, expects: [201, 204])
83
+ return [] if response.status == 204
84
+ claim_id = URI.parse(response.get_header("Location")).path.split('/')[-1]
85
+ process_claim(claim_id, response.body)
86
+ end
87
+
88
+ def delete_messages(*ids)
89
+ query = "?ids=#{ids.join(',')}"
90
+ @client.request(method: :delete, path: "#{path}/messages", expects: 204, query: query) && true
91
+ end
92
+
93
+ def delete!
94
+ @client.request(method: :delete, path: "#{path}", expects: 204) && true
95
+ end
96
+
97
+ def [](key)
98
+ metadata[key]
99
+ end
100
+
101
+ def []=(key, value)
102
+ new_data = metadata
103
+ new_data[key] = value
104
+ @client.request(method: :put, path: "#{path}/metadata", body: new_data, expects: 204) && true
105
+ end
106
+
107
+ def metadata
108
+ @client.request(method: :get, path: "#{path}/metadata").body
109
+ end
110
+
111
+ def metadata=(new_data)
112
+ @client.request(method: :put, path: "#{path}/metadata", body: new_data, expects: 204) && true
113
+ end
114
+
115
+ def stats
116
+ @client.request(method: :get, path: "#{path}/stats").body
117
+ end
118
+
119
+ def stat(name)
120
+ stats["messages"][name.to_s]
121
+ end
122
+
123
+ def claimed; stat(:claimed); end
124
+ def free; stat(:free); end
125
+ def total; stat(:total); end
126
+ def newest; stat(:newest); end
127
+ def oldest; stat(:oldest); end
128
+
129
+ def path
130
+ "/queues/#{@name}"
131
+ end
132
+
133
+ private
134
+
135
+ def process_messages(msgs)
136
+ msgs.map { |message| Message.new(self, message) }
137
+ end
138
+
139
+ def process_claim(claim_id, msgs)
140
+ Claim.new(self, claim_id, process_messages(msgs))
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,3 @@
1
+ module CloudQueues
2
+ VERSION = "1.0.0"
3
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloud-queues
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Regner
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-10-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: excon
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.25.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.25.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: ! 'cloud-queues
47
+
48
+ ================
49
+
50
+
51
+ Basic (unoffical) ruby interface into Rackspace Cloud Queues
52
+
53
+ '
54
+ email:
55
+ - andrew.regner@rackspace.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files: []
59
+ files:
60
+ - lib/cloud-queues/claim.rb
61
+ - lib/cloud-queues/client.rb
62
+ - lib/cloud-queues/message.rb
63
+ - lib/cloud-queues/queue.rb
64
+ - lib/cloud-queues/version.rb
65
+ - lib/cloud-queues.rb
66
+ - LICENSE
67
+ - README.md
68
+ homepage: https://github.com/adregner/cloud-queues
69
+ licenses:
70
+ - MIT
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.8.25
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Basic (unoffical) ruby interface into Rackspace Cloud Queues
93
+ test_files: []