FanSQS 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cb1bcbb591271928ff993e7fe68b63c3b7f8e577
4
+ data.tar.gz: cd7e8775fe377c7c9158be44ea7bbd61676f61c9
5
+ SHA512:
6
+ metadata.gz: 396454510c18f3653fe53ebeb352cdd3574eeed301bd5ad0845f9147e9caf0ac394a320c3202001eb023ed953a899996d9830f42c8019c8999710b98f108df16
7
+ data.tar.gz: 8c7e247c2a1ea5912f04dd83f004f698c8c1e73123f1c6999037214562d8898d70b9aeef5066e26c360571e57eac600baaca23bafad7eb73c1fcb4ff89bddb2e
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
19
+ *.swo
20
+ Session.vim
21
+ dump.rdb
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'FanSQS/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "FanSQS"
8
+ spec.version = FanSQS::VERSION
9
+ spec.authors = ["Khang Pham"]
10
+ spec.email = ["vkhang55@gmail.com"]
11
+ spec.summary = %q{Distributed messages queuing system using AWS SQS}
12
+ spec.description = %q{Distributed messages queuing system using AWS SQS}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "rake"
22
+ spec.add_dependency "aws-sdk", "~> 1.41.0"
23
+
24
+ # For testing
25
+ spec.add_development_dependency "bundler", "~> 1.5"
26
+ spec.add_development_dependency "guard"
27
+ spec.add_development_dependency "guard-rspec"
28
+ spec.add_development_dependency 'pry'
29
+ spec.add_development_dependency "rspec"
30
+ spec.add_development_dependency 'rspec-mocks'
31
+ spec.add_development_dependency 'shoulda-matchers'
32
+ end
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in FanSQS.gemspec
4
+ gemspec
@@ -0,0 +1,13 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
9
+
10
+ # Turnip features and steps
11
+ watch(%r{^spec/acceptance/(.+)\.feature$})
12
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
13
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Khang Pham
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,6 @@
1
+ TO-DOs
2
+ ======
3
+ 1. Add rake task to start poller
4
+ 2. Priority queues
5
+ 3. Auto retries on error
6
+ 4. Hook up Travis
@@ -0,0 +1,62 @@
1
+ # FanSQS
2
+
3
+ FanSQS is a background job processing engine for Ruby using [AWS SQS](http://aws.amazon.com/sqs/) for message storage.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'FanSQS'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install FanSQS
18
+
19
+ ## Rails Setup
20
+
21
+ Create a **config/initializer.rb** file in your application directory and insert this line:
22
+
23
+ ```ruby
24
+ AWS.config( access_key_id: '<your_access_key_id>', secret_access_key: '<your_secret_access_key>')
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ FanSQS integrates seemlessly with your Rails applications. Just *include* the module **FanSQS::Worker** in the class that encapsulates your job logic and define a class method called *perform*. The method *perform* should be able to accept any number of parameters of any type.
30
+
31
+ ```ruby
32
+ class MessagePublisher
33
+ include FanSQS::Worker
34
+ set_fan_sqs_options queue: :message_queue # use the key :queue to define the message queue name
35
+
36
+ def self.perform(arg1, arg2, ...)
37
+ # code to do work
38
+ end
39
+ end
40
+ ```
41
+
42
+ To push jobs into an SQS queue for processing, simply call:
43
+
44
+ ```ruby
45
+ MessagePublisher.perform_async(arg1, arg2, ...)
46
+ ```
47
+
48
+ ## Features to add
49
+
50
+ 1. Rake task to start poller
51
+ 2. Priority queues
52
+ 3. Auto retries on error
53
+ 4. Hook up to Travis
54
+
55
+ ## Contributing
56
+
57
+ 1. Fork it ( http://github.com/<my-github-username>/FanSQS/fork )
58
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
59
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
60
+ 4. Test coverage for your changes. Sorry I will not merge changes without test coverage!
61
+ 5. Push to the branch (`git push origin my-new-feature`)
62
+ 6. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,26 @@
1
+ require "FanSQS/pool"
2
+ require "FanSQS/version"
3
+ require "FanSQS/message_parser"
4
+ require "FanSQS/queue_wrapper"
5
+ require "FanSQS/queues_cache"
6
+ require "FanSQS/poller"
7
+ require "FanSQS/worker"
8
+ require "aws"
9
+
10
+ # Reference: [http://ruby.awsblog.com/post/Tx16QY1CI5GVBFT/Threading-with-the-AWS-SDK-for-Ruby]
11
+ # Issue this line will give significant performance boost
12
+ AWS.eager_autoload! AWS::SQS
13
+
14
+ # $pool = Pool.new(50) # Performs max at 50 threads. Need to make this value configurable later.
15
+
16
+ module FanSQS
17
+ extend self
18
+
19
+ ErrorQueue = :fan_sqs_queue_error
20
+ class << self
21
+ attr_accessor :pool
22
+ end
23
+ self.pool = Pool.new(100)
24
+ end
25
+
26
+ require 'FanSQS/railtie' if defined?(Rails) # include FanSQS Rake task if Rails is defined
@@ -0,0 +1,12 @@
1
+ module FanSQS
2
+ class MessageParser
3
+ class << self
4
+ def parse(msg)
5
+ json = JSON.parse(msg, symbolize_names: true)
6
+ return json
7
+ rescue JSON::ParserError # malformed JSON
8
+ return nil
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,42 @@
1
+ require 'thread'
2
+
3
+ module FanSQS
4
+ class Poller
5
+ class << self
6
+ def start(qnames = [])
7
+ @queues_cache = FanSQS::QueuesCache.new(qnames)
8
+ loop do
9
+ @queues_cache.fetch.each do |queue|
10
+ FanSQS.pool.schedule do # Allows for multiple concurrent (non-blocking) HTTP requests to SQS
11
+ queue.receive_messages(limit: 10) do |message|
12
+ process(message.body)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def process(msg)
22
+ message = MessageParser.parse(msg)
23
+ Thread.new do
24
+ begin
25
+ klass = Object::const_get(message[:class])
26
+ klass.send(:perform, *message[:arguments])
27
+ rescue ArgumentError => e
28
+ store_exception(e, message)
29
+ end
30
+ end
31
+ end
32
+
33
+ def store_exception(exception, message)
34
+ error_message = { class: message[:class],
35
+ arguments: message[:arguments],
36
+ stack_trace: exception.backtrace.join("\n") }
37
+ queue = FanSQS::QueueWrapper.instantiate(FanSQS::ErrorQueue)
38
+ queue.send_message(error_message.to_json)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,155 @@
1
+ # Credit to Kim Burgestrand @ http://burgestrand.se/articles/quick-and-simple-ruby-thread-pool.html
2
+
3
+ # Ruby Thread Pool
4
+ # ================
5
+ # A thread pool is useful when you wish to do some work in a thread, but do
6
+ # not know how much work you will be doing in advance. Spawning one thread
7
+ # for each task is potentially expensive, as threads are not free.
8
+ #
9
+ # In this case, it might be more beneficial to start a predefined set of
10
+ # threads and then hand off work to them as it becomes available. This is
11
+ # the pure essence of what a thread pool is: an array of threads, all just
12
+ # waiting to do some work for you!
13
+ #
14
+ # Prerequisites
15
+ # -------------
16
+
17
+ # We need the [Queue](http://rdoc.info/stdlib/thread/1.9.2/Queue), as our
18
+ # thread pool is largely dependent on it. Thanks to this, the implementation
19
+ # becomes very simple!
20
+ require 'thread'
21
+
22
+ # Public Interface
23
+ # ----------------
24
+
25
+ # `Pool` is our thread pool class. It will allow us to do three operations:
26
+ #
27
+ # - `.new(size)` creates a thread pool of a given size
28
+ # - `#schedule(*args, &job)` schedules a new job to be executed
29
+ # - `#shutdown` shuts down all threads (after letting them finish working, of course)
30
+ module FanSQS
31
+ class Pool
32
+
33
+ # ### initialization, or `Pool.new(size)`
34
+ # Creating a new `Pool` involves a certain amount of work. First, however,
35
+ # we need to define its’ `size`. It defines how many threads we will have
36
+ # working internally.
37
+ #
38
+ # Which size is best for you is hard to answer. You do not want it to be
39
+ # too low, as then you won’t be able to do as many things concurrently.
40
+ # However, if you make it too high Ruby will spend too much time switching
41
+ # between threads, and that will also degrade performance!
42
+ def initialize(size)
43
+ # Before we do anything else, we need to store some information about
44
+ # our pool. `@size` is useful later, when we want to shut our pool down,
45
+ # and `@jobs` is the heart of our pool that allows us to schedule work.
46
+ @size = size
47
+ @jobs = Queue.new
48
+
49
+ # #### Creating our pool of threads
50
+ # Once preparation is done, it’s time to create our pool of threads.
51
+ # Each thread store its’ index in a thread-local variable, in case we
52
+ # need to know which thread a job is executing in later on.
53
+ @pool = Array.new(@size) do |i|
54
+ Thread.new do
55
+ Thread.current[:id] = i
56
+
57
+ # We start off by defining a `catch` around our worker loop. This
58
+ # way we’ve provided a method for graceful shutdown of our threads.
59
+ # Shutting down is merely a `#schedule { throw :exit }` away!
60
+ catch(:exit) do
61
+ # The worker thread life-cycle is very simple. We continuously wait
62
+ # for tasks to be put into our job `Queue`. If the `Queue` is empty,
63
+ # we will wait until it’s not.
64
+ loop do
65
+ # Once we have a piece of work to be done, we will pull out the
66
+ # information we need and get to work.
67
+ job, args = @jobs.pop
68
+ job.call(*args)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ # ### Work scheduling
76
+
77
+ # To schedule a piece of work to be done is to say to the `Pool` that you
78
+ # want something done.
79
+ def schedule(*args, &block)
80
+ # Your given task will not be run immediately; rather, it will be put
81
+ # into the work `Queue` and executed once a thread is ready to work.
82
+ @jobs << [block, args]
83
+ end
84
+
85
+ # ### Graceful shutdown
86
+
87
+ # If you ever wish to close down your application, I took the liberty of
88
+ # making it easy for you to wait for any currently executing jobs to finish
89
+ # before you exit.
90
+ def shutdown
91
+ # A graceful shutdown involves threads exiting cleanly themselves, and
92
+ # since we’ve defined a `catch`-handler around the threads’ worker loop
93
+ # it is simply a matter of throwing `:exit`. Thus, if we throw one `:exit`
94
+ # for each thread in our pool, they will all exit eventually!
95
+ @size.times do
96
+ schedule { throw :exit }
97
+ end
98
+
99
+ # And now one final thing: wait for our `throw :exit` jobs to be run on
100
+ # all our worker threads. This call will not return until all worker threads
101
+ # have exited.
102
+ @pool.map(&:join)
103
+ end
104
+ end
105
+ end
106
+
107
+ # Demonstration
108
+ # -------------
109
+ # Running this file will display how the thread pool works.
110
+ # if $0 == __FILE__
111
+ # # - First, we create a new thread pool with a size of 10. This number is
112
+ # # lower than our planned amount of work, to show that threads do not
113
+ # # exit once they have finished a task.
114
+ # p = Pool.new(10)
115
+ #
116
+ # # - Next we simulate some workload by scheduling a large amount of work
117
+ # # to be done. The actual time taken for each job is randomized. This
118
+ # # is to demonstrate that even if two tasks are scheduled approximately
119
+ # # at the same time, the one that takes less time to execute is likely
120
+ # # to finish before the other one.
121
+ # 20.times do |i|
122
+ # p.schedule do
123
+ # sleep rand(4) + 2
124
+ # puts "Job #{i} finished by thread #{Thread.current[:id]}"
125
+ # end
126
+ # end
127
+ #
128
+ # # - Finally, register an `at_exit`-hook that will wait for our thread pool
129
+ # # to properly shut down before allowing our script to completely exit.
130
+ # at_exit { p.shutdown }
131
+ # end
132
+
133
+ # License (X11 License)
134
+ # =====================
135
+ #
136
+ # Copyright (c) 2012, Kim Burgestrand <kim@burgestrand.se>
137
+ #
138
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
139
+ # of this software and associated documentation files (the "Software"), to deal
140
+ # in the Software without restriction, including without limitation the rights
141
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
142
+ # copies of the Software, and to permit persons to whom the Software is
143
+ # furnished to do so, subject to the following conditions:
144
+ #
145
+ # The above copyright notice and this permission notice shall be included in
146
+ # all copies or substantial portions of the Software.
147
+ #
148
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
149
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
150
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
151
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
152
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
153
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
154
+ # SOFTWARE.
155
+
@@ -0,0 +1,37 @@
1
+ module FanSQS
2
+ class QueueWrapper
3
+ @cache ||= {} # cache for queues
4
+
5
+ class << self
6
+ # Find out if the queue exists. If it does not exist, create a new one
7
+ # This line below has a high chance of causing confusion as it will try to use either FanSQS::Queue or AWS::SQS::Queue. Maybe use the class from AWS gem instead?
8
+ def instantiate(qname)
9
+ name = formatted_queue_name(qname)
10
+ exists?(name) || create_new(name)
11
+ end
12
+
13
+ def create_new(name)
14
+ AWS.sqs.queues.create(name.to_s)
15
+ end
16
+
17
+ def cache
18
+ @cache
19
+ end
20
+
21
+ def exists?(qname)
22
+ if @cache[qname]
23
+ return @cache[qname]
24
+ else
25
+ return @cache[qname] = AWS.sqs.queues.named(formatted_queue_name(qname)) while @cache[qname] == nil
26
+ end
27
+ rescue AWS::SQS::Errors::NonExistentQueue
28
+ return false
29
+ end
30
+
31
+ private
32
+ def formatted_queue_name(qname)
33
+ qname.to_s
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,59 @@
1
+ # QueuesCache's purpose is for speed optimization. Using this class, we do not have to constantly ping
2
+ # AWS for new queues. It will ping AWS only every X times.
3
+ module FanSQS
4
+ class QueuesCache
5
+ RESET_THRESHOLD = 20000
6
+
7
+ def initialize(qnames)
8
+ @sqs_client ||= AWS::SQS::Client.new
9
+ @qnames = resolve_qnames(qnames)
10
+ @counter = 1
11
+ @queues = []
12
+ end
13
+
14
+ def fetch
15
+ if @qnames.empty?
16
+ fetch_all_queues
17
+ else
18
+ fetch_specific_queues
19
+ end
20
+ end
21
+
22
+ private
23
+ # Fetch all queues except for the queue equals to FanSQS::ErrorQueue
24
+ def fetch_all_queues
25
+ if @counter % RESET_THRESHOLD == 0
26
+ @counter = 1 # reset counter
27
+ @queue_names = @sqs_client.list_queues[:queue_urls].map { |q| q.split('/').last }.uniq
28
+ @queue_names.reject! { |name| name == FanSQS::ErrorQueue.to_s } # do not include the error queue
29
+ @queues = @queue_names.inject([]) { |queues, name| queues << QueueWrapper.instantiate(name) } # reset cache & fetch queues from AWS
30
+ else
31
+ @counter += 1
32
+ @queues
33
+ end
34
+ end
35
+
36
+ # Fetch only specific queues. Can also fetch queues with prefix = "xxx*"
37
+ def fetch_specific_queues
38
+ return @queues unless @queues.empty?
39
+ @qnames.each do |qname|
40
+ if qname =~ /\*/
41
+ queues_with_prefix = AWS::SQS.new.queues.with_prefix(qname.split('*').first)
42
+ queues_with_prefix.each do |queue|
43
+ @queues << queue
44
+ end
45
+ else
46
+ @queues << AWS::SQS.new.queues.named(qname)
47
+ end
48
+ end
49
+ end
50
+
51
+ def resolve_qnames(qnames)
52
+ if qnames.is_a?(Array)
53
+ qnames.map(&:to_s)
54
+ else
55
+ qnames.to_s.split(',').uniq
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ module FanSQS
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load File.expand_path(File.dirname(__FILE__) + "/tasks/FanSQS.rake")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ namespace :FanSQS do
2
+ desc "FanSQS constantly polls for new job from all queues in SQS and execute jobs"
3
+ task :start_polling => :environment do
4
+ puts "FanSQS polling starts ..."
5
+ FanSQS::Poller.start
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module FanSQS
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,30 @@
1
+ require 'thread'
2
+
3
+ module FanSQS
4
+ module Worker
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ base.class_attribute :fan_sqs_options_hash
8
+ end
9
+
10
+ module ClassMethods
11
+ def perform_async(*args)
12
+ FanSQS.pool.schedule do # Allows for multiple concurrent (non-blocking) HTTP requests to SQS
13
+ queue, sent_message = nil
14
+ qname = fan_sqs_options_hash ? fan_sqs_options_hash[:queue] : :fan_sqs_queue
15
+ queue = FanSQS::QueueWrapper.instantiate(qname)
16
+ params = { class: self.name, arguments: args }
17
+ sent_message = queue.send_message(params.to_json) while sent_message.try(&:md5) == nil # retry until receives sent_message confirmation
18
+ end
19
+ end
20
+
21
+ def set_fan_sqs_options(options = {})
22
+ if options.empty?
23
+ self.fan_sqs_options_hash = { queue: :fan_sqs_queue }
24
+ else
25
+ self.fan_sqs_options_hash = options
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ describe FanSQS::MessageParser do
4
+ describe "#parse" do
5
+ it "should return a JSON object with expected result" do
6
+ msg = { a: 1, b: 2}
7
+ json = FanSQS::MessageParser.parse(msg.to_json)
8
+ expect(json).to eq(msg)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe FanSQS::Poller do
4
+ describe "#start" do
5
+ it "is a pending example"
6
+ end
7
+
8
+ describe "#process" do
9
+ it "is a pending example"
10
+ end
11
+
12
+ describe "#store_exception" do
13
+ it "is a pending example"
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe FanSQS::Pool do
4
+ describe "#initialize" do
5
+ it "is a pending example"
6
+ end
7
+
8
+ describe "#schedule" do
9
+ it "is a pending example"
10
+ end
11
+
12
+ describe "#shutdown" do
13
+ it "is a pending example"
14
+ end
15
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe FanSQS::QueueWrapper do
4
+ before do
5
+ stub_retrieving_named_queues
6
+ end
7
+
8
+ describe "#exists?" do
9
+ context "cached" do
10
+ it "should returns a named queue from @cache" do
11
+ sample_queue = mocked_queue
12
+ FanSQS::QueueWrapper.instance_variable_set(:@cache, {:sample_qname => sample_queue})
13
+ expect(FanSQS::QueueWrapper.exists?(:sample_qname)).to eq(sample_queue)
14
+ end
15
+ end
16
+
17
+ context "not cached" do
18
+ before do
19
+ FanSQS::QueueWrapper.instance_variable_set(:@cache, {}) #empty cache
20
+ end
21
+
22
+ it "should returns fetches the queue from AWS" do
23
+ expect_any_instance_of(AWS::SQS::QueueCollection).to receive(:named)
24
+ FanSQS::QueueWrapper.exists?(:sample_qname)
25
+ end
26
+
27
+ context "non-existent queue" do
28
+ it "should raise exception AWS::SQS::Errors::NonExistentQueue and returns false" do
29
+ stub_retrieving_named_queues_raise_exception
30
+ expect(FanSQS::QueueWrapper.exists?(:sample_qname)).to eq(false)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ describe "#create_new" do
37
+ it "should call #create method from QueueCollection" do
38
+ expect_any_instance_of(AWS::SQS::QueueCollection).to receive(:create).once
39
+ FanSQS::QueueWrapper.create_new(:sample_queue)
40
+ end
41
+ end
42
+
43
+ describe "#instantiate" do
44
+ context "cached" do
45
+ it "should call exists?, receive a mocked queue, and not call create_new" do
46
+ sample_queue = mocked_queue
47
+ FanSQS::QueueWrapper.instance_variable_set(:@cache, {:sample_qname => sample_queue})
48
+ expect(FanSQS::QueueWrapper).to_not receive(:create_new)
49
+ FanSQS::QueueWrapper.instantiate(:sample_qname)
50
+ end
51
+
52
+ it "should call exists?, catch an exception, and call create_new" do
53
+ stub_retrieving_named_queues_raise_exception
54
+ FanSQS::QueueWrapper.instance_variable_set(:@cache, {})
55
+ expect(FanSQS::QueueWrapper).to_not receive(:create_new).once
56
+ FanSQS::QueueWrapper.instantiate(:sample_qname)
57
+ end
58
+ end
59
+ end
60
+
61
+ it "formats symbol to string qname" do
62
+ expect(FanSQS::QueueWrapper.send(:formatted_queue_name, :symbol)).to be_an_instance_of(String)
63
+ end
64
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ describe FanSQS::QueuesCache do
4
+ describe "#initialize" do
5
+ it "should call these methods and end up with these values" do
6
+ qnames = [ :queue_1, :queue_2 ]
7
+ @queues_cache = FanSQS::QueuesCache.new(qnames)
8
+ expect(@queues_cache.instance_variable_get(:@qnames)).to eq(qnames.map(&:to_s))
9
+ expect(@queues_cache.instance_variable_get(:@counter)).to eq(1)
10
+ expect(@queues_cache.instance_variable_get(:@queues)).to eq([])
11
+ end
12
+ end
13
+
14
+ describe "#fetch" do
15
+ context "@qnames.empty" do
16
+ it "should call fetch_all_queues" do
17
+ qnames = []
18
+ @queues_cache = FanSQS::QueuesCache.new(qnames)
19
+ expect(@queues_cache).to receive(:fetch_all_queues).once
20
+ @queues_cache.fetch
21
+ end
22
+ end
23
+
24
+ context "@qnames is not empty" do
25
+ it "should call fetch_specific_queues" do
26
+ qnames = [:queue_1, :queue_2]
27
+ @queues_cache = FanSQS::QueuesCache.new(qnames)
28
+ expect(@queues_cache).to receive(:fetch_specific_queues).once
29
+ @queues_cache.fetch
30
+ end
31
+ end
32
+ end
33
+
34
+ describe "#fetch_all_queues" do
35
+ context "counter % RESET_THRESHOLD != 0" do
36
+ before do
37
+ @queues_cache = FanSQS::QueuesCache.new([:queue_1, :queue_2])
38
+ @sample_queues = [ mocked_queue('queue_1'), mocked_queue('queue_2') ]
39
+ @queues_cache.instance_variable_set(:@queues, @sample_queues)
40
+ end
41
+
42
+ it "should return 2 queues" do
43
+ queues = @queues_cache.send(:fetch_all_queues)
44
+ expect(queues.size).to eq(2)
45
+ expect(queues).to eq(@sample_queues)
46
+ end
47
+ end
48
+
49
+ context "counter % RESET_THRESHOLD == 0" do
50
+ before do
51
+ stub_client_list_queues
52
+ stub_retrieving_named_queues
53
+ @queues_cache = FanSQS::QueuesCache.new([:queue_1, :queue_2, :queue_3])
54
+ @queues_cache.instance_variable_set(:@cache, { :queue_1 => mocked_queue('queue_1'), :queue_2 => mocked_queue('queue_2'), :queue_3 => mocked_queue('queue_3')})
55
+ @queues_cache.instance_variable_set(:@counter, FanSQS::QueuesCache::RESET_THRESHOLD)
56
+ end
57
+
58
+ it "should return 3 queues" do
59
+ queues = @queues_cache.send(:fetch_all_queues)
60
+ expect(queues.size).to eq(3)
61
+ end
62
+ end
63
+ end
64
+
65
+ describe "#fetch_specific_queues" do
66
+ context "@queues is not empty" do
67
+ before do
68
+ @queues_cache = FanSQS::QueuesCache.new([:queue_1, :queue_2, :queue_3])
69
+ @queues_cache.instance_variable_set(:@queues, [ mocked_queue('queue_1') ])
70
+ end
71
+
72
+ it "should return @queues" do
73
+ queues = @queues_cache.send(:fetch_specific_queues)
74
+ expect(queues.size).to eq(1)
75
+ end
76
+ end
77
+
78
+ context "@queues is empty" do
79
+ before do
80
+ stub_retrieving_named_queues
81
+ @queues_cache = FanSQS::QueuesCache.new([:queue_1, :queue_2])
82
+ @queues_cache.instance_variable_set(:@queues, [ ])
83
+ end
84
+
85
+ it "should return 2 queues" do
86
+ queues = @queues_cache.send(:fetch_specific_queues)
87
+ expect(queues.size).to eq(2)
88
+ end
89
+ end
90
+ end
91
+
92
+ describe "#resolve_qnames" do
93
+ let!(:qnames) { [ :q1, :q2 ] }
94
+ let!(:obj) { FanSQS::QueuesCache.new(:sample) }
95
+
96
+ it "should return the argument as it is passed in if the argument is an Array" do
97
+ expect(obj.send(:resolve_qnames, qnames)).to eq(qnames.map(&:to_s))
98
+ end
99
+
100
+ it "should return the list of queues if it is passed in as a String" do
101
+ expect(obj.send(:resolve_qnames, 'q1,q2,q3,q1').sort).to eq([ 'q1', 'q2', 'q3' ])
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe FanSQS::Worker do
4
+ describe "#included" do
5
+ it "is a pending example"
6
+ end
7
+
8
+ describe "#perform_async" do
9
+ it "is a pending example"
10
+ end
11
+
12
+ describe "#set_fan_sqs_options" do
13
+ it "is a pending example"
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+
8
+ require 'FanSQS'
9
+
10
+ Dir["./spec/support/**/*.rb"].each {|f| require f}
11
+
12
+ RSpec.configure do |config|
13
+ config.run_all_when_everything_filtered = true
14
+ config.filter_run :focus
15
+
16
+ config.include SQSQueueMocks
17
+ config.include SQSQueueStubs
18
+ config.include SQSReceivedMessageMocks
19
+ config.include SQSSentMessageMocks
20
+
21
+ # Run specs in random order to surface order dependencies. If you find an
22
+ # order dependency and want to debug it, you can fix the order by providing
23
+ # the seed, which is printed after each run.
24
+ # --seed 1234
25
+ config.order = 'random'
26
+ end
@@ -0,0 +1,13 @@
1
+ module SQSQueueMocks
2
+ def mocked_queue(name = 'queue')
3
+ queue = double(name)
4
+ received_messages = mocked_received_message_collection(10)
5
+ allow(queue).to receive(:receive_messages).with({ limit: 10 }).and_return(received_messages)
6
+ end
7
+
8
+ def mocked_queue_collection(size = 3)
9
+ queues = []
10
+ size.times { |i| queues << mocked_queue("queue_#{i}") }
11
+ queues
12
+ end
13
+ end
@@ -0,0 +1,47 @@
1
+ module SQSQueueStubs
2
+ # stubbed for
3
+ # AWS.sqs.queues.create(name.to_s)
4
+ def stub_queue_create
5
+ allow_any_instance_of(AWS::SQS::QueueCollection).to receive(:create).with(an_instance_of(String)).and_return(mocked_queue)
6
+ end
7
+
8
+ # stubbed for
9
+ # AWS::SQS.new.queues.named(qname)
10
+ # AWS.sqs.queues.named(formatted_queue_name(qname))
11
+ def stub_retrieving_named_queues
12
+ allow_any_instance_of(AWS::SQS::QueueCollection).to receive(:named).with(an_instance_of(String)).and_return(mocked_queue)
13
+ end
14
+
15
+ # stubbed for
16
+ # AWS::SQS.new.queues.named(qname)
17
+ def stub_retrieving_named_queues_raise_exception
18
+ allow_any_instance_of(AWS::SQS::QueueCollection).to receive(:named).with(an_instance_of(String)).and_raise(AWS::SQS::Errors::NonExistentQueue)
19
+ end
20
+
21
+ # stubbed for
22
+ # AWS::SQS.new.queues.with_prefix(qname.split('*').first)
23
+ def stub_retrieving_queues_with_prefix
24
+ allow_any_instance_of(AWS::SQS::QueueCollection).to receive(:with_refix).with(any_args).and_return(mocked_queue_collection)
25
+ end
26
+
27
+ # stubbed for
28
+ # AWS::SQS::Client.new.list_queues[:queue_urls]
29
+ def stub_client_list_queues
30
+ queue_urls = { queue_urls: ["https://sqs.us-east-1.amazonaws.com/1/queue_1", "https://sqs.us-east-1.amazonaws.com/2/queue_2", "https://sqs.us-east-1.amazonaws.com/3/queue_3"] }
31
+ allow_any_instance_of(AWS::SQS::Client::V20121105).to receive(:list_queues).and_return(queue_urls)
32
+ end
33
+
34
+ # stub for
35
+ # queue.receive_messages(limit: 10) do |message|
36
+ def stub_queue_receive_messages(size = 10)
37
+ received_messages = mocked_received_message_collection(size)
38
+ allow_any_instance_of(AWS::SQS::Queue).to receive(:receive_messages).with(limit: size).and_yield(receive_messages)
39
+ end
40
+
41
+ # stub for
42
+ # params = { class: self.name, arguments: args }
43
+ # queue.send_message(params.to_json)
44
+ def stub_queue_send_message(size = 10)
45
+ allow_any_instance_of(AWS::SQS::Queue).to receive(:send_message).with(an_instance_of(Hash)).and_return(mocked_sent_message)
46
+ end
47
+ end
@@ -0,0 +1,17 @@
1
+ module SQSReceivedMessageMocks
2
+ def mocked_received_message(name = 'received_message')
3
+ received_message = double(name)
4
+ allow(received_message).to receive(:body).and_return(
5
+ {
6
+ class: 'MessagePublisher',
7
+ arguments: [ name ]
8
+ }.to_json
9
+ )
10
+ end
11
+
12
+ def mocked_received_message_collection(size = 3)
13
+ received_messages = []
14
+ size.times { |i| received_messages << mocked_received_message("received_message_#{i}") }
15
+ received_messages
16
+ end
17
+ end
@@ -0,0 +1,7 @@
1
+ module SQSSentMessageMocks
2
+ def mocked_sent_message(name = 'sent_message')
3
+ sent_message = double(name)
4
+ allow(sent_message).to receive(:md5).and_return('md5_hash_value')
5
+ sent_message
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,211 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: FanSQS
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Khang Pham
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.41.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.41.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rspec
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec-mocks
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: shoulda-matchers
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Distributed messages queuing system using AWS SQS
140
+ email:
141
+ - vkhang55@gmail.com
142
+ executables: []
143
+ extensions: []
144
+ extra_rdoc_files: []
145
+ files:
146
+ - ".gitignore"
147
+ - ".rspec"
148
+ - FanSQS.gemspec
149
+ - Gemfile
150
+ - Guardfile
151
+ - LICENSE.txt
152
+ - NOTES.txt
153
+ - README.md
154
+ - Rakefile
155
+ - lib/FanSQS.rb
156
+ - lib/FanSQS/message_parser.rb
157
+ - lib/FanSQS/poller.rb
158
+ - lib/FanSQS/pool.rb
159
+ - lib/FanSQS/queue_wrapper.rb
160
+ - lib/FanSQS/queues_cache.rb
161
+ - lib/FanSQS/railtie.rb
162
+ - lib/FanSQS/tasks/FanSQS.rake
163
+ - lib/FanSQS/version.rb
164
+ - lib/FanSQS/worker.rb
165
+ - spec/FanSQS/message_parser_spec.rb
166
+ - spec/FanSQS/poller_spec.rb
167
+ - spec/FanSQS/pool_spec.rb
168
+ - spec/FanSQS/queue_wrapper_spec.rb
169
+ - spec/FanSQS/queues_cache_spec.rb
170
+ - spec/FanSQS/worker_spec.rb
171
+ - spec/spec_helper.rb
172
+ - spec/support/sqs_queue_mocks.rb
173
+ - spec/support/sqs_queue_stubs.rb
174
+ - spec/support/sqs_received_message_mocks.rb
175
+ - spec/support/sqs_sent_message_mocks.rb
176
+ homepage: ''
177
+ licenses:
178
+ - MIT
179
+ metadata: {}
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ">="
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubyforge_project:
196
+ rubygems_version: 2.2.2
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: Distributed messages queuing system using AWS SQS
200
+ test_files:
201
+ - spec/FanSQS/message_parser_spec.rb
202
+ - spec/FanSQS/poller_spec.rb
203
+ - spec/FanSQS/pool_spec.rb
204
+ - spec/FanSQS/queue_wrapper_spec.rb
205
+ - spec/FanSQS/queues_cache_spec.rb
206
+ - spec/FanSQS/worker_spec.rb
207
+ - spec/spec_helper.rb
208
+ - spec/support/sqs_queue_mocks.rb
209
+ - spec/support/sqs_queue_stubs.rb
210
+ - spec/support/sqs_received_message_mocks.rb
211
+ - spec/support/sqs_sent_message_mocks.rb