RubyNestNats
The ruby_nest_nats
gem allows you to listen for (and reply to) NATS messages asynchronously in a Ruby application.
TODO
- [x] docs
- [ ] tests
- [x] "controller"-style classes for reply organization
- [x] runtime subscription additions
- [x] multiple queues
- [ ]
on_error
handler so you can send a response (what's standard?) - [ ] config options for URL/host/port/etc.
- [ ] config for restart behavior (default is to restart listening on any
StandardError
) - [ ] consider using child processes instead of threads
Installation
Locally (to your application)
Add the gem to your application's Gemfile
:
gem 'ruby_nest_nats'
...and then run:
bundle install
Globally (to your system)
Alternatively, install it globally:
gem install ruby_nest_nats
NATS server (important!)
This gem also requires a NATS server to be installed and running before use. See the NATS documentation for more details.
Usage
Starting the NATS server
You'll need to start a NATS server before running your Ruby application. If you installed it via Docker, you might start it like so:
docker run -p 4222:4222 -p 8222:8222 -p 6222:6222 -ti nats:latest
NOTE: You may need to run that command with
sudo
on some systems, depending on the permissions of your Docker installation.NOTE: For other methods of running a NATS server, see the NATS documentation.
Logging
Attaching a logger
Attach a logger to have ruby_nest_nats
write out logs for messages received, responses sent, errors raised, lifecycle events, etc.
require 'ruby_nest_nats'
require 'logger'
nats_logger = Logger.new(STDOUT)
nats_logger.level = Logger::INFO
RubyNestNats::Client.logger = nats_logger
In a Rails application, you might do this instead:
RubyNestNats::Client.logger = Rails.logger
Log levels
The following will be logged at the specified log levels
DEBUG
: Lifecycle events (starting NATS listeners, stopping NATS, reply registration, setting the default queue, etc.), as well as everything underINFO
,WARN
, andERROR
INFO
: Message activity over NATS (received a message, replied with a message, etc.), as well as everything underWARN
andERROR
WARN
: Error handled gracefully (listening restarted due to some exception, etc.), as well as everything underERROR
ERROR
: Some exception was raised in-thread (error in handler, error in subscription, etc.)
Setting a default queue
Set a default queue for subscriptions.
RubyNestNats::Client.default_queue = "foobar"
Leave the ::default_queue
blank (or assign nil
) to use no default queue.
RubyNestNats::Client.default_queue = nil
Registering message handlers
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.
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).
RubyNestNats::Client.reply_to("some.subject", queue: "foobar") { |data| "Got it! #{data.inspect}" }
RubyNestNats::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
RubyNestNats::Client.reply_to("other.subject") do |data|
if data["foo"] == "bar"
{ is_bar: "Yep!" }
else
{ is_bar: "No way!" }
end
end
RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") do
"My turn!"
end
Starting the listeners
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.
RubyNestNats::Client.start!
NOTE: If an error is raised in one of the handlers,
RubyNestNats::Client
will restart automatically.NOTE: You can invoke
::reply_to
to create additional message subscriptions afterRubyNestNats::Client.start!
, but be aware that this forces the client to restart. You may see (benign, already-handled) errors in the logs generated when this restart happens. It will force the client to restart and re-subscribe after each additional::reply_to
invoked after::start!
. So, if you have a lot of additional::reply_to
invocations, you may want to consider refactoring so that your call toRubyNestNats::Client.start!
occurs after those additions.NOTE: The
::start!
method can be safely called multiple times; only the first will be honored, and any subsequent calls to::start!
after the client is already started will do nothing (except write a "NATS is already running" log to the logger at theDEBUG
level).
Basic full working example (in vanilla Ruby)
The following should be enough to start a ruby_nest_nats
setup in your Ruby application, using what we've learned so far.
NOTE: For a more organized structure and implementation in a larger app (like a Rails project), see the "controller" section below.
require 'ruby_nest_nats'
require 'logger'
nats_logger = Logger.new(STDOUT)
nats_logger.level = Logger::DEBUG
RubyNestNats::Client.logger = nats_logger
RubyNestNats::Client.default_queue = "foobar"
RubyNestNats::Client.reply_to("some.subject") { |data| "Got it! #{data.inspect}" }
RubyNestNats::Client.reply_to("some.*.pattern") { |data, subject| "Got #{data} on #{subject}" }
RubyNestNats::Client.reply_to("subject.in.queue", queue: "barbaz") { { msg: "My turn!", turn: 5 } }
RubyNestNats::Client.start!
Creating "controller"-style classes for listener organization
Create controller classes which inherit from RubyNestNats::Controller
in order to give your message listeners some structure.
Use the ::default_queue
macro to set a default queue string. If omitted, the controller will fall back on the global default queue assigned with RubyNestNats::Client::default_queue=
(as described here). If no default queue is set in either the controller or globally, then the default queue will be blank. Set the default queue to nil
in a controller to override the global default queue and explicitly make the default queue blank for that controller.
Use the ::subject
macro to create a block for listening to that subject segment. Nested calls to ::subject
will append each subsequent subject/pattern string to the last (joined by a periods). There is no limit to the level of nesting.
You can register a response for the built-up subject/pattern string using the ::response
macro. Pass a block to ::response
which optionally takes two arguments (the same arguments supplied to the block of RubyNestNats::Client::reply_to
). The result of that block will be sent as a response to the message received.
class HelloController < RubyNestNats::Controller
default_queue "foobar"
subject "hello" do
subject "jerk" do
response do |data|
# The subject at this point is "hello.jerk"
"Hey #{data['name']}... that's not cool, man."
end
end
subject "and" do
subject "wassup" do
response do |data|
# The subject at this point is "hello.and.wassup"
"Hey, how ya doin', #{data['name']}?"
end
end
subject "goodbye" do
response do |data|
# The subject at this point is "hello.and.goodbye"
"Hi #{data['name']}! But also GOODBYE."
end
end
end
end
subject "hows" do
subject "*" do
subject "doing" do
response do |data, subject|
# The subject at this point is "hows.<wildcard>.doing" (i.e., the
# subjects "hows.jack.doing" and "hows.jill.doing" will both match)
sender_name = data["name"]
other_person_name = subject.split(".")[1]
desc = rand < 0.5 ? "terribly" : "great"
"Well, #{sender_name}, #{other_person_name} is actually doing #{desc}."
end
end
end
end
end
NOTE: If you implement controllers like this and you are using code-autoloading machinery (like Zeitwerk in Rails), you will need to make sure these paths are eager-loaded when your app starts. If you don't,
ruby_nest_nats
will not register the listeners, and will not respond to messages for the specified subjects.For example: in a Rails project (assuming you have your NATS controllers in a directory called
app/nats/
), you may want to put something like the following in an initializer (such asconfig/initializers/nats.rb
):RubyNestNats::Client.logger = Rails.logger RubyNestNats::Client.default_queue = "foobar" # ... Rails.application.config.after_initialize do nats_controller_paths = Dir[Rails.root.join("app", "nats", "**", "*_controller.rb")] nats_controller_paths.each { |file_path| require_dependency(file_path) } RubyNestNats::Client.start! end
Development
Install dependencies
To install the Ruby dependencies, run:
bin/setup
This gem also requires a NATS server to be installed and running. See the NATS documentation for more details.
Open a console
To open a REPL with the gem's code loaded, run:
bin/console
Run the tests
To run the RSpec test suites, run:
bundle exec rake spec
...or (if your Ruby setup has good defaults) just this:
rake spec
Run the linter
bundle exec rubocop
Create a release
Bump the RubyNestNats::VERSION
value in lib/ruby_nest_nats/version.rb
, commit, and then run:
bundle exec rake release
...or (if your Ruby setup has good defaults) just this:
rake release
This will:
- create a git tag for the new version,
- push the commits,
- build the gem, and
- push it to rubygems.org.
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.
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.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/Openbay/ruby_nest_nats.
License
The gem is available as open source under the terms of the MIT License.