true_queue 0.9.5

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