super_queue 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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: []