protein 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +32 -0
- data/lib/protein.rb +17 -0
- data/lib/protein/amqp_adapter.rb +132 -0
- data/lib/protein/client.rb +82 -0
- data/lib/protein/errors.rb +16 -0
- data/lib/protein/get_const.rb +19 -0
- data/lib/protein/http_adapter.rb +94 -0
- data/lib/protein/payload.rb +71 -0
- data/lib/protein/pointer.rb +93 -0
- data/lib/protein/processor.rb +67 -0
- data/lib/protein/proto_compiler.rb +42 -0
- data/lib/protein/router.rb +55 -0
- data/lib/protein/server.rb +30 -0
- data/lib/protein/service.rb +130 -0
- data/lib/protein/service_error.rb +15 -0
- data/lib/protein/transport.rb +27 -0
- data/lib/protein/version.rb +3 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 640ce556cbb7f7709579ad076cd242880081d3bd
|
4
|
+
data.tar.gz: 682c86726085e8ab7c794a2bea141726a718edf9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d1015ee49c77d00f0e16d9816f2f80a05d36750844482c9fc1d0a4ef54658e534816b41c3749ee2d39f59ae6ae77fb2c3312567060c46bcb1df768541b0e2497
|
7
|
+
data.tar.gz: 41d4c03428c44c138ff8e9be1a7d2161393c0ab6cc9c610a58feefd7bcffcbd9951f166ea565a01b4dca48192167b5fb98f40d12b14d163dcbc932bddbdb390e
|
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Protein for Ruby
|
2
|
+
|
3
|
+
***Multi-platform remote procedure call (RPC) system based on Protocol Buffers***
|
4
|
+
|
5
|
+
Features:
|
6
|
+
|
7
|
+
- Implement RPC services and clients for Elixir and Ruby platforms
|
8
|
+
- Call remote services using unified, simple client API
|
9
|
+
- Call to services for an immediate response or push non-blocking requests to async services
|
10
|
+
- Define services via unified, configurable DSL
|
11
|
+
- Define service input/outputs using the widely acclaimed Google Protocol Buffers format
|
12
|
+
- Transport your calls via HTTP or AMQP transports
|
13
|
+
|
14
|
+
Packages:
|
15
|
+
|
16
|
+
- [Protein for Elixir](http://github.com/surgeventures/protein-elixir)
|
17
|
+
- [Protein for Ruby](http://github.com/surgeventures/protein-ruby)
|
18
|
+
|
19
|
+
## Getting Started
|
20
|
+
|
21
|
+
Add `protein` as a dependency to your project in `Gemfile`:
|
22
|
+
|
23
|
+
```elixir
|
24
|
+
gem "protein", "~> x.x.x"
|
25
|
+
```
|
26
|
+
|
27
|
+
Then run `bundle install` to fetch it.
|
28
|
+
|
29
|
+
## Documentation
|
30
|
+
|
31
|
+
We don't provide documentation for Ruby package at the moment. Please look for answers in the code
|
32
|
+
and in documentation for Elixir package - basic concepts and API shapes are akin.
|
data/lib/protein.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "protein/errors"
|
2
|
+
require "protein/proto_compiler"
|
3
|
+
require "protein/get_const"
|
4
|
+
|
5
|
+
require "protein/router"
|
6
|
+
require "protein/client"
|
7
|
+
require "protein/server"
|
8
|
+
require "protein/processor"
|
9
|
+
require "protein/payload"
|
10
|
+
|
11
|
+
require "protein/pointer"
|
12
|
+
require "protein/service_error"
|
13
|
+
require "protein/service"
|
14
|
+
|
15
|
+
require "protein/transport"
|
16
|
+
require "protein/http_adapter"
|
17
|
+
require "protein/amqp_adapter"
|
@@ -0,0 +1,132 @@
|
|
1
|
+
require "bunny"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module Protein
|
5
|
+
class AMQPAdapter
|
6
|
+
class << self
|
7
|
+
def from_hash(hash)
|
8
|
+
if (new_url = hash[:url])
|
9
|
+
url(new_url)
|
10
|
+
end
|
11
|
+
|
12
|
+
if (new_queue = hash[:queue])
|
13
|
+
queue(new_queue)
|
14
|
+
end
|
15
|
+
|
16
|
+
if hash.has_key?(:timeout)
|
17
|
+
timeout(hash[:timeout])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def url(url = nil)
|
22
|
+
@url = url if url
|
23
|
+
@url || raise(DefinitionError, "url is not defined")
|
24
|
+
end
|
25
|
+
|
26
|
+
def queue(queue = nil)
|
27
|
+
@queue = queue if queue
|
28
|
+
@queue || raise(DefinitionError, "queue is not defined")
|
29
|
+
end
|
30
|
+
|
31
|
+
def timeout(timeout = :not_set)
|
32
|
+
@timeout = timeout if timeout != :not_set
|
33
|
+
instance_variable_defined?("@timeout") ? @timeout : 15_000
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_reader :reply_queue
|
37
|
+
attr_accessor :response, :call_id
|
38
|
+
attr_reader :lock, :condition
|
39
|
+
|
40
|
+
def call(request_payload)
|
41
|
+
prepare_client
|
42
|
+
|
43
|
+
@call_id = SecureRandom.uuid
|
44
|
+
|
45
|
+
@x.publish(request_payload,
|
46
|
+
correlation_id: @call_id,
|
47
|
+
routing_key: @server_queue,
|
48
|
+
reply_to: @reply_queue.name,
|
49
|
+
expiration: timeout)
|
50
|
+
|
51
|
+
self.response = nil
|
52
|
+
lock.synchronize { condition.wait(lock, timeout && timeout * 0.001) }
|
53
|
+
|
54
|
+
if response == nil
|
55
|
+
raise(TransportError, "timeout after #{timeout}ms")
|
56
|
+
elsif response == "ESRV"
|
57
|
+
raise(TransportError, "failed to process the request")
|
58
|
+
else
|
59
|
+
response
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def push(message_payload)
|
64
|
+
prepare_client
|
65
|
+
|
66
|
+
@x.publish(message_payload,
|
67
|
+
routing_key: @server_queue)
|
68
|
+
end
|
69
|
+
|
70
|
+
def serve(router)
|
71
|
+
@conn = Bunny.new(url)
|
72
|
+
@conn.start
|
73
|
+
@ch = @conn.create_channel
|
74
|
+
@q = @ch.queue(queue)
|
75
|
+
@x = @ch.default_exchange
|
76
|
+
|
77
|
+
Rails.logger.info "Connected to #{url}, serving RPC calls from #{queue}"
|
78
|
+
|
79
|
+
loop do
|
80
|
+
begin
|
81
|
+
@q.subscribe(block: true, manual_ack: true) do |delivery_info, properties, payload|
|
82
|
+
begin
|
83
|
+
@error = nil
|
84
|
+
response = Processor.call(router, payload)
|
85
|
+
rescue Exception => error
|
86
|
+
@error = error
|
87
|
+
response = "ESRV"
|
88
|
+
end
|
89
|
+
|
90
|
+
if response
|
91
|
+
@x.publish(response,
|
92
|
+
routing_key: properties.reply_to,
|
93
|
+
correlation_id: properties.correlation_id)
|
94
|
+
end
|
95
|
+
|
96
|
+
@ch.ack(delivery_info.delivery_tag)
|
97
|
+
|
98
|
+
raise(@error) if @error
|
99
|
+
end
|
100
|
+
rescue StandardError => e
|
101
|
+
Rails.logger.info "RPC server error: #{e.inspect}, restarting the server in 5s..."
|
102
|
+
sleep 5
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def prepare_client
|
110
|
+
return if @conn
|
111
|
+
|
112
|
+
@conn = Bunny.new(url)
|
113
|
+
@conn.start
|
114
|
+
@ch = @conn.create_channel
|
115
|
+
@x = @ch.default_exchange
|
116
|
+
@server_queue = queue
|
117
|
+
@reply_queue = @ch.queue("", exclusive: true)
|
118
|
+
@lock = Mutex.new
|
119
|
+
@condition = ConditionVariable.new
|
120
|
+
|
121
|
+
that = self
|
122
|
+
|
123
|
+
@reply_queue.subscribe do |delivery_info, properties, payload|
|
124
|
+
if properties[:correlation_id] == that.call_id
|
125
|
+
that.response = payload
|
126
|
+
that.lock.synchronize{that.condition.signal}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Protein
|
2
|
+
class Client
|
3
|
+
class << self
|
4
|
+
def route(router)
|
5
|
+
@router = Router.define(router)
|
6
|
+
end
|
7
|
+
|
8
|
+
def service(service)
|
9
|
+
@router ||= Class.new(Router)
|
10
|
+
@router.service(service)
|
11
|
+
end
|
12
|
+
|
13
|
+
def proto(proto)
|
14
|
+
service = Class.new(Service)
|
15
|
+
service.proto(proto)
|
16
|
+
|
17
|
+
@router ||= Class.new(Router)
|
18
|
+
@router.service(service)
|
19
|
+
end
|
20
|
+
|
21
|
+
def router
|
22
|
+
GetConst.call(@router)
|
23
|
+
end
|
24
|
+
|
25
|
+
def transport(transport, opts = {})
|
26
|
+
@transport_class = Transport.define(transport, opts)
|
27
|
+
end
|
28
|
+
|
29
|
+
def transport_class
|
30
|
+
GetConst.call(@transport_class)
|
31
|
+
end
|
32
|
+
|
33
|
+
def call(request)
|
34
|
+
service_class = router.resolve_by_request(request)
|
35
|
+
|
36
|
+
raise(ArgumentError, "called to non-responding service") unless service_class.response?
|
37
|
+
|
38
|
+
service_name = service_class.service_name
|
39
|
+
request_class = service_class.request_class
|
40
|
+
request_buf = request_class.encode(request)
|
41
|
+
request_payload = Payload::Request.encode(service_name, request_buf)
|
42
|
+
|
43
|
+
response_payload = transport_class.call(request_payload)
|
44
|
+
response_buf, errors = Payload::Response.decode(response_payload)
|
45
|
+
service_instance = service_class.new(request)
|
46
|
+
|
47
|
+
if response_buf
|
48
|
+
response = service_class.response_class.decode(response_buf)
|
49
|
+
service_instance.resolve(response)
|
50
|
+
elsif errors
|
51
|
+
service_instance.reject(errors)
|
52
|
+
end
|
53
|
+
|
54
|
+
service_instance
|
55
|
+
end
|
56
|
+
|
57
|
+
def call!(request)
|
58
|
+
service_instance = call(request)
|
59
|
+
if service_instance.failure?
|
60
|
+
raise(CallError, service_instance.errors)
|
61
|
+
end
|
62
|
+
|
63
|
+
service_instance.response
|
64
|
+
end
|
65
|
+
|
66
|
+
def push(request)
|
67
|
+
service_class = router.resolve_by_request(request)
|
68
|
+
|
69
|
+
raise(ArgumentError, "pushed to responding service") if service_class.response?
|
70
|
+
|
71
|
+
service_name = service_class.service_name
|
72
|
+
request_class = service_class.request_class
|
73
|
+
request_buf = request_class.encode(request)
|
74
|
+
request_payload = Payload::Request.encode(service_name, request_buf)
|
75
|
+
|
76
|
+
transport_class.push(request_payload)
|
77
|
+
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Protein
|
2
|
+
class CallError < StandardError
|
3
|
+
def initialize(errors)
|
4
|
+
super(
|
5
|
+
errors.map do |error|
|
6
|
+
error.reason.inspect + (error.pointer ? " (at #{error.pointer})" : "")
|
7
|
+
end.join(", ")
|
8
|
+
)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
class DefinitionError < StandardError; end
|
12
|
+
class PointerError < StandardError; end
|
13
|
+
class ProcessingError < StandardError; end
|
14
|
+
class RoutingError < StandardError; end
|
15
|
+
class TransportError < StandardError; end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Protein
|
2
|
+
class GetConst
|
3
|
+
class << self
|
4
|
+
def call(input)
|
5
|
+
if input.is_a?(String)
|
6
|
+
input.constantize
|
7
|
+
elsif input != nil
|
8
|
+
input
|
9
|
+
else
|
10
|
+
raise DefinitionError, "unset required option"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def map(array)
|
15
|
+
array.map { |input| call(input) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Protein
|
5
|
+
class HTTPAdapter
|
6
|
+
HTTPS_SCHEME = "https".freeze
|
7
|
+
|
8
|
+
class Middleware
|
9
|
+
def initialize(router, secret)
|
10
|
+
@router = router
|
11
|
+
@secret = secret
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
check_secret(env)
|
16
|
+
|
17
|
+
request_payload = env["rack.input"].read
|
18
|
+
response_payload = Processor.call(@router, request_payload)
|
19
|
+
|
20
|
+
["200", {}, [response_payload]]
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def check_secret(env)
|
26
|
+
return if @secret == env["HTTP_X_RPC_SECRET"]
|
27
|
+
|
28
|
+
raise(TransportError, "invalid secret")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
def from_hash(hash)
|
34
|
+
if (new_url = hash[:url])
|
35
|
+
url(new_url)
|
36
|
+
end
|
37
|
+
|
38
|
+
if (new_path = hash[:path])
|
39
|
+
path(new_path)
|
40
|
+
end
|
41
|
+
|
42
|
+
if (new_secret = hash[:secret])
|
43
|
+
secret(new_secret)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def url(url = nil)
|
48
|
+
if url
|
49
|
+
@url = url
|
50
|
+
@path = URI(url).path
|
51
|
+
end
|
52
|
+
|
53
|
+
@url || raise(DefinitionError, "url is not defined")
|
54
|
+
end
|
55
|
+
|
56
|
+
def path(path = nil)
|
57
|
+
@path = path if path
|
58
|
+
@path || raise(DefinitionError, "path is not defined")
|
59
|
+
end
|
60
|
+
|
61
|
+
def secret(secret = nil)
|
62
|
+
@secret = secret if secret
|
63
|
+
@secret || raise(DefinitionError, "secret is not defined")
|
64
|
+
end
|
65
|
+
|
66
|
+
def call(request_payload)
|
67
|
+
make_http_request(request_payload, build_headers())
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def build_headers
|
73
|
+
{ "X-RPC-Secret" => secret }
|
74
|
+
end
|
75
|
+
|
76
|
+
def make_http_request(body, headers)
|
77
|
+
uri = URI.parse(url)
|
78
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
79
|
+
http.use_ssl = uri.scheme == HTTPS_SCHEME
|
80
|
+
http_request = Net::HTTP::Post.new(uri.path, headers)
|
81
|
+
http_request.body = body
|
82
|
+
http_response = http.request(http_request)
|
83
|
+
|
84
|
+
unless http_response.is_a?(Net::HTTPSuccess)
|
85
|
+
raise(TransportError, "HTTP request failed with code #{http_response.code}")
|
86
|
+
end
|
87
|
+
|
88
|
+
http_response.body
|
89
|
+
rescue SystemCallError => e
|
90
|
+
raise(TransportError, e.to_s)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Protein
|
5
|
+
module Payload
|
6
|
+
class Request
|
7
|
+
class << self
|
8
|
+
def encode(service_name, request_buf)
|
9
|
+
JSON.dump({
|
10
|
+
"service_name" => service_name,
|
11
|
+
"request_buf_b64" => Base64.strict_encode64(request_buf)
|
12
|
+
})
|
13
|
+
end
|
14
|
+
|
15
|
+
def decode(payload)
|
16
|
+
hash = JSON.parse(payload)
|
17
|
+
service_name = hash["service_name"]
|
18
|
+
request_buf_b64 = hash["request_buf_b64"]
|
19
|
+
request_buf = Base64.strict_decode64(request_buf_b64)
|
20
|
+
|
21
|
+
[service_name, request_buf]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Response
|
27
|
+
class << self
|
28
|
+
def encode(response_buf, errors)
|
29
|
+
JSON.dump({
|
30
|
+
"response_buf_b64" => response_buf && Base64.strict_encode64(response_buf),
|
31
|
+
"errors" => encode_errors(errors)
|
32
|
+
})
|
33
|
+
end
|
34
|
+
|
35
|
+
def decode(payload)
|
36
|
+
hash = JSON.parse(payload)
|
37
|
+
response_buf_b64 = hash["response_buf_b64"]
|
38
|
+
response_buf = response_buf_b64 && Base64.strict_decode64(response_buf_b64)
|
39
|
+
errors = hash["errors"]
|
40
|
+
errors = errors && decode_errors(errors)
|
41
|
+
|
42
|
+
[response_buf, errors]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def encode_errors(errors)
|
48
|
+
errors && errors.map do |error|
|
49
|
+
{
|
50
|
+
"reason" => error.reason.is_a?(Symbol) ? ":#{error.reason}" : error.reason,
|
51
|
+
"pointer" => error.pointer && error.pointer.dump
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def decode_errors(errors)
|
57
|
+
errors && errors.map do |error|
|
58
|
+
reason_string = error["reason"]
|
59
|
+
reason = reason_string =~ /^\:/ ? reason_string[1..-1].to_sym : reason_string
|
60
|
+
pointer = error["pointer"]
|
61
|
+
|
62
|
+
ServiceError.new(
|
63
|
+
reason: reason,
|
64
|
+
pointer: pointer && Pointer.new(nil, "request").load(pointer)
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module Protein
|
2
|
+
class Pointer
|
3
|
+
def initialize(base, base_name = "context", input = nil)
|
4
|
+
@base = base
|
5
|
+
@base_name = base_name
|
6
|
+
|
7
|
+
if input.is_a?(String)
|
8
|
+
parse(input)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def dump
|
13
|
+
@access_path || raise(ProcessingError, "pointer can't be empty")
|
14
|
+
end
|
15
|
+
|
16
|
+
def load(input)
|
17
|
+
@access_path = input.map do |type, key|
|
18
|
+
[type.to_sym, key]
|
19
|
+
end
|
20
|
+
|
21
|
+
traverse_access_path(traverse_values: false)
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse(input)
|
27
|
+
unless input =~ Regexp.new("^#{@base_name}")
|
28
|
+
raise PointerError, "access path should start with `#{@base_name}`"
|
29
|
+
end
|
30
|
+
|
31
|
+
access_path = input.scan(/(\.(\w+))|(\[(\d+)\])|(\[['"](\w+)['"]\])/).map do |match|
|
32
|
+
if (key = match[1])
|
33
|
+
[:struct, key]
|
34
|
+
elsif (key = match[3])
|
35
|
+
[:repeated, key.to_i]
|
36
|
+
elsif (key = match[5])
|
37
|
+
[:map, key]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if access_path.empty?
|
42
|
+
raise PointerError, "access path should not be empty"
|
43
|
+
end
|
44
|
+
|
45
|
+
@access_path = access_path
|
46
|
+
traverse_access_path
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_s
|
50
|
+
@access_strings.join
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect_path
|
54
|
+
@access_values.each_with_index do |access_value, index|
|
55
|
+
if index > 0
|
56
|
+
accessor_type, accessor_key = @access_path[index - 1]
|
57
|
+
puts "Accessing #{accessor_type} with key #{accessor_key.inspect}"
|
58
|
+
end
|
59
|
+
puts "At #{access_value.inspect}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def traverse_access_path(traverse_values: true)
|
66
|
+
if traverse_values
|
67
|
+
access_value = @base
|
68
|
+
access_values = [@base]
|
69
|
+
end
|
70
|
+
access_strings = [@base_name]
|
71
|
+
|
72
|
+
begin
|
73
|
+
@access_path.each do |type, key|
|
74
|
+
case type
|
75
|
+
when :struct
|
76
|
+
access_strings << ".#{key}"
|
77
|
+
access_value = access_value.send(key) if traverse_values
|
78
|
+
when :repeated, :map
|
79
|
+
access_strings << "[#{key.inspect}]"
|
80
|
+
access_value = access_value[key] if traverse_values
|
81
|
+
end
|
82
|
+
|
83
|
+
access_values << access_value if traverse_values
|
84
|
+
end
|
85
|
+
rescue StandardError
|
86
|
+
raise PointerError, "unable to access #{access_strings.join}"
|
87
|
+
end
|
88
|
+
|
89
|
+
@access_values = access_values if traverse_values
|
90
|
+
@access_strings = access_strings
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Protein
|
2
|
+
class Processor
|
3
|
+
class << self
|
4
|
+
def call(router, request_payload)
|
5
|
+
service_name, request_buf = Payload::Request.decode(request_payload)
|
6
|
+
service_class = router.resolve_by_name(service_name)
|
7
|
+
|
8
|
+
if service_class.response?
|
9
|
+
process_and_log_call(service_name, service_class, request_buf)
|
10
|
+
else
|
11
|
+
process_and_log_push(service_name, service_class, request_buf)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def process_and_log_call(service_name, service_class, request_buf)
|
18
|
+
Rails.logger.info "Processing RPC call: #{service_name}"
|
19
|
+
|
20
|
+
start_time = Time.now
|
21
|
+
response_buf, errors = process_call(service_class, request_buf)
|
22
|
+
duration_ms = ((Time.now - start_time) * 1000).round
|
23
|
+
|
24
|
+
Rails.logger.info "#{response_buf ? 'Resolved' : 'Rejected'} in #{duration_ms}ms"
|
25
|
+
|
26
|
+
Payload::Response.encode(response_buf, errors) if service_class.response?
|
27
|
+
end
|
28
|
+
|
29
|
+
def process_call(service_class, request_buf)
|
30
|
+
request_class = service_class.request_class
|
31
|
+
request = request_class.decode(request_buf)
|
32
|
+
service_instance = service_class.new(request)
|
33
|
+
|
34
|
+
service_instance.process
|
35
|
+
|
36
|
+
if service_instance.success?
|
37
|
+
response_class = service_class.response_class
|
38
|
+
response_buf = response_class.encode(service_instance.response)
|
39
|
+
|
40
|
+
[response_buf, nil]
|
41
|
+
else
|
42
|
+
[nil, service_instance.errors]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def process_and_log_push(service_name, service_class, request_buf)
|
47
|
+
Rails.logger.info "Processing RPC push: #{service_name}"
|
48
|
+
|
49
|
+
start_time = Time.now
|
50
|
+
process_push(service_class, request_buf)
|
51
|
+
duration_ms = ((Time.now - start_time) * 1000).round
|
52
|
+
|
53
|
+
Rails.logger.info "Processed in #{duration_ms}ms"
|
54
|
+
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def process_push(service_class, request_buf)
|
59
|
+
request_class = service_class.request_class
|
60
|
+
request = request_class.decode(request_buf)
|
61
|
+
service_instance = service_class.new(request)
|
62
|
+
|
63
|
+
service_instance.process
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Protein
|
2
|
+
class ProtoCompiler
|
3
|
+
class << self
|
4
|
+
def call(proto_directory: "./lib", namespace: nil)
|
5
|
+
proto_files = Dir.glob("#{proto_directory}/**/*.proto")
|
6
|
+
|
7
|
+
proto_files.each do |proto_file|
|
8
|
+
puts "Compiling #{proto_file}"
|
9
|
+
|
10
|
+
cmd_args = [
|
11
|
+
"protoc",
|
12
|
+
"-I", proto_directory,
|
13
|
+
"--ruby_out", proto_directory,
|
14
|
+
proto_file
|
15
|
+
]
|
16
|
+
|
17
|
+
output = `#{cmd_args.shelljoin} 2>&1`
|
18
|
+
|
19
|
+
unless $?.success?
|
20
|
+
raise "Proto compilation failed:\n#{output}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
rewrite_namespace(proto_directory, namespace) if namespace
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def rewrite_namespace(proto_directory, namespace)
|
30
|
+
proto_files = Dir.glob("#{proto_directory}/**/*_pb.rb")
|
31
|
+
|
32
|
+
proto_files.each do |proto_file|
|
33
|
+
puts "Namespacing #{proto_file} to #{namespace}"
|
34
|
+
|
35
|
+
old_content = File.read(proto_file)
|
36
|
+
|
37
|
+
File.write(proto_file, "module #{namespace}\n#{old_content}\nend\n")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Protein
|
2
|
+
class Router
|
3
|
+
class << self
|
4
|
+
def define(router)
|
5
|
+
if router.is_a?(Class) || router.is_a?(String)
|
6
|
+
router
|
7
|
+
elsif router.is_a?(Hash)
|
8
|
+
router_class = Class.new(Router)
|
9
|
+
router_class.from_hash(router)
|
10
|
+
router_class
|
11
|
+
else
|
12
|
+
raise(DefinitionError, "invalid router definition")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def from_hash(hash)
|
17
|
+
if hash[:services]
|
18
|
+
hash[:services].each do |each_service|
|
19
|
+
service(each_service)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if hash[:protos]
|
24
|
+
hash[:protos].each do |each_proto|
|
25
|
+
service_class = Class.new(Service)
|
26
|
+
service_class.proto(each_proto)
|
27
|
+
|
28
|
+
service(service_class)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def service(service_class)
|
34
|
+
@services ||= []
|
35
|
+
@services << service_class
|
36
|
+
end
|
37
|
+
|
38
|
+
def services
|
39
|
+
GetConst.map(@services || [])
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolve_by_name(service_name)
|
43
|
+
services.find do |service|
|
44
|
+
service.service_name == service_name.to_s
|
45
|
+
end || raise(RoutingError, "service #{service_name.inspect} not found")
|
46
|
+
end
|
47
|
+
|
48
|
+
def resolve_by_request(request)
|
49
|
+
services.find do |service|
|
50
|
+
request.is_a?(service.request_class)
|
51
|
+
end || raise(RoutingError, "service for #{request.class} not found")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Protein
|
2
|
+
class Server
|
3
|
+
class << self
|
4
|
+
def route(router)
|
5
|
+
@router = Router.define(router)
|
6
|
+
end
|
7
|
+
|
8
|
+
def service(service)
|
9
|
+
@router ||= Class.new(Router)
|
10
|
+
@router.service(service)
|
11
|
+
end
|
12
|
+
|
13
|
+
def router
|
14
|
+
GetConst.call(@router)
|
15
|
+
end
|
16
|
+
|
17
|
+
def transport(transport, opts = {})
|
18
|
+
@transport_class = Transport.define(transport, opts)
|
19
|
+
end
|
20
|
+
|
21
|
+
def transport_class
|
22
|
+
GetConst.call(@transport_class)
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
transport_class.serve(router)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module Protein
|
2
|
+
class Service
|
3
|
+
class << self
|
4
|
+
def service(service_name)
|
5
|
+
@service_name = service_name
|
6
|
+
end
|
7
|
+
|
8
|
+
def service_name
|
9
|
+
@service_name || raise(DefinitionError, "service name is not defined")
|
10
|
+
end
|
11
|
+
|
12
|
+
def proto(proto_module = nil)
|
13
|
+
@proto_module = proto_module
|
14
|
+
@service_name ||= proto_module.to_s.split("::").last.underscore
|
15
|
+
@request_class ||= "#{proto_module}::Request"
|
16
|
+
@response_class ||= "#{proto_module}::Response"
|
17
|
+
end
|
18
|
+
|
19
|
+
def proto_module
|
20
|
+
GetConst.call(@proto_module)
|
21
|
+
end
|
22
|
+
|
23
|
+
def request(request_class)
|
24
|
+
@request_class = request_class
|
25
|
+
end
|
26
|
+
|
27
|
+
def request_class
|
28
|
+
GetConst.call(@request_class)
|
29
|
+
end
|
30
|
+
|
31
|
+
def response?
|
32
|
+
response_class != false &&
|
33
|
+
(!response_class.is_a?(String) || !response_class.safe_constantize.nil?)
|
34
|
+
rescue NameError
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
def response(response_class)
|
39
|
+
@response_class = response_class
|
40
|
+
end
|
41
|
+
|
42
|
+
def response_class
|
43
|
+
GetConst.call(@response_class)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :request, :response, :errors
|
48
|
+
delegate :response?, :response_class, to: :class
|
49
|
+
|
50
|
+
def initialize(request)
|
51
|
+
@request = request
|
52
|
+
end
|
53
|
+
|
54
|
+
def process
|
55
|
+
@success = nil
|
56
|
+
@response = response_class.new if response?
|
57
|
+
@errors = []
|
58
|
+
|
59
|
+
call
|
60
|
+
|
61
|
+
raise(ProcessingError, "resolve/reject must be called") if response? && @success.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
def resolve(response = nil)
|
65
|
+
raise(ProcessingError, "can't resolve non-responding service") unless response?
|
66
|
+
raise(ProcessingError, "unable to resolve with previous errors") if @errors && @errors.any?
|
67
|
+
|
68
|
+
@success = true
|
69
|
+
@response =
|
70
|
+
if !response
|
71
|
+
@response
|
72
|
+
elsif response.is_a?(response_class)
|
73
|
+
response
|
74
|
+
else
|
75
|
+
response_class.new(response)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def reject(*args)
|
80
|
+
raise(ProcessingError, "can't reject non-responding service") unless response?
|
81
|
+
if args.any? && @errors && @errors.any?
|
82
|
+
raise(ProcessingError, "unable to reject with both rejection value and previous errors")
|
83
|
+
end
|
84
|
+
|
85
|
+
@success = false
|
86
|
+
@errors =
|
87
|
+
if args.empty? && @errors && @errors.any?
|
88
|
+
@errors
|
89
|
+
elsif args.empty?
|
90
|
+
[build_error(:error)]
|
91
|
+
elsif args.length == 1 && args[0].is_a?(Array) && args[0].any?
|
92
|
+
args[0].map { |error| build_error(error) }
|
93
|
+
else
|
94
|
+
[build_error(*args)]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def success?
|
99
|
+
raise(ProcessingError, "resolve/reject must be called first") if @success.nil?
|
100
|
+
|
101
|
+
@success
|
102
|
+
end
|
103
|
+
|
104
|
+
def failure?
|
105
|
+
!success?
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_error(*args)
|
109
|
+
@errors << build_error(*args)
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def build_error(*args)
|
115
|
+
if args[0].is_a?(String) || args[0].is_a?(Symbol)
|
116
|
+
reason = args[0]
|
117
|
+
pointer =
|
118
|
+
if args[1].is_a?(Hash) && (at = args[1][:at])
|
119
|
+
Pointer.new(request, "request", at)
|
120
|
+
end
|
121
|
+
|
122
|
+
ServiceError.new(reason: reason, pointer: pointer)
|
123
|
+
elsif args[0].is_a?(ServiceError)
|
124
|
+
args[0]
|
125
|
+
elsif args[0].is_a?(Hash)
|
126
|
+
ServiceError.new(reason: args[0][:reason], pointer: args[0][:pointer])
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Protein
|
2
|
+
class ServiceError
|
3
|
+
attr_reader :reason
|
4
|
+
attr_accessor :pointer
|
5
|
+
|
6
|
+
def initialize(reason: nil, pointer: nil)
|
7
|
+
@reason = reason if reason
|
8
|
+
@pointer = pointer if pointer
|
9
|
+
|
10
|
+
unless @reason.is_a?(String) || @reason.is_a?(Symbol)
|
11
|
+
raise(ProcessingError, "error reason must be a string or symbol")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Protein
|
2
|
+
class Transport
|
3
|
+
class << self
|
4
|
+
def define(transport, opts = {})
|
5
|
+
if transport.is_a?(Class) || transport.is_a?(String)
|
6
|
+
transport_class
|
7
|
+
elsif transport.is_a?(Symbol)
|
8
|
+
transport_base_class =
|
9
|
+
case transport
|
10
|
+
when :http
|
11
|
+
Protein::HTTPAdapter
|
12
|
+
when :amqp
|
13
|
+
Protein::AMQPAdapter
|
14
|
+
else
|
15
|
+
raise(DefinitionError, "invalid transport: #{transport.inspect}")
|
16
|
+
end
|
17
|
+
|
18
|
+
transport_class = Class.new(transport_base_class)
|
19
|
+
transport_class.from_hash(opts)
|
20
|
+
transport_class
|
21
|
+
else
|
22
|
+
raise(DefinitionError, "invalid transport definition")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: protein
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Karol Słuszniak
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-12 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: karol@shedul.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files:
|
18
|
+
- README.md
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/protein.rb
|
22
|
+
- lib/protein/amqp_adapter.rb
|
23
|
+
- lib/protein/client.rb
|
24
|
+
- lib/protein/errors.rb
|
25
|
+
- lib/protein/get_const.rb
|
26
|
+
- lib/protein/http_adapter.rb
|
27
|
+
- lib/protein/payload.rb
|
28
|
+
- lib/protein/pointer.rb
|
29
|
+
- lib/protein/processor.rb
|
30
|
+
- lib/protein/proto_compiler.rb
|
31
|
+
- lib/protein/router.rb
|
32
|
+
- lib/protein/server.rb
|
33
|
+
- lib/protein/service.rb
|
34
|
+
- lib/protein/service_error.rb
|
35
|
+
- lib/protein/transport.rb
|
36
|
+
- lib/protein/version.rb
|
37
|
+
homepage: http://github.com/surgeventures/protein-ruby
|
38
|
+
licenses:
|
39
|
+
- MIT
|
40
|
+
metadata: {}
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
require_paths:
|
44
|
+
- lib
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
requirements: []
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 2.6.8
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: Multi-platform remote procedure call (RPC) system based on Protocol Buffers
|
61
|
+
test_files: []
|