msgpack_rpc_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7feba3f261abbfdb61f1f4f320d4f981fdc4b97f
4
+ data.tar.gz: 6beec26d066279f6c098ac88df223f80610e6a04
5
+ SHA512:
6
+ metadata.gz: f84815a076edb16128aba33a22aff7bb3c18af4d2f247656eec5e73cafad0c405191f424b28741b6942c3923d9246571a8720db901f565420110447fc45b5e7c
7
+ data.tar.gz: 5dbf537b5ff8920da50310857ecd88d5238ca4ed182e1d09141b261725400ce16220f89770f05424ff6640aad9e9062a4188a2f181b41604d5d4d5991baa550e
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in msgpack_rpc_client.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 BrightBytes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,65 @@
1
+ # MsgpackRpcClient [![CircleCI](https://circleci.com/gh/brightbytes/msgpack_rpc_client.svg?style=svg)](https://circleci.com/gh/brightbytes/msgpack_rpc_client)
2
+
3
+ ## A lean Ruby client for the MessagePack-RPC protocol
4
+
5
+ Use this gem to achieve reliable, fault-tolerant RPC with your microservices.
6
+
7
+ ### Differences from the "official" implementation (the msgpack-rpc gem)
8
+
9
+ The official implementation:
10
+
11
+ * depends on the cool.io gem, labeled "retired" [on the homepage](https://coolio.github.io).
12
+ * does not re-establish connections.
13
+ * designed to be asynchronous
14
+ * displayed instability under high load in production
15
+
16
+ This implementation:
17
+
18
+ * has no dependencies
19
+ * is under 200 lines in one class
20
+ * automatically re-establishes connections in the case of inevitable network errors, service restarts, deploys, and so on.
21
+ * supports SSL
22
+ * threadsafe
23
+ * reliable
24
+ * high load tested
25
+ * used in production, with up to a 1000 requests per second in a single frontend request.
26
+ * supports JRuby and MRI from version 1.9.3 - for those who have a huge legacy app that you're dying to factor into microservices!
27
+
28
+ However, this implementation **does NOT support asynchronous calls** - if you require this feature, it is not for you. However, from my experience, almost no Ruby applications require asynchronous communication.
29
+
30
+ ## Installation
31
+
32
+ Add this line to your application's Gemfile:
33
+
34
+ ```ruby
35
+ gem 'msgpack_rpc_client'
36
+ ```
37
+
38
+ And then execute:
39
+
40
+ $ bundle
41
+
42
+ Or install it yourself as:
43
+
44
+ $ gem install msgpack_rpc_client
45
+
46
+ ## Usage
47
+
48
+ ``` ruby
49
+ require 'msgpack_rpc_client'
50
+
51
+ client = MsgpackRpcClient.new('127.0.0.1', 12345)
52
+ response = client.call('HelloWorld', name: 'Ruby')
53
+ ```
54
+
55
+ See the `examples` directory for a complete server-client example.
56
+
57
+ ## Contributing
58
+
59
+ Bug reports and pull requests are welcome on GitHub at https://github.com/brightbytes/msgpack_rpc_client.
60
+
61
+
62
+ ## License
63
+
64
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
65
+
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ machine:
2
+ environment:
3
+ RUBY_VERSIONS: 1.9.3,2.0.0,2.1.10,2.2.5,2.3.1,jruby-1.7.25,jruby-9.1.2.0
4
+
5
+ dependencies:
6
+ override:
7
+ - rvm get head
8
+ - rvm install $RUBY_VERSIONS
9
+ - rvm $RUBY_VERSIONS --verbose do gem install bundler -v 1.10.6
10
+ - rvm $RUBY_VERSIONS --verbose do bundle install
11
+
12
+ test:
13
+ override:
14
+ - rvm $RUBY_VERSIONS --verbose do bundle exec rspec spec
@@ -0,0 +1,25 @@
1
+ # Run server.go in a parallel process first
2
+
3
+ require 'json'
4
+ require 'msgpack_rpc_client'
5
+ require 'benchmark/ips'
6
+
7
+ msgpack_client = MsgpackRpcClient.new('127.0.0.1', 12345)
8
+ http_uri = URI('http://localhost:12344/hello_world')
9
+
10
+ Benchmark.ips do |x|
11
+ x.report 'http/json' do
12
+ req = Net::HTTP::Post.new(http_uri, 'Content-Type' => 'application/json')
13
+ req.body = { name: 'Ruby' }.to_json
14
+ res = Net::HTTP.start(http_uri.hostname, http_uri.port) do |http|
15
+ http.request(req)
16
+ end
17
+ JSON.parse(res.body, symbolize_keys: true)
18
+ end
19
+
20
+ x.report 'msgpack-rpc' do
21
+ msgpack_client.call('Greeter.HelloWorld', name: 'Ruby')
22
+ end
23
+
24
+ x.compare!
25
+ end
@@ -0,0 +1,15 @@
1
+ # Run server.go in a parallel process first
2
+
3
+ require 'msgpack_rpc_client'
4
+
5
+ client = MsgpackRpcClient.new('127.0.0.1', 12345)
6
+
7
+ response = client.call('Greeter.HelloWorld', name: 'Ruby')
8
+ # {:greeting=>"Hello, Ruby"}
9
+ puts response[:greeting]
10
+
11
+ puts client.call('Greeter.HelloWorld', {})
12
+ # {:error=>"Name is required"}
13
+
14
+ puts client.call('Greeter.BogusMethod', {})
15
+ # MsgpackRpcError exception: Server responded with error: rpc: can't find method Greeter.BogusMethod
@@ -0,0 +1,82 @@
1
+ // go get github.com/ugorji/go/codec
2
+ // go run server.go
3
+ package main
4
+
5
+ import (
6
+ "fmt"
7
+ "log"
8
+ "net"
9
+ "net/http"
10
+ "net/rpc"
11
+
12
+ "github.com/ugorji/go/codec"
13
+ )
14
+
15
+ // The request structure type
16
+ // JSON tags are reused by go-codec for all encoding formats
17
+ type HelloWorldRequest struct {
18
+ Name string `json:"name"`
19
+ }
20
+
21
+ // The response structure type
22
+ type HelloWorldResponse struct {
23
+ Greeting string `json:"greeting,omitempty"`
24
+ Error string `json:"error,omitempty"`
25
+ }
26
+
27
+ // Our empty placeholder server type
28
+ type HelloWorldServer struct{}
29
+
30
+ // The request handler. It follows the usual net/rpc requirements.
31
+ // Note that the return `error` is akin to a 500 Internal Server Error,
32
+ // and shouldn't be used for errors directed at the consumer.
33
+ func (s *HelloWorldServer) HelloWorld(request *HelloWorldRequest, response *HelloWorldResponse) error {
34
+ if request.Name == "" {
35
+ response.Error = "Name is required"
36
+ return nil
37
+ }
38
+ response.Greeting = fmt.Sprintf("Hello, %s", request.Name)
39
+ return nil
40
+ }
41
+
42
+ func main() {
43
+ // Register the service with RPC
44
+ var s HelloWorldServer
45
+ rpc.RegisterName("Greeter", &s)
46
+
47
+ go func() {
48
+ // Using custom codecs with net/rpc is verbose, but isolated to this piece of code
49
+ var mh codec.MsgpackHandle
50
+ listener, err := net.Listen("tcp", "127.0.0.1:12345")
51
+ if err != nil {
52
+ log.Fatalf("failed to listen on msgpack-rpc address: %v", err)
53
+ }
54
+ log.Print("Msgpack listening on port 12345, Ctrl+C to abort")
55
+ for {
56
+ conn, err := listener.Accept()
57
+ if err != nil {
58
+ log.Fatalf("failed to accept msgpack-rpc connection: %v", err)
59
+ }
60
+ rpcCodec := codec.MsgpackSpecRpc.ServerCodec(conn, &mh)
61
+ go func() {
62
+ rpc.ServeCodec(rpcCodec)
63
+ }()
64
+ }
65
+ }()
66
+
67
+ // HTTP/JSON API for the benchmarking example
68
+ h := new(codec.JsonHandle)
69
+ http.HandleFunc("/hello_world", func(w http.ResponseWriter, r *http.Request) {
70
+ var request HelloWorldRequest
71
+ var response HelloWorldResponse
72
+
73
+ dec := codec.NewDecoder(r.Body, h)
74
+ dec.Decode(&request)
75
+ s.HelloWorld(&request, &response)
76
+ w.WriteHeader(http.StatusOK)
77
+ enc := codec.NewEncoder(w, h)
78
+ enc.Encode(response)
79
+ })
80
+ log.Print("HTTP listening on port 12344, Ctrl+C to abort")
81
+ http.ListenAndServe("127.0.0.1:12344", nil)
82
+ }
@@ -0,0 +1,142 @@
1
+ require "msgpack_rpc_client/version"
2
+ require 'msgpack'
3
+ require 'socket'
4
+
5
+ # MessagePack-RPC client
6
+ class MsgpackRpcClient
7
+ class Error < RuntimeError; end
8
+
9
+ DEFAULT_MAX_RETRIES = 10
10
+ DEFAULT_MAX_CONNECT_RETRIES = 5
11
+ DEFAULT_CONNECT_RETRY_WAIT = 0.1 # seconds
12
+ MAX_MSGID = 1_000_000_000
13
+
14
+ attr_reader :host, :port, :use_ssl, :max_retries, :max_connect_retries, :connect_retry_wait
15
+
16
+ # Initialize client and establish connection to server.
17
+ #
18
+ # (While it may seem beneficial to not connect in the constructor and wait for
19
+ # the first RPC call, I believe it's better to fail early.
20
+ #
21
+ # * host, port, use_ssl - configure your connection
22
+ # * logger - logger (default nil, set to Rails.logger in a Rails app)
23
+ #
24
+ # Parameters that are better left alone:
25
+ # * max_retries - number of times to retry sending a request
26
+ # * max_connect_retries - number of times to retry connecting to the server
27
+ # * connect_retry_wait - wait between connection retry attempts
28
+ #
29
+ # TODO: once we are long past the <2.0.0 legacy, replace with named args
30
+ def initialize(options={})
31
+ @host = options.fetch(:host)
32
+ @port = options.fetch(:port)
33
+ @use_ssl = options.fetch(:use_ssl, false)
34
+ @logger = options.fetch(:logger, nil)
35
+ @max_retries = options.fetch(:max_retries, DEFAULT_MAX_RETRIES)
36
+ @max_connect_retries = options.fetch(:max_connect_retries, DEFAULT_MAX_CONNECT_RETRIES)
37
+ @connect_retry_wait = options.fetch(:connect_retry_wait, DEFAULT_CONNECT_RETRY_WAIT)
38
+ @msgid = 1
39
+ @call_mutex = Mutex.new
40
+ init_socket
41
+ end
42
+
43
+ # Call an RPC method. Will reconnect if the server is down. Threadsafe.
44
+ #
45
+ # * Params is anything serializable with Messagepack.
46
+ # * Hashes in response will be deserialized with symbolized keys
47
+ def call(method_name, *params)
48
+ request = nil
49
+ response = nil
50
+
51
+ @call_mutex.synchronize do
52
+ request = [0, @msgid, method_name, params]
53
+ @msgid = (@msgid % MAX_MSGID) + 1
54
+ response = make_request_with_retries(request)
55
+ end
56
+
57
+ if response[0] != 1
58
+ raise MsgpackRpcClient::Error, 'Response does not bear the proper type flag - something is very wrong'
59
+ end
60
+ if response[1] != request[1]
61
+ raise MsgpackRpcClient::Error, 'Response message id does not match request message id - something is very wrong'
62
+ end
63
+ if response[2] != nil
64
+ raise MsgpackRpcClient::Error, "Server responded with error: #{response[2]}"
65
+ end
66
+
67
+ response[3]
68
+ end
69
+
70
+ private
71
+
72
+ # Handles socket connectivity details of sending and receiving. Retries on error.
73
+ def make_request_with_retries(request)
74
+ retry_count = 0
75
+ begin
76
+ @packer.write(request).flush
77
+ @unpacker.read
78
+ rescue EOFError, IOError, Errno::EPIPE, Errno::ETIMEDOUT, Errno::ECONNRESET
79
+ @logger.error("[MSGPACK-RPC] Msgpack-RPC socket interrupted. Re-establishing commmunications.") if @logger
80
+ retry_count += 1
81
+ if retry_count == max_retries
82
+ raise MsgpackRpcClient::Error, "Failed to re-establish communications with server"
83
+ else
84
+ init_socket
85
+ retry
86
+ end
87
+ end
88
+ end
89
+
90
+ # Opens a socket according to provided configuration. Retries on error.
91
+ def init_socket
92
+ @socket.close if @socket
93
+ retry_count = 0
94
+ begin
95
+ @logger.info("[MSGPACK-RPC] Connecting to Msgpack-RPC server...") if @logger
96
+ @socket = TCPSocket.new(host, port)
97
+ configure_socket_keepalive
98
+ init_ssl if use_ssl
99
+ rescue Errno::ECONNREFUSED
100
+ @logger.error("[MSGPACK-RPC] Connection refused") if @logger
101
+ retry_count += 1
102
+ if retry_count == max_connect_retries
103
+ raise MsgpackRpcClient::Error, "Could not connect to MsgPack-RPC server"
104
+ else
105
+ sleep(connect_retry_wait)
106
+ # might have a chance with a different instance
107
+ retry
108
+ end
109
+ end
110
+ # Attach streaming packer/unpacker to the socket
111
+ @packer = MessagePack::Packer.new(@socket)
112
+ @unpacker = MessagePack::Unpacker.new(@socket, symbolize_keys: true)
113
+ end
114
+
115
+ # Configure the TCP stack to send keepalive messages, as we want a long-living
116
+ # connection.
117
+ #
118
+ # (Not 100% reliable)
119
+ def configure_socket_keepalive
120
+ @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
121
+ if defined?(Socket::TCP_KEEPINTVL) # Not available on JRuby
122
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, 10)
123
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPCNT, 5)
124
+ end
125
+ if defined?(Socket::TCP_KEEPIDLE) # Not available on BSD / OSX
126
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPIDLE, 50)
127
+ end
128
+ end
129
+
130
+ # Open an SSL socket on top of the TCP socket.
131
+ #
132
+ # VERIFY_PEER is mandatory; if you have problems with it, just don't use SSL -
133
+ # without verification it gives no security benefits but only increases cpu load.
134
+ def init_ssl
135
+ ctx = OpenSSL::SSL::SSLContext.new
136
+ ctx.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
137
+ # We are overwriting the TCP socket with the SSL socket here.
138
+ @socket = OpenSSL::SSL::SSLSocket.new(@socket, ctx)
139
+ @socket.sync_close = true
140
+ @socket.connect
141
+ end
142
+ end
@@ -0,0 +1,3 @@
1
+ class MsgpackRpcClient
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'msgpack_rpc_client/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "msgpack_rpc_client"
8
+ spec.version = MsgpackRpcClient::VERSION
9
+ spec.authors = ["BrightBytes", "Leonid Shevtsov"]
10
+ spec.email = ["leonid@shevtsov.me"]
11
+
12
+ spec.summary = %q{A lean Ruby client for the MessagePack-RPC protocol.}
13
+ spec.homepage = "https://github.com/brightbytes/msgpack_rpc_client"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "msgpack"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.10"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec"
26
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: msgpack_rpc_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - BrightBytes
8
+ - Leonid Shevtsov
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2016-08-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: msgpack
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: bundler
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.10'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.10'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description:
71
+ email:
72
+ - leonid@shevtsov.me
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - circle.yml
84
+ - examples/benchmark.rb
85
+ - examples/client.rb
86
+ - examples/server.go
87
+ - lib/msgpack_rpc_client.rb
88
+ - lib/msgpack_rpc_client/version.rb
89
+ - msgpack_rpc_client.gemspec
90
+ homepage: https://github.com/brightbytes/msgpack_rpc_client
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.5.2
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: A lean Ruby client for the MessagePack-RPC protocol.
114
+ test_files: []
115
+ has_rdoc: