leopard 0.1.6 → 0.2.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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/.version.txt +1 -1
- data/CHANGELOG.md +19 -0
- data/doc/service-api-vs-rest.adoc +149 -0
- data/examples/echo_endpoint.rb +4 -3
- data/lib/leopard/message_wrapper.rb +6 -5
- data/lib/leopard/nats_api_server.rb +40 -16
- 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: 550ad9a639b0f8caf0989990853c1ab49428f4f48c0b28a6fdc30c30d8450a03
|
4
|
+
data.tar.gz: 0b2fdc7d99a23fe156b446b724407eedb074806d989542f42af4ff63d74744c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68cc478abf3538e4d567fe170fa72340632ac056608d16aa1dc426842c2c9de3fbb6001ac8c75058a43d41558d625fbe31ad52104d54d5c33f5363b5d84d1f7e
|
7
|
+
data.tar.gz: 9b9769a6a7c5eeadc28424f530fef164cd6ed558f1fb975afbecbefb01d96c10428bf653fddf3a6840072fe02b89a6b15eee121f55e9bf5cbd01b0e56aee1a69
|
data/.version.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,24 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [0.2.0](https://github.com/rubyists/leopard/compare/v0.1.7...v0.2.0) (2025-08-07)
|
4
|
+
|
5
|
+
|
6
|
+
### ⚠ BREAKING CHANGES
|
7
|
+
|
8
|
+
* Big move to use kwargs as the initializer for classes which include us ([#28](https://github.com/rubyists/leopard/issues/28))
|
9
|
+
|
10
|
+
### Bug Fixes
|
11
|
+
|
12
|
+
* Big move to use kwargs as the initializer for classes which include us ([#28](https://github.com/rubyists/leopard/issues/28)) ([72293b4](https://github.com/rubyists/leopard/commit/72293b434998679fe3ff2d467a6a39c11a325b5a))
|
13
|
+
|
14
|
+
## [0.1.7](https://github.com/rubyists/leopard/compare/v0.1.6...v0.1.7) (2025-08-06)
|
15
|
+
|
16
|
+
|
17
|
+
### Bug Fixes
|
18
|
+
|
19
|
+
* 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))
|
20
|
+
* 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))
|
21
|
+
|
3
22
|
## [0.1.6](https://github.com/rubyists/leopard/compare/v0.1.5...v0.1.6) (2025-08-03)
|
4
23
|
|
5
24
|
|
@@ -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
@@ -7,11 +7,12 @@ require_relative '../lib/leopard/nats_api_server'
|
|
7
7
|
class EchoService
|
8
8
|
include Rubyists::Leopard::NatsApiServer
|
9
9
|
|
10
|
-
def initialize(a_var
|
10
|
+
def initialize(a_var: 1)
|
11
11
|
logger.info "EchoService initialized with a_var: #{a_var}"
|
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
|
@@ -20,8 +21,8 @@ if __FILE__ == $PROGRAM_NAME
|
|
20
21
|
service_opts: {
|
21
22
|
name: 'example.echo',
|
22
23
|
version: '1.0.0',
|
23
|
-
instance_args:
|
24
|
+
instance_args: { a_var: 2 },
|
24
25
|
},
|
25
|
-
instances:
|
26
|
+
instances: 1,
|
26
27
|
)
|
27
28
|
end
|
@@ -10,10 +10,11 @@ module Rubyists
|
|
10
10
|
#
|
11
11
|
# @!attribute [r] data
|
12
12
|
# @return [Object] The parsed data from the NATS message.
|
13
|
+
attr_reader :raw, :data
|
13
14
|
#
|
14
|
-
# @!attribute [
|
15
|
+
# @!attribute [w] headers
|
15
16
|
# @return [Hash] The headers from the NATS message.
|
16
|
-
|
17
|
+
attr_accessor :headers
|
17
18
|
|
18
19
|
# @param nats_msg [NATS::Message] The NATS message to wrap.
|
19
20
|
def initialize(nats_msg)
|
@@ -26,15 +27,15 @@ module Rubyists
|
|
26
27
|
#
|
27
28
|
# @return [void]
|
28
29
|
def respond(payload)
|
30
|
+
raw.header = headers unless headers.empty?
|
29
31
|
raw.respond(serialize(payload))
|
30
32
|
end
|
31
33
|
|
32
34
|
# @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
35
|
#
|
35
36
|
# @return [void]
|
36
|
-
def respond_with_error(err
|
37
|
-
raw.respond_with_error(err.to_s
|
37
|
+
def respond_with_error(err)
|
38
|
+
raw.respond_with_error(err.to_s)
|
38
39
|
end
|
39
40
|
|
40
41
|
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,33 +90,37 @@ 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}"
|
101
|
+
raise ArgumentError, 'instance_args must be a Hash' if @instance_args && !@instance_args.is_a?(Hash)
|
102
|
+
|
99
103
|
count.times do
|
100
|
-
|
101
|
-
gps = groups.dup
|
102
|
-
pool.post { build_worker(url, opts, eps, gps, workers) }
|
104
|
+
pool.post { build_worker(url, opts, workers, blocking) }
|
103
105
|
end
|
104
106
|
pool
|
105
107
|
end
|
106
108
|
|
107
109
|
# Builds a worker instance and sets it up with the NATS server.
|
108
110
|
#
|
109
|
-
# @param
|
110
|
-
# @param
|
111
|
-
# @param eps [Array<Hash>] The list of endpoints to add.
|
112
|
-
# @param gps [Hash] The groups to add.
|
111
|
+
# @param nats_url [String] The URL of the NATS server.
|
112
|
+
# @param service_opts [Hash] Options for the NATS service.
|
113
113
|
# @param workers [Array] The array to store worker instances.
|
114
|
+
# @param blocking [Boolean] If true, blocks the current thread until the worker is set up.
|
114
115
|
#
|
115
116
|
# @return [void]
|
116
|
-
def build_worker(
|
117
|
-
worker = @instance_args ? new(
|
117
|
+
def build_worker(nats_url, service_opts, workers, blocking)
|
118
|
+
worker = @instance_args ? new(**@instance_args) : new
|
118
119
|
workers << worker
|
119
|
-
|
120
|
+
args = { nats_url:, service_opts: }
|
121
|
+
return worker.setup_worker!(**args) if blocking
|
122
|
+
|
123
|
+
worker.setup_worker(**args)
|
120
124
|
end
|
121
125
|
|
122
126
|
# Shuts down the NATS API server gracefully.
|
@@ -174,7 +178,6 @@ module Rubyists
|
|
174
178
|
|
175
179
|
# Sets up a worker thread for the NATS API server.
|
176
180
|
# This method connects to the NATS server, adds the service, groups, and endpoints,
|
177
|
-
# and keeps the worker thread alive.
|
178
181
|
#
|
179
182
|
# @param url [String] The URL of the NATS server.
|
180
183
|
# @param opts [Hash] Options for the NATS service.
|
@@ -182,12 +185,21 @@ module Rubyists
|
|
182
185
|
# @param gps [Hash] The groups to add.
|
183
186
|
#
|
184
187
|
# @return [void]
|
185
|
-
def setup_worker(
|
188
|
+
def setup_worker(nats_url: 'nats://localhost:4222', service_opts: {})
|
186
189
|
@thread = Thread.current
|
187
|
-
@client = NATS.connect
|
188
|
-
@service = @client.services.add(
|
190
|
+
@client = NATS.connect nats_url
|
191
|
+
@service = @client.services.add(build_service_opts(service_opts:))
|
192
|
+
gps = self.class.groups.dup
|
193
|
+
eps = self.class.endpoints.dup
|
189
194
|
group_map = add_groups(gps)
|
190
195
|
add_endpoints eps, group_map
|
196
|
+
end
|
197
|
+
|
198
|
+
# Sets up a worker thread for the NATS API server and blocks the current thread.
|
199
|
+
#
|
200
|
+
# @see #setup_worker
|
201
|
+
def setup_worker!(nats_url: 'nats://localhost:4222', service_opts: {})
|
202
|
+
setup_worker(nats_url:, service_opts:)
|
191
203
|
sleep
|
192
204
|
end
|
193
205
|
|
@@ -202,6 +214,18 @@ module Rubyists
|
|
202
214
|
|
203
215
|
private
|
204
216
|
|
217
|
+
# Builds the service options for the NATS service.
|
218
|
+
#
|
219
|
+
# @param service_opts [Hash] Options for the NATS service.
|
220
|
+
#
|
221
|
+
# @return [Hash] The complete service options including name and version.
|
222
|
+
def build_service_opts(service_opts:)
|
223
|
+
{
|
224
|
+
name: self.class.name.split('::').join('.'),
|
225
|
+
version: '0.1.0',
|
226
|
+
}.merge(service_opts)
|
227
|
+
end
|
228
|
+
|
205
229
|
# Adds groups to the NATS service.
|
206
230
|
#
|
207
231
|
# @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.
|
4
|
+
version: 0.2.0
|
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-07 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
|