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.
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
+ [![Development Status](https://github.com/socketry/async-grpc/workflows/Test/badge.svg)](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,3 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
@@ -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: []