Skip to content

paddor/nanowire

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nanowire

Gem Version License: MIT Ruby

One API for NNG and ZeroMQ — swap messaging backends without changing application code.

Like ActiveRecord abstracts databases, Nanowire abstracts messaging libraries.


Why

  • Migrating from ZMQ to NNG (or vice-versa) — change one line (Nanowire.backend = :nng), not every socket call in your codebase.
  • Hedging your bets — evaluate both backends under real workloads before committing to one.
  • Writing libraries — gem authors can offer messaging support without forcing a backend choice on users.
  • Teaching — demonstrate messaging patterns once, run them on either stack.

NNG and ZMQ use incompatible wire protocols — they cannot talk to each other. Nanowire unifies the Ruby API, not the wire format.

Install

You need at least one backend installed:

# NNG backend
gem install nng        # + system: apt install libnng-dev / brew install nng

# ZMQ backend
gem install cztop      # + system: apt install libczmq-dev / brew install czmq

Then add Nanowire:

gem install nanowire
# or in Gemfile
gem 'nanowire'

Quick Start

Backend selection

require 'nanowire'

# Pick one:
Nanowire.backend = :nng                # in code
# ENV['NANOWIRE_BACKEND'] = 'zmq'     # via environment
# Nanowire::Req.new(backend: :zmq)    # per socket

Request / Reply

require 'nanowire'
require 'async'

Sync do |task|
  rep = Nanowire::Rep.new
  rep.listen('tcp://127.0.0.1:5555')

  req = Nanowire::Req.new
  req.dial('tcp://127.0.0.1:5555')

  task.async do
    msg = rep.receive
    rep.send("re: #{msg.body}")
  end

  req.send('hello')
  puts req.receive.body  # => "re: hello"
end

Pub / Sub

Sync do |task|
  pub = Nanowire::Pub.new
  pub.listen('ipc:///tmp/pubsub.sock')

  weather = Nanowire::Sub.new(prefix: 'weather.')
  weather.dial('ipc:///tmp/pubsub.sock')

  sleep 0.05

  task.async do
    pub.send('weather.rain')
    pub.send('sports.goal')
  end

  puts weather.receive.body  # => "weather.rain"
  # "sports.goal" is filtered out
end

Runtime topic management:

sub.subscribe('sports.')
sub.unsubscribe('weather.')

Push / Pull (Pipeline)

Sync do |task|
  pull = Nanowire::Pull.new
  pull.listen('tcp://127.0.0.1:5556')

  push = Nanowire::Push.new
  push.dial('tcp://127.0.0.1:5556')

  task.async { push.send('work item') }
  puts pull.receive.body  # => "work item"
end

Pair

Sync do |task|
  a = Nanowire::Pair.new
  a.listen('ipc:///tmp/pair.sock')

  b = Nanowire::Pair.new
  b.dial('ipc:///tmp/pair.sock')

  task.async { a.send('ping') }
  puts b.receive.body  # => "ping"

  task.async { b.send('pong') }
  puts a.receive.body  # => "pong"
end

API

Socket lifecycle

Method Description
#listen(url) Bind/listen on an address (returns self)
#dial(url) Connect to an address (returns self)
#send(data) Send a string (returns self)
#receive Receive a Nanowire::Message
#close Close the socket

Timeouts

req.recv_timeout = 5.0   # seconds (Float), nil = no timeout
req.send_timeout = 2.0

Async / fiber support

req.wait_readable(timeout)   # poll for readability
req.wait_writable(timeout)   # poll for writability

Both backends support FD-based IO waiting for integration with async schedulers.

Message

msg = req.receive
msg.body    # => String (binary-safe)
msg.to_s    # => alias for body
msg.to_str  # => implicit String coercion (works with interpolation, puts, etc.)

Native escape hatch

req.native  # => NNG::Socket::Req0 or CZTop::Socket::REQ

Drop down to the underlying socket when you need backend-specific features like TLS, raw mode, or ROUTER/DEALER.

Errors

Nanowire NNG ZMQ
Nanowire::TimeoutError NNG::Error::TimedOut IO::TimeoutError
Nanowire::ClosedError NNG::Error::ObjectClosed SystemCallError
Nanowire::StateError NNG::Error::IncorrectState ArgumentError

Supported patterns

Pattern Classes Direction
Request/Reply Req, Rep bidirectional
Publish/Subscribe Pub, Sub unidirectional
Pipeline Push, Pull unidirectional
Exclusive pair Pair bidirectional

Not abstracted (use #native)

Some features are too different across backends or exist in only one:

  • ROUTER/DEALER (ZMQ) vs raw mode (NNG)
  • TLS, pipe introspection, abstract:// (NNG-only)
  • XPUB/XSUB, STREAM (ZMQ-only)
  • Survey/Respondent, Bus (NNG-only)

Performance

NNG vs ZMQ through the Nanowire abstraction layer. Ruby 4.0.1, benchmark-ips (warmup: 1, time: 3):

Throughput (push/pull, messages/second)

ipc tcp
NNG async 11.5k 7.3k
ZMQ async 12.1k 12.0k
NNG threads 11.4k 6.2k
ZMQ threads 13.4k 9.9k

Latency (req/rep roundtrip)

ipc tcp
NNG async 263 µs 246 µs
ZMQ async 147 µs 156 µs
NNG threads 117 µs 173 µs
ZMQ threads 200 µs 222 µs

ZMQ has higher throughput (especially for larger messages). NNG wins threaded latency. See bench/ for full results and scripts.

Migration guide

Switching a cztop codebase to Nanowire:

# Before (ZMQ-specific)
req = CZTop::Socket::REQ.new
req.connect('tcp://127.0.0.1:5555')
req << 'hello'
parts = req.receive          # => Array<String>
puts parts.last

# After (backend-agnostic)
req = Nanowire::Req.new      # backend: :zmq or :nng
req.dial('tcp://127.0.0.1:5555')
req.send('hello')
msg = req.receive            # => Nanowire::Message
puts msg.body

Key differences:

  • bind / connect become listen / dial (NNG terminology, clearer semantics)
  • << becomes #send (no operator overloading)
  • receive returns a Message object, not Array<String>
  • Timeouts are always in seconds (Float), never milliseconds

Custom adapters

Nanowire ships with NNG and ZMQ adapters, but the adapter interface is a simple duck type — any class that implements initialize, listen, dial, send, receive, close, timeout accessors, and wrap_error can be registered:

Nanowire::Adapter.register(:my_backend, MyAdapter)

sock = Nanowire::Req.new(backend: :my_backend)

The landscape of broker-less messaging libraries with ZMQ-style scalability protocols is small (realistically: ZMQ, NNG, and nanomsg classic), so this is less about a plugin ecosystem and more about keeping the door open — for in-process test fakes, for nanomsg legacy codebases, or for whatever comes next.

Development

bundle install
bundle exec rake test          # runs against both backends
bundle exec ruby bench/async/latency.rb

License

MIT

About

One API for NNG and ZMQ

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages