├── .gitignore ├── init.rb ├── .github └── FUNDING.yml ├── Gemfile ├── Gemfile.lock ├── lib ├── brb │ ├── exception.rb │ ├── logger.rb │ ├── service.rb │ ├── event_machine.rb │ ├── request.rb │ ├── tunnel.rb │ └── tunnel │ │ └── shared.rb └── brb.rb ├── CHANGELOG.rdoc ├── Rakefile ├── spec ├── brb │ ├── brb_service_spec.rb │ ├── brb_logger_spec.rb │ ├── brb_massive_usage_spec.rb │ └── brb_tunnel_spec.rb └── spec_helper.rb ├── brb.gemspec ├── examples ├── simple_core.rb └── simple_client.rb ├── MIT-LICENSE └── README.rdoc /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | doc -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require File.join(File.dirname(__FILE__), 'lib', 'brb.rb') -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: 'https://triplebyte.com/a/ZBAdDFG/brb' 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | source 'http://gems.github.com' 3 | 4 | gem "rspec", "~> 1.3.2" 5 | gem "eventmachine" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | remote: http://gems.github.com/ 4 | specs: 5 | eventmachine (0.12.10) 6 | rspec (1.3.2) 7 | 8 | PLATFORMS 9 | ruby 10 | 11 | DEPENDENCIES 12 | eventmachine 13 | rspec (~> 1.3.2) 14 | -------------------------------------------------------------------------------- /lib/brb/exception.rb: -------------------------------------------------------------------------------- 1 | # Future BrB custom exceptions will come here 2 | class BrBException < Exception 3 | end 4 | 5 | class BrBCallbackWithBlockingMethodException < BrBException 6 | def initialize 7 | super('Out request can not be blocking and have a callback at the same time !') 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/brb.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'brb', 'logger.rb') 2 | require File.join(File.dirname(__FILE__), 'brb', 'exception.rb') 3 | require File.join(File.dirname(__FILE__), 'brb', 'event_machine.rb') 4 | require File.join(File.dirname(__FILE__), 'brb', 'service.rb') 5 | require File.join(File.dirname(__FILE__), 'brb', 'tunnel.rb') -------------------------------------------------------------------------------- /CHANGELOG.rdoc: -------------------------------------------------------------------------------- 1 | 0.3.0 (May 21, 2010) 2 | 3 | * Added Callback functionality through block 4 | * Added Callback example 5 | * Little spec refactoring 6 | 7 | 0.2.2 (Apr 21, 2010) 8 | 9 | * Change silent option to verbose 10 | 11 | 0.2.1 (Apr 16, 2010) 12 | 13 | * Automatically start EM if not started 14 | * Deprecate the usage of BrB::Service.instance => use BrB::Service instead 15 | 16 | 0.2.0 (Apr 16, 2010) 17 | 18 | * Releasing gem and wiki 19 | 20 | 0.1.0 (Feb 01, 2009) 21 | 22 | * Initial release -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'spec/rake/spectask' 4 | 5 | spec_files = Rake::FileList["spec/**/*_spec.rb"] 6 | 7 | desc "Run specs for current Rails version" 8 | Spec::Rake::SpecTask.new do |t| 9 | t.spec_files = spec_files 10 | t.spec_opts = ["-c --format specdoc"] 11 | end 12 | 13 | task :default => :spec 14 | 15 | desc "Run simple core" 16 | task :simple_core_example do 17 | require 'examples/simple_core' 18 | end 19 | 20 | desc "Run simple client (call simple_core_before)" 21 | task :simple_client_example do 22 | require 'examples/simple_client' 23 | end -------------------------------------------------------------------------------- /spec/brb/brb_service_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe :brb_service do 4 | before(:all) do 5 | @brb = BrB::Service 6 | @brb.stop_service 7 | @brb_test = BrBTest.new 8 | open_service(self) 9 | end 10 | 11 | # Start the service 12 | it "should open a service on localhost:6200" do 13 | @brb.uri.should_not be_nil 14 | end 15 | 16 | # Finally, stop the service 17 | it "should stop the service" do 18 | @brb.stop_service 19 | @brb.uri.should be_nil 20 | end 21 | 22 | it "should start again the service after a stop" do 23 | open_service(self) 24 | @brb.stop_service 25 | open_service(self) 26 | @brb.uri.should_not be_nil 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /brb.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "brb" 3 | s.version = "0.3.1" 4 | s.author = "Guillaume Luccisano" 5 | s.email = "guillaume.luccisano@gmail.com" 6 | s.homepage = "http://github.com/kwi/BrB" 7 | s.summary = "BrB is a simple, fully transparent and extremely fast interface for doing simple distributed ruby" 8 | s.description = "BrB is a simple, fully transparent and extremely fast interface for doing simple distributed ruby and message passing" 9 | s.requirements << 'eventmachine' 10 | 11 | s.add_dependency('eventmachine', '> 0.12') 12 | 13 | 14 | s.files = Dir["{examples,lib,spec}/**/*", "[A-Z]*", "init.rb"] 15 | s.require_path = "lib" 16 | 17 | s.rubyforge_project = s.name 18 | s.required_rubygems_version = ">= 1.3.4" 19 | end -------------------------------------------------------------------------------- /lib/brb/logger.rb: -------------------------------------------------------------------------------- 1 | require "logger" 2 | 3 | module BrB 4 | class << self 5 | 6 | # returns the default logger instance 7 | def default_logger 8 | Logger.new(STDOUT) 9 | end 10 | 11 | # set a custom logger instance 12 | def logger=(custom_logger) 13 | @@logger = custom_logger 14 | end 15 | 16 | # returns the logger instance 17 | def logger 18 | # use default logger if no custom logger is set 19 | @@logger = default_logger unless defined? @@logger 20 | 21 | # this overwrites the original method with a static definition 22 | eval %Q{ 23 | def logger 24 | @@logger 25 | end 26 | } 27 | @@logger 28 | end 29 | end 30 | 31 | # alias to BrB.logger 32 | def logger 33 | BrB.logger 34 | end 35 | end -------------------------------------------------------------------------------- /spec/brb/brb_logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class CustomLogger 4 | attr_accessor :level, :history 5 | def initialize 6 | @history = [] 7 | end 8 | def info(msg) 9 | @history << msg 10 | end 11 | alias :error :info 12 | alias :warn :info 13 | alias :debug :info 14 | end 15 | 16 | describe :brb_logger do 17 | before(:each) do 18 | @original_logger = BrB.logger 19 | end 20 | 21 | after(:each) do 22 | BrB.logger = @original_logger 23 | end 24 | 25 | it 'should be assigned a default logger' do 26 | BrB.logger.should_not be_nil 27 | BrB.logger.class.should == Logger 28 | end 29 | 30 | it 'should be possible to use a custom logger' do 31 | BrB.logger = CustomLogger.new 32 | BrB.logger.info('foo') 33 | BrB.logger.history.last.should == 'foo' 34 | end 35 | end -------------------------------------------------------------------------------- /examples/simple_core.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '../init.rb') 2 | 3 | class ExposedCoreObject 4 | 5 | def simple_api_method 6 | puts "#{Thread.current} > In simple api method, now sleeping" 7 | yield if block_given? 8 | sleep 1 9 | puts "#{Thread.current} > Done sleeping in simple api method, return" 10 | return 'OK' 11 | end 12 | 13 | def simple_long_api_method 14 | puts "#{Thread.current} > In simple long api method, now sleeping" 15 | sleep 10 16 | puts "#{Thread.current} > Done sleeping in long api method, return" 17 | return 'OK LONG' 18 | end 19 | 20 | end 21 | 22 | Thread.abort_on_exception = true 23 | 24 | port = 5555 25 | host = 'localhost' 26 | 27 | puts " > Starting the core on brb://#{host}:#{port}" 28 | BrB::Service.start_service(:object => ExposedCoreObject.new, :verbose => true, :host => host, :port => port) 29 | EM.reactor_thread.join -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Guillaume Luccisano - g-mai|: guillaume.luccisano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /examples/simple_client.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '../init.rb') 2 | 3 | port = 5555 4 | host = 'localhost' 5 | 6 | # Connecting to the core server, retrieving its interface object : core 7 | # We do not want to expose an object, so the first parameter is nil 8 | core = BrB::Tunnel.create(nil, "brb://#{host}:#{port}", :verbose => true) 9 | 10 | # Calling 10 times an non blocking method on the distant core server 11 | 10.times do 12 | core.simple_api_method # Do not wait for response 13 | end 14 | 15 | # Calling 10 times again long treatment time distant methods 16 | 10.times do 17 | core.simple_long_api_method # Do not wait for response 18 | end 19 | 20 | # Calling a blocking method with _block on the distant core server : 21 | puts " >> Calling 1s call, and wait for response..." 22 | r = core.simple_api_method_block 23 | puts " > Api response : #{r}" 24 | 25 | puts " >> Calling long call, and wait for response..." 26 | r = core.simple_long_api_method_block 27 | puts " > Api long response : #{r}" 28 | 29 | ## Calling method with a callback block for handling the return value 30 | core.simple_api_method do |r| 31 | puts " > Get the callback response : #{r}" 32 | end 33 | 34 | puts " >> Callback method has been called continue .." 35 | sleep 2 36 | 37 | core.stop_service 38 | 39 | # Our job is over, close event machine : 40 | EM.stop -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'spec' 3 | 4 | Thread.abort_on_exception = true 5 | 6 | require File.dirname(__FILE__) + '/../init.rb' 7 | 8 | def open_service(object, host = 'localhost', port = 6200) 9 | BrB::Service.start_service(:object => object, :verbose => false, :host => host, :port => port) 10 | end 11 | 12 | def connect_to_the_service(object_exposed, uri, &block) 13 | BrB::Tunnel.create(object_exposed, uri, :verbose => false, &block) 14 | end 15 | 16 | class BrBTest 17 | attr_reader :last_call 18 | attr_reader :last_args 19 | attr_reader :nb_call 20 | 21 | def increment_nb_call(call_name, *args) 22 | @last_call = call_name 23 | @last_args = args 24 | @nb_call ||= 0 25 | @nb_call += 1 26 | end 27 | 28 | def very_long(ar) 29 | increment_nb_call(:very_long, ar) 30 | end 31 | 32 | def fourargs(arg1, arg2, arg3, arg4) 33 | increment_nb_call(:fourargs, arg1, arg2, arg3, arg4) 34 | end 35 | 36 | def noarg 37 | increment_nb_call(:noarg) 38 | end 39 | 40 | def one_arg_with_return(ar) 41 | increment_nb_call(:one_arg_with_return) 42 | return ar 43 | end 44 | 45 | def return_same_value(val) 46 | increment_nb_call(:return_same_value) 47 | return val 48 | end 49 | 50 | def return_same_value_twice(val, val2) 51 | increment_nb_call(:return_same_value_twice) 52 | return val, val2 53 | end 54 | end -------------------------------------------------------------------------------- /lib/brb/service.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Brb Main class used to do basic distributed ruby, Simple but fast 3 | # Use two distinct canal, one for the command reception, and the other one for send return value 4 | # 5 | module BrB 6 | class Service 7 | @@uri = nil 8 | @@em_signature = nil 9 | @@verbose = false 10 | 11 | class << self 12 | 13 | public 14 | 15 | # Start a server hosted on the object given, 16 | # If an uri is given, automatcilay connect to the distant brb object 17 | def start_service(opts = {}, &block) 18 | return if @@em_signature 19 | 20 | @@verbose = opts[:verbose] 21 | BrB.logger.level = @@verbose ? Logger::INFO : Logger::WARN 22 | 23 | addr = opts[:uri] || "brb://#{opts[:host] || 'localhost'}:#{opts[:port] || 6200}" 24 | 25 | BrB.logger.info " [BrB] Start service on #{addr} ..." 26 | @@uri, @@em_signature = BrB::Protocol::open_server(addr, BrB::Tunnel::Handler, opts.merge(:block => block)) 27 | BrB.logger.info " [BrB] Service started on #{@@uri}" 28 | end 29 | 30 | def uri 31 | @@uri 32 | end 33 | 34 | # Stop the Brb Service 35 | def stop_service 36 | return if !@@em_signature or !EM::reactor_running? 37 | 38 | BrB.logger.info " [BrB] Stop service on #{@@uri}" 39 | sign = @@em_signature 40 | q = Queue.new # Creation of a Queue for waiting server to stop 41 | EM::schedule do 42 | q << EM::stop_server(sign) 43 | end 44 | q.pop 45 | @@em_signature = nil 46 | @@uri = nil 47 | end 48 | 49 | # Deprecated old method 50 | def instance 51 | BrB.logger.warn "DEPRECATION WARNING: BrB::Service::instance is deprecated => Just use BrB::Service" 52 | self 53 | end 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /lib/brb/event_machine.rb: -------------------------------------------------------------------------------- 1 | # Define a BrB::Protocol using event machine 2 | require 'eventmachine' 3 | 4 | module BrB 5 | class EventMachine 6 | 7 | class << self 8 | 9 | private 10 | # If EM::run has not been called yet, start the EM reactor in another thread. 11 | def ensure_em_is_started! 12 | if !EM::reactor_running? 13 | # Launch event machine reactor 14 | q = Queue.new 15 | Thread.new do 16 | EM::run do 17 | q << true # Set to the calling thread that the reactor is running 18 | #EM::set_quantum(20) 19 | #EventMachine::epoll 20 | end 21 | end 22 | # Wait for event machine running : 23 | q.pop 24 | end 25 | 26 | end 27 | 28 | public 29 | def open(uri, klass, opts = {}) 30 | host, port = parse_uri(uri) 31 | begin 32 | ensure_em_is_started! 33 | 34 | q = Queue.new 35 | EM.schedule do 36 | q << EM::connect(host, port, klass, opts.merge(:uri => "brb://#{host}:#{port}")) 37 | end 38 | 39 | # Wait for socket connection with the q.pop 40 | return q.pop 41 | 42 | rescue Exception => e 43 | BrB.logger.error e.backtrace.join("\n") 44 | raise "#{e} - #{uri}" 45 | end 46 | end 47 | 48 | def open_server(uri, klass, opts = {}) 49 | host, port = parse_uri(uri) 50 | max = 80 # Nb try before giving up 51 | begin 52 | uri = "brb://#{host}:#{port}" 53 | ensure_em_is_started! 54 | 55 | # Schedule server creation for thread safety 56 | q = Queue.new 57 | EM.schedule do 58 | q << EM::start_server(host, port, klass, opts.merge(:uri => uri)) 59 | end 60 | 61 | # Wait for server creation with the q.pop 62 | return uri, q.pop 63 | 64 | rescue Exception => e 65 | max -= 1 66 | port += 1 67 | retry if max > 0 68 | BrB.logger.error e.backtrace.join("\n") 69 | raise "#{e} - BrB Tcp Event machine Can not bind on #{host}:#{port}" 70 | end 71 | 72 | end 73 | end 74 | end 75 | 76 | class Protocol < EventMachine 77 | 78 | class << self 79 | 80 | def parse_uri(uri) 81 | if /^brb:\/\/(.+):([0-9]+)$/ =~ uri 82 | [$1, $2.to_i] 83 | else 84 | raise "Bad tcp BrB url: '#{uri}'" 85 | end 86 | end 87 | 88 | end 89 | end 90 | end 91 | 92 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = BrB - Easy and Fast distributed ruby 2 | 3 | BrB is a simple, fully transparent and extremely fast interface for doing simple distributed Ruby. 4 | The core of the architecture is provided by EventMachine (a fast and reliable IO event library). 5 | 6 | BrB was built in order to achieve these 4 main goals : 7 | * Simple and fast message passing between distant Ruby processes. 8 | * Message passing with of return values when needed. 9 | * Being extremely fast in order to handle more than a few thousand messages per second. 10 | * Being completely transparent for developer. 11 | 12 | The principle is simple and inspired from Drb (standard distributed ruby library) : 13 | A process exposes an object over the network and any ruby process (after having established a connection tunnel) can directly call a method on the exposed object. BrB handles that part, so it’s fully transparent in the Ruby code. 14 | 15 | BrB only support message passing with Marshable dumpable object : String, symbol, Array, hash, Number, Object etc... 16 | That mean you can not send file descriptor, Thread or another funky things like that :) 17 | 18 | For any question, use the Ruby BrB google group: http://groups.google.com/group/ruby-brb 19 | 20 | == Main Functionalities 21 | 22 | * Unlimited objects exposed 23 | * Processes can expose object and be client to exposed object too at the same time 24 | * Do not wait for return by default : just do simple message passing 25 | * Handle return values without blocking with the usage of a simple block 26 | * Blocking wait for a return value if needed by simply adding _block at the end of the method name 27 | * Transmission of Exception when blocking call 28 | * Thread safe if used correctly with Event Machine 29 | 30 | == How it works 31 | 32 | First of all, a process declare himself as a sever and expose any object on a given address and port. 33 | Then, any number of distant processes can create a Tunnel with that server and can expose an object in exchange too. 34 | After connection are ready, just call method on the tunnel. It will just act like normal method calling on the exposed object ! 35 | 36 | == What BrB is designed for ? 37 | 38 | * Doing Simple message passing between ruby process. 39 | * Connecting hundred of ruby process transparently. 40 | * Building a real-time scalable (game) server 41 | * Taking important load on a server easily just by distributing the load on multiple BrB instance. 42 | * Taking advantage of multi-core and multi-threaded systems. 43 | 44 | == TODO 45 | * Writing more examples 46 | * Publish Benchmarks VS drb 47 | * Improve logging mechanism 48 | * Clean up 49 | 50 | == Contributors 51 | 52 | * kwi (Guillaume Luccisano) 53 | * bwalton (Brian Walton) 54 | * dpree (Jens Bissinger) 55 | 56 | 57 | Copyright (c) 2009-2010 Guillaume Luccisano - g-mai|: guillaume.luccisano, released under the MIT license 58 | -------------------------------------------------------------------------------- /lib/brb/request.rb: -------------------------------------------------------------------------------- 1 | module BrB 2 | module Request 3 | 4 | MessageRequestCode = :s 5 | CallbackRequestCode = :c 6 | ReturnCode = :r 7 | 8 | def is_brb_request_blocking?(meth) 9 | if m = meth.to_s and m.rindex('_block') == (m.size - 6) 10 | return true 11 | end 12 | nil 13 | end 14 | 15 | # Execute a request on a distant object 16 | def new_brb_out_request(meth, *args, &blck) 17 | Thread.current[:brb_nb_out] ||= 0 18 | Thread.current[:brb_nb_out] += 1 19 | 20 | raise BrBCallbackWithBlockingMethodException.new if is_brb_request_blocking?(meth) and block_given? 21 | 22 | block = (is_brb_request_blocking?(meth) or block_given?) ? Thread.current.to_s.to_sym : nil 23 | if block 24 | args << block 25 | args << Thread.current[:brb_nb_out] 26 | end 27 | 28 | if block_given? 29 | # Simulate a method with _block in order to make BrB send the answer 30 | meth = "#{meth}_block".to_sym 31 | end 32 | 33 | args.size > 0 ? brb_send([MessageRequestCode, meth, args]) : brb_send([MessageRequestCode, meth]) 34 | 35 | if block_given? 36 | # Declare the callback 37 | declare_callback(block, Thread.current[:brb_nb_out], &blck) 38 | 39 | elsif block # Block until the request return 40 | 41 | #TimeMonitor.instance.watch_thread!(@timeout_rcv_value || 45) 42 | begin 43 | r = recv(block, Thread.current[:brb_nb_out], &blck) 44 | rescue Exception => e 45 | raise e 46 | ensure 47 | #TimeMonitor.instance.remove_thread! 48 | end 49 | if r.kind_of? Exception 50 | raise r 51 | end 52 | return r 53 | end 54 | 55 | nil 56 | end 57 | 58 | # Execute a request on the local object 59 | def new_brb_in_request(meth, *args) 60 | 61 | if is_brb_request_blocking?(meth) 62 | 63 | m = meth.to_s 64 | m = m[0, m.size - 6].to_sym 65 | 66 | idrequest = args.pop 67 | thread = args.pop 68 | begin 69 | r = ((args.size > 0) ? @object.send(m, *args) : @object.send(m)) 70 | brb_send([ReturnCode, r, thread, idrequest]) 71 | rescue Exception => e 72 | brb_send([ReturnCode, e, thread, idrequest]) 73 | BrB.logger.error e.to_s 74 | BrB.logger.error e.backtrace.join("\n") 75 | #raise e 76 | end 77 | else 78 | 79 | begin 80 | (args.size > 0) ? @object.send(meth, *args) : @object.send(meth) 81 | rescue Exception => e 82 | BrB.logger.error "#{e.to_s} => By calling #{meth} on #{@object.class} with args : #{args.inspect}" 83 | BrB.logger.error e.backtrace.join("\n") 84 | raise e 85 | end 86 | 87 | end 88 | 89 | end 90 | 91 | end 92 | end -------------------------------------------------------------------------------- /lib/brb/tunnel.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'eventmachine' 3 | require File.join(File.dirname(__FILE__), 'request.rb') 4 | require File.join(File.dirname(__FILE__), 'tunnel', 'shared.rb') 5 | 6 | module BrB 7 | module Tunnel 8 | 9 | # Create a BrB Tunnel by connecting to a distant BrB service 10 | # Pass a block if you want to get register and unregister events 11 | # The first parameter object is the object you want to expose in the BrB tunnel 12 | def self.create(object, uri = nil, opts = {}, &block) 13 | BrB::Protocol.open(uri, BrB::Tunnel::Handler, opts.merge(:object => object, :block => block)) 14 | end 15 | 16 | # Brb interface Handler for Tunnel over Event machine 17 | class Handler < ::EventMachine::Connection 18 | attr_reader :uri 19 | 20 | include BrB::Request 21 | include BrB::Tunnel::Shared 22 | 23 | def initialize(opts = {}) 24 | super 25 | @object = opts[:object] 26 | @verbose = opts[:verbose] 27 | BrB.logger.level = @verbose ? Logger::INFO : Logger::WARN 28 | @timeout_rcv_value = opts[:timeout] || 30 # Currently not implemented due to the lack of performance of ruby Timeout 29 | @close_after_timeout = opts[:close_after_timeout] || false 30 | @uri = opts[:uri] 31 | @replock = Mutex.new 32 | @responses = {} 33 | @block = opts[:block] 34 | 35 | @queue = Queue.new 36 | @buffer = '' 37 | 38 | # Callbacks handling : 39 | @callbacks = {} 40 | @callbacks_mutex = Mutex.new 41 | end 42 | 43 | # EventMachine Callback, called after connection has been initialized 44 | def post_init 45 | BrB.logger.info " [BrB] Tunnel initialized on #{@uri}" 46 | @active = true 47 | if @block 48 | EM.defer do 49 | @block.call(:register, self) 50 | end 51 | end 52 | end 53 | 54 | def close_connection(after_writing = false) 55 | @active = false 56 | super 57 | end 58 | 59 | # EventMachine unbind event 60 | # The connection has been closed 61 | def unbind 62 | BrB.logger.info ' [BrB] Tunnel service closed' 63 | @active = false 64 | if @block 65 | EM.defer do 66 | @block.call(:unregister, self) 67 | end 68 | end 69 | end 70 | 71 | # Stop the service 72 | def stop_service 73 | BrB.logger.info ' [BrB] Stopping Tunnel service...' 74 | @active = false 75 | EM.schedule do 76 | close_connection 77 | end 78 | end 79 | 80 | # Return true if the tunnel is currently active 81 | def active? 82 | @active 83 | end 84 | 85 | # When no method is found on tunnel interface, create an brb out request 86 | def method_missing(meth, *args, &block) 87 | return nil if !@active 88 | new_brb_out_request(meth, *args, &block) 89 | end 90 | end 91 | end 92 | 93 | end -------------------------------------------------------------------------------- /spec/brb/brb_massive_usage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe :brb_massive_usage do 4 | before(:all) do 5 | @brb = BrB::Service 6 | @brb.stop_service 7 | @brb_test = BrBTest.new 8 | open_service(@brb_test) 9 | @clients = [] 10 | 20.times do 11 | @clients << connect_to_the_service(self, @brb.uri) do |type, tunnel| 12 | end 13 | end 14 | end 15 | 16 | def random_client 17 | @clients[rand(@clients.size)] 18 | end 19 | 20 | # Start the service 21 | it "should the service be started" do 22 | @clients.each do |cl| 23 | cl.should_not be_nil 24 | cl.active?.should be_true 25 | cl.uri.should == @brb.uri 26 | end 27 | end 28 | 29 | it "should works with massive simple messaging" do 30 | nb_call_before = @brb_test.nb_call || 0 31 | nb_call_to_do = 500 32 | 33 | @clients.each do |cl| 34 | nb_call_to_do.times do 35 | cl.noarg 36 | end 37 | end 38 | 39 | sleep 5 40 | # Wait a little in order to be sure all the stack is processed 41 | @brb_test.last_call.should == :noarg 42 | @brb_test.nb_call.should == (nb_call_to_do * @clients.size) + nb_call_before 43 | end 44 | 45 | it "should works with massive callbacks" do 46 | block_called = 0 47 | nb_callbacks = 1000 48 | nb_callbacks.times do |i| 49 | random_client.return_same_value(i) do |callback_return_value| 50 | callback_return_value.should == i 51 | block_called += 1 52 | end 53 | end 54 | 55 | sleep 2 56 | # Wait a little in order to be sure the method is called 57 | @brb_test.last_call.should == :return_same_value 58 | block_called.should == nb_callbacks 59 | end 60 | 61 | it "should works with massive simple messaging including blocking messaging and callbacks" do 62 | nb_call_before = @brb_test.nb_call || 0 63 | nb_call_to_do = 500 64 | nb_call_blocking_to_do = 50 65 | 66 | t = Thread.new do 67 | @clients.each do |cl| 68 | nb_call_blocking_to_do.times do 69 | val = Time.now.to_f 70 | cl.return_same_value_block(val).should == val 71 | end 72 | end 73 | end 74 | 75 | block_called = 0 76 | nb_callbacks = 1000 77 | nb_callbacks.times do |i| 78 | random_client.return_same_value(i) do |callback_return_value| 79 | callback_return_value.should == i 80 | block_called += 1 81 | end 82 | end 83 | 84 | 85 | @clients.each do |cl| 86 | nb_call_to_do.times do 87 | cl.noarg 88 | end 89 | end 90 | 91 | sleep 5 92 | block_called.should == nb_callbacks 93 | t.join 94 | # Wait a little in order to be sure all the stack is processed 95 | @brb_test.nb_call.should == nb_callbacks + (nb_call_to_do * @clients.size + nb_call_blocking_to_do * @clients.size) + nb_call_before 96 | end 97 | 98 | # Finally, stop the service 99 | it "should stop the service after usage" do 100 | @brb.stop_service 101 | @brb.uri.should be_nil 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/brb/tunnel/shared.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module BrB 4 | module Tunnel 5 | module Shared 6 | def make_proxy(r) 7 | if r.is_a?(Array) 8 | t = [] 9 | r.each do |obj| 10 | t << if obj.is_a? Array 11 | make_proxy(obj) 12 | elsif !obj.is_a?(Symbol) and !obj.is_a?(String) and obj and !(Marshal::dump(obj) rescue nil) 13 | #BrB.logger.debug " - > Make proxy for : #{obj.class}" 14 | obj.to_s.to_sym 15 | else 16 | obj 17 | end 18 | end 19 | return t 20 | else 21 | return r.to_s 22 | end 23 | end 24 | 25 | def brb_send(r) 26 | return nil if !@active 27 | s = Marshal::dump(r) rescue Marshal::dump(make_proxy(r)) 28 | 29 | s = [s.size].pack('N') + s 30 | EM.schedule do 31 | send_data s 32 | end 33 | end 34 | 35 | SizeOfPackedInt = [1].pack('N').size 36 | 37 | def load_request 38 | return nil if @buffer.size < SizeOfPackedInt 39 | len = @buffer.unpack('N').first + SizeOfPackedInt 40 | if @buffer.size < len 41 | return nil 42 | end 43 | 44 | obj = Marshal::load(@buffer[SizeOfPackedInt, len]) 45 | @buffer.slice!(0,len) 46 | return obj 47 | end 48 | 49 | def receive_data(data) 50 | @buffer << data 51 | 52 | while obj = load_request 53 | if obj[0] == BrB::Request::ReturnCode 54 | 55 | # Return if we have a callback handling the return : 56 | next if treat_callback_return(obj[1], obj[2], obj[3]) 57 | 58 | # No callback, so blocking thread is waiting : 59 | @replock.lock 60 | @responses[obj[2]] ||= Queue.new 61 | @replock.unlock 62 | @responses[obj[2]] << [obj[1], obj[3]] 63 | else 64 | @queue << obj 65 | 66 | EM.defer do 67 | treat_request(@queue.pop) 68 | end 69 | 70 | end 71 | end 72 | end 73 | 74 | def treat_request(obj) 75 | if obj.size == 2 76 | new_brb_in_request(obj[1]) 77 | else 78 | new_brb_in_request(obj[1], *(obj.last)) 79 | end 80 | end 81 | 82 | # Declare a new callback to call for a given request 83 | # Thread safe code 84 | def declare_callback(key, nb_out, &block) 85 | @callbacks_mutex.lock 86 | 87 | @callbacks[key] ||= {} 88 | @callbacks[key][nb_out] = block 89 | 90 | ensure 91 | @callbacks_mutex.unlock 92 | end 93 | 94 | # Return associated callback if present 95 | # And if present, delete the associate callback from the table 96 | # Thread safe code 97 | def get_callback(key, nb_out) 98 | @callbacks_mutex.lock 99 | 100 | if @callbacks[key] and b = @callbacks[key].delete(nb_out) 101 | return b 102 | end 103 | 104 | ensure 105 | @callbacks_mutex.unlock 106 | end 107 | 108 | # Call a callback if present, return true if exists 109 | # Non blocking action, use EM.defer 110 | def treat_callback_return(ret, key, nb_out) 111 | 112 | if b = get_callback(key, nb_out) 113 | EM.defer do 114 | # With arity, handle multiple block arguments or no arguments 115 | b.arity == 1 ? b.call(ret) : (b.arity == 0 ? b.call : b.call(*ret)) 116 | end 117 | 118 | # A callback has been found and called, return true 119 | return true 120 | end 121 | 122 | # No callback, do nothing 123 | return nil 124 | end 125 | 126 | # Blocking method that wait on the @responses table an answer 127 | def recv(key, nb_out) 128 | begin 129 | @replock.lock 130 | r = @responses[key] ||= Queue.new 131 | @replock.unlock 132 | while rep = r.pop 133 | if rep[1] == nb_out # On check ke c'est bien la réponse que l'on attend 134 | 135 | # Call the callback 136 | if block_given? 137 | yield(rep[0]) 138 | end 139 | 140 | return rep[0] 141 | end 142 | if rep[1] > nb_out 143 | return nil 144 | end 145 | end 146 | rescue Exception => e 147 | if @close_after_timeout == true 148 | stop_service 149 | sleep 1 150 | raise e 151 | else 152 | raise e 153 | end 154 | end 155 | end 156 | end 157 | end 158 | end -------------------------------------------------------------------------------- /spec/brb/brb_tunnel_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | $last_unregistered = nil 4 | $last_registered = nil 5 | 6 | describe :brb_tunnel do 7 | before(:all) do 8 | @brb = BrB::Service 9 | @brb.stop_service 10 | @brb_test = BrBTest.new 11 | open_service(@brb_test) 12 | @client = connect_to_the_service(self, @brb.uri) do |type, tunnel| 13 | if type == :unregister 14 | $last_unregistered = tunnel 15 | elsif type == :register 16 | $last_registered = tunnel 17 | end 18 | end 19 | end 20 | 21 | # Start the service 22 | it "should the service be started" do 23 | @client.should_not be_nil 24 | @client.active?.should be_true 25 | @client.uri.should == @brb.uri 26 | end 27 | 28 | it "should have get the register message" do 29 | sleep 0.2 # Be sure to get the message 30 | $last_registered.class.should == @client.class 31 | end 32 | 33 | it "should correctly call simple distant method without args and without return" do 34 | @client.noarg 35 | sleep 0.2 36 | # Wait a little in order to be sure the method is called 37 | @brb_test.last_call.should == :noarg 38 | end 39 | 40 | it "should correctly call simple distant method without args and without return multipe times" do 41 | nb_call_before = @brb_test.nb_call 42 | nb_call_to_do = 50 43 | 44 | nb_call_to_do.times do 45 | @client.noarg 46 | end 47 | sleep 0.2 48 | # Wait a little in order to be sure the method is called 49 | @brb_test.last_call.should == :noarg 50 | @brb_test.nb_call.should == nb_call_to_do + nb_call_before 51 | end 52 | 53 | it "should correctly call distant method with one argument" do 54 | @client.very_long(:hello) 55 | sleep 0.2 56 | # Wait a little in order to be sure the method is called 57 | @brb_test.last_args.should == [:hello] 58 | end 59 | 60 | it "should correctly call distant method with multiple arguments" do 61 | args = [:one, :two, 3, "four"] 62 | @client.fourargs(*args) 63 | sleep 0.2 64 | # Wait a little in order to be sure the method is called 65 | @brb_test.last_args.should == args 66 | end 67 | 68 | it "should correctly return arguments symbol value" do 69 | @client.one_arg_with_return_block(:hello).should == :hello 70 | end 71 | 72 | it "should correctly return arguments string value" do 73 | @client.one_arg_with_return_block('hello').should == 'hello' 74 | end 75 | 76 | it "should correctly return arguments Fixnum value" do 77 | @client.one_arg_with_return_block(42).should == 42 78 | end 79 | 80 | it "should correctly return arguments Float value" do 81 | @client.one_arg_with_return_block(42.42).should == 42.42 82 | end 83 | 84 | it "should correctly return arguments Table value" do 85 | @client.one_arg_with_return_block([:one, :two, 3, "four"]).should == [:one, :two, 3, "four"] 86 | end 87 | 88 | it "should correctly return arguments Hash value" do 89 | h = {:yoyo => :titi, "salut" => 45} 90 | @client.one_arg_with_return_block(h).should == h 91 | end 92 | 93 | it "should correctly return multiple values" do 94 | r1, r2 = @client.return_same_value_twice_block(:ret, :ret2) 95 | r1.should == :ret 96 | r2.should == :ret2 97 | end 98 | 99 | it "should dump to symbol undumpable value by using the proxy" do 100 | @client.return_same_value_block(Thread.current).class.should == Symbol 101 | @client.return_same_value_block(Thread.current).should == Thread.current.to_s.to_sym 102 | end 103 | 104 | it "should transmit with success exception when blocking" do 105 | e = nil 106 | begin 107 | @client.notavalidmeth_block 108 | rescue Exception => e 109 | end 110 | e.should be_a NameError 111 | end 112 | 113 | it "should use block as non blocking callback with return value" do 114 | block_called = nil 115 | @client.return_same_value(:arg) do |v| 116 | v.should == :arg 117 | block_called = true 118 | end 119 | sleep 0.2 120 | # Wait a little in order to be sure the method is called 121 | @brb_test.last_call.should == :return_same_value 122 | block_called.should == true 123 | end 124 | 125 | it "should correctly handle multiple values return with callbacks" do 126 | block_called = nil 127 | @client.return_same_value_twice(:ret, :ret2) do |r1, r2| 128 | r1.should == :ret 129 | r2.should == :ret2 130 | block_called = true 131 | end 132 | sleep 0.2 133 | # Wait a little in order to be sure the method is called 134 | @brb_test.last_call.should == :return_same_value_twice 135 | block_called.should == true 136 | end 137 | 138 | it "should correctly handle no block args return with callbacks" do 139 | block_called = nil 140 | @client.return_same_value_twice(:ret, :ret2) do 141 | block_called = true 142 | end 143 | sleep 0.2 144 | # Wait a little in order to be sure the method is called 145 | @brb_test.last_call.should == :return_same_value_twice 146 | block_called.should == true 147 | end 148 | 149 | it "should raise an exception when calling a blocking method with a callback" do 150 | e = nil 151 | begin 152 | @client.return_same_value_block(:arg) do |v| 153 | end 154 | rescue Exception => e 155 | end 156 | e.should_not be_nil 157 | end 158 | 159 | # Finally, stop the service 160 | it "should stop the service after usage" do 161 | @brb.stop_service 162 | @brb.uri.should be_nil 163 | end 164 | 165 | # Finally, stop the client tunnel 166 | it "should stop the tunnel after usage" do 167 | @client.stop_service 168 | @client.active?.should_not be_true 169 | end 170 | 171 | it "should have get the unregister message" do 172 | sleep 0.2 # Be sure to get the message 173 | $last_unregistered.class.should == @client.class 174 | end 175 | end 176 | 177 | --------------------------------------------------------------------------------