Best Practice for Using Relations via Operator Framework

Hello,

After spending some time playing around with operator a bit, I’m stuck trying to get information out of the event object on relation changed. I’m wondering if there are any examples laying around that show the componentry involved in sending and receiving information via a relation between a provides and requires interface of two related charms?

Thanks!

2 Likes

Pinging @jameinel and @chipaca

I’m not quite sure what information you’re struggling with, but I can give a basic layout of how it works. (For starters, we have reference documentation currently available here, and I currently have a PR to update the related documentation). We want to have a simpler URL for API docs, but the one we want was used by a now-defunct project, and RTD says they need to wait 6 weeks for upstream to respond before they’ll give it to us.

But to type out what you probably want here:

Relation objects have 3 relevant attributes

  • .app is the Application of the related app. (eg, mysql related to wordpress, the associated Relation for wordpress would have mysql as the related app.)
  • .units are the Unit objects for units which have joined the relation (wordpress’s Relation object would have [mysql/0, mysql/1] as the units.)
  • .data is the data bag containing all of the relation data, both for your units/app and for remote units/app keyed by their objects.

RelationEvent objects have 3 interesting objects as well

  • .relation is the Relation that is changing
  • .app is the remote app. In the case of application data bag changing, only this is set. In the case of a unit data bag changing, it will still have the app.
  • .unit the remote unit that is changing. Might be None if the application data is what is changing.

So a common pattern is:

def _on_relation_changed(self, event):
  remote_data = event.relation.data[event.app]
  if event.unit is not None:
    remote_data = event.relation.data[event.unit]
  # check a key
  if remote_data['key'] == 'value':
    ...
  # respond
  if self.model.is_leader():
    event.relation.data[self.app]['foo'] = 'bar'
  event.relation.data[self.unit]['baz'] = 'quux'

@jamesbeedy Does that give you the pointers you were looking for?

1 Like

@jameinel possibly I can expand a bit more where I’m getting tripped up.

Take the example of relating two applications on a common interface where one application defines the provides side and the other defines the requires side.

What are the concepts, components and processes involved in facilitating the exchange of information over the interface via constructs in the operator framework?
For example, is it to be assumed that the providing charm must first set data on .joined so that the data is available for the requiring side of the relation to get on .available?

Are there any conventions or constraints on how this can or can’t/should or shouldn’t be done?

Maybe this example can help explain what I’ve gathered this far, and what I’m missing.

Goal

Given two operator charms, provide information from one application to another application using a common interface.

Step 1 - Define relation endpoints

Each charm must define an interface in the metadata.yaml for the side of the relation that it is responsible for; provides or requires.

Ok, ok.

Step 2 - Facilitate exchange of information from provider to requirer

Both charms need to implement code to facilitate either the sending or receiving of information via the respective provides and requires interfaces. This is accomplished by (where I get lost):

  1. Both charms define a class that inherits from Object in which the desired relation events are observed and mapped to the hander code.
    Are there any constraints on how/where/when this can be done?

    • Can both charms observe .joined and communicate information over the relation event for .joined?
    • What does the charm author need to be conscious of in respect to the lifecycle of the relation hooks when sending/receiving information via event.relation?
  2. The providing charm’s handler code executes when the desired relation hook event is observed. The data is set by the provider on the event using its own application object as a parent key in event.relation.data for it to store data in that it wants to send to the requiring charm on the other end of the relation.

  3. The requiring charm accesses the data set by the providing charm via event.relation when the event is observed and subsequently the handler mapped to the event execute. The handler code of the providing charm can access the event data for the unit or application you are making the relation to by using event.unit or event.app. This allows to index into event.relation.data to get the data set by the unit or application on the other side (provides) of the relation.

Am I on the right track here? Is there anything you might add/take away?

Joined is fired whenever a unit of the other application gets past start() and ‘joins’ the relationship. You can certainly respond to that, though one a unit sets data, that won’t be seen until the next relation-changed for the other application. (Just to say, you can certainly use -joined, but you’ll also need to support -changed.)

The relations hooks are generally

  • relation-created (new in Juju 2.8) happens when the user does juju relate and lets you know about the remote application (in env var JUJU_REMOTE_APP)
  • relation-joined when a unit of the related app starts
  • relation-changed when a related unit or app changes their relation data
  • relation-departed when a unit leaves the relation
  • relation-broken when the relation is gone

Note that if a user does “juju remove-relation” you still get relation-departed for each unit before relation-broken once the relation is actually torn down.

I think you’re very much on the right track.

@jameinel @timClicks I seem to have a basic relation example working.

Here are my minimal working examples for relating a providing charm and a requiring charm.

juju relate foo-provider foo-requirer will cause the providing application to send the value “bar” via the key “foo” in the relation.data to the requiring application.

I tried to make this as minimal as possible to focus on the relation bits. Anything you might add or take away from this?

Thanks!

Some feedback:

  • First off, I think these are quite good as “minimal show you how it interconnects” charms.

  • One recommendation is to make your event handlers (on_start, etc) be private (_on_start). Especially for the shared components (FooRequires), it makes it clearer what are the public parts of your class, and what parts are internal details that you shouldn’t touch.

  • Similarly, I would recommend doc strings on the Class and on the public methods of the class. Again, it is to draw attention to the things that the user should interact with, and distract from details that they don’t need to worry about.

  • You’re actual on_relation_changed assumes that ‘foo’ is always available and already set. It would probably be better to do:

    foo = event.relation.data[event.unit].get('foo', None)
    if foo is not None:
      self.on.foo_available.emit()
    

    That also helps seed the idea that you don’t unconditionally trigger an event from a component, but you process the low level events in order to provide meaningful events.

  • At this point, we’re much more interested in reusable FooRequires classes than FooProvides classes, so I would focus there. If there are nice clean abstractions from a provides side, then by all means do it. But it is less likely that you will have 2 charms providing the same service than you’ll have 2 charms consuming that service. (A lot more apps that talk to postgresql than implementations of postgresql.)

  • It is still a point of debate what to call the classes. It often makes more sense to call it PostgresqlClient than PostgresqlRequires. And while I like linking it to config.yaml, I think there is a tradeoff between linking and having code that is clearer to read.

2 Likes