leopard 0.1.6 → 0.1.7
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/.release-please-manifest.json +1 -1
- data/.version.txt +1 -1
- data/CHANGELOG.md +8 -0
- data/doc/service-api-vs-rest.adoc +149 -0
- data/examples/echo_endpoint.rb +2 -1
- data/lib/leopard/message_wrapper.rb +2 -3
- data/lib/leopard/nats_api_server.rb +34 -13
- data/lib/leopard/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 343e274b8fffbe093d59b1d819a4b0be0ba9efe109c4883c535ba3692f1854a3
|
4
|
+
data.tar.gz: 6b32780cf1bd40d7b0191a79a533382dbc6ed828162d9c6ebeefa2c838191c77
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ee58b9e7a03d9dee24f3d338e700c0d0456ff0056379e06405418365023a14dc7484bad768d7deff2727cb7e01f0daa331234bfa6b0875b5891f79577a1880e
|
7
|
+
data.tar.gz: '047850ac66d9a789bdaad311ef764bed2bea365776a67d6f47f9634eb039edc303ab48d6227ad443f4f2a49b5fba1f1cd63c5d45068120b544d9a1e8a65c292d'
|
data/.version.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.7
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.1.7](https://github.com/rubyists/leopard/compare/v0.1.6...v0.1.7) (2025-08-06)
|
4
|
+
|
5
|
+
|
6
|
+
### Bug Fixes
|
7
|
+
|
8
|
+
* Allow non blocking all the way down to the instance level ([#27](https://github.com/rubyists/leopard/issues/27)) ([01748a5](https://github.com/rubyists/leopard/commit/01748a56bc927ee1dbc70d2351fd12037e5b4bef))
|
9
|
+
* Stop sending code argument to respond_with_error, it does not accept it ([#23](https://github.com/rubyists/leopard/issues/23)) ([9d87b8c](https://github.com/rubyists/leopard/commit/9d87b8c308a1fdff72769863711bb6bb942b3677))
|
10
|
+
|
3
11
|
## [0.1.6](https://github.com/rubyists/leopard/compare/v0.1.5...v0.1.6) (2025-08-03)
|
4
12
|
|
5
13
|
|
@@ -0,0 +1,149 @@
|
|
1
|
+
= NATS ServiceApi (via Leopard) vs REST
|
2
|
+
:revdate: 2025-08-03
|
3
|
+
:doctype: whitepaper
|
4
|
+
|
5
|
+
== Abstract
|
6
|
+
Leopard’s NATS ServiceApi wrapper delivers full-featured service-to-service communication, comparable with REST/GRPC.
|
7
|
+
With NATS, you get discovery, health checks, and observability, all at the protocol layer.
|
8
|
+
|
9
|
+
All Leopard does is add an Easy Button to serve these endpoints with concurrency to scale NATS connections
|
10
|
+
horizontally, and to allow for per-endpoint (or groups of endpoints) scaling.
|
11
|
+
|
12
|
+
This abstraction preserves the simplicity of REST requests with the resilience of asynchronous messaging
|
13
|
+
under the hood.
|
14
|
+
|
15
|
+
This paper contrasts the NATS ServiceApi model with traditional REST, outlines its key benefits and trade-offs,
|
16
|
+
and illustrates how Leopard can specifically streamline microservice (and nanoservice) architectures.
|
17
|
+
|
18
|
+
== 1. Introduction
|
19
|
+
Microservices communicate most often via HTTP/REST today, but REST comes with overhead: service registries,
|
20
|
+
load-balancers, schema/version endpoints, and brittle synchronous request patterns.
|
21
|
+
Leopard leverages NATS’s ServiceApi layer to deliver:
|
22
|
+
|
23
|
+
* **Automatic discovery** via `$SRV.PING/INFO/STATS` subjects
|
24
|
+
* **Dynamic load-balanced routing** with queue groups
|
25
|
+
* **Built-in telemetry** (per-endpoint request counts, latencies, errors)
|
26
|
+
* **Scalable workers** (Concurrent::FixedThreadPool with Leopard, currently)
|
27
|
+
* **Versioned endpoints** multiple versions can be registered concurrently, no need for /v1, /v2, etc.
|
28
|
+
|
29
|
+
== 2. Architecture Overview
|
30
|
+
Leopard embeds a “mini web-framework” in Ruby, registering each endpoint on a NATS subject and grouping instances for load-sharing:
|
31
|
+
|
32
|
+
[source,mermaid]
|
33
|
+
----
|
34
|
+
graph LR
|
35
|
+
subgraph ServiceInstance
|
36
|
+
A[NatsApiServer.run] --> B[Registers endpoints]
|
37
|
+
end
|
38
|
+
subgraph NATS_Cluster
|
39
|
+
B --> C["$SRV.INFO.calc"]
|
40
|
+
B --> D["calc.add queue group"]
|
41
|
+
end
|
42
|
+
E[Client] -->|request calc.add| D
|
43
|
+
D --> F[Worker Thread pool]
|
44
|
+
F --> G[Handler & Dry Monads]
|
45
|
+
G -->|respond or error| E
|
46
|
+
----
|
47
|
+
|
48
|
+
== 3. Key Benefits
|
49
|
+
|
50
|
+
=== 3.1 Automatic Discovery & Monitoring
|
51
|
+
Leopard services auto-advertise on well-known NATS subjects. Clients can query:
|
52
|
+
|
53
|
+
* `$SRV.PING.<name>` – discover live instances & measure RTT
|
54
|
+
* `$SRV.INFO.<name>` – retrieve endpoint schemas & metadata
|
55
|
+
* `$SRV.STATS.<name>` – fetch per-endpoint metrics
|
56
|
+
|
57
|
+
No external service-registry (Consul, etcd) or custom HTTP health paths required.
|
58
|
+
|
59
|
+
=== 3.2 Scaling-Per-Endpoint-Group
|
60
|
+
Each endpoint (mapped to a NATS subject) is registered with an optional queue group.
|
61
|
+
This allows it to enjoy native NATS queue-group load balancing. You can:
|
62
|
+
|
63
|
+
* Scale thread-pooled workers independently per service
|
64
|
+
* Horizontally add new service instances without redeploying clients
|
65
|
+
* Isolate hot-paths (e.g. “reports.generate”) onto dedicated worker farms
|
66
|
+
|
67
|
+
=== 3.3 Observability & Telemetry
|
68
|
+
Leopard exposes stats out-of-the-box:
|
69
|
+
|
70
|
+
* Request counts, error counts, processing time
|
71
|
+
* Custom `on_stats` hooks for business metrics
|
72
|
+
* Integration with Prometheus or any NATS-capable dashboard
|
73
|
+
|
74
|
+
=== 3.4 Asynchronous, Resilient Communication
|
75
|
+
Unlike blocking HTTP calls, Leopard’s NATS requests can:
|
76
|
+
|
77
|
+
* Employ timeouts, retries, and dead-letter queues
|
78
|
+
* Fit into event-driven pipelines, decoupling producers and consumers
|
79
|
+
* Maintain throughput under partial outages
|
80
|
+
|
81
|
+
== 4. Comparison with REST
|
82
|
+
[cols="1,1,1", options="header"]
|
83
|
+
|===
|
84
|
+
| Feature | REST (HTTP) | NATS ServiceApi
|
85
|
+
|
86
|
+
| Discovery
|
87
|
+
| Requires external service registry or API gateway, A.K.A. "it's always DNS"
|
88
|
+
| Built-in via `$SRV.PING`, `$SRV.INFO`, `$SRV.STATS` Works uniformly across all languages
|
89
|
+
|
90
|
+
| Load Balancing
|
91
|
+
| HTTP load balancer or DNS round-robin
|
92
|
+
| Native queue-group load balancing per subject
|
93
|
+
|
94
|
+
| Telemetry
|
95
|
+
| Custom instrumentation (e.g., `/metrics` endpoint)
|
96
|
+
| Auto-collected stats (`service.stats`) and `on_stats` hooks
|
97
|
+
|
98
|
+
| Latency & Overhead
|
99
|
+
| Higher (HTTP/TCP handshake, headers, JSON)
|
100
|
+
| Low-latency binary protocol with optional JSON payloads (other formats supported with plugins)
|
101
|
+
|
102
|
+
| Communication Model
|
103
|
+
| Synchronous, blocking request/response
|
104
|
+
| Asynchronous request/reply, decoupled via subjects
|
105
|
+
|
106
|
+
| Schema & Validation
|
107
|
+
| OpenAPI/Swagger externally managed
|
108
|
+
| Optional metadata on endpoints + pluggable middleware
|
109
|
+
|
110
|
+
| Error Handling
|
111
|
+
| HTTP status codes and response bodies
|
112
|
+
| Standardized error headers (`Nats-Service-Error`, `Nats-Service-Error-Code`)
|
113
|
+
|
114
|
+
| Multi-Language Support
|
115
|
+
| Varies by framework; patterns differ per language
|
116
|
+
| Uniform ServiceApi semantics with native clients in all major languages
|
117
|
+
|
118
|
+
| Scalability
|
119
|
+
| Scale replicas behind LB
|
120
|
+
| Scale thread pools vertically + horizontal instances independently, by endpoint groups (or even a single endpoint)
|
121
|
+
|===
|
122
|
+
|
123
|
+
== 5. Trade-Offs & Considerations
|
124
|
+
. **Dependency on NATS**
|
125
|
+
Leopard requires a healthy NATS cluster; network partition or broker outage impacts all services. (This is not unlike Redis or Postgres dependencies)
|
126
|
+
. **Learning Curve**
|
127
|
+
Teams must understand NATS subjects, queue groups, and ServiceApi conventions. (Easier with helpers like Leopard’s `NatsApiServer`.)
|
128
|
+
. **Language Support**
|
129
|
+
While Leopard is Ruby-centric, NATS ServiceApi is cross-language. All teams must adopt compatible clients. (And handle concurrency and error handling in their own way.)
|
130
|
+
. **Subject Naming**
|
131
|
+
Adopting a consistent naming convention for subjects is crucial. This can be a challenge in large teams.
|
132
|
+
NATS can support a massive number of subjects. But to avoid confusion, subjects should have
|
133
|
+
clear, descriptive names that reflect the service and endpoint purpose.
|
134
|
+
Even though NATS service API exposes auto-discovery, there could (should?) be a central authoritative
|
135
|
+
document that defines the subject structure and naming conventions. This can avoid the TOCTOE issue.
|
136
|
+
(You check for a subject, it's not used, but then someone implements it in the meantime.)
|
137
|
+
That leads to: there should be a "registry" of subjects, that can be queried by developers
|
138
|
+
to discover available subjects. This can avoid confusion and ensure that all developers are on the same
|
139
|
+
page and not conflicting with one another.
|
140
|
+
|
141
|
+
== 6. What, then?
|
142
|
+
Leopard’s NATS ServiceApi framework offers a powerful alternative to REST:
|
143
|
+
zero-config discovery, per-endpoint scaling, rich observability, and asynchronous resilience.
|
144
|
+
|
145
|
+
For high-throughput, low-latency microservice (nano-service?) ecosystems, Leopard can simplify infrastructure,
|
146
|
+
reduce boilerplate, and improve operational visibility.
|
147
|
+
|
148
|
+
Leopard's aim is to retain the expressiveness and composability of idiomatic Ruby, while leveraging
|
149
|
+
NATS's ServiceApi performance and flexibility.
|
data/examples/echo_endpoint.rb
CHANGED
@@ -12,6 +12,7 @@ class EchoService
|
|
12
12
|
end
|
13
13
|
|
14
14
|
endpoint(:echo) { |msg| Success(msg.data) }
|
15
|
+
endpoint(:echo_fail) { |msg| Failure({ failure: '*boom*', data: msg.data }) }
|
15
16
|
end
|
16
17
|
|
17
18
|
if __FILE__ == $PROGRAM_NAME
|
@@ -22,6 +23,6 @@ if __FILE__ == $PROGRAM_NAME
|
|
22
23
|
version: '1.0.0',
|
23
24
|
instance_args: [2],
|
24
25
|
},
|
25
|
-
instances:
|
26
|
+
instances: 1,
|
26
27
|
)
|
27
28
|
end
|
@@ -30,11 +30,10 @@ module Rubyists
|
|
30
30
|
end
|
31
31
|
|
32
32
|
# @param err [String, Exception] The error message or exception to respond with.
|
33
|
-
# @param code [Integer] The HTTP status code to use for the error response.
|
34
33
|
#
|
35
34
|
# @return [void]
|
36
|
-
def respond_with_error(err
|
37
|
-
raw.respond_with_error(err.to_s
|
35
|
+
def respond_with_error(err)
|
36
|
+
raw.respond_with_error(err.to_s)
|
38
37
|
end
|
39
38
|
|
40
39
|
private
|
@@ -75,7 +75,7 @@ module Rubyists
|
|
75
75
|
def run(nats_url:, service_opts:, instances: 1, blocking: true)
|
76
76
|
logger.info 'Booting NATS API server...'
|
77
77
|
workers = Concurrent::Array.new
|
78
|
-
pool = spawn_instances(nats_url, service_opts, instances, workers)
|
78
|
+
pool = spawn_instances(nats_url, service_opts, instances, workers, blocking)
|
79
79
|
logger.info 'Setting up signal trap...'
|
80
80
|
trap_signals(workers, pool)
|
81
81
|
return pool unless blocking
|
@@ -90,16 +90,16 @@ module Rubyists
|
|
90
90
|
# @param url [String] The URL of the NATS server.
|
91
91
|
# @param opts [Hash] Options for the NATS service.
|
92
92
|
# @param count [Integer] The number of instances to spawn.
|
93
|
+
# @param workers [Array] The array to store worker instances.
|
94
|
+
# @param blocking [Boolean] If false, does not block current thread after starting the server.
|
93
95
|
#
|
94
96
|
# @return [Concurrent::FixedThreadPool] The thread pool managing the worker threads.
|
95
|
-
def spawn_instances(url, opts, count, workers)
|
97
|
+
def spawn_instances(url, opts, count, workers, blocking)
|
96
98
|
pool = Concurrent::FixedThreadPool.new(count)
|
97
99
|
@instance_args = opts.delete(:instance_args) || nil
|
98
100
|
logger.info "Building #{count} workers with options: #{opts.inspect}, instance_args: #{@instance_args}"
|
99
101
|
count.times do
|
100
|
-
|
101
|
-
gps = groups.dup
|
102
|
-
pool.post { build_worker(url, opts, eps, gps, workers) }
|
102
|
+
pool.post { build_worker(url, opts, workers, blocking) }
|
103
103
|
end
|
104
104
|
pool
|
105
105
|
end
|
@@ -108,15 +108,16 @@ module Rubyists
|
|
108
108
|
#
|
109
109
|
# @param url [String] The URL of the NATS server.
|
110
110
|
# @param opts [Hash] Options for the NATS service.
|
111
|
-
# @param eps [Array<Hash>] The list of endpoints to add.
|
112
|
-
# @param gps [Hash] The groups to add.
|
113
111
|
# @param workers [Array] The array to store worker instances.
|
112
|
+
# @param blocking [Boolean] If true, blocks the current thread until the worker is set up.
|
114
113
|
#
|
115
114
|
# @return [void]
|
116
|
-
def build_worker(url, opts,
|
115
|
+
def build_worker(url, opts, workers, blocking)
|
117
116
|
worker = @instance_args ? new(*@instance_args) : new
|
118
117
|
workers << worker
|
119
|
-
worker.setup_worker(url, opts
|
118
|
+
return worker.setup_worker!(nats_url: url, service_opts: opts) if blocking
|
119
|
+
|
120
|
+
worker.setup_worker(nats_url: url, service_opts: opts)
|
120
121
|
end
|
121
122
|
|
122
123
|
# Shuts down the NATS API server gracefully.
|
@@ -174,7 +175,6 @@ module Rubyists
|
|
174
175
|
|
175
176
|
# Sets up a worker thread for the NATS API server.
|
176
177
|
# This method connects to the NATS server, adds the service, groups, and endpoints,
|
177
|
-
# and keeps the worker thread alive.
|
178
178
|
#
|
179
179
|
# @param url [String] The URL of the NATS server.
|
180
180
|
# @param opts [Hash] Options for the NATS service.
|
@@ -182,12 +182,21 @@ module Rubyists
|
|
182
182
|
# @param gps [Hash] The groups to add.
|
183
183
|
#
|
184
184
|
# @return [void]
|
185
|
-
def setup_worker(
|
185
|
+
def setup_worker(nats_url: 'nats://localhost:4222', service_opts: {})
|
186
186
|
@thread = Thread.current
|
187
|
-
@client = NATS.connect
|
188
|
-
@service = @client.services.add(
|
187
|
+
@client = NATS.connect nats_url
|
188
|
+
@service = @client.services.add(build_service_opts(service_opts:))
|
189
|
+
gps = self.class.groups.dup
|
190
|
+
eps = self.class.endpoints.dup
|
189
191
|
group_map = add_groups(gps)
|
190
192
|
add_endpoints eps, group_map
|
193
|
+
end
|
194
|
+
|
195
|
+
# Sets up a worker thread for the NATS API server and blocks the current thread.
|
196
|
+
#
|
197
|
+
# @see #setup_worker
|
198
|
+
def setup_worker!(nats_url: 'nats://localhost:4222', service_opts: {})
|
199
|
+
setup_worker(nats_url:, service_opts:)
|
191
200
|
sleep
|
192
201
|
end
|
193
202
|
|
@@ -202,6 +211,18 @@ module Rubyists
|
|
202
211
|
|
203
212
|
private
|
204
213
|
|
214
|
+
# Builds the service options for the NATS service.
|
215
|
+
#
|
216
|
+
# @param service_opts [Hash] Options for the NATS service.
|
217
|
+
#
|
218
|
+
# @return [Hash] The complete service options including name and version.
|
219
|
+
def build_service_opts(service_opts:)
|
220
|
+
{
|
221
|
+
name: self.class.name.split('::').join('.'),
|
222
|
+
version: '0.1.0',
|
223
|
+
}.merge(service_opts)
|
224
|
+
end
|
225
|
+
|
205
226
|
# Adds groups to the NATS service.
|
206
227
|
#
|
207
228
|
# @param gps [Hash] The groups to add, where keys are group names and values are group definitions.
|
data/lib/leopard/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: leopard
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- bougyman
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-08-
|
10
|
+
date: 2025-08-06 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: concurrent-ruby
|
@@ -98,6 +98,7 @@ files:
|
|
98
98
|
- ci/nats/accounts.txt
|
99
99
|
- ci/nats/start.sh
|
100
100
|
- ci/publish-gem.sh
|
101
|
+
- doc/service-api-vs-rest.adoc
|
101
102
|
- examples/echo_endpoint.rb
|
102
103
|
- lib/leopard.rb
|
103
104
|
- lib/leopard/errors.rb
|