protobuf-nats 0.13.1.pre3 → 0.13.1.pre4
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/.github/workflows/ci.yml +64 -0
- data/CHANGELOG.md +40 -31
- data/README.md +140 -191
- data/lib/protobuf/nats/client.rb +51 -115
- data/lib/protobuf/nats/config.rb +29 -6
- data/lib/protobuf/nats/errors.rb +10 -2
- data/lib/protobuf/nats/response_muxer.rb +115 -61
- data/lib/protobuf/nats/response_muxer_request.rb +0 -6
- data/lib/protobuf/nats/server.rb +81 -29
- data/lib/protobuf/nats/super_subscription_manager.rb +49 -9
- data/lib/protobuf/nats/thread_pool.rb +20 -8
- data/lib/protobuf/nats/uuidv7_helper.rb +15 -0
- data/lib/protobuf/nats/version.rb +1 -1
- data/lib/protobuf/nats.rb +56 -0
- data/protobuf-nats.gemspec +5 -2
- metadata +12 -19
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58402417b6b024ac4c6f5f69827c02f9e130bd05d67cca6c74ed6f1c73086b36
|
|
4
|
+
data.tar.gz: a7b7d90363cd6ed871de6f28a6359cb1baa485a9926f8cca60fdda758a75e7f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f53ccc62e55dd7a648183e5255559d7b2c5b87f4318218a3a882fa83bede0ac26d51a856b3efadd83e8fea165fbbf2b1ed8938912a9e4efa1bb5448a7e0c0ec2
|
|
7
|
+
data.tar.gz: ac82d95e2e9b047c3edfec4b516d10580b825cb4075068f704ffe3f9039e41aaee050658fa94125b054ad063f9a313490fc327d9250c9ca9c5cfdbb5a314ac01
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
ruby:
|
|
15
|
+
- "3.1"
|
|
16
|
+
- "3.4"
|
|
17
|
+
- "jruby-9.4"
|
|
18
|
+
- "jruby-10.0"
|
|
19
|
+
|
|
20
|
+
# Real NATS server for the integration specs (spec/integration/); they
|
|
21
|
+
# auto-detect it on localhost:4222 (see spec/spec_helper.rb).
|
|
22
|
+
services:
|
|
23
|
+
nats:
|
|
24
|
+
image: nats:2.14-linux
|
|
25
|
+
ports:
|
|
26
|
+
- 4222:4222
|
|
27
|
+
|
|
28
|
+
env:
|
|
29
|
+
JRUBY_OPTS: "-J-Xmx1024m"
|
|
30
|
+
RAILS_ENV: test
|
|
31
|
+
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- uses: ruby/setup-ruby@v1
|
|
36
|
+
with:
|
|
37
|
+
ruby-version: ${{ matrix.ruby }}
|
|
38
|
+
bundler-cache: true
|
|
39
|
+
|
|
40
|
+
# The cluster failover spec (spec/integration/failover_spec.rb) spawns
|
|
41
|
+
# its own two-node cluster, so it needs the nats-server binary itself --
|
|
42
|
+
# the service container above only covers the single-node specs.
|
|
43
|
+
- name: Install nats-server binary (cluster failover spec)
|
|
44
|
+
run: |
|
|
45
|
+
NATS_VERSION=v2.14.3
|
|
46
|
+
curl -sfL "https://github.com/nats-io/nats-server/releases/download/${NATS_VERSION}/nats-server-${NATS_VERSION}-linux-amd64.tar.gz" | tar xz
|
|
47
|
+
sudo mv "nats-server-${NATS_VERSION}-linux-amd64/nats-server" /usr/local/bin/
|
|
48
|
+
nats-server --version
|
|
49
|
+
|
|
50
|
+
- name: Wait for NATS
|
|
51
|
+
run: |
|
|
52
|
+
echo "Waiting for NATS to start..."
|
|
53
|
+
for i in $(seq 1 30); do
|
|
54
|
+
if nc -z localhost 4222; then
|
|
55
|
+
echo "NATS is ready!"
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
sleep 1
|
|
59
|
+
done
|
|
60
|
+
echo "NATS did not become ready" >&2
|
|
61
|
+
exit 1
|
|
62
|
+
|
|
63
|
+
- name: Run tests
|
|
64
|
+
run: bundle exec rspec
|
data/CHANGELOG.md
CHANGED
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### 0.13.1
|
|
4
|
-
|
|
3
|
+
### 0.13.1
|
|
4
|
+
Fixes regressions from the JNats → nats-pure migration (0.13.0) plus a full reliability, performance, and security hardening pass. Highlights: the client reconnects and retries correctly through dropped connections, failing nodes, and terminal closes; the server survives overload and connection loss instead of going silently deaf; TLS actually verifies the server certificate.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
6
|
+
#### Client: reconnect & retry
|
|
7
|
+
- Restored dropped-connection retries (the retry rescue matched an error nothing raised). The client now retries the transport errors nats-pure actually raises: `EOFError`, `IOError`, `Errno::ECONNRESET`/`EPIPE`/`ECONNREFUSED`/`ECONNABORTED`/`ETIMEDOUT`/`EHOSTUNREACH`/`ENETUNREACH`, `NATS::IO::ConnectionClosedError`, and Java `IOException` on JRuby.
|
|
8
|
+
- A terminally closed connection self-heals: `on_close` drops the cached connection, the next request (or in-flight retry) rebuilds it, and the response muxer detects the swap and re-subscribes on the live connection. Previously every RPC timed out until the process restarted.
|
|
9
|
+
- A muxer restart wakes in-flight waiters immediately instead of leaving them to burn the full timeout on responses that can never arrive.
|
|
10
|
+
- Retries are bounded and jittered (`PB_NATS_CLIENT_MAX_RETRIES`, `PB_NATS_CLIENT_RECONNECT_DELAY_SPLAY_LIMIT`), and the final failed attempt raises immediately instead of sleeping first.
|
|
11
|
+
- The muxer token TTL stretches with a response timeout configured beyond 600s, so long waits aren't cleaned up mid-request.
|
|
12
12
|
|
|
13
|
-
####
|
|
14
|
-
-
|
|
15
|
-
-
|
|
13
|
+
#### Server: reliability
|
|
14
|
+
- Subscriptions no longer go permanently deaf under cumulative traffic: the byte-based slow-consumer limit (which nats-pure never decrements on our consumption path) is disabled on both client and server subscriptions; the accurate message-count limit still applies.
|
|
15
|
+
- Tuning `PB_NATS_SERVER_INTAKE_QUEUE_SIZE` down is safe: the slow-consumer limit is kept aligned with the queue capacity, so overload drops promptly instead of blocking nats-pure's read thread (which froze PING/PONG and every subject).
|
|
16
|
+
- A terminally closed connection stops the server (logs + `server.connection_closed`) so a supervisor restarts it, instead of idling forever subscribed to nothing.
|
|
17
|
+
- Failed handlers publish an RPC error response so the client fails fast instead of hanging until its response timeout; a failed success-publish no longer emits a duplicate error response; client-facing error messages are generic (details stay in server logs).
|
|
18
|
+
- Pause/resume no longer leaks subscriptions; the thread-pool counter no longer goes negative at shutdown; dispatch/intake threads park instead of busy-spinning on a closed queue; self-healing always respawns a replacement dispatcher, with thread-safe backoff that decays when healthy.
|
|
19
|
+
- Opt-in stale-request shedding (`PB_NATS_SERVER_STALE_REQUEST_MS`) and opt-in overdue-handler reclaim (`PB_NATS_SERVER_RECLAIM_OVERDUE_HANDLERS`). Handlers are still never aborted by default, and shutdown drains in-flight handlers before closing.
|
|
20
|
+
- Lifecycle callbacks (client and server) register before `connect`, so handshake-window events are observed; a failed handshake closes the half-open client instead of leaking its reader/flusher threads.
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
|
|
22
|
+
#### Performance
|
|
23
|
+
- Server intake fans out across `PB_NATS_SERVER_SUBSCRIPTION_HANDLERS` threads (default `processor_count` on JRuby, 1 on CRuby): ~8.5× intake throughput, head-of-line stalls ~505ms → ~0.4ms (`bench/server_intake_bench.rb`).
|
|
24
|
+
- Muxer dispatch dropped its per-message lock (~2.7× faster on JRuby, `bench/muxer_resilience_bench.rb`) and extracts reply tokens without `split` allocations.
|
|
25
|
+
- `ThreadPool#push` no longer supervises the worker pool per request (the server's 1s `replenish` tick is the sole respawn path), and `ResponseMuxer#start`'s once-per-RPC check is a lock-free atomic read.
|
|
26
|
+
- User error callbacks run on a bounded executor off nats-pure's read thread; drops are counted (`error_callback_drop_count`) and instrumented.
|
|
27
|
+
|
|
28
|
+
#### Failover & configuration
|
|
29
|
+
- New yaml keys `reconnect_time_wait`, `ping_interval`, and `max_outstanding_pings` are forwarded to nats-pure for faster dead-node detection (defaults unchanged); `max_reconnect_attempts: -1` reconnects forever.
|
|
30
|
+
- Numeric env vars parse strictly: malformed values (`"5s"`, `"fast,slow"`) log and fall back to defaults instead of silently becoming `0`; `PB_NATS_SERVER_MAX_QUEUE_SIZE` defaults to the resolved thread count.
|
|
31
|
+
- `connection_options` forwards only nats-pure-recognized keys (the dead JNats-era `:disable_reconnect_buffer` option is gone), and connections are named (`PB_NATS_CONNECTION_NAME` > yaml `connection_name` > hostname) for NATS monitoring.
|
|
32
|
+
- A yaml config that is empty or has no section for the current environment falls back to defaults instead of crashing at boot.
|
|
33
|
+
- New in-flight handler observability: `server.inflight_count`, `server.inflight_oldest_age_ms`, `server.overdue_handler_count`, `server.pending_intake_queue_size`, `server.slow_handler` (opt-in), `server.thread_pool_saturated`; server durations use a monotonic clock.
|
|
34
|
+
|
|
35
|
+
#### Security
|
|
36
|
+
- TLS now verifies the NATS server certificate chain (`VERIFY_PEER`, trusting `tls_ca_cert` or the system store). **Breaking for misconfigured deployments** whose certificates don't chain to the trusted CA — they previously connected unverified.
|
|
37
|
+
- TLS negotiates 1.2–1.3 (replacing the deprecated 1.2 hard pin); OpenSSL builds without TLS 1.3 degrade to a 1.2 ceiling instead of raising.
|
|
38
|
+
- YAML config uses `safe_load` (aliases allowed, arbitrary object deserialization rejected). TLS client keys may be any key type (`OpenSSL::PKey.read`).
|
|
39
|
+
- Known gap: TLS hostname (SAN/CN) verification remains off — it needs per-connection plumbing in nats-pure; tracked separately.
|
|
40
|
+
|
|
41
|
+
#### Testing, CI, dependencies
|
|
42
|
+
- Real-NATS integration specs (auto-detected on `localhost:4222`): full RPC round trip, concurrency with real NACK backpressure, terminal-close self-heal, and a two-node cluster failover spec that spawns its own cluster and kills the node the client is connected to (gated on the `nats-server` binary). GitHub Actions runs the suite on CRuby 3.1/3.4 and JRuby 9.4/10.0.
|
|
43
|
+
- nats-pure pinned to `>= 2.5, < 3`: the gem relies on nats-pure internals (pending-queue swap, slow-consumer semantics, subscription replay, infinite-reconnect flag) verified against 2.5.
|
|
44
|
+
- Removed the unused `connection_pool` dependency and the dead client subscription-pool code; `require "timeout"` is explicit where used.
|
|
45
|
+
- Soak-tested with chaos runs (nats-server killed twice mid-run): 99.4% success on CRuby 3.4, 100% on JRuby 10.0.
|
|
36
46
|
|
|
37
47
|
### 0.13.0
|
|
38
48
|
This is a large overhaul of the client and server internals.
|
|
@@ -55,4 +65,3 @@ This is a large overhaul of the client and server internals.
|
|
|
55
65
|
- Bumped `activesupport` to `>= 6.1` (from `>= 3.2`).
|
|
56
66
|
- Added `concurrent-ruby` (`~> 1.3.6`, pinned so `logger` is included) and `uuid7` runtime dependencies.
|
|
57
67
|
- Pinned `i18n` to `< 1.15.0` in the Gemfile (workaround for ruby-i18n/i18n#735).
|
|
58
|
-
|
data/README.md
CHANGED
|
@@ -16,122 +16,9 @@ Add this line to your application's Gemfile:
|
|
|
16
16
|
gem 'protobuf-nats'
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
And then execute:
|
|
20
|
-
|
|
21
|
-
$ bundle
|
|
22
|
-
|
|
23
|
-
Or install it yourself as:
|
|
24
|
-
|
|
25
|
-
$ gem install protobuf-nats
|
|
26
|
-
|
|
27
|
-
## Configuring
|
|
28
|
-
|
|
29
|
-
### Environment Variables
|
|
30
|
-
|
|
31
|
-
You can also use the following environment variables to tune parameters:
|
|
32
|
-
|
|
33
|
-
`PB_NATS_SERVER_MAX_QUEUE_SIZE` - The size of the queue in front of your thread pool (default: thread count passed to CLI).
|
|
34
|
-
|
|
35
|
-
`PB_NATS_SERVER_PAUSE_FILE_PATH` - If this file exists, the server will pause by unsubscribing all services. When the
|
|
36
|
-
file is removed it will resubscribe and restart slow start (default: `nil`).
|
|
37
|
-
|
|
38
|
-
`PB_NATS_SERVER_SLOW_START_DELAY` - Seconds to wait before adding another round of subscriptions (default 10).
|
|
39
|
-
|
|
40
|
-
`PB_NATS_SERVER_SUBSCRIPTIONS_PER_RPC_ENDPOINT` - Number of subscriptions to create for each rpc endpoint. This number is
|
|
41
|
-
used to allow JVM based servers to warm-up slowly to prevent jolts in runtime performance across your RPC network
|
|
42
|
-
(default: 10). Each subscription joins the NATS queue group for its endpoint, so every request is still delivered to
|
|
43
|
-
exactly one consumer — this knob controls subscription/interest count, not duplicate delivery.
|
|
44
|
-
|
|
45
|
-
`PB_NATS_SERVER_SUBSCRIPTION_HANDLERS` - Number of threads that drain the shared intake queue and publish ACK/NACKs
|
|
46
|
-
(see [How it works](#how-it-works)). Defaults to `Concurrent.processor_count` on JRuby and `1` on CRuby. This is the
|
|
47
|
-
*consumer* parallelism for messages this server has already received; it does not change how many topics are subscribed
|
|
48
|
-
to or the queue-group delivery semantics. Minimum of 1.
|
|
49
|
-
|
|
50
|
-
`PB_NATS_SERVER_SLOW_HANDLER_THRESHOLD_MS` - If set (> 0), emit `server.slow_handler` when a handler runs longer than this
|
|
51
|
-
many milliseconds. Informational/SLA only — handlers are never aborted (default: 0, off).
|
|
52
|
-
|
|
53
|
-
`PB_NATS_SERVER_HANDLER_OVERDUE_MS` - A handler still running past this many milliseconds is reported as "overdue"
|
|
54
|
-
(`server.handler_overdue` + `server.overdue_handler_count`) — i.e. the client has already given up (`response_timeout`)
|
|
55
|
-
so the work is orphaned. Defaults above the client's 60s response timeout so legitimate long operations are not flagged
|
|
56
|
-
(default: 65000). **This should track your clients' `PB_NATS_CLIENT_RESPONSE_TIMEOUT`** — set it to roughly that value (plus
|
|
57
|
-
a small grace). If clients use a longer response timeout, raise this so handlers aren't flagged overdue while a client is
|
|
58
|
-
still waiting; if shorter, lower it so orphaned work is surfaced promptly.
|
|
59
|
-
|
|
60
|
-
`PB_NATS_SERVER_RECLAIM_OVERDUE_HANDLERS` - When `"true"`, actively reclaim the thread-pool slot held by an overdue
|
|
61
|
-
handler (one past `PB_NATS_SERVER_HANDLER_OVERDUE_MS`, whose client has already given up) by raising
|
|
62
|
-
`Errors::HandlerOverdue` into the worker; emits `server.handler_reclaimed`. **Off by default** — the documented contract
|
|
63
|
-
is that handlers are never aborted, since killing a thread mid-handler can corrupt state. Enable only when orphaned work
|
|
64
|
-
is saturating the pool and the server is NACKing healthy traffic (default: false).
|
|
65
|
-
|
|
66
|
-
`PB_NATS_CLIENT_ACK_TIMEOUT` - Seconds to wait for an ACK from the rpc server (default: 5 seconds).
|
|
67
|
-
|
|
68
|
-
`PB_NATS_CLIENT_NACK_BACKOFF_INTERVALS` - Array of milliseconds to wait between NACK retries (default: "0,1,3,5,10").
|
|
69
|
-
|
|
70
|
-
`PB_NATS_CLIENT_NACK_BACKOFF_SPLAY_LIMIT` - Milliseconds to add to the NACK backoff timeout to avoid bursting retries
|
|
71
|
-
(default: 10 milliseconds).
|
|
72
|
-
|
|
73
|
-
`PB_NATS_CLIENT_RESPONSE_TIMEOUT` - Seconds to wait for a non-ACK response from the rpc server (default: 60 seconds).
|
|
74
|
-
|
|
75
|
-
`PB_NATS_CLIENT_RECONNECT_DELAY` - When a request hits a transient transport error (e.g. the NATS connection drops or is reset), the client sleeps this many seconds before retrying to give the connection time to re-establish (default: the ACK timeout). See [Resilience](#resilience).
|
|
76
|
-
|
|
77
|
-
`PB_NATS_CLIENT_RECONNECT_DELAY_SPLAY_LIMIT` - Random jitter (milliseconds, `0..limit`) added to the reconnect delay so a fleet hitting the same NATS outage does not reconnect in lockstep (default: 1000). Set to 0 to disable jitter.
|
|
78
|
-
|
|
79
|
-
`PB_NATS_CLIENT_MAX_RETRIES` - Number of attempts for ack-timeouts and transient transport errors before raising (default: 3).
|
|
80
|
-
|
|
81
|
-
`PB_NATS_CLIENT_SUBSCRIPTION_POOL_SIZE` - If subscription pooling is desired for the request/response cycle then the pool size maximum should be set; the pool is lazy and therefore will only start new subscriptions as necessary (default: 0)
|
|
82
|
-
|
|
83
|
-
`PB_NATS_RESPONSE_MUXER_DISPATCHERS` - Number of dispatcher threads draining the shared response subscription (see [ResponseMuxer](#how-it-works)). Defaults to `Concurrent.processor_count` on JRuby (true parallelism) and `1` on CRuby (the GVL makes extra dispatchers pointless). Minimum of 1.
|
|
84
|
-
|
|
85
|
-
`PB_NATS_CONNECTION_NAME` - A friendly name for the NATS connection, surfaced in NATS server monitoring, error
|
|
86
|
-
reporting, and debugging. Shared by both the client and server connections. Precedence: this env var > the
|
|
87
|
-
`connection_name` yaml key > the hostname (`Socket.gethostname`), so the connection is never nameless (default: hostname).
|
|
88
|
-
|
|
89
|
-
`PROTOBUF_NATS_CONFIG_PATH` - Custom path to the config yaml (default: "config/protobuf_nats.yml").
|
|
90
|
-
|
|
91
|
-
### YAML Config
|
|
92
|
-
|
|
93
|
-
The client and server are configured via environment variables defined in the `nats-pure` gem. However, there are a
|
|
94
|
-
few params which cannot be set: `servers`, `uses_tls`, `subscription_key_replacements`, and `connect_timeout`, so those must be defined in a yml file.
|
|
95
|
-
|
|
96
|
-
The library will automatically look for a file with a relative path of `config/protobuf_nats.yml`, but you may override
|
|
97
|
-
this by specifying a different file via the `PROTOBUF_NATS_CONFIG_PATH` env variable.
|
|
98
|
-
|
|
99
|
-
The `subscription_key_replacements` feature is something we have found useful for local testing, but it is subject to breaking changes.
|
|
100
|
-
|
|
101
|
-
An example config looks like this:
|
|
102
|
-
```
|
|
103
|
-
# Stored at config/protobuf_nats.yml
|
|
104
|
-
---
|
|
105
|
-
production:
|
|
106
|
-
servers:
|
|
107
|
-
- "nats://127.0.0.1:4222"
|
|
108
|
-
- "nats://127.0.0.1:4223"
|
|
109
|
-
- "nats://127.0.0.1:4224"
|
|
110
|
-
max_reconnect_attempts: 500
|
|
111
|
-
connection_name: "my-service"
|
|
112
|
-
uses_tls: true
|
|
113
|
-
tls_client_cert: "/path/to/client-cert.pem"
|
|
114
|
-
tls_client_key: "/path/to/client-key.pem"
|
|
115
|
-
tls_ca_cert: "/path/to/ca.pem"
|
|
116
|
-
connect_timeout: 2
|
|
117
|
-
server_subscription_key_only_subscribe_to_when_includes_any_of:
|
|
118
|
-
- "search"
|
|
119
|
-
- "create"
|
|
120
|
-
server_subscription_key_do_not_subscribe_to_when_includes_any_of:
|
|
121
|
-
- "old_search"
|
|
122
|
-
- "old_create"
|
|
123
|
-
subscription_key_replacements:
|
|
124
|
-
- "original_service": "replacement_service"
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
When `uses_tls` is set, the client negotiates TLS with a floor of 1.2 and a ceiling of 1.3: it uses TLS 1.3 where the NATS server supports it and falls back to 1.2 otherwise (verified on JRuby 9.4 and 10.0).
|
|
128
|
-
|
|
129
|
-
The client **verifies the NATS server's certificate chain** (`verify_mode = VERIFY_PEER`). When `tls_ca_cert` is set, only certificates chaining to that CA are trusted (the private-CA case); otherwise the system trust store is used. **Note:** a server whose certificate does not chain to the configured CA will be rejected — if you are upgrading from a release that did not verify (`< 0.13.1`), make sure `tls_ca_cert` points at the CA that signed your NATS server certificate. Hostname (SAN/CN) verification is not yet enabled (chain verification still ensures the certificate is signed by your trusted CA).
|
|
130
|
-
|
|
131
19
|
## Usage
|
|
132
20
|
|
|
133
|
-
This library is
|
|
134
|
-
`protobuf` use this library, you need to set the following env variable:
|
|
21
|
+
This library is an alternative transport for the `protobuf` gem. Point `protobuf` at it with:
|
|
135
22
|
|
|
136
23
|
```
|
|
137
24
|
PB_SERVER_TYPE="protobuf/nats/runner"
|
|
@@ -140,11 +27,10 @@ PB_CLIENT_TYPE="protobuf/nats/client"
|
|
|
140
27
|
|
|
141
28
|
## Example
|
|
142
29
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Here's a tl;dr example. You might have a protobuf definition and implementation like this:
|
|
30
|
+
For a more detailed example, see the `warehouse` app in the `examples` directory.
|
|
146
31
|
|
|
147
32
|
```ruby
|
|
33
|
+
# app.rb
|
|
148
34
|
require "protobuf/nats"
|
|
149
35
|
|
|
150
36
|
class User < ::Protobuf::Message
|
|
@@ -161,103 +47,165 @@ class UserService < ::Protobuf::Rpc::Service
|
|
|
161
47
|
end
|
|
162
48
|
```
|
|
163
49
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
We can now start an rpc server using the protobuf-nats runner and client:
|
|
50
|
+
Start a server, then call it from a client:
|
|
167
51
|
|
|
168
52
|
```
|
|
169
53
|
$ export PB_SERVER_TYPE="protobuf/nats/runner"
|
|
170
54
|
$ export PB_CLIENT_TYPE="protobuf/nats/client"
|
|
171
55
|
$ bundle exec rpc_server start ./app.rb
|
|
172
|
-
...
|
|
173
|
-
I, [2017-03-24T12:16:02.539930 #12512] INFO -- : Creating subscriptions:
|
|
174
|
-
I, [2017-03-24T12:16:02.543927 #12512] INFO -- : - rpc.user_service.create
|
|
175
|
-
...
|
|
176
|
-
```
|
|
177
56
|
|
|
178
|
-
And we can start a client and begin communicating:
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
$ export PB_SERVER_TYPE="protobuf/nats/runner"
|
|
182
|
-
$ export PB_CLIENT_TYPE="protobuf/nats/client"
|
|
183
57
|
$ bundle exec irb -r ./app
|
|
184
|
-
irb
|
|
58
|
+
irb> UserService.client.create(User.new(:username => "testing 123"))
|
|
185
59
|
=> #<User id=123 username="testing 123">
|
|
186
60
|
```
|
|
187
61
|
|
|
188
|
-
|
|
62
|
+
An rpc without a matching instance method (e.g. an unimplemented `search`) is simply not subscribed to.
|
|
189
63
|
|
|
190
|
-
|
|
191
|
-
|
|
64
|
+
## Configuring
|
|
65
|
+
|
|
66
|
+
### Environment variables
|
|
67
|
+
|
|
68
|
+
Numeric variables are parsed strictly: a malformed value (e.g. `PB_NATS_CLIENT_ACK_TIMEOUT=5s`) is logged and the
|
|
69
|
+
default is used, instead of silently becoming `0`.
|
|
70
|
+
|
|
71
|
+
#### Client
|
|
72
|
+
|
|
73
|
+
| Variable | Default | Description |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `PB_NATS_CLIENT_ACK_TIMEOUT` | `5` | Seconds to wait for the server's ACK. |
|
|
76
|
+
| `PB_NATS_CLIENT_RESPONSE_TIMEOUT` | `60` | Seconds to wait for the RPC response. |
|
|
77
|
+
| `PB_NATS_CLIENT_MAX_RETRIES` | `3` | Attempts for ACK timeouts and transient transport errors. Retries re-send the request — see [Delivery semantics](#delivery-semantics-at-least-once). |
|
|
78
|
+
| `PB_NATS_CLIENT_NACK_BACKOFF_INTERVALS` | `0,1,3,5,10` | Milliseconds to wait between NACK retries (one attempt per interval). |
|
|
79
|
+
| `PB_NATS_CLIENT_NACK_BACKOFF_SPLAY_LIMIT` | `10` | Random jitter (ms) added to each NACK backoff. |
|
|
80
|
+
| `PB_NATS_CLIENT_RECONNECT_DELAY` | ACK timeout | Seconds to sleep before retrying after a transient transport error — see [Resilience](#resilience). |
|
|
81
|
+
| `PB_NATS_CLIENT_RECONNECT_DELAY_SPLAY_LIMIT` | `1000` | Random jitter (ms, `0..limit`) added to the reconnect delay so a fleet doesn't retry in lockstep. `0` disables. |
|
|
82
|
+
| `PB_NATS_RESPONSE_MUXER_DISPATCHERS` | CPUs on JRuby, `1` on CRuby | Threads draining the shared response subscription (min 1). |
|
|
83
|
+
|
|
84
|
+
#### Server
|
|
85
|
+
|
|
86
|
+
| Variable | Default | Description |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `PB_NATS_SERVER_MAX_QUEUE_SIZE` | thread count | Queue in front of the handler thread pool; requests beyond it are NACKed. |
|
|
89
|
+
| `PB_NATS_SERVER_SUBSCRIPTION_HANDLERS` | CPUs on JRuby, `1` on CRuby | Threads draining the shared intake queue and publishing ACK/NACKs (min 1). Consumer parallelism only — does not change queue-group delivery. |
|
|
90
|
+
| `PB_NATS_SERVER_INTAKE_QUEUE_SIZE` | `65536` | Capacity of the shared intake queue. Smaller turns overload into prompt drops-and-retries instead of a deep stale backlog; tune down alongside `PB_NATS_SERVER_STALE_REQUEST_MS`. |
|
|
91
|
+
| `PB_NATS_SERVER_SUBSCRIPTIONS_PER_RPC_ENDPOINT` | `10` | Subscriptions created per endpoint (lets JVM servers warm up gradually). Queue groups still deliver each request to exactly one consumer. |
|
|
92
|
+
| `PB_NATS_SERVER_SLOW_START_DELAY` | `10` | Seconds between slow-start subscription rounds. |
|
|
93
|
+
| `PB_NATS_SERVER_PAUSE_FILE_PATH` | `nil` | While this file exists the server unsubscribes from all services; it resubscribes (with slow start) when the file is removed. |
|
|
94
|
+
| `PB_NATS_SERVER_SLOW_HANDLER_THRESHOLD_MS` | `0` (off) | Emit `server.slow_handler` when a handler runs longer than this. Informational only. |
|
|
95
|
+
| `PB_NATS_SERVER_HANDLER_OVERDUE_MS` | `65000` | Age at which a still-running handler is reported overdue (its client has already given up). Keep ≈ your clients' response timeout plus a small grace. |
|
|
96
|
+
| `PB_NATS_SERVER_RECLAIM_OVERDUE_HANDLERS` | `false` | `"true"` reclaims an overdue handler's pool slot by aborting it — see note below. |
|
|
97
|
+
| `PB_NATS_SERVER_STALE_REQUEST_MS` | `0` (off) | Drop requests older than this at intake (the client has already retried or given up) — see note below. |
|
|
98
|
+
| `PB_NATS_SERVER_SHUTDOWN_DRAIN_TIMEOUT` | overdue ms / 1000 + 5 | Seconds to let in-flight handlers finish during shutdown before abandoning them. |
|
|
99
|
+
|
|
100
|
+
#### Shared
|
|
101
|
+
|
|
102
|
+
| Variable | Default | Description |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `PB_NATS_CONNECTION_NAME` | hostname | Connection name shown in NATS server monitoring. Precedence: env var > yaml `connection_name` > hostname. |
|
|
105
|
+
| `PROTOBUF_NATS_CONFIG_PATH` | `config/protobuf_nats.yml` | Path to the yaml config. |
|
|
106
|
+
|
|
107
|
+
**Overdue handlers are never aborted by default** — killing a thread mid-handler can corrupt state. Enable
|
|
108
|
+
`PB_NATS_SERVER_RECLAIM_OVERDUE_HANDLERS` only when orphaned work (whose clients already gave up) is saturating the
|
|
109
|
+
pool and healthy traffic is being NACKed.
|
|
110
|
+
|
|
111
|
+
**Stale-request shedding trusts client clocks.** The request age comes from the UUIDv7 reply token, which encodes the
|
|
112
|
+
*client's* wall-clock time — enable only with sane NTP across hosts, and keep the threshold comfortably above the
|
|
113
|
+
client ACK timeout. Requests without a UUIDv7 token (foreign clients) are never shed.
|
|
114
|
+
|
|
115
|
+
### YAML config
|
|
116
|
+
|
|
117
|
+
Connection-level settings live in a yaml file, keyed by environment (`RAILS_ENV` / `RACK_ENV` / `APP_ENV`, default
|
|
118
|
+
`development`). The default path is `config/protobuf_nats.yml`; override with `PROTOBUF_NATS_CONFIG_PATH`.
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
# config/protobuf_nats.yml
|
|
122
|
+
---
|
|
123
|
+
production:
|
|
124
|
+
servers:
|
|
125
|
+
- "nats://127.0.0.1:4222"
|
|
126
|
+
- "nats://127.0.0.1:4223"
|
|
127
|
+
- "nats://127.0.0.1:4224"
|
|
128
|
+
max_reconnect_attempts: 500 # -1 reconnects forever
|
|
129
|
+
reconnect_time_wait: 2 # seconds between reconnect attempts (nats-pure default: 2)
|
|
130
|
+
ping_interval: 120 # seconds between health-check PINGs (nats-pure default: 120)
|
|
131
|
+
max_outstanding_pings: 2 # missed PINGs before the connection is declared dead (nats-pure default: 2)
|
|
132
|
+
connection_name: "my-service"
|
|
133
|
+
connect_timeout: 2
|
|
134
|
+
uses_tls: true
|
|
135
|
+
tls_client_cert: "/path/to/client-cert.pem"
|
|
136
|
+
tls_client_key: "/path/to/client-key.pem"
|
|
137
|
+
tls_ca_cert: "/path/to/ca.pem"
|
|
138
|
+
server_subscription_key_only_subscribe_to_when_includes_any_of:
|
|
139
|
+
- "search"
|
|
140
|
+
- "create"
|
|
141
|
+
server_subscription_key_do_not_subscribe_to_when_includes_any_of:
|
|
142
|
+
- "old_search"
|
|
143
|
+
- "old_create"
|
|
144
|
+
subscription_key_replacements:
|
|
145
|
+
- "original_service": "replacement_service"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`subscription_key_replacements` is useful for local testing but subject to breaking changes.
|
|
149
|
+
|
|
150
|
+
### TLS
|
|
151
|
+
|
|
152
|
+
With `uses_tls`, the client negotiates TLS 1.2–1.3 and **verifies the NATS server's certificate chain**
|
|
153
|
+
(`VERIFY_PEER`): certificates must chain to `tls_ca_cert` when set, or to the system trust store otherwise. If you are
|
|
154
|
+
upgrading from a release that did not verify (`< 0.13.1`), make sure `tls_ca_cert` points at the CA that signed your
|
|
155
|
+
NATS server certificate, or the connection will be rejected. Hostname (SAN/CN) verification is not yet enabled.
|
|
192
156
|
|
|
193
157
|
## How it works
|
|
194
158
|
|
|
195
|
-
`protobuf-nats` uses
|
|
196
|
-
|
|
197
|
-
- **ResponseMuxer** (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
handler threads, so a slow ACK publish on one message can't head-of-line block every other subject. Handler threads
|
|
208
|
-
self-heal with exponential backoff.
|
|
209
|
-
- **Server observability** — beyond the thread-pool gauges, the server emits in-flight handler metrics
|
|
210
|
-
(`server.inflight_count`, `server.inflight_oldest_age_ms`, `server.overdue_handler_count`, `server.handler_overdue`,
|
|
211
|
-
`server.pending_intake_queue_size`, `server.thread_pool_saturated`). Long-running handlers are allowed and never aborted
|
|
212
|
-
by default; a handler is only "overdue" once it outlives the client's `response_timeout` (see
|
|
213
|
-
`PB_NATS_SERVER_HANDLER_OVERDUE_MS`). Overdue handlers can optionally be reclaimed (emitting `server.handler_reclaimed`)
|
|
214
|
-
via `PB_NATS_SERVER_RECLAIM_OVERDUE_HANDLERS`.
|
|
215
|
-
- **Error-callback observability** — `on_error` callbacks run on a bounded background executor so a slow callback can't
|
|
216
|
-
stall message processing. If that executor saturates under an error flood, dropped callbacks are counted
|
|
217
|
-
(`Protobuf::Nats.error_callback_drop_count`) and emit `error_callback_dropped` rather than being lost silently.
|
|
159
|
+
`protobuf-nats` uses `nats-pure`'s `NATS::IO::Client` on both CRuby and JRuby.
|
|
160
|
+
|
|
161
|
+
- **ResponseMuxer** (client) — one wildcard subscription multiplexes all RPC responses instead of subscribing per
|
|
162
|
+
request. Dispatcher threads route each reply to its waiting caller by UUIDv7 token, lock-free on the hot path, and
|
|
163
|
+
self-heal with decaying exponential backoff if they crash.
|
|
164
|
+
- **SuperSubscriptionManager** (server) — manages the endpoint subscriptions (NATS queue groups: one consumer per
|
|
165
|
+
request) including slow start and pause/resume. All subscriptions feed one shared intake queue drained by handler
|
|
166
|
+
threads, so a slow ACK publish can't head-of-line block other subjects. Handlers self-heal like the muxer.
|
|
167
|
+
- **Observability** — thread-pool gauges plus in-flight handler metrics (`server.inflight_count`,
|
|
168
|
+
`server.inflight_oldest_age_ms`, `server.overdue_handler_count`, `server.pending_intake_queue_size`,
|
|
169
|
+
`server.thread_pool_saturated`). Error callbacks run on a bounded background executor; drops are counted
|
|
170
|
+
(`Protobuf::Nats.error_callback_drop_count`) and emit `error_callback_dropped`.
|
|
218
171
|
|
|
219
172
|
## Resilience
|
|
220
173
|
|
|
221
|
-
The client
|
|
222
|
-
|
|
223
|
-
- **Transient transport errors are retried
|
|
224
|
-
`
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
`
|
|
229
|
-
- **
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
- **The
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
subscription-restart window won't busy-spin the dispatch loop.
|
|
237
|
-
|
|
238
|
-
See `bench/muxer_resilience_bench.rb` for microbenchmarks of the dispatch hot path and these resilience paths.
|
|
174
|
+
The client rides out transient NATS hiccups rather than surfacing them as request failures:
|
|
175
|
+
|
|
176
|
+
- **Transient transport errors are retried** (`Errors::RETRYABLE_TRANSPORT_ERRORS`): the client sleeps
|
|
177
|
+
`PB_NATS_CLIENT_RECONNECT_DELAY` (plus jitter) and retries up to `PB_NATS_CLIENT_MAX_RETRIES`, rebuilding a
|
|
178
|
+
terminally closed connection — and moving the muxer's subscription onto it — before each retry.
|
|
179
|
+
- **A failing NATS node fails over automatically.** `nats-pure` reconnects through the whole `servers` pool and replays
|
|
180
|
+
all subscriptions on the new node. A node that dies *silently* (no FIN/RST) is only detected by the PING health
|
|
181
|
+
check — up to `ping_interval * max_outstanding_pings` (240s at defaults); lower those yaml keys for faster failover.
|
|
182
|
+
- **Missing ACKs and NACKs are retried** with their own timeouts/backoff. Every retry re-sends the request — see
|
|
183
|
+
[Delivery semantics](#delivery-semantics-at-least-once).
|
|
184
|
+
- **Server-side failures fail the caller fast.** A failed handler publishes a generic RPC error response (details stay
|
|
185
|
+
in server logs) so the client raises immediately instead of waiting out its response timeout.
|
|
186
|
+
- **The server exits rather than running deaf.** If its connection is terminally closed (reconnects exhausted), the run
|
|
187
|
+
loop stops — emitting `server.connection_closed` — so a supervisor restarts the process. Use
|
|
188
|
+
`max_reconnect_attempts: -1` to prefer in-process reconnects forever.
|
|
239
189
|
|
|
240
190
|
## Delivery semantics (at-least-once)
|
|
241
191
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
192
|
+
RPC delivery is **at-least-once** and the gem does **not** deduplicate: a client retry can re-send a request the server
|
|
193
|
+
already processed, so a single call can run a handler more than once. This is deliberate — dropping work on a transient
|
|
194
|
+
blip is usually worse than occasionally repeating it.
|
|
245
195
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
-
|
|
196
|
+
Making that safe is the service author's responsibility: key writes on a natural id or idempotency token
|
|
197
|
+
(upsert / `find_or_create`), and make external side effects (charges, emails, downstream RPCs) safe to repeat or guard
|
|
198
|
+
them with your own dedup. An opt-in per-RPC dedup with a pluggable store may be added later; it will not be the default.
|
|
249
199
|
|
|
250
|
-
|
|
200
|
+
## Future improvements
|
|
251
201
|
|
|
252
|
-
## Future Improvements (locked behind ruby version)
|
|
253
202
|
- Migrate from the `uuid7` gem to native `Random#uuid_v7` once the minimum Ruby version supports it (see `UUIDv7Helper`).
|
|
254
203
|
|
|
255
204
|
## Benchmarks
|
|
256
205
|
|
|
257
|
-
Microbenchmarks live in `bench/` and
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
- `bench/server_intake_bench.rb` — server intake fan-out (~8× throughput, head-of-line stall ~505ms → ~2ms) and the handler-exhaustion observability.
|
|
206
|
+
Microbenchmarks live in `bench/` and need no NATS server; see `bench/bench.md`. Highlights on JRuby: muxer dispatch
|
|
207
|
+
~2.5× faster with the per-message lock removed (`bench/muxer_resilience_bench.rb`), server intake fan-out ~8×
|
|
208
|
+
throughput with head-of-line stalls ~505ms → ~2ms (`bench/server_intake_bench.rb`).
|
|
261
209
|
|
|
262
210
|
```
|
|
263
211
|
bundle exec ruby -Ilib bench/server_intake_bench.rb
|
|
@@ -266,16 +214,17 @@ bundle exec ruby -Ilib bench/muxer_resilience_bench.rb
|
|
|
266
214
|
|
|
267
215
|
## Development
|
|
268
216
|
|
|
269
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
|
|
270
|
-
|
|
271
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
217
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can
|
|
218
|
+
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
272
219
|
|
|
220
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
|
|
221
|
+
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
|
|
222
|
+
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
273
223
|
|
|
274
224
|
## Contributing
|
|
275
225
|
|
|
276
226
|
Bug reports and pull requests are welcome on GitHub at https://github.com/mxenabled/protobuf-nats.
|
|
277
227
|
|
|
278
|
-
|
|
279
228
|
## License
|
|
280
229
|
|
|
281
230
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|