grpc_service_mesh 0.16.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/LICENSE +21 -0
- data/README.md +63 -0
- data/lib/grpc_service_mesh/errors.rb +23 -0
- data/lib/grpc_service_mesh/mesh_error.rb +119 -0
- data/lib/grpc_service_mesh/metadata.rb +28 -0
- data/lib/grpc_service_mesh/registry.rb +37 -0
- data/lib/grpc_service_mesh/rpc.rb +43 -0
- data/lib/grpc_service_mesh/rpc_client.rb +80 -0
- data/lib/grpc_service_mesh/rpc_runtime.rb +53 -0
- data/lib/grpc_service_mesh/rpc_service.rb +78 -0
- data/lib/grpc_service_mesh/transport_router.rb +42 -0
- data/lib/grpc_service_mesh/version.rb +5 -0
- data/lib/grpc_service_mesh/wire.rb +48 -0
- data/lib/grpc_service_mesh.rb +42 -0
- data/lib/mesh/options_pb.rb +17 -0
- metadata +104 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 241b9590dcd6bc041d6e575e74ece01d4fa165d4380c16a275b9823a05cadb94
|
|
4
|
+
data.tar.gz: a5d716495824f89fc9f3bf9eaf6ed1b163459fee48d5f8270d4a8905538ac5c5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 821daff5b253055c2baa16b9ebc41e38f624752ac0b21d4d01d510680327daaf7a040c8c740aa03211258d2d60a443464a0620411e146abb765f21cb1f7bffb1
|
|
7
|
+
data.tar.gz: 01417c334a7980dc1da34d37266b3caeba22d01ec95d0db793abab0f025ee548c919e4754ccddfc7597a809571e144d7ca86a0d28ef13457964cabdbe4520bec
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paymentbox
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# grpc-service-mesh-ruby
|
|
2
|
+
|
|
3
|
+
`grpc_service_mesh` is the Ruby implementation of the
|
|
4
|
+
[gRPC Service Mesh API](https://github.com/Paymentbox-com/grpc-service-mesh-api).
|
|
5
|
+
It carries protobuf messages over any transport that implements the
|
|
6
|
+
[Service Mesh API Specification](https://github.com/Paymentbox-com/service-mesh-api)
|
|
7
|
+
through the Ruby contract in the `service_mesh` gem, from
|
|
8
|
+
[service-mesh-ruby](https://github.com/Paymentbox-com/service-mesh-ruby).
|
|
9
|
+
|
|
10
|
+
The `GrpcServiceMesh` module holds the specification's non-generated types:
|
|
11
|
+
* `TransportRouter`
|
|
12
|
+
* `Registry`
|
|
13
|
+
* `RPCRuntime`
|
|
14
|
+
* `MeshError`
|
|
15
|
+
|
|
16
|
+
It also holds the two base classes that generated code builds on:
|
|
17
|
+
* `RPCService`
|
|
18
|
+
* `RPCClient`
|
|
19
|
+
|
|
20
|
+
Generated code comes from `grpc-service-mesh-gen` in the specification
|
|
21
|
+
repository and lives in a definitions project.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
The gem and its `service_mesh` dependency are installed from their
|
|
26
|
+
repositories at a tag, so the Gemfile names both git sources.
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# Gemfile
|
|
30
|
+
gem "grpc_service_mesh", git: "https://github.com/Paymentbox-com/grpc-service-mesh-ruby", tag: "v0.15.0"
|
|
31
|
+
gem "service_mesh", git: "https://github.com/Paymentbox-com/service-mesh-ruby", tag: "v0.4.2"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Ruby 3.3 or newer. The gem depends on `service_mesh`,
|
|
35
|
+
`google-protobuf`, and `googleapis-common-protos-types`. The last of these
|
|
36
|
+
provides `Google::Rpc::Status`, `Google::Rpc::Code`, and the detail types in
|
|
37
|
+
`google/rpc/error_details.proto`.
|
|
38
|
+
|
|
39
|
+
No transport is a dependency. The application adds the transport gem it uses.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
An application uses this library through the generated code that depends on it. The
|
|
44
|
+
[gRPC Service Mesh API](https://github.com/Paymentbox-com/grpc-service-mesh-api) describes how Ruby code is
|
|
45
|
+
generated from `.proto` files.
|
|
46
|
+
|
|
47
|
+
### Reference Examples
|
|
48
|
+
|
|
49
|
+
The examples in these docs use the reference definitions in `spec/support/definitions/`. They declare
|
|
50
|
+
`shop.OrderService`, with a `ROUTE` method `Place` and a `TOPIC` method `Placed`, in deployment group `shop` over the
|
|
51
|
+
transport named `mem`, the in-memory transport the specs use. The generated module is `Shop`, and the generated
|
|
52
|
+
per-transport maps are in `ServiceMaps`. The generated code is in `spec/support/testproto/`, written by
|
|
53
|
+
`grpc-service-mesh-gen` as a consumer would generate it, and described under [Generated Code](docs/generated-code.md).
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
- [Setup](docs/setup.md): configuring the `TransportRouter`, registering services, and running an `RPCRuntime`
|
|
58
|
+
- [Handlers](docs/handlers.md): implementing an `RPCService`, endpoint and subscriber handlers, returning errors, reply metadata, and message metadata
|
|
59
|
+
- [Calling](docs/calling.md): calling generated clients, metadata and transport options, reply metadata, and the wire format
|
|
60
|
+
- [MeshError](docs/mesherror.md): constructing and reading `MeshError`, and the errors the library raises
|
|
61
|
+
- [Generated Code](docs/generated-code.md): the generated reference output for Ruby, and what generated code relies on in this library
|
|
62
|
+
- [Public API](docs/public-api.md): every public constant and method in `GrpcServiceMesh`
|
|
63
|
+
- [Development](docs/development.md): the specification protos, the recipes, and the tests
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# Base of the errors this library raises for misuse of its own contract.
|
|
5
|
+
# MeshError is an application error and stands apart from this tree.
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
|
|
8
|
+
# A Target's transport metadata names a transport the router does not hold.
|
|
9
|
+
class UnknownTransport < Error
|
|
10
|
+
def initialize(name) = super("unknown transport #{name.inspect}")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# TransportRouter#close could not close one or more clients. +failures+ maps
|
|
14
|
+
# each transport name to the exception its client's close raised.
|
|
15
|
+
class CloseFailed < Error
|
|
16
|
+
attr_reader :failures
|
|
17
|
+
|
|
18
|
+
def initialize(failures)
|
|
19
|
+
@failures = failures
|
|
20
|
+
super(failures.map { |name, e| "#{name}: #{e.message}" }.join("; "))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "google/protobuf"
|
|
4
|
+
require "google/protobuf/well_known_types"
|
|
5
|
+
require "google/rpc/code_pb"
|
|
6
|
+
require "google/rpc/status_pb"
|
|
7
|
+
require "google/rpc/error_details_pb"
|
|
8
|
+
|
|
9
|
+
module GrpcServiceMesh
|
|
10
|
+
# The application error. A ROUTE handler raises it; the caller rescues it.
|
|
11
|
+
# It wraps a google.rpc.Status and carries the message metadata of the
|
|
12
|
+
# reply that reports it.
|
|
13
|
+
class MeshError < StandardError
|
|
14
|
+
include Metadata
|
|
15
|
+
|
|
16
|
+
attr_reader :proto
|
|
17
|
+
|
|
18
|
+
# MeshError.new(code, message, *details, mesh_metadata: {}). Returns the
|
|
19
|
+
# subclass for the code when one exists; a subclass called directly takes
|
|
20
|
+
# (message, *details, mesh_metadata: {}) and builds itself.
|
|
21
|
+
def self.new(*args, mesh_metadata: {})
|
|
22
|
+
if equal?(MeshError)
|
|
23
|
+
unless args.first.is_a?(Symbol) || args.first.is_a?(Integer)
|
|
24
|
+
raise ArgumentError, "MeshError.new takes a Google::Rpc::Code first, such as MeshError.new(:NOT_FOUND, \"no such order\"); " \
|
|
25
|
+
"UnknownError.new(message) raises an UNKNOWN error"
|
|
26
|
+
end
|
|
27
|
+
klass = BY_CODE[code_number(args.first)]
|
|
28
|
+
return klass.new(*args.drop(1), mesh_metadata: mesh_metadata) if klass
|
|
29
|
+
end
|
|
30
|
+
super
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# What +raise Klass, "msg"+ calls; builds the error as new does.
|
|
34
|
+
def self.exception(*args, **kwargs)
|
|
35
|
+
new(*args, **kwargs)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Wraps an existing Google::Rpc::Status.
|
|
39
|
+
def self.from_proto(status)
|
|
40
|
+
new(status.code, status.message, *status.details)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# +code+ is a Google::Rpc::Code name (:NOT_FOUND) or number (5).
|
|
44
|
+
# +details+ are protobuf messages, packed into Google::Protobuf::Any,
|
|
45
|
+
# or Any values already packed. +mesh_metadata+ is the metadata the
|
|
46
|
+
# error reply carries.
|
|
47
|
+
def initialize(code, message, *details, mesh_metadata: {})
|
|
48
|
+
text = message.to_s
|
|
49
|
+
@proto = Google::Rpc::Status.new(
|
|
50
|
+
code: self.class.code_number(code),
|
|
51
|
+
message: text,
|
|
52
|
+
details: details.map { |d| pack(d) }
|
|
53
|
+
)
|
|
54
|
+
self.mesh_metadata = mesh_metadata
|
|
55
|
+
super(text)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The Google::Rpc::Code name, or the number when it has no name.
|
|
59
|
+
def code
|
|
60
|
+
Google::Rpc::Code.lookup(@proto.code) || @proto.code
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The wrapped Status's details, as Google::Protobuf::Any values.
|
|
64
|
+
def details
|
|
65
|
+
@proto.details.to_a
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private def pack(detail)
|
|
69
|
+
return detail if detail.is_a?(Google::Protobuf::Any)
|
|
70
|
+
unless detail.class.respond_to?(:descriptor)
|
|
71
|
+
raise TypeError, "a detail must be a protobuf message, got #{detail.class}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Google::Protobuf::Any.pack(detail)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.code_number(code)
|
|
78
|
+
case code
|
|
79
|
+
when Integer then code
|
|
80
|
+
when Symbol
|
|
81
|
+
Google::Rpc::Code.resolve(code) or raise ArgumentError, "unknown Google::Rpc::Code #{code.inspect}"
|
|
82
|
+
else
|
|
83
|
+
raise ArgumentError, "code must be a Google::Rpc::Code name or number, got #{code.inspect}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# One subclass per Google::Rpc::Code other than OK, each fixing its code.
|
|
89
|
+
# MeshError.new and from_proto return the subclass for the code.
|
|
90
|
+
{
|
|
91
|
+
CancelledError: :CANCELLED,
|
|
92
|
+
UnknownError: :UNKNOWN,
|
|
93
|
+
InvalidArgumentError: :INVALID_ARGUMENT,
|
|
94
|
+
DeadlineExceededError: :DEADLINE_EXCEEDED,
|
|
95
|
+
NotFoundError: :NOT_FOUND,
|
|
96
|
+
AlreadyExistsError: :ALREADY_EXISTS,
|
|
97
|
+
PermissionDeniedError: :PERMISSION_DENIED,
|
|
98
|
+
UnauthenticatedError: :UNAUTHENTICATED,
|
|
99
|
+
ResourceExhaustedError: :RESOURCE_EXHAUSTED,
|
|
100
|
+
FailedPreconditionError: :FAILED_PRECONDITION,
|
|
101
|
+
AbortedError: :ABORTED,
|
|
102
|
+
OutOfRangeError: :OUT_OF_RANGE,
|
|
103
|
+
UnimplementedError: :UNIMPLEMENTED,
|
|
104
|
+
InternalError: :INTERNAL,
|
|
105
|
+
UnavailableError: :UNAVAILABLE,
|
|
106
|
+
DataLossError: :DATA_LOSS
|
|
107
|
+
}.each do |name, code|
|
|
108
|
+
klass = Class.new(MeshError) do
|
|
109
|
+
const_set(:CODE, code)
|
|
110
|
+
|
|
111
|
+
def self.new(message, *details, mesh_metadata: {})
|
|
112
|
+
super(self::CODE, message, *details, mesh_metadata: mesh_metadata)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
const_set(name, klass)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
MeshError::BY_CODE = MeshError.subclasses.to_h { |k| [MeshError.code_number(k::CODE), k] }.freeze
|
|
119
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# Message metadata carried on a message object. Generated code includes it
|
|
5
|
+
# into every message class an rpc takes or returns; MeshError includes it
|
|
6
|
+
# too. The library sets it on the objects it builds and reads it from the
|
|
7
|
+
# objects the application gives it, and never writes to an object the
|
|
8
|
+
# application built.
|
|
9
|
+
module Metadata
|
|
10
|
+
EMPTY = {}.freeze
|
|
11
|
+
private_constant :EMPTY
|
|
12
|
+
|
|
13
|
+
# The metadata Hash of this object. On first read it is a new empty Hash
|
|
14
|
+
# stored on the object, so keys can be written into it directly. A frozen
|
|
15
|
+
# object with none set has a frozen empty Hash.
|
|
16
|
+
def mesh_metadata
|
|
17
|
+
return @mesh_metadata if instance_variable_defined?(:@mesh_metadata) && @mesh_metadata
|
|
18
|
+
return EMPTY if frozen?
|
|
19
|
+
|
|
20
|
+
@mesh_metadata = {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Sets the metadata. Takes a Hash, or nil for none.
|
|
24
|
+
def mesh_metadata=(metadata)
|
|
25
|
+
@mesh_metadata = Hash(metadata)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# Collects every Endpoint and Subscriber the process serves. The process
|
|
5
|
+
# registry is GrpcServiceMesh.registry.
|
|
6
|
+
class Registry
|
|
7
|
+
def initialize
|
|
8
|
+
@endpoints = []
|
|
9
|
+
@subscribers = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Adds the bindings of +service+, an RPCService instance. Every binding
|
|
13
|
+
# is kept, so two registrations of one Target hand the transport two
|
|
14
|
+
# bindings for it.
|
|
15
|
+
def register(service)
|
|
16
|
+
@endpoints.concat(service.endpoints)
|
|
17
|
+
@subscribers.concat(service.subscribers)
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Endpoints whose Target carries +deployment_group+.
|
|
22
|
+
def endpoints(deployment_group)
|
|
23
|
+
@endpoints.select { |e| in_group?(e, deployment_group) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Subscribers whose Target carries +deployment_group+.
|
|
27
|
+
def subscribers(deployment_group)
|
|
28
|
+
@subscribers.select { |s| in_group?(s, deployment_group) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def in_group?(binding, deployment_group)
|
|
34
|
+
binding.target.metadata[ServiceMesh::DEPLOYMENT_GROUP_KEY] == deployment_group
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# One rpc method as the generated code declares it. +owner+ is the class
|
|
5
|
+
# the declaration appeared in.
|
|
6
|
+
Rpc = Data.define(:name, :target, :input, :output, :kind, :owner) do
|
|
7
|
+
def initialize(name:, target:, input:, kind:, owner:, output: nil)
|
|
8
|
+
raise ServiceMesh::KindMismatch, "rpc #{name}: declared #{kind}, target is #{target.kind}" unless kind == target.kind
|
|
9
|
+
|
|
10
|
+
super(name: name, target: target, input: input, output: output, kind: kind, owner: owner)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def route? = kind == :route
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Class-level declaration of rpcs, extended by RPCService and RPCClient.
|
|
17
|
+
module RpcDSL
|
|
18
|
+
# Declares one rpc. +name+ is the Ruby method name, +target+ the
|
|
19
|
+
# ServiceMesh::Target, +input+ and +output+ the message classes, +kind+
|
|
20
|
+
# :route or :topic. +output+ is not used for a topic.
|
|
21
|
+
def rpc(name, target:, input:, kind:, output: nil)
|
|
22
|
+
rpc = Rpc.new(name: name, target: target, input: input, output: output, kind: kind, owner: self)
|
|
23
|
+
own_rpcs[rpc.name] = rpc
|
|
24
|
+
define_rpc(rpc)
|
|
25
|
+
rpc
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Every declared rpc by name, including those of superclasses.
|
|
29
|
+
def rpcs
|
|
30
|
+
inherited = superclass.respond_to?(:rpcs) ? superclass.rpcs : {}
|
|
31
|
+
inherited.merge(own_rpcs)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def own_rpcs
|
|
37
|
+
@own_rpcs ||= {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def define_rpc(rpc)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# Base of every generated client class. Each declared rpc becomes a class
|
|
5
|
+
# method, +name(request)+, that resolves the transport's Client through the
|
|
6
|
+
# process router on every call. The request's mesh_metadata is read and left
|
|
7
|
+
# unchanged: its keys that start with OPTION_PREFIX are the transport
|
|
8
|
+
# options, with the prefix removed, and the other keys are the message
|
|
9
|
+
# metadata.
|
|
10
|
+
class RPCClient
|
|
11
|
+
extend RpcDSL
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def define_rpc(rpc)
|
|
17
|
+
if rpc.route?
|
|
18
|
+
define_singleton_method(rpc.name) do |request|
|
|
19
|
+
RPCClient.request(rpc, request)
|
|
20
|
+
end
|
|
21
|
+
else
|
|
22
|
+
define_singleton_method(rpc.name) do |request|
|
|
23
|
+
RPCClient.publish(rpc, request)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sends +request+ to a route and returns the decoded response, whose
|
|
30
|
+
# mesh_metadata is the reply's metadata without keys that start with
|
|
31
|
+
# OPTION_PREFIX. Raises MeshError for a reply carrying Grpc-Status, or
|
|
32
|
+
# INTERNAL when either payload does not decode; either error's
|
|
33
|
+
# mesh_metadata is the same reply metadata.
|
|
34
|
+
def self.request(rpc, request)
|
|
35
|
+
message, options = outbound(rpc, request)
|
|
36
|
+
reply = client_for(rpc.target).request(message, options)
|
|
37
|
+
reply_metadata = Wire.without_options(reply.metadata)
|
|
38
|
+
begin
|
|
39
|
+
if reply_metadata.key?(Wire::GRPC_STATUS_KEY)
|
|
40
|
+
raise MeshError.from_proto(decode(Google::Rpc::Status, reply.payload))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
response = decode(rpc.output, reply.payload)
|
|
44
|
+
rescue MeshError => e
|
|
45
|
+
e.mesh_metadata = reply_metadata
|
|
46
|
+
raise
|
|
47
|
+
end
|
|
48
|
+
response.mesh_metadata = reply_metadata
|
|
49
|
+
response
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Publishes +request+ to a topic. Returns nil.
|
|
53
|
+
def self.publish(rpc, request)
|
|
54
|
+
message, options = outbound(rpc, request)
|
|
55
|
+
client_for(rpc.target).publish(message, options)
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.client_for(target)
|
|
60
|
+
GrpcServiceMesh.transport_router.client(target.metadata["transport"])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The message for +request+ and the transport options, split from the
|
|
64
|
+
# request's mesh_metadata.
|
|
65
|
+
def self.outbound(rpc, request)
|
|
66
|
+
raise TypeError, "#{rpc.name} takes a #{rpc.input}, got #{request.class}" unless request.is_a?(rpc.input)
|
|
67
|
+
|
|
68
|
+
metadata, options = Wire.split_options(request.mesh_metadata)
|
|
69
|
+
[ServiceMesh::Message.new(target: rpc.target, metadata: Wire.metadata(metadata), payload: request.to_proto), options]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.decode(klass, payload)
|
|
73
|
+
klass.decode(payload)
|
|
74
|
+
rescue Google::Protobuf::ParseError => e
|
|
75
|
+
raise MeshError.new(:INTERNAL, "reply does not decode as #{klass.descriptor.name}: #{e.message}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private_class_method :client_for, :outbound, :decode
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# The service process for one transport and one deployment group. new
|
|
5
|
+
# takes the transport's client from the process router and the matching
|
|
6
|
+
# bindings from the process registry, and builds the transport's Runtime
|
|
7
|
+
# from them with the +runtime+ constructor.
|
|
8
|
+
class RPCRuntime
|
|
9
|
+
attr_reader :transport, :deployment_group, :t_runtime
|
|
10
|
+
|
|
11
|
+
# +runtime+ is ->(client, config, endpoints:, subscribers:) returning the
|
|
12
|
+
# transport's Runtime. It receives +config+ with deployment_group set.
|
|
13
|
+
# +endpoints+ and +subscribers+, when given, replace the registry's list
|
|
14
|
+
# of that kind, and [] serves none. Raises ArgumentError for a given
|
|
15
|
+
# binding whose Target's deployment_group or transport is not this
|
|
16
|
+
# runtime's, UnknownTransport, and whatever +runtime+ raises.
|
|
17
|
+
def initialize(transport:, deployment_group:, runtime:, config: {}, endpoints: nil, subscribers: nil)
|
|
18
|
+
raise ArgumentError, "runtime: must be a callable that builds the transport's Runtime, got #{runtime.inspect}" unless runtime.respond_to?(:call)
|
|
19
|
+
|
|
20
|
+
registry = GrpcServiceMesh.registry
|
|
21
|
+
client = GrpcServiceMesh.transport_router.client(transport)
|
|
22
|
+
@transport = transport
|
|
23
|
+
@deployment_group = deployment_group
|
|
24
|
+
endpoints&.each { |binding| check_target("endpoint", binding.target) }
|
|
25
|
+
subscribers&.each { |binding| check_target("subscriber", binding.target) }
|
|
26
|
+
@t_runtime = runtime.call(
|
|
27
|
+
client,
|
|
28
|
+
config.to_h.merge(ServiceMesh::DEPLOYMENT_GROUP_KEY => deployment_group),
|
|
29
|
+
endpoints: endpoints || registry.endpoints(deployment_group),
|
|
30
|
+
subscribers: subscribers || registry.subscribers(deployment_group)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def start = @t_runtime.start
|
|
35
|
+
|
|
36
|
+
def stop(drain) = @t_runtime.stop(drain)
|
|
37
|
+
|
|
38
|
+
def running? = @t_runtime.running?
|
|
39
|
+
|
|
40
|
+
def client = @t_runtime.client
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def check_target(kind, target)
|
|
45
|
+
{ServiceMesh::DEPLOYMENT_GROUP_KEY => @deployment_group, "transport" => @transport}.each do |key, want|
|
|
46
|
+
got = target.metadata[key]
|
|
47
|
+
next if got == want
|
|
48
|
+
|
|
49
|
+
raise ArgumentError, "#{kind} #{target.segments.inspect} has #{key} #{got.inspect}, want #{want.inspect}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# Base of every generated service class. The generated subclass declares its
|
|
5
|
+
# rpcs; the application subclasses that and defines a method per rpc it
|
|
6
|
+
# serves, taking the decoded request, whose mesh_metadata is the inbound
|
|
7
|
+
# message metadata. A route method returns the response; the response's
|
|
8
|
+
# mesh_metadata, without keys that start with OPTION_PREFIX, is the reply's
|
|
9
|
+
# metadata. The same holds for a raised MeshError.
|
|
10
|
+
class RPCService
|
|
11
|
+
extend RpcDSL
|
|
12
|
+
|
|
13
|
+
# A ServiceMesh::Endpoint for each route rpc this instance implements.
|
|
14
|
+
def endpoints
|
|
15
|
+
implemented.select(&:route?).map { |rpc| ServiceMesh::Endpoint.new(target: rpc.target, handler: endpoint_handler(rpc)) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# A ServiceMesh::Subscriber for each topic rpc this instance implements.
|
|
19
|
+
def subscribers
|
|
20
|
+
implemented.reject(&:route?).map { |rpc| ServiceMesh::Subscriber.new(target: rpc.target, handler: subscriber_handler(rpc)) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Rpcs whose method is defined in the declaring class or below it.
|
|
26
|
+
def implemented
|
|
27
|
+
self.class.rpcs.values.select do |rpc|
|
|
28
|
+
self.class.method_defined?(rpc.name) && self.class.instance_method(rpc.name).owner <= rpc.owner
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def endpoint_handler(rpc)
|
|
33
|
+
lambda do |message|
|
|
34
|
+
request = begin
|
|
35
|
+
decode_inbound(rpc, message)
|
|
36
|
+
rescue Google::Protobuf::ParseError => e
|
|
37
|
+
next status_reply(message, MeshError.new(:INTERNAL, e.message))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
response = public_send(rpc.name, request)
|
|
41
|
+
unless response.is_a?(rpc.output)
|
|
42
|
+
raise TypeError, "#{rpc.name} returned #{response.class}, expected #{rpc.output}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
metadata = Wire.metadata(Wire.without_options(response.mesh_metadata).except(Wire::GRPC_STATUS_KEY))
|
|
46
|
+
ServiceMesh::Message.new(target: message.target, metadata: metadata, payload: response.to_proto)
|
|
47
|
+
rescue MeshError => e
|
|
48
|
+
status_reply(message, e)
|
|
49
|
+
rescue => e
|
|
50
|
+
status_reply(message, MeshError.new(:UNKNOWN, e.message))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def subscriber_handler(rpc)
|
|
55
|
+
lambda do |message|
|
|
56
|
+
public_send(rpc.name, decode_inbound(rpc, message))
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The decoded payload carrying the message's metadata. A payload that
|
|
62
|
+
# does not decode raises Google::Protobuf::ParseError naming the input
|
|
63
|
+
# type.
|
|
64
|
+
def decode_inbound(rpc, message)
|
|
65
|
+
request = begin
|
|
66
|
+
rpc.input.decode(message.payload)
|
|
67
|
+
rescue Google::Protobuf::ParseError => e
|
|
68
|
+
raise Google::Protobuf::ParseError, "request does not decode as #{rpc.input.descriptor.name}: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
request.mesh_metadata = message.metadata
|
|
71
|
+
request
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def status_reply(message, error)
|
|
75
|
+
ServiceMesh::Message.new(target: message.target, metadata: Wire.status_metadata(error, Wire.without_options(error.mesh_metadata)), payload: error.proto.to_proto)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# Holds one Client per transport name: the transport-specific Client the
|
|
5
|
+
# application built and owns. The process router is
|
|
6
|
+
# GrpcServiceMesh.transport_router. Clients are added at boot; client is a
|
|
7
|
+
# hash read.
|
|
8
|
+
class TransportRouter
|
|
9
|
+
def initialize
|
|
10
|
+
@clients = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Adds a client. Adding under a name already present replaces the client.
|
|
14
|
+
def add(name, client)
|
|
15
|
+
@clients[name] = client
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def names
|
|
20
|
+
@clients.keys
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The Client for +name+. Raises UnknownTransport.
|
|
24
|
+
def client(name)
|
|
25
|
+
@clients.fetch(name) { raise UnknownTransport.new(name) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Closes every client, even when some fail to close. Returns nil, or
|
|
29
|
+
# raises CloseFailed carrying each client's exception by transport name.
|
|
30
|
+
def close
|
|
31
|
+
failures = {}
|
|
32
|
+
@clients.each do |name, client|
|
|
33
|
+
client.close
|
|
34
|
+
rescue => e
|
|
35
|
+
failures[name] = e
|
|
36
|
+
end
|
|
37
|
+
raise CloseFailed.new(failures) unless failures.empty?
|
|
38
|
+
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GrpcServiceMesh
|
|
4
|
+
# The start of an outgoing metadata key that is a transport option. A client
|
|
5
|
+
# method passes each such key, with the prefix removed, to the transport's
|
|
6
|
+
# request or publish options, and sends the other keys as message metadata.
|
|
7
|
+
# Reply metadata never carries such keys. The match is exact and
|
|
8
|
+
# case-sensitive.
|
|
9
|
+
OPTION_PREFIX = "Mesh-Option-"
|
|
10
|
+
|
|
11
|
+
# Metadata keys and values the specification fixes on every message.
|
|
12
|
+
module Wire
|
|
13
|
+
CONTENT_TYPE_KEY = "Content-Type"
|
|
14
|
+
CONTENT_TYPE = "application/x-protobuf"
|
|
15
|
+
GRPC_STATUS_KEY = "Grpc-Status"
|
|
16
|
+
|
|
17
|
+
# Metadata for a message carrying a payload of the method's type.
|
|
18
|
+
def self.metadata(extra = {})
|
|
19
|
+
extra.to_h.merge(CONTENT_TYPE_KEY => CONTENT_TYPE)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Splits +metadata+ into the message metadata and the transport options:
|
|
23
|
+
# the keys that start with OPTION_PREFIX become options with the prefix
|
|
24
|
+
# removed. Returns [metadata, options], both new Hashes.
|
|
25
|
+
def self.split_options(metadata)
|
|
26
|
+
options = {}
|
|
27
|
+
rest = {}
|
|
28
|
+
metadata.to_h.each do |key, value|
|
|
29
|
+
if key.to_s.start_with?(OPTION_PREFIX)
|
|
30
|
+
options[key.to_s.delete_prefix(OPTION_PREFIX)] = value
|
|
31
|
+
else
|
|
32
|
+
rest[key] = value
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
[rest, options]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A copy of +metadata+ without the keys that start with OPTION_PREFIX.
|
|
39
|
+
def self.without_options(metadata)
|
|
40
|
+
split_options(metadata).first
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Metadata for a reply whose payload is an encoded google.rpc.Status.
|
|
44
|
+
def self.status_metadata(error, extra = {})
|
|
45
|
+
extra.to_h.merge(CONTENT_TYPE_KEY => CONTENT_TYPE, GRPC_STATUS_KEY => error.proto.code.to_s)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "service_mesh"
|
|
4
|
+
|
|
5
|
+
require_relative "grpc_service_mesh/version"
|
|
6
|
+
require_relative "grpc_service_mesh/errors"
|
|
7
|
+
require_relative "grpc_service_mesh/wire"
|
|
8
|
+
require_relative "grpc_service_mesh/metadata"
|
|
9
|
+
require_relative "grpc_service_mesh/mesh_error"
|
|
10
|
+
require_relative "grpc_service_mesh/transport_router"
|
|
11
|
+
require_relative "grpc_service_mesh/registry"
|
|
12
|
+
require_relative "grpc_service_mesh/rpc"
|
|
13
|
+
require_relative "grpc_service_mesh/rpc_service"
|
|
14
|
+
require_relative "grpc_service_mesh/rpc_client"
|
|
15
|
+
require_relative "grpc_service_mesh/rpc_runtime"
|
|
16
|
+
|
|
17
|
+
# Ruby library for the gRPC Service Mesh API. Generated code declares rpcs on
|
|
18
|
+
# RPCService and RPCClient subclasses; the application configures the process
|
|
19
|
+
# TransportRouter, registers its services, and starts an RPCRuntime.
|
|
20
|
+
module GrpcServiceMesh
|
|
21
|
+
class << self
|
|
22
|
+
# The process TransportRouter.
|
|
23
|
+
def transport_router
|
|
24
|
+
@transport_router ||= TransportRouter.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The process Registry.
|
|
28
|
+
def registry
|
|
29
|
+
@registry ||= Registry.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Shortcut for transport_router.add.
|
|
33
|
+
def add_transport(name, client)
|
|
34
|
+
transport_router.add(name, client)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Shortcut for registry.register.
|
|
38
|
+
def register(service)
|
|
39
|
+
registry.register(service)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: mesh/options.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
require 'google/protobuf/descriptor_pb'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
descriptor_data = "\n\x12mesh/options.proto\x12\x04mesh\x1a google/protobuf/descriptor.proto*\x1c\n\x04Kind\x12\t\n\x05ROUTE\x10\x00\x12\t\n\x05TOPIC\x10\x01::\n\x04kind\x12\x1e.google.protobuf.MethodOptions\x18\xd1\x86\x03 \x01(\x0e\x32\n.mesh.Kind:8\n\x0e\x63onsumer_group\x12\x1e.google.protobuf.MethodOptions\x18\xd2\x86\x03 \x01(\t:8\n\x10\x64\x65ployment_group\x12\x1c.google.protobuf.FileOptions\x18\xd3\x86\x03 \x01(\t:1\n\ttransport\x12\x1c.google.protobuf.FileOptions\x18\xd4\x86\x03 \x01(\t:3\n\x0broot_prefix\x12\x1c.google.protobuf.FileOptions\x18\xd5\x86\x03 \x01(\tB<Z:github.com/Paymentbox-com/grpc-service-mesh-go/meshoptionsb\x06proto3"
|
|
11
|
+
|
|
12
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
13
|
+
pool.add_serialized_file(descriptor_data)
|
|
14
|
+
|
|
15
|
+
module Mesh
|
|
16
|
+
Kind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("mesh.Kind").enummodule
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: grpc_service_mesh
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.16.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Bryant Morrill
|
|
8
|
+
- Paymentbox
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2026-09-29 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: google-protobuf
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - "~>"
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '4.26'
|
|
21
|
+
type: :runtime
|
|
22
|
+
prerelease: false
|
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
+
requirements:
|
|
25
|
+
- - "~>"
|
|
26
|
+
- !ruby/object:Gem::Version
|
|
27
|
+
version: '4.26'
|
|
28
|
+
- !ruby/object:Gem::Dependency
|
|
29
|
+
name: googleapis-common-protos-types
|
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
|
31
|
+
requirements:
|
|
32
|
+
- - "~>"
|
|
33
|
+
- !ruby/object:Gem::Version
|
|
34
|
+
version: '1.15'
|
|
35
|
+
type: :runtime
|
|
36
|
+
prerelease: false
|
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - "~>"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '1.15'
|
|
42
|
+
- !ruby/object:Gem::Dependency
|
|
43
|
+
name: service_mesh
|
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - "~>"
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0.4'
|
|
49
|
+
type: :runtime
|
|
50
|
+
prerelease: false
|
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
+
requirements:
|
|
53
|
+
- - "~>"
|
|
54
|
+
- !ruby/object:Gem::Version
|
|
55
|
+
version: '0.4'
|
|
56
|
+
description:
|
|
57
|
+
email:
|
|
58
|
+
- bryantreadmorrill@gmail.com
|
|
59
|
+
executables: []
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- LICENSE
|
|
64
|
+
- README.md
|
|
65
|
+
- lib/grpc_service_mesh.rb
|
|
66
|
+
- lib/grpc_service_mesh/errors.rb
|
|
67
|
+
- lib/grpc_service_mesh/mesh_error.rb
|
|
68
|
+
- lib/grpc_service_mesh/metadata.rb
|
|
69
|
+
- lib/grpc_service_mesh/registry.rb
|
|
70
|
+
- lib/grpc_service_mesh/rpc.rb
|
|
71
|
+
- lib/grpc_service_mesh/rpc_client.rb
|
|
72
|
+
- lib/grpc_service_mesh/rpc_runtime.rb
|
|
73
|
+
- lib/grpc_service_mesh/rpc_service.rb
|
|
74
|
+
- lib/grpc_service_mesh/transport_router.rb
|
|
75
|
+
- lib/grpc_service_mesh/version.rb
|
|
76
|
+
- lib/grpc_service_mesh/wire.rb
|
|
77
|
+
- lib/mesh/options_pb.rb
|
|
78
|
+
homepage: https://github.com/Paymentbox-com/grpc-service-mesh-ruby
|
|
79
|
+
licenses:
|
|
80
|
+
- MIT
|
|
81
|
+
metadata:
|
|
82
|
+
homepage_uri: https://github.com/Paymentbox-com/grpc-service-mesh-ruby
|
|
83
|
+
source_code_uri: https://github.com/Paymentbox-com/grpc-service-mesh-ruby
|
|
84
|
+
rubygems_mfa_required: 'true'
|
|
85
|
+
post_install_message:
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '3.3'
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 3.5.22
|
|
101
|
+
signing_key:
|
|
102
|
+
specification_version: 4
|
|
103
|
+
summary: Ruby library for the gRPC Service Mesh API
|
|
104
|
+
test_files: []
|