ruby_nest_nats 0.1.2 → 0.1.3
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/README.md +168 -12
- data/lib/ruby_nest_nats.rb +103 -36
- data/lib/ruby_nest_nats/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 12ced8e11e87c01a5106285c4b01e7cb75f338adbb952b95d2362d928c76133c
|
4
|
+
data.tar.gz: '049dd24c5e1015e8b3e90be3f77f352bd934c7837c71881e8f4235a47498e2ec'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3340f499fe967b290f3b2b21e2f34b69a55ed8b8a003c9e0af897df556890fcb884bdf99f25f67e5195a614c4fde21f311d395d7376bab07b1f51fe88bd37b56
|
7
|
+
data.tar.gz: 23eabe3e8b26ba41f9e4ca38f55e8ae48c7362a4658d16d92a9b6284843d55ce34a912c3eae8ad8d312a14457b5ea230cc85422bbde3539c66b3788f1c52ba6d
|
data/README.md
CHANGED
@@ -1,46 +1,202 @@
|
|
1
1
|
# RubyNestNats
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
The `ruby_nest_nats` gem allows you to listen for (and reply to) NATS messages asynchronously in a Ruby application.
|
6
4
|
|
7
5
|
## TODO
|
8
6
|
|
9
|
-
- [
|
7
|
+
- [x] docs
|
10
8
|
- [ ] tests
|
11
|
-
- [ ]
|
9
|
+
- [ ] "controller"-style classes for reply organization
|
10
|
+
- [x] multiple queues
|
12
11
|
- [ ] `on_error` handler so you can send a response (what's standard?)
|
12
|
+
- [ ] config options for URL/host/port/etc.
|
13
13
|
- [ ] config for restart behavior (default is to restart listening on any `StandardError`)
|
14
14
|
|
15
15
|
## Installation
|
16
16
|
|
17
|
-
|
17
|
+
### Locally (to your application)
|
18
|
+
|
19
|
+
Add the gem to your application's `Gemfile`:
|
18
20
|
|
19
21
|
```ruby
|
20
22
|
gem 'ruby_nest_nats'
|
21
23
|
```
|
22
24
|
|
23
|
-
|
25
|
+
...and then run:
|
24
26
|
|
25
|
-
|
27
|
+
```bash
|
28
|
+
bundle install
|
29
|
+
```
|
26
30
|
|
27
|
-
|
31
|
+
### Globally (to your system)
|
28
32
|
|
29
|
-
|
33
|
+
Alternatively, install it globally:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
gem install ruby_nest_nats
|
37
|
+
```
|
30
38
|
|
31
39
|
## Usage
|
32
40
|
|
33
|
-
|
41
|
+
### Logging
|
42
|
+
|
43
|
+
#### Attaching a logger
|
44
|
+
|
45
|
+
Attach a logger to have `ruby_nest_nats` write out logs for messages received, responses sent, errors raised, lifecycle events, etc.
|
46
|
+
|
47
|
+
```rb
|
48
|
+
require 'logger'
|
49
|
+
|
50
|
+
nats_logger = Logger.new(STDOUT)
|
51
|
+
nats_logger.level = Logger::INFO
|
52
|
+
|
53
|
+
RubyNestNats::Client.logger = nats_logger
|
54
|
+
```
|
55
|
+
|
56
|
+
In a Rails application, you might do this instead:
|
57
|
+
|
58
|
+
```rb
|
59
|
+
RubyNestNats::Client.logger = Rails.logger
|
60
|
+
```
|
61
|
+
|
62
|
+
#### Log levels
|
63
|
+
|
64
|
+
The following will be logged at the specified log levels
|
65
|
+
|
66
|
+
- `DEBUG`: Lifecycle events (starting NATS listeners, stopping NATS, reply registration, setting the default queue, etc.), as well as everything under `INFO`, `WARN`, and `ERROR`
|
67
|
+
- `INFO`: Message activity over NATS (received a message, replied with a message, etc.), as well as everything under `WARN` and `ERROR`
|
68
|
+
- `WARN`: Error handled gracefully (listening restarted due to some exception, etc.), as well as everything under `ERROR`
|
69
|
+
- `ERROR`: Some exception was raised in-thread (error in handler, error in subscription, etc.)
|
70
|
+
|
71
|
+
### Setting a default queue
|
72
|
+
|
73
|
+
Set a default queue for subscriptions.
|
74
|
+
|
75
|
+
```rb
|
76
|
+
RubyNestNats::Client.default_queue = "foobar"
|
77
|
+
```
|
78
|
+
|
79
|
+
Leave the `::default_queue` blank (or assign `nil`) to use no default queue.
|
80
|
+
|
81
|
+
```rb
|
82
|
+
RubyNestNats::Client.default_queue = nil
|
83
|
+
```
|
84
|
+
|
85
|
+
### Registering message handlers
|
86
|
+
|
87
|
+
Register a message handler with the `RubyNestNats::Client::reply_to` method. Pass a subject string as the first argument (either a static subject string or a pattern to match more than one subject). Specify a queue (or don't) with the `queue:` option. If you don't provide the `queue:` option, it will be set to the value of `default_queue`, or to `nil` (no queue) if a default queue hasn't been set.
|
88
|
+
|
89
|
+
The result of the given block will be published in reply to the message. The block is passed two arguments when a message matching the subject is received: `data` and `subject`. The `data` argument is the payload of the message (JSON objects/arrays will be parsed into string-keyed `Hash` objects/`Array` objects, respectively). The `subject` argument is the subject of the message received (mostly only useful if a _pattern_ was specified instead of a static subject string).
|
90
|
+
|
91
|
+
```rb
|
92
|
+
RubyNestNats::Client.reply_to("some.subject", queue: "foobar") { |data| "Got it! #{data.inspect}" }
|
93
|
+
|
94
|
+
RubyNestNats::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
|
95
|
+
|
96
|
+
RubyNestNats::Client.reply_to("other.subject") do |data|
|
97
|
+
if data["foo"] == "bar"
|
98
|
+
{ is_bar: "Yep!" }
|
99
|
+
else
|
100
|
+
{ is_bar: "No way!" }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") do
|
105
|
+
"My turn!"
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
### Starting the listeners
|
110
|
+
|
111
|
+
Start listening for messages with the `RubyNestNats::Client::start!` method. This will spin up a non-blocking thread that subscribes to subjects (as specified by invocation(s) of `::reply_to`) and waits for messages to come in. When a message is received, the appropriate `::reply_to` block will be used to compute a response, and that response will be published.
|
112
|
+
|
113
|
+
> **NOTE:** If an error is raised in one of the handlers, `RubyNestNats::Client` will restart automatically.
|
114
|
+
|
115
|
+
```rb
|
116
|
+
RubyNestNats::Client.start!
|
117
|
+
```
|
118
|
+
|
119
|
+
### Full example
|
120
|
+
|
121
|
+
```rb
|
122
|
+
RubyNestNats::Client.logger = Rails.logger
|
123
|
+
RubyNestNats::Client.default_queue = "foobar"
|
124
|
+
|
125
|
+
RubyNestNats::Client.reply_to("some.subject") { |data| "Got it! #{data.inspect}" }
|
126
|
+
RubyNestNats::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
|
127
|
+
RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") { { msg: "My turn!", turn: 5 } }
|
128
|
+
|
129
|
+
RubyNestNats::Client.start!
|
130
|
+
```
|
34
131
|
|
35
132
|
## Development
|
36
133
|
|
134
|
+
### Install dependencies
|
135
|
+
|
136
|
+
To install the Ruby dependencies, run:
|
137
|
+
|
138
|
+
```bash
|
139
|
+
bin/setup
|
140
|
+
```
|
141
|
+
|
142
|
+
This gem also requires a NATS server to be running. See [the NATS documentation](https://docs.nats.io/nats-server/installation) for more details.
|
143
|
+
|
144
|
+
### Open a console
|
145
|
+
|
146
|
+
To open a REPL with the gem's code loaded, run:
|
147
|
+
|
148
|
+
```bash
|
149
|
+
bin/console
|
150
|
+
```
|
151
|
+
|
152
|
+
### Run the tests
|
153
|
+
|
154
|
+
To run the RSpec test suites, run:
|
155
|
+
|
156
|
+
```bash
|
157
|
+
bundle exec rake spec
|
158
|
+
```
|
159
|
+
|
160
|
+
...or (if your Ruby setup has good defaults) just this:
|
161
|
+
|
162
|
+
```bash
|
163
|
+
rake spec
|
164
|
+
```
|
165
|
+
|
166
|
+
### Run the linter
|
167
|
+
|
168
|
+
```bash
|
169
|
+
bundle exec rubocop
|
170
|
+
```
|
171
|
+
|
172
|
+
### Create a release
|
173
|
+
|
174
|
+
Bump the `RubyNestNats::VERSION` value in `lib/ruby_nest_nats/version.rb`, commit, and then run:
|
175
|
+
|
176
|
+
```bash
|
177
|
+
bundle exec rake release
|
178
|
+
```
|
179
|
+
|
180
|
+
...or (if your Ruby setup has good defaults) just this:
|
181
|
+
|
182
|
+
```bash
|
183
|
+
rake release
|
184
|
+
```
|
185
|
+
|
186
|
+
This will:
|
187
|
+
|
188
|
+
1. create a git tag for the new version,
|
189
|
+
1. push the commits,
|
190
|
+
1. build the gem, and
|
191
|
+
1. push it to [rubygems.org](https://rubygems.org/gems/ruby_nest_nats).
|
192
|
+
|
37
193
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
38
194
|
|
39
195
|
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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
40
196
|
|
41
197
|
## Contributing
|
42
198
|
|
43
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
199
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Openbay/ruby_nest_nats.
|
44
200
|
|
45
201
|
## License
|
46
202
|
|
data/lib/ruby_nest_nats.rb
CHANGED
@@ -9,6 +9,7 @@ module RubyNestNats
|
|
9
9
|
class Client
|
10
10
|
class << self
|
11
11
|
def logger=(some_logger)
|
12
|
+
log("Setting the logger to #{some_logger.inspect}")
|
12
13
|
@logger = some_logger
|
13
14
|
end
|
14
15
|
|
@@ -16,82 +17,148 @@ module RubyNestNats
|
|
16
17
|
@logger
|
17
18
|
end
|
18
19
|
|
19
|
-
def
|
20
|
-
|
20
|
+
def default_queue=(some_queue)
|
21
|
+
queue = presence(some_queue.to_s)
|
22
|
+
log("Setting the default queue to #{queue}", level: :debug)
|
23
|
+
@default_queue = queue
|
21
24
|
end
|
22
25
|
|
23
|
-
def
|
24
|
-
@
|
25
|
-
end
|
26
|
-
|
27
|
-
def queue
|
28
|
-
@queue
|
29
|
-
end
|
30
|
-
|
31
|
-
def replies
|
32
|
-
@replies ||= []
|
26
|
+
def default_queue
|
27
|
+
@default_queue
|
33
28
|
end
|
34
29
|
|
35
30
|
def started?
|
36
31
|
@started ||= false
|
37
32
|
end
|
38
33
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
if started?
|
43
|
-
raise StandardError, "NATS already started"
|
44
|
-
elsif !block_given?
|
45
|
-
raise ArgumentError, "Response block must be provided"
|
46
|
-
elsif replies.any? { |reply| reply[:subject] == subject }
|
47
|
-
raise ArgumentError, "Already registered a reply to #{subject}"
|
48
|
-
end
|
34
|
+
def stopped?
|
35
|
+
!started?
|
36
|
+
end
|
49
37
|
|
50
|
-
|
51
|
-
|
38
|
+
def reply_to(subject, queue: nil, &block)
|
39
|
+
subject = subject.to_s
|
40
|
+
queue = (presence(queue) || default_queue).to_s
|
41
|
+
log("Registering a reply handler for subject '#{subject}'#{" in queue '#{queue}'" if queue}", level: :debug)
|
42
|
+
register_reply!(subject: subject, handler: block, queue: queue)
|
52
43
|
end
|
53
44
|
|
54
45
|
def listen
|
55
46
|
NATS.start do
|
56
47
|
replies.each do |replier|
|
48
|
+
log("Subscribing to subject '#{replier[:subject]}'#{" in queue '#{replier[:queue]}'" if replier[:queue]}", level: :debug)
|
49
|
+
|
57
50
|
NATS.subscribe(replier[:subject], queue: replier[:queue]) do |message, inbox, subject|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
51
|
+
parsed_message = JSON.parse(message)
|
52
|
+
id, data, pattern = parsed_message.values_at("id", "data", "pattern")
|
53
|
+
|
54
|
+
log("Received a message!")
|
55
|
+
message_desc = <<~LOG_MESSAGE
|
56
|
+
id: #{id || '(none)'}
|
57
|
+
pattern: #{pattern || '(none)'}
|
58
|
+
subject: #{subject || '(none)'}
|
59
|
+
data: #{data.to_json}
|
60
|
+
inbox: #{inbox || '(none)'}
|
61
|
+
LOG_MESSAGE
|
62
|
+
log(message_desc, indent: 2)
|
63
|
+
|
64
|
+
response_data = replier[:handler].call(data)
|
65
|
+
|
66
|
+
log("Responding with '#{response_data}'")
|
67
|
+
|
68
|
+
NATS.publish(inbox, response_data.to_json, queue: replier[:queue])
|
62
69
|
end
|
63
70
|
end
|
64
71
|
end
|
65
72
|
end
|
66
73
|
|
67
74
|
def stop!
|
68
|
-
log("Stopping NATS")
|
69
|
-
NATS.stop
|
70
|
-
|
75
|
+
log("Stopping NATS", level: :debug)
|
76
|
+
NATS.stop rescue nil
|
77
|
+
stopped!
|
71
78
|
end
|
72
79
|
|
73
80
|
def restart!
|
74
|
-
log("Restarting NATS
|
81
|
+
log("Restarting NATS", level: :warn)
|
75
82
|
stop!
|
76
83
|
start!
|
77
84
|
end
|
78
85
|
|
79
86
|
def start!
|
80
|
-
log("Starting NATS")
|
81
|
-
return log("NATS is already running") if started?
|
87
|
+
log("Starting NATS", level: :debug)
|
82
88
|
|
83
|
-
|
89
|
+
if started?
|
90
|
+
log("NATS is already running", level: :debug)
|
91
|
+
return
|
92
|
+
end
|
93
|
+
|
94
|
+
started!
|
84
95
|
|
85
96
|
Thread.new do
|
86
97
|
Thread.handle_interrupt(StandardError => :never) do
|
87
98
|
begin
|
88
99
|
Thread.handle_interrupt(StandardError => :immediate) { listen }
|
89
|
-
|
100
|
+
rescue => e
|
101
|
+
log("Encountered an error:", level: :error)
|
102
|
+
log(e.full_message, level: :error, indent: 2)
|
103
|
+
|
90
104
|
restart!
|
105
|
+
raise e
|
91
106
|
end
|
92
107
|
end
|
93
108
|
end
|
94
109
|
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
def log(text, level: :info, indent: 0)
|
114
|
+
return unless logger
|
115
|
+
|
116
|
+
timestamp = Time.now.to_s
|
117
|
+
text_lines = text.split("\n")
|
118
|
+
indentation = indent.is_a?(String) ? indent : (" " * indent)
|
119
|
+
|
120
|
+
text_lines.each do |line|
|
121
|
+
logger.send(level, "[#{timestamp}] RubyNestNats | #{indentation}#{line}")
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def started!
|
126
|
+
@started = true
|
127
|
+
end
|
128
|
+
|
129
|
+
def stopped!
|
130
|
+
@started = false
|
131
|
+
end
|
132
|
+
|
133
|
+
def replies
|
134
|
+
@replies ||= []
|
135
|
+
end
|
136
|
+
|
137
|
+
def reply_registered?(raw_subject)
|
138
|
+
subject = raw_subject.to_s
|
139
|
+
replies.any? { |reply| reply[:subject] == subject }
|
140
|
+
end
|
141
|
+
|
142
|
+
def register_reply!(subject:, handler:, queue: nil)
|
143
|
+
raise StandardError, "NATS already started" if started? # TODO: remove when runtime additions are implemented
|
144
|
+
raise ArgumentError, "Subject must be a string" unless subject.is_a?(String)
|
145
|
+
raise ArgumentError, "Must provide a message handler for #{subject}" unless handler.respond_to?(:call)
|
146
|
+
raise ArgumentError, "Already registered a reply to #{subject}" if reply_registered?(subject)
|
147
|
+
|
148
|
+
replies << { subject: subject, handler: handler, queue: presence(queue) || default_queue }
|
149
|
+
end
|
150
|
+
|
151
|
+
def blank?(value)
|
152
|
+
value.respond_to?(:empty?) ? value.empty? : !value
|
153
|
+
end
|
154
|
+
|
155
|
+
def present?(value)
|
156
|
+
!blank?(value)
|
157
|
+
end
|
158
|
+
|
159
|
+
def presence(value)
|
160
|
+
present?(value) ? value : nil
|
161
|
+
end
|
95
162
|
end
|
96
163
|
end
|
97
164
|
end
|