fastbeans 0.3.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
- metadata.gz: fab0fdaf53dfe7c4f0f821c7ccf713f937034f50
4
- data.tar.gz: d14e754ccef7d5ee62ca7e78079dd155401f509f
3
+ metadata.gz: 158a2d7ac50d0efe24b525b82c28c6a9222c8df6
4
+ data.tar.gz: fec6f6afc2ad0a825df2f5f70e0d7f468acd09a8
5
5
  !binary "U0hBNTEy":
6
- metadata.gz: 6ec9d3d4080268b7604dbd51c223b370f1eeeb823fb5e7bc6bc930ca4b0b8e582dd31e2eb576fcb27375ded8b28dc66ed3633c9c65b04f0f37b56700af8e14a2
7
- data.tar.gz: 571af395f94112bc12bf2ef2d42666fe68aed6234b932e3a934fd0686865343efa51f164ffb7d7642d4bfad14637777ac0bd7408ad3db158069b12318ab10534
6
+ metadata.gz: 725a765d91e556af64d0cabc5b8f2daacd3d816e986be956356fbd3d8a0b835fa764fec3e81ad3af75c99892cc507b7ba31d090dda017bdbefff60bf3f9de8ae
7
+ data.tar.gz: 3a1c0a4cfa84149afccb88745c56ea8b950692d2f26f85e0d5feb8a7c5d6ae1c509e17b64e6f81383908dc3d62e1152b428cf04c61ed87bfccb07a1a2f3ea9f6
data/Rakefile CHANGED
@@ -1 +1,11 @@
1
1
  require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'lib/fastbeans'
7
+ t.test_files = FileList['test/*_test.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+ task :default => :test
data/fastbeans.gemspec CHANGED
@@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
23
23
  spec.add_runtime_dependency "connection_pool"
24
24
  spec.add_development_dependency "bundler", "~> 1.3"
25
25
  spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "mocha"
26
27
  end
@@ -1,25 +1,26 @@
1
1
  require 'msgpack'
2
2
  require 'rufus-lru'
3
- require 'fastbeans/connection'
4
- require 'fastbeans/response'
5
3
  require 'connection_pool'
4
+ require 'connection'
6
5
 
7
6
  module Fastbeans
8
7
  class Client
9
8
  CALL_CACHE_SIZE=100
10
9
 
11
10
  attr_reader :call_cache
11
+ attr_accessor :connection_class
12
12
 
13
- def initialize(host="127.0.0.1", port=12345, cache_size=nil, pool_opts={})
13
+ def initialize(host='127.0.0.1', port=12345, cache_size=nil, pool_opts={})
14
14
  @host, @port = host, port
15
15
  @cache_size ||= CALL_CACHE_SIZE
16
16
  @call_cache = Rufus::Lru::SynchronizedHash.new(@cache_size)
17
17
  @pool_opts = {:size => 5, :timeout => 5}.update(pool_opts)
18
+ @connection_class = Fastbeans::Connection
18
19
  end
19
20
 
20
21
  def pool
21
22
  @pool ||= ConnectionPool.new(@pool_opts) do
22
- Fastbeans::Connection.new(@host, @port)
23
+ @connection_class.new(@host, @port)
23
24
  end
24
25
  end
25
26
 
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'fastbeans/request'
2
3
 
3
4
  module Fastbeans
4
5
  class Connection
@@ -9,17 +10,23 @@ module Fastbeans
9
10
  def initialize(host, port)
10
11
  Fastbeans.debug("Connecting to #{host}:#{port}")
11
12
  @host, @port = host, port
12
- connect!(@host, @port)
13
+ @socket = connect!(@host, @port)
13
14
  end
14
15
 
15
16
  def connect!(host, port)
16
17
  @socket = TCPSocket.new(host, port)
17
18
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
19
+ @socket
20
+ end
21
+
22
+ def get_socket
23
+ @socket || connect!(@host, @port)
18
24
  end
19
25
 
20
26
  def disconnect!
21
27
  if @socket
22
28
  @socket.close rescue nil
29
+ @socket = nil
23
30
  end
24
31
  end
25
32
 
@@ -31,7 +38,7 @@ module Fastbeans
31
38
  def call(*data)
32
39
  retries = 0
33
40
  begin
34
- call_without_retries(*data)
41
+ call_without_retries(data)
35
42
  rescue Fastbeans::RemoteConnectionFailed => e
36
43
  Fastbeans.debug(e)
37
44
  if retries < MAX_RETRIES
@@ -49,18 +56,26 @@ module Fastbeans
49
56
  end
50
57
  end
51
58
 
52
- def call_without_retries(*data)
53
- payload = MessagePack.pack(data).force_encoding("BINARY")
54
- @socket.write([payload.bytesize].pack("N"))
55
- @socket.write(payload)
56
- raw_resp = MessagePack.load(@socket)
57
- resp = Response.new(data, raw_resp)
58
- resp.payload
59
+ def call_without_retries(data)
60
+ perform(data)
61
+
59
62
  rescue IOError, Errno::EPIPE, MessagePack::MalformedFormatError => e
63
+ disconnect!
60
64
  ne = RemoteConnectionFailed.new(e.message)
61
65
  ne.orig_exc = e
62
66
  raise ne
67
+
68
+ rescue Exception
69
+ disconnect!
70
+ raise
63
71
  end
64
72
 
73
+ def with_socket
74
+ yield(get_socket)
75
+ end
76
+
77
+ def perform(data)
78
+ Request.new(self).perform(data)
79
+ end
65
80
  end
66
81
  end
@@ -1,13 +1,14 @@
1
1
  module Fastbeans
2
2
 
3
- class Exception < StandardError
3
+ class RPCException < StandardError
4
4
  attr_accessor :orig_exc
5
5
  end
6
6
 
7
- class RemoteCallFailed < Fastbeans::Exception; end
8
- class RemoteException < Fastbeans::Exception; end
9
- class RemoteConnectionFailed < Fastbeans::Exception; end
10
- class RemoteConnectionDead < Fastbeans::Exception; end
11
- class AutogeneratedException < Fastbeans::Exception; end
7
+ class RemoteCallFailed < Fastbeans::RPCException; end
8
+ class RemoteException < Fastbeans::RPCException; end
9
+ class RemoteConnectionFailed < Fastbeans::RPCException; end
10
+ class RemoteConnectionDead < Fastbeans::RPCException; end
11
+ class AutogeneratedException < Fastbeans::RPCException; end
12
+ class ResponseSignatureMismatch < RemoteConnectionFailed; end
12
13
 
13
14
  end
@@ -0,0 +1,49 @@
1
+ require 'digest/md5'
2
+ require 'response'
3
+
4
+ module Fastbeans
5
+ class Request
6
+ attr_reader :connection
7
+
8
+ def initialize(connection)
9
+ @connection = connection
10
+ end
11
+
12
+ def sign(call_data)
13
+ Digest::MD5.hexdigest(call_data.inspect)
14
+ end
15
+
16
+ def build_payload(call_data)
17
+ signature = sign(call_data)
18
+ signed_data = [signature, call_data]
19
+ payload = MessagePack.pack(signed_data)
20
+ if payload.respond_to?(:force_encoding)
21
+ payload.force_encoding('BINARY')
22
+ end
23
+ [signature, payload]
24
+ end
25
+
26
+ def write_payload(sock, payload)
27
+ sock.write([payload.bytesize].pack('N'))
28
+ sock.write(payload)
29
+ end
30
+
31
+ def read_response(sock, call_data)
32
+ raw_resp = MessagePack.load(sock)
33
+ Fastbeans::Response.new(call_data, raw_resp)
34
+ end
35
+
36
+ def perform(call_data)
37
+ connection.with_socket do |sock|
38
+ signature, payload = build_payload(call_data)
39
+ write_payload(sock, payload)
40
+ resp = read_response(sock, call_data)
41
+ if resp.signed_with?(signature)
42
+ resp.payload
43
+ else
44
+ raise ResponseSignatureMismatch, "Received #{resp.signature} signature instead of expected #{signature} for #{call_data} call"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,6 +1,4 @@
1
1
  module Fastbeans
2
-
3
-
4
2
  class Response
5
3
 
6
4
  def initialize(call_data, raw_response)
@@ -12,9 +10,21 @@ module Fastbeans
12
10
  @raw_response.is_a?(Hash) and @raw_response.has_key?("fastbeans-error")
13
11
  end
14
12
 
13
+ def signature
14
+ unless error?
15
+ @raw_response[0]
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def signed_with?(orig_signature)
22
+ signature == orig_signature
23
+ end
24
+
15
25
  def payload
16
26
  unless error?
17
- @raw_response
27
+ @raw_response[1]
18
28
  else
19
29
  raise to_exception
20
30
  end
@@ -44,9 +54,9 @@ module Fastbeans
44
54
  word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
45
55
  word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
46
56
  word.tr!("-", "_")
57
+ word.gsub!(/\s+/, "_")
47
58
  word.downcase!
48
59
  word
49
60
  end
50
61
  end
51
-
52
62
  end
@@ -1,3 +1,3 @@
1
1
  module Fastbeans
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.2"
3
3
  end
data/lib/fastbeans.rb CHANGED
@@ -27,9 +27,9 @@ module Fastbeans
27
27
 
28
28
  def exception(classname)
29
29
  begin
30
- Fastbeans.const_get(classname)
30
+ Fastbeans.const_get(classname.to_s)
31
31
  rescue NameError
32
- Fastbeans.const_set(classname, Class.new(Fastbeans::AutogeneratedException))
32
+ Fastbeans.const_set(classname.to_s, Class.new(Fastbeans::AutogeneratedException))
33
33
  end
34
34
  end
35
35
  end
@@ -0,0 +1,26 @@
1
+ require_relative 'test_helper'
2
+
3
+ class ClientTest < MiniTest::Unit::TestCase
4
+
5
+ def setup
6
+ @client = Fastbeans::Client.new
7
+ @client.connection_class = MockConnection
8
+ end
9
+
10
+ def test_how_call_works
11
+ real = @client.call("+", 1, 2, 3)
12
+ assert_equal ["+", 1, 2, 3], real
13
+ end
14
+
15
+ def test_how_cached_call_works
16
+ real1 = @client.cached_call("+", 1, 2, 3)
17
+ real2 = @client.cached_call("+", 1, 2, 3)
18
+ assert_equal real1.object_id, real2.object_id
19
+
20
+ @client.clear_call_cache!
21
+
22
+ real3 = @client.cached_call("+", 1, 2, 3)
23
+ assert real1.object_id != real3.object_id
24
+ end
25
+ end
26
+
@@ -0,0 +1,85 @@
1
+ require_relative 'test_helper'
2
+
3
+ class ConnectionTest < MiniTest::Unit::TestCase
4
+
5
+ def setup
6
+ Fastbeans::Connection.any_instance.stubs(:connect!).returns(StringIO.new)
7
+ @conn = Fastbeans::Connection.new('localhost', 12345)
8
+ @test_msg = ['+', 1, 2, 3]
9
+ end
10
+
11
+ def test_socket_presence
12
+ assert_is_a_socket @conn.socket
13
+ end
14
+
15
+ def test_disconnect
16
+ @conn.disconnect!
17
+ assert_nil @conn.socket
18
+ end
19
+
20
+ def test_get_socket
21
+ assert_is_a_socket @conn.get_socket
22
+
23
+ @conn.disconnect!
24
+
25
+ assert_is_a_socket @conn.get_socket
26
+ end
27
+
28
+ def test_reconnect
29
+ @conn.expects(:disconnect!)
30
+ @conn.expects(:connect!).with('localhost', 12345)
31
+ @conn.reconnect!
32
+ end
33
+
34
+ def test_with_socket
35
+ @conn.with_socket do |sock|
36
+ assert_is_a_socket sock
37
+ end
38
+ end
39
+
40
+ def test_call_without_retries
41
+ Fastbeans::Request.any_instance.expects(:perform).with(@test_msg).returns(:reply)
42
+ assert_equal :reply, @conn.call_without_retries(@test_msg)
43
+ end
44
+
45
+ def test_exception_during_call
46
+ @conn.expects(:perform).with(@test_msg).raises(RuntimeError)
47
+ @conn.expects(:disconnect!)
48
+ assert_raises RuntimeError do
49
+ @conn.call_without_retries(@test_msg)
50
+ end
51
+ end
52
+
53
+ def test_io_exceptions_during_call
54
+ ioexcs = [IOError, Errno::EPIPE, MessagePack::MalformedFormatError]
55
+ ioexcs.each do |exc|
56
+ @conn.expects(:perform).with(@test_msg).raises(exc)
57
+ @conn.expects(:disconnect!)
58
+ assert_raises Fastbeans::RemoteConnectionFailed do
59
+ @conn.call_without_retries(@test_msg)
60
+ end
61
+ end
62
+ end
63
+
64
+ def test_perform
65
+ req = mock
66
+ req.expects(:perform).with(@test_msg).returns(:result)
67
+ Fastbeans::Request.expects(:new).with(@conn).returns(req)
68
+ assert_equal :result, @conn.perform(@test_msg)
69
+ end
70
+
71
+ def test_call_with_retries
72
+ @conn.expects(:call_without_retries).
73
+ with(@test_msg).times(4).
74
+ raises(Fastbeans::RemoteConnectionFailed)
75
+ @conn.expects(:reconnect!).times(3)
76
+ assert_raises Fastbeans::RemoteConnectionDead do
77
+ assert_nil @conn.call(*@test_msg)
78
+ end
79
+ end
80
+
81
+ def assert_is_a_socket(sock)
82
+ assert_instance_of StringIO, sock
83
+ end
84
+
85
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'test_helper'
2
+
3
+ class FastbeansTest < MiniTest::Unit::TestCase
4
+
5
+ def test_should_autocreate_exceptions
6
+ assert_equal Fastbeans::RemoteException, Fastbeans.exception('RemoteException')
7
+ assert_equal 'Fastbeans::SomeException', Fastbeans.exception('SomeException').to_s
8
+ end
9
+ end
@@ -0,0 +1,78 @@
1
+ require_relative 'test_helper'
2
+
3
+ class RequestTest < MiniTest::Unit::TestCase
4
+
5
+ def setup
6
+ @conn = MockConnection.new
7
+ @req = Fastbeans::Request.new(@conn)
8
+ @test_msg = ['+', 1, 2, 3]
9
+ @test_sign = '2446e1d1a2947cfac9bbb0290b40d413'
10
+ @test_msgpack = "\x92\xDA\0 2446e1d1a2947cfac9bbb0290b40d413\x94\xA1+\x01\x02\x03"
11
+ @test_msgpack.force_encoding("BINARY")
12
+ end
13
+
14
+ def test_should_sign_call_data
15
+ samples = {
16
+ ['a', 1, 2, 3] => '60fb262a7b770985a64bb29e84d92af4',
17
+ ['a', 1, 2, {:a => 3}] => '3d8e75d66df7134d7260954c6e9c3b64',
18
+ ['clojure.something/blah', [1, 2, 3]] => 'f8c15a77f1c43ccd1ca29b0b6d969e9f',
19
+ ['hello'] => '1f0bf5038e61951a95c4a2a9db7c400a',
20
+ [:hello] => '776222fe61dcc5551da706f3ec5c12c1',
21
+ @test_msg => @test_sign
22
+ }
23
+ samples.each do |inp, outp|
24
+ assert_equal outp, @req.sign(inp), "Wrong signature for #{inp}"
25
+ end
26
+ end
27
+
28
+ def test_build_payload
29
+ signature, payload = @req.build_payload(@test_msg)
30
+ assert_equal @test_sign, signature
31
+ assert_equal @test_msgpack, payload
32
+ if payload.respond_to?(:force_encoding)
33
+ # Ruby 2.0 case
34
+ assert_equal Encoding::BINARY, payload.encoding
35
+ end
36
+ end
37
+
38
+ def test_write_payload
39
+ io = StringIO.new
40
+ @req.write_payload(io, "simple-test")
41
+ io.rewind
42
+ assert_equal "\0\0\0\vsimple-test", io.read
43
+ end
44
+
45
+ def test_read_response
46
+ io = StringIO.new(@test_msgpack)
47
+ resp = @req.read_response(io, @test_msg)
48
+ assert_instance_of Fastbeans::Response, resp
49
+ assert_equal '2446e1d1a2947cfac9bbb0290b40d413', resp.signature
50
+ assert_equal @test_msg, resp.payload
51
+ end
52
+
53
+ def test_perform
54
+ @conn.socket = StringIO.new
55
+ @req.expects(:build_payload).with(@test_msg).returns([@test_sign, @test_msgpack])
56
+ @req.expects(:write_payload).with(@conn.socket, @test_msgpack)
57
+ resp = mock
58
+ resp.expects(:signed_with?).with(@test_sign).returns(true)
59
+ resp.expects(:payload).returns(@test_msg)
60
+ @req.expects(:read_response).with(@conn.socket, @test_msg).returns(resp)
61
+
62
+ assert_equal @test_msg, @req.perform(@test_msg)
63
+ end
64
+
65
+ def test_perform_signature_failure
66
+ @conn.socket = StringIO.new
67
+ @req.expects(:build_payload).with(@test_msg).returns([@test_sign, @test_msgpack])
68
+ @req.expects(:write_payload).with(@conn.socket, @test_msgpack)
69
+ resp = mock
70
+ resp.expects(:signed_with?).with(@test_sign).returns(false)
71
+ resp.expects(:signature).returns("incorrectsignature")
72
+ @req.expects(:read_response).with(@conn.socket, @test_msg).returns(resp)
73
+
74
+ assert_raises Fastbeans::ResponseSignatureMismatch do
75
+ @req.perform(@test_msg)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,47 @@
1
+ require_relative 'test_helper'
2
+
3
+ class ResponseTest < MiniTest::Unit::TestCase
4
+
5
+ def setup
6
+ @test_msg = ['+', 1, 2, 3]
7
+ @test_sign = '2446e1d1a2947cfac9bbb0290b40d413'
8
+ @test_resp = [@test_sign, ['+', 1, 2, 3]]
9
+ @resp = Fastbeans::Response.new(@test_msg, @test_resp)
10
+ @err_resp = { 'fastbeans-error' => 'Total screw-up',
11
+ 'error-information' => 'No ways to recover.'}
12
+ @eresp = Fastbeans::Response.new(@test_msg, @err_resp)
13
+ end
14
+
15
+ def test_error?
16
+ assert @eresp.error?
17
+ assert !@resp.error?
18
+ end
19
+
20
+ def test_signature
21
+ assert_nil @eresp.signature
22
+ assert_equal @test_sign, @resp.signature
23
+ end
24
+
25
+ def test_signed_with
26
+ assert @resp.signed_with?(@test_sign)
27
+ end
28
+
29
+ def test_payload_for_errors
30
+ begin
31
+ @eresp.payload
32
+ rescue Fastbeans::AutogeneratedException => e
33
+ assert_equal "Fastbeans::TotalScrewUp", e.class.to_s
34
+ end
35
+ end
36
+
37
+ def test_payload
38
+ assert_equal @test_msg, @resp.payload
39
+ end
40
+
41
+ def test_support_methods
42
+ assert_equal 'hello_word', @resp.underscore('hello-word')
43
+ assert_equal 'hello_word', @resp.underscore('hello word')
44
+ assert_equal 'HelloWord', @resp.camelize('hello_word')
45
+ assert_equal 'ABC', @resp.camelize('a_b_c')
46
+ end
47
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fastbeans
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dima Sabanin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-05-30 00:00:00.000000000 Z
11
+ date: 2013-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ! '>='
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: mocha
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  description: Tiny and fast RPC client for Ruby to call Clojure code
84
98
  email:
85
99
  - sdmitry@gmail.com
@@ -97,8 +111,14 @@ files:
97
111
  - lib/fastbeans/client.rb
98
112
  - lib/fastbeans/connection.rb
99
113
  - lib/fastbeans/errors.rb
114
+ - lib/fastbeans/request.rb
100
115
  - lib/fastbeans/response.rb
101
116
  - lib/fastbeans/version.rb
117
+ - test/client_test.rb
118
+ - test/connection_test.rb
119
+ - test/fastbeans_test.rb
120
+ - test/request_test.rb
121
+ - test/response_test.rb
102
122
  homepage: https://github.com/dsabanin/fastbeans-rb
103
123
  licenses:
104
124
  - MIT
@@ -123,5 +143,10 @@ rubygems_version: 2.0.3
123
143
  signing_key:
124
144
  specification_version: 4
125
145
  summary: Ruby piece of Ruby/Clojure RPC system extracted from beanstalkapp.com
126
- test_files: []
146
+ test_files:
147
+ - test/client_test.rb
148
+ - test/connection_test.rb
149
+ - test/fastbeans_test.rb
150
+ - test/request_test.rb
151
+ - test/response_test.rb
127
152
  has_rdoc: