mongodb_queue 1.0.0

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: a379e4ec7e6b43e4166d273dc9eff160010afd3e
4
+ data.tar.gz: e6b9fc4e04a191106eb1c5dd6dad91d198a7da3a
5
+ SHA512:
6
+ metadata.gz: b5fb39651738ec15e074ab64e2412f2817fd4883fa30fcdc8d321c1b12c7e45e8c9ba449a2a58a64c3cea106769b22caa6476d688236e406f4be38966da38bfb
7
+ data.tar.gz: edf9e76abd37c8d3bce8a31f9d5fbaf95ce4969be9d1e1811fdc6ae57742fec385df72cd44325f737b73ec8ca057d2c13256b014323e6864fbd875caa69f6ee1
@@ -0,0 +1,42 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+ Gemfile.lock
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalisation:
25
+ /.bundle/
26
+ /lib/bundler/man/
27
+
28
+ # for a library or gem, you might want to ignore these files since the code is
29
+ # intended to run in multiple environments; otherwise, check them in:
30
+ # Gemfile.lock
31
+ # .ruby-version
32
+ # .ruby-gemset
33
+
34
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35
+ .rvmrc
36
+
37
+ # Specific to IntelliJ
38
+ /.idea/
39
+ *.iml
40
+
41
+ test/mongo_config_helper.rb
42
+ test/reports
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongodb_queue.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
@@ -0,0 +1,124 @@
1
+ mongodb_queue
2
+ =============
3
+
4
+ [![Build Status](https://api.shippable.com/projects/5498868ed46935d5fbc0d547/badge?branchName=master)](https://app.shippable.com/projects/5498868ed46935d5fbc0d547/builds/latest) [![Gem Version](https://badge.fury.io/rb/mongodb_queue.svg)](http://badge.fury.io/rb/mongodb_queue) [![Downloads](http://ruby-gem-downloads-badge.herokuapp.com/mongodb_queue?type=total)](https://rubygems.org/gems/mongodb_queue)
5
+
6
+
7
+ MongoDB based work queue written in Ruby. Supports multiple queues.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'mongodb_queue'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install mongodb_queue
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ require 'mongodb_queue'
29
+
30
+ queue = MongoDBQueue::MongoDBQueue.new({address: 'localhost', port: 27017, database: 'test-db', collection: 'test-queue'})
31
+ person = {name: 'John', age: 32, id: '123456789'}
32
+ person2 = {name: 'Jane', age: 29, id: '123456789'}
33
+
34
+ # Basic Usage
35
+ queue.enqueue(person)
36
+ queued_person = queue.dequeue
37
+
38
+ # Custom Queue Names
39
+ queue.enqueue(person, :friends)
40
+ friend = queue.dequeue(:friends)
41
+
42
+ # Multiple Queues
43
+ queue.enqueue(person, [:faculty, :staff])
44
+ faculty_member = queue.dequeue(:faculty)
45
+ staff_member = queue.dequeue(:staff)
46
+
47
+ # Prevent Duplicate Items
48
+ @queue.enqueue(person, :faculty, {unique_field: :id_num})
49
+ @queue.enqueue(person2, :faculty, {unique_field: :id_num}) # This wont be queued
50
+ faculty_member = queue.dequeue(:faculty)
51
+
52
+ # Delete item when dequeued
53
+ # Be careful with this if using multiple queues as it deletes the document from all queues.
54
+ queue.enqueue(person, :faculty)
55
+ faculty_member = queue.dequeue(:faculty, {delete: true})
56
+
57
+ # Use a different dequeued status
58
+ queue.enqueue(person)
59
+ faculty_member = queue.dequeue(MongoDBQueue::DEFAULT_QUEUE, {status: :processing})
60
+
61
+ # Remove documents that have finished processing
62
+ queue.remove_all([:success, :error])
63
+
64
+ # Requeue documents that have been in a dequeued status for 5 minutes (300 seconds)
65
+ queue.requeue_timed_out(300)
66
+
67
+ # Requeue documents that have been in a processing status for 5 minutes (300 seconds)
68
+ queue.requeue_timed_out(300, :processing)
69
+
70
+ # Unset the name field for successfully processed documents
71
+ queue.unset_all([:success], :name)
72
+
73
+ # Disconnect from MongoDB when done
74
+ queue.destroy
75
+ ```
76
+
77
+ ## Sample MongoDB Document
78
+
79
+ This is a document that has been added to 2 queues - test_queue and test_queue2. It has already been dequeued from test_queue
80
+
81
+ ``` json
82
+ {
83
+ "_id" : ObjectId("549b1678b83b50d6bf000001"),
84
+ "name" : "John",
85
+ "age" : 32,
86
+ "id_num" : "123456789",
87
+ "queue" : [
88
+ {
89
+ "name" : "test_queue",
90
+ "status" : "dequeued",
91
+ "queued_timestamp" : ISODate("2014-12-24T19:39:36.051Z"),
92
+ "dequeued_timestamp" : ISODate("2014-12-24T19:39:36.053Z")
93
+ },
94
+ {
95
+ "name" : "test_queue2",
96
+ "status" : "queued",
97
+ "queued_timestamp" : ISODate("2014-12-24T19:39:55.052Z")
98
+ }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ ## Contributing
104
+
105
+ 1. Fork it ( https://github.com/dashingrocket/mongodb_queue/fork )
106
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
107
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
108
+ 4. Push to the branch (`git push origin my-new-feature`)
109
+ 5. Create a new Pull Request
110
+
111
+ ## License
112
+ Copyright 2014 Dashing Rocket, Ltd.
113
+
114
+ Licensed under the Apache License, Version 2.0 (the "License");
115
+ you may not use this file except in compliance with the License.
116
+ You may obtain a copy of the License at
117
+
118
+ http://www.apache.org/licenses/LICENSE-2.0
119
+
120
+ Unless required by applicable law or agreed to in writing, software
121
+ distributed under the License is distributed on an "AS IS" BASIS,
122
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
123
+ See the License for the specific language governing permissions and
124
+ limitations under the License.
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+ require 'ci/reporter/rake/test_unit'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = FileList['test/*_test.rb']
8
+ end
9
+
10
+ desc 'Run tests'
11
+ task :default => ['ci:setup:testunit', :test]
@@ -0,0 +1,243 @@
1
+ require 'logger'
2
+ require 'mongo'
3
+
4
+ module MongoDBQueue
5
+ # The default queue name
6
+ DEFAULT_QUEUE = :default_queue
7
+
8
+ # The default status of a document that is queued.
9
+ DEFAULT_QUEUE_STATUS = 'queued'
10
+
11
+ # The default status of a document that is dequeued.
12
+ DEFAULT_DEQUEUE_STATUS = 'dequeued'
13
+
14
+ # MongoDB Backed Queue
15
+ #
16
+ # @author Jesse Bowes
17
+ class MongoDBQueue
18
+ # The underlying queue's collection
19
+ attr_reader :collection
20
+
21
+ # Initializer
22
+ # @param settings [Hash] MongoDB connection settings
23
+ # @option settings [String] :address MongoDB address
24
+ # @option settings [Integer] :port MongoDB port
25
+ # @option settings [String] :database MongoDB database to use
26
+ # @option settings [String] :collection MongoDB collection to use
27
+ # @option settings [String] :username MongoDB username to use (optional)
28
+ # @option settings [String] :password MongoDB password to use (optional)
29
+ # @param logger [Logger] Use a specific logger, otherwise logs to STDOUT
30
+ def initialize(settings, logger=nil)
31
+ @logger = logger || Logger.new(STDOUT)
32
+ check_settings(settings)
33
+ @settings = settings
34
+ @unique_fields = []
35
+ connect_mongo
36
+ end
37
+
38
+ # Disconnects from MongoDB. Call before exiting.
39
+ def destroy
40
+ begin
41
+ disconnect_mongo
42
+ rescue; end
43
+ end
44
+
45
+ # Add an object to a queue
46
+ # @param object [Hash] The object to queue
47
+ # @param queue_names [Array] A list of queues to add the object to.
48
+ # @param opts [Hash] Options
49
+ # @option opts [String] :unique_field Prevent duplicate documents being queued on the same queue by this unique field
50
+ def enqueue(object, queue_names = [DEFAULT_QUEUE], opts={})
51
+ connect_mongo
52
+ unique_field = opts[:unique_field]
53
+ set_unique unique_field
54
+ send_queues(queue_names, object, unique_field)
55
+ end
56
+
57
+ # Gets an object from a queue
58
+ # @param queue_name [String] The queue to get the object from
59
+ # @param opts [Hash] Options
60
+ # @option opts [String] :status ({DEFAULT_DEQUEUE_STATUS}) The status to mark the document as after it's dequeued.
61
+ # @option opts [Boolean] :delete (false) Delete the object from ALL queues when dequeued.
62
+ # @return [Hash] Queued object or nil
63
+ def dequeue(queue_name = DEFAULT_QUEUE, opts={})
64
+ connect_mongo
65
+ queue_name = queue_name.to_s
66
+ status = opts[:status] || DEFAULT_DEQUEUE_STATUS
67
+ status = status.to_s
68
+ delete = opts[:delete]
69
+
70
+ query = {queue: {'$elemMatch' => {name: queue_name, status: DEFAULT_QUEUE_STATUS}}}
71
+
72
+ if delete
73
+ @collection.find_and_modify(query: query, remove: true)
74
+ else
75
+ @collection.find_and_modify(query: query, update: {'$set' => {'queue.$.status' => status, "queue.$.#{status}_timestamp" => Time.now}})
76
+ end
77
+ end
78
+
79
+ # Removes all MongoDB documents that have all their queue statuses set to the provided status(es).
80
+ # @param statuses [Array] Queue statuses that qualify a document for removal
81
+ # @return [Integer] Number of documents removed
82
+ def remove_all(statuses = [DEFAULT_DEQUEUE_STATUS])
83
+ statuses = [statuses].flatten.map{|s|s.to_s}
84
+ STDERR.puts "#{statuses}"
85
+ num_removed = 0
86
+ potential_items = @collection.find({'queue.status' => {'$in' => statuses}}, {fields: ['queue.status']})
87
+ potential_items.each do |item|
88
+ id = item['_id']
89
+ item_statuses = item['queue'].map{|s|s['status'].to_s}
90
+ item_statuses.uniq!
91
+ other_statuses = item_statuses - statuses
92
+
93
+ if other_statuses.empty?
94
+ result = @collection.remove({'_id' => id})
95
+ num_removed += result['n'] if result['n']
96
+ end
97
+ end
98
+ num_removed
99
+ end
100
+
101
+ # Requeues all documents that were dequeued more than timeout_sec ago and have a status that is in statuses
102
+ # @param statuses [Array] Queue statuses that qualify a document for requeuing
103
+ # @return [Integer] Number of documents requeued
104
+ def requeue_timed_out(timeout_sec, statuses = [DEFAULT_DEQUEUE_STATUS])
105
+ statuses = [statuses].flatten.map{|s|s.to_s}
106
+ timeout_time = Time.now - timeout_sec
107
+ num_requeued = 0
108
+ potential_items = @collection.find({'queue.status' => {'$in' => statuses}}, {fields: ['queue']})
109
+
110
+ potential_items.each do |item|
111
+ id = item['_id']
112
+ item['queue'].each do |queue|
113
+ queue_name = queue['name']
114
+ status = queue['status']
115
+ timestamp_field = "#{status}_timestamp"
116
+ timestamp = queue[timestamp_field]
117
+
118
+ if statuses.include?(status) && timestamp < timeout_time
119
+ # Requeue this item
120
+ result = @collection.update({'_id' => id, 'queue.name' => queue_name}, {'$set' => {'queue.$.status' => DEFAULT_QUEUE_STATUS}})
121
+ num_requeued += result['n'] if result['n']
122
+ end
123
+ end
124
+ end
125
+ num_requeued
126
+ end
127
+
128
+ # Unsets specified fields in MongoDB documents that have all their queue statuses set to the provided status(es).
129
+ # @param statuses [Array] Queue statuses that qualify a document for unsetting
130
+ # @param fields [Array] Fields to be unset
131
+ # @return [Integer] Number of documents removed
132
+ def unset_all(statuses, fields)
133
+ statuses = [statuses].flatten.map{|s|s.to_s}
134
+ fields = [fields].flatten.map{|f|f.to_s}
135
+ num_modified = 0
136
+ potential_items = @collection.find({'queue.status' => {'$in' => statuses}}, {fields: ['queue.status']})
137
+ fields_hash = {}
138
+ fields.each do |field|
139
+ fields_hash[field] = ''
140
+ end
141
+ potential_items.each do |item|
142
+ id = item['_id']
143
+ item_statuses = item['queue'].map{|s|s['status'].to_s}
144
+ item_statuses.uniq!
145
+ other_statuses = item_statuses - statuses
146
+
147
+ if other_statuses.empty?
148
+ result = @collection.update({'_id' => id}, {'$unset' => fields_hash})
149
+ num_modified += result['n'] if result['n']
150
+ end
151
+ end
152
+ num_modified
153
+ end
154
+
155
+ # Sets a specific field as being unique
156
+ # @param unique_field [String] The field that is unique
157
+ # @return [String] the name of the index created (if not already created)
158
+ def set_unique(unique_field)
159
+ if unique_field && !@unique_fields.include?(unique_field)
160
+ @unique_fields << unique_field
161
+ create_index(unique_field, {unique: true})
162
+ end
163
+ end
164
+
165
+ # Passthrough to create a Mongo Index
166
+ # @see Mongo::Collection#create_index
167
+ # @param spec [String] The field that is unique
168
+ # @return [String] the name of the index created.
169
+ def create_index(spec, opts={})
170
+ @collection.create_index(spec, opts)
171
+ end
172
+
173
+ private
174
+ def check_settings(settings)
175
+ raise 'No database address set' if settings[:address].nil?
176
+ raise 'No database port set' if settings[:port].nil?
177
+ raise 'No database set' if settings[:database].nil?
178
+ raise 'No collection set' if settings[:collection].nil?
179
+ end
180
+
181
+ def connect_mongo
182
+ if @client.nil? || !@client.connected?
183
+ @client = Mongo::MongoClient.new(@settings[:address], @settings[:port])
184
+ db = @client[@settings[:database]]
185
+ db.authenticate(@settings[:username], @settings[:password]) if @settings[:username] || @settings[:password]
186
+ @collection = db[@settings[:collection]]
187
+ @collection.create_index('queue.name')
188
+ @collection.create_index('queue.status')
189
+ @collection.create_index({'queue.name' => Mongo::ASCENDING, 'queue.status' => Mongo::ASCENDING})
190
+ end
191
+ end
192
+
193
+ def disconnect_mongo
194
+ @client.close if @client
195
+ end
196
+
197
+ def send_queues(queues, data, unique_field)
198
+ queues = [queues].flatten
199
+ queues.reject!{|q| q.nil? || q.empty?}
200
+ queues.map!{|q|q.to_s}
201
+
202
+ queue_list = []
203
+
204
+ doc = get_existing_doc(data, unique_field)
205
+
206
+ if doc.nil?
207
+ queues.each {|q| queue_list << {name: q, status: DEFAULT_QUEUE_STATUS, "#{DEFAULT_QUEUE_STATUS}_timestamp" => Time.now}}
208
+ if queue_list.empty?
209
+ @logger.info "Skipping item #{data.object_id}. No destination queues."
210
+ return nil
211
+ else
212
+ data[:queue] = queue_list
213
+ @logger.info "Queuing item #{data.object_id} into #{queue_list.collect{|q|q[:name]}}"
214
+ return @collection.insert data
215
+ end
216
+ else
217
+ @logger.info "\tAlready received unique #{data[unique_field]}."
218
+ docid = doc['_id']
219
+ previous_queues = doc['queue']
220
+ queues.each do |q|
221
+ exists = previous_queues.any? {|h| h['name'] == q}
222
+ queue_list << {name: q, status: DEFAULT_QUEUE_STATUS, "#{DEFAULT_QUEUE_STATUS}_timestamp" => Time.now} unless exists
223
+ end
224
+
225
+ if queue_list.empty?
226
+ @logger.info "\t\tSkipping item #{data[unique_field]}. No new queues"
227
+ return nil
228
+ else
229
+ @logger.info "Queuing item #{data[unique_field]} into #{queue_list.collect{|q|q[:name]}}"
230
+ return @collection.update({'_id' => docid}, {'$set' => data, '$addToSet' => { queue: { '$each' =>queue_list }}})
231
+ end
232
+ end
233
+ end
234
+
235
+ def get_existing_doc(data, unique_field)
236
+ if unique_field
237
+ @collection.find({unique_field => data[unique_field]}).next_document
238
+ else
239
+ nil
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,5 @@
1
+ # Namespace for classes and modules that handle MongoDB Backed Queue
2
+ module MongoDBQueue
3
+ # MongoDBQueue Version
4
+ VERSION = '1.0.0'
5
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongodb_queue/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'mongodb_queue'
8
+ spec.version = MongoDBQueue::VERSION
9
+ spec.authors = ['Jesse Bowes']
10
+ spec.email = ['jbowes@dashingrocket.com']
11
+ spec.summary = 'MongoDB Messaging Queue'
12
+ spec.description = 'A mongoDB based messaging queue that supports multiple queues'
13
+ spec.homepage = 'https://github.com/dashingrocket/mongodb_queue'
14
+ spec.license = 'Apache-2.0'
15
+ spec.required_ruby_version = '>= 1.9.3'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.7'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'ci_reporter_test_unit'
25
+ spec.add_development_dependency 'simplecov'
26
+ spec.add_development_dependency 'simplecov-cobertura'
27
+
28
+ spec.add_runtime_dependency 'bson_ext', '~> 1.11'
29
+ spec.add_runtime_dependency 'mongo', '~> 1.11'
30
+ end
@@ -0,0 +1,25 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9
5
+ - 2.0.0
6
+ - 2.1
7
+
8
+ services:
9
+ - mongodb
10
+
11
+ before_script:
12
+ - mongo test-db --eval 'db.addUser("test-user", "test-pass");'
13
+ - mongod --version
14
+
15
+ env:
16
+ - CI_REPORTS=shippable/testresults COVERAGE_REPORTS=shippable/codecoverage
17
+
18
+ cache: true
19
+
20
+ notifications:
21
+ email:
22
+ recipients:
23
+ - jbowes@dashingrocket.com
24
+ on_success: change
25
+ on_failure: always
@@ -0,0 +1,260 @@
1
+ require_relative 'test_helper'
2
+
3
+ require 'test/unit'
4
+ require 'mongodb_queue'
5
+
6
+ class MongoDBQueueTest < Test::Unit::TestCase
7
+
8
+ # Called before every test method runs. Can be used
9
+ # to set up fixture information.
10
+ def setup
11
+ @config = get_config
12
+ @queue = MongoDBQueue::MongoDBQueue.new(@config)
13
+
14
+ @inspect_queue = @queue.collection
15
+ end
16
+
17
+ # the uby mongo driver appears to derive _id from ruby's object id, so lets make a new object
18
+ def get_person
19
+ {name: 'John', age: 32, id_num: '123456789'}
20
+ end
21
+
22
+ # Get config or overridden config
23
+ def get_config
24
+ begin
25
+ # Allow overriding of config for local testing
26
+ require_relative 'mongo_config_helper'
27
+ config = MongoDBConfigHelper::config
28
+ rescue LoadError
29
+ config = {
30
+ address: 'localhost',
31
+ port: 27017,
32
+ database: 'test-db',
33
+ collection: 'test-collection',
34
+ username: 'test-user',
35
+ password: 'test-pass'
36
+ }
37
+ end
38
+ config
39
+ end
40
+
41
+
42
+ # Called after every test method runs. Can be used to tear
43
+ # down fixture information.
44
+ def teardown
45
+ @inspect_queue.remove
46
+ @inspect_queue.drop_indexes
47
+ @queue.destroy
48
+ end
49
+
50
+ def assert_empty_queue(queue = MongoDBQueue::DEFAULT_QUEUE)
51
+ assert_nil(@queue.dequeue(queue)) # Queue is empty
52
+ end
53
+
54
+ def test_initialize_no_address
55
+ @config.delete :address
56
+ exception = assert_raise(RuntimeError) {MongoDBQueue::MongoDBQueue.new(@config)}
57
+ assert_equal('No database address set', exception.message)
58
+ end
59
+
60
+ def test_initialize_no_port
61
+ @config.delete :port
62
+ exception = assert_raise(RuntimeError) {MongoDBQueue::MongoDBQueue.new(@config)}
63
+ assert_equal('No database port set', exception.message)
64
+ end
65
+
66
+ def test_initialize_no_db
67
+ @config.delete :database
68
+ exception = assert_raise(RuntimeError) {MongoDBQueue::MongoDBQueue.new(@config)}
69
+ assert_equal('No database set', exception.message)
70
+ end
71
+
72
+ def test_initialize_no_collection
73
+ @config.delete :collection
74
+ exception = assert_raise(RuntimeError) {MongoDBQueue::MongoDBQueue.new(@config)}
75
+ assert_equal('No collection set', exception.message)
76
+ end
77
+
78
+ def test_enqueue_dequeue
79
+ person = get_person
80
+ assert_not_nil @queue.enqueue(person)
81
+ dequeued = @queue.dequeue
82
+
83
+ assert_equal(person[:name], dequeued['name'])
84
+ assert_equal(person[:age], dequeued['age'])
85
+ assert_equal(person[:id_num], dequeued['id_num'])
86
+
87
+ assert_nil(@queue.dequeue)
88
+ assert_not_nil @inspect_queue.find.next_document
89
+ end
90
+
91
+ def test_enqueue_dequeue_delete
92
+ person = get_person
93
+ assert_not_nil @queue.enqueue(person)
94
+ dequeued = @queue.dequeue(MongoDBQueue::DEFAULT_QUEUE, {delete: true})
95
+
96
+ assert_equal(person[:name], dequeued['name'])
97
+ assert_equal(person[:age], dequeued['age'])
98
+ assert_equal(person[:id_num], dequeued['id_num'])
99
+
100
+ assert_nil(@queue.dequeue)
101
+ assert_nil @inspect_queue.find.next_document
102
+ end
103
+
104
+ def test_enqueue_dequeue_with_queue
105
+ person = get_person
106
+ assert_not_nil @queue.enqueue(person, :test_queue)
107
+ dequeued = @queue.dequeue(:test_queue)
108
+
109
+ assert_equal(person[:name], dequeued['name'])
110
+ assert_equal(person[:age], dequeued['age'])
111
+ assert_equal(person[:id_num], dequeued['id_num'])
112
+
113
+ assert_empty_queue(:test_queue)
114
+ end
115
+
116
+ def test_queue_same_doc_twice
117
+ assert_not_nil @queue.enqueue(get_person)
118
+ assert_not_nil @queue.enqueue(get_person)
119
+
120
+ person = get_person
121
+
122
+ dequeued = @queue.dequeue
123
+ assert_equal(person[:name], dequeued['name'])
124
+ assert_equal(person[:age], dequeued['age'])
125
+ assert_equal(person[:id_num], dequeued['id_num'])
126
+
127
+ dequeued2 = @queue.dequeue
128
+ assert_equal(person[:name], dequeued2['name'])
129
+ assert_equal(person[:age], dequeued2['age'])
130
+ assert_equal(person[:id_num], dequeued2['id_num'])
131
+
132
+ assert_empty_queue
133
+ end
134
+
135
+ def test_queue_unique_doc_twice
136
+ person1 = get_person
137
+ person2 = get_person
138
+
139
+ person1[:id_num] = 123
140
+ person2[:id_num] = person1[:id_num]
141
+
142
+ assert_not_nil @queue.enqueue(person1, :test_queue, {unique_field: :id_num})
143
+ assert_nil @queue.enqueue(person2, :test_queue, {unique_field: :id_num})
144
+
145
+ dequeued = @queue.dequeue(:test_queue)
146
+ assert_equal(person1[:name], dequeued['name'])
147
+ assert_equal(person1[:age], dequeued['age'])
148
+ assert_equal(person1[:id_num], dequeued['id_num'])
149
+
150
+ assert_empty_queue(:test_queue)
151
+ end
152
+
153
+ def test_add_queue
154
+ assert_not_nil @queue.enqueue(get_person, :test_queue, {unique_field: :id_num})
155
+ assert_not_nil @queue.enqueue(get_person, [:test_queue, :test_queue2], {unique_field: :id_num})
156
+
157
+ person = get_person
158
+
159
+ dequeued = @queue.dequeue(:test_queue)
160
+ assert_equal(person[:name], dequeued['name'])
161
+ assert_equal(person[:age], dequeued['age'])
162
+ assert_equal(person[:id_num], dequeued['id_num'])
163
+
164
+ dequeued = @queue.dequeue(:test_queue2)
165
+ assert_equal(person[:name], dequeued['name'])
166
+ assert_equal(person[:age], dequeued['age'])
167
+ assert_equal(person[:id_num], dequeued['id_num'])
168
+
169
+ assert_empty_queue(:test_queue)
170
+ assert_empty_queue(:test_queue2)
171
+ end
172
+
173
+ def test_no_queue
174
+ assert_nil @queue.enqueue(get_person, [nil, ''])
175
+ end
176
+
177
+ def test_remove_all_with_status_same
178
+ # q1 & q2 => success
179
+ @queue.enqueue(get_person, [:queue1, :queue2])
180
+ @queue.dequeue(:queue1, {status: :success})
181
+ @queue.dequeue(:queue2, {status: :success})
182
+ assert_equal(1, @queue.remove_all(:success))
183
+ assert_equal(0, @queue.remove_all(:success))
184
+ end
185
+
186
+ def test_remove_all_with_status_multiple
187
+ # q1 => success, q2 => error
188
+ @queue.enqueue(get_person, [:queue1, :queue2])
189
+ @queue.dequeue(:queue1, {status: :success})
190
+ @queue.dequeue(:queue2, {status: :error})
191
+ assert_equal(1, @queue.remove_all([:success, :error]))
192
+ assert_equal(0, @queue.remove_all([:success, :error]))
193
+ end
194
+
195
+ def test_remove_all_with_status_single
196
+ # q1 => success, q2 => error
197
+ @queue.enqueue(get_person, [:queue1])
198
+ @queue.dequeue(:queue1, {status: :success})
199
+ assert_equal(1, @queue.remove_all([:success, :error]))
200
+ assert_equal(0, @queue.remove_all([:success, :error]))
201
+ end
202
+
203
+ def test_remove_all_not_qualified
204
+ # q1 => success, q2 => processing
205
+ @queue.enqueue(get_person, [:queue1, :queue2])
206
+ @queue.dequeue(:queue1, {status: :success})
207
+ @queue.dequeue(:queue2, {status: :processing})
208
+ assert_equal(0, @queue.remove_all([:success, :error]))
209
+ end
210
+
211
+ def test_requeue_timed_out
212
+ @queue.enqueue(get_person)
213
+ @queue.dequeue
214
+ sleep 0.1
215
+ assert_nil @queue.dequeue
216
+ assert_equal(1, @queue.requeue_timed_out(0.1, MongoDBQueue::DEFAULT_DEQUEUE_STATUS))
217
+ assert_not_nil @queue.dequeue
218
+ end
219
+
220
+ def test_requeue_timed_out_young
221
+ @queue.enqueue(get_person)
222
+ @queue.dequeue
223
+ assert_nil @queue.dequeue
224
+ assert_equal(0, @queue.requeue_timed_out(5, MongoDBQueue::DEFAULT_DEQUEUE_STATUS))
225
+ assert_nil @queue.dequeue
226
+ end
227
+
228
+ def test_requeue_timed_out_multiple
229
+ @queue.enqueue(get_person, [:queue1, :queue2, :queue3])
230
+ @queue.dequeue(:queue1, {status: :success})
231
+ @queue.dequeue(:queue2, {status: :error})
232
+ @queue.dequeue(:queue3, {status: :processing})
233
+ sleep 0.1
234
+ assert_nil @queue.dequeue(:queue1)
235
+ assert_nil @queue.dequeue(:queue2)
236
+ assert_nil @queue.dequeue(:queue3)
237
+ assert_equal(2, @queue.requeue_timed_out(0.1, [:error, :processing]))
238
+ assert_nil @queue.dequeue(:queue1)
239
+ assert_not_nil @queue.dequeue(:queue2)
240
+ assert_not_nil @queue.dequeue(:queue3)
241
+ end
242
+
243
+ def test_unset_all
244
+ @queue.enqueue(get_person, [:queue1, :queue2])
245
+ @queue.dequeue(:queue1, {status: :success})
246
+ @queue.dequeue(:queue2, {status: :error})
247
+ assert_not_nil @inspect_queue.find().next_document['name']
248
+ assert_equal(1, @queue.unset_all([:success, :error], :name))
249
+ assert_nil @inspect_queue.find().next_document['name']
250
+ end
251
+
252
+ def test_unset_all_not_qualified
253
+ @queue.enqueue(get_person, [:queue1, :queue2])
254
+ @queue.dequeue(:queue1, {status: :success})
255
+ @queue.dequeue(:queue2, {status: :error})
256
+ assert_not_nil @inspect_queue.find().next_document['name']
257
+ assert_equal(0, @queue.unset_all([:success], :name))
258
+ assert_not_nil @inspect_queue.find().next_document['name']
259
+ end
260
+ end
@@ -0,0 +1,10 @@
1
+ require 'simplecov'
2
+ require 'simplecov-cobertura'
3
+
4
+ SimpleCov.formatters = [
5
+ SimpleCov::Formatter::HTMLFormatter,
6
+ SimpleCov::Formatter::CoberturaFormatter
7
+ ]
8
+
9
+ SimpleCov.coverage_dir(ENV['COVERAGE_REPORTS'])
10
+ SimpleCov.start
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongodb_queue
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jesse Bowes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: ci_reporter_test_unit
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
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: simplecov-cobertura
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: bson_ext
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.11'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.11'
97
+ - !ruby/object:Gem::Dependency
98
+ name: mongo
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.11'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.11'
111
+ description: A mongoDB based messaging queue that supports multiple queues
112
+ email:
113
+ - jbowes@dashingrocket.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - Gemfile
120
+ - LICENSE
121
+ - README.md
122
+ - Rakefile
123
+ - lib/mongodb_queue.rb
124
+ - lib/mongodb_queue/version.rb
125
+ - mongodb_queue.gemspec
126
+ - shippable.yml
127
+ - test/mongodb_queue_test.rb
128
+ - test/test_helper.rb
129
+ homepage: https://github.com/dashingrocket/mongodb_queue
130
+ licenses:
131
+ - Apache-2.0
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 1.9.3
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.4.3
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: MongoDB Messaging Queue
153
+ test_files:
154
+ - test/mongodb_queue_test.rb
155
+ - test/test_helper.rb