Skip to content
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

Closed
wants to merge 1 commit into from
Closed

Allow ordered operations via plugin order #627

wants to merge 1 commit into from

Conversation

johnSchnake
Copy link
Contributor

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:

Add support for ordered operations via plugin "order" field.

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]>
@johnSchnake johnSchnake requested a review from stevesloka March 11, 2019 18:54
go func() {
pluginsRun := map[int]bool{}
for {
for index, p := range plugins {
Copy link
Contributor Author

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

Copy link
Contributor

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)
Copy link
Contributor Author

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())
Copy link
Contributor Author

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 {
Copy link
Contributor Author

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.

@johnSchnake johnSchnake requested a review from akutz March 11, 2019 19:53
@johnSchnake
Copy link
Contributor Author

@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)

Copy link

@akutz akutz left a 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{}
Copy link

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.

Copy link
Contributor Author

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
Copy link

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?

Copy link
Contributor Author

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 {
Copy link

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.

Copy link
Contributor Author

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?

Copy link

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 {
Copy link

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?

Copy link
Contributor Author

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.
Copy link

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.

Copy link
Contributor Author

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)

Copy link

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 {
Copy link

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 {
Copy link

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 {
Copy link

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.

Copy link
Contributor Author

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.

@johnSchnake
Copy link
Contributor Author

@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.

@stale
Copy link

stale bot commented Nov 4, 2020

There has not been much activity here. We'll be closing this issue if there are no follow-ups within 15 days.

@stale stale bot added the misc/wontfix label Nov 4, 2020
@stale stale bot closed this Nov 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow for user ordered operations in aggregator
3 participants