tonic-rails 0.6.0-x86_64-linux-gnu → 0.7.0-x86_64-linux-gnu
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 +4 -4
- data/CHANGELOG.md +22 -1
- data/README.md +66 -1
- data/lib/tonic/rails/3.3/tonic_rails_native.so +0 -0
- data/lib/tonic/rails/3.4/tonic_rails_native.so +0 -0
- data/lib/tonic/rails/4.0/tonic_rails_native.so +0 -0
- data/lib/tonic/rails/client/retry_policy.rb +73 -0
- data/lib/tonic/rails/client.rb +115 -26
- data/lib/tonic/rails/instrumentation.rb +5 -4
- data/lib/tonic/rails/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83bded7e4b04dc72602f73fa065ac32bde6f97631fe4450525432ef6f93df506
|
|
4
|
+
data.tar.gz: 13bb870aa4bfb1487fa9827745aceac8c1a0a211053a9b5adafebe494975307e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 539c6220bfa47fb98b506c826112ad5728bde68693c37a3479dd2727baff3b14fd07cb689c4c72419d05228a4c3042ea4ce636b44f7c1cdbad06013a2084f8ba
|
|
7
|
+
data.tar.gz: 9ac35b6b569b9f4083ea342c5e55fe321dda57b89ec4d797ec41624ae2477e47957f71cd4569f9c42fb592d5b1860037ba0fdf2378a75fbba2cf3ed894e1c3da
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ minor versions may contain breaking changes.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.0] - 2026-10-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Client: opt-in retries, per client or per call (`retries: true` or a
|
|
15
|
+
Hash of `max_attempts`, `codes`, `initial_backoff`, `max_backoff` and
|
|
16
|
+
`multiplier`), following gRPC's retry design. Only calls the server
|
|
17
|
+
never started on are retried: a failed connection, or a status sent
|
|
18
|
+
without response headers. Waits are random with exponential caps, stop
|
|
19
|
+
before the deadline and follow `grpc-retry-pushback-ms`. Each retry is
|
|
20
|
+
logged, and `request_rpc.tonic_rails` reports `:attempts`. Closes #39.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- The server answers calls it turns away before a handler runs (shutting
|
|
25
|
+
down, dispatch queue full, malformed requests) with a Trailers-Only
|
|
26
|
+
response: the status in the headers and no body. Clients that follow
|
|
27
|
+
gRPC's retry rules can tell those calls never started and retry them
|
|
28
|
+
safely.
|
|
29
|
+
|
|
10
30
|
## [0.6.0] - 2026-10-11
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -166,7 +186,8 @@ First public release.
|
|
|
166
186
|
- Precompiled native gems for Linux (x86_64, aarch64; glibc and musl) and
|
|
167
187
|
macOS (x86_64, arm64), Ruby 3.3 to 4.0.
|
|
168
188
|
|
|
169
|
-
[Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.
|
|
189
|
+
[Unreleased]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.7.0...HEAD
|
|
190
|
+
[0.7.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.6.0...v0.7.0
|
|
170
191
|
[0.6.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.5.0...v0.6.0
|
|
171
192
|
[0.5.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.4.0...v0.5.0
|
|
172
193
|
[0.4.0]: https://github.com/CodingAnarchy/tonic-rails/compare/v0.3.0...v0.4.0
|
data/README.md
CHANGED
|
@@ -507,6 +507,68 @@ If a handler lets the error propagate, its caller receives the same
|
|
|
507
507
|
status, message, details and trailing metadata. Rescue it and raise your
|
|
508
508
|
own error when those are internal.
|
|
509
509
|
|
|
510
|
+
### Retries
|
|
511
|
+
|
|
512
|
+
Retries are off unless you turn them on, for a client or a single call:
|
|
513
|
+
|
|
514
|
+
```ruby
|
|
515
|
+
ITEMS = Tonic::Rails::Client.new("items.internal:50051", deadline: 2, retries: true)
|
|
516
|
+
|
|
517
|
+
items.get_item({ id: 1 }, retries: false) # not this call
|
|
518
|
+
items.get_item({ id: 1 }, retries: { max_attempts: 5 }) # or differently
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
`retries: true` tries a call up to 3 times when it fails with
|
|
522
|
+
`UNAVAILABLE`. A Hash changes any of these:
|
|
523
|
+
|
|
524
|
+
| Field | Default | |
|
|
525
|
+
|---|---|---|
|
|
526
|
+
| `max_attempts` | 3 | attempts in all, from 2 to 5 |
|
|
527
|
+
| `codes` | `[:unavailable]` | status codes worth retrying |
|
|
528
|
+
| `initial_backoff` | 0.1 | seconds; the cap on the first wait |
|
|
529
|
+
| `max_backoff` | 1 | seconds; the cap on any wait |
|
|
530
|
+
| `multiplier` | 2 | how much the cap grows per attempt |
|
|
531
|
+
|
|
532
|
+
The rules come from gRPC's own retry design, so they avoid the usual
|
|
533
|
+
mistakes of a hand-written retry loop:
|
|
534
|
+
|
|
535
|
+
- **A call is retried only if the server never started on it.** That
|
|
536
|
+
means the connection failed (refused, reset, a pod restarting), or the
|
|
537
|
+
server answered with only a status and no response headers. Servers do
|
|
538
|
+
that when they turn a call away before any handler runs: a
|
|
539
|
+
tonic-rails server that's shutting down or whose queue is full, or a
|
|
540
|
+
proxy with no healthy backend. Once response headers arrive, the call
|
|
541
|
+
is never repeated, even if it then fails with a code you listed.
|
|
542
|
+
- **Waits are random.** Each one is anywhere from zero up to a cap that
|
|
543
|
+
starts at `initial_backoff` and grows by `multiplier` per attempt, up to
|
|
544
|
+
`max_backoff`. Clients that failed together don't retry together.
|
|
545
|
+
- **The deadline covers every attempt.** If the next attempt couldn't
|
|
546
|
+
start before the deadline, the client stops and raises the last
|
|
547
|
+
failure.
|
|
548
|
+
- **The server can steer.** A `grpc-retry-pushback-ms` trailer sets the
|
|
549
|
+
wait, and a negative one stops retries.
|
|
550
|
+
- A server stream is retried only before its first message.
|
|
551
|
+
|
|
552
|
+
Which calls are safe to retry? With the default `[:unavailable]`, nearly
|
|
553
|
+
all of them. By gRPC convention, `UNAVAILABLE` means the call can be tried
|
|
554
|
+
again, and from a tonic-rails server it's retried only when no handler
|
|
555
|
+
ran. For a tonic-rails server, `:resource_exhausted` is worth adding too:
|
|
556
|
+
a full dispatch queue returns it before any handler runs.
|
|
557
|
+
|
|
558
|
+
Take more care with codes a handler might raise after doing some work,
|
|
559
|
+
like `:aborted`. A tonic-rails server sends headers before it runs a
|
|
560
|
+
handler, so those calls still won't be retried. Other servers may answer
|
|
561
|
+
with only a status after a handler ran, so list those codes only for
|
|
562
|
+
methods that are safe to run twice.
|
|
563
|
+
|
|
564
|
+
Each retry is logged as a warning, through the client's `logger:` (by
|
|
565
|
+
default `Tonic::Rails.configuration.logger`, which is the Rails logger in
|
|
566
|
+
a Rails app):
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
tonic-rails: /demo.v1.ItemService/GetItem failed with UNAVAILABLE: ... (attempt 1 of 3); retrying in 64ms
|
|
570
|
+
```
|
|
571
|
+
|
|
510
572
|
### Metadata
|
|
511
573
|
|
|
512
574
|
`metadata:` on a call, and on the client for every call, works like
|
|
@@ -549,6 +611,8 @@ the client is created, not on the first call.
|
|
|
549
611
|
| `metadata` | `{}` | sent with every call |
|
|
550
612
|
| `max_message_size` | 4 MiB | largest message sent or received |
|
|
551
613
|
| `connect_timeout` | 5 | seconds to wait for a connection |
|
|
614
|
+
| `retries` | `false` | retry calls the server never started on; see [Retries](#retries) |
|
|
615
|
+
| `logger` | `Tonic::Rails.configuration.logger` | where retries are logged |
|
|
552
616
|
|
|
553
617
|
### Fork safety
|
|
554
618
|
|
|
@@ -830,7 +894,8 @@ When a call fails, `event.payload[:exception_object]` is the
|
|
|
830
894
|
unexpected, that error's `cause` is the original exception.
|
|
831
895
|
|
|
832
896
|
Calls made with `Tonic::Rails::Client` publish `request_rpc.tonic_rails`,
|
|
833
|
-
with the same payload plus `:address`, the server called
|
|
897
|
+
with the same payload plus `:address`, the server called, and
|
|
898
|
+
`:attempts`, how many times it was tried. For a
|
|
834
899
|
server-streaming call, the event covers the whole stream, including the
|
|
835
900
|
time your block spends on each message.
|
|
836
901
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tonic
|
|
4
|
+
module Rails
|
|
5
|
+
class Client
|
|
6
|
+
# When and how often a call is retried, following gRPC's retry design
|
|
7
|
+
# (A6). Pass one, a Hash of its fields or `true` for the defaults as
|
|
8
|
+
# `retries:` to Client.new or to a single call.
|
|
9
|
+
#
|
|
10
|
+
# Tonic::Rails::Client.new("items.internal:50051", retries: true)
|
|
11
|
+
# Tonic::Rails::Client.new("items.internal:50051", retries: { max_attempts: 4 })
|
|
12
|
+
# items.get_item({ id: 1 }, retries: false)
|
|
13
|
+
#
|
|
14
|
+
# Only calls the server hasn't started on are retried: the connection
|
|
15
|
+
# failed, or the server answered with a status and no response
|
|
16
|
+
# headers. Each wait is random, up to a cap that grows from
|
|
17
|
+
# `initial_backoff` by `multiplier` per attempt to `max_backoff`.
|
|
18
|
+
# Retrying stops when the next attempt couldn't start before the
|
|
19
|
+
# call's deadline.
|
|
20
|
+
RetryPolicy = Data.define(:max_attempts, :codes, :initial_backoff, :max_backoff, :multiplier) do
|
|
21
|
+
# Defaults for the fields left out.
|
|
22
|
+
def self.defaults
|
|
23
|
+
{ max_attempts: 3, codes: [:unavailable], initial_backoff: 0.1, max_backoff: 1, multiplier: 2 }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @param option [RetryPolicy, Hash, true, false, nil]
|
|
27
|
+
# @return [RetryPolicy, nil] nil for no retries
|
|
28
|
+
def self.from(option)
|
|
29
|
+
case option
|
|
30
|
+
when nil, false then nil
|
|
31
|
+
when true then new(**defaults)
|
|
32
|
+
when Hash then new(**defaults, **option)
|
|
33
|
+
when self then option
|
|
34
|
+
else raise ArgumentError, "retries must be true, false, a Hash or a #{name}, got #{option.class}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param max_attempts [Integer] attempts in all, from 2 to 5. gRPC
|
|
39
|
+
# caps it at 5, so a misconfigured client can't multiply an
|
|
40
|
+
# outage's traffic by more than that.
|
|
41
|
+
# @param codes [Array<Symbol, Integer>] status codes worth retrying
|
|
42
|
+
# @param initial_backoff [Numeric] seconds; the cap on the first wait
|
|
43
|
+
# @param max_backoff [Numeric] seconds; the cap on any wait
|
|
44
|
+
# @param multiplier [Numeric] how much the cap grows per attempt
|
|
45
|
+
def initialize(max_attempts:, codes:, initial_backoff:, max_backoff:, multiplier:)
|
|
46
|
+
unless max_attempts.is_a?(Integer) && max_attempts.between?(2, 5)
|
|
47
|
+
raise ArgumentError, "max_attempts must be an Integer from 2 to 5"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
codes = Array(codes).map { |code| RpcError.normalize_code(code) }.uniq.freeze
|
|
51
|
+
raise ArgumentError, "codes must name at least one status code other than OK" if codes.empty? || codes.include?(0)
|
|
52
|
+
|
|
53
|
+
[[:initial_backoff, initial_backoff], [:max_backoff, max_backoff]].each do |field, value|
|
|
54
|
+
raise ArgumentError, "#{field} must be a positive number of seconds" unless value.is_a?(Numeric) && value.positive?
|
|
55
|
+
end
|
|
56
|
+
raise ArgumentError, "multiplier must be at least 1" unless multiplier.is_a?(Numeric) && multiplier >= 1
|
|
57
|
+
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @param code [Integer]
|
|
62
|
+
def retry?(code)
|
|
63
|
+
codes.include?(code)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Seconds to wait after failed attempt number `attempt` (from 1).
|
|
67
|
+
def backoff(attempt)
|
|
68
|
+
rand * [initial_backoff * (multiplier**(attempt - 1)), max_backoff].min
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/tonic/rails/client.rb
CHANGED
|
@@ -14,6 +14,9 @@ module Tonic
|
|
|
14
14
|
# item = items.get_item({ id: 1 }, deadline: 0.5, metadata: { "x-request-id" => request_id })
|
|
15
15
|
# items.list_items(category: "books").each { |item| ... }
|
|
16
16
|
#
|
|
17
|
+
# Retries are opt-in: `retries: true` (or a Hash; see RetryPolicy) on
|
|
18
|
+
# the client or a call retries calls the server never started on.
|
|
19
|
+
#
|
|
17
20
|
# Stubs are built from the protobuf descriptors that
|
|
18
21
|
# `tonic_rails:compile_protos` (protoc --ruby_out) generates; no
|
|
19
22
|
# `_services_pb.rb` is needed. A failed call raises
|
|
@@ -29,12 +32,20 @@ module Tonic
|
|
|
29
32
|
|
|
30
33
|
DEFAULT_MAX_MESSAGE_SIZE = 4 * 1024 * 1024
|
|
31
34
|
|
|
35
|
+
# Trailer a server sends to ask for a retry after so many
|
|
36
|
+
# milliseconds, or (with a negative value) for none.
|
|
37
|
+
RETRY_PUSHBACK = "grpc-retry-pushback-ms"
|
|
38
|
+
private_constant :RETRY_PUSHBACK
|
|
39
|
+
|
|
32
40
|
# @return [String] "host:port"
|
|
33
41
|
attr_reader :address
|
|
34
42
|
|
|
35
43
|
# @return [Numeric, nil] default deadline in seconds
|
|
36
44
|
attr_reader :deadline
|
|
37
45
|
|
|
46
|
+
# @return [RetryPolicy, nil] how calls are retried, unless they say otherwise
|
|
47
|
+
attr_reader :retries
|
|
48
|
+
|
|
38
49
|
# @param address [String] "host:port"
|
|
39
50
|
# @param tls [Boolean] false for plaintext, which must be explicit
|
|
40
51
|
# @param tls_ca_path [String, nil] CA certificates (PEM) to trust instead
|
|
@@ -48,9 +59,13 @@ module Tonic
|
|
|
48
59
|
# @param metadata [Hash] sent with every call (see Tonic::Rails::Metadata)
|
|
49
60
|
# @param max_message_size [Integer] largest message sent or received, in bytes
|
|
50
61
|
# @param connect_timeout [Numeric] seconds to wait for a connection
|
|
62
|
+
# @param retries [RetryPolicy, Hash, Boolean] retry calls the server
|
|
63
|
+
# never started on (see RetryPolicy); off by default
|
|
64
|
+
# @param logger [Logger, nil] where retries are logged; defaults to
|
|
65
|
+
# Tonic::Rails.configuration.logger
|
|
51
66
|
def initialize(address, tls: true, tls_ca_path: nil, tls_cert_path: nil, tls_key_path: nil,
|
|
52
67
|
tls_server_name: nil, deadline: nil, metadata: {}, max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
|
|
53
|
-
connect_timeout: 5)
|
|
68
|
+
connect_timeout: 5, retries: false, logger: nil)
|
|
54
69
|
Tonic::Rails.native_extension!
|
|
55
70
|
validate_address!(address)
|
|
56
71
|
validate_tls!(tls, tls_cert_path, tls_key_path)
|
|
@@ -62,6 +77,8 @@ module Tonic
|
|
|
62
77
|
|
|
63
78
|
@address = address
|
|
64
79
|
@deadline = deadline
|
|
80
|
+
@retries = RetryPolicy.from(retries)
|
|
81
|
+
@logger = logger
|
|
65
82
|
@metadata = Metadata.encode(metadata)
|
|
66
83
|
@stubs = {}
|
|
67
84
|
tls_options = [read_pem(tls_ca_path, "CA"), read_pem(tls_cert_path, "certificate"),
|
|
@@ -106,14 +123,16 @@ module Tonic
|
|
|
106
123
|
|
|
107
124
|
# Make a unary call. Stubs call this; use them instead.
|
|
108
125
|
# @api private
|
|
109
|
-
def unary(path, input, output, request, deadline: nil, metadata: {}, return_call: false)
|
|
110
|
-
|
|
126
|
+
def unary(path, input, output, request, deadline: nil, metadata: {}, return_call: false, retries: nil)
|
|
127
|
+
bytes = input.encode(request_message(path, input, request))
|
|
111
128
|
metadata = call_metadata(metadata)
|
|
112
|
-
|
|
129
|
+
deadline_at = deadline_at(deadline)
|
|
130
|
+
policy = retry_policy(retries)
|
|
113
131
|
|
|
114
|
-
Instrumentation.instrument_request(path, address, metadata) do
|
|
115
|
-
code, error_message, response, headers, trailers, details =
|
|
116
|
-
@native.unary(path,
|
|
132
|
+
Instrumentation.instrument_request(path, address, metadata) do |payload|
|
|
133
|
+
code, error_message, response, headers, trailers, details = with_retries(path, policy, deadline_at, payload) do
|
|
134
|
+
@native.unary(path, bytes, metadata, remaining_ms(deadline_at))
|
|
135
|
+
end
|
|
117
136
|
headers = decode_metadata(headers)
|
|
118
137
|
trailers = decode_metadata(trailers)
|
|
119
138
|
check_status!(code, error_message, trailers, details)
|
|
@@ -125,28 +144,25 @@ module Tonic
|
|
|
125
144
|
|
|
126
145
|
# Start a server-streaming call. Stubs call this; use them instead.
|
|
127
146
|
# @api private
|
|
128
|
-
def server_streaming(path, input, output, request, deadline: nil, metadata: {})
|
|
129
|
-
|
|
147
|
+
def server_streaming(path, input, output, request, deadline: nil, metadata: {}, retries: nil)
|
|
148
|
+
bytes = input.encode(request_message(path, input, request))
|
|
130
149
|
metadata = call_metadata(metadata)
|
|
131
150
|
# Fixed now, so the stream keeps the deadline of the RPC this
|
|
132
151
|
# thread is handling even if it's read later.
|
|
133
152
|
deadline_at = deadline_at(deadline)
|
|
153
|
+
policy = retry_policy(retries)
|
|
134
154
|
|
|
135
155
|
ServerStream.new(path) do |stream, block|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
end
|
|
143
|
-
code, error_message, _, headers, trailers, details = native.result
|
|
144
|
-
trailers = decode_metadata(trailers)
|
|
145
|
-
stream.finish(decode_metadata(headers), trailers)
|
|
146
|
-
check_status!(code, error_message, trailers, details)
|
|
147
|
-
ensure
|
|
148
|
-
native.close
|
|
156
|
+
Instrumentation.instrument_request(path, address, metadata) do |payload|
|
|
157
|
+
# A message arrives only after the response headers, which
|
|
158
|
+
# commit the call, so an attempt that yielded anything is never
|
|
159
|
+
# retried.
|
|
160
|
+
code, error_message, _, headers, trailers, details = with_retries(path, policy, deadline_at, payload) do
|
|
161
|
+
read_stream(@native.server_streaming(path, bytes, metadata, remaining_ms(deadline_at)), output, path, &block)
|
|
149
162
|
end
|
|
163
|
+
trailers = decode_metadata(trailers)
|
|
164
|
+
stream.finish(decode_metadata(headers), trailers)
|
|
165
|
+
check_status!(code, error_message, trailers, details)
|
|
150
166
|
end
|
|
151
167
|
end
|
|
152
168
|
end
|
|
@@ -222,6 +238,75 @@ module Tonic
|
|
|
222
238
|
(remaining * 1000).ceil
|
|
223
239
|
end
|
|
224
240
|
|
|
241
|
+
def retry_policy(retries)
|
|
242
|
+
retries.nil? ? @retries : RetryPolicy.from(retries)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Yield each message of a native stream, then return its result.
|
|
246
|
+
def read_stream(native, output, path)
|
|
247
|
+
while (bytes = native.next)
|
|
248
|
+
yield decode_response(output, bytes, path)
|
|
249
|
+
end
|
|
250
|
+
native.result
|
|
251
|
+
ensure
|
|
252
|
+
native.close
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Run attempts of a call (the block returns a native result) until one
|
|
256
|
+
# succeeds or shouldn't be retried; return its result.
|
|
257
|
+
def with_retries(path, policy, deadline_at, payload)
|
|
258
|
+
attempt = 1
|
|
259
|
+
loop do
|
|
260
|
+
payload[:attempts] = attempt if payload
|
|
261
|
+
result = yield
|
|
262
|
+
delay = retry_delay(policy, attempt, result, deadline_at)
|
|
263
|
+
if delay.nil?
|
|
264
|
+
log_gave_up(path, attempt, result) if attempt > 1 && !result[0].zero?
|
|
265
|
+
return result
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
log_retry(path, attempt, policy.max_attempts, result, delay)
|
|
269
|
+
sleep(delay)
|
|
270
|
+
attempt += 1
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Seconds to wait before retrying, or nil not to retry. The server
|
|
275
|
+
# can ask for a delay, or for no retry, with grpc-retry-pushback-ms.
|
|
276
|
+
def retry_delay(policy, attempt, result, deadline_at)
|
|
277
|
+
code, _, _, _, trailers, _, committed = result
|
|
278
|
+
return if policy.nil? || code.zero? || committed || attempt >= policy.max_attempts || !policy.retry?(code)
|
|
279
|
+
|
|
280
|
+
pushback = trailers.assoc(RETRY_PUSHBACK)&.last
|
|
281
|
+
delay = if pushback.nil? then policy.backoff(attempt)
|
|
282
|
+
elsif pushback.match?(/\A\d+\z/) then pushback.to_i / 1000.0
|
|
283
|
+
end
|
|
284
|
+
return if delay.nil?
|
|
285
|
+
return if deadline_at && Process.clock_gettime(Process::CLOCK_MONOTONIC) + delay >= deadline_at
|
|
286
|
+
|
|
287
|
+
delay
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def logger
|
|
291
|
+
@logger || Tonic::Rails.configuration.logger
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def log_retry(path, attempt, max_attempts, result, delay)
|
|
295
|
+
logger&.warn(format("tonic-rails: %<path>s failed with %<status>s (attempt %<attempt>d of %<max>d); " \
|
|
296
|
+
"retrying in %<ms>dms", path: path, status: status_text(result), attempt: attempt,
|
|
297
|
+
max: max_attempts, ms: (delay * 1000).round))
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def log_gave_up(path, attempts, result)
|
|
301
|
+
logger&.warn("tonic-rails: #{path} failed with #{status_text(result)} after #{attempts} attempts")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def status_text(result)
|
|
305
|
+
code, message = result
|
|
306
|
+
text = RpcError::CODE_NAMES.fetch(code, code).to_s.upcase
|
|
307
|
+
message.empty? ? text : "#{text}: #{message}"
|
|
308
|
+
end
|
|
309
|
+
|
|
225
310
|
def check_status!(code, message, trailers, details)
|
|
226
311
|
return if code.zero?
|
|
227
312
|
|
|
@@ -282,16 +367,18 @@ module Tonic
|
|
|
282
367
|
input = method.input_type.msgclass
|
|
283
368
|
output = method.output_type.msgclass
|
|
284
369
|
if method.server_streaming
|
|
285
|
-
define_method(name) do |request = nil, deadline: nil, metadata: {}, **fields|
|
|
370
|
+
define_method(name) do |request = nil, deadline: nil, metadata: {}, retries: nil, **fields|
|
|
286
371
|
request = rpc_request(name, request, fields)
|
|
287
|
-
@client.server_streaming(path, input, output, request, deadline: deadline, metadata: metadata
|
|
372
|
+
@client.server_streaming(path, input, output, request, deadline: deadline, metadata: metadata,
|
|
373
|
+
retries: retries)
|
|
288
374
|
end
|
|
289
375
|
else
|
|
290
376
|
# `get_item(id: 1)` works as well as `get_item({ id: 1 })`.
|
|
291
|
-
define_method(name) do |request = nil, deadline: nil, metadata: {}, return_call: false,
|
|
377
|
+
define_method(name) do |request = nil, deadline: nil, metadata: {}, return_call: false, retries: nil,
|
|
378
|
+
**fields|
|
|
292
379
|
request = rpc_request(name, request, fields)
|
|
293
380
|
@client.unary(path, input, output, request, deadline: deadline, metadata: metadata,
|
|
294
|
-
return_call: return_call)
|
|
381
|
+
return_call: return_call, retries: retries)
|
|
295
382
|
end
|
|
296
383
|
end
|
|
297
384
|
end
|
|
@@ -369,3 +456,5 @@ module Tonic
|
|
|
369
456
|
end
|
|
370
457
|
end
|
|
371
458
|
end
|
|
459
|
+
|
|
460
|
+
require "tonic/rails/client/retry_policy"
|
|
@@ -25,8 +25,8 @@ module Tonic
|
|
|
25
25
|
EVENT = "process_rpc.tonic_rails"
|
|
26
26
|
|
|
27
27
|
# Outgoing calls made with Tonic::Rails::Client publish
|
|
28
|
-
# "request_rpc.tonic_rails", with the same payload plus :address,
|
|
29
|
-
#
|
|
28
|
+
# "request_rpc.tonic_rails", with the same payload plus :address, the
|
|
29
|
+
# server called, and :attempts, how many times it was tried.
|
|
30
30
|
REQUEST_EVENT = "request_rpc.tonic_rails"
|
|
31
31
|
|
|
32
32
|
module_function
|
|
@@ -41,16 +41,17 @@ module Tonic
|
|
|
41
41
|
publish(REQUEST_EVENT, path, metadata, address: address, &)
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Yields the payload, or nil when nothing is listening.
|
|
44
45
|
# @api private
|
|
45
46
|
def publish(event, path, metadata, **extra)
|
|
46
47
|
# Without subscribers, skip building the payload: this runs on
|
|
47
48
|
# every RPC.
|
|
48
|
-
return yield unless ActiveSupport::Notifications.notifier.listening?(event)
|
|
49
|
+
return yield(nil) unless ActiveSupport::Notifications.notifier.listening?(event)
|
|
49
50
|
|
|
50
51
|
service, rpc = path.delete_prefix("/").split("/", 2)
|
|
51
52
|
payload = { path: path, service: service, rpc: rpc, metadata: metadata, code: nil, status: nil, **extra }
|
|
52
53
|
ActiveSupport::Notifications.instrument(event, payload) do
|
|
53
|
-
result = yield
|
|
54
|
+
result = yield(payload)
|
|
54
55
|
payload[:code] = 0
|
|
55
56
|
payload[:status] = :ok
|
|
56
57
|
result
|
data/lib/tonic/rails/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: tonic-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: x86_64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Tanous
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/tonic/rails/4.0/tonic_rails_native.so
|
|
69
69
|
- lib/tonic/rails/base.rb
|
|
70
70
|
- lib/tonic/rails/client.rb
|
|
71
|
+
- lib/tonic/rails/client/retry_policy.rb
|
|
71
72
|
- lib/tonic/rails/cluster.rb
|
|
72
73
|
- lib/tonic/rails/configuration.rb
|
|
73
74
|
- lib/tonic/rails/health.rb
|