super_queue 0.2.1 → 0.3.1
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 +155 -85
- metadata +5 -5
data/lib/super_queue.rb
CHANGED
@@ -1,19 +1,32 @@
|
|
1
1
|
require 'aws-sdk'
|
2
2
|
require 'base64'
|
3
|
-
require 'socket'
|
4
3
|
require 'digest/md5'
|
5
|
-
require 'zlib'
|
6
4
|
|
7
5
|
class SuperQueue
|
8
6
|
|
7
|
+
class S3Pointer < Hash
|
8
|
+
def initialize(key)
|
9
|
+
super
|
10
|
+
self.merge!(:s3_key => key)
|
11
|
+
end
|
12
|
+
|
13
|
+
def s3_key
|
14
|
+
self[:s3_key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def s3_key=(value)
|
18
|
+
self[:s3_key] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
9
22
|
def initialize(opts)
|
10
23
|
AWS.eager_autoload! # for thread safety
|
11
24
|
check_opts(opts)
|
12
|
-
@should_poll_sqs = opts[:should_poll_sqs]
|
13
25
|
@buffer_size = opts[:buffer_size] || 100
|
26
|
+
@use_s3 = opts[:use_s3]
|
14
27
|
@queue_name = generate_queue_name(opts)
|
15
28
|
@request_count = 0
|
16
|
-
|
29
|
+
initialize_aws(opts)
|
17
30
|
|
18
31
|
@waiting = []
|
19
32
|
@waiting.taint
|
@@ -21,16 +34,20 @@ class SuperQueue
|
|
21
34
|
@mutex = Mutex.new
|
22
35
|
@in_buffer = []
|
23
36
|
@out_buffer = []
|
37
|
+
@deletion_buffer = []
|
24
38
|
@deletion_queue = []
|
25
39
|
|
26
|
-
@
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
40
|
+
@gc = Thread.new do
|
41
|
+
begin
|
42
|
+
collect_garbage
|
43
|
+
rescue Exception => @gc_error
|
44
|
+
raise @gc_error
|
45
|
+
end
|
46
|
+
end
|
31
47
|
end
|
32
48
|
|
33
49
|
def push(p)
|
50
|
+
check_for_errors
|
34
51
|
@mutex.synchronize {
|
35
52
|
@in_buffer.push p
|
36
53
|
clear_in_buffer if @in_buffer.size >= @buffer_size
|
@@ -44,8 +61,9 @@ class SuperQueue
|
|
44
61
|
end
|
45
62
|
|
46
63
|
def pop(non_block=false)
|
64
|
+
check_for_errors
|
47
65
|
@mutex.synchronize {
|
48
|
-
|
66
|
+
loop do
|
49
67
|
if @out_buffer.empty?
|
50
68
|
if fill_out_buffer_from_sqs_queue || fill_out_buffer_from_in_buffer
|
51
69
|
return pop_out_buffer
|
@@ -62,19 +80,22 @@ class SuperQueue
|
|
62
80
|
end
|
63
81
|
|
64
82
|
def length
|
83
|
+
check_for_errors
|
65
84
|
@mutex.synchronize {
|
66
|
-
|
85
|
+
sqsl = sqs_length
|
86
|
+
return sqsl + @in_buffer.size + @out_buffer.size
|
67
87
|
}
|
68
88
|
end
|
69
89
|
|
70
|
-
def empty?
|
71
|
-
self.length == 0
|
72
|
-
end
|
73
|
-
|
74
90
|
def num_waiting
|
91
|
+
check_for_errors
|
75
92
|
@waiting.size
|
76
93
|
end
|
77
94
|
|
95
|
+
def empty?
|
96
|
+
self.length == 0
|
97
|
+
end
|
98
|
+
|
78
99
|
def clear
|
79
100
|
begin
|
80
101
|
self.pop(true)
|
@@ -84,19 +105,21 @@ class SuperQueue
|
|
84
105
|
end
|
85
106
|
|
86
107
|
def shutdown
|
87
|
-
@sqs_tracker.terminate if @should_poll_sqs
|
88
108
|
@mutex.synchronize { clear_in_buffer }
|
89
109
|
@gc.terminate
|
90
|
-
@mutex.synchronize {
|
110
|
+
@mutex.synchronize { fill_deletion_queue_from_buffer } if @deletion_buffer.any?
|
111
|
+
@mutex.synchronize { clear_deletion_queue } if @deletion_queue.any?
|
112
|
+
@done = true
|
91
113
|
end
|
92
114
|
|
93
115
|
def destroy
|
94
|
-
@sqs_tracker.terminate if @should_poll_sqs
|
95
116
|
@gc.terminate
|
96
|
-
|
117
|
+
delete_aws_resources
|
118
|
+
@done = true
|
97
119
|
end
|
98
120
|
|
99
121
|
def sqs_requests
|
122
|
+
check_for_errors
|
100
123
|
@request_count
|
101
124
|
end
|
102
125
|
|
@@ -119,10 +142,15 @@ class SuperQueue
|
|
119
142
|
private
|
120
143
|
|
121
144
|
#
|
122
|
-
# Amazon
|
145
|
+
# Amazon AWS methods
|
123
146
|
#
|
124
|
-
def
|
125
|
-
|
147
|
+
def initialize_aws(opts)
|
148
|
+
aws_options = {
|
149
|
+
:access_key_id => opts[:aws_access_key_id],
|
150
|
+
:secret_access_key => opts[:aws_secret_access_key]
|
151
|
+
}
|
152
|
+
@sqs = AWS::SQS.new(aws_options)
|
153
|
+
@s3 = AWS::S3.new(aws_options)
|
126
154
|
create_sqs_queue(opts)
|
127
155
|
if opts[:replace_existing_queue] && (sqs_length > 0)
|
128
156
|
delete_queue
|
@@ -130,30 +158,16 @@ class SuperQueue
|
|
130
158
|
sleep 62 # You must wait 60s after deleting a q to create one with the same name
|
131
159
|
create_sqs_queue(opts)
|
132
160
|
end
|
161
|
+
@bucket = open_s3_bucket
|
133
162
|
end
|
134
163
|
|
135
|
-
def
|
136
|
-
|
137
|
-
|
138
|
-
:secret_access_key => opts[:aws_secret_access_key]
|
139
|
-
}
|
140
|
-
begin
|
141
|
-
@sqs = AWS::SQS.new(aws_options)
|
142
|
-
rescue Exception => e
|
143
|
-
raise e
|
144
|
-
end
|
164
|
+
def create_sqs_queue(opts)
|
165
|
+
@sqs_queue = find_queue_by_name || new_sqs_queue(opts)
|
166
|
+
check_for_queue_creation_success
|
145
167
|
end
|
146
168
|
|
147
|
-
def
|
148
|
-
|
149
|
-
begin
|
150
|
-
@sqs_queue = find_queue_by_name || new_sqs_queue(opts)
|
151
|
-
check_for_queue_creation_success
|
152
|
-
rescue RuntimeError => e
|
153
|
-
retries += 1
|
154
|
-
sleep 1
|
155
|
-
(retries >= 20) ? retry : raise(e)
|
156
|
-
end
|
169
|
+
def open_s3_bucket
|
170
|
+
@s3.buckets[queue_name].exists? ? @s3.buckets[queue_name] : @s3.buckets.create(queue_name)
|
157
171
|
end
|
158
172
|
|
159
173
|
def find_queue_by_name
|
@@ -189,17 +203,20 @@ class SuperQueue
|
|
189
203
|
number_of_batches.times do
|
190
204
|
batch = []
|
191
205
|
10.times do
|
206
|
+
next if @in_buffer.empty?
|
192
207
|
p = @in_buffer.shift
|
193
|
-
|
194
|
-
|
208
|
+
unless should_send_to_s3?(p)
|
209
|
+
batch << encode(p)
|
210
|
+
else
|
211
|
+
batch << encode(send_payload_to_s3(p))
|
212
|
+
end
|
195
213
|
end
|
196
214
|
batches << batch
|
197
215
|
end
|
198
216
|
|
199
|
-
#Ugliness! But I'm not sure how else to tackle this at the moment
|
200
217
|
batches.each do |b|
|
201
218
|
@request_count += 1
|
202
|
-
@sqs_queue.batch_send(b)
|
219
|
+
@sqs_queue.batch_send(b) if b.any?
|
203
220
|
end
|
204
221
|
end
|
205
222
|
|
@@ -207,34 +224,72 @@ class SuperQueue
|
|
207
224
|
messages = []
|
208
225
|
number_of_batches = number_of_messages_to_receive / 10
|
209
226
|
number_of_batches += 1 if number_of_messages_to_receive % 10
|
210
|
-
number_of_batches.times do
|
211
|
-
batch = @sqs_queue.receive_messages(:limit => 10)
|
212
|
-
batch.each do |
|
213
|
-
|
214
|
-
|
227
|
+
number_of_batches.times do
|
228
|
+
batch = @sqs_queue.receive_messages(:limit => 10).compact
|
229
|
+
batch.each do |message|
|
230
|
+
obj = decode(message.body)
|
231
|
+
unless obj.is_a?(SuperQueue::S3Pointer)
|
232
|
+
messages << {
|
233
|
+
:payload => obj,
|
234
|
+
:sqs_handle => message
|
235
|
+
}
|
215
236
|
else
|
216
|
-
|
237
|
+
p = fetch_payload_from_s3(obj)
|
238
|
+
messages << {
|
239
|
+
:payload => p,
|
240
|
+
:sqs_handle => message,
|
241
|
+
:s3_key => obj.s3_key } if p
|
217
242
|
end
|
218
|
-
messages << obj
|
219
243
|
end
|
220
244
|
@request_count += 1
|
221
245
|
end
|
222
246
|
messages
|
223
247
|
end
|
224
248
|
|
249
|
+
def send_payload_to_s3(p)
|
250
|
+
dump = Marshal.dump(p)
|
251
|
+
digest = Digest::MD5.hexdigest(dump)
|
252
|
+
return digest if @bucket.objects[digest].exists?
|
253
|
+
retryable(:tries => 5) { @bucket.objects[digest].write(dump) }
|
254
|
+
S3Pointer.new(digest)
|
255
|
+
end
|
256
|
+
|
257
|
+
def fetch_payload_from_s3(pointer)
|
258
|
+
payload = nil
|
259
|
+
retries = 0
|
260
|
+
begin
|
261
|
+
payload = Marshal.load(@bucket.objects[pointer.s3_key].read)
|
262
|
+
rescue AWS::S3::Errors::NoSuchKey
|
263
|
+
return nil
|
264
|
+
rescue
|
265
|
+
retries +=1
|
266
|
+
retry if retries < 5
|
267
|
+
end
|
268
|
+
payload
|
269
|
+
end
|
270
|
+
|
271
|
+
def should_send_to_s3?(p)
|
272
|
+
@use_s3
|
273
|
+
end
|
274
|
+
|
225
275
|
def sqs_length
|
226
276
|
n = @sqs_queue.approximate_number_of_messages
|
227
277
|
return n.is_a?(Integer) ? n : 0
|
228
278
|
end
|
229
279
|
|
230
|
-
def
|
280
|
+
def delete_aws_resources
|
231
281
|
@request_count += 1
|
232
282
|
@sqs_queue.delete
|
283
|
+
@bucket.delete!
|
233
284
|
end
|
234
285
|
|
235
286
|
def clear_deletion_queue
|
236
287
|
while !@deletion_queue.empty?
|
237
|
-
@
|
288
|
+
sqs_handles = @deletion_queue[0..9].map { |m| m[:sqs_handle] }.compact
|
289
|
+
s3_keys = @deletion_queue[0..9].map { |m| m[:s3_key] }.compact
|
290
|
+
10.times { @deletion_queue.shift }
|
291
|
+
@sqs_queue.batch_delete(sqs_handles) if sqs_handles.any?
|
292
|
+
s3_keys.each { |key| @bucket.objects[key].delete }
|
238
293
|
@request_count += 1
|
239
294
|
end
|
240
295
|
end
|
@@ -244,10 +299,15 @@ class SuperQueue
|
|
244
299
|
#
|
245
300
|
def fill_out_buffer_from_sqs_queue
|
246
301
|
return false if sqs_length == 0
|
247
|
-
|
248
|
-
while (@out_buffer.size < @buffer_size)
|
302
|
+
nil_count = 0
|
303
|
+
while (@out_buffer.size < @buffer_size) && (nil_count < 2)
|
249
304
|
messages = get_messages_from_queue(@buffer_size - @out_buffer.size)
|
250
|
-
messages.
|
305
|
+
if messages.empty?
|
306
|
+
nil_count += 1
|
307
|
+
else
|
308
|
+
messages.each { |m| @out_buffer.push(m) }
|
309
|
+
nil_count = 0
|
310
|
+
end
|
251
311
|
end
|
252
312
|
!@out_buffer.empty?
|
253
313
|
end
|
@@ -260,9 +320,14 @@ class SuperQueue
|
|
260
320
|
!@out_buffer.empty?
|
261
321
|
end
|
262
322
|
|
323
|
+
def fill_deletion_queue_from_buffer
|
324
|
+
@deletion_queue += @deletion_buffer
|
325
|
+
@deletion_buffer = []
|
326
|
+
end
|
327
|
+
|
263
328
|
def pop_out_buffer
|
264
329
|
m = @out_buffer.shift
|
265
|
-
@
|
330
|
+
@deletion_buffer << { :sqs_handle => m[:sqs_handle], :s3_key => m[:s3_key] }
|
266
331
|
m[:payload]
|
267
332
|
end
|
268
333
|
|
@@ -272,6 +337,9 @@ class SuperQueue
|
|
272
337
|
end
|
273
338
|
end
|
274
339
|
|
340
|
+
#
|
341
|
+
# Misc helper methods
|
342
|
+
#
|
275
343
|
def check_opts(opts)
|
276
344
|
raise "Options can't be nil!" if opts.nil?
|
277
345
|
raise "Minimun :buffer_size is 5." if opts[:buffer_size] && (opts[:buffer_size] < 5)
|
@@ -279,30 +347,37 @@ class SuperQueue
|
|
279
347
|
raise "Visbility timeout must be an integer (in seconds)!" if opts[:visibility_timeout] && !opts[:visibility_timeout].is_a?(Integer)
|
280
348
|
end
|
281
349
|
|
282
|
-
|
283
|
-
|
284
|
-
|
350
|
+
def check_for_errors
|
351
|
+
raise @gc_error if @gc_error
|
352
|
+
raise @sqs_error if @sqs_error
|
353
|
+
raise "Queue is no longer available!" if @done == true
|
354
|
+
end
|
355
|
+
|
285
356
|
def encode(p)
|
286
357
|
Base64.urlsafe_encode64(Marshal.dump(p))
|
287
358
|
end
|
288
359
|
|
289
360
|
def decode(ser_obj)
|
290
|
-
|
291
|
-
obj = Base64.urlsafe_decode64(ser_obj)
|
292
|
-
#puts "Decode: Object decoded as #{obj}. Doing Marshal load..."
|
293
|
-
ret = Marshal.load(obj)
|
294
|
-
#puts "Decode: Marshal loaded as #{ret}"
|
295
|
-
ret
|
296
|
-
end
|
297
|
-
|
298
|
-
def is_a_link?(s)
|
299
|
-
return false unless s.is_a? String
|
300
|
-
(s[0..6] == "http://") || (s[0..7] == "https://")
|
361
|
+
Marshal.load(Base64.urlsafe_decode64(ser_obj))
|
301
362
|
end
|
302
363
|
|
303
364
|
def generate_queue_name(opts)
|
304
365
|
q_name = opts[:name] || random_name
|
305
|
-
return opts[:namespace] ? "
|
366
|
+
return opts[:namespace] ? "queue-#{opts[:namespace]}-#{q_name}" : "queue-#{q_name}"
|
367
|
+
end
|
368
|
+
|
369
|
+
def retryable(options = {}, &block)
|
370
|
+
opts = { :tries => 1, :on => Exception }.merge(options)
|
371
|
+
|
372
|
+
retry_exception, retries = opts[:on], opts[:tries]
|
373
|
+
|
374
|
+
begin
|
375
|
+
return yield
|
376
|
+
rescue retry_exception
|
377
|
+
retry if (retries -= 1) > 0
|
378
|
+
end
|
379
|
+
|
380
|
+
yield
|
306
381
|
end
|
307
382
|
|
308
383
|
#
|
@@ -315,8 +390,9 @@ class SuperQueue
|
|
315
390
|
end
|
316
391
|
|
317
392
|
def random_name
|
318
|
-
o = [('a'..'z'),('
|
319
|
-
(0...
|
393
|
+
o = [('a'..'z'),('1'..'9')].map{|i| i.to_a}.flatten
|
394
|
+
random_element = (0...25).map{ o[rand(o.length)] }.join
|
395
|
+
"temp-name-#{random_element}"
|
320
396
|
end
|
321
397
|
|
322
398
|
def queue_name
|
@@ -326,19 +402,13 @@ class SuperQueue
|
|
326
402
|
#
|
327
403
|
# Maintence thread-related methods
|
328
404
|
#
|
329
|
-
def poll_sqs
|
330
|
-
loop do
|
331
|
-
@mutex.synchronize { fill_out_buffer_from_sqs_queue || fill_out_buffer_from_in_buffer } if @out_buffer.empty?
|
332
|
-
@mutex.synchronize { clear_in_buffer } if !@in_buffer.empty? && (@in_buffer.size > @buffer_size)
|
333
|
-
Thread.pass
|
334
|
-
end
|
335
|
-
end
|
336
405
|
|
337
406
|
def collect_garbage
|
338
407
|
loop do
|
339
408
|
#This also needs a condition to clear the del queue if there are any handles where the invisibility is about to expire
|
340
|
-
@mutex.synchronize {
|
341
|
-
|
409
|
+
@mutex.synchronize { fill_deletion_queue_from_buffer } if @deletion_buffer.any?
|
410
|
+
Thread.pass
|
411
|
+
@mutex.synchronize { clear_deletion_queue } if @deletion_queue.any?
|
342
412
|
end
|
343
413
|
end
|
344
414
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: super_queue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.1
|
5
4
|
prerelease:
|
5
|
+
version: 0.3.1
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Jon Stokes
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-02-
|
12
|
+
date: 2013-02-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: aws-sdk
|
@@ -27,7 +27,7 @@ dependencies:
|
|
27
27
|
none: false
|
28
28
|
prerelease: false
|
29
29
|
type: :runtime
|
30
|
-
description: A
|
30
|
+
description: A thread-safe SQS- and S3-backed queue.
|
31
31
|
email: jon@jonstokes.com
|
32
32
|
executables: []
|
33
33
|
extensions: []
|
@@ -56,8 +56,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
56
|
none: false
|
57
57
|
requirements: []
|
58
58
|
rubyforge_project:
|
59
|
-
rubygems_version: 1.8.
|
59
|
+
rubygems_version: 1.8.24
|
60
60
|
signing_key:
|
61
61
|
specification_version: 3
|
62
|
-
summary:
|
62
|
+
summary: A thread-safe, SQS- and S3-backed queue structure for ruby that works just like a normal queue, except it's essentially infinite and can use very little memory.
|
63
63
|
test_files: []
|