simrpc 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/LICENSE +22 -0
  2. data/README +25 -0
  3. data/lib/semaphore.rb +57 -0
  4. data/lib/simrpc.rb +831 -0
  5. metadata +69 -0
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 Mohammed Morsi <movitto@yahoo.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,25 @@
1
+ Simrpc - A Simple RPC library using AMQP as the transport mechanism
2
+ Copyright (C) 2009 Mohammed Morsi <movitto@yahoo.com>
3
+ Simrpc is made available under the MIT License
4
+
5
+ I. Intro
6
+ =====================================================
7
+ Simrpc is a simple Ruby module for rpc communication,
8
+ that uses Apache QPID as the transport mechanism.
9
+
10
+ II. Using
11
+ =====================================================
12
+ Until I write a tutorial, the simrpc test suite
13
+ should detail common usage. The Simrpc API
14
+ http://projects.morsi.org/simrpc/doc/ (generated
15
+ from rdoc) details the complete interface
16
+
17
+ III. TODO
18
+ =====================================================
19
+ Efficiency improvements
20
+ C++ rewrite, w/ Ruby and other wrappers, so
21
+ Simrpc can be used as across-languages
22
+
23
+ IV. Authors
24
+ =====================================================
25
+ Mohammed Morsi <movitto@yahoo.com>
data/lib/semaphore.rb ADDED
@@ -0,0 +1,57 @@
1
+ # $Id: semaphore.rb,v 1.2 2003/03/15 20:10:10 fukumoto Exp $
2
+ #
3
+ # Copied unmodified from:
4
+ # http://www.imasy.or.jp/~fukumoto/ruby/semaphore.rb
5
+ # Licensed under The Ruby License:
6
+ # http://raa.ruby-lang.org/project/semaphore/
7
+
8
+ class CountingSemaphore
9
+
10
+ def initialize(initvalue = 0)
11
+ @counter = initvalue
12
+ @waiting_list = []
13
+ end
14
+
15
+ def wait
16
+ Thread.critical = true
17
+ if (@counter -= 1) < 0
18
+ @waiting_list.push(Thread.current)
19
+ Thread.stop
20
+ end
21
+ self
22
+ ensure
23
+ Thread.critical = false
24
+ end
25
+
26
+ def signal
27
+ Thread.critical = true
28
+ begin
29
+ if (@counter += 1) <= 0
30
+ t = @waiting_list.shift
31
+ t.wakeup if t
32
+ end
33
+ rescue ThreadError
34
+ retry
35
+ end
36
+ self
37
+ ensure
38
+ Thread.critical = false
39
+ end
40
+
41
+ alias down wait
42
+ alias up signal
43
+ alias P wait
44
+ alias V signal
45
+
46
+ def exclusive
47
+ wait
48
+ yield
49
+ ensure
50
+ signal
51
+ end
52
+
53
+ alias synchronize exclusive
54
+
55
+ end
56
+
57
+ Semaphore = CountingSemaphore
data/lib/simrpc.rb ADDED
@@ -0,0 +1,831 @@
1
+ # simrpc - simple remote procedure call library
2
+ #
3
+ # Implements a simple to use method based RPC for ruby
4
+ # built upon Apache Qpid
5
+ #
6
+ # Copyright (C) 2009 Mohammed Morsi <movitto@yahoo.com>
7
+ #
8
+ # Permission is hereby granted, free of charge, to any person
9
+ # obtaining a copy of this software and associated documentation
10
+ # files (the "Software"), to deal in the Software without
11
+ # restriction, including without limitation the rights to use,
12
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ # copies of the Software, and to permit persons to whom the
14
+ # Software is furnished to do so, subject to the following
15
+ # conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be
18
+ # included in all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
+ # OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ require 'rexml/document'
30
+ require 'logger'
31
+
32
+ require 'qpid'
33
+ require 'socket'
34
+ require 'semaphore'
35
+
36
+ require 'activesupport' # for inflector demodulize
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
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simrpc
3
+ version: !ruby/object:Gem::Version
4
+ version: "0.1"
5
+ platform: ruby
6
+ authors:
7
+ - Mohammed Morsi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-19 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: qpid
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.1.1
24
+ version:
25
+ description: simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.
26
+ email: movitto@yahoo.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README
33
+ - LICENSE
34
+ files:
35
+ - lib/semaphore.rb
36
+ - lib/simrpc.rb
37
+ - README
38
+ - LICENSE
39
+ has_rdoc: true
40
+ homepage: http://projects.morsi.org/Simrpc
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --inline-source
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 1.3.1
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.3.5
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: simrpc is a simple Ruby module for rpc communication, using Apache QPID as the transport mechanism.
68
+ test_files: []
69
+