Introducing Signals -- a charm lib to exchange simple notifications between charms

Why?

Bob and Alice are two charms. Bob wishes Alice to know that he’s ready for something. Bob and Alice maybe are already related, but abusing that relation to ‘wake up’ the other charm and send some simple payload not necessarily ‘about’ that relation feels iffy.

So what about we create another relation, only for that specific signal to go through? A channel, so to say. We want to do that on Alice and Bob, and maybe we want to have multiple separate channels. That’s a lot of code duplication.

What?

Hereby we present signals, a charm lib to facilitate setting up endpoints by which to send simple notifications from one charm to the other.

Setup

Add to metadata.yaml, in both charms, an endpoint with interface signals.

requires:
  signals:
    interface: signals

It does not matter whether you put requires or provides; both charms could even have the same role. Signals can go both ways, there’s no real semantics to the role in this context.

Technically the interface name also does not matter, so long as it’s the same in both charms.

Now add to your charm.py:

from charms.signals.v0.signals import Signals, SignalEvent

class MyCharm(CharmBase):
    def __init__(...):
         self.signals = Signals(endpoint='signals')

Your charm is now ready to send and receive signals via the 'signals' channel.

How?

Sending a signal

You send a signal by:

self.signals.send(name='ping', payload='hello signals')

By default this will send the signal on all available channels for the signals endpoint. So if multiple charms are related to the emitter, they will all receive the ‘ping’ signal (with the same payload). To target a specific relation instead, you can pass a Relation instance to the send method:

relation: Relation = self.get_relation()
self.signals.send(name='ping', payload='hello signals', relation=relation)

This means only the remote units belonging to that relation will receive the signal.

Receiving a signal

You receive a signal by observing the Signals.on.receive event.

You do so by adding to your charm’s __init__:

self.framework.observe(self.signals.on.receive, 
                       self._on_signal_receive)

Many signals!

The idea is that if you have multiple signal channels, say, one to notify one another of readiness, another to notify one another of some service status, you can idiomatically do:

requires:
  ready:
    interface: signals
  service:
    interface: signals

and then:

self.ready = Signals(self, 'ready')
self.service = Signals(self, 'service')

self.framework.observe(self.ready.on.receive, 
                       self._on_ready_receive)
self.framework.observe(self.service.on.receive, 
                       self._on_service_receive)

Name and payload

A signal consists of a name, which charms can use to identify different signal types sent through the same channel, and a payload, whose semantics will presumably differ depending on the type. It can be any string.

A typical _on_signal_receive implementation will look like:

def _on_signal_receive(self, event: SignalEvent):
        if event.name == 'pong':
            self.pong_received(event.payload)
        if event.name == 'cheese':
            self.smile(event.payload)
        else:
            raise ValueError(event.name, 'not a known signal type')

When?

Now. What are you waiting for?

charmcraft fetch-lib charms.signals.v0.signals

Source: https://github.com/PietroPasotti/signals

4 Likes