Glitch typing #2

Merged
merged 12 commits into from Oct 17, 2016

Conversation

Projects
None yet
3 participants
Contributor

bcsaller commented Oct 16, 2016

Before I take this too far I was to see if people like the approach. It attempts to move from an ad-hoc typing system to an officially supported one. Currently it is using runtime type validation but we might want to look at switching the type checking to mypy and hook in its static analysis at the time the plan is formed. That said that is still runtime check, so I am fine with this now.

This also moved the test under the top level test directory. I like to separate the tests from the runtime as they sometimes have different deps and testing deps won't be packaged with the release.

petevg and others added some commits Oct 11, 2016

Made selectors better.
We now have a way to validate that selectors chain into each other, and
to validate that we start with a valid starting selector.
units -> objects
Renamed to indicate that we might not be dealing with units. Also
actually pass objects in to the action.
Glitch now actually uses libjuju.
As of this commit, it is still broken -- it generates an invalid plan,
and when that is fixed, will probably run invalid actions against that
plan. But it's a start :-)
Glitch now generates valid plans.
Tests now also pass, though they contain many cheats (we don't really
dispatch actions right now, for example).
Cleaned up actions.
Reworked ACTION_MAP into Actions class, along the same lines as the
Selectors class. Added infrastructure that runs the action function on
each item in the set, and can eventually do more automagic around the
action. Fixed some mistakes in plan.py and main.py that the actions
refactor surfaced.

Refactored the tests so that the actions actually run. This needs more
work: we don't actually catch failures, or do the other useful things
that matrix does in its more fleshed out event loop.

I really like the type hinting!

Pete also made the move into matrix/plugins/glitch/ at my behest in the other branch, so you might want to check against the original branch to make sure you didn't miss any other changes of note.

+ @wraps(func)
+ async def wrapped(model, objects, **kwargs):
+ for obj in objects:
+ await func(model, obj, **kwargs)
@johnsca

johnsca Oct 17, 2016

Owner

I think it would be better to have the actions explicitly accept sets of objects, rather than implicitly converting them by calling the action multiple times, because some implementations (e.g., juju run or juju run-action) can handle multiple objects natively, which would be more efficient.

However, since the calls will be dispatched async, it's not a big enough deal to consider a blocker.

@bcsaller

bcsaller Oct 17, 2016

Contributor

I was hoping to avoid that all action include the boilerplate for doing set iteration. In addition when we add things like inflight, interval and max_retries I didn't want each action to have to carry support for those, Being able to manage the individual options seems the fix for that.

That said we can change the calling protocol to include (set, item in set) for action and support a StopIteration return that says "I dealt with the entire set"

@johnsca

johnsca Oct 17, 2016

Owner

That's a fair point. I'm fine with sticking with the current approach to ease single-point-of-handling of those options. That functionality is clearly not compatible with the actions handling multiple items themselves.

+# Define your actions here
+#
+@action
+async def reboot(model: Model, unit: Unit):
@johnsca

johnsca Oct 17, 2016

Owner

Note for self: If an action can accept multiple types (e.g., units or applications), it can use Union (e.g., Union[Unit, Application]).

+
+
+# XXX: this most likely will need an async def
+# depending on libjuju
@johnsca

johnsca Oct 17, 2016

Owner

I don't think so. All this is doing is accessing attributes on the model. The model is updated in the background by a watcher, so accessing the attributes doesn't make any calls to the API and is entirely synchronous.

@bcsaller

bcsaller Oct 17, 2016

Contributor

Yeah, I was thinking if things like health checks would be different or not, but we can ask that they populate the model the same as the watchers do perhaps.

Other than some quibbles with naming conventions, which might just be a style difference (I prefer to be as verbose as I can be, while still keeping lines to 80 characters), and what I think is an error in the loop in main.select, this looks good to me.

+ return wrapped
+
+# Public singleton
+Actions = _Actions()
@petevg

petevg Oct 17, 2016

Collaborator

Probably should be actions or ACTIONS, esp. since the class is basically just a dict in a Singleton's clothing; If we're going to get rid of the classmethods, might as well get rid of the ugly camel case, too. :-)

@bcsaller

bcsaller Oct 17, 2016

Contributor

I agree, uppercase is fine for what is really a global. Also making change s/Selectors/SELECTORS/

+def default_resolver(model, kind, name):
+ if kind not in ["application", "unit", "model", "controller", "relation"]:
+ return None
+ entities = getattr(model, kind + "s")
@petevg

petevg Oct 17, 2016

Collaborator

I think that, whenever you are tempted to call any of the *attr methods, you should think carefully about the approach, as it tends to mean that you are about to write code that is difficult to reason about, or that is going to do surprising things with unexpected input (you do guard against this in the line above).

I can see why it is compelling to have some automagic to translate some strings to objects in the model (though this doesn't address the needs of the units selector, which might want to act on multiple applications). It does start us down the path of treating the plan file as a DSL, and parsing it into executable code.

@bcsaller

bcsaller Oct 17, 2016

Contributor

I think maybe a lookup table rather than a str manipulation is better here, but the basic issue remains.

+ args = [model]
+ # This can raise many an exception
+ for selector in selectors:
+ data = selector.copy()
@petevg

petevg Oct 17, 2016

Collaborator

I think that you should feel free to just do selector = selector.copy() here. We don't need the old reference, and it makes the code below more readable if the person reading it doesn't have to keep track of what "data" is.

@bcsaller

bcsaller Oct 17, 2016

Contributor

agreed

+ # This can raise many an exception
+ for selector in selectors:
+ data = selector.copy()
+ m = Selectors.get(data.pop('selector'))
@petevg

petevg Oct 17, 2016

Collaborator

Would prefer the original .func, or .get_func, just as a semantic reminder that you are getting an executable. Makes the code easier to review and contribute to :-)

@bcsaller

bcsaller Oct 17, 2016

Contributor

I named the return select_func, maybe that helps.

+ data[k] = o
+
+ #print(m, args, data)
+ cur = m(*args, **data)
@petevg

petevg Oct 17, 2016

Collaborator

You need to update *args to include objects here, correct?

... or get rid of the generic args argument, and explicitly pass in model and objects.

@johnsca

johnsca Oct 17, 2016

Owner

Not objects, but cur. But I'm +1 to making it explicit, as cur = m(model, cur, **data)

@johnsca

johnsca Oct 17, 2016

Owner

To enable making the call explicit, the selectors would need to change to always accept the "working set" param, even for "top-level selectors" like units.

@petevg

petevg Oct 17, 2016

Collaborator

johnsca: Yes. I was thinking in terms of last week's pass at this, where I was abusing the dynamic typing of Python to slip in the "nounits" arg, and keep all the selectors consistent in terms of the number of positional arguments that they accept. Since we're being more formal here, the dancing about with args bit makes sense.

+
+ #print(m, args, data)
+ cur = m(*args, **data)
+ args = [model, cur]
@petevg

petevg Oct 17, 2016

Collaborator

Edit: nm. my eyes had missed the indentation level (I thought that we were returning after this statement).

This code doesn't do anything. Did you initially intend to return args here, instead of just returning cur?

@johnsca

johnsca Oct 17, 2016

Owner

It does do something: it adds the working set to the positional params. Currently, "top-level" selectors like units don't actually accept an initial working set; thus, the objects list here is ignored. I think making the working set explicit, required, and initialized by objects is a good idea.

@petevg

petevg Oct 17, 2016

Collaborator

johnsca: aha. I missed where we were in the nesting. You're correct.

+ """
+ rule.log.info("Starting glitch")
+
+ output_filename = DEFAULT_PLAN_NAME.format(time.time())
@petevg

petevg Oct 17, 2016

Collaborator

If we run a lot of tests in a row, we'll wind up spamming the temp dir with executed plans. I think that I'd rather just write out a single plan file, and leave it up to the tester/tooling to make backups of past runs if they desire.

@bcsaller

bcsaller Oct 17, 2016

Contributor

I am annoyed by my impl as well, reverting :)

+
+class _Selectors(dict, metaclass=Singleton):
+ def decorate(self, f):
+ wrapper = enforce.runtime_validation(f)
@petevg

petevg Oct 17, 2016

Collaborator

I'm always wary when I see type checking in Python -- it feels like an argument for not using Python :-)

In this case, I had already begun to add some loose "guarantees" to the code, and this does so more formally, without needing to trust the programmer as much. I think that it makes sense, even if it does make me want to just port the project to Scala :-)

@johnsca

johnsca Oct 17, 2016

Owner

I don't understand the sentiment that adding type checking implies we should give up Python entirely. As you mentioned, we were already starting to do ad-hoc type checking, and there are plenty of other reasons to use Python over Scala.

Type hints are now a standard feature of Python, albeit optional, and it we should use it when it makes sense. One of Python's strengths has always been the willingness to adopt ideas from other languages and paradigms when they make sense, while still allowing flexibility to not use them when they don't.

+ # if there are string names being passed (from a serialized plan for
+ # example) we must resolve them relative to the current model. This is
+ # pluggable using a resolver object which takes a model,
+ cur = None
@johnsca

johnsca Oct 17, 2016

Owner

I think Pete's right, and this should be initialized to (or just replaced entirely with) objects

@johnsca

johnsca Oct 17, 2016

Owner

Also, I'd like to push for calling this var working_set instead of cur or objects.

@bcsaller

bcsaller Oct 17, 2016

Contributor

I don't understand the first comment here, let's circle back on this.

+
+ rule.log.info("Writing glitch plan to {}".format(output_filename))
+ with open(output_filename, 'w') as output_file:
+ output_file.write(yaml.dump(glitch_plan))
@johnsca

johnsca Oct 17, 2016

Owner

I'd like to push for writing the plan to disk just after validating it, before executing it.

@bcsaller

bcsaller Oct 17, 2016

Contributor

done

@bcsaller bcsaller merged commit 9f6bcee into master Oct 17, 2016

@bcsaller bcsaller deleted the glitch_typing branch Oct 17, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment