Event Registration Expectations (Dry Monitor)

What are the expectations around registering, subscribing, and publishing events with the built in “notfications” component, which is an instance of Dry::Monitior::Notifications?

The following is a transcript of a discord discussion on the topic. I’ve decided to post it here for searchability for other’s who have the same question.

Aaron Allen

Dear Hanami, Where is the appropriate place for a slice to register_events in the notification system?

Adam Lassek

I do it in a provider like so:

CIAM::Slice.register_provider :webhooks do
  start do
    webhook = CIAM::Events::Webhook.new

    webhook.subscribe CIAM::Events::Logger.new

    if Enabled?(:webhook_events)
      webhook.subscribe CIAM::Events::WebhookListener.new
    end

    register "events.webhook", webhook
  end
end

Aaron Allen

Is this for the built in Dry Monitor or is this something custom?

Adam Lassek

Ah, you’re talking about dry-monitor for the container? My example is dry-events

Aaron Allen

yeah I was just planning to use the built in notification system
so that slices can chat

Adam Lassek

So your own custom monitor instance? i.e. you’re not talking about monitoring key usage?

Aaron Allen

I’m talking about:

app['notifications']
my_slice['notifications']

which is a shared instance of dry-monitor notifications

It’s there but it’s not super well documented how one should go about utilizing it

Adam Lassek

The simplest answer is probably initialize it as a top-level provider and autoinject it to every slice
I would only do that differently if you needed separate notification channels

Aaron Allen

it’s already auto injected but I can’t seem to do:

Hanami.app.configure_provider :notifications
  #...
end

Without it barfing
I guess I could just do a generic:

MySlice::Slice.register_provider :events do
  start do
    EVENTS.each { |e| target['notifications'].register_event(e) }
  end
end

but I was hoping there was some “official” way to go about this (edited)

Adam Lassek

There is no “official” integration yet AFAIK
Good candidate for an extension, though.

Aaron Allen

Cool hopefully @timriley can give clarity on the intended use when he wakes up

Adam Lassek

When you say “barfing” what exactly is the problem?

Aaron Allen

basically it appears if you do:

Hanami.app.configure_provider :notifications do
  #...
end

it appears to override the default notifications provider

Adam Lassek

Are you using hanami-db ?

Aaron Allen

aye

Adam Lassek

okay there is official integration for this I was wrong hanami/lib/hanami/app.rb at 252f448781710f7cb73cda684a2dc02941d210e3 · hanami/hanami · GitHub
that was introduced with DB support I believe, because ROM relies on it
So you could reuse that, or define your own under a different key name

Aaron Allen

no what’s provided is plenty sufficient its just a matter of figuring out where events are meant to be registered in the stack
because you can’t subscribe to events that aren’t registered

Adam Lassek

configure_provider isn’t working because this is coming from a dry-system extension, not a hanami provider

it is initialized in prepare_all , before your providers will run. So you could define a provider that registers these events, you just need to name it differently

this is bad ergonomics, perhaps we should write an official provider to make configuration easier

Aaron Allen

MySlice::Slice.register_provider :events do
  start do
    target['notifications'].register_event('my_event')
  end
end
my_slice['notifications'].subscribe('my_event')
# => you are trying to subscribe to an event: `my_event` that has not been registered (Dry::Events::InvalidSubscriberError)

Adam Lassek

did you run my_slice.start(:events) ?

Aaron Allen

same error

actually no running my_slice.start(:events) Does make it work but am I meant to log in to every production console and run that every time the app restarts?

Adam Lassek

the console defaults to lazy-loading, production will generally run app.boot

root:0> app[:notifications].__bus__.events.values
=> [#<Dry::Events::Event id="test_event" payload={}>]

If you wanted production to also lazy-load, you could do something like

environment(:production) do
  start :events
end

in config/app.rb

Aaron Allen

yeah this isn’t great DX I’d really like to see something like:

module MySlice
  class Slice < Hanami::Slice
    config.notifications.register_event('my_event')
  end
end

or something of that nature

Adam Lassek

Not a bad suggestion, or something like

config.notifications += %w[
  event_one
  event_two
]

Aaron Allen

aye

or even better:

config.emits += #...

but most importantly it needs to be done in a way that I can subscribe to events in console without needing to run app.boot

oh and also important:

module MyOtherSlice
  class Slice < Hanami::Slice
    config.notifications.subscribe('event_one', 'listeners.my_event_one_listener')
  end
end

because as of right now I have no earthly idea how I’m meant to handle actually subscribing to the events

Adam Lassek

Well, the bus doesn’t exist until the prepare lifecycle. So this means the earliest you could subscribe is inside a provider. I don’t think we could support doing this as configuration like that because I think block-based subscription is possible as well.

Aaron Allen

aye

Adam Lassek

To generalize the problem, we have a DX footgun happening because you need to run a provider when opening a console that depends on a side-effect, and it can’t be inferred by key name

Aaron Allen

It’s possible the intended use case was for notifications to be an internal tool strickly for Hanami to use but if so why expose it in the components?

Adam Lassek

Like I said, it was a dependency of ROM that introduced it, but I don’t think we specifically intended it to be internal-only either

if you wanted to access telemetry from ROM it’s necessary to be able to access the bus

But not a first-class feature, so didn’t get the ergonomic design DB configuration did

I would like to hear @timriley 's thoughts, but it sounds like replacing the dry-system plugin with a Hanami provider that supports configure_provider :notifications would solve a lot of these problems

But we also want to preserve the lazy-loading aspect of the container system, so maybe it would be useful to have something like HANAMI_AUTOSTART=notifications

Thanks as always for your thoughtful interrogation of Hanami’s features, @aaronmallen, and thanks also for bringing this discussion into the forum.

@alassek is right in describing the internal notifications bus as a fairly underbaked part of Hanami. I’d like to make it better.

Right now I would consider the "notifications" component as semi-private. It exists for framework wiring, and we’ve never documented it. In this current state, I consider it something we can change across releases without it becoming a “breaking change” (though of course we’d document the changes, either way). This is a good thing: it gives us leeway to properly rethink it.

I see the future of "notifications" as being a proper framework-level event bus. The kind of thing that Hanami extension libraries use to propagate and listen to useful events from other parts of the stack. An “active support notifications” equivalent. And of course, if users have use for such a thing in their own apps, they’d be welcome to use it too.

In the meantime, if you want domain-level events, I’d consider following @alassek’s advice and just setting up your own event bus that you can fully control. I’ve done this before in Hanami-style apps and it’s worked well. This way you get what you need while sidestepping all the current awkwardness. It’ll also insulate you from potential changes to "notifications", because like I said above, it definitely feels ripe for some adjustments.

While I’m here, some other things:

  • First, an admission: I definitely spent more time thinking about the things that depend on "notifications" than I did on "notifications" itself. I mostly copied that over from our proto-framework that pre-dated and eventually turned into Hanami 2. So any attention on this is welcome. It definitely needs it.
  • I agree with @alassek that a first-party provider for notifications would likely be better for configurability than the dry-system plugin. We could also think about renaming the container key to something that better describes its purpose.
  • I think Dry Monitor is one of our least ergonomic gems and I’d love to change that. I’ve so often had to fight its global vs local consequences. I mean, look at this. It shouldn’t have to be this hard.
  • Related: I’m also vaguely uncomfortable about this. That rack_logger kind of gets orphaned as soon as we’ve attached it. It just doesn’t feel right to me.
  • While I’m here, I’ve never felt fully at ease with the level of strictness imposed by Dry Events, wherein you must register event types before any corresponding events be published. I wonder if a slight loosening of that might make it more practical to work with?

@timriley think having a first class event bus is probably important for an eco system like hanami where breaking your app down into “slices” is such a major feature of the framework. Cross domain chatter in these types of eco systems is a must IMO. I also agree with your last point so much that I would honestly argue “registering” an event is mostly fruitless. The consequences of subscribing to an event that doesn’t exist are pretty small (your listener just never gets triggered). I’ll definitely jump in on a different system for my needs (likely dry-events), however I would also encourage you to reconsider taking up the component name “notifications” for a framework level dependency.

@aaronmallen Thanks for the feedback! Your point about the importance of pubsub for a slice-oriented app is a good one. I’ve tended to find myself needing one every time I’ve created something using multiple slices.

Feels like we should think about Hanami offering both types of things: a framework-level bus for internal things, and some way for a user to set up an event bus for their own domain events. The latter is where I wouldn’t want us to be too presumptive, though. There could be value in multiple domain-level busses in a given app, so I wouldn’t want to end up trying to offer something that is too restrictive.

1 Like

I disagree, silent failures are really bad problems that can be a nightmare to diagnose, and event-based logic only makes this more likely.

I would prefer to make a distinction between a forgiving development environment, and a strict deployment environment, i.e. I probably don’t care about it when I’m opening a dev console locally, but I want a misspelled or missing event to result in a deployment failure before it hits production.

1 Like

That’s a fair point. My worry is that what happens when a subscriber gets loaded before the registerer? i.e. Slice A registers a listener to Slice B but slice A is loaded first