qup 1.2.0 → 1.4.0

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.
@@ -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