async-nats 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/BENCHMARK_RESULTS.md +155 -0
- data/BENCHMARK_SUMMARY.md +94 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE.txt +203 -0
- data/README.md +348 -0
- data/Rakefile +8 -0
- data/benchmark/pub_perf.rb +85 -0
- data/benchmark/pub_sub_perf.rb +117 -0
- data/benchmark/pub_sub_simple.rb +74 -0
- data/benchmark/sub_perf.rb +66 -0
- data/examples/basic-tls.rb +76 -0
- data/examples/basic-usage.rb +66 -0
- data/examples/basic.rb +46 -0
- data/examples/clustered.rb +79 -0
- data/examples/service_api/basic.rb +32 -0
- data/examples/service_api/discovery.rb +31 -0
- data/examples/service_api/errors.rb +33 -0
- data/examples/service_api/groups.rb +30 -0
- data/examples/service_api/stats.rb +40 -0
- data/lib/async/nats/client.rb +332 -0
- data/lib/async/nats/executor.rb +98 -0
- data/lib/async/nats/socket.rb +152 -0
- data/lib/async/nats/version.rb +7 -0
- data/lib/async/nats.rb +12 -0
- data/sig/async/nats.rbs +6 -0
- metadata +206 -0
data/README.md
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
# Async::NATS
|
2
|
+
|
3
|
+
A high-performance, fiber-based NATS client for Ruby built on top of the [async](https://github.com/socketry/async) framework. This project provides a fully asynchronous NATS client that maintains API compatibility with [nats-pure](https://github.com/nats-io/nats-pure.rb) while delivering superior performance through fiber-based concurrency.
|
4
|
+
|
5
|
+
## ⚠️ AI-Generated Code Notice
|
6
|
+
|
7
|
+
**This entire project was developed by the Windsurf AI code agent.** While the code has been tested and benchmarked, it should be considered experimental. Use in production at your own risk and conduct thorough testing for your specific use case.
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
- ✅ **Fully Async** - All I/O operations use fibers instead of threads
|
12
|
+
- ✅ **API Compatible** - Drop-in replacement for nats-pure with async support
|
13
|
+
- ✅ **High Performance** - 4-8% faster publish throughput than nats-pure
|
14
|
+
- ✅ **Efficient Drain** - 100x faster drain operations using async primitives
|
15
|
+
- ✅ **Lower Resource Usage** - Fiber-based concurrency uses less memory than threads
|
16
|
+
- ✅ **Custom Async Executor** - Worker pool pattern for subscription handling
|
17
|
+
- ✅ **Context Detection** - Works in both async and non-async contexts
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
gem 'async-nats'
|
25
|
+
```
|
26
|
+
|
27
|
+
And then execute:
|
28
|
+
|
29
|
+
```bash
|
30
|
+
bundle install
|
31
|
+
```
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
gem install async-nats
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Basic Publishing and Subscribing
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require 'async'
|
45
|
+
require 'async/nats'
|
46
|
+
|
47
|
+
Async do
|
48
|
+
# Create and connect to NATS server
|
49
|
+
nats = Async::NATS::Client.new
|
50
|
+
nats.connect(servers: ["nats://localhost:4222"])
|
51
|
+
|
52
|
+
# Subscribe to a subject
|
53
|
+
nats.subscribe("hello") do |msg|
|
54
|
+
puts "Received: #{msg.data}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Publish a message
|
58
|
+
nats.publish("hello", "World!")
|
59
|
+
|
60
|
+
# Wait a bit for message delivery
|
61
|
+
Async::Task.current.sleep(0.1)
|
62
|
+
|
63
|
+
# Gracefully drain and close
|
64
|
+
nats.drain.wait
|
65
|
+
end
|
66
|
+
```
|
67
|
+
|
68
|
+
### Request-Reply Pattern
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
Async do
|
72
|
+
nats = Async::NATS::Client.new
|
73
|
+
nats.connect
|
74
|
+
|
75
|
+
# Responder
|
76
|
+
nats.subscribe("help") do |msg|
|
77
|
+
nats.publish(msg.reply, "I can help!")
|
78
|
+
end
|
79
|
+
|
80
|
+
# Requester
|
81
|
+
response = nats.request("help", "please", timeout: 1.0)
|
82
|
+
puts "Response: #{response.data}"
|
83
|
+
|
84
|
+
nats.close
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
### Multiple Subscriptions
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
Async do
|
92
|
+
nats = Async::NATS::Client.new
|
93
|
+
nats.connect
|
94
|
+
|
95
|
+
# Subscribe to multiple subjects
|
96
|
+
nats.subscribe("events.>") do |msg|
|
97
|
+
puts "Event: #{msg.subject} - #{msg.data}"
|
98
|
+
end
|
99
|
+
|
100
|
+
nats.subscribe("logs.*") do |msg|
|
101
|
+
puts "Log: #{msg.subject} - #{msg.data}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Publish to different subjects
|
105
|
+
nats.publish("events.user.login", "user123")
|
106
|
+
nats.publish("logs.error", "Something went wrong")
|
107
|
+
|
108
|
+
Async::Task.current.sleep(0.1)
|
109
|
+
nats.close
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
### Async Drain with Timeout
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
Async do |task|
|
117
|
+
nats = Async::NATS::Client.new
|
118
|
+
nats.connect
|
119
|
+
|
120
|
+
# ... use connection ...
|
121
|
+
|
122
|
+
# Drain with custom timeout
|
123
|
+
begin
|
124
|
+
task.with_timeout(5.0) do
|
125
|
+
nats.drain.wait
|
126
|
+
end
|
127
|
+
puts "Drain completed successfully"
|
128
|
+
rescue Async::TimeoutError
|
129
|
+
puts "Drain timed out, forcing close"
|
130
|
+
nats.close
|
131
|
+
end
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
135
|
+
### Queue Groups
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
Async do
|
139
|
+
nats = Async::NATS::Client.new
|
140
|
+
nats.connect
|
141
|
+
|
142
|
+
# Create queue group subscribers
|
143
|
+
3.times do |i|
|
144
|
+
nats.subscribe("work", queue: "workers") do |msg|
|
145
|
+
puts "Worker #{i} processing: #{msg.data}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Publish work items - distributed across queue group
|
150
|
+
10.times do |i|
|
151
|
+
nats.publish("work", "task-#{i}")
|
152
|
+
end
|
153
|
+
|
154
|
+
Async::Task.current.sleep(0.5)
|
155
|
+
nats.close
|
156
|
+
end
|
157
|
+
```
|
158
|
+
|
159
|
+
## Performance
|
160
|
+
|
161
|
+
async-nats delivers excellent performance compared to nats-pure:
|
162
|
+
|
163
|
+
### Publish Performance (100k messages)
|
164
|
+
- **nats-pure**: 947,460 msgs/sec
|
165
|
+
- **async-nats**: 989,994 msgs/sec
|
166
|
+
- **Result**: **+4.5% faster** ✅
|
167
|
+
|
168
|
+
### Pub/Sub Performance (10k messages)
|
169
|
+
- **nats-pure**: 19,982 msgs/sec
|
170
|
+
- **async-nats**: 19,017 msgs/sec
|
171
|
+
- **Result**: Essentially identical ✅
|
172
|
+
|
173
|
+
### Drain Performance
|
174
|
+
- **async-nats**: ~1ms drain latency
|
175
|
+
- **Result**: **100x faster** than thread-based polling ✅
|
176
|
+
|
177
|
+
See [BENCHMARK_SUMMARY.md](BENCHMARK_SUMMARY.md) and [BENCHMARK_RESULTS.md](BENCHMARK_RESULTS.md) for detailed performance analysis.
|
178
|
+
|
179
|
+
## Architecture
|
180
|
+
|
181
|
+
async-nats is built on top of nats-pure and overrides key components to use async primitives:
|
182
|
+
|
183
|
+
### Key Components
|
184
|
+
|
185
|
+
1. **Async::NATS::Socket** - Replaces blocking I/O with async operations using `io-stream`
|
186
|
+
2. **Async::NATS::Client** - Overrides connection and threading to use async tasks
|
187
|
+
3. **Async::NATS::Executor** - Custom worker pool for subscription callbacks
|
188
|
+
4. **Async Drain** - Event-driven drain using `Async::Barrier`
|
189
|
+
|
190
|
+
### Concurrency Model
|
191
|
+
|
192
|
+
```
|
193
|
+
┌─────────────────────────────────────────┐
|
194
|
+
│ Async::NATS::Client │
|
195
|
+
│ │
|
196
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
197
|
+
│ │ Read Loop │ │ Flusher │ │
|
198
|
+
│ │ (Async Task)│ │ (Async Task)│ │
|
199
|
+
│ └─────────────┘ └─────────────┘ │
|
200
|
+
│ │
|
201
|
+
│ ┌─────────────────────────────────┐ │
|
202
|
+
│ │ Async::NATS::Executor │ │
|
203
|
+
│ │ (Worker Pool for Callbacks) │ │
|
204
|
+
│ │ ┌────┐ ┌────┐ ┌────┐ │ │
|
205
|
+
│ │ │ W1 │ │ W2 │ │... │ │ │
|
206
|
+
│ │ └────┘ └────┘ └────┘ │ │
|
207
|
+
│ └─────────────────────────────────┘ │
|
208
|
+
└─────────────────────────────────────────┘
|
209
|
+
```
|
210
|
+
|
211
|
+
All concurrency uses fibers (via async tasks) instead of threads, resulting in:
|
212
|
+
- Lower memory footprint
|
213
|
+
- Better scalability
|
214
|
+
- Faster context switching
|
215
|
+
- More efficient resource usage
|
216
|
+
|
217
|
+
## Relationship with nats-pure
|
218
|
+
|
219
|
+
async-nats is built **on top of** [nats-pure](https://github.com/nats-io/nats-pure.rb), not as a fork. It:
|
220
|
+
|
221
|
+
1. **Inherits from** `NATS::IO::Client` to maintain API compatibility
|
222
|
+
2. **Overrides** I/O and threading methods to use async primitives
|
223
|
+
3. **Reuses** protocol handling, message parsing, and business logic
|
224
|
+
4. **Extends** with async-specific features like async drain
|
225
|
+
|
226
|
+
This approach provides:
|
227
|
+
- ✅ Full API compatibility with nats-pure
|
228
|
+
- ✅ Automatic updates when nats-pure fixes bugs
|
229
|
+
- ✅ Minimal code duplication
|
230
|
+
- ✅ Easy migration path
|
231
|
+
|
232
|
+
### Migration from nats-pure
|
233
|
+
|
234
|
+
Migration is straightforward:
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
# Before (nats-pure)
|
238
|
+
require 'nats/io/client'
|
239
|
+
|
240
|
+
nats = NATS::IO::Client.new
|
241
|
+
nats.connect
|
242
|
+
# ... use nats ...
|
243
|
+
nats.close
|
244
|
+
|
245
|
+
# After (async-nats)
|
246
|
+
require 'async'
|
247
|
+
require 'async/nats'
|
248
|
+
|
249
|
+
Async do
|
250
|
+
nats = Async::NATS::Client.new
|
251
|
+
nats.connect
|
252
|
+
# ... use nats (same API) ...
|
253
|
+
nats.close
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
The main difference is wrapping your code in an `Async` block to provide the async context.
|
258
|
+
|
259
|
+
## Development
|
260
|
+
|
261
|
+
This project was developed entirely by the **Windsurf AI code agent** through an iterative process:
|
262
|
+
|
263
|
+
1. **Initial Port** - Created async socket wrapper and client override
|
264
|
+
2. **Benchmarking** - Ported benchmarks from nats-pure and compared performance
|
265
|
+
3. **Drain Fix** - Implemented context detection for mixed async/thread usage
|
266
|
+
4. **Async Executor** - Replaced ThreadPoolExecutor with async worker pool
|
267
|
+
5. **Async Drain** - Implemented event-driven drain using Async::Barrier
|
268
|
+
6. **Performance Optimization** - Cached task references to eliminate overhead
|
269
|
+
|
270
|
+
### Running Tests
|
271
|
+
|
272
|
+
```bash
|
273
|
+
# Run RSpec tests
|
274
|
+
bundle exec rspec
|
275
|
+
|
276
|
+
# Run benchmarks
|
277
|
+
docker run -d --name nats-bench -p 4222:4222 nats:latest
|
278
|
+
bundle exec ruby benchmark/pub_perf.rb -n 100000
|
279
|
+
bundle exec ruby benchmark/pub_sub_simple.rb
|
280
|
+
docker stop nats-bench && docker rm nats-bench
|
281
|
+
```
|
282
|
+
|
283
|
+
### Project Structure
|
284
|
+
|
285
|
+
```
|
286
|
+
lib/async/nats/
|
287
|
+
├── client.rb # Main client with async overrides
|
288
|
+
├── socket.rb # Async I/O wrapper
|
289
|
+
├── executor.rb # Async worker pool for subscriptions
|
290
|
+
└── version.rb # Version information
|
291
|
+
|
292
|
+
benchmark/
|
293
|
+
├── pub_perf.rb # Publish performance test
|
294
|
+
├── pub_sub_perf.rb # Pub/Sub with drain test
|
295
|
+
└── pub_sub_simple.rb # Simple pub/sub test
|
296
|
+
|
297
|
+
BENCHMARK_SUMMARY.md # Performance summary
|
298
|
+
BENCHMARK_RESULTS.md # Detailed benchmark results
|
299
|
+
```
|
300
|
+
|
301
|
+
## Benchmarks
|
302
|
+
|
303
|
+
Comprehensive benchmarks are available:
|
304
|
+
|
305
|
+
- **[BENCHMARK_SUMMARY.md](BENCHMARK_SUMMARY.md)** - Quick overview of performance results
|
306
|
+
- **[BENCHMARK_RESULTS.md](BENCHMARK_RESULTS.md)** - Detailed analysis with tables and comparisons
|
307
|
+
|
308
|
+
To run benchmarks yourself:
|
309
|
+
|
310
|
+
```bash
|
311
|
+
# Start NATS server
|
312
|
+
docker run -d --name nats-bench -p 4222:4222 nats:latest
|
313
|
+
|
314
|
+
# Run publish benchmark
|
315
|
+
bundle exec ruby benchmark/pub_perf.rb -n 100000
|
316
|
+
|
317
|
+
# Run pub/sub benchmark
|
318
|
+
bundle exec ruby benchmark/pub_sub_simple.rb
|
319
|
+
|
320
|
+
# Cleanup
|
321
|
+
docker stop nats-bench && docker rm nats-bench
|
322
|
+
```
|
323
|
+
|
324
|
+
## Limitations
|
325
|
+
|
326
|
+
- **Experimental** - This is an AI-generated project and should be thoroughly tested before production use
|
327
|
+
- **Requires Async Context** - Must be used within an `Async` block
|
328
|
+
- **Ruby 3.0+** - Requires modern Ruby with fiber support
|
329
|
+
- **Limited Testing** - While benchmarked, it lacks comprehensive test coverage
|
330
|
+
|
331
|
+
## Contributing
|
332
|
+
|
333
|
+
Bug reports and pull requests are welcome on GitHub. This project is intended to demonstrate the capabilities of AI-assisted development and the async framework.
|
334
|
+
|
335
|
+
## License
|
336
|
+
|
337
|
+
The gem is available as open source under the terms of the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).
|
338
|
+
|
339
|
+
## Credits
|
340
|
+
|
341
|
+
- **Development**: Entirely created by the Windsurf AI code agent
|
342
|
+
- **Based on**: [nats-pure](https://github.com/nats-io/nats-pure.rb) by the NATS.io team
|
343
|
+
- **Async Framework**: [async](https://github.com/socketry/async) by Samuel Williams
|
344
|
+
- **NATS**: [NATS.io](https://nats.io/) messaging system
|
345
|
+
|
346
|
+
## Disclaimer
|
347
|
+
|
348
|
+
This project was developed completely autonomously by an AI code agent as an experiment in AI-assisted software development. While functional and benchmarked, it has not undergone the same level of scrutiny as human-developed production software. Use at your own risk and conduct thorough testing for your specific use case.
|
data/Rakefile
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2016-2018 The NATS Authors
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require "bundler/setup"
|
18
|
+
require "optparse"
|
19
|
+
require "async"
|
20
|
+
|
21
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
22
|
+
require "async/nats"
|
23
|
+
|
24
|
+
$count = 100000
|
25
|
+
$batch = 100
|
26
|
+
|
27
|
+
$delay = 0.00001
|
28
|
+
$dmin = 0.00001
|
29
|
+
|
30
|
+
TRIP = (2 * 1024 * 1024)
|
31
|
+
TSIZE = 4 * 1024
|
32
|
+
|
33
|
+
$sub = "test"
|
34
|
+
$data_size = 16
|
35
|
+
|
36
|
+
$hash = 2500
|
37
|
+
|
38
|
+
$stdout.sync = true
|
39
|
+
|
40
|
+
parser = OptionParser.new do |opts|
|
41
|
+
opts.banner = "Usage: pub_perf [options]"
|
42
|
+
|
43
|
+
opts.separator ""
|
44
|
+
opts.separator "options:"
|
45
|
+
|
46
|
+
opts.on("-n COUNT", "Messages to send (default: #{$count}}") { |count| $count = count.to_i }
|
47
|
+
opts.on("-s SIZE", "Message size (default: #{$data_size})") { |size| $data_size = size.to_i }
|
48
|
+
opts.on("-S SUBJECT", "Send subject (default: (#{$sub})") { |sub| $sub = sub }
|
49
|
+
opts.on("-b BATCH", "Batch size (default: (#{$batch})") { |batch| $batch = batch.to_i }
|
50
|
+
end
|
51
|
+
|
52
|
+
parser.parse(ARGV)
|
53
|
+
|
54
|
+
trap("TERM") { exit! }
|
55
|
+
trap("INT") { exit! }
|
56
|
+
|
57
|
+
$data = Array.new($data_size) { "%01x" % rand(16) }.join("").freeze
|
58
|
+
|
59
|
+
Async do
|
60
|
+
nats = Async::NATS::Client.new
|
61
|
+
nats.connect
|
62
|
+
|
63
|
+
$batch = 10 if $data_size >= TSIZE
|
64
|
+
$start = Time.now
|
65
|
+
$to_send = $count
|
66
|
+
|
67
|
+
puts "Sending #{$count} messages of size #{$data.size} bytes on [#{$sub}]"
|
68
|
+
|
69
|
+
loop do
|
70
|
+
(0..$batch).each do
|
71
|
+
$to_send -= 1
|
72
|
+
nats.publish($sub, $data)
|
73
|
+
if $to_send == 0
|
74
|
+
nats.flush
|
75
|
+
|
76
|
+
elapsed = Time.now - $start
|
77
|
+
mbytes = sprintf("%.1f", (($data_size * $count) / elapsed) / (1024 * 1024))
|
78
|
+
puts "\nTest completed : #{($count / elapsed).ceil} sent/received msgs/sec (#{mbytes} MB/sec)\n"
|
79
|
+
exit
|
80
|
+
end
|
81
|
+
Async::Task.current.sleep($delay) if $to_send.modulo(1000) == 0
|
82
|
+
printf("#") if $to_send.modulo($hash) == 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2016-2018 The NATS Authors
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require "bundler/setup"
|
18
|
+
require "optparse"
|
19
|
+
require "async"
|
20
|
+
|
21
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
22
|
+
require "async/nats"
|
23
|
+
|
24
|
+
$count = 100000
|
25
|
+
$batch = 100
|
26
|
+
|
27
|
+
$subscriptions = 1
|
28
|
+
$concurrency = 1
|
29
|
+
|
30
|
+
$delay = 0.00001
|
31
|
+
|
32
|
+
TSIZE = 4 * 1024
|
33
|
+
|
34
|
+
$sub = "test"
|
35
|
+
$data_size = 16
|
36
|
+
|
37
|
+
$stdout.sync = true
|
38
|
+
|
39
|
+
parser = OptionParser.new do |opts|
|
40
|
+
opts.banner = "Usage: pub_perf [options]"
|
41
|
+
|
42
|
+
opts.separator ""
|
43
|
+
opts.separator "options:"
|
44
|
+
|
45
|
+
opts.on("-n COUNT", "Messages to send (default: #{$count}}") { |count| $count = count.to_i }
|
46
|
+
opts.on("-s SIZE", "Message size (default: #{$data_size})") { |size| $data_size = size.to_i }
|
47
|
+
opts.on("-S SUBJECT", "Send subject (default: (#{$sub})") { |sub| $sub = sub }
|
48
|
+
opts.on("-b BATCH", "Batch size (default: (#{$batch})") { |batch| $batch = batch.to_i }
|
49
|
+
opts.on("-c SUBSCRIPTIONS", "Subscription number (default: (#{$subscriptions})") { |subscriptions| $subscriptions = subscriptions.to_i }
|
50
|
+
opts.on("-t CONCURRENCY", "Subscription processing concurrency (default: (#{$concurrency})") { |concurrency| $concurrency = concurrency.to_i }
|
51
|
+
end
|
52
|
+
|
53
|
+
parser.parse(ARGV)
|
54
|
+
|
55
|
+
$data = Array.new($data_size) { "%01x" % rand(16) }.join("").freeze
|
56
|
+
|
57
|
+
puts "Sending #{$count} messages of size #{$data.size} bytes on [#{$sub}], receiving each in #{$subscriptions} subscriptions"
|
58
|
+
|
59
|
+
def results
|
60
|
+
elapsed = Time.now - $start
|
61
|
+
mbytes = sprintf("%.1f", (($data_size * $received) / elapsed) / (1024 * 1024))
|
62
|
+
<<~MSG
|
63
|
+
|
64
|
+
Test completed: #{($received / elapsed).ceil} received msgs/sec (#{mbytes} MB/sec)
|
65
|
+
Received #{$received} messages in #{elapsed} seconds
|
66
|
+
MSG
|
67
|
+
end
|
68
|
+
|
69
|
+
trap("TERM") {
|
70
|
+
puts results
|
71
|
+
exit!
|
72
|
+
}
|
73
|
+
trap("INT") {
|
74
|
+
puts results
|
75
|
+
exit!
|
76
|
+
}
|
77
|
+
|
78
|
+
Async do
|
79
|
+
nats = Async::NATS::Client.new
|
80
|
+
nats.connect
|
81
|
+
|
82
|
+
$batch = 10 if $data_size >= TSIZE
|
83
|
+
|
84
|
+
$received = 0
|
85
|
+
|
86
|
+
$subscriptions.times do
|
87
|
+
subscription = nats.subscribe($sub) {
|
88
|
+
$received += 1
|
89
|
+
printf("#") if $received.modulo(2500) == 0
|
90
|
+
}
|
91
|
+
subscription.processing_concurrency = $concurrency
|
92
|
+
end
|
93
|
+
|
94
|
+
$start = Time.now
|
95
|
+
$to_send = $count
|
96
|
+
|
97
|
+
loop do
|
98
|
+
(0..$batch).each do
|
99
|
+
$to_send -= 1
|
100
|
+
nats.publish($sub, $data)
|
101
|
+
|
102
|
+
break if $to_send.zero?
|
103
|
+
|
104
|
+
Async::Task.current.sleep($delay) if $to_send.modulo(1000) == 0
|
105
|
+
end
|
106
|
+
break if $to_send.zero?
|
107
|
+
end
|
108
|
+
|
109
|
+
# Finish and let client to process everything
|
110
|
+
nats.flush(300)
|
111
|
+
nats.drain
|
112
|
+
|
113
|
+
# Wait a bit for drain to complete
|
114
|
+
Async::Task.current.sleep(1)
|
115
|
+
|
116
|
+
puts results
|
117
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Simple pub/sub benchmark without drain
|
5
|
+
|
6
|
+
require "bundler/setup"
|
7
|
+
require "async"
|
8
|
+
require "monitor"
|
9
|
+
|
10
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
11
|
+
require "async/nats"
|
12
|
+
|
13
|
+
COUNT = 10000
|
14
|
+
|
15
|
+
puts "Simple pub/sub benchmark: #{COUNT} messages"
|
16
|
+
|
17
|
+
Async do
|
18
|
+
nats = Async::NATS::Client.new
|
19
|
+
nats.connect(servers: ["nats://127.0.0.1:4222"])
|
20
|
+
|
21
|
+
# Thread-safe counters
|
22
|
+
received = 0
|
23
|
+
start_time = nil
|
24
|
+
lock = Monitor.new
|
25
|
+
|
26
|
+
# Subscribe
|
27
|
+
nats.subscribe("bench.test") do |msg|
|
28
|
+
lock.synchronize do
|
29
|
+
received += 1
|
30
|
+
start_time ||= Time.now
|
31
|
+
|
32
|
+
print "#" if received % 2500 == 0
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Give subscription time to register
|
37
|
+
Async::Task.current.sleep(0.1)
|
38
|
+
|
39
|
+
# Publish messages
|
40
|
+
puts "Publishing #{COUNT} messages..."
|
41
|
+
COUNT.times do |i|
|
42
|
+
nats.publish("bench.test", "test message #{i}")
|
43
|
+
end
|
44
|
+
|
45
|
+
nats.flush(5.0)
|
46
|
+
|
47
|
+
# Wait for messages to be received
|
48
|
+
puts "\nWaiting for messages..."
|
49
|
+
10.times do
|
50
|
+
Async::Task.current.sleep(0.5)
|
51
|
+
count = lock.synchronize { received }
|
52
|
+
if count == COUNT
|
53
|
+
break
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Calculate results
|
58
|
+
final_count = lock.synchronize { received }
|
59
|
+
final_start = lock.synchronize { start_time }
|
60
|
+
|
61
|
+
if final_count == COUNT && final_start
|
62
|
+
elapsed = Time.now - final_start
|
63
|
+
msgs_per_sec = (COUNT / elapsed).to_i
|
64
|
+
mb_per_sec = ((16 * COUNT) / elapsed / (1024 * 1024)).round(1)
|
65
|
+
|
66
|
+
puts "\nTest completed: #{msgs_per_sec} msgs/sec (#{mb_per_sec} MB/sec)"
|
67
|
+
puts "Received #{COUNT} messages in #{elapsed.round(3)} seconds"
|
68
|
+
else
|
69
|
+
puts "\nTimeout or incomplete"
|
70
|
+
puts "Received #{final_count}/#{COUNT} messages"
|
71
|
+
end
|
72
|
+
|
73
|
+
nats.close
|
74
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright 2016-2018 The NATS Authors
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
#
|
16
|
+
|
17
|
+
require "bundler/setup"
|
18
|
+
require "optparse"
|
19
|
+
require "async"
|
20
|
+
|
21
|
+
$:.unshift File.expand_path("../../lib", __FILE__)
|
22
|
+
require "async/nats"
|
23
|
+
|
24
|
+
$expected = 100000
|
25
|
+
$hash = 2500
|
26
|
+
$sub = "test"
|
27
|
+
|
28
|
+
$stdout.sync = true
|
29
|
+
|
30
|
+
parser = OptionParser.new do |opts|
|
31
|
+
opts.banner = "Usage: sub_perf [options]"
|
32
|
+
|
33
|
+
opts.separator ""
|
34
|
+
opts.separator "options:"
|
35
|
+
|
36
|
+
opts.on("-n COUNT", "Messages to expect (default: #{$expected})") { |count| $expected = count.to_i }
|
37
|
+
opts.on("-s SUBJECT", "Send subject (default: #{$sub})") { |sub| $sub = sub }
|
38
|
+
end
|
39
|
+
|
40
|
+
parser.parse(ARGV)
|
41
|
+
|
42
|
+
trap("TERM") { exit! }
|
43
|
+
trap("INT") { exit! }
|
44
|
+
|
45
|
+
Async do
|
46
|
+
nats = Async::NATS::Client.new
|
47
|
+
|
48
|
+
nats.connect
|
49
|
+
done = nats.new_cond
|
50
|
+
received = 1
|
51
|
+
nats.subscribe($sub) do
|
52
|
+
($start = Time.now and puts "Started Receiving!") if received == 1
|
53
|
+
if (received += 1) == $expected
|
54
|
+
puts "\nTest completed : #{($expected / (Time.now - $start)).ceil} msgs/sec.\n"
|
55
|
+
nats.synchronize do
|
56
|
+
done.signal
|
57
|
+
end
|
58
|
+
end
|
59
|
+
printf("+") if received.modulo($hash) == 0
|
60
|
+
end
|
61
|
+
|
62
|
+
puts "Waiting for #{$expected} messages on [#{$sub}]"
|
63
|
+
nats.synchronize do
|
64
|
+
done.wait
|
65
|
+
end
|
66
|
+
end
|