-
Notifications
You must be signed in to change notification settings - Fork 901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Order specifications for hooks #4168
Order specifications for hooks #4168
Conversation
It needs to use a non-clashing name for the internal var. Signed-off-by: Rusty Russell <[email protected]>
8809845
to
0c1d19b
Compare
This addresses #4005 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 0c1d19b
doc/PLUGINS.md
Outdated
"after" arrays of plugin names, which control what order they will be | ||
called in. If a plugin name is unknown, it is ignored. | ||
|
||
if the plugins names are unknown, they are ignored, otherwise if the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this repeats the last line of the previous paragraph? i'd remove one.
json_tok_full_len(t), | ||
json_tok_full(buffer, t)); | ||
name = json_strdup(tmpctx, buffer, nametok); | ||
beforetok = json_get_member(buffer, t, "before"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it took me a second to fully grasp the implication of 'before' and 'after'. Renaming them to "run_before" and "run_after" might be more immediately obvious?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more obvious in context, than when reading in a patch, e.g.:
"hooks": [
{"name": "htlc_accepted",
"before": ["someplugin.py"],
"after": ["foo.py"]}
]
The next patch will use these to order the hooks. Signed-off-by: Rusty Russell <[email protected]> Changelog-Added: plugins: hooks can now specify that they must be called 'before' or 'after' other plugins.
Signed-off-by: Rusty Russell <[email protected]>
0c1d19b
to
7dd4c39
Compare
concept ack @rustyrussell as you wrote in 4005
It is also very helpful to give the user an option for plugin ordering (unless otherwise specified by the devs via before/after) by checking commandline and config order of plugins. The rationale behind is it the following: Before and after list are to be maintained by the plugin developers. Before that happens, a user typically has the problem first and needs to wait for the devs to to make the changes so his specific setup works. Since the devs will often not know all the plugin combinations out in the wild this can take a while or, in the worst case, never happens. So I think we might also add the commandline/config order as 'last resort/default' if no before/after ordering has been specified for the plugins. For that we need a stable sorting algorithm and feed it with the initial order of plugins from commandline/config. |
The current implementation has this effect, in fact. Technically it's not quite a stable sort, as dependencies can pull in things out of order, but we could fix that (I doubt anyone will notice, though). For example, assume plugins in alphabetical order (they're in directory-sort order usually BTW). A has to be before D, and B has to be before C. In this case, algorithm will go A B D C. Easy to fix though. |
OK, fixed to make the default ordering equal to the specified one. Also, removed a problem where we'd warn about missing dependencies since we evaluated them as we loaded. This was more invasive, but I think simplifies the code a little anyway. |
Thanks @rustyrussell Super wired, I stress tested your changes a little, but it seems there is an issue with the default commandline/config order (unless otherwise by before/after specified). How to reproduce
What happensThe flaky test can fail. When we look in the logs of l2 we see that the 'reject for a reason' hook is not called but 'reject by design', which should never happen if correct default order would be applied. The patch for the test that uses the new feature
|
Argh, good catch: final patch actually fixes the problem, and for some reason I didn't push it! |
And I turned your patch into a commit, credited you and appended. Thanks! |
works like a charm |
@rustyrussell regarding IRC chat:
Can you elaborate on this? I was thinking that it would be sufficient to have the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some minor questions for my own understanding, and a tiny python bug, otherwise a very solid PR 👍
before: List[str] = [], | ||
after: List[str] = []) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is dangerous: never use a mutable instance as the default argument. They'll be shared between invocations.
https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments
This should be before: List[str] = None
and then check for None
in the body.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, nice catch, thanks!
/* If this was last one manifests were waiting for, handle deps */ | ||
if (p->plugin_state == AWAITING_GETMANIFEST_RESPONSE) | ||
check_plugins_manifests(p->plugins); | ||
|
||
/* If this was the last one init was waiting for, handle cmd replies */ | ||
if (p->plugin_state == AWAITING_INIT_RESPONSE) | ||
check_plugins_initted(p->plugins); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be a weird place to perform these checks. We look at a random plugin being destroyed and if that was in a specific state we assume it must have been the last one in this state? We should kick this up to the set of plugins and perform the check there after removing the plugin to be destroyed. No need to replicate the last-one-turns-off-the-light test here :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we've already unlisted the plugin at this point. We could, and probably should simply call check_plugins_xxx() unconditionally here.
I wonder if we should have a plugin_set_state() which calls these internally. Less efficient, but clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the payment_continue
function that is used to continue in a state machine? I'm all for it ^^
lightningd/plugin_hook.c
Outdated
tal_resize(s, s_num + 1); | ||
memmove(*s + pos+1, *s + pos, sizeof(**s) * (s_num - pos)); | ||
(*s)[pos] = n; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite follow why this is working: we're striking dependencies off of the graph node until we finally find that it has no more, then add it to the list. However, before this change we added it to the end of the list, which was always safe (no dependency could jump over us). After this change we suddenly use insertion sort on the index
at a potentially earlier position in the list, meaning we could end up being inserted before one of our dependencies. What am I missing here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right!
We should instead gather all the things we are going to add from one edge, and order those then add as a group. That means if B and C depend on A, we append A, then sort (B,C) and append those.
I will write a test which triggers this first...
I ended up reworking and thus simplifying the algo. It's less efficient (O(N^2)) but nobody cares. |
before: List[str] = None, | ||
after: List[str] = None) -> JsonDecoratorType: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now becomes an Optional[List[str]]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for forgetting this in my first round of comments :-)
if before: | ||
method.before = before | ||
if after: | ||
method.after = after |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should always set these, otherwise we'd have to check with hasattr
whether the hook has that field.
method.before = [] if before else before
method.after = [] if after else after
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, mypy didn't like that, so went for an open-coded variant.
struct hook_node { | ||
bool finished; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the semantics of finished
? It's only used in sorting so I'd assume it means that it is in its final position.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Included in sorted results. I'll add a comment
if (graph[i].num_incoming != 0) | ||
continue; | ||
if (!best | ||
|| best->hook->plugin->index > graph[i].hook->plugin->index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're never moving the plugin in the graph
, so under no circumstance should this ever be true (a later iteration should always have a higher index
). Since that's the case I think we can remove the index
altogether, and break/return after finding a first best
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's making an assumption that graph is in index order. It (I'm fairly sure) starts that way, but can be perturbed by previous calls to plugin_hook_make_ordered. If plugin B says it has to be before A and after C, we'll go C B A. If B gets removed, we should return to A C.
Does anyone care? Probably not, but this way is clear, at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On further thought, if we define it as "minimum perturbation to achieve required ordering" which is a totally reasonable goal, we can indeed do this simplification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... Nope. Hooks start in "who returned from getmanifest" order which is not the same as "order specified by config" order.
Hm, I restarted a travis flake which I couldn't reproduce locally also not by running it 100 times and parallel: It was complaining about unclean node shutdown, but it succeeded upon first restart/retry. |
@rustyrussell still (sorry for bugging):
|
689c8aa
to
7defce9
Compare
We should make it so every hook can be registered by multiple plugins.
I agree, at the moment. But hooks are the only place where plugins interact, so I'd rather see explicitly what they care about. If we ever get order conflicts, it's easier to see what they want when they have priorities on specific hooks. |
7defce9
to
fe6b1e0
Compare
Trivial rebase to fold in fixups and add a comment on |
And use that to add simple tests. Signed-off-by: Rusty Russell <[email protected]>
Signed-off-by: Rusty Russell <[email protected]>
Now both python and c libraries are updated, we can officially deprecate the old form. Signed-off-by: Rusty Russell <[email protected]> Changelog-Deprecated: plugins: hooks should now be specified using objects, not raw names.
…rtup. We fail this, because we check dependencies as they come in. Signed-off-by: Rusty Russell <[email protected]>
This means we need to stop at this stage even in the runtime-loaded case. Signed-off-by: Rusty Russell <[email protected]>
…hange. Suggested-by: @mschmook Signed-off-by: Rusty Russell <[email protected]>
We previously registered hooks up in who-replies-to-getmanifest-first order, but then if any had dependencies it would scatter that order. This allows users to manually set dependencies developers have forgotten by specifying the plugins manually in their configuration or cmdline. This was an excellent consideration by @mschmook. Signed-off-by: Rusty Russell <[email protected]>
I think this is what Travis is having an issue with, but it work fine locally. Signed-off-by: Rusty Russell <[email protected]>
fe6b1e0
to
7d41997
Compare
absolutely yes, I missed the point that you were referring to 'unchained' hooks. |
@@ -678,14 +678,25 @@ declares that it'd like to be consulted on what to do next for certain | |||
events in the daemon. A hook can then decide how `lightningd` should | |||
react to the given event. | |||
|
|||
When hooks are registered, they can optionally specify "before" and | |||
"after" arrays of plugin names, which control what order they will be |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General question about 'plugin names': Is this same as for cli plugin start/stop
commands, meaning either full path (useless for this feature) or basename? If so, we may note that the plugin name includes its extension, meaning full filename.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, filename or full path name.
ACK 7d41997 |
This allows hooks to specify they must be before/after some other plugins. Either by basename or full pathname.
As our plugin ecosystem grows and gets more sophisticated, such conflicts will inevitably occur where multiple plugins want to ensure they are called in certain order.
We could implement a priority number system, but that's usually just window-dressing on what people really want (though, unlike this, would allow you to specify that you must be first, or last: we could add wildcards if we wanted to).
Getting this in now means that after a few releases, plugin authors will simply be able to assume its existence.
Fixes: #4005 (thanks @m-schmoock for pointing that out!)