connectrpc-falcon 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/LICENSE +21 -0
- data/README.md +352 -0
- data/examples/falcon.rb +31 -0
- data/lib/connectrpc/falcon/completion_join.rb +247 -0
- data/lib/connectrpc/falcon/endpoint.rb +58 -0
- data/lib/connectrpc/falcon/environment.rb +69 -0
- data/lib/connectrpc/falcon/http1.rb +501 -0
- data/lib/connectrpc/falcon/rack_guard.rb +175 -0
- data/lib/connectrpc/falcon/server.rb +121 -0
- data/lib/connectrpc/falcon/version.rb +7 -0
- data/lib/connectrpc/falcon.rb +21 -0
- data/rbs/async_service_0_25/service.rbs +29 -0
- data/sig/connectrpc-falcon.rbs +66 -0
- data/sig/manifest.yaml +2 -0
- metadata +151 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6b19635dd523a8e74030ff16895e26bb0e48425980c371ff738f9c5c7dfdda82
|
|
4
|
+
data.tar.gz: 5e6af3b34ebb3a0b0aa27d0101897fffeb8b77aede76c7b6da8a7ae5ef5d11f9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 57a7800ba24453582815b87d79bb748b332bbc3fa03c595dccb01656df48d84be01d1d917f19afe7510eac619bf33898489201182ab077cf8d335bd659a0c36b
|
|
7
|
+
data.tar.gz: b66a1c98fa1039136f71bd12177bfb59cf9dfbc2a7a1b0f6f84ccc9d38b7c3719d8533f5a41e84d589004715f87b05755524fad6c93794836a4da1419b4b28c7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jim Myhrberg
|
|
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,352 @@
|
|
|
1
|
+
# connectrpc-falcon
|
|
2
|
+
|
|
3
|
+
Optional Falcon process-worker hosting for Connect unary and streaming RPC, with native
|
|
4
|
+
Protocol::HTTP dispatch and an explicit ordinary Rack/Rails fallback.
|
|
5
|
+
Require `connectrpc/falcon`; core and Rails bundles do not load this package.
|
|
6
|
+
The API is experimental. This package is MIT licensed and released with the
|
|
7
|
+
whole [release train](../../docs/releasing.md).
|
|
8
|
+
|
|
9
|
+
See the [supported 0.x contract](../../docs/support.md) for the target support
|
|
10
|
+
profile, matched package versions, compatibility policy and adoption gates.
|
|
11
|
+
|
|
12
|
+
## Run a service
|
|
13
|
+
|
|
14
|
+
The [runnable configuration](examples/falcon.rb) uses Falcon's normal service
|
|
15
|
+
DSL and Google protobuf's standard `StringValue` message. In an application
|
|
16
|
+
bundle containing `connectrpc-falcon` and `google-protobuf`, run:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bundle exec falcon host examples/falcon.rb
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The example binds localhost, starts two workers and serves
|
|
23
|
+
`/demo.EchoService/Echo`. `mise run falcon:example` runs an ephemeral version,
|
|
24
|
+
checks both codecs and the Rack fallback, then stops all workers.
|
|
25
|
+
|
|
26
|
+
Include `ConnectRPC::Falcon::Environment` in each service. Define
|
|
27
|
+
`connectrpc_server` to construct a `ConnectRPC::Server` in the worker. Optional
|
|
28
|
+
`rack_app`, `tls`, `tls_provider` and `server_options` methods supply the fallback,
|
|
29
|
+
TLS source and shared limits. `url` describes the listener. Application
|
|
30
|
+
`prepare!` runs after fork, before static TLS compilation or the first provider
|
|
31
|
+
call. Initialize application-owned database clients there; the library never
|
|
32
|
+
opens a database connection. Falcon binds raw TCP in the parent and owns
|
|
33
|
+
worker count, readiness, health checks and its default restart policy.
|
|
34
|
+
|
|
35
|
+
For direct use inside an application-owned Async task:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
endpoint = ConnectRPC::Falcon::Endpoint.parse("https://127.0.0.1:9292")
|
|
39
|
+
host = ConnectRPC::Falcon::Server.new(
|
|
40
|
+
server: connect_server,
|
|
41
|
+
endpoint: endpoint,
|
|
42
|
+
rack_app: ordinary_rack_app,
|
|
43
|
+
tls_provider: -> { application_tls_snapshot }
|
|
44
|
+
)
|
|
45
|
+
begin
|
|
46
|
+
host.run
|
|
47
|
+
# Run the rest of the application in this reactor.
|
|
48
|
+
ensure
|
|
49
|
+
host.close(grace: 5)
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Rack fallback caching is disabled by default (`cache: false`). Falcon's fallback
|
|
54
|
+
content encoding remains active, and native RPC bypasses both middleware layers.
|
|
55
|
+
To enable Falcon's existing shared in-memory cache within each worker, pass
|
|
56
|
+
`cache: true` to `ConnectRPC::Falcon::Server.new`. In a service configuration,
|
|
57
|
+
use the supported `server_options` path:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
server_options { { cache: true } }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The inherited Falcon `cache` DSL setting does not configure this host. `cache:`
|
|
64
|
+
must be `true` or `false`; with no `rack_app`, either value has no effect.
|
|
65
|
+
|
|
66
|
+
Opt-in retains the pinned Falcon 0.57.0 / async-http-cache 0.4.6 semantics. Cache
|
|
67
|
+
keys use authority, method and path, plus response `Vary` headers. Custom
|
|
68
|
+
authorization headers are not automatically cache keys. GET/HEAD requests
|
|
69
|
+
without a body, `Authorization` or `Cookie` can have 200, 203, 300, 301, 302, 404
|
|
70
|
+
and 410 responses cached without an explicit `Cache-Control` header. Entries
|
|
71
|
+
without `max-age` can persist for the worker's lifetime.
|
|
72
|
+
Applications must set `Cache-Control` and `Vary` so responses are safe to share.
|
|
73
|
+
Use `private` or `no-store` for unshared content; response `no-cache` alone is
|
|
74
|
+
insufficient. Use appropriate `Vary` headers for deliberately shared content
|
|
75
|
+
that depends on request headers. These are upstream cache semantics, not a
|
|
76
|
+
promise of full RFC cache policy support.
|
|
77
|
+
|
|
78
|
+
A TLS provider returns a complete Hash with exactly these symbol keys:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
{
|
|
82
|
+
version: "application-owned-opaque-version",
|
|
83
|
+
certificate_chain: [leaf_pem, intermediate_pem],
|
|
84
|
+
private_key: private_key_pem,
|
|
85
|
+
client_ca: [explicit_client_ca_pem]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use explicit `client_ca: nil` for server-only TLS. A missing/nil provider result,
|
|
90
|
+
missing field, invalid key/chain/trust or source exception fails closed; it never
|
|
91
|
+
selects plaintext. Listener scheme and TLS settings must agree. The same
|
|
92
|
+
complete configuration without `version` can be supplied as static `tls:`.
|
|
93
|
+
|
|
94
|
+
## TLS lifecycle
|
|
95
|
+
|
|
96
|
+
The worker caches immutable compiled snapshots. Handshakes perform no provider
|
|
97
|
+
or database work. Every raw accept captures one generation before scheduling,
|
|
98
|
+
including no-SNI/IP clients and sockets waiting to send their ClientHello.
|
|
99
|
+
Pending handshakes keep that captured identity even if a refresh completes.
|
|
100
|
+
TLS negotiates `h2` or `http/1.1`; no ALPN selects HTTP/1.1. Plaintext supports
|
|
101
|
+
HTTP/2 prior knowledge and HTTP/1. The standalone Async Host remains strict h2.
|
|
102
|
+
|
|
103
|
+
Provider defaults are `refresh_interval: 60`, `refresh_jitter: 0.1` and
|
|
104
|
+
`refresh_timeout: 10`. Scheduling uses monotonic time after completed source
|
|
105
|
+
checks, with uniform positive/negative jitter. Concurrent `refresh_tls` callers
|
|
106
|
+
join one worker-local attempt. Cancelling one waiter does not cancel that
|
|
107
|
+
attempt. Recursive provider refresh/close calls fail safely. These cooperative
|
|
108
|
+
timeouts and shutdown bounds cannot preempt arbitrary native or CPU-bound code.
|
|
109
|
+
There is no fixed database-outage propagation guarantee or fleet-atomic update.
|
|
110
|
+
|
|
111
|
+
Every result is validated. Equal versions with equal canonical identity/trust
|
|
112
|
+
content preserve generation and context; PEM whitespace and trust-anchor order
|
|
113
|
+
do not cause churn. Equal versions with changed content fail. A different
|
|
114
|
+
version can deliberately restore previously used material with a new generation.
|
|
115
|
+
Failed refreshes retain the last good snapshot and source-check timestamp.
|
|
116
|
+
There is no maximum-staleness policy. Actual wall-clock expiry of the earliest
|
|
117
|
+
served identity-chain certificate rejects new accepts and pending handshake
|
|
118
|
+
starts. Established connections retain their negotiated identity/trust.
|
|
119
|
+
|
|
120
|
+
`refresh_tls` requests an immediate provider check. `reload_tls` accepts the
|
|
121
|
+
three static TLS fields and conflicts with provider ownership. `tls_generation`
|
|
122
|
+
is initially 1 for TLS. `tls_status` returns safe state, generation, category,
|
|
123
|
+
`source_check_age` and `publication_age` in monotonic seconds; it never exposes
|
|
124
|
+
the source version, certificates, keys or fingerprints. `on_tls_error:` receives
|
|
125
|
+
an immutable safe category/status event. Default diagnostics are fixed safe
|
|
126
|
+
text; reporter exceptions are isolated. Initial failure prevents successful
|
|
127
|
+
TLS/application traffic before Falcon reports startup failure. TCP backlog can
|
|
128
|
+
still queue connections on the parent-bound listener.
|
|
129
|
+
|
|
130
|
+
`drain_tls(grace: 5)` captures a generation cutoff and retires older connections.
|
|
131
|
+
Pending older handshakes close immediately. Already admitted responses can
|
|
132
|
+
finish; later requests/streams are refused. `peer_certificate` returns a detached
|
|
133
|
+
authenticated client leaf only in the exact session handler task. Child tasks,
|
|
134
|
+
other servers, plaintext and server-only TLS have no client identity. It does
|
|
135
|
+
not claim that a peer-presented chain is a verified chain.
|
|
136
|
+
|
|
137
|
+
TLS keeps the shared Async policy: explicit-only trust anchors, scoped partial
|
|
138
|
+
chains, TLS 1.2+, fresh session context per generation, disabled session cache
|
|
139
|
+
and `OP_NO_TICKET`. This does not claim that OpenSSL emits zero TLS 1.3 tickets.
|
|
140
|
+
|
|
141
|
+
## Routing and shutdown
|
|
142
|
+
|
|
143
|
+
The native application owns each registered service prefix, including unknown
|
|
144
|
+
methods. It separates the raw query from the path for core dispatch. Safe unary GET
|
|
145
|
+
uses the same application over HTTP/1 and HTTP/2; the app must route intended
|
|
146
|
+
GET calls and own cache/security policy. Native RPC retains the core
|
|
147
|
+
opaque identity/gzip bytes, wire/inflated caps, deadlines, metadata and errors.
|
|
148
|
+
Falcon's generic content encoding/cache middleware wraps only `rack_app`.
|
|
149
|
+
Native handlers do not inherit Rails callbacks/authentication. Ordinary Rails
|
|
150
|
+
fallback preserves its controller, CSRF and session policy.
|
|
151
|
+
|
|
152
|
+
This listener rejects upgrades, CONNECT tunnels, full/partial Rack hijack and
|
|
153
|
+
protocol upgrades before successful upgrade bytes or socket transfer. WebSockets
|
|
154
|
+
and ActionCable are unsupported here. Ordinary enumerable, chunked, streamed
|
|
155
|
+
and callable HTTP bodies are supported. Callable bodies must close their supplied
|
|
156
|
+
stream according to the Protocol::Rack contract. Plain Falcon and the existing
|
|
157
|
+
Rack/Rails integration APIs are unchanged.
|
|
158
|
+
|
|
159
|
+
Native H2 uses the shared Async private bounded ingress for both RPC requests and
|
|
160
|
+
ordinary Rack fallback input. Slow or nonreading Rack consumers withhold stream
|
|
161
|
+
credit while healthy same-connection streams can advance. Connect HTTP/1.1 early responses use the bounded input retirement policy below.
|
|
162
|
+
See the [Async ingress bounds](../connectrpc-async/README.md#backend-boundaries-and-verification)
|
|
163
|
+
for padding, cleanup, allocation layers and the limits of this guarantee.
|
|
164
|
+
|
|
165
|
+
`close(grace: 5)` and normal managed worker stop stop accepts/refresh, then
|
|
166
|
+
perform bounded connection drain. HTTP/1 completion includes fixed/chunked wire
|
|
167
|
+
bytes and response-finished callbacks. On grace expiry, teardown closes raw TCP
|
|
168
|
+
before cancelling owned work and buffered/TLS cleanup. Callback close is guarded
|
|
169
|
+
against reentry. Fatal teardown or owner cancellation outside the managed stop
|
|
170
|
+
boundary uses raw-first abort and offers no response-delivery guarantee.
|
|
171
|
+
HTTP response completion is separate from TLS `close_notify`; orderly TLS
|
|
172
|
+
shutdown is not promised.
|
|
173
|
+
|
|
174
|
+
External abort and close callers wait for actual connection retirement, including
|
|
175
|
+
owned processors, output tasks, cleanup descendants and final disposal. Owned
|
|
176
|
+
tasks initiating or reentering abort request retirement without waiting on
|
|
177
|
+
cleanup that must join them. A session accepted handle's existing `wait_closed`
|
|
178
|
+
operation uses this same boundary, so the active connection metric and server
|
|
179
|
+
connection slot remain retained until cleanup completes. Each external abort/close
|
|
180
|
+
joiner retains its own first interruption through the join.
|
|
181
|
+
An external close caller interrupted during a nonzero graceful drain requests
|
|
182
|
+
forced session retirement, joins actual cleanup, then raises its identical first
|
|
183
|
+
exception. Competitors keep their own result or exception, and an already-decided
|
|
184
|
+
outcome remains unchanged.
|
|
185
|
+
|
|
186
|
+
`Server#close` returns `bool?`: a resource-owned handler initiating positive-grace
|
|
187
|
+
close starts the ordinary asynchronous drain and returns nil while pending.
|
|
188
|
+
Admitted siblings keep their grace and can continue processing. Owned repeated
|
|
189
|
+
requests and cleanup/close coordinator reentry also return nil while pending,
|
|
190
|
+
without joining cleanup that must join them. Nil neither decides the outcome nor
|
|
191
|
+
publishes completion. Repeated requests retain the first closer and expiry even
|
|
192
|
+
when passed a different grace. External callers join actual cleanup and receive
|
|
193
|
+
the final boolean, true for drained or false for forced teardown. Once closed,
|
|
194
|
+
all calls return that same boolean, including coordinator reentry. Zero grace
|
|
195
|
+
still forces teardown; explicit abort and already-decided outcomes are preserved.
|
|
196
|
+
|
|
197
|
+
Private task joins distinguish a resolved result from actual finalization. They
|
|
198
|
+
block on required children and task-local progress/finalization notifications,
|
|
199
|
+
retaining captured child identities through reparenting or transient changes.
|
|
200
|
+
A scheduling opportunity after raw close establishes no completion boundary;
|
|
201
|
+
all retained task joins still precede disposal and retirement publication.
|
|
202
|
+
|
|
203
|
+
The grace period bounds cooperative drain before forced teardown; it is not a
|
|
204
|
+
hard cleanup timer. Non-yielding application/native work can delay retirement
|
|
205
|
+
indefinitely. Ordinary owner cancellation leaves independent cleanup running
|
|
206
|
+
while an external waiter or lifetime guard keeps the tree alive; terminating the
|
|
207
|
+
reactor cannot guarantee completion. H2 client pool cleanup can also outlast an
|
|
208
|
+
RPC deadline, as described in the [Async guide](../connectrpc-async/README.md).
|
|
209
|
+
|
|
210
|
+
Falcon's local HTTP/1 resource and the shared Async session use source-coupled
|
|
211
|
+
private completion and lifetime-anchor operations. Use matched Async/Falcon
|
|
212
|
+
package versions. These operations add no public API or SPI and change neither
|
|
213
|
+
HTTP/1 response timing nor the input retirement limits below.
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
The independent checksummed bundle uses `vendor/falcon` and reports full optional
|
|
218
|
+
library coverage under `coverage/falcon/<Ruby version>`:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
mise run falcon:setup
|
|
222
|
+
mise run falcon:test
|
|
223
|
+
mise run falcon:check
|
|
224
|
+
mise run falcon:verify
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`falcon:tools`, `falcon:build`, `falcon:package`, `falcon:example` and
|
|
228
|
+
`falcon:interop` are separate discoverable leaves. Both Ruby lanes exercise real
|
|
229
|
+
localhost protocols and workers. The Go peer uses the repository's independently
|
|
230
|
+
pinned Connect/Go graph. This is unary subset evidence, not full conformance.
|
|
231
|
+
|
|
232
|
+
## RPC observation
|
|
233
|
+
|
|
234
|
+
Configure `instrumentation:` on the core Client/Server to enable the fixed
|
|
235
|
+
neutral per-call lifecycle or the optional [OpenTelemetry adapter](../connectrpc-otel/README.md).
|
|
236
|
+
Native hosts create explicit handles before request admission, activate the
|
|
237
|
+
actual handler tasks, and finish after full-write/reset/cleanup evidence.
|
|
238
|
+
Connection-level setup/admission before an RPC exists is outside this lifecycle.
|
|
239
|
+
|
|
240
|
+
## Static signatures
|
|
241
|
+
|
|
242
|
+
This package owns `sig/` and its discovery manifest. Add
|
|
243
|
+
`library "connectrpc-falcon"` to a strict Steep target with this gem installed.
|
|
244
|
+
See the [RBS guide](../../docs/rbs.md) for supported APIs, extension contracts,
|
|
245
|
+
partial records, runtime validation and framework-signature limits.
|
|
246
|
+
`mise run rbs:check` checks application callers for this package; strict core
|
|
247
|
+
source checking is a separate target from backend implementation checking.
|
|
248
|
+
|
|
249
|
+
The finite upstream compatibility signatures in `rbs/async_service_0_25` are explicit
|
|
250
|
+
opt-ins, separate from normal gem discovery. See the [RBS guide](../../docs/rbs.md#opt-in-framework-compatibility)
|
|
251
|
+
for tested versions, supported framework forms and how to avoid duplicate
|
|
252
|
+
application-owned declarations.
|
|
253
|
+
|
|
254
|
+
## Explicit unary cancellation
|
|
255
|
+
|
|
256
|
+
Pass `cancellation: source.token` to core or generated client calls, where
|
|
257
|
+
`source = ConnectRPC::CancellationSource.new`. The source is permanent and
|
|
258
|
+
shareable; only `source.cancel` initiates cancellation. `timeout: nil` keeps no
|
|
259
|
+
RPC deadline or wire timeout. Context offers cooperative `cancelled?`,
|
|
260
|
+
`cancellation_reason` and `check!`. See the [core guide](../../README.md) for
|
|
261
|
+
listener, arbitration and registration lifetime rules.
|
|
262
|
+
|
|
263
|
+
The Async transport accepts foreign-thread cancellation through a Promise;
|
|
264
|
+
only the owner reactor stops private SDK tasks and performs stream cleanup.
|
|
265
|
+
Healthy multiplexed siblings survive. A stuck frame retains the one 100 ms
|
|
266
|
+
raw-first whole-connection cleanup fallback. Native HTTP/2 Context observes
|
|
267
|
+
only an actual peer RST_STREAM(CANCEL), before input teardown. Other resets,
|
|
268
|
+
normal half-close, connection loss and host shutdown do not fabricate explicit
|
|
269
|
+
cancellation. Processing completion detaches the source independently of delivery.
|
|
270
|
+
|
|
271
|
+
Falcon HTTP/1 and portable Rack/Rails do not add peer detection or application
|
|
272
|
+
token env injection. Application handlers remain cooperative.
|
|
273
|
+
|
|
274
|
+
## Native streaming
|
|
275
|
+
|
|
276
|
+
The core [streaming guide](../../docs/streaming.md) documents all three scoped
|
|
277
|
+
Connect streaming shapes, typed generated clients/handlers, early headers,
|
|
278
|
+
per-message limits and bounded ownership. Native Async/Falcon HTTP/2 supports
|
|
279
|
+
both codecs and identity/gzip over h2c/TLS/mTLS. Falcon additionally serves Connect server and client
|
|
280
|
+
streaming over HTTP/1.1,
|
|
281
|
+
with protobuf/ProtoJSON and identity/gzip over TCP/TLS/mTLS. The standalone
|
|
282
|
+
Async Host and original Async Transport remain H2-only. The explicit
|
|
283
|
+
`ConnectRPC::Async::HTTP1Transport` supports Connect unary, safe GET, server
|
|
284
|
+
streaming and client streaming in an application-owned reactor.
|
|
285
|
+
|
|
286
|
+
The streaming guide defines Falcon's cooperative 500 ms total final-close budget
|
|
287
|
+
and unchanged 100 ms / 256 KiB discard limits. Native Connect streaming clients
|
|
288
|
+
have a separate one-second absolute HTTP tail policy. It applies even with a nil
|
|
289
|
+
RPC deadline and can reject otherwise valid late EOF or slow intermediaries.
|
|
290
|
+
|
|
291
|
+
Incomplete H1 uploads disable reuse at response-head resolution. Falcon delivers
|
|
292
|
+
the full response and HTTP EOF after original processor cleanup, then half-closes
|
|
293
|
+
raw TCP writes. One absolute final-close expiry starts at the actual final HTTP
|
|
294
|
+
flush plus 500 ms, before body callbacks or lifecycle observation. Original-input
|
|
295
|
+
discard retains one absolute 100 ms allowance and 256 KiB cap, including adapter
|
|
296
|
+
remainders and full 64 KiB read reservations. After discard stops, Falcon waits
|
|
297
|
+
only the remaining part of the original 500 ms budget, without further reads.
|
|
298
|
+
Following pipelined requests are never parsed or dispatched. Admission/socket
|
|
299
|
+
ownership lasts until retirement and original processing join. Earlier owner
|
|
300
|
+
shutdown can shorten either phase; failed delivery receives no success grace.
|
|
301
|
+
|
|
302
|
+
Both timers are cooperative and may overshoot under scheduler delay. Holding
|
|
303
|
+
admission during remaining grace increases resource retention. This is best
|
|
304
|
+
effort reset avoidance, with no arbitrary network-delivery guarantee. The separate
|
|
305
|
+
`falcon:conformance:http1` production profiles pass 174/174 identity and 160/160
|
|
306
|
+
gzip cases on Ruby 3.4.12 and 4.0.9 with unchanged assertions and inventories.
|
|
307
|
+
Web rejection-head ordering is repaired separately, with its gate excluded from
|
|
308
|
+
Connect response-first delivery. Historical Web failures and the baseline
|
|
309
|
+
repair remain recorded in the
|
|
310
|
+
[conformance status](../../conformance/README.md#connect-http11-server-profiles).
|
|
311
|
+
|
|
312
|
+
`falcon:conformance` runs actual server profiles. `falcon:interop:streaming`
|
|
313
|
+
exercises real workers with the pinned Go streaming client. Both belong to
|
|
314
|
+
`falcon:verify`; duplicated native-client runs are not Falcon server evidence.
|
|
315
|
+
|
|
316
|
+
## Native gRPC
|
|
317
|
+
|
|
318
|
+
Native HTTP/2 supports unary, server streaming, client streaming and bidi gRPC
|
|
319
|
+
with protobuf, identity/gzip, h2c, verified TLS and mTLS. Configure the core
|
|
320
|
+
Client with `protocol: :grpc` and the Server with `:grpc` or `:auto`. Generated
|
|
321
|
+
clients and handlers reuse the existing declarations. Portable HTTP/1 execution
|
|
322
|
+
retains its early rejection guard. See the [gRPC guide](../../docs/grpc.md) for
|
|
323
|
+
status codecs, actual final HEADERS, independent status bounds and native field
|
|
324
|
+
retention limits. Connect's one-second terminal tail policy remains Connect-only.
|
|
325
|
+
|
|
326
|
+
Use `falcon:conformance:grpc` for exact official inventories and
|
|
327
|
+
`falcon:interop:grpc` for generated official grpc-go peer checks. Both belong
|
|
328
|
+
to the owning verify task. `grpc:generation:check` reproduces official Go and
|
|
329
|
+
Ruby peer outputs; `grpc:status:check` reproduces private Any/Status descriptors.
|
|
330
|
+
|
|
331
|
+
## gRPC-Web
|
|
332
|
+
|
|
333
|
+
Native H1 and H2 support explicitly enabled binary/text unary and server-stream gRPC-Web. H1 retains original processor identity and admission through wire/callback completion. Browser incremental delivery uses text.
|
|
334
|
+
See the [Web guide](../../docs/grpc-web.md) for protocol, CORS, bounds and completion rules.
|
|
335
|
+
|
|
336
|
+
Rejected native Web HTTP/1 input now has an explicit resource-policy allowance:
|
|
337
|
+
one absolute monotonic 100 ms budget to finish the original fixed/chunked body
|
|
338
|
+
framing. Progress never resets it. The valid grpc-timeout captured at original
|
|
339
|
+
application entry can shorten this allowance; cancellation, host drain and
|
|
340
|
+
owner abort retain control. Reads use the pinned HTTP/1 body's at-most-64 KiB
|
|
341
|
+
chunks, without aggregating the upload or consuming the next request. An existing
|
|
342
|
+
input-adapter remainder can coexist with one discard read, each at most 64 KiB;
|
|
343
|
+
this is not a total allocation or RSS bound. Successful
|
|
344
|
+
calls already consume their input and do not enter this tail policy.
|
|
345
|
+
|
|
346
|
+
On expiry or malformed framing, cleanup retires only the affected connection,
|
|
347
|
+
closing raw TCP before body/TLS cleanup. The original RPC processing outcome
|
|
348
|
+
remains separate from aborted delivery and cleanup failure. Terminal bytes stay
|
|
349
|
+
staged until the original processor and application cleanup succeed. This is a
|
|
350
|
+
best effort within bounded cleanup, not a delivery promise for arbitrarily slow
|
|
351
|
+
uploads. The tail budget runs once and does not restart or stack; existing
|
|
352
|
+
backend input-cancellation and teardown allowances remain separate.
|
data/examples/falcon.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Run with: bundle exec falcon host examples/falcon.rb
|
|
4
|
+
require "connectrpc/falcon"
|
|
5
|
+
require "connectrpc/bindings/google_protobuf"
|
|
6
|
+
require "google/protobuf/wrappers_pb"
|
|
7
|
+
|
|
8
|
+
service "connectrpc-echo" do
|
|
9
|
+
include ConnectRPC::Falcon::Environment
|
|
10
|
+
|
|
11
|
+
url { ENV.fetch("CONNECTRPC_URL", "http://127.0.0.1:9292") }
|
|
12
|
+
count 2
|
|
13
|
+
|
|
14
|
+
# Application initialization runs in each Falcon worker before TLS loads.
|
|
15
|
+
prepare! do |_instance|
|
|
16
|
+
# Initialize application-owned dependencies here, after fork.
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
connectrpc_server do
|
|
20
|
+
method = ConnectRPC::Method.new(service: "demo.EchoService", name: "Echo",
|
|
21
|
+
request_type: Google::Protobuf::StringValue,
|
|
22
|
+
response_type: Google::Protobuf::StringValue, idempotency: :no_side_effects)
|
|
23
|
+
service = ConnectRPC::Service.new(name: "demo.EchoService", methods: [method])
|
|
24
|
+
ConnectRPC::Server.new(services: [service], binding: ConnectRPC::Bindings::GoogleProtobuf.new,
|
|
25
|
+
handlers: { method.path => ->(message, _context) { message } })
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
rack_app do
|
|
29
|
+
->(_env) { [200, { "content-type" => "text/plain" }, ["Ordinary Rack fallback\n"]] }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConnectRPC
|
|
4
|
+
module Falcon
|
|
5
|
+
# A result is distinct from task finalization and retained child ownership.
|
|
6
|
+
class CompletionJoin
|
|
7
|
+
# Installed only on a selected instance, with independent state per gem.
|
|
8
|
+
module Consume
|
|
9
|
+
def consume
|
|
10
|
+
result = super
|
|
11
|
+
@connectrpc_falcon_completion&.consumed
|
|
12
|
+
result
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
private_constant :Consume
|
|
16
|
+
|
|
17
|
+
class Observation
|
|
18
|
+
attr_reader :required, :generation, :promise, :violation
|
|
19
|
+
|
|
20
|
+
def initialize(target)
|
|
21
|
+
@target = target
|
|
22
|
+
@fiber = target.fiber
|
|
23
|
+
@scheduler = Fiber.scheduler
|
|
24
|
+
@required = {}.compare_by_identity
|
|
25
|
+
@generation = 0
|
|
26
|
+
@promise = ::Async::Promise.new
|
|
27
|
+
@active = true
|
|
28
|
+
@terminal = false
|
|
29
|
+
@queued = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def terminal? = @terminal
|
|
33
|
+
def fiber_alive? = @fiber&.alive?
|
|
34
|
+
|
|
35
|
+
def retain(children)
|
|
36
|
+
added = false
|
|
37
|
+
children.each do |child|
|
|
38
|
+
next if @required.key?(child)
|
|
39
|
+
|
|
40
|
+
@required[child] = true
|
|
41
|
+
added = true
|
|
42
|
+
end
|
|
43
|
+
advance if added
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Node#consume can expose work before Task#finished? becomes true.
|
|
47
|
+
# Capture identities here, before any deferred check or tree mutation.
|
|
48
|
+
def consumed
|
|
49
|
+
return unless @active
|
|
50
|
+
|
|
51
|
+
retain(CompletionJoin.children(@target))
|
|
52
|
+
return if @terminal || @queued
|
|
53
|
+
|
|
54
|
+
no_work = @required.each_key.none? { |child| CompletionJoin.pending?(child) }
|
|
55
|
+
return unless @target.finished? || (@target.fiber.nil? && no_work)
|
|
56
|
+
|
|
57
|
+
raise "Completion scheduler closed before finalization" if @scheduler.closed?
|
|
58
|
+
|
|
59
|
+
@queued = Check.new(self)
|
|
60
|
+
@scheduler.push(@queued)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
violate(e)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def check
|
|
66
|
+
@queued = nil
|
|
67
|
+
retain(CompletionJoin.children(@target))
|
|
68
|
+
unless @target.finished?
|
|
69
|
+
if !fiber_alive? && @required.each_key.none? { |child| CompletionJoin.pending?(child) }
|
|
70
|
+
raise CompletionJoin.residual_violation(@target)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
return
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
raise "Finished task still has its captured live fiber" if fiber_alive?
|
|
77
|
+
|
|
78
|
+
@terminal = true
|
|
79
|
+
# The original fiber is gone, but retained joins can still expose
|
|
80
|
+
# new membership at consume. Keep that hook active until joined.
|
|
81
|
+
@fiber = @scheduler = nil
|
|
82
|
+
advance
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def violate(error)
|
|
86
|
+
return if @violation
|
|
87
|
+
|
|
88
|
+
@violation = error
|
|
89
|
+
release
|
|
90
|
+
advance
|
|
91
|
+
rescue StandardError
|
|
92
|
+
# Keep the violation inspectable even if shutdown prevents waking.
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def release
|
|
97
|
+
@active = false
|
|
98
|
+
@fiber = @scheduler = nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def advance
|
|
104
|
+
previous = @promise
|
|
105
|
+
@generation += 1
|
|
106
|
+
@promise = ::Async::Promise.new
|
|
107
|
+
previous.resolve(nil)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
private_constant :Observation
|
|
111
|
+
|
|
112
|
+
# Scheduler callbacks run after finish!'s non-yielding tail. They are
|
|
113
|
+
# neither Tasks nor children, and never execute application work.
|
|
114
|
+
class Check
|
|
115
|
+
def initialize(observation)
|
|
116
|
+
@observation = observation
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def alive? = !@observation.nil?
|
|
120
|
+
|
|
121
|
+
def transfer
|
|
122
|
+
observation = @observation
|
|
123
|
+
return unless observation
|
|
124
|
+
|
|
125
|
+
@observation = nil
|
|
126
|
+
observation.check
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
observation.violate(e)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
private_constant :Check
|
|
132
|
+
|
|
133
|
+
def self.observation(target)
|
|
134
|
+
target.instance_variable_get(:@connectrpc_falcon_completion)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def self.children(target)
|
|
138
|
+
target.children&.to_a&.reject { |child| child.transient? || child.finished? } || []
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def self.pending?(target, seen = {}.compare_by_identity)
|
|
142
|
+
return false if seen.key?(target)
|
|
143
|
+
return true unless target.finished?
|
|
144
|
+
|
|
145
|
+
seen[target] = true
|
|
146
|
+
state = observation(target)
|
|
147
|
+
return false unless state
|
|
148
|
+
|
|
149
|
+
!state.terminal? || state.violation || state.required.each_key.any? { |child| pending?(child, seen) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def self.scan(target, retained)
|
|
153
|
+
actual = children(target)
|
|
154
|
+
state = observation(target)
|
|
155
|
+
state&.retain(actual)
|
|
156
|
+
actual.each { |child| retained[child] = true }
|
|
157
|
+
state&.required&.each_key { |child| retained[child] = true }
|
|
158
|
+
retained.keys.select { |child| pending?(child) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def self.residual_violation(target)
|
|
162
|
+
attached = target.children&.to_a&.map do |child|
|
|
163
|
+
{ status: child.status, finished: child.finished?, transient: child.transient? }
|
|
164
|
+
end
|
|
165
|
+
RuntimeError.new("Completion observation violation: unfinished task without a live fiber or required work; " \
|
|
166
|
+
"status=#{target.status}; attached_children=#{attached.inspect}")
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.call(task, failure: false, on_interrupt: nil)
|
|
170
|
+
interruption = stored_failure = nil
|
|
171
|
+
pending = [[task, false, {}.compare_by_identity]]
|
|
172
|
+
begin
|
|
173
|
+
yield if block_given?
|
|
174
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- finish the join before propagating caller interruption
|
|
175
|
+
interruption = e
|
|
176
|
+
end
|
|
177
|
+
while (entry = pending.last)
|
|
178
|
+
target, known, retained = entry
|
|
179
|
+
raise ArgumentError, "Cannot join the current task" if target.current?
|
|
180
|
+
|
|
181
|
+
begin
|
|
182
|
+
if interruption && on_interrupt
|
|
183
|
+
request = on_interrupt
|
|
184
|
+
on_interrupt = nil
|
|
185
|
+
request.call
|
|
186
|
+
end
|
|
187
|
+
target.wait unless known
|
|
188
|
+
entry[1] = true
|
|
189
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- repeated interruption cannot abandon retirement
|
|
190
|
+
if target.failed? && target.result.equal?(e)
|
|
191
|
+
stored_failure ||= e if (failure && target.equal?(task)) || !e.is_a?(StandardError)
|
|
192
|
+
entry[1] = true
|
|
193
|
+
else
|
|
194
|
+
interruption ||= e
|
|
195
|
+
end
|
|
196
|
+
next
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
work = scan(target, retained)
|
|
200
|
+
state = observation(target)
|
|
201
|
+
state.consumed if state && target.finished? && !state.terminal?
|
|
202
|
+
raise state.violation if state&.violation
|
|
203
|
+
|
|
204
|
+
unless work.empty?
|
|
205
|
+
pending.concat(work.map { |child| [child, false, {}.compare_by_identity] })
|
|
206
|
+
next
|
|
207
|
+
end
|
|
208
|
+
if target.finished? && (!state || state.terminal?)
|
|
209
|
+
state&.release
|
|
210
|
+
pending.pop
|
|
211
|
+
next
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
unless state
|
|
215
|
+
raise residual_violation(target) unless target.fiber&.alive?
|
|
216
|
+
|
|
217
|
+
state = Observation.new(target)
|
|
218
|
+
target.instance_variable_set(:@connectrpc_falcon_completion, state)
|
|
219
|
+
target.extend(Consume)
|
|
220
|
+
# Installation, capture and the first rescan have no yielding gap.
|
|
221
|
+
state.consumed
|
|
222
|
+
next
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
generation = state.generation
|
|
226
|
+
signal = state.promise
|
|
227
|
+
next unless scan(target, retained).empty?
|
|
228
|
+
raise state.violation if state.violation
|
|
229
|
+
next if target.finished? && state.terminal?
|
|
230
|
+
raise residual_violation(target) if !target.finished? && !state.fiber_alive?
|
|
231
|
+
next unless generation == state.generation && signal.equal?(state.promise)
|
|
232
|
+
|
|
233
|
+
begin
|
|
234
|
+
signal.wait
|
|
235
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- latch interruption owns no completion credit
|
|
236
|
+
interruption ||= e
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
raise interruption if interruption
|
|
240
|
+
raise stored_failure if stored_failure
|
|
241
|
+
|
|
242
|
+
task.result
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
private_constant :CompletionJoin
|
|
246
|
+
end
|
|
247
|
+
end
|