GithubHelp home page GithubHelp logo

otobus / event_bus Goto Github PK

View Code? Open in Web Editor NEW
693.0 12.0 40.0 425 KB

:surfer: Traceable, extendable and minimalist **event bus** implementation for Elixir with built-in **event store** and **event watcher** based on ETS.

Home Page: https://hexdocs.pm/event_bus

License: MIT License

Elixir 100.00%
elixir eventbus event-sourcing eventstore message-bus instrumentation

event_bus's Introduction

EventBus

Build Status Module Version Hex Docs Total Download License Last Updated

Traceable, extendable and minimalist event bus implementation for Elixir with built-in event store and event watcher based on ETS.

Event Bus

Table of Contents

Features

Getting Started

Installation

Usage

Sample Subscriber Implementation

Event Storage Details

Traceability

EventBus.Metrics and UI

Documentation

Addons

Wiki

Contributing

License

Code of Conduct

Questions

Features

  • Fast data writes with enabled concurrent writes to ETS.

  • Fast data reads with enabled concurrent reads from ETS.

  • Fast by design. Almost all implementation data accesses have O(1) complexity.

  • Memory friendly. Instead of pushing event data, pushes event shadow(event id and topic) to only interested subscribers.

  • Applies queueing theory to handle inputs.

  • Extendable with addons.

  • Traceable with optional attributes. Optional attributes compatible with opentracing platform.

  • Minimal with required attributes(In case, you want it work minimal use 3 required attributes to deliver your events).

Getting Started

Start using event_bus library in five basic steps:

Installation

The package can be installed by adding :event_bus to your list of dependencies in mix.exs:

def deps do
  [
    {:event_bus, "~> 1.7.0"}
  ]
end

Be sure to include event_bus in your mix.exs Mixfile:

def application do
  [
    applications: [
      # ...
      :event_bus
    ]
  ]
end

Usage

Register event topics in config.exs
config :event_bus, topics: [:message_received, :another_event_occurred]
Register/unregister event topics on demand
# register
EventBus.register_topic(:webhook_received)
> :ok

# unregister topic
# Warning: It also deletes the related topic tables!
EventBus.unregister_topic(:webhook_received)
> :ok
Subscribe to the 'event bus' with a subscriber and list of given topics, Notification Manager will match with Regex
# to catch every event topic
EventBus.subscribe({MyEventSubscriber, [".*"]})
> :ok

# to catch specific topics
EventBus.subscribe({MyEventSubscriber, ["purchase_", "booking_confirmed$", "flight_passed$"]})
> :ok

# if your subscriber has a config
config = %{}
subscriber = {MyEventSubscriber, config}
EventBus.subscribe({subscriber, [".*"]})
> :ok
Unsubscribe from the 'event bus'
EventBus.unsubscribe(MyEventSubscriber)
> :ok

# if your subscriber has a config
config = %{}
EventBus.unsubscribe({MyEventSubscriber, config})
> :ok
List subscribers
EventBus.subscribers()
> [{MyEventSubscriber, [".*"]}, {{AnotherSubscriber, %{}}, [".*"]}]
List subscribers of a specific event
EventBus.subscribers(:hello_received)
> [MyEventSubscriber, {{AnotherSubscriber, %{}}}]
Event data structure

Data structure for EventBus.Model.Event

%EventBus.Model.Event{
  id: String.t | integer(), # required
  transaction_id: String.t | integer(), # optional
  topic: atom(), # required
  data: any() # required,
  initialized_at: integer(), # optional, might be seconds, milliseconds or microseconds even nanoseconds since Elixir does not have a limit on integer size
  occurred_at: integer(), # optional, might be seconds, milliseconds or microseconds even nanoseconds since Elixir does not have a limit on integer size
  source: String.t, # optional, source of the event, who created it
  ttl: integer() # optional, might be seconds, milliseconds or microseconds even nanoseconds since Elixir does not have a limit on integer size. If `ttl` field is set, it is recommended to set `occurred_at` field too.
}

transaction_id attribute

Firstly, transaction_id attribute is an optional field, if you need to store any meta identifier related to event transaction, it is the place to store. Secondly, transaction_id is one of the good ways to track events related to the same transaction on a chain of events. If you have time, have a look to the story.

initialized_at attribute

Optional, but good to have field for all events to track when the event generator started to process for generating this event.

occurred_at attribute

Optional, but good to have field for all events to track when the event occurred with unix timestamp value. The library does not automatically set this value since the value depends on the timing choice.

ttl attribute

Optional, but might to have field for all events to invalidate an event after a certain amount of time. Currently, the event_bus library does not do any specific thing using this field. If you need to discard an event in a certain amount of time, that field would be very useful.

Note: If you set this field, then occurred_at field is required.

Define an event struct
alias EventBus.Model.Event
event = %Event{id: "123", transaction_id: "1",
  topic: :hello_received, data: %{message: "Hello"}}
another_event = %Event{id: "124", transaction_id: "1",
  topic: :bye_received, data: [user_id: 1, goal: "exit"]}

Important Note: It is important to have unique identifier for each event struct per topic. I recommend to use a unique id generator like {:uuid, "~> 1.1"}.

Notify all subscribers with EventBus.Model.Event data
EventBus.notify(event)
> :ok
EventBus.notify(another_event)
> :ok
Fetch an event from the store
topic = :bye_received
id = "124"
EventBus.fetch_event({topic, id})
> %EventBus.Model.Event{data: [user_id: 1, goal: "exit"], id: "124", topic: :bye_received, transaction_id: "1"}

# To fetch only the event data
EventBus.fetch_event_data({topic, id})
> [user_id: 1, goal: "exit"]
Mark as completed on Event Observation Manager
subscriber = MyEventSubscriber
# If your subscriber has config then pass tuple
subscriber = {MyEventSubscriber, config}
EventBus.mark_as_completed({subscriber, {:bye_received, id}})
> :ok
Mark as skipped on Event Observation Manager
subscriber = MyEventSubscriber
# If your subscriber has config then pass tuple
subscriber = {MyEventSubscriber, config}
EventBus.mark_as_skipped({subscriber, {:bye_received, id}})
> :ok
Check if a topic exists?
EventBus.topic_exist?(:metrics_updated)
> false
Use block builder to build EventBus.Model.Event struct

Builder automatically sets initialized_at and occurred_at attributes

use EventBus.EventSource

id = "some unique id"
topic = :user_created
transaction_id = "tx" # optional
ttl = 600_000 # optional
source = "my event creator" # optional

params = %{id: id, topic: topic, transaction_id: transaction_id, ttl: ttl, source: source}
EventSource.build(params) do
  # do some calc in here
  Process.sleep(1)
  # as a result return only the event data
  %{email: "[email protected]", name: "John Doe"}
end
> %EventBus.Model.Event{data: %{email: "[email protected]", name: "John Doe"},
 id: "some unique id", initialized_at: 1515274599140491,
 occurred_at: 1515274599141211, source: "my event creator", topic: :user_created, transaction_id: "tx", ttl: 600000}

It is recommended to set optional params in event_bus application config, this will allow you to auto generate majority of optional values without writing code. Here is a sample config for event_bus:

config :event_bus,
  topics: [], # list of atoms
  ttl: 30_000_000, # integer
  time_unit: :microsecond, # atom
  id_generator: EventBus.Util.Base62 # module: must implement 'unique_id/0' function

After having such config like above, you can generate events without providing optional attributes like below:

# Without optional params
params = %{topic: topic}
EventSource.build(params) do
  %{email: "[email protected]", name: "John Doe"}
end
> %EventBus.Model.Event{data: %{email: "[email protected]", name: "John Doe"},
 id: "Ewk7fL6Erv0vsW6S", initialized_at: 1515274599140491,
 occurred_at: 1515274599141211, source: "AutoModuleName", topic: :user_created,
 transaction_id: nil, ttl: 30_000_000}

# With optional error topic param
params = %{id: id, topic: topic, error_topic: :user_create_erred}
EventSource.build(params) do
  {:error, %{email: "Invalid format"}}
end
> %EventBus.Model.Event{data: {:error, %{email: "Invalid format"}},
 id: "some unique id", initialized_at: 1515274599140491,
 occurred_at: 1515274599141211, source: nil, topic: :user_create_erred,
 transaction_id: nil, ttl: 30_000_000}
Use block notifier to notify event data to given topic

Builder automatically sets initialized_at and occurred_at attributes

use EventBus.EventSource

id = "some unique id"
topic = :user_created
error_topic = :user_create_erred # optional (in case error tuple return in yield execution, it will use :error_topic value as :topic for event creation)
transaction_id = "tx" # optional
ttl = 600_000 # optional
source = "my event creator" # optional
EventBus.register_topic(topic) # in case you didn't register it in `config.exs`

params = %{id: id, topic: topic, transaction_id: transaction_id, ttl: ttl, source: source, error_topic: error_topic}
EventSource.notify(params) do
  # do some calc in here
  # as a result return only the event data
  %{email: "[email protected]", name: "Mrs Jane Doe"}
end
> # it automatically calls notify method with event data and return only event data as response
> %{email: "[email protected]", name: "Mrs Jane Doe"}

Sample Subscriber Implementation

defmodule MyEventSubscriber do
  ...

  # if your subscriber does not have a config
  def process({topic, id} = event_shadow) do
    GenServer.cast(__MODULE__, event_shadow)
    :ok
  end

  ...

  # if your subscriber has a config
  def process({config, topic, id} = event_shadow_with_conf) do
    GenServer.cast(__MODULE__, event_shadow_with_conf)
    :ok
  end

  ...


  # if your subscriber does not have a config
  def handle_cast({:bye_received, id} = event_shadow, state) do
    event = EventBus.fetch_event(event_shadow)
    # do sth with event

    # update the watcher!
    # version >= 1.4.0
    EventBus.mark_as_completed({__MODULE__, event_shadow})
    # all versions
    EventBus.mark_as_completed({__MODULE__, :bye_received, id})
    ...
    {:noreply, state}
  end

  def handle_cast({:hello_received, id} = event_shadow, state) do
    event = EventBus.fetch_event({:hello_received, id})
    # do sth with EventBus.Model.Event

    # update the watcher!
    # version >= 1.4.0
    EventBus.mark_as_completed({__MODULE__, event_shadow})
    # all versions
    EventBus.mark_as_completed({__MODULE__, :hello_received, id})
    ...
    {:noreply, state}
  end

  def handle_cast({topic, id} = event_shadow, state) do
    # version >= 1.4.0
    EventBus.mark_as_skipped({__MODULE__, event_shadow})

    # all versions
    EventBus.mark_as_skipped({__MODULE__, topic, id})
    {:noreply, state}
  end

  ...

  # if your subscriber has a config
  def handle_cast({config, :bye_received, id}, state) do
    event = EventBus.fetch_event({:bye_received, id})
    # do sth with event

    # update the watcher!
    subscriber = {__MODULE__, config}
    EventBus.mark_as_completed({subscriber, :bye_received, id})
    ...
    {:noreply, state}
  end

  def handle_cast({config, :hello_received, id}, state) do
    event = EventBus.fetch_event({:hello_received, id})
    # do sth with EventBus.Model.Event

    # update the watcher!
    subscriber = {__MODULE__, config}
    EventBus.mark_as_completed({subscriber, :hello_received, id})
    ...
    {:noreply, state}
  end

  def handle_cast({config, topic, id}, state) do
    subscriber = {__MODULE__, config}
    EventBus.mark_as_skipped({subscriber, topic, id})
    {:noreply, state}
  end

  ...
end

Event Storage Details

When an event configured in config file, 2 ETS tables will be created for the event on app start.

All event data is temporarily saved to the ETS tables with the name :eb_es_<<topic>> until all subscribers processed the data. This table is a read heavy table. When a subscriber needs to process the event data, it queries this table to fetch event data.

To watch event status, a separate watcher table is created for each event type with the name :eb_ew_<<topic>>. This table is used for keeping the status of the event. Observation Manager updates this table frequently with the notification of the event subscribers.

When all subscribers process the event data, data in the event store and watcher, automatically deleted by the Observation Manager. If you need to see the status of unprocessed events, event watcher table is one of the good places to query.

For example; to get the list unprocessed events for :hello_received event:

# The following command will return a list of tuples with the `id`, and `event_subscribers_list` where `subscribers` is the list of event subscribers, `completers` is the subscribers those processed the event and notified `Observation Manager`, and lastly `skippers` is the subscribers those skipped the event without processing.

# Assume you have an event with the name ':hello_received'
:ets.tab2list(:eb_ew_hello_received)
> [{id, {subscribers, completers, skippers}}, ...]

ETS storage SHOULD NOT be considered as a persistent storage. If you need to store events to a persistent data store, then subscribe to all event types by a module with [".*"] event topic then save every event data.

For example;

EventBus.subscribe({MyDataStore, [".*"]})

# then in your data store save the event
defmodule MyDataStore do
  ...

  def process({topic, id} = event_shadow) do
    GenServer.cast(__MODULE__, event_shadow)
    :ok
  end

  ...

  def handle_cast({topic, id}, state) do
    event = EventBus.fetch_event({topic, id})
    # write your logic to save event_data to a persistent store

    EventBus.mark_as_completed({__MODULE__, {topic, id}})
    {:noreply, state}
  end
end

Traceability

EventBus comes with a good enough data structure to track the event life cycle with its optional parameters. For a traceable system, it is highly recommend to fill optional fields on event data. It is also encouraged to use EventSource.notify block/yield to automatically set the initialized_at and occurred_at values.

System Events

This feature removed with the version 1.3 to keep the core library simple. If you need to trace system events please check the sample wrapper implementation from the wiki page.

EventBus.Metrics Library

EventBus has some addons to extend its optional functionalities. One of them is event_bus_metrics library which comes with a UI, RESTful endpoints and SSE streams to provide instant metrics for event_bus topics.

EventBus.Metrics Instructions

Documentation

Addons

A few sample addons listed below. Please do not hesitate to add your own addon to the list.

Addon Name Description Link Docs
event_bus_postgres Fast event consumer to persist event_bus events to Postgres using GenStage Github HexDocs
event_bus_logger Deadly simple log subscriber implementation Github HexDocs
event_bus_metrics Metrics UI and metrics API endpoints for EventBus events for debugging and monitoring Hex HexDocs

Note: The addons under https://github.com/otobus organization implemented as a sample, but feel free to use them in your project with respecting their licenses.

Copyright and License

MIT

Copyright (c) 2022 Mustafa Turan

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

event_bus's People

Contributors

dependabot-preview[bot] avatar dependabot-support avatar fadhil avatar kianmeng avatar mustafaturan avatar quinnwilton avatar ryanmojo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

event_bus's Issues

Log empty topic listeners

The current implementation allows all events to create insertion to Store and Observation ETS tables even if there is no subscriber for the topic. This may cause unnecessary operations.

Following actions might be reasonable:

  • Log these topics
  • Don't insert event data if there is no subscription for the event topic

Broken Contract

The call:
EventBus.subscribe(
  {Core.Topics.Handler,
   [
     :topic1,
     :topic2,
     :topic3,
     :topic4,
     ...
   ]}
)

breaks the contract
(listener_with_topic_patterns()) :: :ok

Getting dialyzer errrors when I try to subsribe to some topics. Any ideas?

Errors in test with OTP26.1.2/Elixir 1.15.7

Describe the bug
Errors in test with OTP26.1.2/Elixir 1.15.7

To Reproduce
Steps to reproduce the behavior:

  1. git clone https://github.com/otobus/event_bus
  2. cd event_bus
  3. mix deps.get
  4. mix test

Expected behavior
Pass all test.

Logs

.......
13:03:30.443 [warning] Topic(:user_created) doesn't have subscribers
......................
13:03:36.207 [info] [EVENTBUS][STORE] metrics_received_5.E1.ets_fetch_error
.............

  1) test notify (EventBus.Service.NotificationTest)
     test/event_bus/services/notification_test.exs:41
     Expected truthy, got false
     code: assert String.contains?(
             logs,
             "Event log for %EventBus.Model.Event{data: [1, 2], id: \"E1\", initialized_at: nil, occurred_at: nil, source: \"NotificationTest\", topic: :metrics_received, transaction_id: \"T1\", ttl: nil}"
           )
     arguments:

         # 1
         "\n13:03:36.534 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!\n%RuntimeError{message: \"I don't want to handle your event\"}\n\n13:03:36.534 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!\n%RuntimeError{message: \"I don't want to handle your event\"}\n\n13:03:36.539 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!\n%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}\n\n13:03:36.539 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!\n%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}\n\n13:03:36.540 [info] Event log for %EventBus.Model.Event{id: \"E1\", transaction_id: \"T1\", topic: :metrics_received, data: [1, 2], initialized_at: nil, occurred_at: nil, source: \"NotificationTest\", ttl: nil}\n\n13:03:36.541 [info] Event log for %EventBus.Model.Event{id: \"E123\", transaction_id: \"T1\", topic: :metrics_summed, data: {3, [1, 2]}, initialized_at: nil, occurred_at: nil, source: \"AnotherCalculator\", ttl: nil}\n\n13:03:36.541 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!\n%RuntimeError{message: \"I don't want to handle your event\"}\n\n13:03:36.541 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!\n%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}\n\n13:03:36.541 [info] Event log for %EventBus.Model.Event{id: \"E123\", transaction_id: \"T1\", topic: :metrics_summed, data: {3, [1, 2]}, initialized_at: nil, occurred_at: nil, source: \"Logger\", ttl: nil}\n"

         # 2
         "Event log for %EventBus.Model.Event{data: [1, 2], id: \"E1\", initialized_at: nil, occurred_at: nil, source: \"NotificationTest\", topic: :metrics_received, transaction_id: \"T1\", ttl: nil}"

     stacktrace:
       test/event_bus/services/notification_test.exs:72: (test)

..........
13:03:37.755 [info] [EVENTBUS][OBSERVATION] some_event_occurred1.NA.ets_fetch_error
.
13:03:37.757 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!
%RuntimeError{message: "I don't want to handle your event"}
.
13:03:37.757 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!
%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}
.
13:03:37.757 [info] Event log for %EventBus.Model.Event{id: "E1", transaction_id: "T1", topic: :metrics_received, data: [1, 2], initialized_at: nil, occurred_at: nil, source: "NotifierTest", ttl: nil}
.
13:03:37.757 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!
%RuntimeError{message: "I don't want to handle your event"}
.
13:03:37.757 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!
%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}

13:03:37.757 [info] Event log for %EventBus.Model.Event{id: "E123", transaction_id: "T1", topic: :metrics_summed, data: {3, [1, 2]}, initialized_at: nil, occurred_at: nil, source: "AnotherCalculator", ttl: nil}

13:03:37.757 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!
%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}

13:03:37.757 [info] Event log for %EventBus.Model.Event{id: "E123", transaction_id: "T1", topic: :metrics_summed, data: {3, [1, 2]}, initialized_at: nil, occurred_at: nil, source: "Logger", ttl: nil}

13:03:37.758 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!
%RuntimeError{message: "I don't want to handle your event"}


  2) test notify (EventBusTest)
     test/event_bus_test.exs:31
     Expected truthy, got false
     code: assert String.contains?(
             logs,
             "Event log for %EventBus.Model.Event{data:" <>
               " [1, 7], id: \"M1\", initialized_at: nil, occurred_at: nil," <>
               " source: \"EventBusTest\", topic: :metrics_received," <> " transaction_id: \"T1\", ttl: nil}"
           )
     arguments:

         # 1
         "\n13:03:37.858 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!\n%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}\n\n13:03:37.858 [info] Event log for %EventBus.Model.Event{id: \"M1\", transaction_id: \"T1\", topic: :metrics_received, data: [1, 7], initialized_at: nil, occurred_at: nil, source: \"EventBusTest\", ttl: nil}\n\n13:03:37.858 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!\n%RuntimeError{message: \"I don't want to handle your event\"}\n\n13:03:37.858 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!\n%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}\n\n13:03:37.859 [info] Event log for %EventBus.Model.Event{id: \"E123\", transaction_id: \"T1\", topic: :metrics_summed, data: {8, [1, 7]}, initialized_at: nil, occurred_at: nil, source: \"Logger\", ttl: nil}\n\n13:03:37.859 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!\n%RuntimeError{message: \"I don't want to handle your event\"}\n\n13:03:37.859 [info] Elixir.EventBus.Support.Helper.BadOne.process/1 raised an error!\n%UndefinedFunctionError{module: EventBus.Support.Helper.BadOne, function: :process, arity: 1, reason: nil, message: nil}\n\n13:03:37.859 [info] Event log for %EventBus.Model.Event{id: \"E123\", transaction_id: \"T1\", topic: :metrics_summed, data: {8, [1, 7]}, initialized_at: nil, occurred_at: nil, source: \"AnotherCalculator\", ttl: nil}\n\n13:03:37.859 [info] Elixir.EventBus.Support.Helper.AnotherBadOne.process/1 raised an error!\n%RuntimeError{message: \"I don't want to handle your event\"}\n"

         # 2
         "Event log for %EventBus.Model.Event{data: [1, 7], id: \"M1\", initialized_at: nil, occurred_at: nil, source: \"EventBusTest\", topic: :metrics_received, transaction_id: \"T1\", ttl: nil}"

     stacktrace:
       test/event_bus_test.exs:48: (test)

............
Finished in 8.8 seconds (0.00s async, 8.8s sync)
71 tests, 2 failures

Randomized with seed 292741

please complete the following information:

  • OS: Alpine 3.8, Ubuntu x.x, Centos x.x, Mac OS 10.13.2
  • Elixir version: 1.15.7
  • OTP version: 26.1.2

Additional context
Add any other context about the problem here.

EventBusMetrics uuid dependency rename to elixir_uuid

Upgrading the dependencies of a project I found out that event_bus_metrics
depends on uuid >= 1.1.8. I use uuid in my project as a main dep too. Since that needs to be renamed to elixir_uuid I'm getting the following compile error.

Unchecked dependencies for environment dev:
* uuid (Hex package)
  could not find an app file at "_build/dev/lib/uuid/ebin/uuid.app". This may happen if the dependency was not yet compiled or the dependency indeed has no app file (then you can pass app: false as option)
** (Mix) Can't continue due to errors on dependencies

I think this is having to do with event_bus_metrics still wanting uuid but my project using elixir_uuid now

Clustered events

Hi,

This is a question more than an issue.

  • Is it possible to run the event bus in a clustered environment?
  • If yes, is it possible to provide each event only once to a subscriber module globally?

I read through the docs but unfortunately this wasn't clear from them.

Update ex_doc dependency

0.18.4 does not support Elixir v1.7 and later. For recent Elixir versions, make sure to depend on {:ex_doc, "~> 0.19"}

Dialzyer warnings in EventSource.notify (even in EventSource.build )

With a simple call like:

EventSource.notify %{topic: :my_topic} do
      %{account: foobar, error: "some"}
end

Dialyzer warns about The pattern {'error', __@4} can never match the type....
Code in the macro seems correct, but maybe dialyzer goes mad about it?

As a side note, a macro is really needed for EventSource.build and EventSource.notify funs? Why not a simpler EventSource.build(params, fun) where fun is the the current do/end block? After all, everything done in those macros are executed a runtime, so no real need for the macro.

Or I'm totally wrong?

Small tweaks to Creating Event Consumers wiki page

I made some small tweaks to the Creating Event Consumers wiki page (just small language changes and an indentation fix), but since GitHub doesn't allow pull requests to the wiki I'm making an issue instead.

I cloned the wiki and made the changes in that fork. You can see the new version here, and a diff of changes here.

If there's a better way you'd like these changes presented I can definitely do something else.

Thanks!

Suggestions to enhance README

Hi there,

Thank you so much for creating this library! I'm excited to jump in.

As a relative newcomer to Elixir, I have a couple of observations about library setup in my app that tripped me up. Things that should be obvious to the longtime Elixir programmer are not as intuitive to me - adding some of these pointers might help other folks like me!

  1. It would be helpful to include a snippet in your installation section about adding :event_bus as an application entry in the Mixfile.application section in mix.exs.

  2. You may also want to be explicit in your documentation that any Listener modules need to be implemented as GenServers, and should be placed under supervision.

Let me know if you'd prefer that I take a swag at this in the README and open a PR.

Thanks!

Flaky tests with seed 939297

Describe the bug
Running tests with seed 939297 triggers an error

To Reproduce
Steps to reproduce the behavior:

  1. mix test --seed 939297

Expected behavior
The test should pass

Log
image

please complete the following information:

  • OS: Mac OS 10.15
  • Elixir version: 1.11.0
  • OTP version: 23.0

Wrong Base62 module alias in 1.7.0

Describe the bug
After upgrading from 1.6.2 to 1.7.0 I started seeing this error when calling the EventBus.EventSource.notify/2 macro:

** (UndefinedFunctionError) function Base62.unique_id/0 is undefined (module Base62 is not available)
    Base62.unique_id()
    (sanbase 0.0.1) lib/sanbase/event_bus/event_bus.ex:84: Sanbase.EventBus.notify/1
    iex:1: (file)

This is fixed by adding the following to my config.exs file:

config :event_bus,
  id_generator: EventBus.Util.Base62

Alternatively, it should be fixed by properly referring to the module from the EventBus.EventSource.build/2 macro.

The issue seems to be introduced here:
89fbe49#diff-db1da10c33c19893e8843ec6476e5a3fb7525fdc8b7da6c81f3d5c1344408b55L18-R55

Expected behavior
The module to be properly aliased and not reported as undefined.

Enviroment:

  • OS: MacOS Monterey 12.6
  • Elixir version: 1.14.0
  • OTP version: 25.0

Change Time Format to UTC and avoid time shifting issues.

The current implementation uses System.os_time which can vary depending on locale and daylight savings time. It also returns a time_unit :: integer rather than straight int which breaks a contract and makes dialyzer complain. The better alternative is to use the DateTime module.

DateTime.utc_now |> DateTime.to_unix(:microseconds)

Flaky tests with seed 249016

Describe the bug
Running tests with seed 249016 triggers an error

To Reproduce
Steps to reproduce the behavior:

  1. mix test --seed 249016

Expected behavior
The test should pass

Log
image

please complete the following information:

  • OS: Mac OS 10.15
  • Elixir version: 1.11.0
  • OTP version: 23.0

Rename all `micro_services`, `micro_service` and `microservices` as `microservice`

To standardize, and follow Erlang 19+ consistency rename all micro_services, micro_service and microservices time unit occurrences to microservice

Occurences:
https://github.com/otobus/event_bus/search?q=micro_seconds&unscoped_q=micro_seconds
https://github.com/otobus/event_bus/search?q=microseconds&unscoped_q=microseconds

Ref: https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/system.ex#L65

//cc @ShaneWilton

Define high level types in main module level to provide naming conventions

Current EventBus main module spec does not provide high-level type specs for the type safety.

TODO:

  • Add public high-level types for each function to prevent misusage of functions.
  • Remove string type support on topic name registrations and deregistration
  • Allow event_shadow as tuple on mark_as_completed/1 and mark_as_skipped/1

Outbox guarantees

Hi @mustafaturan ,

would you please clarify what are outbox guarantees for EventBus with PostgreSQL: does is there are promise of at-leat-once delivery if EventBus.notify(event) returned :ok?

Is there any node termination logic inside the library that will postpone application shutdown until all data is written to the DB?

Thank you.

And kudos for great work, the library looks solid ๐Ÿ‘

Add custom error handler

Right now the errors are just logged into logger. But there should be a way to add a custom handler. Likely to notify a service Sentry or Airbrake if something happened.

Compiler warning when adding event_bus to app

Describe the bug
Compiler warnings about :crypto when running mix deps.compile in an app that uses event_bus.

To Reproduce
Steps to reproduce the behavior:

  1. Install the deps in your app's mix.exs
  2. Run the command mix deps.get && mix deps.compile
  3. See compiler warning about it not depending on crypto.

Expected behavior
We shouldn't see this warning when compiling deps in an app that includes event_bus.

Logs

    ==> event_bus
Compiling 17 files (.ex)
warning: :crypto.bytes_to_integer/1 defined in application :crypto is used by the current application but the current application does not depend on :crypto. To fix this, you must do one of:

  1. If :crypto is part of Erlang/Elixir, you must include it under :extra_applications inside "def application" in your mix.exs

  2. If :crypto is a dependency, make sure it is listed under "def deps" in your mix.exs

  3. In case you don't want to add a requirement to :crypto, you may optionally skip this warning by adding [xref: [exclude: [:crypto]]] to your "def project" in mix.exs

  lib/event_bus/utils/base62.ex:30: EventBus.Util.Base62.random/2

please complete the following information:

  • OS: Mac OS 10.15.7
  • Elixir version: 1.11.3
  • OTP version: 23.2.2

Sample Listener Implementation

Do you have a more complete example of a listener? At the top of the example is ...
Are you doing "use GenServer"?

Does your dialyzer report show that handle_cast has no return?

It would be helpful to have a full working implementation as an example. So far I have used EventBus.nofity(event) and then I was able to look up the event, but nothing in my handle_cast function is being done. I placed an IO.inspect handle_cast and nothing is output.

any ideas?

Tracing Causation

Currently, EventBus supports setting a transaction_id on events, in order to trace chains of related events. This works for determining what events are correlated, but is insufficient for reconstructing the precise causal relationships between events.

Imagine the following series of events:

       /-> C -> D
A -> B 
       \-> E

As currently implemented, all of the above events will have the same transaction_id, and so are obviously correlated. It is not possible, however, to determine that D was caused by C, which was caused by B, and so on.

One possible solution to this would be extending the event schema to include an additional field for causation_id, which could be set to the id of the previous event in the chain. This is a pattern described by Greg Young in his definition of Event Sourcing. Another explanation is available here: https://blog.arkency.com/correlation-id-and-causation-id-in-evented-systems/

The existing transaction_id field fills effectively the same role as the correlation_id, and doesn't need to be changed in my opinion :)

What are your thoughts on this proposal?

Missing `nil` handling on `mark_as_completed`

Describe the bug
During a day or two in production, an error occurs at mark_as_completed where fetch becomes nil. Based on the snippet below:

  @doc false
  @spec mark_as_completed(subscriber_with_event_ref()) :: :ok
  def mark_as_completed({subscriber, event_shadow}) do
    {subscribers, completers, skippers} = fetch(event_shadow)   # Does not handle `nil` 
    save_or_delete(event_shadow, {subscribers, [subscriber | completers], skippers})
  end

  @doc false
  @spec fetch(event_shadow()) :: any()
  def fetch({topic, id}) do
    case Ets.lookup(table_name(topic), id) do
      [{_, data}] -> data
      _ -> nil
    end
  end

A guard clause for nil can be put here to prevent the application from crashing.

To Reproduce
Steps to reproduce the behavior:

  1. Release an application with :event_bus in FreeBSD
  2. Wait for a day or two
  3. Application crashes and stops because of mark_as_completed

Expected behavior
Despite ETS data loss, application should safely handle errors and not crash the application

Logs

No logs
  • OS: FreeBSD 11.2
  • Elixir version: 1.9.0
  • OTP version: 21.0

Badly formatted error message for unregistered topic

Describe the bug
When trying to send an event to a topic that has not been registered with EventBus.register_topic/1, you get an error like the following:

[warn] Topic(:foo doesn't exist!) doesn't have subscribers

This should be clarified to show either the topic doesn't exist, or it doesn't have subscribers, and not display registration_status within the braces. Additionally, it should print the topic exactly as the user has passed, instead of coercing it to look like an atom, as the log message is confusing if the user has passed a string topic instead of an atom.

Below is the offending line:

"Topic(:#{topic}#{registration_status(topic)}) doesn't have subscribers"

To Reproduce

Send an %EventBus.Model.Event{} to a non-registered topic and observe the warn log entry.

Expected behavior

As per above.


I can submit a PR if you'd like, I just wanted to report and run my suggestions by you before starting.

Typos in README

Super minor, but I found some typos on the README, examples include, but are not limited to:

Tracable with optional attributes. vs Traceable with optional attributes.

EventSource.nofify block/yield to automatically vs EventSource.notify block/yield to automatically

Multiple nodes

Hi!

Wondering if it's possible to run one event_bus across multiple nodes? Since it uses ETS?
It wasn't obvious to me, if you have any pointers I'd be happy to create a demo app or update the documentation.

Thanks for creating this project! It is super cool and easy to read ๐Ÿ‘Œ

Dynamic listeners?

Hi

Is it not possible to register an arbitrary process as a listener using its PID? If I've understood the current implementation correctly, then each subscriber has to be implemented as a named module

Dialyzer errors involving @eb_time_unit

@eb_time_unit defaults to :micro_seconds, but the Elixir typespecs only specify :microseconds as a valid value (even though both work). This leads to Dialyzer errors when notifying to a topic:

The call:
System.monotonic_time(:micro_seconds)

breaks the contract
(time_unit()) :: integer()

Update version number to latest minor on README

Even though {:event_bus, "~> 1.3"} refers to the available latest, according to hex.pm stats, some developers downloads 1.3.0 instead of 1.3.7 version of the library. To overcome this issue, simply keep the latest version on README file.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.