mqrpc 0.0.1 → 0.0.3

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.
data/lib/mqrpc/agent.rb CHANGED
@@ -3,6 +3,7 @@ require 'amqp'
3
3
  require 'mq'
4
4
  require 'mqrpc/logger'
5
5
  require 'mqrpc/operation'
6
+ require 'mqrpc/sizedhash'
6
7
  require 'thread'
7
8
  require 'uuid'
8
9
 
@@ -27,6 +28,7 @@ module MQRPC
27
28
  # TODO: document this class
28
29
  class Agent
29
30
  MAXBUF = 20
31
+ MAXMESSAGEWAIT = MAXBUF * 20
30
32
 
31
33
  def initialize(config)
32
34
  Thread::abort_on_exception = true
@@ -35,11 +37,13 @@ module MQRPC
35
37
  @id = UUID::generate
36
38
  @outbuffer = Hash.new { |h,k| h[k] = [] }
37
39
  @queues = []
38
- @receive_queue = Queue.new
39
40
  @topics = []
40
- @want_queues = []
41
- @want_topics = []
42
- #@slidingwindow = LogStash::SlidingWindowSet.new
41
+ @receive_queue = Queue.new
42
+ @want_subscriptions = Queue.new
43
+ @slidingwindow = Hash.new do |h,k|
44
+ MQRPC::logger.info "New sliding window for #{k}"
45
+ h[k] = SizedThreadSafeHash.new(MAXMESSAGEWAIT)
46
+ end
43
47
 
44
48
  @mq = nil
45
49
  @message_operations = Hash.new
@@ -49,9 +53,13 @@ module MQRPC
49
53
  @amqp_ready = false
50
54
 
51
55
  start_amqp
56
+
57
+ # Wait for our AMQP thread to get going. Mainly, it needs to set
58
+ # @mq, so we'll block until it's available.
52
59
  @startup_mutex.synchronize do
53
60
  MQRPC::logger.debug "Waiting for @mq ..."
54
61
  @startup_condvar.wait(@startup_mutex) if !@amqp_ready
62
+ MQRPC::logger.debug "Got it, continuing with #{self.class} init..."
55
63
  end
56
64
 
57
65
  start_receiver
@@ -78,10 +86,11 @@ module MQRPC
78
86
  MQRPC::logger.info "Subscribing to main queue #{@id}"
79
87
  mq_q = @mq.queue(@id, :auto_delete => true)
80
88
  mq_q.subscribe(:ack =>true) { |hdr, msg| @receive_queue << [hdr, msg] }
81
- handle_new_subscriptions
89
+ #handle_new_subscriptions
82
90
 
83
91
  # TODO(sissel): make this a deferred thread that reads from a Queue
84
- EM.add_periodic_timer(5) { handle_new_subscriptions }
92
+ #EM.add_periodic_timer(5) { handle_new_subscriptions }
93
+ EM.defer { handle_subscriptions }
85
94
 
86
95
  EM.add_periodic_timer(1) do
87
96
  # TODO(sissel): add locking
@@ -102,11 +111,11 @@ module MQRPC
102
111
  end # def start_receiver
103
112
 
104
113
  def subscribe(name)
105
- @want_queues << name
114
+ @want_subscriptions << [:queue, name]
106
115
  end # def subscribe
107
116
 
108
117
  def subscribe_topic(name)
109
- @want_topics << name
118
+ @want_subscriptions << [:topic, name]
110
119
  end # def subscribe_topic
111
120
 
112
121
  def handle_message(hdr, msg_body)
@@ -115,12 +124,20 @@ module MQRPC
115
124
  obj = [obj]
116
125
  end
117
126
 
127
+ queue = hdr.routing_key
118
128
  obj.each do |item|
119
129
  message = Message.new_from_data(item)
120
- #if @slidingwindow.include?(message.id)
121
- #puts "Removing ack for #{message.id}"
122
- #@slidingwindow.delete(message.id)
123
- #end
130
+ slidingwindow = @slidingwindow[queue]
131
+ if message.respond_to?(:from_queue)
132
+ slidingwindow = @slidingwindow[message.from_queue]
133
+ end
134
+ MQRPC::logger.debug "#{Thread.current} Got message #{message.class}##{message.id} on queue #{queue}"
135
+ MQRPC::logger.debug "Received message: #{message.inspect}"
136
+ if (message.respond_to?(:in_reply_to) and
137
+ slidingwindow.include?(message.in_reply_to))
138
+ MQRPC::logger.debug "Got response to #{message.in_reply_to}"
139
+ slidingwindow.delete(message.in_reply_to)
140
+ end
124
141
  name = message.class.name.split(":")[-1]
125
142
  func = "#{name}Handler"
126
143
 
@@ -129,11 +146,12 @@ module MQRPC
129
146
  if (message.respond_to?(:in_reply_to) and
130
147
  @message_operations.has_key?(message.in_reply_to))
131
148
  operation = @message_operations[message.in_reply_to]
132
- operation.call message
149
+ operation.call(message)
133
150
  elsif @handler.respond_to?(func)
134
151
  @handler.send(func, message) do |response|
135
- reply = message.reply_to
136
- sendmsg(reply, response)
152
+ reply_destination = message.reply_to
153
+ response.from_queue = queue
154
+ sendmsg(reply_destination, response)
137
155
  end
138
156
 
139
157
  # We should allow the message handler to defer acking if they want
@@ -150,6 +168,28 @@ module MQRPC
150
168
  @amqpthread.join
151
169
  end # run
152
170
 
171
+ def handle_subscriptions
172
+ while true do
173
+ type, name = @want_subscriptions.pop
174
+ case type
175
+ when :queue
176
+ next if @queues.include?(name)
177
+ MQRPC::logger.info "Subscribing to queue #{name}"
178
+ mq_q = @mq.queue(name, :durable => true)
179
+ mq_q.subscribe(:ack => true) { |hdr, msg| @receive_queue << [hdr, msg] }
180
+ @queues << name
181
+ when :topic
182
+ MQRPC::logger.info "Subscribing to topic #{name}"
183
+ exchange = @mq.topic(@config.mqexchange)
184
+ mq_q = @mq.queue("#{@id}-#{name}",
185
+ :exclusive => true,
186
+ :auto_delete => true).bind(exchange, :key => name)
187
+ mq_q.subscribe { |hdr, msg| @receive_queue << [hdr, msg] }
188
+ @topics << name
189
+ end
190
+ end
191
+ end
192
+
153
193
  def handle_new_subscriptions
154
194
  todo = @want_queues - @queues
155
195
  todo.each do |queue|
@@ -162,7 +202,7 @@ module MQRPC
162
202
  todo = @want_topics - @topics
163
203
  todo.each do |topic|
164
204
  MQRPC::logger.info "Subscribing to topic #{topic}"
165
- exchange = @mq.topic("amq.topic")
205
+ exchange = @mq.topic(@config.mqexchange)
166
206
  mq_q = @mq.queue("#{@id}-#{topic}",
167
207
  :exclusive => true,
168
208
  :auto_delete => true).bind(exchange, :key => topic)
@@ -172,8 +212,6 @@ module MQRPC
172
212
  end # handle_new_subscriptions
173
213
 
174
214
  def flushout(destination)
175
- return unless @mq # wait until we are connected
176
-
177
215
  msgs = @outbuffer[destination]
178
216
  return if msgs.length == 0
179
217
  data = msgs.to_json
@@ -182,28 +220,26 @@ module MQRPC
182
220
  end
183
221
 
184
222
  def sendmsg_topic(key, msg)
185
- return unless @mq # wait until we are connected
186
223
  if (msg.is_a?(RequestMessage) and msg.id == nil)
187
224
  msg.generate_id!
188
225
  end
189
226
  msg.timestamp = Time.now.to_f
190
227
 
191
228
  data = msg.to_json
192
- @mq.topic("amq.topic").publish(data, :key => key)
229
+ @mq.topic(@config.mqexchange).publish(data, :key => key)
193
230
  end
194
231
 
195
232
  def sendmsg(destination, msg, &callback)
196
- return unless @mq # wait until we are connected
197
233
  if (msg.is_a?(RequestMessage) and msg.id == nil)
198
234
  msg.generate_id!
199
235
  end
200
236
  msg.timestamp = Time.now.to_f
201
237
  msg.reply_to = @id
202
238
 
203
- #if (msg.is_a?(RequestMessage) and !msg.is_a?(ResponseMessage))
204
- #MQRPC::logger.info "Tracking #{msg.class.name}##{msg.id}"
205
- #@slidingwindow << msg.id
206
- #end
239
+ if msg.is_a?(RequestMessage)
240
+ MQRPC::logger.debug "Tracking #{msg.class.name}##{msg.id} to #{destination}"
241
+ @slidingwindow[destination][msg.id] = true
242
+ end
207
243
 
208
244
  if msg.buffer?
209
245
  @outbuffer[destination] << msg
@@ -211,6 +247,7 @@ module MQRPC
211
247
  flushout(destination)
212
248
  end
213
249
  else
250
+ MQRPC::logger.debug "#{Thread.current} Sending to #{destination}: #{msg.inspect}"
214
251
  @mq.queue(destination, :durable => true).publish([msg].to_json, :persistent => true)
215
252
  end
216
253
 
data/lib/mqrpc/config.rb CHANGED
@@ -5,6 +5,7 @@ module MQRPC
5
5
  attr_reader :mquser
6
6
  attr_reader :mqpass
7
7
  attr_reader :mqvhost
8
+ attr_reader :mqexchange
8
9
 
9
10
  def initialize(options = {})
10
11
  @mqhost = options["mqhost"] || "localhost"
@@ -12,6 +13,7 @@ module MQRPC
12
13
  @mquser = options["mquser"] || "guest"
13
14
  @mqpass = options["mqpass"] || "guest"
14
15
  @mqvhost = options["mqvhost"] || "/"
16
+ @mqexchange = options["mqexchange"] || "mqrpc.topic"
15
17
  end # def initialize
16
18
  end # class Config
17
19
  end # module MQRPC
data/lib/mqrpc/message.rb CHANGED
@@ -26,6 +26,7 @@ module BindToHash
26
26
  )
27
27
  end
28
28
 
29
+ private
29
30
  def __genhashpath(key)
30
31
  # TODO(sissel): enforce 'key' needs to be a string or symbol?
31
32
  path = key.split("/").select { |x| x.length > 0 }\
@@ -48,14 +49,43 @@ module MQRPC
48
49
  header :message_class
49
50
  header :reply_to
50
51
  header :timestamp
52
+ header :args
53
+ def self.inherited(subclass)
54
+ MQRPC::logger.debug "Message '#{subclass.name}' subclasses #{self.name}"
55
+ @@knowntypes[subclass.name] = subclass
56
+
57
+ # Call the class initializer if it has one.
58
+ if subclass.respond_to?(:class_initialize)
59
+ subclass.class_initialize
60
+ end
61
+ end # def self.inherited
62
+
63
+ def self.new_from_data(data)
64
+ obj = nil
65
+ name = data["message_class"]
66
+ if @@knowntypes.has_key?(name)
67
+ obj = @@knowntypes[name].new
68
+ else
69
+ $stderr.puts "No known message class: #{name}, #{data.inspect}"
70
+ obj = Message.new
71
+ end
72
+ obj.data = data
73
+ return obj
74
+ end
51
75
 
52
76
  def initialize
77
+ @data = Hash.new
78
+ want_buffer(false)
79
+
53
80
  generate_id!
81
+ self.message_class = self.class.name
82
+ self.args = Hash.new
54
83
  end
55
84
 
56
85
  def generate_id!
57
86
  @@idlock.synchronize do
58
87
  self.id = @@idseq
88
+ #puts "Generating id. #{self.class}.id == #{self.id}"
59
89
  @@idseq += 1
60
90
  end
61
91
  end
@@ -72,35 +102,6 @@ module MQRPC
72
102
  @buffer = want_buffer
73
103
  end
74
104
 
75
- def self.inherited(subclass)
76
- MQRPC::logger.debug "Message '#{subclass.name}' subclasses #{self.name}"
77
- @@knowntypes[subclass.name] = subclass
78
-
79
- # Call the class initializer if it has one.
80
- if subclass.respond_to?(:class_initialize)
81
- subclass.class_initialize
82
- end
83
- end # def self.inherited
84
-
85
- def initialize
86
- @data = Hash.new
87
- want_buffer(false)
88
- self.message_class = self.class.name
89
- end
90
-
91
- def self.new_from_data(data)
92
- obj = nil
93
- name = data["message_class"]
94
- if @@knowntypes.has_key?(name)
95
- obj = @@knowntypes[name].new
96
- else
97
- $stderr.puts "No known message class: #{name}, #{data.inspect}"
98
- obj = Message.new
99
- end
100
- obj.data = data
101
- return obj
102
- end
103
-
104
105
  def to_json(*args)
105
106
  return @data.to_json(*args)
106
107
  end
@@ -110,18 +111,12 @@ module MQRPC
110
111
  end # class Message
111
112
 
112
113
  class RequestMessage < Message
113
- header :args
114
-
115
- def initialize
116
- super
117
- self.args = Hash.new
118
- end
119
-
114
+ # Nothing.
120
115
  end # class RequestMessage
121
116
 
122
117
  class ResponseMessage < Message
123
118
  header :in_reply_to
124
- header :args
119
+ header :from_queue
125
120
 
126
121
  def initialize(source_request=nil)
127
122
  super()
@@ -1,5 +1,5 @@
1
1
  require 'rubygems'
2
- require 'lib/mqrpc'
2
+ require 'mqrpc'
3
3
 
4
4
  module MQRPC::Messages
5
5
  class PingRequest < MQRPC::RequestMessage
@@ -16,17 +16,16 @@ module MQRPC
16
16
  end # def initialize
17
17
 
18
18
  def call(*args)
19
- # TODO(sissel): allow the callback to simply invoke 'finished' on this
20
- # operation rather than requiring it to emit ':finished'
19
+ # TODO(sissel): Come up with a better way for the callback to declare
20
+ # that it is not done than simply returning ':continue'
21
21
  @mutex.synchronize do
22
22
  ret = @callback.call(*args)
23
- if ret == :finished
24
- MQRPC::logger.debug "operation #{self} finished"
23
+ if ret != :continue
24
+ #MQRPC::logger.debug "operation #{self} finished"
25
25
  @finished = true
26
26
  @cv.signal
27
- else
28
- return ret
29
27
  end
28
+ return ret
30
29
  end
31
30
  end # def call
32
31
 
@@ -0,0 +1,97 @@
1
+ require 'thread'
2
+ require 'mqrpc'
3
+
4
+ # Thread-safe sized hash similar to SizedQueue.
5
+ # The only time we will block execution is in setting new items.
6
+ # That is, SizedThreadSafeHash#[]=
7
+ class SizedThreadSafeHash
8
+ def initialize(size)
9
+ @lock = Mutex.new
10
+ @size = size
11
+ @condvar = ConditionVariable.new
12
+ @data = Hash.new
13
+ end # def initialize
14
+
15
+ # set a key and value
16
+ def []=(key, value)
17
+ @lock.synchronize do
18
+ # If adding a new item, wait if the hash is full
19
+ if !@data.has_key?(key) and _withlock_full?
20
+ MQRPC::logger.info "#{self}: Waiting to add key #{key.inspect}, hash is full"
21
+ #MQRPC::logger.info "#{self}: Keys: #{@data.keys.inspect}"
22
+ #MQRPC::logger.info "#{self} current thread is #{Thread.current}"
23
+ #pp @data
24
+ @condvar.wait(@lock)
25
+ end
26
+ @data[key] = value
27
+ end
28
+ end # def []=
29
+
30
+ # get an value by key
31
+ def [](key)
32
+ @lock.synchronize do
33
+ return @data[key]
34
+ end
35
+ end # def []
36
+
37
+ # boolean, does the hash have a given key?
38
+ def has_key?(key)
39
+ @lock.synchronize do
40
+ return @data.has_key?(key)
41
+ end
42
+ end # def has_key?
43
+
44
+ alias :include? :has_key?
45
+
46
+ # delete a key
47
+ def delete(key)
48
+ @lock.synchronize do
49
+ was_full = _withlock_full?
50
+ @data.delete(key)
51
+ if was_full
52
+ MQRPC::logger.info "#{self}: signalling non-fullness"
53
+ @condvar.signal
54
+ end
55
+ end
56
+ end # def delete
57
+
58
+ # boolean, indicates true when the hash is full (has size == initialized size)
59
+ def full?
60
+ @lock.synchronize do
61
+ return _withlock_full?
62
+ end
63
+ end # def full?
64
+
65
+ # return the size (total number of entries) in this hash.
66
+ def size
67
+ @lock.synchronize do
68
+ return @data.size
69
+ end
70
+ end
71
+
72
+ # Return an array of keys for this hash.
73
+ def keys
74
+ @lock.synchronize do
75
+ return @data.keys
76
+ end
77
+ end
78
+ alias :length :size
79
+
80
+ private
81
+ def _withlock_full?
82
+ return @data.size >= @size
83
+ end
84
+ end # class SizedThreadSafeHash
85
+
86
+ #sh = SizedThreadSafeHash.new(10)
87
+ #Thread.new do
88
+ #0.upto(100) do |i|
89
+ #sh[i] = true
90
+ #end
91
+ #end
92
+
93
+ #0.upto(100) do |i|
94
+ #puts sh[i]
95
+ #sh.delete(i)
96
+ #sleep 1
97
+ #end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mqrpc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Sissel, Pete Fritchman
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-05 00:00:00 -08:00
12
+ date: 2009-11-07 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -58,6 +58,7 @@ files:
58
58
  - lib/mqrpc/message.rb
59
59
  - lib/mqrpc/operation.rb
60
60
  - lib/mqrpc/messages/ping.rb
61
+ - lib/mqrpc/sizedhash.rb
61
62
  - lib/mqrpc.rb
62
63
  has_rdoc: true
63
64
  homepage: http://code.google.com/p/logstash/wiki/MQRPC