protein 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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: []
|