async-grpc 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/code.md +0 -0
- data/context/getting-started.md +174 -0
- data/context/index.yaml +12 -0
- data/design.md +1121 -0
- data/lib/async/grpc/client.rb +351 -0
- data/lib/async/grpc/dispatcher_middleware.rb +113 -0
- data/lib/async/grpc/service.rb +93 -0
- data/lib/async/grpc/stub.rb +89 -0
- data/lib/async/grpc/version.rb +12 -0
- data/lib/async/grpc.rb +15 -0
- data/license.md +21 -0
- data/readme.md +52 -0
- data/releases.md +3 -0
- data/spanner_integration.md +309 -0
- metadata +81 -0
data/readme.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Async::GRPC
|
|
2
|
+
|
|
3
|
+
Asynchronous gRPC client and server implementation built on top of `protocol-grpc` and `async-http`.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/socketry/async-grpc/actions?workflow=Test)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
`async-grpc` provides asynchronous networking and concurrency for gRPC:
|
|
10
|
+
|
|
11
|
+
- **Asynchronous client** - Wraps `Async::HTTP::Client` to provide gRPC-specific call methods with automatic message framing and status handling.
|
|
12
|
+
- **Method-based stubs** - Create type-safe stubs from `Protocol::GRPC::Interface` definitions. Accepts both PascalCase and snake\_case method names for convenience.
|
|
13
|
+
- **Server middleware** - `DispatcherMiddleware` routes requests to registered services based on path.
|
|
14
|
+
- **All RPC patterns** - Supports unary, server streaming, client streaming, and bidirectional streaming RPCs.
|
|
15
|
+
- **Interface-based services** - Define services using `Protocol::GRPC::Interface` with automatic PascalCase to snake\_case method name conversion for Ruby implementations.
|
|
16
|
+
- **HTTP/2 transport** - Built on `async-http` with automatic HTTP/2 multiplexing and connection pooling.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
Please see the [project documentation](https://socketry.github.io/async-grpc/) for more details.
|
|
21
|
+
|
|
22
|
+
- [Getting Started](https://socketry.github.io/async-grpc/guides/getting-started/index) - This guide explains how to get started with `Async::GRPC` for building gRPC clients and servers.
|
|
23
|
+
|
|
24
|
+
## Releases
|
|
25
|
+
|
|
26
|
+
Please see the [project releases](https://socketry.github.io/async-grpc/releases/index) for all releases.
|
|
27
|
+
|
|
28
|
+
### v0.1.0
|
|
29
|
+
|
|
30
|
+
## See Also
|
|
31
|
+
|
|
32
|
+
- [protocol-grpc](https://github.com/socketry/protocol-grpc) — Protocol abstractions for gRPC that this gem builds upon.
|
|
33
|
+
- [async-http](https://github.com/socketry/async-http) — Asynchronous HTTP client and server with HTTP/2 support.
|
|
34
|
+
- [protocol-http](https://github.com/socketry/protocol-http) — HTTP protocol abstractions.
|
|
35
|
+
|
|
36
|
+
## Contributing
|
|
37
|
+
|
|
38
|
+
We welcome contributions to this project.
|
|
39
|
+
|
|
40
|
+
1. Fork it.
|
|
41
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
42
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
43
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
|
44
|
+
5. Create new Pull Request.
|
|
45
|
+
|
|
46
|
+
### Developer Certificate of Origin
|
|
47
|
+
|
|
48
|
+
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
|
|
49
|
+
|
|
50
|
+
### Community Guidelines
|
|
51
|
+
|
|
52
|
+
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
|
data/releases.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Integrating async-grpc with Google Cloud Spanner
|
|
2
|
+
|
|
3
|
+
## Current State Analysis
|
|
4
|
+
|
|
5
|
+
### How ruby-spanner Uses gRPC
|
|
6
|
+
|
|
7
|
+
Looking at `google-cloud-spanner/lib/google/cloud/spanner/service.rb`:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
def channel
|
|
11
|
+
require "grpc"
|
|
12
|
+
GRPC::Core::Channel.new host, chan_args, chan_creds
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def chan_creds
|
|
16
|
+
return credentials if insecure?
|
|
17
|
+
require "grpc"
|
|
18
|
+
GRPC::Core::ChannelCredentials.new.compose \
|
|
19
|
+
GRPC::Core::CallCredentials.new credentials.client.updater_proc
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def service
|
|
23
|
+
return mocked_service if mocked_service
|
|
24
|
+
@service ||=
|
|
25
|
+
V1::Spanner::Client.new do |config|
|
|
26
|
+
config.credentials = channel # <-- Passes gRPC channel
|
|
27
|
+
config.quota_project = @quota_project
|
|
28
|
+
config.timeout = timeout if timeout
|
|
29
|
+
config.endpoint = host if host
|
|
30
|
+
config.universe_domain = @universe_domain
|
|
31
|
+
config.lib_name = lib_name_with_prefix
|
|
32
|
+
config.lib_version = Google::Cloud::Spanner::VERSION
|
|
33
|
+
config.metadata = { "google-cloud-resource-prefix" => "projects/#{@project}" }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Key Dependencies
|
|
39
|
+
|
|
40
|
+
1. **gRPC C Extension** (`grpc` gem)
|
|
41
|
+
- `GRPC::Core::Channel` - connection management
|
|
42
|
+
- `GRPC::Core::ChannelCredentials` - TLS/SSL setup
|
|
43
|
+
- `GRPC::Core::CallCredentials` - per-call auth (OAuth2 tokens)
|
|
44
|
+
|
|
45
|
+
2. **Generated Client Stubs** (from `google-cloud-spanner-v1` gem)
|
|
46
|
+
- `Google::Cloud::Spanner::V1::Spanner::Client`
|
|
47
|
+
- Generated by `protoc` with `grpc` plugin
|
|
48
|
+
- Expects a gRPC channel as credentials
|
|
49
|
+
|
|
50
|
+
## Replacement Strategy
|
|
51
|
+
|
|
52
|
+
### Option 1: Drop-In Channel Replacement (Most Feasible)
|
|
53
|
+
|
|
54
|
+
Create an adapter that implements the `GRPC::Core::Channel` interface:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
module Async
|
|
58
|
+
module GRPC
|
|
59
|
+
# Adapter that makes Async::GRPC::Client compatible with
|
|
60
|
+
# Google's generated gRPC client stubs
|
|
61
|
+
class ChannelAdapter
|
|
62
|
+
def initialize(endpoint, channel_args = {}, channel_creds = nil)
|
|
63
|
+
@endpoint = endpoint
|
|
64
|
+
@client = Async::GRPC::Client.new(endpoint)
|
|
65
|
+
@channel_creds = channel_creds
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Called by generated stubs to make RPC calls
|
|
69
|
+
# Must implement the gRPC::Core::Channel interface
|
|
70
|
+
def request_response(path, request, marshal, unmarshal, deadline: nil, metadata: {})
|
|
71
|
+
# Parse service/method from path
|
|
72
|
+
# path format: "/google.spanner.v1.Spanner/ExecuteStreamingSql"
|
|
73
|
+
parts = path.split("/").last(2)
|
|
74
|
+
service = parts[0].split(".").last
|
|
75
|
+
method = parts[1]
|
|
76
|
+
|
|
77
|
+
# Add auth metadata
|
|
78
|
+
if @channel_creds
|
|
79
|
+
auth_metadata = @channel_creds.call(method)
|
|
80
|
+
metadata.merge!(auth_metadata)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Marshal request
|
|
84
|
+
request_data = marshal.call(request)
|
|
85
|
+
|
|
86
|
+
# Make the call
|
|
87
|
+
response_data = Async do
|
|
88
|
+
@client.unary(
|
|
89
|
+
service,
|
|
90
|
+
method,
|
|
91
|
+
request_data,
|
|
92
|
+
metadata: metadata,
|
|
93
|
+
timeout: deadline
|
|
94
|
+
)
|
|
95
|
+
end.wait
|
|
96
|
+
|
|
97
|
+
# Unmarshal response
|
|
98
|
+
unmarshal.call(response_data)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# For server-streaming RPCs
|
|
102
|
+
def request_stream(path, request, marshal, unmarshal, deadline: nil, metadata: {})
|
|
103
|
+
# Similar but returns enumerator
|
|
104
|
+
Enumerator.new do |yielder|
|
|
105
|
+
Async do
|
|
106
|
+
@client.server_streaming do |response_data|
|
|
107
|
+
yielder << unmarshal.call(response_data)
|
|
108
|
+
end
|
|
109
|
+
end.wait
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# For client-streaming RPCs
|
|
114
|
+
def stream_request(path, marshal, unmarshal, deadline: nil, metadata: {})
|
|
115
|
+
# Returns [input_stream, output_future]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# For bidirectional streaming RPCs
|
|
119
|
+
def stream_stream(path, marshal, unmarshal, deadline: nil, metadata: {})
|
|
120
|
+
# Returns [input_stream, output_enumerator]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def close
|
|
124
|
+
@client.close
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Option 2: Regenerate Client Stubs (More Invasive)
|
|
132
|
+
|
|
133
|
+
Instead of using Google's generated stubs, regenerate them with our own generator:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Generate Spanner service stubs using protocol-grpc
|
|
137
|
+
bake protocol:grpc:generate google/spanner/v1/spanner.proto
|
|
138
|
+
|
|
139
|
+
# This would generate:
|
|
140
|
+
# - lib/google/spanner/v1/spanner_grpc.rb (client stubs)
|
|
141
|
+
# - Compatible with Async::GRPC::Client
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Then modify `service.rb`:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
require "google/spanner/v1/spanner_grpc" # Our generated stubs
|
|
148
|
+
|
|
149
|
+
def service
|
|
150
|
+
@service ||= begin
|
|
151
|
+
endpoint = Async::HTTP::Endpoint.parse("https://#{host}")
|
|
152
|
+
client = Async::GRPC::Client.new(endpoint)
|
|
153
|
+
|
|
154
|
+
Google::Spanner::V1::SpannerClient.new(client) # Our stub
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Option 3: Monkey-Patch Mock Interface (Testing/Development Only)
|
|
160
|
+
|
|
161
|
+
Use Spanner's built-in mocking capability:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# In service.rb
|
|
165
|
+
attr_accessor :mocked_service # Already exists!
|
|
166
|
+
|
|
167
|
+
# Our replacement
|
|
168
|
+
class AsyncGRPCSpannerService
|
|
169
|
+
def initialize(client)
|
|
170
|
+
@client = client
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Implement all Spanner RPC methods
|
|
174
|
+
def execute_streaming_sql(request, options = {})
|
|
175
|
+
# Call via async-grpc
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def begin_transaction(request, options = {})
|
|
179
|
+
# ...
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# ... implement all 20+ RPC methods
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Usage
|
|
186
|
+
service = Google::Cloud::Spanner::Service.new
|
|
187
|
+
service.mocked_service = AsyncGRPCSpannerService.new(async_grpc_client)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Required Interface
|
|
191
|
+
|
|
192
|
+
To make this work, `Async::GRPC::Client` would need to support:
|
|
193
|
+
|
|
194
|
+
### 1. Raw Binary Messages
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# Currently: pass protobuf objects
|
|
198
|
+
client.unary(service, method, request_object, response_class: MyReply)
|
|
199
|
+
|
|
200
|
+
# Need to support: pass raw binary
|
|
201
|
+
client.unary_binary(service, method, request_binary) # => response_binary
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 2. Marshal/Unmarshal Callbacks
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
client.unary(
|
|
208
|
+
service,
|
|
209
|
+
method,
|
|
210
|
+
request,
|
|
211
|
+
marshal: ->(obj){obj.to_proto},
|
|
212
|
+
unmarshal: ->(data){MyReply.decode(data)}
|
|
213
|
+
)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 3. Compatible Metadata/Headers
|
|
217
|
+
|
|
218
|
+
Google's stubs expect specific metadata format (OAuth2 tokens, quota project, etc.)
|
|
219
|
+
|
|
220
|
+
## Recommendation
|
|
221
|
+
|
|
222
|
+
**Option 1 (Channel Adapter) is most feasible** because:
|
|
223
|
+
|
|
224
|
+
1. ✅ No need to regenerate all Google API stubs
|
|
225
|
+
2. ✅ Works with existing `google-cloud-spanner-v1` gem
|
|
226
|
+
3. ✅ Minimal changes to Spanner gem
|
|
227
|
+
4. ✅ Can be done incrementally (test with one RPC at a time)
|
|
228
|
+
5. ✅ Falls back to standard gRPC if issues arise
|
|
229
|
+
|
|
230
|
+
## Implementation Plan
|
|
231
|
+
|
|
232
|
+
### Phase 1: Proof of Concept
|
|
233
|
+
|
|
234
|
+
1. Implement `Async::GRPC::ChannelAdapter`
|
|
235
|
+
2. Test with simple unary RPC (e.g., `CreateSession`)
|
|
236
|
+
3. Verify it works end-to-end
|
|
237
|
+
|
|
238
|
+
### Phase 2: Full Interface
|
|
239
|
+
|
|
240
|
+
1. Implement all four RPC types (unary, client streaming, server streaming, bidirectional)
|
|
241
|
+
2. Handle auth metadata properly
|
|
242
|
+
3. Support deadlines and cancellation
|
|
243
|
+
|
|
244
|
+
### Phase 3: Production Ready
|
|
245
|
+
|
|
246
|
+
1. Handle all gRPC edge cases
|
|
247
|
+
2. Proper error mapping
|
|
248
|
+
3. Connection pooling
|
|
249
|
+
4. Performance testing
|
|
250
|
+
|
|
251
|
+
## Challenges
|
|
252
|
+
|
|
253
|
+
### 1. Generated Stub Format
|
|
254
|
+
|
|
255
|
+
Google's generated stubs use Gapic (Google API Client) framework, which has its own conventions. We'd need to understand:
|
|
256
|
+
- Exact method signatures expected
|
|
257
|
+
- How streaming responses are yielded
|
|
258
|
+
- Error handling patterns
|
|
259
|
+
|
|
260
|
+
### 2. Authentication
|
|
261
|
+
|
|
262
|
+
Google Cloud uses:
|
|
263
|
+
- OAuth2 access tokens (refreshed automatically)
|
|
264
|
+
- Per-RPC credentials (added as metadata)
|
|
265
|
+
- Service account key files
|
|
266
|
+
|
|
267
|
+
Our adapter must support this auth flow.
|
|
268
|
+
|
|
269
|
+
### 3. Retry Logic
|
|
270
|
+
|
|
271
|
+
Google's client has sophisticated retry logic:
|
|
272
|
+
- Exponential backoff
|
|
273
|
+
- Per-method retry policies
|
|
274
|
+
- Idempotency detection
|
|
275
|
+
|
|
276
|
+
We'd need to preserve this behavior.
|
|
277
|
+
|
|
278
|
+
### 4. Observability
|
|
279
|
+
|
|
280
|
+
Google's clients have built-in:
|
|
281
|
+
- OpenTelemetry tracing
|
|
282
|
+
- Metrics/logging
|
|
283
|
+
- Quota tracking
|
|
284
|
+
|
|
285
|
+
## Next Steps
|
|
286
|
+
|
|
287
|
+
1. **Investigate Gapic internals**: Look at how `google-cloud-spanner-v1` generated code works
|
|
288
|
+
2. **Find hook points**: Identify where we can inject our channel
|
|
289
|
+
3. **Build minimal adapter**: Implement just enough to make one RPC work
|
|
290
|
+
4. **Benchmark**: Compare performance async-grpc vs standard gRPC
|
|
291
|
+
|
|
292
|
+
## Benefits if Successful
|
|
293
|
+
|
|
294
|
+
- ✅ Pure Ruby implementation (no C extension)
|
|
295
|
+
- ✅ Async-first design (better concurrency)
|
|
296
|
+
- ✅ Easier debugging (no C stack traces)
|
|
297
|
+
- ✅ Potentially better resource usage
|
|
298
|
+
- ✅ Could work on platforms where C extensions are problematic (e.g., JRuby, TruffleRuby)
|
|
299
|
+
|
|
300
|
+
## Risks
|
|
301
|
+
|
|
302
|
+
- ❌ Incomplete gRPC protocol implementation
|
|
303
|
+
- ❌ Performance might be worse than C extension
|
|
304
|
+
- ❌ Maintenance burden (keep up with gRPC spec changes)
|
|
305
|
+
- ❌ Edge cases we haven't thought of
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: async-grpc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Samuel Williams
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async-http
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: protocol-grpc
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.2'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.2'
|
|
40
|
+
executables: []
|
|
41
|
+
extensions: []
|
|
42
|
+
extra_rdoc_files: []
|
|
43
|
+
files:
|
|
44
|
+
- code.md
|
|
45
|
+
- context/getting-started.md
|
|
46
|
+
- context/index.yaml
|
|
47
|
+
- design.md
|
|
48
|
+
- lib/async/grpc.rb
|
|
49
|
+
- lib/async/grpc/client.rb
|
|
50
|
+
- lib/async/grpc/dispatcher_middleware.rb
|
|
51
|
+
- lib/async/grpc/service.rb
|
|
52
|
+
- lib/async/grpc/stub.rb
|
|
53
|
+
- lib/async/grpc/version.rb
|
|
54
|
+
- license.md
|
|
55
|
+
- readme.md
|
|
56
|
+
- releases.md
|
|
57
|
+
- spanner_integration.md
|
|
58
|
+
homepage: https://github.com/socketry/async-grpc
|
|
59
|
+
licenses:
|
|
60
|
+
- MIT
|
|
61
|
+
metadata:
|
|
62
|
+
documentation_uri: https://socketry.github.io/async-grpc/
|
|
63
|
+
source_code_uri: https://github.com/socketry/async-grpc.git
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '3.2'
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 3.6.9
|
|
79
|
+
specification_version: 4
|
|
80
|
+
summary: Client and server implementation for gRPC using Async.
|
|
81
|
+
test_files: []
|