meskyanichi/msngr

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# Msngr

[![Gem Version](https://badge.fury.io/rb/msngr.svg)](http://badge.fury.io/rb/msngr)
[![Test
Status](https://github.com/mrrooijen/msngr/workflows/Test/badge.svg)](https://github.com/mrrooijen/msngr/actions)
[![Code Climate](https://codeclimate.com/github/mrrooijen/msngr.png)](https://codeclimate.com/github/mrrooijen/msngr)

A light-weight Ruby library for multi-threaded Ruby applications that allows threads to share a single service connection for more efficient messaging.

This library was sponsored by [HireFire].

The documentation can be found on [RubyDoc].


### Compatibility

- Ruby (MRI) 2.4+


### Installation

Add the gem to your Gemfile and run `bundle`.

```rb
gem "msngr"
```


## Usage

Consider a Rails 4 application with websocket support using Rack Hijack through [Tubesock], and you want to use the Redis service as a message queue.

*Note: This gem isn't Rails- or Tubesock-specific. This is just an example.*

```rb
# In an initializer
REDIS = Redis.new

# A controller
class MainController < ApplicationController
  include Tubesock::Hijack

  def connection
    hijack do |websocket|
      redis = Thread.new do
        Redis.new.subscribe("chatroom") do |on|
          on.message { |_, message| websocket.send_data(message) }
        end
      end

      websocket.onmessage { |message| REDIS.publish("chatroom", message) }
      websocket.onclose { redis.kill }
    end
  end
end
```

The above would work, however each web socket connection would require:

* A Ruby Thread for the web socket connection (Puma App Server)
* A Ruby Thread for the Hijack (Tubesock)
* A Ruby Thread for the Redis Connection to allow the blocking subscribe operation
* A Redis Connection to subscribe

Now consider the following setup with Msngr:

```rb
# In an initializer
require "msngr/clients/redis"

client    = Msngr::Clients::Redis.new
MESSENGER = Msngr.new(client).tap(&:listen!)
REDIS     = Redis.new

# A controller
class MainController < ApplicationController
  include Tubesock::Hijack

  def connection
    hijack do |websocket|
      receiver = MESSENGER.subscribe(/chatroom/)

      websocket.onopen do
        receiver.on_message { |message| websocket.send_data(message) }
        receiver.on_unsubscribe { REDIS.publish("chatroom", "You left the chat.") }
      end

      websocket.onmessage { |message| REDIS.publish("chatroom", message) }
      websocket.onclose { MESSENGER.unsubscribe(receiver) }
    end
  end
end
```

For each web socket connection with this setup the resource requirements are:

* A Ruby Thread for the web socket connection (Puma App Server)
* A Ruby Thread for the Hijack (Tubesock)

This means that each request will require 2 Ruby Threads (instead of 3), and no additional Redis connections.

## Explanation

This part:

```rb
require "msngr/clients/redis"

client    = Msngr::Clients::Redis.new
MESSENGER = Msngr.new(client).tap(&:listen!)
REDIS     = Redis.new
```

The `client` is an interface object that acts as a Redis client. This will use a single Redis connection, and will be the only Redis connection receiving message from an external Redis server. You can also implement a different interface for your favorite message queue system and pass it in to the Messenger object to start receiving messages from that system.

The `MESSENGER` object is what drains all the messages from the `client` and will use a Ruby Regular Expression to match event patterns to figure out to which `Receiver` instance the message should be dispatched to, using Procs as callbacks.

The `REDIS` object is just a regular Redis object which we can use to publish messages.

Now in the `connection` action inside the `MainController` we have the following:

```rb
hijack do |websocket|
  receiver = MESSENGER.subscribe(/chatroom/)

  websocket.onopen do
    receiver.on_message { |message| websocket.send_data(message) }
    receiver.on_unsubscribe { REDIS.publish("chatroom", "You left the chat.") }
  end

  websocket.onmessage { |message| REDIS.publish("chatroom", message) }
  websocket.onclose { MESSENGER.unsubscribe(receiver) }
end
```

The `receiver` is the result of a new (local) subscription created by the `MESSENGER`. If an incoming event matches the `/chatroom/` pattern, then the `receiver`'s `on_message` callback will be called with the `message` passed in to it. A single `MESSENGER` can (and should) have multiple receivers, which is what happens as a new `receiver` is created for each websocket connection, sharing the same single Redis connnection through `MESSENGER`.

The `receiver.on_unsubscribe` callback can be defined to notifiy the `receiver` that it has been unsubscribed and will no longer receive messages from the `MESSENGER`. You'll want to make sure you unsubscribe all `receiver`s that are no longer used, otherwise this'll cause memory leaks and make your application run slow as the registry will fill up and increase look-up times.


## Creating your own Client

You can simply copy/paste/modify the `lib/msngr/clients/redis.rb` file and implement your own client. All you need to do is make sure the class implements the `on_message` method which should yield the name of the event and the message.

Example from `lib/msngr/clients/redis.rb`:

```rb
def on_message
  connection.psubscribe("*") do |on|
    on.pmessage { |_, event, message| yield event, message }
  end
end
```

This is the only required method to implement a compatible client.


## Try it out!

Be sure you have a Redis server running on your local machine, and do the following:

```
git clone https://github.com/mrrooijen/msngr.git
cd msngr
bundle
pry ./examples/redis.rb
```

This'll open an interactive shell with 4 receivers so you can play with the `r1`, `r2`, `r3`, `r4`, `redis`, and `messenger` variables.


### Contributing

Contributions are welcome, but please conform to these requirements:

- Ruby (MRI) 2.4+
- 100% Spec Coverage
  - Generated by when running the test suite
- 100% [Passing Specs]
  - Run test suite with `$ rspec spec`
- 4.0 [Code Climate Score]
  - Run `$ rubycritic lib` to generate the score locally and receive tips
  - No code smells
  - No duplication

To start contributing, fork the project, clone it, and install the development dependencies:

```
git clone git@github.com:USERNAME/msngr.git
cd msngr
bundle
```

Ensure that everything works:

```
rspec spec
rubycritic lib
```

To run the local documentation server:

```
yard server --reload
```

Create a new branch and start hacking:

```
git checkout -b my-contributions
```

Submit a pull request.


### Author / License

Released under the [MIT License] by [Michael van Rooijen].

[Michael van Rooijen]: https://twitter.com/mrrooijen
[HireFire]: http://hirefire.io
[Passing Specs]: https://travis-ci.org/mrrooijen/msngr
[Code Climate Score]: https://codeclimate.com/github/mrrooijen/msngr
[RubyDoc]: http://rubydoc.info/github/mrrooijen/msngr/master/frames
[MIT License]: https://github.com/mrrooijen/msngr/blob/master/LICENSE
[RubyGems.org]: https://rubygems.org/gems/msngr
[Tubesock]: https://github.com/ngauthier/tubesock