qup 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +41 -0
- data/.gemtest +1 -0
- data/ADAPTER_API.rdoc +97 -0
- data/HISTORY.rdoc +9 -0
- data/Manifest.txt +52 -0
- data/README.rdoc +156 -0
- data/Rakefile +246 -0
- data/lib/qup.rb +48 -0
- data/lib/qup/adapter.rb +57 -0
- data/lib/qup/adapter/kestrel.rb +56 -0
- data/lib/qup/adapter/kestrel/destination.rb +54 -0
- data/lib/qup/adapter/kestrel/queue.rb +101 -0
- data/lib/qup/adapter/kestrel/topic.rb +68 -0
- data/lib/qup/adapter/maildir.rb +57 -0
- data/lib/qup/adapter/maildir/queue.rb +123 -0
- data/lib/qup/adapter/maildir/topic.rb +85 -0
- data/lib/qup/adapter/redis.rb +55 -0
- data/lib/qup/adapter/redis/connection.rb +32 -0
- data/lib/qup/adapter/redis/queue.rb +97 -0
- data/lib/qup/adapter/redis/topic.rb +76 -0
- data/lib/qup/consumer.rb +42 -0
- data/lib/qup/message.rb +18 -0
- data/lib/qup/producer.rb +28 -0
- data/lib/qup/publisher.rb +30 -0
- data/lib/qup/queue_api.rb +124 -0
- data/lib/qup/session.rb +111 -0
- data/lib/qup/subscriber.rb +28 -0
- data/lib/qup/topic_api.rb +92 -0
- data/spec/qup/adapter/kestrel/queue_spec.rb +9 -0
- data/spec/qup/adapter/kestrel/topic_spec.rb +9 -0
- data/spec/qup/adapter/kestrel_context.rb +8 -0
- data/spec/qup/adapter/kestrel_spec.rb +8 -0
- data/spec/qup/adapter/maildir/queue_spec.rb +9 -0
- data/spec/qup/adapter/maildir/topic_spec.rb +9 -0
- data/spec/qup/adapter/maildir_context.rb +10 -0
- data/spec/qup/adapter/maildir_spec.rb +8 -0
- data/spec/qup/adapter/redis/queue_spec.rb +9 -0
- data/spec/qup/adapter/redis/topic_spec.rb +9 -0
- data/spec/qup/adapter/redis_context.rb +6 -0
- data/spec/qup/adapter/redis_spec.rb +8 -0
- data/spec/qup/adapter_spec.rb +28 -0
- data/spec/qup/consumer_spec.rb +40 -0
- data/spec/qup/message_spec.rb +13 -0
- data/spec/qup/producer_spec.rb +18 -0
- data/spec/qup/queue_api_spec.rb +21 -0
- data/spec/qup/session_spec.rb +81 -0
- data/spec/qup/shared_adapter_examples.rb +29 -0
- data/spec/qup/shared_queue_examples.rb +71 -0
- data/spec/qup/shared_topic_examples.rb +57 -0
- data/spec/qup/topic_api_spec.rb +21 -0
- data/spec/qup_spec.rb +37 -0
- data/spec/spec_helper.rb +26 -0
- metadata +281 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'qup/producer'
|
2
|
+
require 'qup/consumer'
|
3
|
+
|
4
|
+
class Qup::Adapter::Maildir
|
5
|
+
#
|
6
|
+
# Internal: The Qup Implementation in the Maildir Adapter
|
7
|
+
#
|
8
|
+
class Queue
|
9
|
+
include Qup::QueueAPI
|
10
|
+
# Internal: the name of the Queue
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Internal: Create a new Queue
|
14
|
+
#
|
15
|
+
# root_path - the root_path for this Queue to create under
|
16
|
+
# name - the String name of the Queue
|
17
|
+
#
|
18
|
+
# Returns a new Queue.
|
19
|
+
def initialize( root_path, name )
|
20
|
+
@root_path = ::Pathname.new( root_path )
|
21
|
+
@name = name
|
22
|
+
@queue_path = @root_path + @name
|
23
|
+
@maildir = ::Maildir.new( @queue_path, true )
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
# Internal: Remove the Queue from the system
|
28
|
+
#
|
29
|
+
# Returns nothing.
|
30
|
+
def destroy
|
31
|
+
@queue_path.rmtree
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Internal: Remove all messages from the Queue
|
36
|
+
#
|
37
|
+
# Returns nothing.
|
38
|
+
def flush
|
39
|
+
::Maildir::SUBDIRS.each do |sub|
|
40
|
+
dir = Pathname.new( File.join( @maildir.path, sub.to_s ))
|
41
|
+
dir.children.each do |p|
|
42
|
+
p.delete if p.file?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
# Internal: return the number of Messages on the Queue
|
49
|
+
#
|
50
|
+
# Returns an integer of the Queue depth
|
51
|
+
def depth
|
52
|
+
total = 0
|
53
|
+
%w[ new cur ].each do |subdir|
|
54
|
+
search_path = File.join( @maildir.path, subdir, '*' )
|
55
|
+
keys = Dir.glob( search_path )
|
56
|
+
total += keys.size
|
57
|
+
end
|
58
|
+
return total
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Internal: Put an item onto the Queue
|
63
|
+
#
|
64
|
+
# message - the data to put onto the queue.
|
65
|
+
#
|
66
|
+
# The 'message' that is passed in is wrapped in a Qup::Message before being
|
67
|
+
# stored.
|
68
|
+
#
|
69
|
+
# A user of the Qup API should use a Producer instance to put items onto the
|
70
|
+
# queue.
|
71
|
+
#
|
72
|
+
# Returns the Message that was put onto the Queue
|
73
|
+
def produce( message )
|
74
|
+
msg = @maildir.add( message )
|
75
|
+
return ::Qup::Message.new( msg.key, msg.data )
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Internal: Retrieve a Message from the Queue
|
80
|
+
#
|
81
|
+
# Yields a Message
|
82
|
+
#
|
83
|
+
# A user of the Qup API should use a Consumer instance to retrieve items
|
84
|
+
# from the Queue.
|
85
|
+
#
|
86
|
+
# Returns a Message
|
87
|
+
def consume(&block)
|
88
|
+
msg = @maildir.list(:new, :limit => 1).first
|
89
|
+
return nil if msg.nil?
|
90
|
+
msg.process
|
91
|
+
msg.seen!
|
92
|
+
q_message = ::Qup::Message.new( msg.key, msg.data )
|
93
|
+
if block_given? then
|
94
|
+
yield_message( q_message, &block )
|
95
|
+
else
|
96
|
+
return q_message
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
# Internal: Acknowledge that message is completed and remove it from the
|
102
|
+
# Queue.
|
103
|
+
#
|
104
|
+
# Returns nothing
|
105
|
+
def acknowledge( message )
|
106
|
+
md_message = @maildir.get( message.key )
|
107
|
+
msg = "Message #{message.key} has not been processed yet"
|
108
|
+
raise ::Qup::Error, msg unless md_message.dir == :cur
|
109
|
+
raise ::Qup::Error, msg unless md_message.seen?
|
110
|
+
md_message.destroy
|
111
|
+
end
|
112
|
+
|
113
|
+
#######
|
114
|
+
private
|
115
|
+
#######
|
116
|
+
|
117
|
+
def yield_message( message, &block )
|
118
|
+
yield message
|
119
|
+
ensure
|
120
|
+
acknowledge( message )
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Qup::Adapter::Maildir
|
2
|
+
#
|
3
|
+
# Internal: A Topic for use in a publish-subscribe Messaging
|
4
|
+
#
|
5
|
+
# The topic delivers each Message that it is give to each and every Subscriber
|
6
|
+
#
|
7
|
+
class Topic
|
8
|
+
include Qup::TopicAPI
|
9
|
+
|
10
|
+
# Internal: the name of the Topic
|
11
|
+
attr_reader :name
|
12
|
+
|
13
|
+
# Internal: Create a new Topic
|
14
|
+
#
|
15
|
+
# root_path - the Session this Topic is attached to
|
16
|
+
# name - the String name of the Topic
|
17
|
+
#
|
18
|
+
# Returns a new Topic.
|
19
|
+
def initialize( root_path, name )
|
20
|
+
@root_path = ::Pathname.new( root_path )
|
21
|
+
@name = name
|
22
|
+
@topic_path = @root_path + @name
|
23
|
+
@subscribers = Hash.new
|
24
|
+
|
25
|
+
FileUtils.mkdir_p( @topic_path )
|
26
|
+
end
|
27
|
+
|
28
|
+
# Internal: Destroy the Topic
|
29
|
+
#
|
30
|
+
# If possible remove the existence of the Topic from the System
|
31
|
+
#
|
32
|
+
# Returns nothing.
|
33
|
+
def destroy
|
34
|
+
@topic_path.rmtree
|
35
|
+
end
|
36
|
+
|
37
|
+
# Internal: Creates a Publisher for the Topic
|
38
|
+
#
|
39
|
+
# Returns a new Publisher
|
40
|
+
def publisher
|
41
|
+
::Qup::Publisher.new( self )
|
42
|
+
end
|
43
|
+
|
44
|
+
# Internal: Create a subscriber for the Topic
|
45
|
+
#
|
46
|
+
# name - the String name of the subscriber
|
47
|
+
#
|
48
|
+
# Creating a subscriber creates a new Subscriber that will receive a copy of
|
49
|
+
# every message that is published to the Topic.
|
50
|
+
#
|
51
|
+
# Subscribers are unique by name, two subscribers with the same name will
|
52
|
+
# act as individual Consumers on a queue of their name.
|
53
|
+
#
|
54
|
+
# Returns a Subscriber
|
55
|
+
def subscriber( name )
|
56
|
+
::Qup::Subscriber.new( self, sub_queue( name ) )
|
57
|
+
end
|
58
|
+
|
59
|
+
# Internal: Return the number of Subscribers to this Topic
|
60
|
+
#
|
61
|
+
# Returns integer
|
62
|
+
def subscriber_count
|
63
|
+
@subscribers.size
|
64
|
+
end
|
65
|
+
|
66
|
+
# Internal: Publish a Message to all the Subscribers
|
67
|
+
#
|
68
|
+
# message - the Object to send to all subscribers
|
69
|
+
#
|
70
|
+
# Returns nothing
|
71
|
+
def publish( message )
|
72
|
+
@subscribers.each do |name, sub|
|
73
|
+
sub.produce( message )
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
#######
|
78
|
+
private
|
79
|
+
#######
|
80
|
+
|
81
|
+
def sub_queue( name )
|
82
|
+
@subscribers[name] ||= ::Qup::Adapter::Maildir::Queue.new( @topic_path, name )
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'qup/adapter'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
class Qup::Adapter
|
5
|
+
# Internal: The backing adapter for Qup that uses Redis as the messaging
|
6
|
+
# infrastructure
|
7
|
+
class Redis < ::Qup::Adapter
|
8
|
+
|
9
|
+
# Register this adapter as :redis
|
10
|
+
register :redis
|
11
|
+
|
12
|
+
# Internal: Create a new Redis Adapter
|
13
|
+
#
|
14
|
+
# uri - the URI instance for this adapter to use
|
15
|
+
def initialize( uri, options = {} )
|
16
|
+
@uri = uri
|
17
|
+
@options = options
|
18
|
+
@closed = false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Internal: Create a new Queue from this Adapter
|
22
|
+
#
|
23
|
+
# name - the String name of the Queue
|
24
|
+
#
|
25
|
+
# Returns a Qup::Queue
|
26
|
+
def queue( name )
|
27
|
+
Qup::Adapter::Redis::Queue.new( @uri, name )
|
28
|
+
end
|
29
|
+
|
30
|
+
# Internal: Create a new Topic from this Adapter
|
31
|
+
#
|
32
|
+
# name - the name of this Topic
|
33
|
+
#
|
34
|
+
# Returns a Qup::Topic
|
35
|
+
def topic( name )
|
36
|
+
Qup::Adapter::Redis::Topic.new( @uri, name )
|
37
|
+
end
|
38
|
+
|
39
|
+
# Internal: Close the Redis adapter
|
40
|
+
#
|
41
|
+
# Return nothing
|
42
|
+
def close
|
43
|
+
@closed = true
|
44
|
+
end
|
45
|
+
|
46
|
+
# Internal: Is the Redis Adapter closed
|
47
|
+
#
|
48
|
+
# Returns true or false
|
49
|
+
def closed?
|
50
|
+
@closed
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
require 'qup/adapter/redis/queue'
|
55
|
+
require 'qup/adapter/redis/topic'
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Qup::Adapter::Redis
|
2
|
+
#
|
3
|
+
# Internal: The Common base class for Redis Topic and Queue
|
4
|
+
#
|
5
|
+
class Connection
|
6
|
+
|
7
|
+
# Public: the name of the Queue or Topic
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
# Public: Create a new Connection
|
11
|
+
#
|
12
|
+
# uri - the connection uri for the Redis Client
|
13
|
+
# name - the String name of the Connection
|
14
|
+
#
|
15
|
+
# Returns a new Connection.
|
16
|
+
def initialize( uri, name )
|
17
|
+
@uri = uri
|
18
|
+
@client = Redis.new :host => @uri.host, :port => @uri.port
|
19
|
+
@name = name
|
20
|
+
end
|
21
|
+
|
22
|
+
# Public: destroy the connection
|
23
|
+
#
|
24
|
+
# Closes the redis client connection.
|
25
|
+
#
|
26
|
+
# Returns nothing.
|
27
|
+
def destroy
|
28
|
+
@client.client.disconnect
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'qup/adapter/redis/connection'
|
2
|
+
|
3
|
+
class Qup::Adapter::Redis
|
4
|
+
#
|
5
|
+
# Internal: The Qup implementation for a Redis Queue
|
6
|
+
#
|
7
|
+
class Queue < Connection
|
8
|
+
include Qup::QueueAPI
|
9
|
+
|
10
|
+
# Internal: create a new Queue
|
11
|
+
#
|
12
|
+
# uri - the connection uri for the Redis Client
|
13
|
+
# name - the String name of the Queue
|
14
|
+
#
|
15
|
+
# Returns a new Queue.
|
16
|
+
def initialize( uri, name )
|
17
|
+
super
|
18
|
+
@open_messages = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# Internal: Destroy the queue
|
22
|
+
#
|
23
|
+
# Removes the list from redis.
|
24
|
+
#
|
25
|
+
# Returns nothing.
|
26
|
+
def destroy
|
27
|
+
@client.del name
|
28
|
+
end
|
29
|
+
|
30
|
+
# Internal: Empty the queue
|
31
|
+
#
|
32
|
+
# In redis, this is accomplished by merely deleting the list. If anything
|
33
|
+
# new is added to it, the key is re-created on the server
|
34
|
+
#
|
35
|
+
# Returns nothing
|
36
|
+
alias :flush :destroy
|
37
|
+
|
38
|
+
# Internal: return the number of Messages on the Queue
|
39
|
+
#
|
40
|
+
# Returns an integer of the Queue depth
|
41
|
+
def depth
|
42
|
+
@client.llen name
|
43
|
+
end
|
44
|
+
|
45
|
+
# Internal: Acknowledge that message is completed and remove it from the
|
46
|
+
# Queue.
|
47
|
+
#
|
48
|
+
# In redis, this doesn't do anything at all. The tracking is only performed
|
49
|
+
# to meet the API requirements.
|
50
|
+
#
|
51
|
+
# Returns nothing.
|
52
|
+
def acknowledge( message )
|
53
|
+
open_msg = @open_messages.delete( message.key )
|
54
|
+
raise Qup::Error, "Message #{message.key} is not currently being consumed" unless open_msg
|
55
|
+
end
|
56
|
+
|
57
|
+
# Internal: Put an item onto the Queue
|
58
|
+
#
|
59
|
+
# message - the data to put onto the queue.
|
60
|
+
#
|
61
|
+
# The 'message' that is passed in is wrapped in a Qup::Message before being
|
62
|
+
# stored.
|
63
|
+
#
|
64
|
+
# Returns the Message that was put onto the Queue
|
65
|
+
def produce( message )
|
66
|
+
@client.lpush name, message
|
67
|
+
return ::Qup::Message.new( message.object_id, message )
|
68
|
+
end
|
69
|
+
|
70
|
+
# Internal: Retrieve a Message from the Queue
|
71
|
+
#
|
72
|
+
# Yields a Message
|
73
|
+
#
|
74
|
+
# Returns a Message
|
75
|
+
def consume(&block)
|
76
|
+
queue_name, data = @client.brpop @name, 0 # blocking pop
|
77
|
+
message = ::Qup::Message.new( data.object_id, data )
|
78
|
+
@open_messages[message.key] = message
|
79
|
+
if block_given? then
|
80
|
+
yield_message( message, &block )
|
81
|
+
else
|
82
|
+
return message
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
#######
|
87
|
+
private
|
88
|
+
#######
|
89
|
+
|
90
|
+
def yield_message( message, &block )
|
91
|
+
yield message
|
92
|
+
ensure
|
93
|
+
acknowledge( message )
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'qup/adapter/redis/connection'
|
2
|
+
|
3
|
+
class Qup::Adapter::Redis
|
4
|
+
#
|
5
|
+
# Internal: The Qup implementation for a Redis Topic
|
6
|
+
#
|
7
|
+
# Based on the API requirements, this is not implemented with Redis pub/sub,
|
8
|
+
# rather it guarantees durability of all published messages by using
|
9
|
+
# sub-queues internally to deliver messages to subscribers.
|
10
|
+
#
|
11
|
+
class Topic < Connection
|
12
|
+
include Qup::TopicAPI
|
13
|
+
|
14
|
+
# Internal: create a new Topic
|
15
|
+
#
|
16
|
+
# uri - the connection uri for the Redis Client
|
17
|
+
# name - the String name of the Topic
|
18
|
+
#
|
19
|
+
# Returns a new Topic.
|
20
|
+
def initialize(uri, name)
|
21
|
+
super
|
22
|
+
@subscribers = Hash.new
|
23
|
+
end
|
24
|
+
|
25
|
+
# Internal: Creates a Publisher for the Topic
|
26
|
+
#
|
27
|
+
# Returns a new Publisher
|
28
|
+
def publisher
|
29
|
+
::Qup::Publisher.new( self )
|
30
|
+
end
|
31
|
+
|
32
|
+
# Internal: Create a subscriber for the Topic
|
33
|
+
#
|
34
|
+
# name - the String name of the subscriber
|
35
|
+
#
|
36
|
+
# Creating a subscriber creates a new Subscriber that will receive a copy of
|
37
|
+
# every message that is published to the Topic.
|
38
|
+
#
|
39
|
+
# Returns a Subscriber
|
40
|
+
def subscriber(name)
|
41
|
+
::Qup::Subscriber.new( self, subscriber_queue_for(name) )
|
42
|
+
end
|
43
|
+
|
44
|
+
# Internal: Return the number of Subscribers to this Topic
|
45
|
+
#
|
46
|
+
# Returns integer
|
47
|
+
def subscriber_count
|
48
|
+
@subscribers.size
|
49
|
+
end
|
50
|
+
|
51
|
+
# Internal: Publish a Message to all the Subscribers
|
52
|
+
#
|
53
|
+
# message - the Object to send to all subscribers
|
54
|
+
#
|
55
|
+
# Returns nothing
|
56
|
+
def publish( message )
|
57
|
+
@subscribers.values.each do |subscriber|
|
58
|
+
subscriber.produce message
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
#######
|
63
|
+
private
|
64
|
+
#######
|
65
|
+
|
66
|
+
# Private: create and register new sub-Queue for the given subscriber
|
67
|
+
#
|
68
|
+
# name - the name of the subscriber
|
69
|
+
#
|
70
|
+
# Returns a Queue
|
71
|
+
def subscriber_queue_for(name)
|
72
|
+
@subscribers[name] ||=
|
73
|
+
::Qup::Adapter::Redis::Queue.new(@uri, "#{@name}-#{name}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|