├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bench ├── client.cr ├── proxy.cr └── server.cr ├── build-bench.sh ├── examples ├── client.cr ├── client.rb ├── ping_pong.cr ├── server.cr ├── server_proxy.cr └── ssl.cr ├── msgpack-rpc.md ├── shard.yml ├── spec ├── .fixtures │ ├── openssl.crt │ └── openssl.key ├── server_proxy_spec.cr ├── simple_rpc_spec.cr └── spec_helper.cr └── src ├── simple_rpc.cr └── simple_rpc ├── client.cr ├── context.cr ├── error.cr ├── proto.cr ├── result.cr ├── server.cr └── server_proxy.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Specs 2 | on: 3 | push: 4 | pull_request: 5 | branches: [master] 6 | schedule: 7 | - cron: '0 0 * * 0' 8 | jobs: 9 | build: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [ubuntu-latest, macos-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Download source 17 | uses: actions/checkout@v2 18 | - name: Install Crystal 19 | uses: crystal-lang/install-crystal@v1 20 | - name: Cache shards 21 | uses: actions/cache@v2 22 | with: 23 | path: ~/.cache/shards 24 | key: ${{ runner.os }}-shards-${{ hashFiles('shard.yml') }} 25 | restore-keys: ${{ runner.os }}-shards- 26 | - name: Install shards 27 | run: shards update 28 | - name: Run tests 29 | run: crystal spec 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | bin_* 7 | *.log 8 | 9 | # Libraries don't need dependency lock 10 | # Dependencies will be locked in application that uses them 11 | /shard.lock 12 | TODO 13 | *.sock 14 | .ruby-version 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.9.0 2 | * Replace resolver from case to hash (should speed up when many methods) 3 | 4 | ## 1.8.1 5 | * Better error messages 6 | 7 | ## 1.8.0 8 | * Feature: Allow Client to pass args directly as msgpack 9 | 10 | ## 1.7.4 11 | * ServerProxy now can recheck dead connections 12 | 13 | ## 1.7.0 14 | * Fixed connection hanging after method not found request 15 | * Add ServerProxy, for proxing on request level 16 | * Some refactors 17 | 18 | ## 1.6.3 19 | * Add raw socket response 20 | 21 | ## 1.6.2 22 | * Add openssl 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Konstantin Makarchev 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # simple_rpc 2 | 3 | [![Build Status](https://github.com/kostya/simple_rpc/actions/workflows/ci.yml/badge.svg)](https://github.com/kostya/simple_rpc/actions/workflows/ci.yml?query=branch%3Amaster+event%3Apush) 4 | 5 | RPC Server and Client for Crystal. Implements [msgpack-rpc](https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md) protocol. Designed to be reliable and stable (catch every possible protocol/socket errors). It also quite fast: benchmark performs at 160Krps for single server process and single clients process (in pool mode). 6 | 7 | ## Installation 8 | 9 | Add this to your application's `shard.yml`: 10 | 11 | ```yaml 12 | dependencies: 13 | simple_rpc: 14 | github: kostya/simple_rpc 15 | ``` 16 | 17 | ## Usage 18 | 19 | 20 | ## Server example 21 | 22 | To create RPC server from your class/struct, just `include SimpleRpc::Proto`, it adds `MyRpc::Server` class and also expose all public methods to the external rpc calls. Each method should define type for each argument and also return type. Types of arguments should supports `MessagePack::Serializable` (by default it supported by most common language types, including Unions). Instance of `MyRpc` created for each rpc call, so you should not use instance variables for between-request interaction. 23 | 24 | ```crystal 25 | require "simple_rpc" 26 | 27 | struct MyRpc 28 | include SimpleRpc::Proto 29 | 30 | def sum(x1 : Int32, x2 : Float64) : Float64 31 | x1 + x2 32 | end 33 | 34 | record Greeting, rand : Float64, msg : String { include MessagePack::Serializable } 35 | 36 | def greeting(name : String) : Greeting 37 | Greeting.new(rand, "Hello from Crystal #{name}") 38 | end 39 | end 40 | 41 | puts "Server listen on 9000 port" 42 | MyRpc::Server.new("127.0.0.1", 9000).run 43 | ``` 44 | 45 | ## Client example 46 | 47 | Client simple method to use is: `.request!(return_type, method_name, *args)`. This call can raise [SimpleRpc::Errors](https://github.com/kostya/simple_rpc/blob/master/src/simple_rpc/error.cr). If you not care about return type use can use `MessagePack::Any` (in example below, you also can use `Greeting` record instead if you share that declaration). If you dont want to raise on errors you can use similar method `request` and process result manually. 48 | 49 | ```crystal 50 | require "simple_rpc" 51 | 52 | client = SimpleRpc::Client.new("127.0.0.1", 9000) 53 | 54 | p client.request!(Float64, :sum, 3, 5.5) 55 | # => 8.5 56 | p client.request!(MessagePack::Any, :greeting, "Vasya") 57 | # => {"rand" => 0.7839463879734746, "msg" => "Hello from Crystal Vasya"} 58 | ``` 59 | 60 | #### MsgpackRPC is multi-language RPC, so you can call it, for example, from Ruby 61 | ```ruby 62 | # gem install msgpack-rpc 63 | require 'msgpack/rpc' 64 | 65 | client = MessagePack::RPC::Client.new('127.0.0.1', 9000) 66 | p client.call(:sum, 3, 5.5) 67 | # => 8.5 68 | p client.call(:greeting, "Vasya") 69 | # => {"rand"=>0.47593728045415334, "msg"=>"Hello from Crystal Vasya"} 70 | ``` 71 | 72 | ## Client modes 73 | 74 | `SimpleRpc::Client` can work in multiple modes, you can choose it by argument `mode`: 75 | 76 | - `:connect_per_request` 77 | Create new connection for every request, after request done close connection. Quite slow (because spend time to create connection), but concurrency unlimited (only by OS). Good for slow requests. Used by default. 78 | 79 | - `:pool` 80 | Create persistent pool of connections. Much faster, but concurrency limited by pool_size (default = 20). Good for millions of very fast requests. Every request have one autoreconnection attempt (because connection in pool can be outdated). 81 | 82 | - `:single` 83 | Single persistent connection. Same as pool of size 1, you should manage concurrency by yourself. Every request have one autoreconnection attempt (because persistent connection can be outdated). 84 | 85 | Example of client, which can handle 50 concurrent requests, and can be used in multifiber environment: 86 | 87 | ```crystal 88 | client = SimpleRpc::Client.new("127.0.0.1", 9000, mode: :pool, pool_size: 50, pool_timeout: 1.0) 89 | ``` 90 | -------------------------------------------------------------------------------- /bench/client.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | class Bench 4 | include SimpleRpc::Proto 5 | end 6 | 7 | # execute 1 mln requests per 10 concurrent fibers in pool mode 8 | # client 10 1000000 2 9 | 10 | CONCURRENCY = (ARGV[0]? || 10).to_i 11 | REQUESTS = (ARGV[1]? || 1000).to_i 12 | mode = case (ARGV[2]? || "0") 13 | when "0" 14 | SimpleRpc::Client::Mode::Single 15 | when "1" 16 | SimpleRpc::Client::Mode::ConnectPerRequest 17 | else 18 | SimpleRpc::Client::Mode::Pool 19 | end 20 | 21 | puts "Running in #{mode}, requests: #{REQUESTS}, concurrency: #{CONCURRENCY}" 22 | 23 | ch = Channel(Bool).new 24 | 25 | n = 0 26 | s = 0.0 27 | t = Time.local 28 | c = 0 29 | e = 0 30 | 31 | CLIENT = Bench::Client.new("127.0.0.1", 9003, mode: SimpleRpc::Client::Mode::Pool, pool_size: CONCURRENCY + 1) 32 | CONCURRENCY.times do 33 | spawn do 34 | client = if mode == SimpleRpc::Client::Mode::Pool 35 | CLIENT 36 | else 37 | Bench::Client.new("127.0.0.1", 9003, mode: mode, pool_size: CONCURRENCY + 1) 38 | end 39 | (REQUESTS // CONCURRENCY).times do 40 | n += 1 41 | res = client.request(Float64, :doit, 1 / n.to_f) 42 | if res.ok? 43 | s += res.value! 44 | c += 1 45 | else 46 | e += 1 47 | raise res.message! 48 | end 49 | end 50 | ch.send(true) 51 | end 52 | end 53 | 54 | CONCURRENCY.times { ch.receive } 55 | delta = (Time.local - t).to_f 56 | puts "result: #{s}, reqs_to_run: #{CONCURRENCY * (REQUESTS // CONCURRENCY)}, reqs_ok: #{c}, reqs_fails: #{e}, in: #{delta}, rps: #{n / delta}" 57 | -------------------------------------------------------------------------------- /bench/proxy.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | port = (ARGV[0]? || 9003).to_i 4 | proxy = SimpleRpc::ServerProxy.new("127.0.0.1", port) 5 | proxy.set_ports [9004, 9005, 9006] 6 | puts "Listen on #{port}" 7 | proxy.run 8 | -------------------------------------------------------------------------------- /bench/server.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | class Bench 4 | include SimpleRpc::Proto 5 | 6 | def doit(a : Float64) : Float64 7 | a * 1.5 + 2.33 8 | end 9 | end 10 | 11 | port = (ARGV[0]? || 9003).to_i 12 | p "Listen on #{port}" 13 | Bench::Server.new("127.0.0.1", port).run 14 | -------------------------------------------------------------------------------- /build-bench.sh: -------------------------------------------------------------------------------- 1 | crystal build bench/rpc.cr --release -o bin_bench_rpc 2 | crystal build bench/bench_server.cr --release -o bin_bench_server 3 | crystal build bench/bench_client.cr --release -o bin_bench_client 4 | rm *.dwarf 5 | -------------------------------------------------------------------------------- /examples/client.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | port = (ARGV[0]? || 9000).to_i 4 | client = SimpleRpc::Client.new("127.0.0.1", port) 5 | 6 | p client.request!(Float64, :sum, 3, 5.5) 7 | # => 8.5 8 | -------------------------------------------------------------------------------- /examples/client.rb: -------------------------------------------------------------------------------- 1 | # gem install msgpack-rpc 2 | require 'msgpack/rpc' 3 | 4 | client = MessagePack::RPC::Client.new('127.0.0.1', 9000) 5 | p client.call(:sum, 3, 5.5) 6 | # => 8.5 7 | p client.call(:greeting, "Vasya") 8 | # => {"rand"=>0.47593728045415334, "msg"=>"Hello from Crystal Vasya"} 9 | -------------------------------------------------------------------------------- /examples/ping_pong.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | # Example ping pong: 4 | # run two processes: 5 | # crystal examples/ping_pong.cr 9000 6 | # crystal examples/ping_pong.cr 9001 9000 7 | 8 | class MyRpc 9 | include SimpleRpc::Proto 10 | 11 | def ping(port : Int32, x : Int32) : Nil 12 | puts "got #{x} from 127.0.0.1:#{port}" 13 | 14 | sleep 0.5 15 | MyRpc::Client.new("127.0.0.1", port).ping(PORT, x + 1) 16 | nil 17 | end 18 | end 19 | 20 | PORT = (ARGV[0]? || 9000).to_i 21 | server = MyRpc::Server.new("127.0.0.1", PORT) 22 | spawn { server.run } 23 | puts "Server on #{PORT} started" 24 | 25 | if ping_port = ARGV[1]? 26 | sleep 1 27 | MyRpc::Client.new("127.0.0.1", ping_port.to_i).ping(PORT, 0) 28 | end 29 | 30 | sleep 31 | -------------------------------------------------------------------------------- /examples/server.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | struct MyRpc 4 | include SimpleRpc::Proto 5 | 6 | def sum(x1 : Int32, x2 : Float64) : Float64 7 | puts "Got sum request #{x1}, #{x2}" 8 | x1 + x2 9 | end 10 | 11 | record Greeting, rand : Float64, msg : String { include MessagePack::Serializable } 12 | 13 | def greeting(name : String) : Greeting 14 | Greeting.new(rand, "Hello from Crystal #{name}") 15 | end 16 | end 17 | 18 | port = (ARGV[0]? || 9000).to_i 19 | puts "Server listen on #{port} port" 20 | MyRpc::Server.new("127.0.0.1", port).run 21 | -------------------------------------------------------------------------------- /examples/server_proxy.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | port = (ARGV[0]? || 9000).to_i 4 | proxy = SimpleRpc::ServerProxy.new("127.0.0.1", port) 5 | proxy.set_ports [9001, 9002] 6 | 7 | puts "Server Proxy listen on #{port} port" 8 | proxy.run 9 | -------------------------------------------------------------------------------- /examples/ssl.cr: -------------------------------------------------------------------------------- 1 | require "../src/simple_rpc" 2 | 3 | struct SslRpc 4 | include SimpleRpc::Proto 5 | 6 | def sum(x1 : Int32, x2 : Float64) : Float64 7 | x1 + x2 8 | end 9 | end 10 | 11 | spawn do 12 | server_context = OpenSSL::SSL::Context::Server.new 13 | server_context.certificate_chain = File.join("spec", ".fixtures", "openssl.crt") 14 | server_context.private_key = File.join("spec", ".fixtures", "openssl.key") 15 | 16 | puts "Server listen on 9000 port" 17 | SslRpc::Server.new("127.0.0.1", 9000, ssl_context: server_context).run 18 | end 19 | 20 | sleep 0 21 | 22 | client_context = OpenSSL::SSL::Context::Client.new 23 | client_context.verify_mode = OpenSSL::SSL::VerifyMode::NONE 24 | client = SimpleRpc::Client.new("127.0.0.1", 9000, ssl_context: client_context) 25 | 26 | p client.request!(Float64, :sum, 1, 2.0) 27 | -------------------------------------------------------------------------------- /msgpack-rpc.md: -------------------------------------------------------------------------------- 1 | # MessagePack-RPC Specification 2 | 3 | ## Notice 4 | 5 | This page describes the specification of the MessagePack-RPC Protocol, a Remote Procedure Call (RPC) Protocol using MessagePack data format. This information is required for developing the MessagePack-RPC language bindings. 6 | 7 | MessagePack-RPC enables the client to call pre-defined server functions remotely. The merits of MessagePack-RPC are as follows. 8 | 9 | ### Compact 10 | 11 | The message between the clients and the server is packed using the MessagePack data format. It's really compact compared to other formats like JSON, XML, etc. and allows to properly pass binary data as such. The network bandwidth can be reduced dramatically. 12 | 13 | ### Fast 14 | 15 | The implementation of MessagePack-RPC is really fast by careful design for modern hardware (multi-core, multi-cpu, etc). The stream deserialization + zero-copy feature effectively overlaps the network transfer and the computation (e.g. deserialization). 16 | 17 | ### Packaged 18 | 19 | The language bindings of MessagePack-RPC are well packaged, by using the default packaging system for each language (e.g. gem for Ruby). 20 | 21 | ### Rich 22 | 23 | Some client implementation support asynchronous calls. The user is thus able to overlap multiple RPC calls in parallel. 24 | 25 | # MessagePack-RPC Protocol specification 26 | 27 | The protocol consists of "Request" message and the corresponding "Response" message. The server must send a "Response" message in reply to the "Request" message. 28 | 29 | ## Request Message 30 | 31 | The request message is a four elements array shown below, packed in MessagePack format. 32 | 33 | ``` 34 | [type, msgid, method, params] 35 | ``` 36 | 37 | ### type 38 | 39 | The message type, must be the integer zero (0) for "Request" messages. 40 | 41 | ### msgid 42 | 43 | A 32-bit unsigned integer number. This number is used as a sequence number. The server's response to the "Request" will have the same msgid. 44 | 45 | ### method 46 | 47 | A string which represents the method name. 48 | 49 | ### params 50 | 51 | An array of the function arguments. The elements of this array are arbitrary objects. 52 | 53 | ## Response Message 54 | 55 | The response message is a four elements array shown below, packed in MessagePack format. 56 | 57 | ``` 58 | [type, msgid, error, result] 59 | ``` 60 | 61 | ### type 62 | 63 | Must be one (integer). One means that this message is the "Response" message. 64 | 65 | ### msgid 66 | 67 | A 32-bit unsigned integer number. This corresponds to the value used in the request message. 68 | 69 | ### error 70 | 71 | If the method is executed correctly, this field is Nil. If the error occurred at the server-side, then this field is an arbitrary object which represents the error. 72 | 73 | ### result 74 | 75 | An arbitrary object, which represents the returned result of the function. If an error occurred, this field should be nil. 76 | 77 | ## Notification Message 78 | 79 | The notification message is a three elements array shown below, packed in MessagePack format. 80 | 81 | ``` 82 | [type, method, params] 83 | ``` 84 | 85 | ### type 86 | 87 | Must be two (integer). Two means that this message is the "Notification" message. 88 | 89 | ### method 90 | 91 | A string, which represents the method name. 92 | 93 | ### params 94 | 95 | An array of the function arguments. The elements of this array are arbitrary objects. 96 | 97 | # The Order of the Response 98 | 99 | The server implementations don't need to send the reply in the order of the received requests. If they receive the multiple messages, they can reply in random order. 100 | 101 | This is required for the pipelining. At the server side, some functions are fast, and some are not. If the server must reply in order, the slow functions delay the other replies even if it's execution is already completed. 102 | 103 | ![feature-pipeline.png](feature-pipeline.png) 104 | 105 | # Client Implementation Details 106 | 107 | There are some client features which client libraries should implement. 108 | 109 | ## Step 1: Synchronous Call 110 | 111 | The first step is to implement the synchronous call. The client is blocked until the RPC is finished. 112 | 113 | ```java 114 | Client client = new Client("localhost", 1985); 115 | Object result = client.call("method_name", arg1, arg2, arg3); 116 | ``` 117 | 118 | ## Step 2: Asynchronous Call 119 | 120 | The second step is to support the asynchronous call. The following figure shows how asynchronous call works. 121 | 122 | ![feature-async.png](feature-async.png) 123 | 124 | The call function finishes immediately and returns the Future object. Then, the user waits for the completion of the call by calling the join() function. Finally, it gets the results by calling the getResult() function. 125 | 126 | ```java 127 | Client client = new Client("localhost", 1985); 128 | Future future = client.asyncCall("method_name", arg1, arg2, arg3); 129 | future.join(); 130 | Object result = future.getResult(); 131 | ``` 132 | 133 | This feature is useful when you call multiple functions at the same time. The example code below overlaps the two resquests, by using async calls. 134 | 135 | ```java 136 | Client client = new Client(...); 137 | Future f1 = client.asyncCall("method1"); 138 | Future f2 = client.asyncCall("method2"); 139 | f1.join(); 140 | f2.join(); 141 | ``` 142 | 143 | Implementing the asynchronous call may require an event loop library. Currently, the following libraries are used. 144 | 145 | * C++: [mpio|http://github.com/frsyuki/mpio] 146 | * Ruby: [Rev|http://rev.rubyforge.org/rdoc/] 147 | * Java: [JBoss netty|http://www.jboss.org/netty] 148 | 149 | ## Step 3: Multiple Transports 150 | 151 | The implementation should support multiple transports like TCP, UDP, or UNIX domain sockets, if possible. 152 | 153 | # Server Implementation Details 154 | 155 | There are many choices on server architectures (e.g. single-threaded, event-based, multi-threaded, SEDA, etc). The implementation may choose one freely. -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: simple_rpc 2 | version: 1.9.1 3 | 4 | authors: 5 | - Konstantin Makarchev 6 | 7 | crystal: "< 2.0.0" 8 | 9 | dependencies: 10 | msgpack: 11 | github: crystal-community/msgpack-crystal 12 | version: ">= 1.3.1" 13 | pool: 14 | github: ysbaddaden/pool 15 | version: "<= 0.2.4" 16 | 17 | 18 | license: MIT 19 | -------------------------------------------------------------------------------- /spec/.fixtures/openssl.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXTCCAkWgAwIBAgIJAKtJGQyJHN83MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV 3 | BAYTAkFSMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQwHhcNMTYwNTI5MTUwMzI1WhcNNDMxMDE0MTUwMzI1WjBF 5 | MQswCQYDVQQGEwJBUjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 7 | CgKCAQEAqz+M6CnLr8wJ5ooDiNU7D2hxfZqxculFP1y2wTDuxJfP8LqPO4o1NLpU 8 | E9H2idIn/iMLZrRpeK38lw8RmorEh0ykOQ2jXbw9Lw+xgQXjmsf0ZcXqSB82VD6q 9 | 7JsGOF+Qq3I/YGegINfiOYMw60r8YEMTBJlz7tyeuJrCx2VUwBOa2Rtx7n0fzSom 10 | 5jYAHEMQA6bAmShNOtCRn45NeVStQS1XTZ6XavmLiCUrgvEfWj+FlrpQQiTqoxGd 11 | dOTz1G0/0+FdJ7By/G/GbDBc2xuix7Fai7qhuLB5KAVd73Vy6T09U5TfDcUi+CNx 12 | cvJu0YPn9vVkRIuAoH3lMpprtzLaGwIDAQABo1AwTjAdBgNVHQ4EFgQU7DjYFtaT 13 | vnHQCfVWLOilVdco8mwwHwYDVR0jBBgwFoAU7DjYFtaTvnHQCfVWLOilVdco8mww 14 | DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAgApc4DjKd3x3lDeENS/s 15 | CZck6IkK+iErXeevIQFvi2poorTdoCdmnl4Hn+VgElTx8OL48WulDtqppEVY5I5t 16 | qT4AU7UXMBvmySw9cOB4nSMSYJVmtAYnVa61WICpQ8tIOunanRxwB32I/BUUc6rr 17 | 0i+iAiW8x4aPsG5SGafIwtfNhY1pJa4nyo/VAJKxEKIl5jgeITBGHCO8ZHHqTcBu 18 | bj/DuWC3vGN5pVR3mb+O7Q1X+nhOZaSJYkB0nfLBKpWdMx0jrp1ZvbwDH5RrzzdD 19 | ZRtrL2CVb3uWpbiCS8UaFcd1PaD92yT0IkxEhdIWH6WyDqs1/DZzbKDzxAlDiaCS 20 | ZA== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /spec/.fixtures/openssl.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCrP4zoKcuvzAnm 3 | igOI1TsPaHF9mrFy6UU/XLbBMO7El8/wuo87ijU0ulQT0faJ0if+IwtmtGl4rfyX 4 | DxGaisSHTKQ5DaNdvD0vD7GBBeOax/RlxepIHzZUPqrsmwY4X5Crcj9gZ6Ag1+I5 5 | gzDrSvxgQxMEmXPu3J64msLHZVTAE5rZG3HufR/NKibmNgAcQxADpsCZKE060JGf 6 | jk15VK1BLVdNnpdq+YuIJSuC8R9aP4WWulBCJOqjEZ105PPUbT/T4V0nsHL8b8Zs 7 | MFzbG6LHsVqLuqG4sHkoBV3vdXLpPT1TlN8NxSL4I3Fy8m7Rg+f29WREi4CgfeUy 8 | mmu3MtobAgMBAAECggEAHmOir7hrCwFcaGrpgajFWFCigzWmc8vtm/bp/5KdbIm8 9 | Pu38aQZ3tqmyLeo+o+qFalXxugIeDWpivrPP3eruQUxagD1pVkMHYIiaaVkQMPF2 10 | 73CVyMKxM3YDgwVnry1WUPZvRL5e7jUhUi9zyO1/p91/THum1SaVjBD6q8PRrFwD 11 | 2hjbi1ZYuzTJE9/7EWnrIeJUUx/TbhTM1aseufCpCbTQA5MA7ZVJKiW9+ZWPUw4x 12 | hfaDJx/kzFWE3DHU0L3eU83AFPZR2rmLOQONB2BhsSr5V7BGLtcY/4HTcRlzTt2g 13 | ZSPZiD7IhpFcOyWiqATmGtmuFOl9dOORHtgF6VxJsQKBgQDdlcRbEbBv+78GQosK 14 | Vh8ByhiDE1GQ4G4MoxxdtainBfbg4Uy+A2GDO9SgrD2VX3CSn+ZtJ8EBOO3wh9io 15 | /+X4Eoitkfpo0ZyAQQbSwXjVCZNANw8T27lYRAjIGVlNf/A3c7k3XdkdvSqkifhx 16 | e8sUFKZVR+82g9s3nAJgGxI5VwKBgQDF2GPdOw64rT3GxYsFl4qp5fc7r5IAgR73 17 | ZPWPVApYrimzpu/5AEPA/dgAcRqflP3HKCJE+gJxtUgOnyd6GXSApD6rkBx3PGd1 18 | 1ZtAsQw3wzWwQxzrQfjv4OMHy1ky1OtORvT/g1zWUKJdE+cm0CmonSbYE2Fgjkh4 19 | +G8spDk23QKBgDMBvLdx9PlyK+DXBIaWmICi8s2Jbuc4olyKV4dCv9Xiy5eshSvg 20 | P1wkM6fgvjRaSeGWqUZLNmR/pFYQD1Gnxlo6effqeIgUaEAlt9pf6t6vW5QWmIPr 21 | uliVIKhfHW13m+ZH30Tdd5Me7mf90pDc/DxdHITZEDmuVJISeYGB+cn1AoGAHdHt 22 | y2yZXXCPPSSNPbyHo/ALga2G3hiYKEXJVV8faBpoIrHovakyjSY1pmtlzePRFHGS 23 | KL9eGvFt+PY4JwkrLDCVWZqRD8/E8FfP3MJSyxzbPMQA2dzJvq4wyf32Zdj91oCP 24 | cOvF1G+26TyUvJ7niIiXUD4rkTgg6ErZxurBzOkCgYAOLlXQC+GkDjOzksSwJ60e 25 | KtcI4/7Z0CA43Tp6rLxcheNAaTcmMtmoqGyp7kChOlcJ5IOl1uXCaWP9Xho7gJqu 26 | bIK9i6hum8b1uTBI2U7FN3pUD+mKgaqXXfHjvVtoB9OS9Rug9loRkyXFtD6EZhdJ 27 | uGKDTMXbzMWWKPpoYd55Vw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /spec/server_proxy_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | class SimpleRpc::Server 4 | @cons = [] of IO 5 | 6 | def _handle(client) 7 | @cons << client 8 | handle(client) 9 | @cons.delete(client) 10 | end 11 | 12 | def close 13 | @server.try(&.close) rescue nil 14 | @cons.each &.close 15 | @server = nil 16 | end 17 | end 18 | 19 | class ProxyProto1 20 | include SimpleRpc::Proto 21 | 22 | def inc(x : Int32) : Tuple(Int32, Int32) 23 | {1, x + 1} 24 | end 25 | end 26 | 27 | class ProxyProto2 28 | include SimpleRpc::Proto 29 | 30 | def inc(x : Int32) : Tuple(Int32, Int32) 31 | {2, x + 1} 32 | end 33 | end 34 | 35 | class ProxyProto3 36 | include SimpleRpc::Proto 37 | 38 | def inc(x : Int32) : Tuple(Int32, Int32) 39 | {3, x + 1} 40 | end 41 | end 42 | 43 | def create_proxy_server(check_dead_ports_in = 1000.seconds) 44 | servers = (0..2).map do |port| 45 | case port % 3 46 | when 0 47 | ProxyProto1::Server.new("127.0.0.1", 44333 + port) 48 | when 1 49 | ProxyProto2::Server.new("127.0.0.1", 44333 + port) 50 | else 51 | ProxyProto3::Server.new("127.0.0.1", 44333 + port) 52 | end 53 | end 54 | proxy = SimpleRpc::ServerProxy.new("127.0.0.1", 44330) 55 | proxy.set_ports [44333, 44334, 44335] 56 | proxy.check_dead_ports_in = check_dead_ports_in 57 | {servers, proxy} 58 | end 59 | 60 | def with_run_proxy_server(servers, proxy, start_after = 0) 61 | servers.each do |server| 62 | spawn { sleep start_after; server.run } 63 | end 64 | spawn { sleep start_after; proxy.run } 65 | sleep 0.1 66 | yield(proxy) 67 | ensure 68 | proxy.close 69 | servers.each &.close 70 | sleep 0.1 71 | end 72 | 73 | def restart_servers_again(servers) 74 | servers.each do |server| 75 | spawn { server.run } 76 | end 77 | sleep 0.1 78 | yield 79 | ensure 80 | servers.each &.close 81 | sleep 0.1 82 | end 83 | 84 | class SimpleRpc::ServerProxy 85 | def check_dead_ports2 86 | check_dead_ports 87 | end 88 | 89 | protected def loggin(msg) 90 | end 91 | end 92 | 93 | context "ServerProxy" do 94 | [SimpleRpc::Client::Mode::ConnectPerRequest, SimpleRpc::Client::Mode::Single, SimpleRpc::Client::Mode::Pool].each do |clmode| 95 | describe "client #{clmode}" do 96 | it "ok" do 97 | servers, proxy = create_proxy_server 98 | with_run_proxy_server(servers, proxy) do 99 | client = SimpleRpc::Client.new("127.0.0.1", 44330, mode: clmode) 100 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 101 | result.should eq 2 102 | end 103 | end 104 | 105 | it "many reqs" do 106 | servers, proxy = create_proxy_server 107 | with_run_proxy_server(servers, proxy) do 108 | ports = [] of Int32 109 | r = 0 110 | 10.times do |i| 111 | client = SimpleRpc::Client.new("127.0.0.1", 44330, mode: clmode) 112 | port, result = client.request!(Tuple(Int32, Int32), :inc, i) 113 | ports << port 114 | r += result 115 | end 116 | 117 | ports.uniq.sort.should eq [1, 2, 3] 118 | r.should eq 55 119 | end 120 | end 121 | 122 | it "use all servers" do 123 | servers, proxy = create_proxy_server 124 | with_run_proxy_server(servers, proxy) do 125 | client = SimpleRpc::Client.new("127.0.0.1", 44330, mode: clmode) 126 | ports = [] of Int32 127 | 128 | 3.times do 129 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 130 | result.should eq 2 131 | 132 | ports << port 133 | end 134 | 135 | ports.sort.should eq [1, 2, 3] 136 | end 137 | end 138 | 139 | it "when 1 server die" do 140 | servers, proxy = create_proxy_server 141 | with_run_proxy_server(servers, proxy) do 142 | client = SimpleRpc::Client.new("127.0.0.1", 44330, mode: clmode) 143 | ports = [] of Int32 144 | 145 | 3.times do 146 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 147 | result.should eq 2 148 | 149 | ports << port 150 | end 151 | 152 | ports.sort.should eq [1, 2, 3] 153 | 154 | # 1 die 155 | ports.clear 156 | servers[0].close 157 | sleep 0.1 158 | 159 | 3.times do 160 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 161 | result.should eq 2 162 | 163 | ports << port 164 | end 165 | 166 | ports.size.should eq 3 167 | ports.uniq.sort.should eq [2, 3] 168 | end 169 | end 170 | 171 | it "when all servers die" do 172 | servers, proxy = create_proxy_server 173 | with_run_proxy_server(servers, proxy) do 174 | client = SimpleRpc::Client.new("127.0.0.1", 44330, mode: clmode) 175 | ports = [] of Int32 176 | 177 | 3.times do 178 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 179 | result.should eq 2 180 | 181 | ports << port 182 | end 183 | 184 | ports.sort.should eq [1, 2, 3] 185 | 186 | # 1 die 187 | ports.clear 188 | servers[0].close 189 | servers[1].close 190 | sleep 0.1 191 | 192 | 3.times do 193 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 194 | result.should eq 2 195 | 196 | ports << port 197 | end 198 | 199 | ports.size.should eq 3 200 | ports.uniq.sort.should eq [3] 201 | 202 | ports.clear 203 | servers[2].close 204 | 205 | sleep 0.1 206 | 207 | expect_raises(SimpleRpc::RuntimeError, "No alive ports") do 208 | client.request!(Tuple(Int32, Int32), :inc, 1) 209 | end 210 | end 211 | end 212 | 213 | it "RECHECK dead port, and run again" do 214 | servers, proxy = create_proxy_server 215 | with_run_proxy_server(servers, proxy) do 216 | client = SimpleRpc::Client.new("127.0.0.1", 44330, mode: clmode) 217 | ports = [] of Int32 218 | 219 | 3.times do 220 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 221 | result.should eq 2 222 | 223 | ports << port 224 | end 225 | 226 | ports.sort.should eq [1, 2, 3] 227 | 228 | # 1 die 229 | ports.clear 230 | servers[0].close 231 | servers[1].close 232 | sleep 0.1 233 | 234 | 3.times do 235 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 236 | result.should eq 2 237 | 238 | ports << port 239 | end 240 | 241 | ports.size.should eq 3 242 | ports.uniq.sort.should eq [3] 243 | 244 | ports.clear 245 | servers[2].close 246 | 247 | sleep 0.1 248 | 249 | expect_raises(SimpleRpc::RuntimeError, "No alive ports") do 250 | client.request!(Tuple(Int32, Int32), :inc, 1) 251 | end 252 | 253 | restart_servers_again(servers) do 254 | proxy.check_dead_ports2 255 | sleep 0.1 256 | 257 | ports.clear 258 | 259 | 3.times do 260 | port, result = client.request!(Tuple(Int32, Int32), :inc, 1) 261 | result.should eq 2 262 | 263 | ports << port 264 | end 265 | 266 | ports.sort.should eq [1, 2, 3] 267 | end 268 | end 269 | end 270 | end 271 | end 272 | end 273 | -------------------------------------------------------------------------------- /spec/simple_rpc_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | require "http/client" 3 | 4 | {% if flag?(:darwin) %} 5 | TIME_ERROR = 0.155 # macos has quite big time error 6 | BIG_TIME_ERROR = TIME_ERROR * 2 7 | ZERO_TIME_ERROR = 0.08 8 | {% else %} 9 | TIME_ERROR = 0.05 10 | BIG_TIME_ERROR = TIME_ERROR * 2 11 | ZERO_TIME_ERROR = 0.03 12 | {% end %} 13 | 14 | describe SimpleRpc do 15 | [SimpleRpc::Client::Mode::ConnectPerRequest, SimpleRpc::Client::Mode::Pool, SimpleRpc::Client::Mode::Single].each do |clmode| 16 | [SpecProto::Client.new(HOST, PORT, mode: clmode), SpecProto::Client.new(unixsocket: UNIXSOCK, mode: clmode), SpecProto::Client.new(HOST, PORT_SSL, mode: clmode, ssl_context: CLIENT_SSL_CTX)].each do |client| 17 | context "CLIENT(#{client.unixsocket ? "UNIX" : "TCP"}:#{clmode})" do 18 | it "ok" do 19 | res = client.bla("3.5", 9.6) 20 | res.ok?.should eq true 21 | res.value!.should eq 33.6 22 | end 23 | 24 | it "ok without wrapper" do 25 | res = client.bla!("3.5", 9.6) 26 | res.should eq 33.6 27 | end 28 | 29 | it "ok raw request" do 30 | res = client.request(Float64, :bla, "3.5", 9.6) 31 | res.ok?.should eq true 32 | res.value!.should eq 33.6 33 | end 34 | 35 | it "error raw request" do 36 | res = client.request(String, :bla, "3.5", 9.6) 37 | res.ok?.should eq false 38 | res.message!.should eq "SimpleRpc::TypeCastError: Receive unexpected result type, expected String" 39 | res.value.should eq nil 40 | end 41 | 42 | it "ok raw request!" do 43 | res = client.request!(Float64, :bla, "3.5", 9.6) 44 | res.should eq 33.6 45 | end 46 | 47 | it "ok raw_result with outsize args" do 48 | unpacker = client.raw_request(:bla) { |packer| {"3.5", 9.6}.to_msgpack(packer) } 49 | res = Float64.new(unpacker) 50 | res.should eq 33.6 51 | end 52 | 53 | it "ok raw_result with outsize args2" do 54 | unpacker = client.raw_request(:bla) { |packer| packer.write_array_start(2); packer.write("3.5"); packer.write(9.6) } 55 | res = Float64.new(unpacker) 56 | res.should eq 33.6 57 | end 58 | 59 | it "error raw request" do 60 | expect_raises(SimpleRpc::TypeCastError, "Receive unexpected result type, expected String") do 61 | client.request!(String, :bla, "3.5", 9.6) 62 | end 63 | end 64 | 65 | it "ok no_args" do 66 | res = client.no_args 67 | res.ok?.should eq true 68 | res.value.should eq 0 69 | end 70 | 71 | it "ok complex" do 72 | res = client.complex(3) 73 | res.ok?.should eq true 74 | res.value!.x.should eq "3" 75 | res.value!.y.should eq({"_0_" => 0, "_1_" => 1, "_2_" => 2}) 76 | end 77 | 78 | it "ok with_default_value" do 79 | res = client.with_default_value("2") 80 | res.value!.should eq 3 81 | 82 | res = client.with_default_value 83 | res.value!.should eq 2 84 | end 85 | 86 | it "ok with named_args" do 87 | res = client.named_args(a: 1, b: "10") 88 | res.ok?.should eq true 89 | res.value!.should eq "1 - \"10\" - nil - nil" 90 | 91 | res = client.named_args(a: 1, c: 2.5) 92 | res.ok?.should eq true 93 | res.value!.should eq "1 - nil - 2.5 - nil" 94 | end 95 | 96 | it "ok with big input args" do 97 | strings = (0..5).map { |i| (0..60000 + i).map(&.unsafe_chr).join } 98 | res = client.bin_input_args(strings, 2.5) 99 | res.ok?.should eq true 100 | res.value!.should eq "488953775.0" 101 | end 102 | 103 | it "ok with big result" do 104 | res = client.big_result(10_000) 105 | res.ok?.should eq true 106 | res.value!.size.should eq 10_000 107 | res.value!["__----9999------"].should eq "asfasdflkqwflqwe9999" 108 | end 109 | 110 | it "exception" do 111 | res = client.bla("O_o", 9.6) 112 | res.message!.should contain "SimpleRpc::RuntimeError: RuntimeError in bla[x : String, y : Float64]: 'Invalid Float64: \"O_o\"'" 113 | res.value.should eq nil 114 | end 115 | 116 | it "exception without wrapper" do 117 | expect_raises(SimpleRpc::RuntimeError, "RuntimeError in bla[x : String, y : Float64]: 'Invalid Float64: \"O_o\"'") do 118 | client.bla!("O_o", 9.6) 119 | end 120 | end 121 | 122 | it "next request after exception should be ok (was a bug)" do 123 | res = client.bla("O_o", 9.6) 124 | res.message!.should contain "SimpleRpc::RuntimeError: RuntimeError in bla[x : String, y : Float64]: 'Invalid Float64: \"O_o\"'" 125 | res.value.should eq nil 126 | 127 | res = client.bla("3.5", 9.6) 128 | res.ok?.should eq true 129 | res.value!.should eq 33.6 130 | end 131 | 132 | it "no server" do 133 | client_bad = SpecProto::Client.new(HOST, PORT + 10000, mode: clmode) 134 | res = client_bad.bla("O_o", 9.6) 135 | res.message!.should eq "SimpleRpc::CannotConnectError: Socket::ConnectError: Error connecting to '#{HOST}:#{PORT + 10000}': Connection refused" 136 | res.value.should eq nil 137 | end 138 | 139 | it "unknown method" do 140 | client2 = SpecProto2::Client.new(HOST, PORT, mode: clmode) 141 | res = client2.zip 142 | res.message!.should eq "SimpleRpc::RuntimeError: method 'zip' not found" 143 | res.value.should eq nil 144 | end 145 | 146 | it "unknown method and next ok request" do 147 | client2 = SimpleRpc::Client.new(HOST, PORT, mode: clmode) 148 | res = client2.request(Nil, :zip, 1, 2, 3) 149 | res.message!.should eq "SimpleRpc::RuntimeError: method 'zip' not found" 150 | res.value.should eq nil 151 | 152 | res = client2.request(Int32, :no_args) 153 | res.ok?.should eq true 154 | res.value.should eq 0 155 | end 156 | 157 | it "bad params" do 158 | client2 = SpecProto2::Client.new(HOST, PORT, mode: clmode) 159 | res = client2.bla(1.3, "2.5") 160 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in bla[x : String, y : Float64]: bad argument x: 'Unexpected token FloatT(1.3) expected StringT or BytesT at 1' (at FloatT(1.3))" 161 | res.value.should eq nil 162 | end 163 | 164 | it "ok sleep" do 165 | should_spend(0.1, TIME_ERROR) do 166 | res = client.sleepi(0.1, 1) 167 | res.ok?.should eq true 168 | res.value.should eq 1 169 | end 170 | end 171 | 172 | it "sleep timeout" do 173 | client_t = SpecProto::Client.new(HOST, PORT, mode: clmode, command_timeout: 0.2) 174 | 175 | should_spend(0.2, TIME_ERROR) do 176 | res = client_t.sleepi(0.5, 2) 177 | res.message!.should eq "SimpleRpc::CommandTimeoutError: Command timed out" 178 | res.value.should eq nil 179 | end 180 | end 181 | 182 | it "ok raw result" do 183 | res = client.request(Tuple(Int32, String, Float64), :raw_result) 184 | res.ok?.should eq true 185 | res.value.should eq({1, "bla", 6.5}) 186 | end 187 | 188 | it "ok stream result" do 189 | res = client.request(Tuple(Int32, String, Float64), :stream_result) 190 | res.ok?.should eq true 191 | res.value.should eq({1, "bla", 6.5}) 192 | end 193 | 194 | it "ok raw socket result" do 195 | res = client.request(Tuple(Int32, String, Float64), :raw_socket_result) 196 | res.ok?.should eq true 197 | res.value.should eq({1, "bla", 7.5}) 198 | end 199 | 200 | context "invariants" do 201 | it "int" do 202 | res = client.request(MessagePack::Type, :invariants, 0) 203 | res.ok?.should eq true 204 | v = res.value! 205 | v.as(Int).should eq 1 206 | end 207 | 208 | it "string" do 209 | res = client.request(MessagePack::Type, :invariants, 1) 210 | res.ok?.should eq true 211 | v = res.value! 212 | v.as(String).should eq "1" 213 | end 214 | 215 | it "float" do 216 | res = client.request(MessagePack::Type, :invariants, 2) 217 | res.ok?.should eq true 218 | v = res.value! 219 | v.as(Float64).should eq 5.5 220 | end 221 | 222 | it "array" do 223 | res = client.request(MessagePack::Type, :invariants, 3) 224 | res.ok?.should eq true 225 | v = res.value! 226 | v.as(Array(MessagePack::Type)).should eq [0, 1, 2] 227 | end 228 | 229 | it "bool" do 230 | res = client.request(MessagePack::Type, :invariants, 4) 231 | res.ok?.should eq true 232 | v = res.value! 233 | v.as(Bool).should eq false 234 | end 235 | end 236 | 237 | context "unions" do 238 | it "int" do 239 | res = client.request(Int32, :unions, 0) 240 | res.ok?.should eq true 241 | res.value!.should eq 1 242 | 243 | res = client.request(Int32, :unions, "0") 244 | res.ok?.should eq true 245 | res.value!.should eq 1 246 | 247 | res = client.request(Int32, :unions, 1.2) 248 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in unions[x : Int32 | String]: bad argument x: 'Couldn't parse data as {Int32, String} at 1' (at FloatT(1.2))" 249 | end 250 | 251 | it "string" do 252 | res = client.request(String, :unions, 1) 253 | res.ok?.should eq true 254 | res.value!.should eq "1" 255 | 256 | res = client.request(String, :unions, "1") 257 | res.ok?.should eq true 258 | res.value!.should eq "1" 259 | 260 | res = client.request(Int32, :unions, 1.2) 261 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in unions[x : Int32 | String]: bad argument x: 'Couldn't parse data as {Int32, String} at 1' (at FloatT(1.2))" 262 | end 263 | 264 | it "float" do 265 | res = client.request(Float64, :unions, 2) 266 | res.ok?.should eq true 267 | res.value!.should eq 5.5 268 | 269 | res = client.request(Float64, :unions, "2") 270 | res.ok?.should eq true 271 | res.value!.should eq 5.5 272 | 273 | res = client.request(Int32, :unions, 1.2) 274 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in unions[x : Int32 | String]: bad argument x: 'Couldn't parse data as {Int32, String} at 1' (at FloatT(1.2))" 275 | end 276 | 277 | it "array" do 278 | res = client.request(Array(Int32), :unions, 3) 279 | res.ok?.should eq true 280 | res.value!.should eq [1, 2, 3] 281 | 282 | res = client.request(Array(Int32), :unions, "3") 283 | res.ok?.should eq true 284 | res.value!.should eq [1, 2, 3] 285 | 286 | res = client.request(Int32, :unions, 1.2) 287 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in unions[x : Int32 | String]: bad argument x: 'Couldn't parse data as {Int32, String} at 1' (at FloatT(1.2))" 288 | end 289 | 290 | it "bool" do 291 | res = client.request(Bool, :unions, 4) 292 | res.ok?.should eq true 293 | res.value!.should eq false 294 | 295 | res = client.request(Bool, :unions, "4") 296 | res.ok?.should eq true 297 | res.value!.should eq false 298 | 299 | res = client.request(Int32, :unions, 1.2) 300 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in unions[x : Int32 | String]: bad argument x: 'Couldn't parse data as {Int32, String} at 1' (at FloatT(1.2))" 301 | end 302 | end 303 | 304 | it "sequence of requests" do 305 | f = 0.0 306 | 307 | connects = [] of IO? 308 | 309 | 100.times do |i| 310 | res = client.bla("#{i}.1", 2.5) 311 | if res.ok? 312 | f += res.value.not_nil! 313 | end 314 | 315 | connects << client.@single.try(&.socket) 316 | end 317 | 318 | f.should eq 12400.0 319 | 320 | if clmode == SimpleRpc::Client::Mode::Single 321 | connects.uniq.size.should eq 1 322 | connects.uniq.should_not eq [nil] 323 | else 324 | connects.uniq.should eq [nil] 325 | end 326 | end 327 | 328 | it "reconnecting after close" do 329 | res = client.bla("2", 2.5) 330 | res.value!.should eq 5.0 331 | 332 | client.close 333 | 334 | res = client.bla("3", 2.5) 335 | res.value!.should eq 7.5 336 | end 337 | 338 | it "raw request, wrong arguments types" do 339 | res = client.request(Float64, :bla, 3.5, 1.1) 340 | res.ok?.should eq false 341 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in bla[x : String, y : Float64]: bad argument x: 'Unexpected token FloatT(3.5) expected StringT or BytesT at 1' (at FloatT(3.5))" 342 | 343 | # after this, should be ok request 344 | res = client.bla("3.5", 9.6) 345 | res.ok?.should eq true 346 | res.value!.should eq 33.6 347 | end 348 | 349 | it "raw request, wrong arguments types" do 350 | res = client.request(Float64, :bla, "3.5", "zopa") 351 | res.ok?.should eq false 352 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in bla[x : String, y : Float64]: bad argument y: 'Unexpected token StringT(\"zopa\") expected IntT or FloatT at 5' (at StringT(\"zopa\"))" 353 | 354 | # after this, should be ok request 355 | res = client.bla("3.5", 9.6) 356 | res.ok?.should eq true 357 | res.value!.should eq 33.6 358 | end 359 | 360 | it "raw request, wrong arguments count" do 361 | res = client.request(Float64, :bla, "3.5") 362 | res.ok?.should eq false 363 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in bla[x : String, y : Float64]: bad arguments count: expected 2, but got 1" 364 | 365 | # after this, should be ok request 366 | res = client.bla("3.5", 9.6) 367 | res.ok?.should eq true 368 | res.value!.should eq 33.6 369 | end 370 | 371 | it "raw request, wrong arguments count" do 372 | res = client.request(Float64, :bla, "3.5", 10, 11, 12) 373 | res.ok?.should eq false 374 | res.message!.should eq "SimpleRpc::RuntimeError: ArgumentError in bla[x : String, y : Float64]: bad arguments count: expected 2, but got 4" 375 | 376 | # after this, should be ok request 377 | res = client.bla("3.5", 9.6) 378 | res.ok?.should eq true 379 | res.value!.should eq 33.6 380 | end 381 | 382 | it "http/client trying connect to server" do 383 | expect_raises(Exception) do 384 | HTTP::Client.get("http://#{HOST}:#{PORT}/bla") 385 | end 386 | 387 | # after request usual request just work 388 | res = client.bla("3.5", 9.6) 389 | res.ok?.should eq true 390 | res.value!.should eq 33.6 391 | end 392 | 393 | it "raw socket_client trying connect to server" do 394 | sock = TCPSocket.new(HOST, PORT) 395 | sock.write_byte(193.to_u8) # unsupported by msgpack symbol 396 | 397 | # after request usual request just work 398 | res = client.bla("3.5", 9.6) 399 | res.ok?.should eq true 400 | res.value!.should eq 33.6 401 | end 402 | 403 | it "connection to bad server" do 404 | client3 = SimpleRpc::Client.new(HOST, TCPPORT, mode: clmode) 405 | res = client3.request(Float64, :bla, "3.5", 9.6) 406 | res.ok?.should eq false 407 | res.message!.should start_with("SimpleRpc::ProtocallError: Unexpected byte '193' at 0") 408 | end 409 | 410 | it "Notify messages also works" do 411 | SpecProto.notify_count = 0 412 | SpecProto.notify_count.should eq 0 413 | 414 | sock = TCPSocket.new(HOST, PORT) 415 | {2_i8, "notif", [5]}.to_msgpack(sock) 416 | sock.flush 417 | 418 | sleep 0.05 419 | SpecProto.notify_count.should eq 5 420 | 421 | sock = TCPSocket.new(HOST, PORT) 422 | {2_i8, "notif", [10]}.to_msgpack(sock) 423 | sock.flush 424 | 425 | sleep 0.05 426 | SpecProto.notify_count.should eq 15 427 | end 428 | 429 | it "Notify with client" do 430 | SpecProto.notify_count = 0 431 | SpecProto.notify_count.should eq 0 432 | 433 | client.notify!("notif", 5) 434 | 435 | sleep 0.05 436 | SpecProto.notify_count.should eq 5 437 | 438 | client.notify!("notif", 15) 439 | 440 | sleep 0.05 441 | SpecProto.notify_count.should eq 20 442 | end 443 | 444 | it "sequence of requests with notify" do 445 | SpecProto.notify_count = 0 446 | SpecProto.notify_count.should eq 0 447 | 448 | f = 0.0 449 | 450 | 100.times do |i| 451 | res = client.bla("#{i}.1", 2.5) 452 | if res.ok? 453 | f += res.value.not_nil! 454 | end 455 | 456 | client.notify!("notif", i) 457 | end 458 | 459 | f.should eq 12400.0 460 | sleep 0.05 461 | SpecProto.notify_count.should eq 4950 462 | end 463 | 464 | context "concurrent requests" do 465 | it "work" do 466 | n = 10 467 | m = 10 468 | ch = Channel(Int32).new 469 | 470 | n.times do |i| 471 | spawn do 472 | cl = if clmode == SimpleRpc::Client::Mode::Single 473 | SpecProto::Client.new(HOST, PORT, mode: SimpleRpc::Client::Mode::Single) 474 | else 475 | client 476 | end 477 | 478 | m.times do |j| 479 | v1 = i * 10000 + j 480 | v = cl.request!(Int32, :sleepi, 0.1 + rand(0.1), v1) 481 | if v == v1 482 | ch.send(v) 483 | else 484 | raise "unexpected value #{v1} -> #{v}" 485 | end 486 | end 487 | end 488 | end 489 | 490 | t = Time.local 491 | (n * m).times { ch.receive } 492 | dt = (Time.local - t).to_f 493 | 494 | dt.should be >= (0.1 * m) 495 | dt.should be < ((0.1 + TIME_ERROR + 0.03) * m) 496 | end 497 | end 498 | end 499 | end 500 | end 501 | 502 | [SimpleRpc::Client::Mode::ConnectPerRequest, SimpleRpc::Client::Mode::Pool, SimpleRpc::Client::Mode::Single].each do |clmode| 503 | [{host: HOST, port: PORT_BAD, mode: clmode, unixsocket: nil}, {unixsocket: UNIXSOCK_BAD, host: "", port: 1, mode: clmode}].each do |client_opts| 504 | context "CLIENT(#{client_opts[:unixsocket] ? "UNIX" : "TCP"}:#{clmode})" do 505 | context "create connection" do 506 | it "raise when no connection, immediately" do 507 | client = SpecProto::Client.new(**client_opts) 508 | should_spend(0.0, ZERO_TIME_ERROR) do 509 | res = client.bla("3.5", 9.6) 510 | res.ok?.should eq false 511 | res.message!.should contain "SimpleRpc::CannotConnectError" 512 | end 513 | 514 | client.last_used_connection.try(&.connection_recreate_attempt).should eq 0 515 | end 516 | 517 | it "raise when no connection, but with reconnectings" do 518 | opts = client_opts.merge(create_connection_retries: 3, create_connection_retry_interval: 0.2) 519 | 520 | client = SpecProto::Client.new(**opts) 521 | should_spend(0.6, BIG_TIME_ERROR) do 522 | res = client.bla("3.5", 9.6) 523 | res.ok?.should eq false 524 | res.message!.should contain "SimpleRpc::CannotConnectError" 525 | end 526 | 527 | client.last_used_connection.try(&.connection_recreate_attempt).should eq 3 528 | end 529 | end 530 | end 531 | end 532 | end 533 | 534 | [SimpleRpc::Client::Mode::ConnectPerRequest, SimpleRpc::Client::Mode::Pool, SimpleRpc::Client::Mode::Single].each do |clmode| 535 | context "mode:#{clmode}" do 536 | it "MIX SSL and no" do 537 | cl_ok = SimpleRpc::Client.new(HOST, PORT, mode: clmode) 538 | cl_err = SimpleRpc::Client.new(HOST, PORT_SSL, mode: clmode) 539 | 540 | cl_ssl_ok = SimpleRpc::Client.new(HOST, PORT_SSL, ssl_context: CLIENT_SSL_CTX, mode: clmode) 541 | cl_ssl_err = SimpleRpc::Client.new(HOST, PORT, ssl_context: CLIENT_SSL_CTX, mode: clmode) 542 | 543 | cl_ok.request!(Float64, :bla, "2.0", 1.5).should eq 3.0 544 | cl_ssl_ok.request!(Float64, :bla, "2.0", 1.5).should eq 3.0 545 | 546 | expect_raises(SimpleRpc::ConnectionLostError, "IO::Error: Error reading socket") do 547 | cl_err.request!(Float64, :bla, "2.0", 1.5) 548 | end 549 | 550 | expect_raises(SimpleRpc::CannotConnectError, "OpenSSL::SSL::Error: SSL_connect") do 551 | cl_ssl_err.request!(Float64, :bla, "2.0", 1.5) 552 | end 553 | 554 | cl_ok.request!(Float64, :bla, "2.0", 1.5).should eq 3.0 555 | cl_ssl_ok.request!(Float64, :bla, "2.0", 1.5).should eq 3.0 556 | end 557 | end 558 | end 559 | 560 | [SimpleRpc::Client::Mode::ConnectPerRequest, SimpleRpc::Client::Mode::Pool, SimpleRpc::Client::Mode::Single].each do |clmode| 561 | [{ {host: HOST, port: PORT2, mode: clmode, unixsocket: nil}, SpecProto::Server.new(HOST, PORT2, logger: L) }, 562 | { {unixsocket: UNIXSOCK2, host: "", port: 1, mode: clmode}, SpecProto::Server.new(unixsocket: UNIXSOCK2, logger: L) }, 563 | ].each do |(client_opts, server)| 564 | context "CLIENT(#{client_opts[:unixsocket] ? "UNIX" : "TCP"}:#{clmode})" do 565 | it "connected after some reconnections" do 566 | opts = client_opts.merge(create_connection_retries: 3, create_connection_retry_interval: 0.2) 567 | with_run_server(server, 0.4) do |server| 568 | client = SpecProto::Client.new(**opts) 569 | should_spend(0.4, BIG_TIME_ERROR) do 570 | res = client.bla("3.5", 9.6) 571 | res.ok?.should eq true 572 | end 573 | 574 | client.last_used_connection.try(&.connection_recreate_attempt).should eq 2 575 | 576 | should_spend(0.0, ZERO_TIME_ERROR) do 577 | res = client.bla("3.5", 9.6) 578 | res.ok?.should eq true 579 | end 580 | 581 | client = SpecProto::Client.new(**opts) 582 | should_spend(0.0, ZERO_TIME_ERROR) do 583 | res = client.bla("3.5", 9.6) 584 | res.ok?.should eq true 585 | end 586 | end 587 | end 588 | end 589 | end 590 | end 591 | end 592 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/simple_rpc" 3 | 4 | L = Log.for("specs") 5 | L.backend = Log::IOBackend.new(File.open("spec.log", "a")) 6 | 7 | HOST = "127.0.0.1" 8 | PORT = 8888 9 | PORT2 = 8889 10 | PORT_SSL = 8890 11 | PORT_BAD = 9999 12 | TCPPORT = 7777 13 | UNIXSOCK = "./tmp_spec_simple_rpc.sock" 14 | UNIXSOCK2 = "./tmp_spec_simple_rpc2.sock" 15 | UNIXSOCK_BAD = "./tmp_spec_simple_rpc_bad.sock" 16 | 17 | record Bla, x : String, y : Hash(String, Int32) { include MessagePack::Serializable } 18 | 19 | class SpecProto 20 | include SimpleRpc::Proto 21 | 22 | def bla(x : String, y : Float64) : Float64 23 | x.to_f * y 24 | end 25 | 26 | def complex(a : Int32) : Bla 27 | h = Hash(String, Int32).new 28 | a.times do |i| 29 | h["_#{i}_"] = i 30 | end 31 | 32 | Bla.new(a.to_s, h) 33 | end 34 | 35 | def sleepi(v : Float64, x : Int32) : Int32 36 | sleep(v) 37 | x 38 | end 39 | 40 | def no_args : Int32 41 | 0 42 | end 43 | 44 | def with_default_value(x : String = "1") : Int32 45 | x.to_i + 1 46 | end 47 | 48 | def raw_result : SimpleRpc::Context::RawMsgpack 49 | SimpleRpc::Context::RawMsgpack.new({1, "bla", 6.5}.to_msgpack) 50 | end 51 | 52 | def stream_result : SimpleRpc::Context::IOMsgpack 53 | bytes = {1, "bla", 6.5}.to_msgpack 54 | io = IO::Memory.new(bytes) 55 | SimpleRpc::Context::IOMsgpack.new(io) 56 | end 57 | 58 | def raw_socket_result : SimpleRpc::Context::RawSocketResponse 59 | self.simple_rpc_context.write_default_response 60 | {1, "bla", 7.5}.to_msgpack(self.simple_rpc_context.io) 61 | SimpleRpc::Context::RawSocketResponse.new 62 | end 63 | 64 | def bin_input_args(x : Array(String), y : Float64) : String 65 | w = 0_u64 66 | 67 | x.each do |s| 68 | s.each_byte { |b| w += b } 69 | end 70 | 71 | (w * y).to_s 72 | end 73 | 74 | def big_result(x : Int32) : Hash(String, String) 75 | h = {} of String => String 76 | x.times do |i| 77 | h["__----#{i}------"] = "asfasdflkqwflqwe#{i}" 78 | end 79 | h 80 | end 81 | 82 | def invariants(x : Int32) : MessagePack::Type 83 | case x 84 | when 0 85 | 1_i64 86 | when 1 87 | "1" 88 | when 2 89 | 5.5 90 | when 3 91 | Array.new(3) { |i| i.to_i64.as(MessagePack::Type) } 92 | else 93 | false 94 | end.as(MessagePack::Type) 95 | end 96 | 97 | def unions(x : Int32 | String) : Int32 | String | Float64 | Array(Int32) | Bool 98 | case x.to_i 99 | when 0 100 | 1 101 | when 1 102 | "1" 103 | when 2 104 | 5.5 105 | when 3 106 | [1, 2, 3] 107 | else 108 | false 109 | end 110 | end 111 | 112 | class_property notify_count = 0 113 | 114 | def notif(x : Int32) : Nil 115 | @@notify_count += x 116 | nil 117 | end 118 | 119 | def named_args(a : Int32, b : String? = nil, c : Float64? = nil, d : Int32? = nil) : String 120 | "#{a.inspect} - #{b.inspect} - #{c.inspect} - #{d.inspect}" 121 | end 122 | end 123 | 124 | class SpecProto2 125 | include SimpleRpc::Proto 126 | 127 | def bla(x : Float64, y : String) : Float64 128 | x * y.to_f 129 | end 130 | 131 | def zip : Nil 132 | end 133 | end 134 | 135 | spawn do 136 | SpecProto::Server.new(HOST, PORT, logger: L).run 137 | end 138 | 139 | spawn do 140 | server_context = OpenSSL::SSL::Context::Server.new 141 | server_context.certificate_chain = File.join(__DIR__, ".fixtures", "openssl.crt") 142 | server_context.private_key = File.join(__DIR__, ".fixtures", "openssl.key") 143 | 144 | SpecProto::Server.new(HOST, PORT_SSL, logger: L, ssl_context: server_context).run 145 | end 146 | 147 | spawn do 148 | File.delete(UNIXSOCK) rescue nil 149 | SpecProto::Server.new(unixsocket: UNIXSOCK, logger: L).run 150 | end 151 | 152 | def bad_server_handle(client) 153 | Tuple(Int8, UInt32, String, Array(MessagePack::Type)).from_msgpack(client) 154 | client.write_byte(193_u8) # write illegal msgpack value 155 | client.flush 156 | end 157 | 158 | def should_spend(timeout, delta = 0.01) 159 | t = Time.local 160 | res = yield 161 | (Time.local - t).to_f.should be_close(timeout, delta) 162 | res 163 | end 164 | 165 | class SimpleRpc::Client 166 | getter last_used_connection : Connection? 167 | 168 | def get_connection 169 | res = previous_def 170 | @last_used_connection = res 171 | res 172 | end 173 | end 174 | 175 | spawn do 176 | bad_server = TCPServer.new(HOST, TCPPORT) 177 | loop do 178 | cli = bad_server.accept 179 | spawn bad_server_handle(cli) 180 | end 181 | end 182 | 183 | def with_run_server(server, start_after = 0) 184 | spawn do 185 | sleep start_after 186 | server.run 187 | end 188 | yield(server) 189 | ensure 190 | server.close 191 | sleep 0.1 192 | end 193 | 194 | CLIENT_SSL_CTX = begin 195 | ctx = OpenSSL::SSL::Context::Client.new 196 | ctx.verify_mode = OpenSSL::SSL::VerifyMode::NONE 197 | ctx 198 | end 199 | 200 | sleep 0.1 201 | -------------------------------------------------------------------------------- /src/simple_rpc.cr: -------------------------------------------------------------------------------- 1 | module SimpleRpc 2 | VERSION = "1.9.1" 3 | 4 | REQUEST = 0_i8 5 | NOTIFY = 2_i8 6 | RESPONSE = 1_i8 7 | 8 | REQUEST_SIZE = 4 9 | NOTIFY_SIZE = 3 10 | RESPONSE_SIZE = 4 11 | 12 | INTERNAL_PING_METHOD = "__simple_rpc_ping__" 13 | 14 | DEFAULT_MSG_ID = 0_u32 15 | end 16 | 17 | require "./simple_rpc/*" 18 | -------------------------------------------------------------------------------- /src/simple_rpc/client.cr: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "msgpack" 3 | require "openssl" 4 | require "pool/connection" 5 | 6 | class SimpleRpc::Client 7 | enum Mode 8 | # Create new connection for every request, after request done close connection. 9 | # Quite slow (because spend time to create connection), but concurrency unlimited (only by OS). 10 | # Good for slow requests. 11 | # [default] 12 | ConnectPerRequest 13 | 14 | # Create persistent pool of connections. 15 | # Much faster, but concurrency limited by pool_size (default = 20). 16 | # Good for millions of very fast requests. 17 | # Every request have one autoreconnection attempt (because connection in pool can be outdated). 18 | Pool 19 | 20 | # Single persistent connection. 21 | # Same as pool of size 1, you should manage concurrency by yourself. 22 | # Every request have one autoreconnection attempt (because persistent connection can be outdated). 23 | Single 24 | end 25 | 26 | getter pool : ConnectionPool(Connection)? 27 | getter single : Connection? 28 | getter mode 29 | 30 | getter host, port, unixsocket, command_timeout, connect_timeout, ssl_context 31 | 32 | def initialize(@host : String = "127.0.0.1", 33 | @port : Int32 = 9999, 34 | @unixsocket : String? = nil, 35 | @ssl_context : OpenSSL::SSL::Context::Client? = nil, 36 | @command_timeout : Float64? = nil, 37 | @connect_timeout : Float64? = nil, 38 | @mode : Mode = Mode::ConnectPerRequest, 39 | pool_size = 20, # pool size for mode = Mode::Pool 40 | pool_timeout = 5.0, # pool timeout for mode = Mode::Pool 41 | @create_connection_retries = 0, # sometimes, server not ready for a cuple seconds (restarted for example), 42 | # and we can set amount of retries to create connection (by default 0), 43 | # when it exceeded it will raise SimpleRpc::CannotConnectError 44 | @create_connection_retry_interval = 0.5 # sleep interval between attempts to create connection is seconds 45 | ) 46 | if @mode == Mode::Pool 47 | @pool = ConnectionPool(Connection).new(capacity: pool_size, timeout: pool_timeout) { create_connection } 48 | end 49 | end 50 | 51 | # Execute request, raise error if error 52 | # First argument is a return type, then method and args 53 | # 54 | # example: 55 | # res = SimpleRpc::Client.request!(type, method, *args) # => type 56 | # res = SimpleRpc::Client.request!(Float64, :bla, 1, "2.5") # => Float64 57 | # 58 | # raises only SimpleRpc::Errors 59 | # SimpleRpc::ProtocallError - when problem in client-server interaction 60 | # SimpleRpc::TypeCastError - when return type not casted to requested 61 | # SimpleRpc::RuntimeError - when task crashed on server 62 | # SimpleRpc::CannotConnectError - when client cant connect to server 63 | # SimpleRpc::CommandTimeoutError - when client wait too long for answer from server 64 | # SimpleRpc::ConnectionLostError - when client lost connection to server 65 | # SimpleRpc::PoolTimeoutError - when no free connections in pool 66 | 67 | def request!(klass : T.class, name, *args) forall T 68 | unpacker = raw_request(name) { |packer| args.to_msgpack(packer) } 69 | begin 70 | klass.new(unpacker) 71 | rescue ex : MessagePack::TypeCastError 72 | raise SimpleRpc::TypeCastError.new("Receive unexpected result type, expected #{klass.inspect}") 73 | end 74 | end 75 | 76 | # Execute request, not raising errors 77 | # First argument is a return type, then method and args 78 | # 79 | # example: 80 | # res = SimpleRpc::Client.request(type, method, *args) # => SimpleRpc::Result(type) 81 | # res = SimpleRpc::Client.request(Float64, :bla, 1, "2.5") # => SimpleRpc::Result(Float64) 82 | # 83 | # if res.ok? 84 | # p res.value! # => Float64 85 | # else 86 | # p res.error! # => SimpleRpc::Errors 87 | # end 88 | # 89 | def request(klass : T.class, name, *args) forall T 90 | res = request!(klass, name, *args) 91 | SimpleRpc::Result(T).new(nil, res) 92 | rescue ex : SimpleRpc::Errors 93 | SimpleRpc::Result(T).new(ex) 94 | end 95 | 96 | def notify!(name, *args) 97 | raw_notify(name) { |packer| args.to_msgpack(packer) } 98 | end 99 | 100 | def raw_request(method, msgid = SimpleRpc::DEFAULT_MSG_ID) 101 | with_connection do |connection| 102 | try_write_request(connection, method, msgid) { |packer| yield packer } 103 | 104 | # read request from server 105 | res = connection.catch_connection_errors do 106 | begin 107 | unpacker = MessagePack::IOUnpacker.new(connection.socket) 108 | msg = read_msg_id(unpacker) 109 | unless msgid == msg 110 | connection.close 111 | raise SimpleRpc::ProtocallError.new("unexpected msgid: expected #{msgid}, but got #{msg}") 112 | end 113 | 114 | MessagePack::NodeUnpacker.new(unpacker.read_node) 115 | rescue ex : MessagePack::TypeCastError | MessagePack::UnexpectedByteError 116 | connection.close 117 | raise SimpleRpc::ProtocallError.new(ex.message) 118 | end 119 | end 120 | 121 | res 122 | end 123 | end 124 | 125 | protected def with_connection 126 | connection = get_connection 127 | connection.socket # establish connection if needed 128 | yield(connection) 129 | ensure 130 | if conn = connection 131 | release_connection(connection) 132 | end 133 | end 134 | 135 | protected def create_connection : Connection 136 | Connection.new(@host, @port, @unixsocket, @ssl_context, @command_timeout, @connect_timeout, @create_connection_retries, @create_connection_retry_interval) 137 | end 138 | 139 | protected def pool! : ConnectionPool(Connection) 140 | @pool.not_nil! 141 | end 142 | 143 | protected def get_connection : Connection 144 | case @mode 145 | when Mode::Pool 146 | _pool = pool! 147 | begin 148 | _pool.checkout 149 | rescue IO::TimeoutError 150 | # not free connection in the pool 151 | raise SimpleRpc::PoolTimeoutError.new("No free connection (used #{_pool.size} of #{_pool.capacity}) after timeout of #{_pool.timeout}s") 152 | end 153 | when Mode::Single 154 | @single ||= create_connection 155 | else 156 | create_connection 157 | end 158 | end 159 | 160 | protected def release_connection(conn) 161 | case @mode 162 | when Mode::ConnectPerRequest 163 | conn.close 164 | when Mode::Pool 165 | pool!.checkin(conn) 166 | else 167 | # skip 168 | end 169 | end 170 | 171 | protected def raw_notify(method) 172 | with_connection do |connection| 173 | try_write_request(connection, method, SimpleRpc::DEFAULT_MSG_ID, true) { |packer| yield packer } 174 | nil 175 | end 176 | end 177 | 178 | # write header to server, but with one reconnection attempt, 179 | # because connection can be outdated for not ConnectPerRequest modes 180 | protected def try_write_request(connection, method, msgid, notify = false) 181 | # write request to server 182 | if @mode.connect_per_request? 183 | write_request(connection, method, msgid, notify) { |packer| yield packer } 184 | else 185 | begin 186 | write_request(connection, method, msgid, notify) { |packer| yield packer } 187 | rescue SimpleRpc::ConnectionError 188 | # reconnecting here, if needed 189 | write_request(connection, method, msgid, notify) { |packer| yield packer } 190 | end 191 | end 192 | end 193 | 194 | protected def write_request(conn, method, msgid, notify = false) 195 | conn.catch_connection_errors do 196 | write_header(conn, method, msgid, notify) { |packer| yield packer } 197 | end 198 | end 199 | 200 | protected def write_header(conn, method, msgid = SimpleRpc::DEFAULT_MSG_ID, notify = false) 201 | sock = conn.socket 202 | packer = MessagePack::Packer.new(sock) 203 | if notify 204 | packer.write_array_start(SimpleRpc::NOTIFY_SIZE) 205 | packer.write(SimpleRpc::NOTIFY) 206 | packer.write(method) 207 | yield packer 208 | else 209 | packer.write_array_start(SimpleRpc::REQUEST_SIZE) 210 | packer.write(SimpleRpc::REQUEST) 211 | packer.write(msgid) 212 | packer.write(method) 213 | yield packer 214 | end 215 | sock.flush 216 | true 217 | end 218 | 219 | protected def read_msg_id(unpacker) : UInt32 220 | size = unpacker.read_array_size 221 | unpacker.finish_token! 222 | 223 | unless size == SimpleRpc::RESPONSE_SIZE 224 | raise MessagePack::TypeCastError.new("Unexpected result array size, should #{SimpleRpc::RESPONSE_SIZE}, not #{size}") 225 | end 226 | 227 | id = Int8.new(unpacker) 228 | 229 | unless id == SimpleRpc::RESPONSE 230 | raise MessagePack::TypeCastError.new("Unexpected message result sign #{id}") 231 | end 232 | 233 | msgid = UInt32.new(unpacker) 234 | 235 | msg = Union(String | Nil).new(unpacker) 236 | if msg 237 | unpacker.skip_value # skip empty result 238 | raise SimpleRpc::RuntimeError.new(msg) 239 | end 240 | 241 | msgid 242 | end 243 | 244 | def close 245 | case @mode 246 | when Mode::Pool 247 | pool!.@pool.each(&.close) 248 | when Mode::Single 249 | @single.try(&.close) 250 | @single = nil 251 | else 252 | # skip 253 | end 254 | end 255 | 256 | private class Connection 257 | getter socket : TCPSocket | UNIXSocket | OpenSSL::SSL::Socket::Client | Nil 258 | getter connection_recreate_attempt 259 | 260 | def initialize(@host : String = "127.0.0.1", 261 | @port : Int32 = 9999, 262 | @unixsocket : String? = nil, 263 | @ssl_context : OpenSSL::SSL::Context::Client? = nil, 264 | @command_timeout : Float64? = nil, 265 | @connect_timeout : Float64? = nil, 266 | @create_connection_retries = 0, 267 | @create_connection_retry_interval = 0.5) 268 | @connection_recreate_attempt = 0 269 | end 270 | 271 | def socket 272 | @socket ||= retried_connect 273 | end 274 | 275 | protected def retried_connect : TCPSocket | UNIXSocket | OpenSSL::SSL::Socket::Client 276 | @connection_recreate_attempt = 0 277 | while true 278 | begin 279 | return connect 280 | rescue ex : SimpleRpc::CannotConnectError 281 | if @connection_recreate_attempt >= @create_connection_retries 282 | raise ex 283 | else 284 | sleep(@create_connection_retry_interval) 285 | @connection_recreate_attempt += 1 286 | end 287 | end 288 | end 289 | end 290 | 291 | protected def connect : TCPSocket | UNIXSocket | OpenSSL::SSL::Socket::Client 292 | _socket = if us = @unixsocket 293 | UNIXSocket.new(us) 294 | else 295 | TCPSocket.new @host, @port, connect_timeout: @connect_timeout 296 | end 297 | 298 | if t = @command_timeout 299 | _socket.read_timeout = t 300 | _socket.write_timeout = t 301 | end 302 | _socket.read_buffering = true 303 | _socket.sync = false 304 | 305 | if (ssl_context = @ssl_context) && _socket.is_a?(TCPSocket) 306 | _socket = OpenSSL::SSL::Socket::Client.new(_socket, ssl_context) 307 | end 308 | 309 | _socket 310 | rescue ex : IO::TimeoutError | Socket::Error | IO::Error | OpenSSL::Error 311 | raise SimpleRpc::CannotConnectError.new("#{ex.class}: #{ex.message}") 312 | end 313 | 314 | def catch_connection_errors 315 | yield 316 | rescue ex : IO::TimeoutError 317 | close 318 | raise SimpleRpc::CommandTimeoutError.new("Command timed out") 319 | rescue ex : Socket::Error | IO::Error | MessagePack::EofError | OpenSSL::Error 320 | close 321 | raise SimpleRpc::ConnectionLostError.new("#{ex.class}: #{ex.message}") 322 | end 323 | 324 | def connected? 325 | @socket != nil 326 | end 327 | 328 | def close 329 | @socket.try(&.close) rescue nil 330 | @socket = nil 331 | end 332 | end 333 | end 334 | -------------------------------------------------------------------------------- /src/simple_rpc/context.cr: -------------------------------------------------------------------------------- 1 | require "log" 2 | 3 | class SimpleRpc::Context 4 | record RawMsgpack, data : Bytes 5 | record IOMsgpack, io : IO 6 | record RawSocketResponse 7 | 8 | property method : String 9 | getter unpacker : MessagePack::IOUnpacker 10 | getter args_count : UInt32 11 | 12 | getter msgid : UInt32 13 | getter io_with_args : IO::Memory 14 | getter io : IO 15 | getter notify : Bool 16 | @logger : Log? 17 | @created_at : Time 18 | 19 | def initialize(@msgid, @method, @io_with_args, @io, @notify, @logger = nil, @created_at = Time.local) 20 | @unpacker = MessagePack::IOUnpacker.new(@io_with_args.rewind) 21 | @args_count = 0 22 | end 23 | 24 | def read_args_count 25 | @args_count = @unpacker.read_array_size 26 | @unpacker.finish_token! 27 | end 28 | 29 | def write_default_response 30 | packer = MessagePack::Packer.new(@io) 31 | packer.write_array_start(SimpleRpc::RESPONSE_SIZE) 32 | packer.write(SimpleRpc::RESPONSE) 33 | packer.write(@msgid) 34 | packer.write(nil) 35 | end 36 | 37 | def write_result(res) 38 | return if @notify 39 | 40 | case res 41 | when RawMsgpack 42 | write_default_response 43 | @io.write(res.data) 44 | when IOMsgpack 45 | write_default_response 46 | IO.copy(res.io, @io) 47 | when RawSocketResponse 48 | # do nothing 49 | # just flush 50 | else 51 | write_default_response 52 | res.to_msgpack(@io) 53 | end 54 | 55 | @io.flush 56 | 57 | if l = @logger 58 | l.info { "SimpleRpc: #{method} (in #{Time.local - @created_at})" } 59 | end 60 | 61 | nil 62 | end 63 | 64 | def write_error(msg) 65 | return if @notify 66 | 67 | {SimpleRpc::RESPONSE, @msgid, msg, nil}.to_msgpack(@io) 68 | @io.flush 69 | 70 | if l = @logger 71 | l.error { "SimpleRpc: #{method}: #{msg} (in #{Time.local - @created_at})" } 72 | end 73 | 74 | nil 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /src/simple_rpc/error.cr: -------------------------------------------------------------------------------- 1 | module SimpleRpc 2 | class Errors < Exception; end # common class for all SimpleRpc errors 3 | 4 | class CommandTimeoutError < Errors; end # when client wait too long for answer from server 5 | 6 | class ConnectionError < Errors; end # common class for all connection errors 7 | 8 | class ConnectionLostError < ConnectionError; end # when client lost connection to server 9 | 10 | class CannotConnectError < ConnectionError; end # when client cant connect to server 11 | 12 | class PoolTimeoutError < ConnectionError; end # when no free connections in pool 13 | 14 | class RuntimeError < Errors; end # when task crashed on server 15 | 16 | class ProtocallError < Errors; end # when problem in client-server interaction 17 | 18 | class TypeCastError < Errors; end # when return type not casted to requested 19 | end 20 | -------------------------------------------------------------------------------- /src/simple_rpc/proto.cr: -------------------------------------------------------------------------------- 1 | require "msgpack" 2 | 3 | module SimpleRpc::Proto 4 | macro included 5 | SIMPLE_RPC_HASH = Hash(String, (SimpleRpc::Context ->) ).new 6 | class Client < SimpleRpc::Client; end 7 | class Server < SimpleRpc::Server; end 8 | 9 | @simple_rpc_context : SimpleRpc::Context? 10 | 11 | protected def simple_rpc_context=(ctx) 12 | @simple_rpc_context = ctx 13 | end 14 | 15 | protected def simple_rpc_context 16 | @simple_rpc_context.not_nil! 17 | end 18 | 19 | macro finished 20 | def self.add_wrappers 21 | \{% for m in @type.methods %} 22 | \{% if m.visibility.stringify == ":public" %} 23 | \{{@type}}::SIMPLE_RPC_HASH["\{{m.name}}"] = ->(ctx : SimpleRpc::Context) { __simple_rpc_wrapper_\{{m.name}}(ctx) } 24 | \{% end %} 25 | \{% end %} 26 | SIMPLE_RPC_HASH[SimpleRpc::INTERNAL_PING_METHOD] = ->(ctx : SimpleRpc::Context) { ctx.write_result(true) } 27 | SIMPLE_RPC_HASH.rehash 28 | end 29 | 30 | \{% for m in @type.methods %} 31 | \{% if m.visibility.stringify == ":public" %} 32 | def self.__simple_rpc_wrapper_\{{m.name}}(ctx : SimpleRpc::Context) 33 | args_need_count = \{{ m.args.size.id }} 34 | if ctx.args_count != args_need_count 35 | return ctx.write_error(\\%Q[ArgumentError in \{{m.name}}\{{m.args.id}}: bad arguments count: expected #{args_need_count}, but got #{ctx.args_count}]) 36 | end 37 | 38 | \{% if m.args.size > 0 %} 39 | \{% for arg in m.args %} 40 | \{% if arg.restriction %} 41 | \%_var_\{{arg.id} = 42 | begin 43 | Union(\{{ arg.restriction }}).new(ctx.unpacker) 44 | rescue ex : MessagePack::TypeCastError 45 | token = ctx.unpacker.@lexer.@token 46 | return ctx.write_error(\\%Q[ArgumentError in \{{m.name}}\{{m.args.id}}: bad argument \{{arg.name}}: '#{ex.message}' (at #{MessagePack::Token.to_s(token)})]) 47 | end 48 | \{% else %} 49 | \{% raise "argument '#{arg}' in method '#{m.name}' must have a type restriction" %} 50 | \{% end %} 51 | \{% end %} 52 | \{% end %} 53 | 54 | res = begin 55 | obj = \{{@type}}.new 56 | obj.simple_rpc_context = ctx 57 | obj.\{{m.name}}(\{% for arg in m.args %} \%_var_\{{arg.id}, \{% end %}) 58 | rescue ex 59 | if ENV["SIMPLE_RPC_BACKTRACE"]? == "1" 60 | msg = \\%Q[RuntimeError in #{ctx.method}\{{m.args.id}}: '#{ex.message}' [#{ex.backtrace.join(", ")}]] 61 | else 62 | msg = \\%Q[RuntimeError in #{ctx.method}\{{m.args.id}}: '#{ex.message}' (run server with env SIMPLE_RPC_BACKTRACE=1 to see backtrace)] 63 | end 64 | return ctx.write_error(msg) 65 | end 66 | 67 | return ctx.write_result(res) 68 | end 69 | \{% end %} 70 | \{% end %} 71 | 72 | class Server 73 | def add_wrappers 74 | \{{@type}}.add_wrappers 75 | end 76 | 77 | def method_find(method : String) : (SimpleRpc::Context ->)? 78 | \{{@type}}::SIMPLE_RPC_HASH[method]? 79 | end 80 | end 81 | 82 | class Client 83 | \{% for m in @type.methods %} 84 | \{% if m.visibility.stringify == ":public" %} 85 | \{% if !m.return_type %} \{% raise "method '#{m.name}' must have a return type" %} \{% end %} 86 | \{% args_list = m.args.join(", ").id %} 87 | \{% args = m.args.map { |a| a.name }.join(", ").id %} 88 | def \{{m.name}}(\{{args_list}}) 89 | request(\{{m.return_type.id}}, \{{m.name.stringify}}, \{{args.id}}) 90 | end 91 | 92 | def \{{m.name}}!(\{{args_list}}) 93 | request!(\{{m.return_type.id}}, \{{m.name.stringify}}, \{{args.id}}) 94 | end 95 | \{% end %} 96 | \{% end %} 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /src/simple_rpc/result.cr: -------------------------------------------------------------------------------- 1 | struct SimpleRpc::Result(T) 2 | getter error, value 3 | 4 | def initialize(@error : Errors? = nil, @value : T | Nil = nil) 5 | end 6 | 7 | def ok? 8 | @error.nil? 9 | end 10 | 11 | def error! 12 | @error.not_nil! 13 | end 14 | 15 | def message! 16 | if e = @error 17 | "#{e.class}: #{e.message}" 18 | else 19 | "" 20 | end 21 | end 22 | 23 | def value! 24 | @value.not_nil! 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /src/simple_rpc/server.cr: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "msgpack" 3 | require "openssl" 4 | require "log" 5 | 6 | abstract class SimpleRpc::Server 7 | @server : TCPServer | UNIXServer | Nil 8 | 9 | def initialize(@host : String = "127.0.0.1", @port : Int32 = 9999, @unixsocket : String? = nil, @ssl_context : OpenSSL::SSL::Context::Server? = nil, 10 | @logger : Log? = nil, @close_connection_after_request = false) 11 | after_initialize 12 | add_wrappers 13 | end 14 | 15 | protected def after_initialize 16 | end 17 | 18 | protected def add_wrappers 19 | end 20 | 21 | private def read_context(io) : Context 22 | unpacker = MessagePack::IOUnpacker.new(io) 23 | size = unpacker.read_array_size 24 | unpacker.finish_token! 25 | 26 | request = (size == SimpleRpc::REQUEST_SIZE) 27 | unless request || size == SimpleRpc::NOTIFY_SIZE 28 | raise MessagePack::TypeCastError.new("Unexpected request array size, should be #{SimpleRpc::REQUEST_SIZE} or #{SimpleRpc::NOTIFY_SIZE}, not #{size}") 29 | end 30 | 31 | id = Int8.new(unpacker) 32 | 33 | if request 34 | raise MessagePack::TypeCastError.new("Unexpected message request sign #{id}") unless id == SimpleRpc::REQUEST 35 | else 36 | raise MessagePack::TypeCastError.new("Unexpected message notify sign #{id}") unless id == SimpleRpc::NOTIFY 37 | end 38 | 39 | msgid = request ? UInt32.new(unpacker) : SimpleRpc::DEFAULT_MSG_ID 40 | method = String.new(unpacker) 41 | 42 | io_with_args = IO::Memory.new 43 | MessagePack::Copy.new(io, io_with_args).copy_object 44 | io_with_args.rewind 45 | 46 | Context.new(msgid, method, io_with_args, io, !request, @logger) 47 | end 48 | 49 | protected def handle(io) 50 | io.read_buffering = true if io.responds_to?(:read_buffering) 51 | io.sync = false if io.responds_to?(:sync=) 52 | 53 | if ssl_context = @ssl_context 54 | io = OpenSSL::SSL::Socket::Server.new(io, ssl_context) 55 | end 56 | 57 | loop do 58 | ctx = read_context(io) 59 | ctx.read_args_count 60 | handle_request(ctx) 61 | break if @close_connection_after_request 62 | end 63 | rescue ex : IO::Error | Socket::Error | MessagePack::TypeCastError | MessagePack::UnexpectedByteError | OpenSSL::Error 64 | if l = @logger 65 | l.error { "SimpleRpc: protocol ERROR #{ex.message}" } 66 | end 67 | rescue ex : MessagePack::EofError 68 | ensure 69 | io.close rescue nil 70 | end 71 | 72 | def _handle(client) 73 | handle(client) 74 | end 75 | 76 | protected def before_run 77 | end 78 | 79 | def run 80 | @server = server = if us = @unixsocket 81 | UNIXServer.new(us) 82 | else 83 | TCPServer.new @host, @port 84 | end 85 | 86 | before_run 87 | loop do 88 | if client = server.accept? 89 | spawn _handle(client) 90 | else 91 | close 92 | break 93 | end 94 | end 95 | end 96 | 97 | abstract def method_find(method : String) : (SimpleRpc::Context ->)? 98 | 99 | protected def handle_request(ctx : Context) 100 | if result = method_find ctx.method 101 | result.call(ctx) 102 | else 103 | ctx.write_error("method '#{ctx.method}' not found") 104 | end 105 | end 106 | 107 | def close 108 | @server.try(&.close) rescue nil 109 | @server = nil 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /src/simple_rpc/server_proxy.cr: -------------------------------------------------------------------------------- 1 | # Sometimes you need to proxy requests to multiple servers, for load balancing 2 | # This class exactly for this 3 | # it change server for every request (by round robin method) 4 | # and doesn't matter what client connection is, persistent or not. 5 | # It also marks dead servers, and reshedule request to another server. 6 | # Perfomance when proxing to 3 servers down from 154 Krps to 71 Krps 7 | # 8 | # proxy = SimpleRpc::ServerProxy.new("127.0.0.1", 9000) 9 | # proxy.set_ports([9001, 9002, 9003]) 10 | # proxy.check_dead_ports_in = 5.seconds 11 | # proxy.run 12 | 13 | class SimpleRpc::ServerProxy < SimpleRpc::Server 14 | # Set proxing ports 15 | # this method can be called at any time, not exactly at beginning of the run 16 | def set_ports(ports : Array(Int32)) 17 | @ports = ports.sort 18 | 19 | @clients.keys.each do |port| 20 | unless ports.includes?(port) 21 | @clients[port].close 22 | @clients.delete(port) 23 | mark_port_dead(port) 24 | end 25 | end 26 | 27 | @ports.each do |port| 28 | unless @clients[port]? 29 | @clients[port] = new_client(port) 30 | add_alive_port(port) 31 | end 32 | end 33 | end 34 | 35 | property check_dead_ports_in = 30.seconds 36 | property print_stats_in = 60.seconds 37 | @ports = Array(Int32).new 38 | @clients = Hash(Int32, SimpleRpc::Client).new 39 | @alive_ports = Array(Int32).new 40 | @alive_ports_current = 0 41 | 42 | protected def before_run 43 | if check_dead_ports_in.to_f > 0 44 | spawn do 45 | loop do 46 | break unless @server 47 | sleep check_dead_ports_in 48 | check_dead_ports 49 | end 50 | end 51 | end 52 | 53 | if print_stats_in.to_f > 0 54 | spawn do 55 | loop do 56 | break unless @server 57 | sleep print_stats_in 58 | loggin "Stat [#{@ports.size}, #{@alive_ports.size}], All ports: #{@ports}, Alive ports: #{@alive_ports}" 59 | end 60 | end 61 | end 62 | end 63 | 64 | protected def new_client(port) 65 | SimpleRpc::Client.new(@host, port, mode: :pool, connect_timeout: 1.0) 66 | end 67 | 68 | protected def add_alive_port(port) 69 | @alive_ports << port 70 | end 71 | 72 | protected def mark_port_dead(port) 73 | @alive_ports.delete(port) 74 | end 75 | 76 | protected def get_next_client 77 | @alive_ports_current = 0 if @alive_ports_current >= @alive_ports.size 78 | port = @alive_ports[@alive_ports_current] 79 | client = @clients[port] 80 | @alive_ports_current += 1 81 | {client, port} 82 | end 83 | 84 | protected def loggin(msg) 85 | puts "[#{Time.local}] -- Proxy(#{@port}): #{msg}" 86 | end 87 | 88 | def method_find(method : String) : (SimpleRpc::Context ->)? 89 | end 90 | 91 | protected def handle_request(ctx : Context) 92 | # special method added by proxy 93 | case ctx.method 94 | when "__simple_rpc_proxy_ports__" 95 | return ctx.write_result({@ports, @alive_ports}) 96 | end 97 | 98 | handle_cxt ctx 99 | end 100 | 101 | protected def handle_cxt(ctx : Context, reschedule_count = 0) 102 | return ctx.write_error("Proxy: No alive ports") if @alive_ports.size == 0 103 | return ctx.write_error("Proxy: All ports busy") if reschedule_count > @ports.size 104 | 105 | client, port = get_next_client 106 | begin 107 | return req(client, ctx) 108 | rescue SimpleRpc::ConnectionLostError # reconnecting here, if needed 109 | return req(client, ctx) 110 | end 111 | 112 | # unreachable actually 113 | false 114 | rescue SimpleRpc::ConnectionError 115 | mark_port_dead(port) 116 | loggin "Dead connection #{port}, reschedule" 117 | return handle_cxt(ctx, reschedule_count + 1) 118 | rescue SimpleRpc::CommandTimeoutError 119 | loggin "Timeout #{port}, reschedule" 120 | return handle_cxt(ctx, reschedule_count + 1) 121 | end 122 | 123 | protected def req(client, ctx) 124 | client.with_connection do |connection| 125 | connection.catch_connection_errors do 126 | client.write_header(connection, ctx.method, ctx.msgid, ctx.notify) do |packer| 127 | MessagePack::Copy.new(ctx.io_with_args.rewind, connection.socket).copy_object # copy array of arguments 128 | end 129 | 130 | unless ctx.notify 131 | MessagePack::Copy.new(connection.socket, ctx.io).copy_object # copy body 132 | end 133 | 134 | ctx.io.flush 135 | end 136 | end 137 | 138 | true 139 | end 140 | 141 | protected def check_dead_ports 142 | return if @alive_ports.size == @ports.size 143 | 144 | dead_ports = @ports - @alive_ports 145 | dead_ports.each do |port| 146 | client = @clients[port] 147 | result = client.request(Bool, SimpleRpc::INTERNAL_PING_METHOD) 148 | if result.ok? && result.value == true 149 | loggin("Alive #{port}") 150 | add_alive_port(port) 151 | end 152 | end 153 | end 154 | end 155 | --------------------------------------------------------------------------------