super_queue 0.0.7
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/lib/super_queue.rb +307 -0
- metadata +47 -0
data/lib/super_queue.rb
ADDED
@@ -0,0 +1,307 @@
|
|
1
|
+
require 'fog'
|
2
|
+
require 'base64'
|
3
|
+
require 'socket'
|
4
|
+
require 'digest/md5'
|
5
|
+
|
6
|
+
class SuperQueue
|
7
|
+
|
8
|
+
def self.mock!
|
9
|
+
@@mock = true
|
10
|
+
Fog.mock!
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.mocking?
|
14
|
+
defined?(@@mock) && @@mock
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(opts)
|
18
|
+
check_opts(opts)
|
19
|
+
opts[:localize_queue] = true unless opts.has_key? :localized_queue
|
20
|
+
opts[:buffer_size] = 100 unless opts.has_key? :buffer_size
|
21
|
+
|
22
|
+
@localize_queue = opts[:localize_queue]
|
23
|
+
@queue_name = generate_queue_name(opts)
|
24
|
+
initialize_sqs(opts)
|
25
|
+
|
26
|
+
@waiting = []
|
27
|
+
@waiting.taint
|
28
|
+
self.taint
|
29
|
+
@mutex = Mutex.new
|
30
|
+
@in_buffer = SizedQueue.new(opts[:buffer_size])
|
31
|
+
@out_buffer = SizedQueue.new(opts[:buffer_size])
|
32
|
+
@deletion_queue = Queue.new
|
33
|
+
@mock_length = 0 if SuperQueue.mocking?
|
34
|
+
|
35
|
+
@sqs_head_tracker = Thread.new { poll_sqs_head }
|
36
|
+
@sqs_tail_tracker = Thread.new { poll_sqs_tail }
|
37
|
+
@garbage_collector = Thread.new { collect_garbage }
|
38
|
+
end
|
39
|
+
|
40
|
+
def push(p)
|
41
|
+
@mutex.synchronize {
|
42
|
+
@in_buffer.push p
|
43
|
+
begin
|
44
|
+
t = @waiting.shift
|
45
|
+
t.wakeup if t
|
46
|
+
rescue ThreadError
|
47
|
+
retry
|
48
|
+
end
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def pop(non_block=false)
|
53
|
+
@mutex.synchronize {
|
54
|
+
while true
|
55
|
+
if @out_buffer.empty?
|
56
|
+
if fill_out_buffer_from_sqs_queue || fill_out_buffer_from_in_buffer
|
57
|
+
return pop_out_buffer(non_block)
|
58
|
+
else
|
59
|
+
raise ThreadError, "queue empty" if non_block
|
60
|
+
@waiting.push Thread.current
|
61
|
+
@mutex.sleep
|
62
|
+
end
|
63
|
+
else
|
64
|
+
return pop_out_buffer(non_block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
def length
|
71
|
+
@mutex.synchronize {
|
72
|
+
return sqs_length + @in_buffer.size + @out_buffer.size
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def empty?
|
77
|
+
self.length == 0
|
78
|
+
end
|
79
|
+
|
80
|
+
def num_waiting
|
81
|
+
@waiting.size
|
82
|
+
end
|
83
|
+
|
84
|
+
def clear
|
85
|
+
begin
|
86
|
+
self.pop(true)
|
87
|
+
rescue ThreadError
|
88
|
+
retry unless self.empty?
|
89
|
+
end until self.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def shutdown
|
93
|
+
@sqs_head_tracker.terminate
|
94
|
+
while !@in_buffer.empty? do
|
95
|
+
@sqs.delete_message(q_url, @in_buffer.pop)
|
96
|
+
end
|
97
|
+
@sqs_tail_tracker.terminate
|
98
|
+
@garbage_collector.terminate
|
99
|
+
while !@deletion_queue.empty?
|
100
|
+
@sqs.delete_message(q_url, @deletion_queue.pop)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def destroy
|
105
|
+
@sqs_head_tracker.terminate
|
106
|
+
@sqs_tail_tracker.terminate
|
107
|
+
@garbage_collector.terminate
|
108
|
+
delete_queue
|
109
|
+
end
|
110
|
+
|
111
|
+
alias enq push
|
112
|
+
alias << push
|
113
|
+
|
114
|
+
alias deq pop
|
115
|
+
alias shift pop
|
116
|
+
|
117
|
+
alias size length
|
118
|
+
|
119
|
+
def url
|
120
|
+
q_url
|
121
|
+
end
|
122
|
+
|
123
|
+
def name
|
124
|
+
queue_name
|
125
|
+
end
|
126
|
+
|
127
|
+
def localized?
|
128
|
+
!!@localize_queue
|
129
|
+
end
|
130
|
+
|
131
|
+
private
|
132
|
+
|
133
|
+
def check_opts(opts)
|
134
|
+
raise "Options can't be nil!" if opts.nil?
|
135
|
+
raise "Minimun :buffer_size is 5." if opts[:buffer_size] && (opts[:buffer_size] < 5)
|
136
|
+
raise "AWS credentials :aws_access_key_id and :aws_secret_access_key required!" unless opts[:aws_access_key_id] && opts[:aws_secret_access_key]
|
137
|
+
raise "Visbility timeout must be an integer (in seconds)!" if opts[:visibility_timeout] && !opts[:visibility_timeout].is_a?(Integer)
|
138
|
+
end
|
139
|
+
|
140
|
+
def initialize_sqs(opts)
|
141
|
+
create_sqs_connection(opts)
|
142
|
+
create_sqs_queue(opts)
|
143
|
+
check_for_queue_creation_success
|
144
|
+
@sqs.set_queue_attributes(q_url, "VisibilityTimeout", opts[:visibility_timeout]) if opts[:visibility_timeout]
|
145
|
+
end
|
146
|
+
|
147
|
+
def create_sqs_connection(opts)
|
148
|
+
aws_options = {
|
149
|
+
:aws_access_key_id => opts[:aws_access_key_id],
|
150
|
+
:aws_secret_access_key => opts[:aws_secret_access_key]
|
151
|
+
}
|
152
|
+
begin
|
153
|
+
@sqs = Fog::AWS::SQS.new(aws_options)
|
154
|
+
rescue Exception => e
|
155
|
+
raise e
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def create_sqs_queue(opts)
|
160
|
+
@sqs_queue = @sqs.create_queue(queue_name)
|
161
|
+
if opts[:replace_existing_queue] && (sqs_length > 0)
|
162
|
+
delete_queue
|
163
|
+
puts "Waiting 60s to create new queue..."
|
164
|
+
sleep 62 # You must wait 60s after deleting a q to create one with the same name
|
165
|
+
if opts[:visibility_timeout]
|
166
|
+
@sqs_queue = @sqs.create_queue(queue_name, {"DefaultVisibilityTimeout" => opts[:visibility_timeout]})
|
167
|
+
else
|
168
|
+
@sqs_queue = @sqs.create_queue(queue_name)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def check_for_queue_creation_success
|
174
|
+
retries = 0
|
175
|
+
while q_url.nil? && (retries < 5)
|
176
|
+
retries += 1
|
177
|
+
sleep 1
|
178
|
+
end
|
179
|
+
raise "Couldn't create queue #{queue_name}, or delete existing queue by this name." if q_url.nil?
|
180
|
+
end
|
181
|
+
|
182
|
+
def send_message_to_queue(p)
|
183
|
+
payload = is_a_link?(p) ? p : Base64.encode64(Marshal.dump(p))
|
184
|
+
@sqs.send_message(q_url, payload)
|
185
|
+
@mock_length += 1 if SuperQueue.mocking?
|
186
|
+
end
|
187
|
+
|
188
|
+
def get_message_from_queue
|
189
|
+
message = @sqs.receive_message(q_url)
|
190
|
+
return nil if message.body.nil? || message.body['Message'].first.nil?
|
191
|
+
handle = message.body['Message'].first['ReceiptHandle']
|
192
|
+
ser_obj = message.body['Message'].first['Body']
|
193
|
+
return nil if ser_obj.nil? || ser_obj.empty?
|
194
|
+
@mock_length -= 1 if SuperQueue.mocking?
|
195
|
+
return {:handle => handle, :payload => ser_obj} if is_a_link?(ser_obj)
|
196
|
+
{ :handle => handle, :payload => Marshal.load(Base64.decode64(ser_obj)) }
|
197
|
+
end
|
198
|
+
|
199
|
+
def q_url
|
200
|
+
return @q_url if @q_url
|
201
|
+
@q_url = @sqs_queue.body['QueueUrl']
|
202
|
+
@q_url
|
203
|
+
end
|
204
|
+
|
205
|
+
def is_a_link?(s)
|
206
|
+
return false unless s.is_a? String
|
207
|
+
(s[0..6] == "http://") || (s[0..7] == "https://")
|
208
|
+
end
|
209
|
+
|
210
|
+
def delete_queue
|
211
|
+
@sqs.delete_queue(q_url)
|
212
|
+
end
|
213
|
+
|
214
|
+
def generate_queue_name(opts)
|
215
|
+
q_name = opts[:name] || random_name
|
216
|
+
if opts[:namespace] && opts[:localize_queue]
|
217
|
+
"#{@namespace}-#{Digest::MD5.hexdigest(local_ip)}-#{q_name}"
|
218
|
+
elsif opts[:namespace]
|
219
|
+
"#{@namespace}-#{q_name}"
|
220
|
+
elsif opts[:localize_queue]
|
221
|
+
"#{Digest::MD5.hexdigest(local_ip)}-#{q_name}"
|
222
|
+
else
|
223
|
+
q_name
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def random_name
|
228
|
+
o = [('a'..'z'),('A'..'Z')].map{|i| i.to_a}.flatten
|
229
|
+
(0...15).map{ o[rand(o.length)] }.join
|
230
|
+
end
|
231
|
+
|
232
|
+
def sqs_length
|
233
|
+
return @mock_length if SuperQueue.mocking?
|
234
|
+
body = @sqs.get_queue_attributes(q_url, "ApproximateNumberOfMessages").body
|
235
|
+
begin
|
236
|
+
retval = 0
|
237
|
+
if body
|
238
|
+
attrs = body["Attributes"]
|
239
|
+
if attrs
|
240
|
+
retval = attrs["ApproximateNumberOfMessages"]
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end until !retval.nil?
|
244
|
+
retval
|
245
|
+
end
|
246
|
+
|
247
|
+
def local_ip
|
248
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily
|
249
|
+
return "127.0.0.1" if SuperQueue.mocking?
|
250
|
+
UDPSocket.open do |s|
|
251
|
+
s.connect '64.233.187.99', 1
|
252
|
+
s.addr.last
|
253
|
+
end
|
254
|
+
ensure
|
255
|
+
Socket.do_not_reverse_lookup = orig
|
256
|
+
end
|
257
|
+
|
258
|
+
def poll_sqs_head
|
259
|
+
loop { send_message_to_queue(@in_buffer.pop) }
|
260
|
+
end
|
261
|
+
|
262
|
+
def poll_sqs_tail
|
263
|
+
loop do
|
264
|
+
nil_count = 0
|
265
|
+
while (@out_buffer.size < @out_buffer.max) && (nil_count < 5) # If you get nil 5 times in a row, SQS is probably empty
|
266
|
+
m = get_message_from_queue
|
267
|
+
if m.nil?
|
268
|
+
nil_count += 1
|
269
|
+
else
|
270
|
+
@out_buffer.push m
|
271
|
+
nil_count = 0
|
272
|
+
end
|
273
|
+
end unless sqs_length == 0
|
274
|
+
sleep
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def collect_garbage
|
279
|
+
loop { @sqs.delete_message(q_url, @deletion_queue.pop) }
|
280
|
+
end
|
281
|
+
|
282
|
+
def fill_out_buffer_from_sqs_queue
|
283
|
+
@sqs_tail_tracker.wakeup if @sqs_tail_tracker.stop?
|
284
|
+
count = 0
|
285
|
+
while @out_buffer.empty? && count != 5
|
286
|
+
sleep 1
|
287
|
+
count += 1
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def fill_out_buffer_from_in_buffer
|
292
|
+
while (@out_buffer.size < @out_buffer.max) && !@in_buffer.empty?
|
293
|
+
@out_buffer.push @in_buffer.pop
|
294
|
+
end
|
295
|
+
!@out_buffer.empty?
|
296
|
+
end
|
297
|
+
|
298
|
+
def pop_out_buffer(non_block)
|
299
|
+
m = @out_buffer.pop(non_block)
|
300
|
+
@deletion_queue << m[:handle]
|
301
|
+
m[:payload]
|
302
|
+
end
|
303
|
+
|
304
|
+
def queue_name
|
305
|
+
@queue_name
|
306
|
+
end
|
307
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: super_queue
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.7
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jon Stokes
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2010-02-09 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: A an SQS-backed queue.
|
15
|
+
email: jon@jonstokes.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/super_queue.rb
|
21
|
+
homepage: http://rubygems.org/gems/superqueue
|
22
|
+
licenses: []
|
23
|
+
post_install_message:
|
24
|
+
rdoc_options: []
|
25
|
+
require_paths:
|
26
|
+
- lib
|
27
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ! '>='
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: !binary |-
|
32
|
+
MA==
|
33
|
+
none: false
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: !binary |-
|
39
|
+
MA==
|
40
|
+
none: false
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 1.8.24
|
44
|
+
signing_key:
|
45
|
+
specification_version: 3
|
46
|
+
summary: An SQS-backed queue structure for ruby that works just like a normal queue, except it's essentially infinite and can use very little memory.
|
47
|
+
test_files: []
|