Project

happn

0.0
Low commit activity in last 3 years
Happn connects a RabbitMQ topic exchange and consumes CREPE events sequentially. It lets developers declare "projectors" that match events on their emitter, kind, name and status, and binds its queue to the exchange according to those matchers.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

>= 1.17
>= 13.0
~> 3.0

Runtime

 Project Readme

Gem for happn

Happn connects a RabbitMQ exchange and listens for CREPE events (possibly generated using flu-rails) sequentially. Happn helps developers to create "Projectors" that define how to match and consume events.

This gem connects a single RabbitMQ queue and bind it automatically to its exchange. These bindings are defined by developers through "matchers" when loading projectors.

Requirements

  • Ruby >= 3.2
  • Tested with RabbitMQ 3.5.8
  • happn-ruby works with or without Rails (tested with Rails 4 and 5).

Installation

Add the gem to your project's Gemfile:

gem "happn"

Then, configure Happn. If you use Rails, you can create an initializer into your Rails app (config/initializers/happn.rb). This code can be called anywhere before starting Happn.init.

Happn.configure do |config|
  config.logger                     = Rails.logger
  config.rabbitmq_host              = ENV["RABBITMQ_HOST"]
  config.rabbitmq_port              = ENV["RABBITMQ_PORT"]
  config.rabbitmq_management_port   = ENV["RABBITMQ_MANAGEMENT_PORT"]
  config.rabbitmq_user              = ENV["RABBITMQ_USER"]
  config.rabbitmq_password          = ENV["RABBITMQ_PASSWORD"]
  config.rabbitmq_queue_name        = ENV["CONSUMER_QUEUE_NAME"]
  config.rabbitmq_exchange_name     = ENV["RABBITMQ_EXCHANGE_NAME"]
  config.rabbitmq_exchange_durable  = ENV["RABBITMQ_EXCHANGE_DURABLE"] == "true"
  config.projector_classes          = [LoggerProjector]
  config.bunny_options              = {}
end

Each configuration is detailed below.

About queues

  • Happn consumes a single queue through the RabbitMQ's Topic Exchange Model.
  • If the queue does not exist when Happn starts, it is created automatically.
  • When connecting a queue, please be careful that each connection parameter must match the existing queue's parameters. For instance, the value of x-queue-mode must match to avoid a PRECONDITION FAILED error.
  • All bindings between queues and their exchange are reset when starting Happn. Based on all the projectors that have been registered (option projector_classes), Happn detects which events must be consumed and binds its queue to the exchange depending on these event matchers.

About projectors

A projector:

  • defines one or multiple matchers to detect which events must be consumed. Matchers can be declared using 4 event properties
    • emitter: e.g. Facebook or MyInternalApi (:all or no value mean "all emitters")
    • kind: e.g. entity_change or kind (:all or no value mean "all kinds")
    • name: e.g. create country or request to destroy bunnies (:all or no value mean "all names")
    • status: e.g. :new or :replayed (:all or no value mean "all statuses")
  • defines how to consume these events.

When a projector raises an Error, Happn stops.

Usage

Start Up

Starting Happn consumes events sequentially. For instance, it can be started from a Rake tasks:

namespace :events do
  desc "Listen all events and consume them."
  task consume: :environment do
    Happn.init
    Happn.start
  end
end

Define a Projector

A projector is a class that defines how to consume one or multiple types of events. This class must:

  • extend Happn::Projector.
  • use its on method to declare which events to match and how to consume them. This must be done in a define_handlers method.
class LoggerProjector < Happn::Projector
  def define_handlers
    on emitter: "MyApplication", name: "create country" do |event|
      Rails.logger("A country has been created and generated an event with id #{event.id}")
    end

    on kind: "request", status: :new do |event|
      Rails.logger("This is a new request to the controller: #{event.data["controller_name"]}")
    end
  end
end

Registering all projectors automatically

If all subclass of Happn::Projector should be registered seamlessly, the configuration can declare the following code (using Rails):

Happn.configure do |config|
  Rails.application.eager_load!
  config.projector_classes = Happn::Projector.descendants
end

When Zeitwerk is defined and when Rails.application.eager_load! returns false, you should call Zeitwerk::Loader.eager_load_all.

Running the tests

The test suite is written with RSpec and needs no running RabbitMQ: the broker is stubbed.

bundle install
bundle exec rake     # or: bundle exec rspec

Running the integration tests

The test suite above needs no running RabbitMQ. A second suite, under spec_integration/, exercises Happn against a real broker instead: queue and binding lifecycle, message routing, consumption and acknowledgement, and the reject-and-exit contract when a projector handler raises.

docker compose up -d --wait   # starts RabbitMQ with the management plugin
bundle exec rake integration  # or: bundle exec rspec spec_integration
docker compose down

Overall configuration options

All options have a default value. However, all of them can be changed in your Happn.configure block.

Option Default Value Type Required? Description Example
logger Logger.new(STDOUT) Logger Optional The logger used by happn Rails.logger
rabbitmq_host "localhost" String Required RabbitMQ exchange's host. "192.168.42.42"
rabbitmq_port 5672 Integer Required RabbitMQ exchange's port. 1234
rabbitmq_user "" String Required RabbitMQ exchange's username. "root"
rabbitmq_password "" String Required RabbitMQ exchange's password. "pouet"
rabbitmq_exchange_name "events" String Required RabbitMQ exchange's name. "myproject"
rabbitmq_management_scheme "http" String Required RabbitMQ exchange's management scheme. This scheme is used when happn must access metadata information about queues, messages, etc. This port is used to create/delete bindings between the queue and its exchange. "https"
rabbitmq_management_port 15672 Integer Required RabbitMQ exchange's management port. This port is used when happn must access metadata information about queues, messages, etc. This port is used to create/delete bindings between the queue and its exchange. 15671
rabbitmq_queue_name "happn-queue" String Required The RabbitMQ queue to create, bind and consume. If the queue does not exist, it will be created at startup. "my-queue"
rabbitmq_exchange_durable true Boolean Optional Make the RabbitMQ's exchange durable or not. From RabbitMQ's documentation: "Durable exchanges survive broker restart whereas transient exchanges do not (they have to be redeclared when broker comes back online)." false
rabbitmq_queue_mode nil String Optional When creating the queue, this option can be passed to set x-queue-mode. For instance, a queue can be made "lazy" by passing "lazy" as a value. See RabbitMQ's documentation for more details. lazy
rabbitmq_prefetch_size 10 Integer Optional Also known as RabbitMQ's QOS. From the RabbitMQ's documentation: "AMQP specifies the basic.qos method to allow you to limit the number of unacknowledged messages on a channel (or connection) when consuming (aka "prefetch count")." 1000
projector_classes [] Array of constants Required All Projector classes to register. This value can be generated by reading all descendant classes from Happn::Projector. [MyProjector]
bunny_options {} Hash of symbols Optional Additional options to add when connecting the RabbitMQ broker. This overrides the existing options with the same name. { verify_peer: true }
management_options {} Hash of symbols Optional Additional options to add when accessing the RabbitMQ Managmement. This overrides the existing options with the same name. The options are defined at https://github.com/ruby-amqp/rabbitmq_http_api_client { verify: false }
on_error nil block with an argument exception false When the consumption of an event raises an Error, the consumption exits. However, this block can be called before exiting the consumption execution. `lambda {

How to release a new version

Releases are published to RubyGems by GitHub Actions (.github/workflows/release.yml), through Trusted Publishing: no API key is stored in this repository, the workflow exchanges a short-lived GitHub OIDC token for a scoped RubyGems credential.

  1. Update Happn::VERSION in lib/happn/version.rb and the CHANGELOG.md
  2. Commit and push these changes to main
  3. Tag the commit and push the tag:
  $ git tag -a v1.1.7 -m "Version 1.1.7"
  $ git push origin v1.1.7

The workflow then checks that the tag matches Happn::VERSION, runs the tests, builds the gem and pushes it. It only publishes tags starting with v.