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.
Files changed (2) hide show
  1. data/lib/super_queue.rb +155 -85
  2. 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
- initialize_sqs(opts)
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
- @compressor = Zlib::Deflate.new
27
- @decompressor = Zlib::Inflate.new
28
-
29
- @sqs_tracker = Thread.new { poll_sqs } if @should_poll_sqs
30
- @gc = Thread.new { collect_garbage }
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
- while true
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
- return sqs_length + @in_buffer.size + @out_buffer.size
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 { clear_deletion_queue }
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
- delete_queue
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 SQS methods
145
+ # Amazon AWS methods
123
146
  #
124
- def initialize_sqs(opts)
125
- create_sqs_connection(opts)
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 create_sqs_connection(opts)
136
- aws_options = {
137
- :access_key_id => opts[:aws_access_key_id],
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 create_sqs_queue(opts)
148
- retries = 0
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
- obj = is_a_link?(p) ? p : encode(p)
194
- batch << obj
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 |c|
211
- batch = @sqs_queue.receive_messages(:limit => 10)
212
- batch.each do |m|
213
- if is_a_link?(m.body)
214
- obj = {:message => m, :payload => m.body}
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
- obj = { :message => m, :payload => decode(m.body) }
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 delete_queue
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
- @sqs_queue.batch_delete(@deletion_queue[0..9])
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
- @gc.wakeup if @gc.stop? # This is the best time to do GC, because there are no pops happening.
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.each { |m| @out_buffer.push m }
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
- @deletion_queue << m[:message] if m[:message]
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
- # Misc helper methods
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
- #puts "Decode: Decoding message #{ser_obj} from base64..."
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] ? "#{@namespace}-#{q_name}" : q_name
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'),('A'..'Z')].map{|i| i.to_a}.flatten
319
- (0...15).map{ o[rand(o.length)] }.join
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 { clear_deletion_queue } if !@deletion_queue.empty? && (@deletion_queue.size >= (@buffer_size / 2))
341
- sleep
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-11 00:00:00.000000000 Z
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 an SQS-backed queue.
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.25
59
+ rubygems_version: 1.8.24
60
60
  signing_key:
61
61
  specification_version: 3
62
- 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.
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: []