qup 1.2.0 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,8 +13,8 @@ class Qup::Adapter::Kestrel
13
13
  # name - the String name of the Topic
14
14
  #
15
15
  # Returns a new Queue
16
- def initialize( address, name )
17
- super(address, name)
16
+ def initialize( client, name )
17
+ super( client, name )
18
18
  @open_messages = {}
19
19
  end
20
20
 
@@ -25,7 +25,7 @@ class Qup::Adapter::Kestrel
25
25
  #
26
26
  # Returns nothing.
27
27
  def flush
28
- @admin_client.flush(@name)
28
+ @client.flush(@name)
29
29
  end
30
30
 
31
31
 
@@ -33,8 +33,7 @@ class Qup::Adapter::Kestrel
33
33
  #
34
34
  # Returns an integer of the Queue depth
35
35
  def depth
36
- stats = @admin_client.stat( @name )
37
- return stats['items']
36
+ @client.stats['queues'][@name]['items']
38
37
  end
39
38
 
40
39
 
@@ -42,9 +41,6 @@ class Qup::Adapter::Kestrel
42
41
  #
43
42
  # message - the data to put onto the queue.
44
43
  #
45
- # The 'message' that is passed in is wrapped in a Qup::Message before being
46
- # stored.
47
- #
48
44
  # A user of the Qup API should use a Producer instance to put items onto the
49
45
  # queue.
50
46
  #
@@ -62,16 +58,16 @@ class Qup::Adapter::Kestrel
62
58
  # A user of the Qup API should use a Consumer instance to retrieve items
63
59
  # from the Queue.
64
60
  #
65
- # Returns a Message
61
+ # Returns a Message or nil if no message was on the queue
66
62
  def consume(&block)
67
- data = @client.get( @name )
68
- q_message = ::Qup::Message.new( data.object_id, data )
69
- @open_messages[q_message.key] = q_message
63
+ q_item = @client.reserve( @name )
64
+ return nil unless q_item
65
+ q_message = ::Qup::Message.new( q_item.object_id, unmarshal_if_marshalled( q_item ))
66
+ @open_messages[q_message.key] = q_item
70
67
  if block_given? then
71
68
  yield_message( q_message, &block )
72
- else
73
- return q_message
74
69
  end
70
+ return q_message
75
71
  end
76
72
 
77
73
 
@@ -85,13 +81,22 @@ class Qup::Adapter::Kestrel
85
81
  def acknowledge( message )
86
82
  open_msg = @open_messages.delete( message.key )
87
83
  raise Qup::Error, "Message #{message.key} is not currently being consumed" unless open_msg
88
- @client.close_last_transaction
84
+ @client.close( @name )
89
85
  end
90
86
 
91
87
  #######
92
88
  private
93
89
  #######
94
90
 
91
+ def unmarshal_if_marshalled( data )
92
+ if data[0].ord == 4 and data[1].ord == 8 then
93
+ Marshal::load( data )
94
+ else
95
+ data
96
+ end
97
+ end
98
+
99
+
95
100
  def yield_message( message, &block )
96
101
  yield message
97
102
  ensure
@@ -1,3 +1,5 @@
1
+ require 'json'
2
+ require 'set'
1
3
  require 'qup/adapter/kestrel/destination'
2
4
  class Qup::Adapter::Kestrel
3
5
  #
@@ -27,20 +29,29 @@ class Qup::Adapter::Kestrel
27
29
  #
28
30
  # Returns a Subscriber
29
31
  def subscriber( name )
30
- ::Qup::Subscriber.new( self, subscriber_queue( name ) )
32
+ ::Qup::Subscriber.new( self, subscriber_queue( name ) )
31
33
  end
32
34
 
33
35
 
34
36
  # Internal: Return the number of Subscribers to this Topic
35
37
  #
38
+ # We want the sub portion of the json document that is in the 'counters'
39
+ # section. The keys in the 'counters' section that represent queue counters
40
+ # are all prefixed with 'q/<queue_name>/<stat>'. To count the number of
41
+ # subscribers to this topic, we just count the uniqe <queue_name> elements
42
+ # that start with this queue's name and followed by a '+'
43
+ #
36
44
  # Returns integer
37
45
  def subscriber_count
38
- c = 0
39
- @client.stats['queues'].keys.each do |k|
40
- next if k =~ /errors$/
41
- c += 1 if k =~ /^#{@name}\+/
46
+ c = Set.new
47
+
48
+ stats['queues'].keys.each do |k|
49
+ next unless k =~ %r{\A#{@name}\+}
50
+ parts = k.split("+")
51
+ c << parts[1]
42
52
  end
43
- return c
53
+
54
+ return c.size
44
55
  end
45
56
 
46
57
  # Internal: Publish a Message to all the Subscribers
@@ -49,7 +60,7 @@ class Qup::Adapter::Kestrel
49
60
  #
50
61
  # Returns nothing
51
62
  def publish( message )
52
- @client.set( @name, message )
63
+ @client.set( @name, message ) # do not expire the message
53
64
  end
54
65
 
55
66
  #######
@@ -58,11 +69,15 @@ class Qup::Adapter::Kestrel
58
69
 
59
70
  def subscriber_queue( sub_name )
60
71
  sname = subscriber_queue_name( sub_name )
61
- ::Qup::Adapter::Kestrel::Queue.new( @address, sname )
72
+ ::Qup::Adapter::Kestrel::Queue.new( @client, sname )
62
73
  end
63
74
 
64
75
  def subscriber_queue_name( sub_name )
65
76
  "#{@name}+#{sub_name}"
66
77
  end
78
+
79
+ def stats
80
+ @client.stats
81
+ end
67
82
  end
68
83
  end
@@ -76,7 +76,8 @@ class Qup::Adapter::Redis
76
76
  #
77
77
  # Returns a Message
78
78
  def consume(&block)
79
- queue_name, data = @client.brpop name, 0 # blocking pop
79
+ data = @client.rpop( name )
80
+ return if data.nil?
80
81
  message = ::Qup::Message.new( data.object_id, data )
81
82
  @open_messages[message.key] = message
82
83
  if block_given? then
@@ -0,0 +1,52 @@
1
+ # Used to sleep for exponentially increasing amounts of time between
2
+ # successive unsuccessful attempts to do something like poll for queue data or
3
+ # reconnect to a client. Maxes out at 1 second between attempts.
4
+ #
5
+ # Call #tick in every iteration of the task you are trying to accomplish. When
6
+ # a successful iteration is completed (for example the queue had data, or
7
+ # connecting was successful) call #reset to let the sleeper know that it can
8
+ # reset the amount of time between attempts back to 0. The sleeper will not
9
+ # call Kernel#sleep if it #reset was just called, so it is safe to call #tick
10
+ # on every iteration, provided that you call #reset on success before calling
11
+ # #tick.
12
+ #
13
+ # Examples:
14
+ #
15
+ # sleeper = BackoffSleeper.new
16
+ #
17
+ # loop do
18
+ # message = check_for_message
19
+ # sleeper.reset unless message.nil?
20
+ # sleeper.tick
21
+ # end
22
+ module Qup
23
+ class BackoffSleeper
24
+ attr_reader :count
25
+
26
+ MULTIPLIERS = [0, 0.01, 0.1, 1]
27
+
28
+ def initialize
29
+ @count = 0
30
+ end
31
+
32
+ # Register an iteration. Sleeps if neccesary. Does not sleep if #reset has
33
+ # just been called.
34
+ def tick
35
+ Kernel.sleep(length) if length > 0
36
+ @count += 1
37
+ end
38
+
39
+ # Reset the backoff sequence to 0.
40
+ def reset
41
+ @count = 0
42
+ end
43
+
44
+ def length
45
+ (multiplier + (multiplier * Kernel.rand)) / 2
46
+ end
47
+
48
+ def multiplier
49
+ MULTIPLIERS[@count] || MULTIPLIERS.last
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,132 @@
1
+ # BatchConsumer makes it easy to implement a batch-oriented consumer pattern
2
+ # by calling the appropriate hooks on the provided "client" class. Clients
3
+ # have the following callbacks:
4
+ #
5
+ # setup - Optional. Called before the client begins receiving messages
6
+ # (immediately after BatchConsumer#run is called).
7
+ # process - Required. Called with an instance of Qup::Message when a
8
+ # message is removed from the queue. After process finishes the message is
9
+ # acknowledged from the queue. process is not wrapped in any exception
10
+ # handling, so exceptions raised in process will propagate through the
11
+ # BatchConsumer and the message will not be acknowledged.
12
+ # teardown - Optional. Called after the client has received :max_size
13
+ # messages OR :max_age has been exceeded.
14
+ #
15
+ # BatchConsumers should be only be run once. If you need to run again, create
16
+ # a new instance.
17
+ #
18
+ # See #initialize for the configuration options.
19
+ #
20
+ # Examples:
21
+ #
22
+ # class FileDumper
23
+ # include Qup::BatchConsumerAPI
24
+ #
25
+ # def setup
26
+ # @file = File.open("/my/file.txt")
27
+ # end
28
+ #
29
+ # def process(message)
30
+ # @file.puts(message.data)
31
+ # end
32
+ #
33
+ # def teardown
34
+ # @file.close
35
+ # end
36
+ # end
37
+ #
38
+ # batch_consumer = Qup::BatchConsumer.new({
39
+ # :max_size => 5000,
40
+ # :max_age => 600,
41
+ # :client => FileDumper.new,
42
+ # :queue_uri => "maildir:///tmp/test-queue",
43
+ # :queue_name => "my-queue"
44
+ # })
45
+ #
46
+ # batch_consumer.run # This blocks until there have been 5000 message OR 600 seconds have passed.
47
+ module Qup
48
+ module BatchConsumerAPI
49
+ def setup
50
+ end
51
+
52
+ def process(message)
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def teardown
57
+ end
58
+ end
59
+
60
+ class BatchConsumer
61
+ # options - A hash of configuration options.
62
+ # :client - The object upon which the callbacks are fired. It's class
63
+ # should include Qup::BatchConsumerAPI for declarative
64
+ # documentation purposes. Technically, the only constraint
65
+ # is that this object implement #setup, #process and
66
+ # #teardown (including Qup::BatchConsumerAPI includes noop
67
+ # definitions for #setup and #teardown). Required.
68
+ # :queue_uri - The Qup format queue URI. Required.
69
+ # :queue_name - The name of the queue that messages will be consumed
70
+ # from. Required.
71
+ # :max_size - The maximum number of messages to process. Optional.
72
+ # :max_age - The maximum number of seconds (from when #run is called)
73
+ # before finishing. Note that :max_size and :max_age are
74
+ # ORed, meaning that the BatchConsumer will finish when
75
+ # the first constraint is met. If neither constraint is
76
+ # provided, the BatchConsumer will never finish (i.e. #run
77
+ # will block indefinitely). Optional.
78
+ def initialize(options = {})
79
+ @message_count = 0
80
+ @options = options
81
+ end
82
+
83
+ def run
84
+ @start = Time.now
85
+ client.setup
86
+
87
+ while live?
88
+ sleeper.tick
89
+ consumer.consume do |message|
90
+ client.process(message)
91
+ @message_count += 1
92
+ sleeper.reset
93
+ end
94
+ end
95
+
96
+ client.teardown
97
+ end
98
+
99
+ def session
100
+ @session ||= Qup::Session.new(@options[:queue_uri], @options[:session_options] || {})
101
+ end
102
+
103
+ private
104
+
105
+ def client
106
+ @options[:client]
107
+ end
108
+
109
+ def sleeper
110
+ @sleeper ||= BackoffSleeper.new
111
+ end
112
+
113
+ def consumer
114
+ @consumer ||= begin
115
+ session.queue(@options[:queue_name]).consumer
116
+ end
117
+ end
118
+
119
+ def live?
120
+ !too_big? && !too_old?
121
+ end
122
+
123
+ def too_big?
124
+ @message_count >= @options[:max_size] if @options[:max_size]
125
+ end
126
+
127
+ def too_old?
128
+ (Time.now - @start) > @options[:max_age] if @options[:max_age]
129
+ end
130
+
131
+ end
132
+ end
@@ -38,5 +38,12 @@ module Qup
38
38
  def acknowledge( message )
39
39
  @queue.acknowledge( message )
40
40
  end
41
+
42
+ # Public: Return how many messages are on the queue for this consumer
43
+ #
44
+ # Returns an integer
45
+ def depth
46
+ @queue.depth
47
+ end
41
48
  end
42
49
  end
@@ -20,6 +20,9 @@ module Qup
20
20
  # Public: The URI of this Session
21
21
  attr_reader :uri
22
22
 
23
+ # Internal: access the adapter specific options.
24
+ attr_reader :options
25
+
23
26
  # Public: Create a new Session
24
27
  #
25
28
  # uri - The connection String used to connect to appropriate provider
@@ -47,9 +50,10 @@ module Qup
47
50
  def initialize( uri, options = {} )
48
51
  @uri = URI.parse( uri )
49
52
  @root_path = Pathname.new( @uri.path )
53
+ @options = options
50
54
 
51
55
  adapter_klass = Qup::Adapters[@uri.scheme]
52
- @adapter = adapter_klass.new( @uri, options )
56
+ @adapter = adapter_klass.new( @uri, @options )
53
57
 
54
58
  @queues = Hash.new
55
59
  @topics = Hash.new
@@ -5,4 +5,5 @@ require 'qup/adapter/kestrel_context'
5
5
  describe 'Qup::Adapter::Kestrel', :kestrel => true do
6
6
  include_context "Qup::Adapter::Kestrel"
7
7
  it_behaves_like Qup::Adapter
8
+
8
9
  end
@@ -23,9 +23,9 @@ describe 'Qup::Adapter::Redis::Queue', :redis => true do
23
23
 
24
24
  describe "#destroy" do
25
25
  it "removes its name from the parent topic's subscriber set" do
26
- redis.smembers("parent").should == ["test"]
26
+ redis.smembers("parent").should be == ["test"]
27
27
  queue.destroy
28
- redis.smembers("parent").should == []
28
+ redis.smembers("parent").should be == []
29
29
  end
30
30
  end
31
31
 
@@ -0,0 +1,73 @@
1
+ require "spec_helper"
2
+
3
+ module Qup
4
+ describe BackoffSleeper do
5
+ before { Kernel.stub(:sleep) }
6
+
7
+ describe "#length" do
8
+ it "it returns the multiplier averaged with the multiplier multiplied by rand" do
9
+ Kernel.stub(:rand).with().and_return(0.5)
10
+
11
+ sleeper = BackoffSleeper.new
12
+ sleeper.stub(:multiplier => 1)
13
+
14
+ sleeper.length.should be == ((1 + (1 * 0.5)) / 2)
15
+ end
16
+ end
17
+
18
+ describe "#tick" do
19
+ it "starts count at 0" do
20
+ subject.count.should be == 0
21
+ end
22
+
23
+ it "increments count by 1 everytime it's called" do
24
+ subject.tick
25
+ subject.count.should be == 1
26
+
27
+ subject.tick
28
+ subject.count.should be == 2
29
+ end
30
+
31
+ it "sleeps for #length if length is > 0" do
32
+ Kernel.should_receive(:sleep).with(0.123)
33
+
34
+ subject.stub(:length => 0.123)
35
+ subject.tick
36
+ end
37
+
38
+ it "doesn't call sleep if the length is 0" do
39
+ Kernel.should_not_receive(:sleep).with(0)
40
+
41
+ subject.stub(:length => 0)
42
+ subject.tick
43
+ end
44
+ end
45
+
46
+ describe "#reset" do
47
+ it "sets count back to 0" do
48
+ subject.tick
49
+ expect { subject.reset }.to change { subject.count }.to(0)
50
+ end
51
+ end
52
+
53
+ describe "#multiplier" do
54
+ it "starts at 0" do
55
+ subject.multiplier.should be == 0
56
+ end
57
+
58
+ it "backs off exponentially" do
59
+ multipliers = (0..2).map do
60
+ subject.tick
61
+ subject.multiplier
62
+ end
63
+
64
+ multipliers.should be == [0.01, 0.1, 1]
65
+ end
66
+
67
+ it "maxes out at the last value of MULTIPLIERS" do
68
+ 100.times { subject.tick }
69
+ subject.multiplier.should == BackoffSleeper::MULTIPLIERS.last
70
+ end
71
+ end
72
+ end
73
+ end