Start of Endpoint implementation for easier interface layers #123

Merged
merged 39 commits into from Nov 28, 2017

Conversation

Projects
None yet
6 participants
Owner

johnsca commented Sep 14, 2017

Beginning of the implementation for the proposal for new style interface layers using the Endpoint base, eschewing the reliance on @hook in favor of @when everywhere, and removal of the "conversations" metaphor. This implements up to and including the "Managed Flags" section.

fixes #107
fixes #88
fixes #61
fixes #102

charms/reactive/altrelations.py
+ return cls.from_name(parts[1])
+
+
+hookenv.atstart(Endpoint._startup)
@stub42

stub42 Sep 15, 2017

Collaborator

Endpoint._startup should not be called in 'restricted' mode, or metrics and similar hooks are going to become heavyweight. Maybe the charms.reactive module should grow a global restricted_mode flag, initialized by main, and Endpoing._startup checks for it.

@johnsca

johnsca Sep 15, 2017

Owner

_run_atstart isn't called in restricted mode, so it won't be.

charms/reactive/bus.py
@@ -65,18 +75,19 @@ class Handler(object):
_CONSUMED_FLAGS = set()
@classmethod
- def get(cls, action):
+ def get(cls, action, suffix=None):
"""
Get or register a handler for the given action.
:param func action: Callback that is called when invoking the Handler
:param func args_source: Optional callback that generates args for the action
@battlemidget

battlemidget Sep 20, 2017

Contributor

Does this need to be updated to reflect the name of the function argument?

charms/reactive/bus.py
@@ -93,14 +104,14 @@ def clear(cls):
"""
cls._HANDLERS = {}
- def __init__(self, action):
+ def __init__(self, action, suffix=None):
"""
Create a new Handler.
:param func action: Callback that is called when invoking the Handler
:param func args_source: Optional callback that generates args for the action
@battlemidget

battlemidget Sep 20, 2017

Contributor

Here to?

charms/reactive/helpers.py
@@ -17,6 +17,7 @@
import re
import json
import hashlib
+from types import SimpleNamespace
@battlemidget

battlemidget Sep 20, 2017

Contributor

best module ever

From what my limited knowledge of the code does it LGTM

johnsca added a commit to juju-solutions/interface-kube-control that referenced this pull request Sep 22, 2017

Refactor to use new Endpoint base class
This changes the layer to use the new Endpoint base class for interface
layers, from juju-solutions/charms.reactive#123.  Some of this could be
cleaned up a bit more with changes to the charm, but I wanted to keep it
backwards compatible for now.

This also requires juju-solutions/charm-flannel#38, which is available
in revision 32 and higher of the flannel charm in the charm store, due
to the changes in the reactive lib.

@johnsca johnsca referenced this pull request in juju-solutions/interface-kube-control Sep 22, 2017

Open

Refactor to use new Endpoint base class #13

Member

kwmonroe commented Sep 26, 2017

+1 on the recent docstring commit -- really nice job explaining wth is going on @johnsca!

Contributor

jamesbeedy commented Oct 25, 2017

@johnsca this is really great. One question, how might a peer interface take advantage of this new approach?

Collaborator

galgalesh commented Oct 27, 2017

I finally found time to look at this PR, I'm really excited about it!

I pushed some docstring changes, please let me know if I'm writing stuff that's incorrect. I feel that "publishing" is a good description of the behavior of relation-set, so I used that to describe the behavior of the send dictionary. What are your thoughts about changing the name of send to to_publish? I feel that explains its behavior better and it addresses the concern we had that send is not true to what actually happens behind the scenes.

I'd like some more time to look at it, show it to the team and write an interface myself, but what I've seen looks very good!

charms/reactive/altrelations.py
if value is None:
return None
relation_name = value['relation']
return cls.from_name(relation_name)
+
+
+class Endpoint(RelationFactory):
@galgalesh

galgalesh Oct 27, 2017

Collaborator

Maybe call it EndpointInterface to make explicit that this isn't an endpoint itself but the interface of an endpoint. Multiple endpoints of the same charm can use the same interface, and thus the same EndpointInterface class, right?

charms/reactive/altrelations.py
+ self._all_units = None
+
+ @property
+ def relation_name(self):
@galgalesh

galgalesh Oct 27, 2017

Collaborator

Shouldn't this be endpoint_name?

@johnsca

johnsca Oct 27, 2017

Owner

My thinking here (and for the Endpoint vs EndpointInterface above) was that the class represented the endpoint interface but specific instances represent actual relations (since they have an associated ID). But I'm also feeling like that distinction might be too esoteric, and really the name is associated with the endpoint itself, not a specific relation. So +1 on this and the above class name change.

charms/reactive/altrelations.py
+ """
+ return len(self.relations) > 0
+
+ def flag(self, flag):
@galgalesh

galgalesh Oct 27, 2017

Collaborator

It seems like this could be used to "document" the "public" flags of an interface layer, is that the case? If so, can you provide an example of how you would do that?

@johnsca

johnsca Oct 27, 2017

Owner

This was really more of a helper for populating the {endpoint_name} (I'm inclined to change that to match the method name, discussed above) portion in a consistent way, and to provide a shortcut if you didn't want to type that bit out all the time (probably a bad idea?). I'm not sure how it would be used to document public flags for the layer since they happen at runtime; can you explain your thoughts there more?

I had also toyed around with the idea of having some class-level structure to pre-define the flags, but you still couldn't populate the {endpoint_name} portion until runtime, so I'm not sure how we could make that work. But that would certainly give us a great way to be able to add doc strings for the flags themselves, perhaps even integrated into the definition itself.

charms/reactive/altrelations.py
+ return self._units
+
+ @property
+ def send_json(self):
@galgalesh

galgalesh Oct 27, 2017

Collaborator

What do you think about refactoring send_json -> send and send -> send_raw? This will make it more clear that the json variant should be used unless you need lower-level access to be compatible with an existing interface.

(to_publish and to_publish_raw if you agree on that change too)

@johnsca

johnsca Oct 27, 2017

Owner

+1 on both.

docs/charms.reactive.decorators.rst
+
+If a handler needs to use an older interface using the legacy
+:class:`~charms.reactive.relations.RelationBase`, or if you're uncertain which
+is used by an interface layer, you can also obtain an instance using
@galgalesh

galgalesh Oct 27, 2017

Collaborator

What do you mean with

if you're uncertain which is used by an interface layer

?

@johnsca

johnsca Oct 27, 2017

Owner

Well, old-style relation instances aren't populated into the global context (because they depend on which specific flag you're handling), so you have to use relation_factory (or the terrible ambiguous handler args) to get access to them. And whether an interface layer uses EndpointInterface or RelationBase is an implementation detail that the charm author shouldn't care about. This will be problematic during the transition, so I'm not sure how to handle it gracefully.

docs/charms.reactive.decorators.rst
+is used by an interface layer, you can also obtain an instance using
+:func:`~charms.reactive.relations.relation_from_flag`.
+
+For backwards compatibility, some decorators can pass instances of these
@galgalesh

galgalesh Oct 27, 2017

Collaborator

How do we handle the case when a handler doesn't accept instances but the decorator wants to pass one? (a "new" handler reacting to a flag of an "old" interface)

@johnsca

johnsca Oct 27, 2017

Owner

I added introspection logic to the invocation code so that, if the handler doesn't take any arguments, it will do the right thing and not pass them. You'd then have to use relation_factory or context.endpoints to access them, depending on how they're implemented (ugh). Though, the decorators will also pass the args through for EndpointInterface flags as well, so we'll probably end up having to continue to do that until everything is converted and then we'll still have the huge amount of inertia to move away (ugh).

Owner

johnsca commented Oct 27, 2017

@jamesbeedy I answered your question on IRC, but for posterity here...

Peer relations are handled exactly the same as other relations, with the only difference being you only need one class, in a peers.py file, rather than two (in requires.py and provides.py). The class itself will be the same, just publishing data and setting flags based on what data has been published. Also, peer relations are guaranteed to only ever have a single relation ID, which makes the helper methods (like all_units) particularly useful for them.

Contributor

jamesbeedy commented Oct 27, 2017

This is great. Thanks @johnsca

charms/reactive/altrelations.py
+ layer.
+
+ *Note that these flags can be used in the decorators of all handlers, not
+ just endpoint handlers, although this should be done with caution.*
@stub42

stub42 Oct 30, 2017

Collaborator

I would go further here. If a charm starts relying on these flags directly, then it stops the interface layer from changing how it manages them. The implementation of the relation has moved from the interface layer into the charm.

"These flags should only be used by the decorators of the endpoint handlers. While it is possible to use them with any decorators in any layer, the endpoing.* flags should be considered internal, private implementation details. It is the interface layers responsibility to manage and document the public flags that make up part of its API."

I would also be interested in the use cases for not having the framework manage the changed and departed flags automatically. Allowing them to persist beyond the current hook will cause problems, because the flag is implicitly tied to a particular remote unit and that remote unit will be gone in future hooks, or even worse may point to an incorrect unit with the potential to cause all sorts of mischief (eg. if you manage to defer a handler waiting on a departed flag until a future hook, the remote unit may actually be a unit that is just joining!).

charms/reactive/altrelations.py
+ relation. The name of the endpoint object is based on the endpoints
+ configured in `metadata.yaml`. Note that endpoints will only be created
+ **if the interface layer of that endpoint is included in the `layer.yaml`
+ file.**
@stub42

stub42 Oct 30, 2017

Collaborator

That is a complex enough description to warrent a short example. Using 'namespace' here is confusing, as to me namespaces are accessed using attribute lookup but in this context dictionary lookups are needed since relation names can contain hyphens, so maybe a reference to the NormalizingNamespace in use here (?) is needed.

I think this may also be incorrect, and it should be context.endpoints rather than context ? 'context' is a pretty awful name, with no indication of what it contains because it is... umm... context sensitive. I'm not sure why context is buried away in helpers when it seems to be a data structure forming the public charms.reactive API, rather than an actual helper.

@johnsca

johnsca Nov 1, 2017

Owner

I'm starting to wonder about the viability of the context pattern for this. As you point out, the name is a bit generic, it requires magic translation of the endpoint name to a valid attribute name, and it ends up exposing the internal implementation (whether the interface layer uses EndpointInterface or RelationBase) by virtue of whether a given endpoint can be accessed via the context or not (although this last is only an issue during the transition, that transition is likely to be long).

We'll probably need to go back to recommending relation_from_flag only.

charms/reactive/altrelations.py
+ file.**
+
+ Endpoint handlers can iterate over the list of joined relations for an
+ endpoint via the :meth:`~charms.reactive.altrelations.Endpoint.relations`
@stub42

stub42 Oct 30, 2017

Collaborator

Is :meth: correct markup here, since it is a property? I'd also just say relations rather than the full charms.reactive...Endpoint.relations path, since this isn't a class property. But I don't know this markup well.

@galgalesh

galgalesh Oct 30, 2017

Collaborator

This is correct markup and it gets formatted as relations, the full path isn't shown in the text but is used to create a url to those docs. PS: HACKING.md explains how you can build the docs locally.

@galgalesh

galgalesh Oct 30, 2017

Collaborator

But :attr: is better because then it doesn't show the () after the name. Fixed.

charms/reactive/altrelations.py
+ @classmethod
+ def from_flag(cls, flag):
+ if '.' not in flag:
+ return None
@stub42

stub42 Oct 30, 2017

Collaborator

This magic may deserve a comment. Why might I get flag without a '.', and why return None rather than an error?

@johnsca

johnsca Nov 1, 2017

Owner

This is just to match how RelationBase.from_flag and MinimalRelationBase.from_flag return None when a relation instance can't be derived from a flag. The only difference being that EndpointInterface.from_flag could return an instance even if the flag isn't set. +1 to adding a comment.

charms/reactive/altrelations.py
+ setattr(context.endpoints, relation_name, endpoint)
+ endpoint._manage_flags()
+ for relation in endpoint.relations:
+ hookenv.atexit(relation._flush_data)
@stub42

stub42 Oct 30, 2017

Collaborator

And we can schedule the removal of the changed* and departed flags here too, as they will stop being meaningful at the end of the hook when the remote unit changes.

charms/reactive/altrelations.py
+ Collection of `Relation`s that are established for this `Endpoint`.
+
+ This is a `KeyList`, so it can be iterated and indexed as a list,
+ or you can look up relations by their ID. For example::
@stub42

stub42 Oct 30, 2017

Collaborator

It turns out that relation ids are unique integers, and Juju is starting to report them as such in things like the new network-get tool. Perhaps we can convince Juju to stop doing that and creating confusion where none is needed. If we stick with KeyList here, I think it means that as far as charms.reactive is concerned relation ids will always be relname:int. Which is probably the sane thing to do, since relname:int is unambiguously a string while just the number could be a str or int.

@johnsca

johnsca Nov 1, 2017

Owner

I do think we should push back on having relation IDs reported as plain numbers, and I'm +1 to normalizing it in the framework if Juju does report them that way.

On the other hand, though, from the charm author's perspective, I would prefer relation IDs to be an implementation detail that they don't need to worry about, as much as possible.

charms/reactive/altrelations.py
+ If the flag does not already contain ``{relation_name}``, it will be
+ prefixed with ``endpoint.{relation_name}.``. Then, ``str.format`` will
+ be used to fill in ``{relation_name}`` with ``self.relation_name``.
+ """
@stub42

stub42 Oct 30, 2017

Collaborator

Its annoying that this is public. It is exposed in the public API that will be used by the charm, but is really intended for use by the interface implementation. Should we be using __ for the internal API, _ for the interface API to be used by subclasses, and public for the endpoing API to be used by the charm? Probably not if this is the only case, as the solution is worse than the (arguable) problem.

charms/reactive/altrelations.py
+ for unit in self.all_units:
+ for key, value in unit.received.items():
+ data_key = 'endpoint.{}.{}.{}.{}'.format(self.relation_name,
+ unit.relation.relation_name,
@stub42

stub42 Oct 30, 2017

Collaborator

How do you know what the relation_name is from the perspective of the remote unit? Or is this just repeating this ends relation_name twice? I would have thought we want the relation id here rather than the two relation names.

This will fail if the relation name (or relation id) contains a period. I don't know if that is possible.

charms/reactive/altrelations.py
+class Relation:
+ def __init__(self, relation_id):
+ self._relation_id = relation_id
+ self._relation_name = relation_id.split(':')[0]
@stub42

stub42 Oct 30, 2017

Collaborator

Yeah, it will be annoying if we need to support relname:x and x at every entry point :-( So lets just say relation ids are always relname:x

charms/reactive/altrelations.py
+ This relation's relation name.
+
+ This will be the same as the
+ :class:`~charms.reactive.altrelations.Endpoint`'s relation name.
@stub42

stub42 Oct 30, 2017

Collaborator

Should charms.reactive.altrelations be renamed charms.reactive.endpoints ?

charms/reactive/altrelations.py
+
+ To prevent circular references, the relation is kept as a weakref. If
+ the relation is garbage-collected before this property is accessed, it
+ will be ``None``.
@stub42

stub42 Oct 30, 2017

Collaborator

The weakref stuff is probably noise to users, a charms.reactive main() will keep the relation around. Consider moving it to a comment to avoid polluting the API docs. Or just drop the weakref and let Python3 deal with the circular reference eventually.

charms/reactive/altrelations.py
+ def __init__(self, items):
+ super().__init__(sorted(items, key=lambda i: (i.relation.relation_id,
+ i.unit_name)),
+ key='unit_name')
@stub42

stub42 Oct 30, 2017

Collaborator

This is sorting by relation id as a string, which will put db:10 before db:2. Similarly the unit_name, which you claim later is order by unit number. Consider key=lambda i: (int(':'.split(i.relation.relation_id)[1]), int('/'.split(i.unit_name)[1]))

@johnsca

johnsca Nov 1, 2017

Owner

Good catch, thanks.

charms/reactive/altrelations.py
+ data of all units in this list, with automatic JSON decoding.
+ """
+ if not hasattr(self, '_data'):
+ # NB: units are reversed so that lowest numbered unit takes precedence
@stub42

stub42 Oct 30, 2017

Collaborator

yer, if you want numerical order you need to do it above.

-def _short_action_id(action):
+def _short_action_id(action, suffix=None):
@stub42

stub42 Oct 30, 2017

Collaborator

Should suffix be optional? Its an internal method, so making it a required argument will help catch any call sites we missed.

@@ -65,18 +75,19 @@ class Handler(object):
_CONSUMED_FLAGS = set()
@stub42

stub42 Oct 30, 2017

Collaborator

Not sure why you want _CONSUMED_FLAGS here instead of _consumed_flags. All-caps is generally reserved for constants in Python. Similarly, _HANDLERS below.

johnsca and others added some commits Sep 14, 2017

Start of Endpoint implementation for easier interface layers
Beginning of the implementation for the [proposal][] for new style
interface layers using the `Endpoint` base, eschewing the reliance on
`@hook` in favor of `@when` everywhere, and removal of the
"conversations" metaphor.  This implements up to and including the
"Managed Flags" section.

[proposal]: https://github.com/juju-solutions/charms.reactive/blob/2.0/2.0-proposal/RELATIONS.md
Refactor Endpoint args, flag, and prefix
Change how args are handled to ensure handlers get the correct args
Change how Endpoints are derived from flags to avoid duplicate instances
Change from `relations.` prefix to `endpoint.` as per doc discussion
Document public API at top level
The internal organization of the library should not be part of the
public API.  Instead, we should export and document the parts of the
library that comprise the public API at the top level of the module.

@johnsca johnsca referenced this pull request in juju-solutions/interface-http Nov 21, 2017

Open

Convert to endpoints #10

Fix JSONUnitDataView.data attribute error
I renamed the attribute to `raw_data` to be more clear, but the base
class (UserDict) depends on it being called `data`, so I wrapped it in a
property.
Owner

johnsca commented Nov 22, 2017

Per @galgalesh's request, since the http interface is used as the example in the docs, I updated the http interface to use Endpoint in this PR: juju-solutions/interface-http#10

@johnsca johnsca merged commit fe06259 into master Nov 28, 2017

1 check passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment