simrpc 0.1 → 0.2
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/LICENSE +1 -1
- data/README.rdoc +79 -0
- data/Rakefile +46 -0
- data/lib/simrpc/common.rb +46 -0
- data/lib/simrpc/exceptions.rb +30 -0
- data/lib/simrpc/message.rb +156 -0
- data/lib/simrpc/node.rb +206 -0
- data/lib/simrpc/qpid_adapter.rb +171 -0
- data/lib/simrpc/schema.rb +400 -0
- data/lib/simrpc.rb +9 -802
- data/spec/common_spec.rb +26 -0
- data/spec/message_spec.rb +93 -0
- data/spec/node_spec.rb +99 -0
- data/spec/qpid_spec.rb +92 -0
- data/spec/schema_spec.rb +348 -0
- data/spec/spec_helper.rb +56 -0
- metadata +25 -23
- data/README +0 -25
- /data/lib/{semaphore.rb → simrpc/semaphore.rb} +0 -0
data/lib/simrpc.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
# Implements a simple to use method based RPC for ruby
|
4
4
|
# built upon Apache Qpid
|
5
5
|
#
|
6
|
-
# Copyright (
|
6
|
+
# Copyright (c) 2010 Mohammed Morsi <movitto@yahoo.com>
|
7
7
|
#
|
8
8
|
# Permission is hereby granted, free of charge, to any person
|
9
9
|
# obtaining a copy of this software and associated documentation
|
@@ -26,806 +26,13 @@
|
|
26
26
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
27
27
|
# OTHER DEALINGS IN THE SOFTWARE.
|
28
28
|
|
29
|
-
|
30
|
-
require 'logger'
|
29
|
+
lib = File.dirname(__FILE__)
|
31
30
|
|
32
|
-
require '
|
33
|
-
require '
|
34
|
-
require '
|
31
|
+
require lib + '/simrpc/common'
|
32
|
+
require lib + '/simrpc/exceptions'
|
33
|
+
require lib + '/simrpc/schema'
|
34
|
+
require lib + '/simrpc/message'
|
35
|
+
require lib + '/simrpc/qpid_adapter'
|
36
|
+
require lib + '/simrpc/node'
|
35
37
|
|
36
|
-
require 'activesupport' # for inflector
|
37
|
-
|
38
|
-
module Simrpc
|
39
|
-
|
40
|
-
# Logger helper class
|
41
|
-
class Logger
|
42
|
-
private
|
43
|
-
def self._instantiate_logger
|
44
|
-
unless defined? @@logger
|
45
|
-
@@logger = ::Logger.new(STDOUT)
|
46
|
-
@@logger.level = ::Logger::FATAL # FATAL ERROR WARN INFO DEBUG
|
47
|
-
end
|
48
|
-
end
|
49
|
-
public
|
50
|
-
def self.method_missing(method_id, *args)
|
51
|
-
_instantiate_logger
|
52
|
-
@@logger.send(method_id, args)
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
# schema module defines classes / methods to define
|
57
|
-
# a simrpc schema / load it from an xml file
|
58
|
-
module Schema
|
59
|
-
|
60
|
-
Types = [ :str, :int, :float, :bool, :obj, :array ]
|
61
|
-
|
62
|
-
# return true if type is a primitive, else false
|
63
|
-
def is_primitive?(type)
|
64
|
-
return [:str, :int, :float, :bool].include?(type)
|
65
|
-
end
|
66
|
-
module_function :is_primitive?
|
67
|
-
|
68
|
-
# convert primitive type from string
|
69
|
-
def primitive_from_str(type, str)
|
70
|
-
if type == :str
|
71
|
-
return str
|
72
|
-
|
73
|
-
elsif type == :int
|
74
|
-
return str.to_i
|
75
|
-
|
76
|
-
elsif type == :float
|
77
|
-
return str.to_f
|
78
|
-
|
79
|
-
elsif type == :bool
|
80
|
-
return str == "true"
|
81
|
-
|
82
|
-
end
|
83
|
-
end
|
84
|
-
module_function :primitive_from_str
|
85
|
-
|
86
|
-
# FIXME store lengths in binary instead of ascii string
|
87
|
-
|
88
|
-
# Data field defintion containing a type, name, and value.
|
89
|
-
# Optinally an associated class or type may be given.
|
90
|
-
# Associated only valid for :obj and :array types, and
|
91
|
-
# must be set to associated ClassDef or Type (:array only)
|
92
|
-
class DataFieldDef
|
93
|
-
attr_accessor :type, :name, :associated
|
94
|
-
|
95
|
-
# indicates this data field should be ignored if it has a null value
|
96
|
-
attr_accessor :ignore_null
|
97
|
-
|
98
|
-
def initialize(args = {})
|
99
|
-
@type = args[:type] unless args[:type].nil?
|
100
|
-
@name = args[:name] unless args[:name].nil?
|
101
|
-
@associated = args[:associated] unless args[:associated].nil?
|
102
|
-
@ignore_null = !args[:ignore_null].nil? && args[:ignore_null]
|
103
|
-
end
|
104
|
-
|
105
|
-
# helper method to lookup and return the specified class name in the
|
106
|
-
# specified schema
|
107
|
-
def find_class_def(cl_name, schema_def)
|
108
|
-
return schema_def.classes.find { |cl| cl.name == cl_name.to_s } unless cl_name.nil? || schema_def.nil?
|
109
|
-
nil
|
110
|
-
end
|
111
|
-
|
112
|
-
# helper method to lookup and return the class definition corresponding to the associated
|
113
|
-
# attribte in the specified schema def
|
114
|
-
def associated_class_def(schema_def)
|
115
|
-
find_class_def(@associated, schema_def) unless @associated.nil?
|
116
|
-
end
|
117
|
-
|
118
|
-
# convert given value of this data field into a string. Provide schema_def
|
119
|
-
# for :obj or :array data fields associated w/ a non-primitive type. converted_classes
|
120
|
-
# is a recursive helper array used/maintained internally
|
121
|
-
def to_s(value, schema_def = nil, converted_classes = [])
|
122
|
-
if value.nil?
|
123
|
-
return ""
|
124
|
-
elsif Schema::is_primitive?(@type)
|
125
|
-
return value.to_s
|
126
|
-
|
127
|
-
elsif type == :array
|
128
|
-
str = "%04d" % value.size
|
129
|
-
value.each { |val|
|
130
|
-
if Schema::is_primitive?(@associated)
|
131
|
-
str += "%04d" % val.to_s.size
|
132
|
-
str += val.to_s
|
133
|
-
else
|
134
|
-
cl_def = associated_class_def(schema_def)
|
135
|
-
unless cl_def.nil?
|
136
|
-
cl_s = cl_def.to_s(val, schema_def, converted_classes)
|
137
|
-
str += "%04d" % cl_s.size
|
138
|
-
str += cl_s
|
139
|
-
end
|
140
|
-
end
|
141
|
-
}
|
142
|
-
return str
|
143
|
-
|
144
|
-
# elsif @type == :map # TODO
|
145
|
-
|
146
|
-
elsif type == :obj
|
147
|
-
cl_name = ""
|
148
|
-
cl_def = associated_class_def(schema_def)
|
149
|
-
|
150
|
-
# if associated class isn't specified, store the class name,
|
151
|
-
# providing for generic object support
|
152
|
-
if cl_def.nil?
|
153
|
-
cl_name = value.class.to_s.demodulize
|
154
|
-
cl_def = find_class_def(cl_name, schema_def)
|
155
|
-
cl_name = "%04d" % cl_name.size + cl_name
|
156
|
-
end
|
157
|
-
|
158
|
-
return cl_name + cl_def.to_s(value, schema_def, converted_classes) unless cl_def.nil?
|
159
|
-
|
160
|
-
end
|
161
|
-
end
|
162
|
-
|
163
|
-
# convert given string representation of this data field into its original value.
|
164
|
-
# Provide schema_def for :obj or :array data fields associated w/ non-primitive types
|
165
|
-
# # coverted_classes is a recursive helper array used/maintained internally
|
166
|
-
def from_s(str, schema_def = nil, converted_classes = [])
|
167
|
-
if str == ""
|
168
|
-
return nil
|
169
|
-
|
170
|
-
elsif Schema::is_primitive?(@type)
|
171
|
-
return Schema::primitive_from_str(@type, str)
|
172
|
-
|
173
|
-
elsif @type == :array
|
174
|
-
res = []
|
175
|
-
cl_def = associated_class_def(schema_def) unless Schema::is_primitive?(@associated)
|
176
|
-
alen = str[0...4].to_i
|
177
|
-
apos = 4
|
178
|
-
(0...alen).each { |i|
|
179
|
-
elen = str[apos...apos+4].to_i
|
180
|
-
parsed = str[apos+4...apos+4+elen]
|
181
|
-
if Schema::is_primitive?(@associated)
|
182
|
-
p = Schema::primitive_from_str(@associated, parsed)
|
183
|
-
res.push p
|
184
|
-
else
|
185
|
-
res.push cl_def.from_s(parsed, schema_def, converted_classes)
|
186
|
-
end
|
187
|
-
apos = apos+4+elen
|
188
|
-
}
|
189
|
-
#parsed = str[4...4+len]
|
190
|
-
return res
|
191
|
-
|
192
|
-
# elsif @type == :map # TODO
|
193
|
-
|
194
|
-
elsif @type == :obj
|
195
|
-
cl_def = associated_class_def(schema_def)
|
196
|
-
|
197
|
-
# if associated class isn't specified, parse the class name,
|
198
|
-
# providing for generic object support
|
199
|
-
if cl_def.nil?
|
200
|
-
cnlen = str[0...4].to_i
|
201
|
-
cname = str[4...cnlen+4]
|
202
|
-
str = str[cnlen+4...str.size]
|
203
|
-
cl_def = find_class_def(cname, schema_def)
|
204
|
-
end
|
205
|
-
|
206
|
-
return cl_def.from_s(str, schema_def, converted_classes) unless cl_def.nil?
|
207
|
-
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
end
|
212
|
-
|
213
|
-
# A class definition, containing data members.
|
214
|
-
# Right now we build into this the assumption that
|
215
|
-
# the 'name' attribute will share the same name as
|
216
|
-
# the actual class name which data will be mapped to / from
|
217
|
-
# and accessors exist on it corresponding to the names of
|
218
|
-
# each of the members
|
219
|
-
class ClassDef
|
220
|
-
# class name
|
221
|
-
# array of DataFieldDef
|
222
|
-
attr_accessor :name, :members
|
223
|
-
|
224
|
-
def initialize
|
225
|
-
@members = []
|
226
|
-
end
|
227
|
-
|
228
|
-
# convert value instance of class represented by this ClassDef
|
229
|
-
# into a string. schema_def must be provided if this ClassDef
|
230
|
-
# contains any associated class members. converted_classes is
|
231
|
-
# a recursive helper array used internally
|
232
|
-
def to_s(value, schema_def = nil, converted_classes = [])
|
233
|
-
return "O" + ("%04d" % converted_classes.index(value)) if converted_classes.include? value # if we already converted the class, store 'O' + its index
|
234
|
-
converted_classes.push value
|
235
|
-
|
236
|
-
# just encode each member w/ length
|
237
|
-
str = ""
|
238
|
-
unless value.nil?
|
239
|
-
@members.each { |member|
|
240
|
-
mval = value.send(member.name.intern)
|
241
|
-
#mval = value.method(member.name.intern).call # invoke member getter
|
242
|
-
mstr = member.to_s(mval, schema_def, converted_classes)
|
243
|
-
mlen = "%04d" % mstr.size
|
244
|
-
#unless mstr == "" && member.ignore_null
|
245
|
-
str += mlen + mstr
|
246
|
-
#end
|
247
|
-
}
|
248
|
-
end
|
249
|
-
return str
|
250
|
-
end
|
251
|
-
|
252
|
-
# convert string instance of class represented by this ClassDef
|
253
|
-
# into actual class instance. schema_def must be provided if this
|
254
|
-
# ClassDef contains any associated class members. converted_classes
|
255
|
-
# is a recurvice helper array used internally.
|
256
|
-
def from_s(str, schema_def = nil, converted_classes = [])
|
257
|
-
return nil if str == ""
|
258
|
-
|
259
|
-
mpos = 0
|
260
|
-
if str[mpos,1] == "O" # if we already converted the class, simply return that
|
261
|
-
return converted_classes[str[1...5].to_i]
|
262
|
-
end
|
263
|
-
|
264
|
-
# construct an instance of the class
|
265
|
-
obj = Object.module_eval("::#{@name}", __FILE__, __LINE__).new
|
266
|
-
converted_classes.push obj
|
267
|
-
|
268
|
-
# decode each member
|
269
|
-
@members.each { |member|
|
270
|
-
mlen = str[mpos...mpos+4].to_i
|
271
|
-
parsed = str[mpos+4...mpos+4+mlen]
|
272
|
-
parsed_o = member.from_s(parsed, schema_def, converted_classes)
|
273
|
-
unless parsed_o.nil? && member.ignore_null
|
274
|
-
obj.send(member.name + "=", parsed_o) # invoke member setter
|
275
|
-
end
|
276
|
-
mpos = mpos+4+mlen
|
277
|
-
}
|
278
|
-
|
279
|
-
return obj
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
# method definition, containing parameters
|
284
|
-
# and return values. May optionally have
|
285
|
-
# a handler to be invoked when a remote
|
286
|
-
# entity invokes this method
|
287
|
-
class MethodDef
|
288
|
-
attr_accessor :name
|
289
|
-
|
290
|
-
# both are arrays of DataFieldDef
|
291
|
-
attr_accessor :parameters, :return_values
|
292
|
-
|
293
|
-
# should be a callable entity that takes
|
294
|
-
# the specified parameters, and returns
|
295
|
-
# the specified return values
|
296
|
-
attr_accessor :handler
|
297
|
-
|
298
|
-
def initialize
|
299
|
-
@parameters = []
|
300
|
-
@return_values = []
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
# schema defintion including all defined classes and methods
|
305
|
-
class SchemaDef
|
306
|
-
# array of ClassDef
|
307
|
-
attr_accessor :classes
|
308
|
-
|
309
|
-
# array of MethodDef
|
310
|
-
attr_accessor :methods
|
311
|
-
|
312
|
-
def initialize
|
313
|
-
@classes = []
|
314
|
-
@methods = []
|
315
|
-
end
|
316
|
-
end
|
317
|
-
|
318
|
-
# parse classes, methods, and data out of a xml definition
|
319
|
-
class Parser
|
320
|
-
|
321
|
-
# Parse and return a SchemaDef.
|
322
|
-
# Specify :schema argument containing xml schema to parse
|
323
|
-
# or :file containing location of file containing xml schema
|
324
|
-
def self.parse(args = {})
|
325
|
-
if(!args[:schema].nil?)
|
326
|
-
schema = args[:schema]
|
327
|
-
elsif(!args[:file].nil?)
|
328
|
-
schema = File.new(args[:file], "r")
|
329
|
-
end
|
330
|
-
|
331
|
-
schema_def = SchemaDef.new
|
332
|
-
|
333
|
-
unless schema.nil? || schema == ""
|
334
|
-
doc = REXML::Document.new(schema)
|
335
|
-
# grab each method definition
|
336
|
-
doc.elements.each('schema/method') do |ele|
|
337
|
-
method = MethodDef.new
|
338
|
-
method.name = ele.attributes["name"]
|
339
|
-
Logger.debug "parsed schema method #{method.name}"
|
340
|
-
|
341
|
-
ele.elements.each("param") do |param|
|
342
|
-
param = _parse_data_def(param)
|
343
|
-
method.parameters.push param
|
344
|
-
Logger.debug " parameter #{param.name}"
|
345
|
-
end
|
346
|
-
|
347
|
-
ele.elements.each("return_value") do |rv|
|
348
|
-
rv = _parse_data_def(rv)
|
349
|
-
method.return_values.push rv
|
350
|
-
Logger.debug " return_value #{rv.name}"
|
351
|
-
end
|
352
|
-
|
353
|
-
schema_def.methods.push method
|
354
|
-
end
|
355
|
-
|
356
|
-
# grab each class definition
|
357
|
-
doc.elements.each('schema/class') do |ele|
|
358
|
-
cl = ClassDef.new
|
359
|
-
cl.name = ele.attributes["name"]
|
360
|
-
Logger.debug "parsed schema class #{cl.name}"
|
361
|
-
|
362
|
-
ele.elements.each("member") do |mem|
|
363
|
-
mem = _parse_data_def(mem)
|
364
|
-
cl.members.push mem
|
365
|
-
Logger.debug " member #{mem.name}"
|
366
|
-
end
|
367
|
-
|
368
|
-
schema_def.classes.push cl
|
369
|
-
end
|
370
|
-
end
|
371
|
-
return schema_def
|
372
|
-
end
|
373
|
-
|
374
|
-
private
|
375
|
-
# helper method to parse a DataFieldDef out of an Element
|
376
|
-
def self._parse_data_def(element)
|
377
|
-
data_field = DataFieldDef.new
|
378
|
-
data_field.type = element.attributes["type"].intern
|
379
|
-
data_field.name = element.attributes["name"]
|
380
|
-
data_field.associated = element.attributes["associated"].intern unless element.attributes["associated"].nil?
|
381
|
-
data_field.ignore_null = true if element.attributes.include? "ignore_null"
|
382
|
-
return data_field
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
|
-
end
|
387
|
-
|
388
|
-
# the message module provides the Message definition,
|
389
|
-
# including a header with routing info and a body
|
390
|
-
# with any number of data fields
|
391
|
-
module Message
|
392
|
-
|
393
|
-
# Simrpc::Message formatter helper module
|
394
|
-
class Formatter
|
395
|
-
|
396
|
-
# helper method to format a data field,
|
397
|
-
# prepending a fixed size to it
|
398
|
-
def self.format_with_size(data)
|
399
|
-
# currently size is set to a 8 digit int
|
400
|
-
len = "%08d" % data.to_s.size
|
401
|
-
len + data.to_s
|
402
|
-
end
|
403
|
-
|
404
|
-
# helper method to parse a data field
|
405
|
-
# off the front of a data sequence, using the
|
406
|
-
# formatted size. Returns parsed data field
|
407
|
-
# and remaining data sequence. If optional
|
408
|
-
# class is given, the from_s method will
|
409
|
-
# be invoked w/ the parsed data field and
|
410
|
-
# returned with the remaining data sequence
|
411
|
-
# instead
|
412
|
-
def self.parse_from_formatted(data, data_class = nil)
|
413
|
-
len = data[0...8].to_i
|
414
|
-
parsed = data[8...8+len]
|
415
|
-
remaining = data[8+len...data.size]
|
416
|
-
return parsed, remaining if data_class.nil?
|
417
|
-
return data_class.from_s(parsed), remaining
|
418
|
-
end
|
419
|
-
end
|
420
|
-
|
421
|
-
# a single field trasnmitted via a message,
|
422
|
-
# containing a key / value pair
|
423
|
-
class Field
|
424
|
-
attr_accessor :name, :value
|
425
|
-
|
426
|
-
def initialize(args = {})
|
427
|
-
@name = args[:name].nil? ? "" : args[:name]
|
428
|
-
@value = args[:value].nil? ? "" : args[:value]
|
429
|
-
end
|
430
|
-
|
431
|
-
def to_s
|
432
|
-
Formatter::format_with_size(@name) + Formatter::format_with_size(@value)
|
433
|
-
end
|
434
|
-
|
435
|
-
def self.from_s(data)
|
436
|
-
field = Field.new
|
437
|
-
field.name, data = Formatter::parse_from_formatted(data)
|
438
|
-
field.value, data = Formatter::parse_from_formatted(data)
|
439
|
-
return field
|
440
|
-
end
|
441
|
-
end
|
442
|
-
|
443
|
-
# header contains various descriptive properies
|
444
|
-
# about a message
|
445
|
-
class Header
|
446
|
-
attr_accessor :type, :target
|
447
|
-
|
448
|
-
def initialize(args = {})
|
449
|
-
@type = args[:type].nil? ? "" : args[:type]
|
450
|
-
@target = args[:target].nil? ? "" : args[:target]
|
451
|
-
end
|
452
|
-
|
453
|
-
def to_s
|
454
|
-
Formatter::format_with_size(@type) + Formatter::format_with_size(@target)
|
455
|
-
end
|
456
|
-
|
457
|
-
def self.from_s(data)
|
458
|
-
header = Header.new
|
459
|
-
header.type, data = Formatter::parse_from_formatted(data)
|
460
|
-
header.target, data = Formatter::parse_from_formatted(data)
|
461
|
-
return header
|
462
|
-
end
|
463
|
-
end
|
464
|
-
|
465
|
-
# body consists of a list of data fields
|
466
|
-
class Body
|
467
|
-
attr_accessor :fields
|
468
|
-
|
469
|
-
def initialize
|
470
|
-
@fields = []
|
471
|
-
end
|
472
|
-
|
473
|
-
def to_s
|
474
|
-
s = ''
|
475
|
-
@fields.each { |field|
|
476
|
-
fs = field.to_s
|
477
|
-
s += Formatter::format_with_size(fs)
|
478
|
-
}
|
479
|
-
return s
|
480
|
-
end
|
481
|
-
|
482
|
-
def self.from_s(data)
|
483
|
-
body = Body.new
|
484
|
-
while(data != "")
|
485
|
-
field, data = Formatter::parse_from_formatted(data)
|
486
|
-
field = Field.from_s field
|
487
|
-
body.fields.push field
|
488
|
-
end
|
489
|
-
return body
|
490
|
-
end
|
491
|
-
end
|
492
|
-
|
493
|
-
# message contains a header / body
|
494
|
-
class Message
|
495
|
-
attr_accessor :header, :body
|
496
|
-
|
497
|
-
def initialize
|
498
|
-
@header = Header.new
|
499
|
-
@body = Body.new
|
500
|
-
end
|
501
|
-
|
502
|
-
def to_s
|
503
|
-
Formatter::format_with_size(@header) + Formatter::format_with_size(@body)
|
504
|
-
end
|
505
|
-
|
506
|
-
def self.from_s(data)
|
507
|
-
message = Message.new
|
508
|
-
message.header, data = Formatter::parse_from_formatted(data, Header)
|
509
|
-
message.body, data = Formatter::parse_from_formatted(data, Body)
|
510
|
-
return message
|
511
|
-
end
|
512
|
-
end
|
513
|
-
|
514
|
-
end
|
515
|
-
|
516
|
-
# The QpidAdapter module implements the simrpc qpid subsystem, providing
|
517
|
-
# a convenient way to access qpid constructs
|
518
|
-
module QpidAdapter
|
519
|
-
|
520
|
-
# Simrpc::Qpid::Node class, represents an enpoint on a qpid
|
521
|
-
# network which has its own exchange and queue which it listens on
|
522
|
-
class Node
|
523
|
-
private
|
524
|
-
# helper method to generate a random id
|
525
|
-
def gen_uuid
|
526
|
-
["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") %
|
527
|
-
Array.new(16) {|x| rand(0xff) }
|
528
|
-
end
|
529
|
-
|
530
|
-
public
|
531
|
-
# a node can have children nodes mapped to by keys
|
532
|
-
attr_accessor :children
|
533
|
-
|
534
|
-
# node always has a node id
|
535
|
-
attr_reader :node_id
|
536
|
-
|
537
|
-
# create the qpid base connection with the specified broker / port
|
538
|
-
# or config file. Then establish exchange and queue and start listening
|
539
|
-
# for requests.
|
540
|
-
#
|
541
|
-
# specify :broker and :port arguments to directly connect to those
|
542
|
-
# specify :config argument to use that yml file
|
543
|
-
# specify MOTEL_AMQP_CONF environment variable to use that yml file
|
544
|
-
# specify :id parameter to set id, else it will be set to a uuid just created
|
545
|
-
def initialize(args = {})
|
546
|
-
# if no id specified generate a new uuid
|
547
|
-
@node_id = args[:id].nil? ? gen_uuid : args[:id]
|
548
|
-
|
549
|
-
# we generate a random session id
|
550
|
-
@session_id = gen_uuid
|
551
|
-
|
552
|
-
# get the broker/port
|
553
|
-
broker = args[:broker].nil? ? "localhost" : args[:broker]
|
554
|
-
port = args[:port].nil? ? 5672 : args[:port]
|
555
|
-
|
556
|
-
if (broker.nil? || port.nil?) && args.has_key?(:config)
|
557
|
-
config =
|
558
|
-
amqpconfig = YAML::load(File.open(args[:config]))
|
559
|
-
broker = amqpconfig["broker"] if broker.nil?
|
560
|
-
port = amqpconfig["port"] if port.nil?
|
561
|
-
end
|
562
|
-
|
563
|
-
### create underlying tcp connection
|
564
|
-
@conn = Qpid::Connection.new(TCPSocket.new(broker,port))
|
565
|
-
@conn.start
|
566
|
-
|
567
|
-
### connect to qpid broker
|
568
|
-
@ssn = @conn.session(@session_id)
|
569
|
-
|
570
|
-
@children = {}
|
571
|
-
|
572
|
-
@accept_lock = Semaphore.new(1)
|
573
|
-
|
574
|
-
# qpid constructs that will be created for node
|
575
|
-
@exchange = args[:exchange].nil? ? @node_id.to_s + "-exchange" : args[:exchange]
|
576
|
-
@queue = args[:queue].nil? ? @node_id.to_s + "-queue" : args[:queue]
|
577
|
-
@local_queue = args[:local_queue].nil? ? @node_id.to_s + "-local-queue" : args[:local_queue]
|
578
|
-
@routing_key = @queue
|
579
|
-
|
580
|
-
Logger.warn "creating qpid exchange #{@exchange} queue #{@queue} binding_key #{@routing_key}"
|
581
|
-
|
582
|
-
if @ssn.exchange_query(@exchange).not_found
|
583
|
-
@ssn.exchange_declare(@exchange, :type => "direct")
|
584
|
-
end
|
585
|
-
|
586
|
-
if @ssn.queue_query(@queue).queue.nil?
|
587
|
-
@ssn.queue_declare(@queue)
|
588
|
-
end
|
589
|
-
|
590
|
-
@ssn.exchange_bind(:exchange => @exchange,
|
591
|
-
:queue => @queue,
|
592
|
-
:binding_key => @routing_key)
|
593
|
-
end
|
594
|
-
|
595
|
-
# Instruct Node to start accepting requests asynchronously and immediately return.
|
596
|
-
# handler must be callable and take node, msg, respond_to arguments, corresponding to
|
597
|
-
# 'self', the message received', and the routing_key which to send any response.
|
598
|
-
def async_accept(&handler)
|
599
|
-
# TODO permit a QpidNode to accept messages from multiple exchanges/queues
|
600
|
-
@accept_lock.wait
|
601
|
-
|
602
|
-
# subscribe to the queue
|
603
|
-
@ssn.message_subscribe(:destination => @local_queue,
|
604
|
-
:queue => @queue,
|
605
|
-
:accept_mode => @ssn.message_accept_mode.none)
|
606
|
-
@incoming = @ssn.incoming(@local_queue)
|
607
|
-
@incoming.start
|
608
|
-
|
609
|
-
Logger.warn "listening for messages on #{@queue}"
|
610
|
-
|
611
|
-
# start receiving messages
|
612
|
-
@incoming.listen{ |msg|
|
613
|
-
Logger.info "queue #{@queue} received message #{msg.body.to_s.size} #{msg.body}"
|
614
|
-
reply_to = msg.get(:message_properties).reply_to.routing_key
|
615
|
-
handler.call(self, msg.body, reply_to)
|
616
|
-
}
|
617
|
-
end
|
618
|
-
|
619
|
-
# block until accept operation is complete
|
620
|
-
def join
|
621
|
-
@accept_lock.wait
|
622
|
-
end
|
623
|
-
|
624
|
-
# instructs QpidServer to stop accepting, blocking
|
625
|
-
# untill all accepting operations have terminated
|
626
|
-
def terminate
|
627
|
-
Logger.warn "terminating qpid session"
|
628
|
-
unless @incoming.nil?
|
629
|
-
@incoming.stop
|
630
|
-
@incoming.close
|
631
|
-
@accept_lock.signal
|
632
|
-
end
|
633
|
-
@ssn.close
|
634
|
-
# TODO undefine the @queue/@exchange
|
635
|
-
end
|
636
|
-
|
637
|
-
# send a message to the specified routing_key
|
638
|
-
def send_message(routing_key, message)
|
639
|
-
dp = @ssn.delivery_properties(:routing_key => routing_key)
|
640
|
-
mp = @ssn.message_properties( :content_type => "text/plain")
|
641
|
-
rp = @ssn.message_properties( :reply_to =>
|
642
|
-
@ssn.reply_to(@exchange, @routing_key))
|
643
|
-
msg = Qpid::Message.new(dp, mp, rp, message.to_s)
|
644
|
-
|
645
|
-
Logger.warn "sending qpid message #{msg.body} to #{routing_key}"
|
646
|
-
|
647
|
-
# send it
|
648
|
-
@ssn.message_transfer(:message => msg)
|
649
|
-
end
|
650
|
-
|
651
|
-
end
|
652
|
-
|
653
|
-
end
|
654
|
-
|
655
|
-
# Simrpc Method Message Controller, generates and handles method messages
|
656
|
-
class MethodMessageController
|
657
|
-
public
|
658
|
-
# initialize with a specified schema definition
|
659
|
-
def initialize(schema_def)
|
660
|
-
@schema_def = schema_def
|
661
|
-
end
|
662
|
-
|
663
|
-
# generate new new method message, setting the message
|
664
|
-
# target to the specified method name, and setting the fields
|
665
|
-
# on the message to the method arguments
|
666
|
-
def generate(method_name, args)
|
667
|
-
@schema_def.methods.each { |method|
|
668
|
-
if method.name == method_name
|
669
|
-
msg = Message::Message.new
|
670
|
-
msg.header.type = 'request'
|
671
|
-
msg.header.target = method.name
|
672
|
-
|
673
|
-
# loop through each param, convering corresponding
|
674
|
-
# argument to message field and adding it to msg
|
675
|
-
i = 0
|
676
|
-
method.parameters.each { |param|
|
677
|
-
field = Message::Field.new
|
678
|
-
field.name = param.name
|
679
|
-
field.value = param.to_s(args[i], @schema_def)
|
680
|
-
msg.body.fields.push field
|
681
|
-
i += 1
|
682
|
-
}
|
683
|
-
|
684
|
-
return msg
|
685
|
-
end
|
686
|
-
}
|
687
|
-
return nil
|
688
|
-
end
|
689
|
-
|
690
|
-
# should be invoked when a message is received,
|
691
|
-
# takes a message, converts it into a method call, and calls the corresponding
|
692
|
-
# handler in the provided schema. Takes return arguments and sends back to caller
|
693
|
-
def message_received(node, message, reply_to)
|
694
|
-
message = Message::Message::from_s(message)
|
695
|
-
@schema_def.methods.each { |method|
|
696
|
-
|
697
|
-
if method.name == message.header.target
|
698
|
-
Logger.info "received method #{method.name} message "
|
699
|
-
|
700
|
-
# for request messages, dispatch to method handler
|
701
|
-
if message.header.type != 'response' && method.handler != nil
|
702
|
-
# order the params
|
703
|
-
params = []
|
704
|
-
method.parameters.each { |data_field|
|
705
|
-
value_field = message.body.fields.find { |f| f.name == data_field.name }
|
706
|
-
params.push data_field.from_s(value_field.value, @schema_def) unless value_field.nil? # TODO what if value_field is nil
|
707
|
-
}
|
708
|
-
|
709
|
-
Logger.info "invoking #{method.name} handler "
|
710
|
-
|
711
|
-
# invoke method handler
|
712
|
-
return_values = method.handler.call(*params) # FIXME handlers can't use 'return' as this will fall through here
|
713
|
-
# FIXME throw a catch block around this call to catch all handler exceptions
|
714
|
-
return_values = [return_values] unless return_values.is_a? Array
|
715
|
-
|
716
|
-
# if method returns no values, do not return response
|
717
|
-
unless method.return_values.size == 0
|
718
|
-
|
719
|
-
# consruct and send response message using return values
|
720
|
-
response = Message::Message.new
|
721
|
-
response.header.type = 'response'
|
722
|
-
response.header.target = method.name
|
723
|
-
(0...method.return_values.size).each { |rvi|
|
724
|
-
field = Message::Field.new
|
725
|
-
field.name = method.return_values[rvi].name
|
726
|
-
field_def = method.return_values.find { |rv| rv.name == field.name }
|
727
|
-
field.value = field_def.to_s(return_values[rvi], @schema_def) unless field_def.nil? # TODO what if field_def is nil
|
728
|
-
response.body.fields.push field
|
729
|
-
}
|
730
|
-
Logger.info "responding to #{reply_to}"
|
731
|
-
node.send_message(reply_to, response)
|
732
|
-
|
733
|
-
end
|
734
|
-
|
735
|
-
# for response values just return converted return values
|
736
|
-
else
|
737
|
-
results = []
|
738
|
-
method.return_values.each { |data_field|
|
739
|
-
value_field = message.body.fields.find { |f| f.name == data_field.name }
|
740
|
-
results.push data_field.from_s(value_field.value, @schema_def) unless value_field.nil? # TODO what if value_field is nil
|
741
|
-
}
|
742
|
-
return results
|
743
|
-
end
|
744
|
-
end
|
745
|
-
}
|
746
|
-
end
|
747
|
-
end
|
748
|
-
|
749
|
-
# Simrpc Node represents ths main api which to communicate and send/listen for data.
|
750
|
-
class Node
|
751
|
-
|
752
|
-
# Instantiate it w/ a specified id
|
753
|
-
# or one will be autogenerated. Specify schema (or location) containing
|
754
|
-
# data and methods which to invoke and/or handle. Optionally specify
|
755
|
-
# a remote destination which to send new messages to. Automatically listens
|
756
|
-
# for incoming messages.
|
757
|
-
def initialize(args = {})
|
758
|
-
@id = args[:id] if args.has_key? :id
|
759
|
-
@schema = args[:schema]
|
760
|
-
@schema_file = args[:schema_file]
|
761
|
-
@destination = args[:destination]
|
762
|
-
|
763
|
-
if !@schema.nil?
|
764
|
-
@schema_def = Schema::Parser.parse(:schema => @schema)
|
765
|
-
elsif !@schema_file.nil?
|
766
|
-
@schema_def = Schema::Parser.parse(:file => @schema_file)
|
767
|
-
end
|
768
|
-
raise ArgumentError, "schema_def cannot be nil" if @schema_def.nil?
|
769
|
-
@mmc = MethodMessageController.new(@schema_def)
|
770
|
-
@message_lock = Semaphore.new(1)
|
771
|
-
@message_lock.wait
|
772
|
-
|
773
|
-
@qpid_node = QpidAdapter::Node.new(:id => @id)
|
774
|
-
@qpid_node.async_accept { |node, msg, reply_to|
|
775
|
-
results = @mmc.message_received(node, msg, reply_to)
|
776
|
-
message_received(results)
|
777
|
-
}
|
778
|
-
end
|
779
|
-
|
780
|
-
def id
|
781
|
-
return @id unless @id.nil?
|
782
|
-
return @qpid_node.node_id
|
783
|
-
end
|
784
|
-
|
785
|
-
# implements, message_received callback to be notified when qpid receives a message
|
786
|
-
def message_received(results)
|
787
|
-
@message_results = results
|
788
|
-
@message_lock.signal
|
789
|
-
end
|
790
|
-
|
791
|
-
# wait until the node is no longer accepting messages
|
792
|
-
def join
|
793
|
-
@qpid_node.join
|
794
|
-
end
|
795
|
-
|
796
|
-
# add a handler which to invoke when an schema method is invoked
|
797
|
-
def handle_method(method, &handler)
|
798
|
-
@schema_def.methods.each { |smethod|
|
799
|
-
if smethod.name == method.to_s
|
800
|
-
smethod.handler = handler
|
801
|
-
break
|
802
|
-
end
|
803
|
-
}
|
804
|
-
end
|
805
|
-
|
806
|
-
# send method request to remote destination w/ the specified args
|
807
|
-
def send_method(method_name, destination, *args)
|
808
|
-
# generate and send new method message
|
809
|
-
msg = @mmc.generate(method_name, args)
|
810
|
-
@qpid_node.send_message(destination + "-queue", msg)
|
811
|
-
|
812
|
-
# FIXME race condition if response is received b4 wait is invoked
|
813
|
-
|
814
|
-
# block if we are expecting return values
|
815
|
-
if @schema_def.methods.find{|m| m.name == method_name}.return_values.size != 0
|
816
|
-
@message_lock.wait # block until response received
|
817
|
-
|
818
|
-
# return return values
|
819
|
-
#@message_received.body.fields.collect { |f| f.value }
|
820
|
-
return *@message_results
|
821
|
-
end
|
822
|
-
end
|
823
|
-
|
824
|
-
# can invoke schema methods directly on Node instances, this will catch
|
825
|
-
# them and send them onto the destination
|
826
|
-
def method_missing(method_id, *args)
|
827
|
-
send_method(method_id.to_s, @destination, *args)
|
828
|
-
end
|
829
|
-
end
|
830
|
-
|
831
|
-
end # module Simrpc
|
38
|
+
require 'activesupport' # for inflector
|