-
Notifications
You must be signed in to change notification settings - Fork 345
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
Allow ordered operations via plugin order #627
Conversation
This change introduces the concept of plugin order. It is backwards compatible since the default value is 0 which can execute with without precondition. A plugin can only be executed after all plugins of lower order have reported results. Fixes #39 Signed-off-by: John Schnake <[email protected]>
go func() { | ||
pluginsRun := map[int]bool{} | ||
for { | ||
for index, p := range 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.
Previously we just synchronously started each plugin and now we just iterate over them checking if we can start them or not.
- uses a map to check if we started each plugin (just using index of the plugin as a key)
- didnt want to alter the slice of plugins since that is used elsewhere
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.
Where do we keep status of each plugin? Could you create a struct to store all the bits about the plugin (i.e. current status, start time, etc).
} | ||
|
||
// Sleep for a shorter period before rechecking plugins. | ||
time.Sleep(10 * time.Second) |
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.
Arbitrary; just created as poc. Would make a constant but honestly 10s seems fine to me since its just doing some iterations on a map its not an expensive operation to do every few sec. Especially since the # of plugins is usually very small
@@ -189,6 +217,11 @@ func Run(client kubernetes.Interface, plugins []plugin.Interface, cfg plugin.Agg | |||
} | |||
} | |||
|
|||
func canStartPlugin(a *Aggregator, p plugin.Interface) bool { | |||
logrus.Infof("schnake: p.order %v current order: %v", p.GetOrder(), a.currentOrder()) |
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.
debug line
@@ -189,6 +217,11 @@ func Run(client kubernetes.Interface, plugins []plugin.Interface, cfg plugin.Agg | |||
} | |||
} | |||
|
|||
func canStartPlugin(a *Aggregator, p plugin.Interface) bool { |
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.
Even though the logic is small I wanted to put it here so that it would be easier to test/manipulate in the future.
@akutz what do you think about simple support of ordered operations like this? See the ticket for the reasoning I had on doing a small version like this; tldr is that I think this unlocks a lot of options for users for minimal effort. I think implementing a full/robust dependency logic and linking runs together will be about 50x harder and (as of now) I've not heard of someone saying they need it (as opposed to just stating they needed ordered ops) |
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.
Hi @johnSchnake,
what do you think about simple support of ordered operations like this?
I think I tried to do this once for muxers as part of HTTP request chain logic, and maintaining a dependency graph is a huge headache. A simple non-branching order is much easier to handle 👍
if err = p.Run(client, cfg.AdvertiseAddress, cert); err != nil { | ||
return errors.Wrapf(err, "error running plugin %v", p.GetName()) | ||
go func() { | ||
pluginsRun := map[int]bool{} |
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.
Hi @johnSchnake,
Suggesting the following:
pluginsRun := map[int]struct{}
Anonymous (or even empty) structs do not consume any memory in Go. If you're using a map as an addressable flag set, anonymous structs should be used as the value type. Dave Cheney wrote a great piece on this a few years back.
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.
@akutz , thanks. Will do. I'm aware of the pattern, I think this is just a bit left over from iterations on the theme.
// If unable to run plugin, wait. | ||
if !canStartPlugin(aggr, p) { | ||
logrus.Info("schnake: cant start plugin, continue") | ||
continue |
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.
Hi @johnSchnake,
This continue
has no sleep, and neither does the loop's beginning. Thus if the plug-in is unable to start, and there's only one plug-in, it will immediately try again. Is this desired?
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, thanks for the find. When fixing up the PR I'll be sure to move the sleep (or something) around appropriately.
return errors.Wrapf(err, "error running plugin %v", p.GetName()) | ||
go func() { | ||
pluginsRun := map[int]bool{} | ||
for { |
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.
Hi @johnSchnake,
You may want to consider using a Go context and channels to select on a channel buffered to the size of the plug-ins and ctx.Done
in case the operation is cancelled.
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.
Hi @akutz , that is an interesting idea but would that actually be necessary here since all this is running in containers in the cluster? A "cancel" in that context would mean the user is stopping the run and we are just blowing up the container entirely, right? Or is there another way you can see that being used?
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.
Hi @johnSchnake,
I didn't have any clear idea on how this was being used. When I see the for {
pattern I usually ask myself if it should be cancellable, and if so, does it make sense to use a Go context. You know the call stack better than I do, so please do whatever you think makes sense.
@@ -189,6 +217,11 @@ func Run(client kubernetes.Interface, plugins []plugin.Interface, cfg plugin.Agg | |||
} | |||
} | |||
|
|||
func canStartPlugin(a *Aggregator, p plugin.Interface) bool { |
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.
Hi @johnSchnake,
Just curious, why isn't this a private function on *Aggregator
?
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 reason; 6 on one hand half a dozen on the other IMO. Under the hood its all the same so it just came down to my random train of thought at the moment of writing it.
@@ -66,6 +66,11 @@ func (b *Base) GetName() string { | |||
return b.Definition.Name | |||
} | |||
|
|||
// GetIOrder returns the order of this Job plugin. |
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.
Hi @johnSchnake,
// GetIOrder
-> // GetOrder
Also, consider rewriting the function signature as func (b Base) GetOrder() int
since there is no reason for the function to be able to write any values to the address of b
. A read on a copy will suffice and offers better protection.
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 the rename.
As far as the pointer, I agree with the unencessity since it is a read. However, go best practice states that you shouldn't have a mixture of pointer/value receivers for a given type. Since other pointer receiver methods exist (and I wasn't planning on changing them all, nor do I think I could with all the implications because this is used by all the other plugin types). (also fwiw I hate linking to the tour as a 'best practice' reference but I can't find the original link I'm thinking of. We can talk more about this if it is a serious concern to you)
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.
Hi @johnSchnake,
I'm not overly concerned about this, but as long as there's a clear pattern to when things read and when things write, I think it's fine. All of GoVmomi does it. Read functions use byCopy receivers, and write functions use byAddress receivers.
I don't feel strongly either way. Just noticed it.
func (b *Base) GetOrder() int { | ||
return b.Definition.Order | ||
} | ||
|
||
// GetSecretName gets a name for a secret based on the plugin name and session ID. | ||
func (b *Base) GetSecretName() string { |
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.
Hi @johnSchnake,
Consider rewriting the function signature as func (b Base) GetSecretName() string
since there is no reason for the function to be able to write any values to the address of b
. A read on a copy will suffice and offers better protection.
@@ -63,7 +63,10 @@ func NewPlugin(dfn plugin.Definition, namespace, sonobuoyImage, imagePullPolicy | |||
// a Job only launches one pod, only one result type is expected. | |||
func (p *Plugin) ExpectedResults(nodes []v1.Node) []plugin.ExpectedResult { |
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.
Hi @johnSchnake,
Consider rewriting the function signature as func func (p Plugin) ExpectedResults(nodes []v1.Node) []plugin.ExpectedResult
since there is no reason for the function to be able to write any values to the address of p
. A read on a copy will suffice and offers better protection.
@@ -100,6 +100,22 @@ func (a *Aggregator) isComplete() bool { | |||
return true | |||
} | |||
|
|||
// currentOrder returns the order of lowest order plugin which doesn't have a result yet. If all have results then -1 is returned. | |||
func (a *Aggregator) currentOrder() int { |
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.
Hi @johnSchnake,
Please consider rewriting the function signature as func (a Aggregator) currentOrder()
since there is no reason for the function to be able to write any values to the address of a
. A read on a copy will suffice and offers better protection.
I do not see where the result Order is set. It seems to be related to the plug-in order. If plug-ins have a defined order, and the number of plug-ins is known, does that also mean the number of expected results are known? If so, then you could also maintain an analogue slice of addresses of the expected results that is sorted by order and use a Search
function from the stdlib sort
package. It may be overkill to do so though. I don't know whether or not the list is ever sufficiently large that keeping a second copy of addresses in memory is worth a faster search result.
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.
The list is usually very small; default is 2 and I could realistically imagine 5-10 being common for someone who is trying to run all their own custom, small plugins on each run. Even in a weird application I can't see it being an orders of magnitude higher to where we are concerned with the performance of a sort.
@akutz Thank you so much for both the high and low-level review. I'm going to consolidate what I've learned and done into a design doc to clarify some design details before finalizing things in this PR (or a related one). A little odd to do a POC, then design doc for what is such a small feature in terms of LOC but I think something like this is pretty important to get right the first time. 💯 for your time and detail. |
There has not been much activity here. We'll be closing this issue if there are no follow-ups within 15 days. |
What this PR does / why we need it:
This change introduces the concept of plugin order. It
is backwards compatible since the default value is 0
which can execute with without precondition.
A plugin can only be executed after all plugins of lower
order have reported results.
Signed-off-by: John Schnake [email protected]
Which issue(s) this PR fixes
Fixes #39
Special notes for your reviewer:
Release note: