true_queue 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+
2
+ ## Overview
3
+
4
+ TrueQueue (as in "the one true queue") is a proxy to multiple queueing backends.
5
+
6
+ The most mature backend is one based on Redis which is a homegrown set of operations over Redis hashes and sorted sets that provides:
7
+
8
+ * A fast in-memory queue, with constant backups to disk.
9
+ * Atomic add and remove operations
10
+ * An inspectable queue: you can see what's in the queue or peek into the head of the queue without changing it.
11
+ * A reservable remove, where if the client quits halfway, the item is put back in.
12
+ * Priority queues
13
+ * And delayed retrieval for items, you can set a timestamp after which the entries are slated for removal.
14
+
15
+ Not to mention the biggest advantage of all: continue to use your existing Redis install!
16
+
17
+ There are other backends as well:
18
+
19
+ * memory: a simple in-process memory queue using a sorted set
20
+ * zermoq: an experimental backend built on zeromq (see bin/zeromq-memory-queue.rb)
21
+ * amqp: an AMQP backend to work with RabbitMQ
22
+
23
+ There are a set of uniform conventions regardless of the queue backend used:
24
+
25
+ * Queues are created when values are added to it. All input is encoded into JSON when stored and decoded when dequeued.
26
+ * When a queue is empty, nil is returned on remove
27
+ * There are always 9 standard methods: add, add\_bulk, remove, peek, list, empty, size, remove\_queues, list_queues.
28
+
29
+ Certain features (for e.g. a reservable remove) might not be available on all queue backends.
30
+
31
+ ## Dependencies
32
+
33
+ Ruby version 1.9.2p290
34
+
35
+ All other dependencies are in the gemspec
36
+
37
+ ## Install
38
+
39
+ $ gem install true_queue
40
+
41
+ ## Spec
42
+
43
+ $ bundle exec guard
44
+
45
+ ## Usage
46
+
47
+ ### Connect
48
+
49
+ For the redis backend,
50
+
51
+ queue = TrueQueue.queue(:redis, options = { :redis_options => { } })
52
+
53
+ For the AMQP backend using bunny,
54
+
55
+ queue = TrueQueue.queue(:amqp, options = { :bunny_options => { } })
56
+
57
+ For the in-memory backend that only stores keys within a process space,
58
+
59
+ queue = TrueQueue.queue(:memory, options = {})
60
+
61
+ For the zeromq backend,
62
+
63
+ queue = TrueQueue.queue(:zeromq, options = {})
64
+
65
+ ### Add an item
66
+
67
+ queue.add("queue_name", {:jobid => 23, :url => 'http://example.com/' })
68
+
69
+ Items can also have arbitrary metadata. They are stored alongside items and returned on a dequeue.
70
+
71
+ queue.add("publish", {:jobid => 23, :url => 'http://example.com/' }, {'importance' => low})
72
+
73
+ Certain metadata have special meaning. If you set a dequeue-timestamp to a Time object, the item will only be dequeued *after* that time. Note that it won't be dequeued exactly *at* the time, but at any time afterwards.
74
+
75
+ # only dequeue 5s after queueing
76
+ queue.add("publish", {:jobid => 23, :url => 'http://example.com/' }, {'dequeue-timestamp' => Time.now + 5 })
77
+
78
+ Another special metadata keyword is priority.
79
+
80
+ # priority is an integer from 1 to 100. Higher priority items are dequeued first.
81
+ queue.add("publish", {:jobid => 23, :url => 'http://example.com/' }, {'priority' => 5})
82
+
83
+ Items with priority set (or a higher priority) are always dequeued first.
84
+
85
+ Note that the AMQP backend doesn't support priorities or the dequeue timestamp.
86
+
87
+ ### Remove an item
88
+
89
+ # dequeue
90
+ queue.remove("publish")
91
+
92
+ \#remove returns an array. The first element is the Ruby object in the queue, the second is the associated metadata (always a Hash).
93
+
94
+ => {:jobid => 23, :url => 'http://example.com/' }, {'priority' => 5}
95
+
96
+ \#remove also can take a block. This is the recommended way to remove an item from a queue.
97
+
98
+ # dequeue into a block
99
+ queue.remove do |item|
100
+ #process item
101
+ ...
102
+ end
103
+
104
+ When a block is passed, Queue ensures that the item is put back in case of an error within the block.
105
+
106
+ Inside a block, you can also manually raise {TrueQueue::RemoveAbort} to put back the item:
107
+
108
+ # dequeue into a block
109
+ queue.remove do |item|
110
+ #this item will be put back
111
+ raise TrueQueue::RemoveAbort
112
+ end
113
+
114
+ Note: you cannot pass in a block using the zeromq or amqp queue types.
115
+
116
+ Another thing to note is that unlike in other queues, **remove does not block and returns nil when the queue is empty**. So you'll have to manually call sleep(delay) and re-poll the queue. This implementation might change in the future:
117
+
118
+ loop do # dequeue into a block
119
+ queue.remove do |item|
120
+ next unless item
121
+ #this item will be put back
122
+ raise TrueQueue::RemoveAbort
123
+ end
124
+
125
+ sleep 1
126
+ end
127
+
128
+ ### List all items in a queue
129
+
130
+ This is an expensive operation, but at times, very useful!
131
+
132
+ queue.list "queue"
133
+
134
+ This is not supported for the amqp queue type.
135
+
136
+ ### List available queues
137
+
138
+ queue.list_queues
139
+
140
+ Returns an array of all queues stored in the Redis instance.
141
+
142
+ ### Remove queues
143
+
144
+ This empties and removes all queues:
145
+
146
+ queue.remove_queues
147
+
148
+ To selectively remove queues:
149
+
150
+ queue.remove_queue "queue1"
151
+ queue.remove_queues "queue1", "queue2"
152
+
153
+ ## Performance & Memory Usage
154
+
155
+ See detailed analysis in spec/performance.
156
+
157
+ ### The Redis Backend
158
+
159
+ An indicative add performance is around 100,000 values stored in 20s: 5K/s write.
160
+
161
+ An indicative normal workflow performance is 200,000 values stored and retrieved in 1 minute: ~3K/s read-write
162
+
163
+ It's also reasonably memory efficient because it uses hashes instead of plain strings to store values. 200,000 values used 20MB (with each value 10 bytes).
164
+
165
+ ### The AMQP Backend
166
+
167
+ The amqp backend uses the excellent bunny gem to connect to RabbitMQ.
168
+
169
+ This is slightly slower than the Redis backend: 200,000 values read-write in around 1m30s (~2K/s read-write)
170
+
171
+ ### The Memory Backend
172
+
173
+ The memory backend only stores keys within the process space.
174
+
175
+ But performance is *very* good. It does 200,000 read/write in around 5s, which is ~40K/s read/write.
176
+
177
+ ### The ZeroMQ Backend
178
+
179
+ The zeromq backend is currently experimental. It's meant to do these things:
180
+
181
+ * Very fast queue adds (5s for 100,000 keys)
182
+ * Consistent reads
183
+ * Eventual consistency via a persistence server
184
+ * A listener based queue interface where a client can request a message rather than messages being pushed down the wire (i.e. 'subscribe' to a queue) (not implemented yet)
185
+
data/TODO.md ADDED
@@ -0,0 +1,65 @@
1
+
2
+ ## TODO
3
+ * zeromq-memory-queue:
4
+ * should store last ACK-ed at
5
+ * should expose a method to get at its stats
6
+ * read/s
7
+ * write/s
8
+ * transactions/s
9
+ * queue lengths
10
+ * memory used
11
+ * last ACK received time
12
+ * selectively tweak a queue (remove or edit items with a specified lkey from a queue)
13
+ * write tests that checks for contention cases, when we have a lot of queues, queue items and workers.
14
+ * flesh out the tests more for every edge case.
15
+ * write implementation tests and split tests into behavior and implementation.
16
+
17
+ ## CHANGES
18
+
19
+ ### 20120224 (vishnu@mobme.in)
20
+ * TrueQueue
21
+
22
+ ### 20111203 (vishnu@mobme.in)
23
+ * Renaming redis_queue to queue
24
+
25
+ ### 20111201 (vishnu@mobme.in)
26
+ * A persistence server for the zeromq queue based on redis
27
+ * The persistence server requests backlogs from the memory queue and
28
+ stores updates in Redis
29
+ * The persistence server sents an ACK for a backlog which says Redis
30
+ processing has been successful.
31
+ * The memory queue maintains a backlog of updates for which it hasn't received
32
+ an ACK.
33
+
34
+ ### 20111130 (vishnu@mobme.in)
35
+ * An experimental zeromq backend and queue server.
36
+
37
+ ### 20111128 (vishnu@mobme.in)
38
+ * Modularizing the code so that we can now use multiple backends.
39
+ * A memory based queue backend based on a C red-black tree implementation.
40
+ * BREAKING. Queues are now created using:
41
+ MobME::Infrastructure::RedisQueue.queue(backend) where backend is either one of :memory or :redis now.
42
+
43
+ ### 20111118 (vishnu@mobme.in)
44
+ * Store values inside small key-ed hashes for maximum memory efficiency.
45
+ * Major reorganization, making everything far more modular & simple.
46
+
47
+ ### 20111113 (vishnu@mobme.in)
48
+ * Initial documentation in Yardoc style.
49
+ * Adding #delete_queue(s) to clear & delete any and all queues permanently
50
+ * Adding #peek to peek at the first element in a queue without deleting it.
51
+ * \#remove is now multi thread and evented systems friendly.
52
+ * \#remove can now take a block that will auto add the item back into the queue when an error happens or a {MobME::Infrastructure::RedisQueueRemoveAbort} is raised.
53
+ * \#list to list every element in the queue. This is an expensive operation.
54
+ * Renaming #clear to #empty
55
+ * Renaming #delete\_queue(s) to #remove\_queue(s)
56
+ * More comprehensive documentation in README.md and TODO.md
57
+ * Using Yajl instead of native JSON
58
+ * Gemifying
59
+
60
+ ### 20111111 (vishnu@mobme.in)
61
+ * Organized the project into the formal MobME ruby tdd template
62
+ * Wrote initial specs for everything implemented.
63
+
64
+ ### Date Uncertain
65
+ * -- initial release --
@@ -0,0 +1,5 @@
1
+
2
+ require "mobme/infrastructure/queue"
3
+ require "mobme/infrastructure/queue/zeromq/server"
4
+
5
+ server = MobME::Infrastructure::Queue::ZeroMQ::Server.new()
@@ -0,0 +1,5 @@
1
+
2
+ require "mobme/infrastructure/queue"
3
+ require "mobme/infrastructure/queue/zeromq/persistence_server"
4
+
5
+ server = MobME::Infrastructure::Queue::ZeroMQ::PersistenceServer.new()
@@ -0,0 +1,37 @@
1
+ require 'yajl'
2
+
3
+ module MobME
4
+ module Infrastructure
5
+ module Queue
6
+ end
7
+ end
8
+ end
9
+
10
+ require_relative 'queue/backend'
11
+ require_relative 'queue/exceptions'
12
+
13
+ module MobME
14
+ module Infrastructure
15
+ module Queue
16
+ def self.queue(backend, options = {})
17
+ case backend
18
+ when :memory
19
+ require_relative 'queue/backends/memory'
20
+ MobME::Infrastructure::Queue::Backends::Memory.new(options)
21
+ when :redis
22
+ require_relative 'queue/backends/redis'
23
+ MobME::Infrastructure::Queue::Backends::Redis.new(options)
24
+ when :zeromq
25
+ require_relative 'queue/backends/zeromq'
26
+ MobME::Infrastructure::Queue::Backends::ZeroMQ.new(options)
27
+ when :amqp
28
+ require_relative 'queue/backends/amqp'
29
+ MobME::Infrastructure::Queue::Backends::AMQP.new(options)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+
37
+
@@ -0,0 +1,45 @@
1
+
2
+ module MobME::Infrastructure::Queue
3
+ module Backends
4
+ end
5
+ end
6
+
7
+ class MobME::Infrastructure::Queue::Backend
8
+ protected
9
+ def score_from_metadata(dequeue_timestamp, priority)
10
+ if dequeue_timestamp
11
+ (dequeue_timestamp.to_f * 1000000).to_i
12
+ else
13
+ ((Time.now.to_f * 1000000).to_i) / (priority || 1)
14
+ end
15
+ end
16
+
17
+ def normalize_metadata(metadata)
18
+ dequeue_timestamp = metadata['dequeue-timestamp']
19
+ if dequeue_timestamp
20
+ unless dequeue_timestamp.is_a? Time
21
+ metadata['dequeue-timestamp'] = Time.now
22
+ end
23
+ end
24
+
25
+ priority = metadata['priority']
26
+ if priority
27
+ priority = priority.to_i
28
+ unless priority.between?(1, 100)
29
+ metadata['priority'] = 1
30
+ end
31
+ end
32
+
33
+ metadata
34
+ end
35
+
36
+ def serialize_item(item, metadata)
37
+ Yajl.dump([item, metadata])
38
+ end
39
+
40
+ def unserialize_item(value)
41
+ json_value = value || Yajl.dump(value) #handle nil to null
42
+ Yajl.load(json_value)
43
+ end
44
+ end
45
+
@@ -0,0 +1,84 @@
1
+
2
+ require "bunny"
3
+
4
+ class MobME::Infrastructure::Queue::Backends::AMQP < MobME::Infrastructure::Queue::Backend
5
+ def initialize(options = {})
6
+ @bunny_options = options[:bunny_options] || {}
7
+ @amqp_queues = {}
8
+
9
+ configure
10
+ end
11
+
12
+ def add(queue, item, metadata = {})
13
+ metadata = normalize_metadata(metadata)
14
+
15
+ #register the queue if needed
16
+ queue_for(queue)
17
+
18
+ @amqp_exchange.publish(serialize_item(item, metadata), :key => queue)
19
+ end
20
+
21
+ # Adds many items together
22
+ def add_bulk(queue, items = [])
23
+ items.each do |item|
24
+ add(queue, item, {})
25
+ end
26
+ end
27
+
28
+ def remove(queue)
29
+ item = queue_for(queue).pop[:payload]
30
+ (:queue_empty == item) ? nil : unserialize_item(item)
31
+ end
32
+
33
+ def peek(queue)
34
+ raise NotImplementedError, "AMQP doesn't support peek!"
35
+ end
36
+
37
+ def size(queue)
38
+ queue_for(queue).message_count
39
+ end
40
+
41
+ def list(queue)
42
+ raise NotImplementedError, "AMQP doesn't support list!"
43
+ end
44
+
45
+ def empty(queue)
46
+ queue_for(queue).purge
47
+ end
48
+
49
+ def list_queues
50
+ @amqp_queues.keys
51
+ end
52
+
53
+ def remove_queues(*queues)
54
+ queues = list_queues if queues.empty?
55
+ queues.each do |queue|
56
+ queue_for(queue).delete
57
+ @amqp_queues.delete(queue)
58
+ end
59
+ end
60
+ alias :remove_queue :remove_queues
61
+
62
+ private
63
+ def queue_for(queue)
64
+ if @amqp_queues[queue]
65
+ @amqp_queues[queue]
66
+ else
67
+ @amqp_queues[queue] = @amqp_client.queue(queue)
68
+ end
69
+ end
70
+
71
+ def configure
72
+ exchange = @bunny_options.delete(:exchange) || ''
73
+ @amqp_client ||= Bunny.new(@bunny_options)
74
+
75
+ connect
76
+ @amqp_exchange ||= @amqp_client.exchange(exchange)
77
+ end
78
+
79
+ def connect
80
+ if :not_connected == @amqp_client.status
81
+ @amqp_client.start
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,95 @@
1
+
2
+ require "algorithms"
3
+
4
+ class MobME::Infrastructure::Queue::Backends::Memory < MobME::Infrastructure::Queue::Backend
5
+ attr_accessor :scores
6
+
7
+ # Initialises the Queue
8
+ # @param [Hash] options all options to pass to the queue
9
+ def initialize(options = {})
10
+ @@queues ||= {}
11
+ end
12
+
13
+ def queues
14
+ @@queues
15
+ end
16
+
17
+ def add(queue, item, metadata = {})
18
+ metadata = normalize_metadata(metadata)
19
+ score = score_from_metadata(metadata['dequeue-timestamp'], metadata['priority'])
20
+
21
+ queues[queue] ||= Containers::CRBTreeMap.new
22
+ queues[queue][score] = serialize_item(item, metadata)
23
+ end
24
+
25
+ # Adds many items together
26
+ def add_bulk(queue, items = [])
27
+ items.each do |item|
28
+ add(queue, item)
29
+ end
30
+ end
31
+
32
+ def remove(queue, &block)
33
+ score = queues[queue].min_key
34
+
35
+ item = item_with_score(queue, score)
36
+
37
+ #If a block is given
38
+ if block_given?
39
+ begin
40
+ block.call(item)
41
+ rescue MobME::Infrastructure::Queue::RemoveAbort
42
+ return
43
+ end
44
+ queues[queue].delete(score) if item
45
+ else
46
+ queues[queue].delete(score) if item
47
+ return item
48
+ end
49
+ end
50
+
51
+ def peek(queue)
52
+ score = queues[queue].min_key
53
+
54
+ item_with_score(queue, score)
55
+ end
56
+
57
+ def list(queue)
58
+ queues[queue].inject([]) { |collect, step| collect << item_with_score(queue, step[0]) }
59
+ end
60
+
61
+ def empty(queue)
62
+ queues[queue] = nil
63
+ queues[queue] = Containers::CRBTreeMap.new
64
+
65
+ true
66
+ end
67
+
68
+ def size(queue)
69
+ queues[queue].size
70
+ end
71
+
72
+ def remove_queues(*queues_to_delete)
73
+ queues_to_delete = list_queues if queues_to_delete.empty?
74
+ queues_to_delete.each do |queue|
75
+ queues.delete(queue)
76
+ end
77
+ end
78
+ alias :remove_queue :remove_queues
79
+
80
+ def list_queues
81
+ queues.keys
82
+ end
83
+
84
+ private
85
+ def item_with_score(queue, score)
86
+ item = if not score
87
+ nil
88
+ elsif score > (Time.now.to_f * 1000000).to_i # We don't return future items!
89
+ nil
90
+ else
91
+ value = queues[queue][score]
92
+ unserialize_item(value)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,255 @@
1
+ require 'redis'
2
+
3
+ class MobME::Infrastructure::Queue::Backends::Redis < MobME::Infrastructure::Queue::Backend
4
+
5
+ # The namespace that all redis queue keys live inside Redis
6
+ NAMESPACE = 'redis:queue:'
7
+
8
+ # The set used to store all keys
9
+ QUEUESET = 'redis:queue:set'
10
+
11
+ # The UUID suffix for keys that store values
12
+ UUID_SUFFIX = ':uuid'
13
+
14
+ # The sorted set suffix for the list of all keys in a queue
15
+ QUEUE_SUFFIX = ':queue'
16
+
17
+ # The hash suffix for the hash that stores values of a queue
18
+ VALUE_SUFFIX = ':values'
19
+
20
+ # Initialises the Queue
21
+ # @param [Hash] options all options to pass to the queue
22
+ # @option options [Hash] :redis_options is passed on to the underlying Redis client
23
+ def initialize(options = {})
24
+ redis_options = options.delete(:redis_options) || {}
25
+
26
+ # Connect to Redis!
27
+ connect(redis_options)
28
+ end
29
+
30
+ # Connect to Redis
31
+ # @param [Hash] options to pass to the Redis client as is
32
+ # @option options :connection Instead of making a new connection, the queue will reuse this existing Redis connection
33
+ def connect(options)
34
+ @redis = options.delete(:connection)
35
+ @redis ||= Redis.new(options)
36
+ end
37
+
38
+ # Add a value to a queue
39
+ # @param [String] queue_name The queue name to add to.
40
+ # @param [Object] item is the item to add
41
+ # @param [Hash] metadata is stored with the item and returned.
42
+ # @option metadata [Time] dequeue-timestamp An item with a dequeue-timestamp is only dequeued after this timestamp.
43
+ # @option metadata [Integer] priority An item with a higher priority is dequeued first. Always between 1 and 100.
44
+ # @return [String] A unique key in the queue name where the item is set.
45
+ def add(queue, item, metadata = {})
46
+ raise ArgumentError, "Metadata must be a hash, but #{metadata.class} given" unless metadata.is_a? Hash
47
+
48
+ metadata = normalize_metadata(metadata)
49
+ uuid = generate_uuid(queue)
50
+
51
+ add_to_queueset(queue)
52
+ write_value(queue, uuid, item, metadata)
53
+ add_to_queue(queue, uuid, metadata['dequeue-timestamp'], metadata['priority'])
54
+
55
+ uuid
56
+ end
57
+
58
+ # Add values to the queue in bulk
59
+ # This works by pipelining writes to Redis, so results are generally much faster
60
+ # @param [String] queue The queue name to add to
61
+ # @param [Array] items The items to add
62
+ def add_bulk(queue, items = [])
63
+ metadata = {}
64
+
65
+ # UUIDs have to be in sync!
66
+ uuids = []
67
+ items.each do |item|
68
+ uuids << generate_uuid(queue)
69
+ end
70
+
71
+ add_to_queueset(queue)
72
+
73
+ @redis.pipelined do
74
+ items.each do |item|
75
+ uuid = uuids.shift
76
+
77
+ # write value
78
+ value_hash = "#{NAMESPACE}#{queue}#{VALUE_SUFFIX}"
79
+ @redis.hset value_hash, uuid, serialize_item(item, metadata)
80
+
81
+ # add to queue
82
+ queue_key = NAMESPACE + queue.to_s + QUEUE_SUFFIX
83
+ @redis.zadd queue_key, score_from_metadata(metadata['dequeue_timestamp'], metadata['priority']), uuid
84
+ end
85
+ end
86
+ end
87
+
88
+ # Remove an item from a queue.
89
+ # When a block is passed, the item is reserved instead and automatically put back in case of an error.
90
+ # Raise MobME::Infrastructure::QueueRemoveAbort within the block to manually abort the remove.
91
+ #
92
+ # @param [String] queue_name is the queue name
93
+ # @yield [[Object, Hash]] An optional block that is passed the item being remove alongside metadata.
94
+ # @return [[Object, Hash]] The item plus the metadata in the queue
95
+ def remove(queue, &block)
96
+ begin
97
+ # Remove the first item!
98
+ uuid = first_in_queue(queue)
99
+ if uuid
100
+ # If we're not able to remove the key from the set here, it means that
101
+ # some other thread (or evented operation) has done it before us, so
102
+ # the current remove is invalid and we should retry!
103
+ raise MobME::Infrastructure::Queue::RemoveConflictException unless remove_from_queue(queue, uuid)
104
+
105
+ queue_item = read_value(queue, uuid)
106
+
107
+ # When a block is given, safely reserve the queue item
108
+ if block_given?
109
+ begin
110
+ block.call(queue_item)
111
+ remove_value(queue, uuid)
112
+ rescue #generic error
113
+ put_back_in_queue(queue, uuid, queue_item)
114
+
115
+ # And now re-raise the error
116
+ raise
117
+ rescue MobME::Infrastructure::Queue::RemoveAbort
118
+ put_back_in_queue(queue, uuid, queue_item)
119
+ end
120
+ else
121
+ remove_value(queue, uuid)
122
+ queue_item
123
+ end
124
+ else
125
+ nil
126
+ end
127
+ rescue MobME::Infrastructure::Queue::RemoveConflictException
128
+ retry
129
+ end
130
+ end
131
+
132
+ # Peek into the first item in a queue without removing it
133
+ # @param [String] queue_name is the queue name
134
+ # @return [[Object, Hash]] The item plus the metadata in the queue
135
+ def peek(queue)
136
+ uuid = first_in_queue(queue)
137
+ read_value(queue, uuid)
138
+ end
139
+
140
+ # Find the size of a queue
141
+ # @param [String] queue_name is the queue name
142
+ # @return [Integer] The size of the queue
143
+ def size(queue)
144
+ queue = NAMESPACE + queue.to_s + QUEUE_SUFFIX
145
+ length = (@redis.zcard queue)
146
+ end
147
+
148
+ # Lists all items in the queue. This is an expensive operation
149
+ # @param [String] queue_name is the queue name
150
+ # @return [Array<Object, Hash>] An array of list items, the first element the object stored and the second, metadata
151
+ def list(queue)
152
+ batch_size = 1_000 # keep this low as the time complexity of zrangebyscore is O(log(N)+M) : M -> the size
153
+
154
+ count = 0; values = []
155
+ (size(queue)/batch_size + 1).times do |i|
156
+ limit = [(batch_size * i), batch_size]
157
+ uuids = range_in_queue(queue, limit)
158
+ batch_values = uuids.map { |uuid| read_value(queue, uuid) }
159
+ values.push(*batch_values)
160
+ end
161
+
162
+ values
163
+ end
164
+
165
+ # Clear the queue
166
+ # @param [String] queue_name is the queue name to clear
167
+ def empty(queue)
168
+ # Delete key and value stores.
169
+ @redis.del "#{NAMESPACE}#{queue}#{VALUE_SUFFIX}"
170
+ @redis.del "#{NAMESPACE}#{queue}#{QUEUE_SUFFIX}"
171
+ end
172
+
173
+ # List all queues
174
+ # @return [Array] A list of queues (includes empty queues that were once available)
175
+ def list_queues
176
+ @redis.smembers QUEUESET
177
+ end
178
+
179
+ # Delete queues
180
+ # @param [optional String ...] queues A list of queues to delete. If empty, all queues are deleted.
181
+ def remove_queues(*queues)
182
+ queues = list_queues if queues.empty?
183
+ queues.each do |queue_name|
184
+ empty(queue_name)
185
+ remove_from_queueset(queue_name)
186
+ end
187
+ end
188
+ alias :remove_queue :remove_queues
189
+
190
+ private
191
+ def add_to_queueset(queue)
192
+ @redis.sadd QUEUESET, queue
193
+ end
194
+
195
+ def remove_from_queueset(queue)
196
+ @redis.srem QUEUESET, queue
197
+ end
198
+
199
+ def first_in_queue(queue)
200
+ queue = NAMESPACE + queue.to_s + QUEUE_SUFFIX
201
+ (@redis.zrangebyscore queue, "-inf", (Time.now.to_f * 1000000).to_i, {:limit => [0, 1]}).first
202
+ end
203
+
204
+ def range_in_queue(queue, limit)
205
+ queue = NAMESPACE + queue.to_s + QUEUE_SUFFIX
206
+ (@redis.zrangebyscore queue, "-inf", (Time.now.to_f * 1000000).to_i, {:limit => limit})
207
+ end
208
+
209
+ def add_to_queue(queue, uuid, dequeue_timestamp, priority)
210
+ # zadd adds to a sorted set, which is sorted by score.
211
+ # When set, the dequeue_timestamp is used as the score. If not, it's just the current timestamp.
212
+ # When set, current timestamp is divided by the integer priority.
213
+ queue = NAMESPACE + queue.to_s + QUEUE_SUFFIX
214
+ @redis.zadd queue, score_from_metadata(dequeue_timestamp, priority), uuid
215
+ end
216
+
217
+ def remove_from_queue(queue, uuid)
218
+ queue = NAMESPACE + queue.to_s + QUEUE_SUFFIX
219
+ (@redis.zrem queue, uuid)
220
+ end
221
+
222
+ def put_back_in_queue(queue, uuid, queue_item)
223
+ # Put the item back in the queue
224
+ metadata = queue_item[1]
225
+ metadata = normalize_metadata(metadata)
226
+ add_to_queue(queue, uuid, metadata['dequeue_timestamp'], metadata['priority'])
227
+ end
228
+
229
+ def write_value(queue, uuid, item, metadata)
230
+ value_hash = "#{NAMESPACE}#{queue}#{VALUE_SUFFIX}"
231
+
232
+ @redis.hset value_hash, uuid, serialize_item(item, metadata)
233
+ end
234
+
235
+ def read_value(queue, uuid)
236
+ if uuid
237
+ value_hash = "#{NAMESPACE}#{queue}#{VALUE_SUFFIX}"
238
+
239
+ value = @redis.hget value_hash, uuid
240
+ unserialize_item(value)
241
+ else
242
+ nil
243
+ end
244
+ end
245
+
246
+ def remove_value(queue, uuid)
247
+ value_hash = "#{NAMESPACE}#{queue}#{VALUE_SUFFIX}"
248
+
249
+ @redis.hdel value_hash, uuid
250
+ end
251
+
252
+ def generate_uuid(queue)
253
+ @redis.incr NAMESPACE + queue.to_s + UUID_SUFFIX
254
+ end
255
+ end
@@ -0,0 +1,70 @@
1
+
2
+ require 'em-zeromq'
3
+ require 'em-synchrony'
4
+ require 'mobme/infrastructure/queue/zeromq/connection_handler'
5
+
6
+ class MobME::Infrastructure::Queue::Backends::ZeroMQ < MobME::Infrastructure::Queue::Backend
7
+ def initialize(options = {})
8
+ @socket = options[:socket] || "ipc:///tmp/mobme-infrastructure-queue-messages.sock"
9
+ connect
10
+ end
11
+
12
+ def connect
13
+ @context = EM::ZeroMQ::Context.new(1)
14
+ @pool = EM::Synchrony::ConnectionPool.new(:size => 20) do
15
+ @context.connect(ZMQ::REQ, @socket)
16
+ end
17
+ end
18
+
19
+ def add(queue, item, metadata = {})
20
+ dispatch(:add, queue, item, metadata)
21
+ end
22
+
23
+ # Adds many items together
24
+ def add_bulk(queue, items = [])
25
+ dispatch(:add_bulk, queue, items)
26
+ end
27
+
28
+ # Simple remove without reserving items
29
+ def remove(queue)
30
+ dispatch(:remove, queue)
31
+ end
32
+
33
+ def peek(queue)
34
+ dispatch(:peek, queue)
35
+ end
36
+
37
+ def size(queue)
38
+ dispatch(:size, queue)
39
+ end
40
+
41
+ def list(queue)
42
+ dispatch(:list, queue)
43
+ end
44
+
45
+ def empty(queue)
46
+ dispatch(:empty, queue)
47
+ end
48
+
49
+ def list_queues
50
+ dispatch(:list_queues)
51
+ end
52
+
53
+ def remove_queues(*queues)
54
+ dispatch(:remove_queues, *queues)
55
+ end
56
+ alias :remove_queue :remove_queues
57
+
58
+ private
59
+ def dispatch(method, *args)
60
+ @pool.execute(false) do |connection|
61
+ handler = MobME::Infrastructure::Queue::ZeroMQ::ConnectionHandler.new(connection)
62
+
63
+ message = Marshal.dump([method, args])
64
+
65
+ handler.send_message(message)
66
+
67
+ Marshal.load(handler.receive_message)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,6 @@
1
+
2
+ class MobME::Infrastructure::Queue::RemoveConflictException < Exception; end
3
+
4
+ # Raise this to abort a remove and put back the item
5
+ class MobME::Infrastructure::Queue::RemoveAbort < Exception; end
6
+
@@ -0,0 +1,50 @@
1
+
2
+ module MobME::Infrastructure::Queue::ZeroMQ
3
+ class ConnectionHandler
4
+ attr_accessor :request
5
+
6
+ def initialize(connection, identity = "")
7
+ @connection = connection
8
+ @connection.handler = self
9
+ @connection.identity = "#{identity ? "#{identity}:" : ""}#{@client_fiber.object_id}"
10
+ @connection.notify_readable = false
11
+ @connection.notify_writable = false
12
+
13
+ @send_messages_buffer = nil
14
+ end
15
+
16
+ def receive_message
17
+ EM.next_tick { @connection.register_readable }
18
+
19
+ @client_fiber = Fiber.current
20
+ Fiber.yield
21
+ end
22
+
23
+ def send_message(*messages)
24
+ @send_messages_buffer = messages
25
+ EM.next_tick { @connection.register_writable }
26
+
27
+ @client_fiber = Fiber.current
28
+ Fiber.yield
29
+ end
30
+
31
+ def on_readable(connection, message)
32
+ request = message.map(&:copy_out_string).join
33
+ message.each { |part| part.close }
34
+
35
+ @connection.notify_readable = false
36
+ @connection.notify_writable = false
37
+
38
+ @client_fiber.resume(request)
39
+ end
40
+
41
+ def on_writable(connection)
42
+ return_value = connection.send_msg *@send_messages_buffer
43
+
44
+ @connection.notify_readable = false
45
+ @connection.notify_writable = false
46
+
47
+ @client_fiber.resume(return_value)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,80 @@
1
+
2
+ require 'ffi-rzmq'
3
+ require 'mobme/infrastructure/queue'
4
+ require 'mobme/infrastructure/queue/zeromq/connection_handler'
5
+ require 'redis/connection/synchrony'
6
+ require 'digest/sha1'
7
+ require 'fileutils'
8
+
9
+ module MobME::Infrastructure::Queue::ZeroMQ
10
+ class PersistenceServer
11
+ def initialize(options = {})
12
+ @queue = MobME::Infrastructure::Queue.queue(:redis)
13
+ @persistence_socket = options[:persistence_socket] || "ipc:///tmp/mobme-infrastructure-queue-persistence.sock"
14
+ @persistence_store_path = options[:persistence_store_path] || "/tmp"
15
+ @backlog_interval = options[:backlog_interval] || 10
16
+
17
+ EM.synchrony do
18
+ create_snapshot_directory
19
+ bind
20
+
21
+ send_backlog_requests
22
+ end
23
+ end
24
+
25
+ def bind
26
+ @context = EM::ZeroMQ::Context.new(1)
27
+
28
+ @persistence_request_server = @context.connect(ZMQ::REQ, @persistence_socket)
29
+ end
30
+
31
+ def send_backlog_requests
32
+ loop do
33
+ handler = MobME::Infrastructure::Queue::ZeroMQ::ConnectionHandler.new(@persistence_request_server)
34
+ handler.send_message Marshal.dump("BACKLOG")
35
+ puts "Sent BACKLOG"
36
+
37
+ snapshot = handler.receive_message
38
+ snapshot = Marshal.load(snapshot) rescue nil
39
+
40
+ case snapshot
41
+ when nil
42
+ when false
43
+ else
44
+ dump_snapshot_to_disk(snapshot)
45
+
46
+ if snapshot and !snapshot.empty?
47
+ handler.send_message Marshal.dump("ACK #{ack_signature(snapshot)}")
48
+
49
+ # We get an OK back from the server
50
+ handler.receive_message
51
+ end
52
+ end
53
+
54
+ EM::Synchrony.sleep(@backlog_interval)
55
+ end
56
+ end
57
+
58
+ private
59
+ def create_snapshot_directory
60
+ @persistence_store_path = Pathname.new(@persistence_store_path).join("db")
61
+ FileUtils.mkdir_p(@persistence_store_path)
62
+ end
63
+
64
+ def dump_snapshot_to_disk(snapshot)
65
+ snapshot.each do |queue, items|
66
+ puts "Snapshotting: #{queue}"
67
+ marshalled_items = StringIO.new(Marshal.dump(items))
68
+ File.open(@persistence_store_path.join("#{queue}.marshal"), "w+") do |file|
69
+ file.write marshalled_items.read
70
+ end
71
+ puts "Done."
72
+ end
73
+ end
74
+
75
+ def ack_signature(snapshot)
76
+ marshaled_snapshot = Marshal.dump(snapshot)
77
+ Digest::SHA1.hexdigest(marshaled_snapshot)
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,107 @@
1
+
2
+ require 'ffi-rzmq'
3
+ require 'digest/sha1'
4
+
5
+ require 'mobme/infrastructure/queue'
6
+ require 'mobme/infrastructure/queue/zeromq/connection_handler'
7
+
8
+ require 'em-synchrony'
9
+ require 'em-zeromq'
10
+
11
+ module MobME::Infrastructure::Queue::ZeroMQ
12
+ class Server
13
+ def initialize(options = {})
14
+ @queue = MobME::Infrastructure::Queue.queue(:memory)
15
+ @messages_socket = options[:messages_socket] || "ipc:///tmp/mobme-infrastructure-queue-messages.sock"
16
+ @persistence_socket = options[:persistence_socket] || "ipc:///tmp/mobme-infrastructure-queue-persistence.sock"
17
+
18
+ EM.synchrony do
19
+ bind
20
+
21
+ Fiber.new { listen_to_messages }.resume
22
+ Fiber.new { listen_to_backlog_requests }.resume
23
+ end
24
+ end
25
+
26
+ def bind
27
+ @context = EM::ZeroMQ::Context.new(1)
28
+
29
+ @messages_reply_server = @context.bind(ZMQ::REP, @messages_socket)
30
+ @persistence_reply_server = @context.bind(ZMQ::REP, @persistence_socket)
31
+ end
32
+
33
+ def listen_to_messages
34
+ loop do
35
+ handler = MobME::Infrastructure::Queue::ZeroMQ::ConnectionHandler.new(@messages_reply_server)
36
+ message = @messages_reply_server.handler.receive_message
37
+
38
+ message = Marshal.load(message) rescue nil
39
+
40
+ queue_return = if message
41
+ route_to_queue(message)
42
+ end
43
+
44
+ @messages_reply_server.handler.send_message(Marshal.dump(queue_return))
45
+ end
46
+ end
47
+
48
+ def listen_to_backlog_requests
49
+ loop do
50
+ handler = MobME::Infrastructure::Queue::ZeroMQ::ConnectionHandler.new(@persistence_reply_server)
51
+ message = @persistence_reply_server.handler.receive_message
52
+
53
+
54
+ message = Marshal.load(message) rescue nil
55
+
56
+ queue_return = if message == "BACKLOG"
57
+ queues_snapshot
58
+ elsif ack_message?(message)
59
+ puts "Got ACK: #{signature_from_ack_message(message)}"
60
+ true
61
+ else
62
+ false
63
+ end
64
+
65
+ @persistence_reply_server.handler.send_message(Marshal.dump(queue_return))
66
+ end
67
+ end
68
+
69
+ private
70
+ def route_to_queue(message)
71
+ method = method_from_message(message)
72
+ args = arguments_from_message(message)
73
+
74
+ begin
75
+ @queue.send(method, *args)
76
+ rescue NoMethodError
77
+ false
78
+ end
79
+ end
80
+
81
+ def queues_snapshot
82
+ snapshot = {}
83
+ queues = @queue.list_queues
84
+ queues.each do |queue|
85
+ snapshot[queue] = @queue.list queue
86
+ end
87
+
88
+ snapshot
89
+ end
90
+
91
+ def method_from_message(message)
92
+ message[0]
93
+ end
94
+
95
+ def arguments_from_message(message)
96
+ message[1]
97
+ end
98
+
99
+ def ack_message?(message)
100
+ message.match(/^ACK (.*)/)
101
+ end
102
+
103
+ def signature_from_ack_message(message)
104
+ message.match(/^ACK (.*)/).to_a[1]
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,3 @@
1
+ require_relative 'mobme/infrastructure/queue'
2
+
3
+ TrueQueue = MobME::Infrastructure::Queue
metadata ADDED
@@ -0,0 +1,249 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: true_queue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.5
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Vishnu Gopal
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-02-24 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70255280344720 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70255280344720
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70255280344240 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70255280344240
36
+ - !ruby/object:Gem::Dependency
37
+ name: guard
38
+ requirement: &70255280343780 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70255280343780
47
+ - !ruby/object:Gem::Dependency
48
+ name: guard-rspec
49
+ requirement: &70255280343280 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70255280343280
58
+ - !ruby/object:Gem::Dependency
59
+ name: simplecov
60
+ requirement: &70255280342820 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70255280342820
69
+ - !ruby/object:Gem::Dependency
70
+ name: flog
71
+ requirement: &70255280342340 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70255280342340
80
+ - !ruby/object:Gem::Dependency
81
+ name: yard
82
+ requirement: &70255280341900 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *70255280341900
91
+ - !ruby/object:Gem::Dependency
92
+ name: simplecov-rcov
93
+ requirement: &70255280341440 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70255280341440
102
+ - !ruby/object:Gem::Dependency
103
+ name: diff-lcs
104
+ requirement: &70255280340980 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *70255280340980
113
+ - !ruby/object:Gem::Dependency
114
+ name: rdiscount
115
+ requirement: &70255280340520 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - ! '>='
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: *70255280340520
124
+ - !ruby/object:Gem::Dependency
125
+ name: hiredis
126
+ requirement: &70255280340000 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: 0.3.1
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: *70255280340000
135
+ - !ruby/object:Gem::Dependency
136
+ name: redis
137
+ requirement: &70255280339440 !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ~>
141
+ - !ruby/object:Gem::Version
142
+ version: 2.2.0
143
+ type: :development
144
+ prerelease: false
145
+ version_requirements: *70255280339440
146
+ - !ruby/object:Gem::Dependency
147
+ name: algorithms
148
+ requirement: &70255280339040 !ruby/object:Gem::Requirement
149
+ none: false
150
+ requirements:
151
+ - - ! '>='
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ type: :development
155
+ prerelease: false
156
+ version_requirements: *70255280339040
157
+ - !ruby/object:Gem::Dependency
158
+ name: em-synchrony
159
+ requirement: &70255280338540 !ruby/object:Gem::Requirement
160
+ none: false
161
+ requirements:
162
+ - - ! '>='
163
+ - !ruby/object:Gem::Version
164
+ version: '0'
165
+ type: :development
166
+ prerelease: false
167
+ version_requirements: *70255280338540
168
+ - !ruby/object:Gem::Dependency
169
+ name: em-zeromq
170
+ requirement: &70255280337980 !ruby/object:Gem::Requirement
171
+ none: false
172
+ requirements:
173
+ - - ! '>='
174
+ - !ruby/object:Gem::Version
175
+ version: '0'
176
+ type: :development
177
+ prerelease: false
178
+ version_requirements: *70255280337980
179
+ - !ruby/object:Gem::Dependency
180
+ name: bunny
181
+ requirement: &70255280337200 !ruby/object:Gem::Requirement
182
+ none: false
183
+ requirements:
184
+ - - ~>
185
+ - !ruby/object:Gem::Version
186
+ version: 0.7.4
187
+ type: :development
188
+ prerelease: false
189
+ version_requirements: *70255280337200
190
+ - !ruby/object:Gem::Dependency
191
+ name: yajl-ruby
192
+ requirement: &70255280336680 !ruby/object:Gem::Requirement
193
+ none: false
194
+ requirements:
195
+ - - ! '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ type: :runtime
199
+ prerelease: false
200
+ version_requirements: *70255280336680
201
+ description: ! 'Queue is a proxy to several queueing libraries: a homegrown queue
202
+ on top of Redis, an in-process memory queue for use in testing, a robust AMQP backend
203
+ based on Bunny, and an experimental zeromq backend'
204
+ email:
205
+ - vishnu@mobme.in
206
+ executables: []
207
+ extensions: []
208
+ extra_rdoc_files: []
209
+ files:
210
+ - lib/mobme/infrastructure/queue/backend.rb
211
+ - lib/mobme/infrastructure/queue/backends/amqp.rb
212
+ - lib/mobme/infrastructure/queue/backends/memory.rb
213
+ - lib/mobme/infrastructure/queue/backends/redis.rb
214
+ - lib/mobme/infrastructure/queue/backends/zeromq.rb
215
+ - lib/mobme/infrastructure/queue/exceptions.rb
216
+ - lib/mobme/infrastructure/queue/zeromq/connection_handler.rb
217
+ - lib/mobme/infrastructure/queue/zeromq/persistence_server.rb
218
+ - lib/mobme/infrastructure/queue/zeromq/server.rb
219
+ - lib/mobme/infrastructure/queue.rb
220
+ - lib/true_queue.rb
221
+ - bin/zeromq-memory-queue.rb
222
+ - bin/zeromq-persistence-server.rb
223
+ - README.md
224
+ - TODO.md
225
+ homepage: http://42.mobme.in/
226
+ licenses: []
227
+ post_install_message:
228
+ rdoc_options: []
229
+ require_paths:
230
+ - lib
231
+ required_ruby_version: !ruby/object:Gem::Requirement
232
+ none: false
233
+ requirements:
234
+ - - ! '>='
235
+ - !ruby/object:Gem::Version
236
+ version: '0'
237
+ required_rubygems_version: !ruby/object:Gem::Requirement
238
+ none: false
239
+ requirements:
240
+ - - ! '>='
241
+ - !ruby/object:Gem::Version
242
+ version: 1.3.6
243
+ requirements: []
244
+ rubyforge_project:
245
+ rubygems_version: 1.8.10
246
+ signing_key:
247
+ specification_version: 3
248
+ summary: A simple proxy to various queue backends.
249
+ test_files: []