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

enable saving config between steps in a single feature #103

Merged
merged 7 commits into from
Mar 2, 2021

Conversation

n3wscott
Copy link

@n3wscott n3wscott commented Feb 24, 2021

Changes

Looking for feedback on a method to pass data between steps in a feature.

Note that the interface introduced here is aligned with the implementation in pkg: https://github.com/knative/pkg/tree/master/kvstore

/kind enhancement

Relates to #66

@google-cla google-cla bot added the cla: yes Indicates the PR's author has signed the CLA. label Feb 24, 2021
@knative-prow-robot knative-prow-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Feb 24, 2021
@@ -52,6 +55,22 @@ func (s *Step) TestName() string {
}
}

// Save a value for use in a later feature step, or error on issue.
func (f *Feature) Save(ctx context.Context, t *testing.T, key string, value interface{}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious what the reasoning is for Save/Load vs. Set/Get? I found it a bit odd that we translate Save/Load into Get/Set underneath.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my thinking is that to me Save/Load semantically means save everything or load everything.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to make a helper method for getting and setting the keys tied up with failing the test, so maybe you have a better name for the method? Maybe I also call it Get and Set, note it is on the top level object, not the Store object.

import "context"

// This is a subset of knative.dev/pkg/kvstore.Interface
type Store interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of defining a separate interface, would it be bad if we did not define this new interface, but instead in the InMemory implementation, we return NonImplemented error instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was attempting to allow to use one of those impls, but not vendor it in directly because the Load and Init and Save did not make sense for Features

Copy link
Contributor

@slinkydeveloper slinkydeveloper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the approach overrall, but i don't like that i need to use feature to Load and Store. This approach with feature is not really modular, because you can't provide a stepfn that wants to push things inside the store without without knowing in advance the feature, breaking the encapsulation between feature and stepfn (for example, think about one of the recorder features pushing useful data inside the kv store when installing for later usage)

What I had in mind was something using the context, where the kv store lives in the context and this context is passed through the various phases of the execution:

	f.Setup("install sender", func(ctx context.Context, t *testing.T) {
		t.Helper()
		var to string
		var event cloudevents.Event
		storepkg.Load(ctx, t, "to", &to)
		storepkg.Load(ctx, t, "event", &event)
		from := feature.MakeRandomK8sName("sender")
		eventshub.Install(from, eventshub.StartSender(to), eventshub.InputEvent(event))
	})

(storepkg is the module where kvstore impl lives)

Also you should provide a Get method that returns interface{} too, so user can choose between the two "styles"

@n3wscott
Copy link
Author

n3wscott commented Mar 1, 2021

having the store be context based will not work because of how passing and threading work, it would have to be injected into the context higher up and then used as a pointer to another kvstore, which we can do as a followup that is feature based. So I say we start with this when I fix it up with the comments and we can look at fancy hooks to access the kvstore later? or maybe ... meh YOLO, brb.

@n3wscott n3wscott force-pushed the kvstore branch 2 times, most recently from 126e09c to 2a10dfe Compare March 1, 2021 19:12
@n3wscott
Copy link
Author

n3wscott commented Mar 1, 2021

ok please take another look, I took the request from @vaikas and @slinkydeveloper and made the feature integration look like this:

setup,

		state.SetOrFail(ctx, t, "event", FullEvent())
...
		state.SetOrFail(ctx, t, "to", to)

using,

		to := state.GetStringOrFail(ctx, t, "to")
		var event cloudevents.Event
		state.GetOrFail(ctx, t, "event", &event)

but nothing is stopping you from loading the KVStore directly:

s := state.FromContext(ctx)
s.Get/Set ...

@vaikas
Copy link
Contributor

vaikas commented Mar 1, 2021

/hold
Just holding in case @slinkydeveloper has more thoughts.
But otherwise, LGTM. Thanks!

@knative-prow-robot knative-prow-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 1, 2021
vaikas
vaikas previously approved these changes Mar 1, 2021
@knative-prow-robot knative-prow-robot added lgtm Indicates that a PR is ready to be merged. approved Indicates a PR has been approved by an approver from all required OWNERS files. labels Mar 1, 2021
pkg/state/context.go Show resolved Hide resolved
if !ok {
return fmt.Errorf("key %s does not exist", key)
}
err := json.Unmarshal([]byte(v), value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah wait i didn't saw it, why storing json? Can't we store just void pointers to things and then cast them when retrieving them? The state to store doesn't necessarily define conversion back and forth to json, and for some reason the user might even not be able to define such conversion (e.g. when you're using a struct from an external library as state)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I avoided this way to prevent mutation of the thing that is stored in the kvstore. Set, Get, mutate get'ed object, Get updated object?? is unexpected to me. cheap and cheerful way is json.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, but maybe what I want to store inside the kvstore is exactly a mutable object, like a list.

In any case, my problem with json is that you might end up storing objects which are not very json compatible but where marshalling doesn't fail, and you will lose some information, and together with it debugging time.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the impl can evolve over time, this is more about the interface and the current problemset is really "how do I pass that url to the next phase"

if you start working out ways of passing callbacks I will burn the building down.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

net.Url is a too simplistic example (and even with it, sometimes encoding and decoding it back and forth to json has weird behaviours). Any more complex state where json encoding/decoding is not defined won't work with this implementation.

if you start working out ways of passing callbacks I will burn the building down.

That's state of the test too 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is a kv store, if you can't save it to a config map, it will not work. That is the bar.

Copy link
Contributor

@slinkydeveloper slinkydeveloper Mar 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why are we using the kv store model for something which is always in memory by design? It's an in memory map, it's the state of the test, it's not something that has to be read/write to a config map.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it might let us do some really interesting things later, like retries or test startup in the middle, or capture test state.

@knative-prow-robot knative-prow-robot removed lgtm Indicates that a PR is ready to be merged. approved Indicates a PR has been approved by an approver from all required OWNERS files. labels Mar 2, 2021
@n3wscott
Copy link
Author

n3wscott commented Mar 2, 2021

Ready.

/unhold

@knative-prow-robot knative-prow-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Mar 2, 2021
func ContextWith(ctx context.Context, s Store) context.Context {
return context.WithValue(ctx, envKey{}, s)
}

// FromContext returns the Store from Context, if not found FromContext will
// panic.
// TODO: revisit if we really want to panic here... likely not.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO panic is absolutely fine here, the Store must always exists in the context passed to StepFn. If it's not there, it's a reconciler-test bug

if !ok {
return fmt.Errorf("key %s does not exist", key)
}
err := json.Unmarshal([]byte(v), value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, but maybe what I want to store inside the kvstore is exactly a mutable object, like a list.

In any case, my problem with json is that you might end up storing objects which are not very json compatible but where marshalling doesn't fail, and you will lose some information, and together with it debugging time.

}

// Set marshals and sets the value given under specified key.
func (cs *KVStore) Set(_ context.Context, key string, value interface{}) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect users to check the error everytime and fail the StepFn if the Set/Get returns an error? Isn't that too much and makes the usage unnecessary complicated? Can't we just panic in case of an error? These Get/Set errors are not recoverable anyway.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am matching a subset of this interface: https://github.com/knative/pkg/blob/master/kvstore/kvstore.go#L23

Copy link
Contributor

@slinkydeveloper slinkydeveloper Mar 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's an implementation, i guess you can always have the GetOrFail and SetOrFail

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't do that because KVStore is not used, it is Store, and Store is the pkg kvstore interface.

I have added methods like this to the context.


import "context"

// This is a subset of knative.dev/pkg/kvstore.Interface
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: @slinkydeveloper ^^

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason why we're trying to match that particular interface about config maps?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that we can swap out this impl for the same that is used in pkg.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are trying to make synergy here, not reboil the world. I made the interface a subset because some of those things did not make sense in the context of features, but I want to use that impl in pkg at some point later.

@knative-prow-robot knative-prow-robot added the lgtm Indicates that a PR is ready to be merged. label Mar 2, 2021
@knative-prow-robot
Copy link

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: vaikas

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@knative-prow-robot knative-prow-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Mar 2, 2021
@knative-prow-robot knative-prow-robot merged commit c687b5f into knative-extensions:master Mar 2, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cla: yes Indicates the PR's author has signed the CLA. kind/enhancement lgtm Indicates that a PR is ready to be merged. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants