simrpc 0.2 → 0.4
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/Rakefile +6 -3
- data/lib/simrpc.rb +2 -1
- data/lib/simrpc/common.rb +9 -0
- data/lib/simrpc/message.rb +29 -9
- data/lib/simrpc/node.rb +75 -82
- data/lib/simrpc/qpid_adapter.rb +18 -4
- data/lib/simrpc/schema.rb +7 -2
- data/lib/simrpc/thread_pool.rb +137 -0
- data/spec/common_spec.rb +10 -0
- data/spec/message_spec.rb +8 -1
- data/spec/node_spec.rb +37 -0
- data/spec/qpid_spec.rb +19 -0
- data/spec/schema_spec.rb +1 -0
- data/spec/spec_helper.rb +1 -1
- metadata +70 -7
data/Rakefile
CHANGED
@@ -8,7 +8,7 @@ require 'spec/rake/spectask'
|
|
8
8
|
require 'rake/gempackagetask'
|
9
9
|
|
10
10
|
GEM_NAME="simrpc"
|
11
|
-
PKG_VERSION=0.
|
11
|
+
PKG_VERSION=0.4
|
12
12
|
|
13
13
|
desc "Run all specs"
|
14
14
|
Spec::Rake::SpecTask.new('spec') do |t|
|
@@ -30,11 +30,14 @@ SPEC = Gem::Specification.new do |s|
|
|
30
30
|
|
31
31
|
s.required_ruby_version = '>= 1.8.1'
|
32
32
|
s.required_rubygems_version = Gem::Requirement.new(">= 1.3.3")
|
33
|
-
|
33
|
+
s.add_development_dependency('rspec', '~> 1.3.0')
|
34
|
+
s.add_dependency('uuid', '~> 2.3.1')
|
35
|
+
s.add_dependency('activesupport', '~> 2.3.8')
|
36
|
+
# !!! we also need the ruby qpid wrapper. which is not currently packaged in gem form
|
34
37
|
|
35
38
|
s.author = "Mohammed Morsi"
|
36
39
|
s.email = "movitto@yahoo.com"
|
37
|
-
s.date = %q{2010-
|
40
|
+
s.date = %q{2010-09-04}
|
38
41
|
s.description = %q{simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.}
|
39
42
|
s.summary = %q{simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.}
|
40
43
|
s.homepage = %q{http://projects.morsi.org/Simrpc}
|
data/lib/simrpc.rb
CHANGED
@@ -30,9 +30,10 @@ lib = File.dirname(__FILE__)
|
|
30
30
|
|
31
31
|
require lib + '/simrpc/common'
|
32
32
|
require lib + '/simrpc/exceptions'
|
33
|
+
require lib + '/simrpc/thread_pool'
|
33
34
|
require lib + '/simrpc/schema'
|
34
35
|
require lib + '/simrpc/message'
|
35
36
|
require lib + '/simrpc/qpid_adapter'
|
36
37
|
require lib + '/simrpc/node'
|
37
38
|
|
38
|
-
require '
|
39
|
+
require 'active_support' # for inflector
|
data/lib/simrpc/common.rb
CHANGED
@@ -24,6 +24,7 @@
|
|
24
24
|
# OTHER DEALINGS IN THE SOFTWARE.
|
25
25
|
|
26
26
|
require 'logger'
|
27
|
+
require 'uuid'
|
27
28
|
|
28
29
|
module Simrpc
|
29
30
|
|
@@ -43,4 +44,12 @@ class Logger
|
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
47
|
+
# ID bank, generates unique ids
|
48
|
+
class IDBank
|
49
|
+
# FIXME right now generates uuid and grabs first 8 chars for message ids, not enough for tru randomness
|
50
|
+
def self.generate
|
51
|
+
UUID.new.generate[0...8]
|
52
|
+
end
|
46
53
|
end
|
54
|
+
|
55
|
+
end # module Simrpc
|
data/lib/simrpc/message.rb
CHANGED
@@ -33,15 +33,15 @@ module Message
|
|
33
33
|
# Simrpc::Message formatter helper module
|
34
34
|
class Formatter
|
35
35
|
|
36
|
-
#
|
37
|
-
# prepending a
|
36
|
+
# Helper method to format a data field,
|
37
|
+
# prepending a size to it
|
38
38
|
def self.format_with_size(data)
|
39
39
|
# currently size is set to a 8 digit int
|
40
40
|
len = "%08d" % data.to_s.size
|
41
41
|
len + data.to_s
|
42
42
|
end
|
43
43
|
|
44
|
-
#
|
44
|
+
# Helper method to parse a data field
|
45
45
|
# off the front of a data sequence, using the
|
46
46
|
# formatted size. Returns parsed data field
|
47
47
|
# and remaining data sequence. If optional
|
@@ -56,6 +56,24 @@ class Formatter
|
|
56
56
|
return parsed, remaining if data_class.nil?
|
57
57
|
return data_class.from_s(parsed), remaining
|
58
58
|
end
|
59
|
+
|
60
|
+
# Helper to format a data field of a fixed size,
|
61
|
+
def self.format_with_fixed_size(size, data)
|
62
|
+
# we will only accept the first <size> characters of data
|
63
|
+
# FIXME throw exception if data.size < size
|
64
|
+
data.to_s[0..size-1]
|
65
|
+
end
|
66
|
+
|
67
|
+
# Helper to parse data of a fixed size and return it and the remaining data.
|
68
|
+
# If optional class is given, the from_s method will be invoked w/ the parsed
|
69
|
+
# data field and returned w/ the remaining data sequence instead
|
70
|
+
def self.parse_from_formatted_with_fixed_size(size, data, data_class = nil)
|
71
|
+
# FIXME throw exception if data.size < size
|
72
|
+
parsed = data[0..size-1]
|
73
|
+
remaining = data[size...data.size]
|
74
|
+
return parsed, remaining if data_class.nil?
|
75
|
+
return data_class.from_s(parsed), remaining
|
76
|
+
end
|
59
77
|
end
|
60
78
|
|
61
79
|
# a single field trasnmitted via a message,
|
@@ -83,19 +101,21 @@ end
|
|
83
101
|
# header contains various descriptive properies
|
84
102
|
# about a message
|
85
103
|
class Header
|
86
|
-
attr_accessor :type, :target
|
104
|
+
attr_accessor :id, :type, :target
|
87
105
|
|
88
106
|
def initialize(args = {})
|
107
|
+
@id = args[:id].nil? ? "" : args[:id]
|
89
108
|
@type = args[:type].nil? ? "" : args[:type]
|
90
109
|
@target = args[:target].nil? ? "" : args[:target]
|
91
110
|
end
|
92
111
|
|
93
112
|
def to_s
|
94
|
-
Formatter::format_with_size(@type) + Formatter::format_with_size(@target)
|
113
|
+
Formatter::format_with_fixed_size(8, @id) + Formatter::format_with_size(@type) + Formatter::format_with_size(@target)
|
95
114
|
end
|
96
115
|
|
97
116
|
def self.from_s(data)
|
98
117
|
header = Header.new
|
118
|
+
header.id, data = Formatter::parse_from_formatted_with_fixed_size(8, data)
|
99
119
|
header.type, data = Formatter::parse_from_formatted(data)
|
100
120
|
header.target, data = Formatter::parse_from_formatted(data)
|
101
121
|
return header
|
@@ -106,7 +126,7 @@ end
|
|
106
126
|
class Body
|
107
127
|
attr_accessor :fields
|
108
128
|
|
109
|
-
def initialize
|
129
|
+
def initialize(args = {})
|
110
130
|
@fields = []
|
111
131
|
end
|
112
132
|
|
@@ -134,9 +154,9 @@ end
|
|
134
154
|
class Message
|
135
155
|
attr_accessor :header, :body
|
136
156
|
|
137
|
-
def initialize
|
138
|
-
@header = Header.new
|
139
|
-
@body = Body.new
|
157
|
+
def initialize(args = {})
|
158
|
+
@header = Header.new(args.has_key?(:header) ? args[:header] : {})
|
159
|
+
@body = Body.new(args.has_key?(:body) ? args[:body] : {})
|
140
160
|
end
|
141
161
|
|
142
162
|
def to_s
|
data/lib/simrpc/node.rb
CHANGED
@@ -33,89 +33,71 @@ class MethodMessageController
|
|
33
33
|
@schema_def = schema_def
|
34
34
|
end
|
35
35
|
|
36
|
-
#
|
36
|
+
# Generate new new method message, setting the message
|
37
37
|
# target to the specified method name, and setting the fields
|
38
38
|
# on the message to the method arguments
|
39
39
|
def generate(method_name, args)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
return msg
|
58
|
-
end
|
59
|
-
}
|
60
|
-
return nil
|
40
|
+
mmethod = @schema_def.methods.find { |method| method.name == method_name }
|
41
|
+
return nil if mmethod.nil?
|
42
|
+
|
43
|
+
msg = Message::Message.new :header => {:id => IDBank.generate, :type => 'request', :target => mmethod.name }
|
44
|
+
|
45
|
+
# if we have too few arguments, fill rest in w/ default values
|
46
|
+
if mmethod.parameters.size > args.size
|
47
|
+
mmethod.parameters[args.size...mmethod.parameters.size].each { |param| args << param.default }
|
48
|
+
end
|
49
|
+
|
50
|
+
msg.body.fields = (0...mmethod.parameters.size).collect { |i|
|
51
|
+
Message::Field.new(:name => mmethod.parameters[i].name,
|
52
|
+
:value => mmethod.parameters[i].to_s(args[i], @schema_def))
|
53
|
+
}
|
54
|
+
|
55
|
+
return msg
|
61
56
|
end
|
62
57
|
|
63
58
|
# should be invoked when a message is received,
|
64
59
|
# takes a message, converts it into a method call, and calls the corresponding
|
65
60
|
# handler in the provided schema. Takes return arguments and sends back to caller
|
66
61
|
def message_received(node, message, reply_to)
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
# for response values just return converted return values
|
109
|
-
else
|
110
|
-
results = []
|
111
|
-
method.return_values.each { |data_field|
|
112
|
-
value_field = message.body.fields.find { |f| f.name == data_field.name }
|
113
|
-
results.push data_field.from_s(value_field.value, @schema_def) unless value_field.nil? # TODO what if value_field is nil
|
114
|
-
}
|
115
|
-
return results
|
116
|
-
end
|
117
|
-
end
|
118
|
-
}
|
62
|
+
message = Message::Message::from_s(message)
|
63
|
+
mmethod = @schema_def.methods.find { |method| method.name == message.header.target }
|
64
|
+
return nil if mmethod.nil?
|
65
|
+
|
66
|
+
Logger.info "received method #{mmethod.name} message "
|
67
|
+
|
68
|
+
# For request messages, dispatch to method handler
|
69
|
+
if message.header.type == 'request'
|
70
|
+
return nil if mmethod.handler.nil?
|
71
|
+
|
72
|
+
# collect the method params
|
73
|
+
params = (0...mmethod.parameters.size).collect { |i| mmethod.parameters[i].from_s(message.body.fields[i].value, @schema_def) }
|
74
|
+
|
75
|
+
Logger.info "invoking #{mmethod.name} handler "
|
76
|
+
|
77
|
+
# invoke method handler
|
78
|
+
return_values = mmethod.handler.call(*params) # FIXME handlers can't use 'return' as this will fall through here
|
79
|
+
# FIXME throw a catch block around this call to catch all handler exceptions
|
80
|
+
return_values = [return_values] unless return_values.is_a? Array
|
81
|
+
|
82
|
+
# consruct and send response message using return values
|
83
|
+
response = Message::Message.new :header => {:id => message.header.id, :type => 'response', :target => mmethod.name }
|
84
|
+
response.body.fields = (0...mmethod.return_values.size).collect { |i|
|
85
|
+
field_def = mmethod.return_values[i]
|
86
|
+
Message::Field.new :name => field_def.name, :value => field_def.to_s(return_values[i], @schema_def)
|
87
|
+
# TODO if mmethod.return_values.size > return_values.size, fill in w/ default values
|
88
|
+
}
|
89
|
+
Logger.info "responding to #{reply_to}"
|
90
|
+
node.send_message(reply_to, response)
|
91
|
+
return message.header.id, params
|
92
|
+
|
93
|
+
# For response values just return converted return values
|
94
|
+
else
|
95
|
+
results = (0...mmethod.return_values.size).collect { |i|
|
96
|
+
mmethod.return_values[i].from_s(message.body.fields[i].value, @schema_def)
|
97
|
+
}
|
98
|
+
return message.header.id, results
|
99
|
+
|
100
|
+
end
|
119
101
|
end
|
120
102
|
end
|
121
103
|
|
@@ -140,15 +122,17 @@ class Node
|
|
140
122
|
end
|
141
123
|
raise ArgumentError, "schema_def cannot be nil" if @schema_def.nil?
|
142
124
|
@mmc = MethodMessageController.new(@schema_def)
|
143
|
-
|
144
|
-
|
125
|
+
|
126
|
+
# hash of message id's -> response locks
|
127
|
+
@message_locks = {}
|
145
128
|
|
146
129
|
# FIXME currently not allowing for any other params to be passed into
|
147
130
|
# QpidAdapter::Node such as broker ip or port, NEED TO FIX THIS
|
131
|
+
# NOTE see QpidAdapter::Node for limitation on using 'inspect'
|
148
132
|
@qpid_node = QpidAdapter::Node.new(:id => @id)
|
149
133
|
@qpid_node.async_accept { |node, msg, reply_to|
|
150
|
-
results = @mmc.message_received(node, msg, reply_to)
|
151
|
-
message_received(results)
|
134
|
+
mid, results = @mmc.message_received(node, msg, reply_to)
|
135
|
+
message_received(mid, results)
|
152
136
|
}
|
153
137
|
end
|
154
138
|
|
@@ -158,9 +142,14 @@ class Node
|
|
158
142
|
end
|
159
143
|
|
160
144
|
# implements, message_received callback to be notified when qpid receives a message
|
161
|
-
def message_received(results)
|
162
|
-
@message_results = results
|
163
|
-
@
|
145
|
+
def message_received(mid, results)
|
146
|
+
@message_results = results unless results.nil?
|
147
|
+
@message_locks[mid].signal unless mid.nil? || !@message_locks.has_key?(mid)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Terminate node operations
|
151
|
+
def terminate
|
152
|
+
@qpid_node.terminate
|
164
153
|
end
|
165
154
|
|
166
155
|
# wait until the node is no longer accepting messages
|
@@ -188,12 +177,16 @@ class Node
|
|
188
177
|
|
189
178
|
# block if we are expecting return values
|
190
179
|
if @schema_def.methods.find{|m| m.name == method_name}.return_values.size != 0
|
191
|
-
@
|
180
|
+
@message_locks[msg.header.id] = Semaphore.new(1)
|
181
|
+
@message_locks[msg.header.id].wait
|
182
|
+
@message_locks[msg.header.id].wait # block until response received
|
183
|
+
@message_locks.delete(msg.header.id)
|
192
184
|
|
193
185
|
# return return values
|
194
186
|
#@message_received.body.fields.collect { |f| f.value }
|
195
187
|
return *@message_results
|
196
188
|
end
|
189
|
+
return nil
|
197
190
|
end
|
198
191
|
|
199
192
|
# can invoke schema methods directly on Node instances, this will catch
|
data/lib/simrpc/qpid_adapter.rb
CHANGED
@@ -37,7 +37,8 @@ module QpidAdapter
|
|
37
37
|
# network which has its own exchange and queue which it listens on
|
38
38
|
class Node
|
39
39
|
private
|
40
|
-
#
|
40
|
+
# Helper method to generate a random id
|
41
|
+
# TODO use uuid since we're pulling in that gem anyways for message ids
|
41
42
|
def gen_uuid
|
42
43
|
["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") %
|
43
44
|
Array.new(16) {|x| rand(0xff) }
|
@@ -77,6 +78,9 @@ class Node
|
|
77
78
|
end
|
78
79
|
|
79
80
|
### create underlying tcp connection
|
81
|
+
# NOTE pretty big bug in ruby/qpid preventing us from using 'inspect'
|
82
|
+
# on this class (causes segfault)
|
83
|
+
# https://issues.apache.org/jira/browse/QPID-2405
|
80
84
|
@conn = Qpid::Connection.new(TCPSocket.new(broker,port))
|
81
85
|
@conn.start
|
82
86
|
|
@@ -85,6 +89,10 @@ class Node
|
|
85
89
|
|
86
90
|
@children = {}
|
87
91
|
|
92
|
+
# threads pool to handle incoming requests
|
93
|
+
# FIXME make the # of threads and timeout configurable)
|
94
|
+
@thread_pool = ThreadPool.new(10, :timeout => 5)
|
95
|
+
|
88
96
|
@accept_lock = Semaphore.new(1)
|
89
97
|
|
90
98
|
# qpid constructs that will be created for node
|
@@ -126,28 +134,34 @@ class Node
|
|
126
134
|
|
127
135
|
# start receiving messages
|
128
136
|
@incoming.listen{ |msg|
|
129
|
-
Logger.info "queue #{@queue} received message #{msg.body
|
137
|
+
Logger.info "queue #{@queue} received message #{msg.body}"
|
130
138
|
reply_to = msg.get(:message_properties).reply_to.routing_key
|
131
|
-
handler
|
139
|
+
# FIXME should delete handler threads as they complete
|
140
|
+
# FIXME handler timeout
|
141
|
+
job = ThreadPoolJob.new { handler.call(self, msg.body, reply_to) }
|
142
|
+
@thread_pool << job
|
132
143
|
}
|
133
144
|
end
|
134
145
|
|
135
146
|
# block until accept operation is complete
|
136
147
|
def join
|
137
148
|
@accept_lock.wait
|
149
|
+
#FIXME @thread_pool.join
|
138
150
|
end
|
139
151
|
|
140
152
|
# instructs QpidServer to stop accepting, blocking
|
141
153
|
# untill all accepting operations have terminated
|
142
154
|
def terminate
|
143
155
|
Logger.warn "terminating qpid session"
|
156
|
+
@thread_pool.stop
|
144
157
|
unless @incoming.nil?
|
145
158
|
@incoming.stop
|
146
159
|
@incoming.close
|
147
160
|
@accept_lock.signal
|
148
161
|
end
|
162
|
+
@ssn.queue_delete(@queue)
|
163
|
+
@ssn.exchange_delete(@exchange)
|
149
164
|
@ssn.close
|
150
|
-
# TODO undefine the @queue/@exchange
|
151
165
|
end
|
152
166
|
|
153
167
|
# send a message to the specified routing_key
|
data/lib/simrpc/schema.rb
CHANGED
@@ -66,9 +66,12 @@ module_function :primitive_from_str
|
|
66
66
|
class DataFieldDef
|
67
67
|
attr_accessor :type, :name, :associated
|
68
68
|
|
69
|
-
#
|
69
|
+
# Indicates this data field should be ignored if it has a null value
|
70
70
|
attr_accessor :ignore_null
|
71
71
|
|
72
|
+
# Indicates the default value of the field
|
73
|
+
attr_accessor :default
|
74
|
+
|
72
75
|
def initialize(args = {})
|
73
76
|
@type = args[:type] unless args[:type].nil?
|
74
77
|
@name = args[:name] unless args[:name].nil?
|
@@ -138,7 +141,7 @@ class DataFieldDef
|
|
138
141
|
# Provide schema_def for :obj or :array data fields associated w/ non-primitive types
|
139
142
|
# # coverted_classes is a recursive helper array used/maintained internally
|
140
143
|
def from_s(str, schema_def = nil, converted_classes = [])
|
141
|
-
if str == ""
|
144
|
+
if str == "" # || str == "nil" # FIXME uncomment & test
|
142
145
|
return nil
|
143
146
|
|
144
147
|
elsif Schema::is_primitive?(@type)
|
@@ -390,8 +393,10 @@ class Parser
|
|
390
393
|
data_field = DataFieldDef.new
|
391
394
|
data_field.type = element.attributes["type"].intern
|
392
395
|
data_field.name = element.attributes["name"]
|
396
|
+
data_field.default = data_field.from_s(element.attributes["default"]) unless element.attributes["default"].nil?
|
393
397
|
data_field.associated = element.attributes["associated"].intern unless element.attributes["associated"].nil?
|
394
398
|
data_field.ignore_null = true if element.attributes.include? "ignore_null"
|
399
|
+
data_field
|
395
400
|
return data_field
|
396
401
|
end
|
397
402
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# Thread Pool
|
2
|
+
#
|
3
|
+
# Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person
|
6
|
+
# obtaining a copy of this software and associated documentation
|
7
|
+
# files (the "Software"), to deal in the Software without
|
8
|
+
# restriction, including without limitation the rights to use,
|
9
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the
|
11
|
+
# Software is furnished to do so, subject to the following
|
12
|
+
# conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be
|
15
|
+
# included in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
19
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
21
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
22
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
23
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
24
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
25
|
+
|
26
|
+
require 'thread'
|
27
|
+
|
28
|
+
module Simrpc
|
29
|
+
|
30
|
+
# Work item to be executed in thread pool
|
31
|
+
class ThreadPoolJob
|
32
|
+
attr_accessor :handler
|
33
|
+
attr_accessor :params
|
34
|
+
|
35
|
+
def initialize(*params, &block)
|
36
|
+
@params = params
|
37
|
+
@handler = block
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# ActiveObject pattern, encapsulate each thread pool thread in object
|
42
|
+
class ThreadPoolJobRunner
|
43
|
+
attr_accessor :time_started
|
44
|
+
|
45
|
+
def initialize(thread_pool)
|
46
|
+
@thread_pool = thread_pool
|
47
|
+
end
|
48
|
+
|
49
|
+
def run
|
50
|
+
@thread = Thread.new {
|
51
|
+
until @thread_pool.terminate
|
52
|
+
work = @thread_pool.next_job
|
53
|
+
@time_started = Time.now
|
54
|
+
work.handler.call *work.params unless work.nil?
|
55
|
+
@time_started = nil
|
56
|
+
end
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def stop
|
61
|
+
@thread.kill
|
62
|
+
@thread.join
|
63
|
+
end
|
64
|
+
|
65
|
+
def join
|
66
|
+
@thread.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Launches a specified number of threads on instantiation,
|
71
|
+
# assigning work to them as it arrives
|
72
|
+
class ThreadPool
|
73
|
+
attr_accessor :terminate
|
74
|
+
|
75
|
+
# Create a thread pool with a specified number of threads
|
76
|
+
def initialize(num_threads, args = {})
|
77
|
+
@num_threads = num_threads
|
78
|
+
@timeout = args[:timeout]
|
79
|
+
@job_runners = []
|
80
|
+
@work_queue = []
|
81
|
+
@work_queue_lock = Mutex.new
|
82
|
+
@work_queue_cv = ConditionVariable.new
|
83
|
+
|
84
|
+
@terminate = false
|
85
|
+
|
86
|
+
0.upto(@num_threads) { |i|
|
87
|
+
runner = ThreadPoolJobRunner.new(self)
|
88
|
+
@job_runners << runner
|
89
|
+
runner.run
|
90
|
+
}
|
91
|
+
|
92
|
+
# optional timeout thread
|
93
|
+
unless @timeout.nil?
|
94
|
+
@timeout_thread = Thread.new {
|
95
|
+
until @terminate
|
96
|
+
sleep @timeout
|
97
|
+
@job_runners.each { |jr|
|
98
|
+
if !jr.time_started.nil? && (Time.now - jr.time_started > @timeout)
|
99
|
+
jr.stop
|
100
|
+
jr.run
|
101
|
+
end
|
102
|
+
}
|
103
|
+
end
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Add work to the pool
|
109
|
+
def <<(work)
|
110
|
+
@work_queue_lock.synchronize {
|
111
|
+
@work_queue << work
|
112
|
+
@work_queue_cv.signal
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return the next job queued up, blocking until one is received
|
117
|
+
# if none are present
|
118
|
+
def next_job
|
119
|
+
work = nil
|
120
|
+
@work_queue_lock.synchronize {
|
121
|
+
# wait until we have work
|
122
|
+
@work_queue_cv.wait(@work_queue_lock) if @work_queue.empty?
|
123
|
+
work = @work_queue.shift
|
124
|
+
}
|
125
|
+
work
|
126
|
+
end
|
127
|
+
|
128
|
+
# Terminate the thread pool
|
129
|
+
def stop
|
130
|
+
@terminate = true
|
131
|
+
@work_queue_lock.synchronize {
|
132
|
+
@work_queue.clear
|
133
|
+
}
|
134
|
+
@job_runners.each { |jr| jr.stop }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/spec/common_spec.rb
CHANGED
data/spec/message_spec.rb
CHANGED
@@ -35,6 +35,12 @@ describe "Simrpc::Message" do
|
|
35
35
|
assert_equal ["foobar", ""], Message::Formatter::parse_from_formatted(formatted)
|
36
36
|
end
|
37
37
|
|
38
|
+
it "should format and parsed a data field with a fixed size" do
|
39
|
+
formatted = Message::Formatter::format_with_fixed_size(3, 123456)
|
40
|
+
formatted.should == "123"
|
41
|
+
assert_equal ["123", ""], Message::Formatter::parse_from_formatted_with_fixed_size(3, formatted)
|
42
|
+
end
|
43
|
+
|
38
44
|
it "should convert a field to and from a string" do
|
39
45
|
field = Message::Field.new(:name => "foo", :value => "bar")
|
40
46
|
field_s = field.to_s
|
@@ -72,13 +78,14 @@ describe "Simrpc::Message" do
|
|
72
78
|
|
73
79
|
it "should convert a message to and from a string" do
|
74
80
|
msg = Message::Message.new
|
81
|
+
msg.header.id = "12345678"
|
75
82
|
msg.header.type = 'request'
|
76
83
|
msg.header.target = 'method'
|
77
84
|
msg.body.fields.push Message::Field.new(:name => "foo", :value => "bar")
|
78
85
|
msg.body.fields.push Message::Field.new(:name => "money", :value => "lotsof")
|
79
86
|
msg_s = msg.to_s
|
80
87
|
|
81
|
-
msg_s.should == "
|
88
|
+
msg_s.should == "000000371234567800000007request00000006method000000650000002200000003foo00000003bar0000002700000005money00000006lotsof"
|
82
89
|
|
83
90
|
msg_o = Message::Message.from_s(msg_s)
|
84
91
|
msg_o.header.type.should == 'request'
|
data/spec/node_spec.rb
CHANGED
@@ -70,9 +70,43 @@ describe "Simrpc::Node" do
|
|
70
70
|
client.send_message("server-queue", msg)
|
71
71
|
finished_lock.wait()
|
72
72
|
|
73
|
+
server.terminate
|
74
|
+
client.terminate
|
75
|
+
|
73
76
|
# FIXME test method that doesn't have any return values
|
74
77
|
end
|
75
78
|
|
79
|
+
it "should handle default values" do
|
80
|
+
schema_def = Schema::Parser.parse(:schema => TEST_SCHEMA)
|
81
|
+
mmc = MethodMessageController.new(schema_def)
|
82
|
+
msg = mmc.generate('foo_method', [2])
|
83
|
+
|
84
|
+
finished_lock = Semaphore.new(1)
|
85
|
+
finished_lock.wait()
|
86
|
+
|
87
|
+
schema_def.methods[0].handler = lambda { |some_int, floating_point_number|
|
88
|
+
some_int.to_i.should == 2
|
89
|
+
floating_point_number.to_f.should == 5.6 # test default value
|
90
|
+
return "yo", MyClass.new
|
91
|
+
}
|
92
|
+
|
93
|
+
server = QpidAdapter::Node.new :id => "server"
|
94
|
+
server.async_accept { |node, msg, reply_to|
|
95
|
+
mmc.message_received(node, msg, reply_to)
|
96
|
+
}
|
97
|
+
|
98
|
+
client = QpidAdapter::Node.new :id => 'client'
|
99
|
+
client.async_accept { |node, msg, reply_to|
|
100
|
+
#msg = Message::Message.from_s(msg)
|
101
|
+
finished_lock.signal()
|
102
|
+
}
|
103
|
+
client.send_message("server-queue", msg)
|
104
|
+
finished_lock.wait()
|
105
|
+
|
106
|
+
server.terminate
|
107
|
+
client.terminate
|
108
|
+
end
|
109
|
+
|
76
110
|
it "it should run properly" do
|
77
111
|
server = Node.new(:id => "server3", :schema => TEST_SCHEMA)
|
78
112
|
client = Node.new(:id => "client3", :schema => TEST_SCHEMA, :destination => "server3")
|
@@ -94,6 +128,9 @@ describe "Simrpc::Node" do
|
|
94
128
|
my_class_instance.float_member.should == 4.2
|
95
129
|
|
96
130
|
# FIXME test method that doesn't have any return values
|
131
|
+
|
132
|
+
client.terminate
|
133
|
+
server.terminate
|
97
134
|
end
|
98
135
|
|
99
136
|
end
|
data/spec/qpid_spec.rb
CHANGED
@@ -38,6 +38,7 @@ describe "Simrpc::QpidAdapter" do
|
|
38
38
|
ssn.error?.should == false
|
39
39
|
#ssn.closed.should == false
|
40
40
|
id.should == "test1"
|
41
|
+
qpid.terminate
|
41
42
|
end
|
42
43
|
|
43
44
|
# ensure we define a queue and exchange
|
@@ -67,6 +68,21 @@ describe "Simrpc::QpidAdapter" do
|
|
67
68
|
#assert !binding_result.queue_not_found?
|
68
69
|
#assert !binding_result.queue_not_matched?
|
69
70
|
#assert !binding_result.key_not_found?
|
71
|
+
|
72
|
+
node.terminate
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should terminate queue and exchange" do
|
76
|
+
node = QpidAdapter::Node.new :id => "qpid_test3"
|
77
|
+
node.terminate
|
78
|
+
|
79
|
+
# need to start a new session since terminate closes the previous one
|
80
|
+
conn = Qpid::Connection.new(TCPSocket.new('localhost', 5672))
|
81
|
+
conn.start
|
82
|
+
ssn = conn.session(UUID.new.generate)
|
83
|
+
|
84
|
+
assert ssn.queue_query("qpid_test3-queue").queue.nil?
|
85
|
+
assert ssn.exchange_query("qpid_test3-exchange").not_found
|
70
86
|
end
|
71
87
|
|
72
88
|
# test sending/receiving a message
|
@@ -87,6 +103,9 @@ describe "Simrpc::QpidAdapter" do
|
|
87
103
|
}
|
88
104
|
client.send_message("server1-queue", "test-data")
|
89
105
|
finished_lock.wait()
|
106
|
+
|
107
|
+
server.terminate
|
108
|
+
client.terminate
|
90
109
|
end
|
91
110
|
|
92
111
|
end
|
data/spec/schema_spec.rb
CHANGED
@@ -49,6 +49,7 @@ describe "Simrpc::Schema" do
|
|
49
49
|
schema_def.methods[0].parameters[0].name.should == 'some_int'
|
50
50
|
schema_def.methods[0].parameters[1].type.should == :float
|
51
51
|
schema_def.methods[0].parameters[1].name.should == 'floating_point_number'
|
52
|
+
schema_def.methods[0].parameters[1].default.should == 5.6
|
52
53
|
schema_def.methods[0].return_values.size.should == 2
|
53
54
|
schema_def.methods[0].return_values[0].type.should == :str
|
54
55
|
schema_def.methods[0].return_values[0].name.should == 'a_string'
|
data/spec/spec_helper.rb
CHANGED
@@ -40,7 +40,7 @@ TEST_SCHEMA =
|
|
40
40
|
"<schema>"+
|
41
41
|
" <method name='foo_method'>" +
|
42
42
|
" <param type='int' name='some_int'/>"+
|
43
|
-
" <param type='float' name='floating_point_number'/>"+
|
43
|
+
" <param type='float' name='floating_point_number' default='5.6' />"+
|
44
44
|
" <return_value type='str' name='a_string' />" +
|
45
45
|
" <return_value type='obj' name='my_class_instance' associated='MyClass' />" +
|
46
46
|
" </method>"+
|
metadata
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: simrpc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
|
4
|
+
hash: 3
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 4
|
9
|
+
version: "0.4"
|
5
10
|
platform: ruby
|
6
11
|
authors:
|
7
12
|
- Mohammed Morsi
|
@@ -9,10 +14,57 @@ autorequire:
|
|
9
14
|
bindir: bin
|
10
15
|
cert_chain: []
|
11
16
|
|
12
|
-
date: 2010-
|
17
|
+
date: 2010-09-04 00:00:00 -04:00
|
13
18
|
default_executable:
|
14
|
-
dependencies:
|
15
|
-
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 27
|
29
|
+
segments:
|
30
|
+
- 1
|
31
|
+
- 3
|
32
|
+
- 0
|
33
|
+
version: 1.3.0
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: uuid
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 1
|
45
|
+
segments:
|
46
|
+
- 2
|
47
|
+
- 3
|
48
|
+
- 1
|
49
|
+
version: 2.3.1
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: activesupport
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 19
|
61
|
+
segments:
|
62
|
+
- 2
|
63
|
+
- 3
|
64
|
+
- 8
|
65
|
+
version: 2.3.8
|
66
|
+
type: :runtime
|
67
|
+
version_requirements: *id003
|
16
68
|
description: simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.
|
17
69
|
email: movitto@yahoo.com
|
18
70
|
executables: []
|
@@ -25,6 +77,7 @@ files:
|
|
25
77
|
- lib/simrpc/message.rb
|
26
78
|
- lib/simrpc/common.rb
|
27
79
|
- lib/simrpc/semaphore.rb
|
80
|
+
- lib/simrpc/thread_pool.rb
|
28
81
|
- lib/simrpc/qpid_adapter.rb
|
29
82
|
- lib/simrpc/exceptions.rb
|
30
83
|
- lib/simrpc/schema.rb
|
@@ -49,21 +102,31 @@ rdoc_options: []
|
|
49
102
|
require_paths:
|
50
103
|
- lib
|
51
104
|
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
52
106
|
requirements:
|
53
107
|
- - ">="
|
54
108
|
- !ruby/object:Gem::Version
|
109
|
+
hash: 53
|
110
|
+
segments:
|
111
|
+
- 1
|
112
|
+
- 8
|
113
|
+
- 1
|
55
114
|
version: 1.8.1
|
56
|
-
version:
|
57
115
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
58
117
|
requirements:
|
59
118
|
- - ">="
|
60
119
|
- !ruby/object:Gem::Version
|
120
|
+
hash: 29
|
121
|
+
segments:
|
122
|
+
- 1
|
123
|
+
- 3
|
124
|
+
- 3
|
61
125
|
version: 1.3.3
|
62
|
-
version:
|
63
126
|
requirements: []
|
64
127
|
|
65
128
|
rubyforge_project:
|
66
|
-
rubygems_version: 1.3.
|
129
|
+
rubygems_version: 1.3.7
|
67
130
|
signing_key:
|
68
131
|
specification_version: 3
|
69
132
|
summary: simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.
|