Skip to content
This repository has been archived by the owner on Oct 17, 2024. It is now read-only.

New watch api #122

Closed
wants to merge 34 commits into from
Closed

New watch api #122

wants to merge 34 commits into from

Conversation

asdine
Copy link
Contributor

@asdine asdine commented May 22, 2019

This PR refreshes the Watch API according to what has been decided in issue #112

The Watch API is now accessible under the following endpoint:

// URL
POST /rulesets/?watch

// Optional query params
?revision

// Payload
{
  "paths": [
    "pathA",
    "pathB",
   ],
  "revision": 10
}

The body specifies which paths you want to watch.
An empty body means watch everything.

This PR also contains a few improvements in the server package

@asdine asdine requested review from tealeg, rogpeppe and yaziine May 22, 2019 15:00
Copy link
Contributor

@rogpeppe rogpeppe left a comment

Choose a reason for hiding this comment

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

This looks really nice in general, thanks. LGTM modulo a bunch of minor comments and suggestions, and one likely bug...

api/etcd/rulesets_test.go Outdated Show resolved Hide resolved
api/etcd/watch.go Outdated Show resolved Hide resolved
api/etcd/watch.go Outdated Show resolved Hide resolved
// Watch the given prefix for anything new.
func (s *RulesetService) Watch(ctx context.Context, prefix string, revision string) (*api.RulesetEvents, error) {
// Watch a list of paths for changes and return a list of events. If the list is empty or nil,
// watch all paths. If the revision is empty, watch from the latest revision.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// watch all paths. If the revision is empty, watch from the latest revision.
// watch all paths. If the revision holds a non-negative decimal number, Watch
// will block until the revision is greater than that number, otherwise
// it will return immediately with the initial events.

?

api/etcd/watch.go Show resolved Hide resolved
http/server/api.go Outdated Show resolved Hide resolved
@@ -50,6 +39,15 @@ func (s *rulesetAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.get(w, r, path)
return
case "POST":
if _, ok := r.URL.Query()["watch"]; ok {
if len(path) != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it might be nicer to have an explicit path to use to watch (e.g. /watch-rulesets), rather than a POST to /rulesets. Not sure.
🌷

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, we need a POST anyway because we need to send a body in the request

Copy link
Contributor

Choose a reason for hiding this comment

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

It was really the re-purposing of the endpoint for both purposes that felt odd to me. I don't mind much - we should really just be using gRPC and then we wouldn't need to care about such trivialities :)

http/server/api.go Outdated Show resolved Hide resolved
http/server/api_test.go Outdated Show resolved Hide resolved
http/server/handler.go Outdated Show resolved Hide resolved
api/etcd/rulesets_test.go Show resolved Hide resolved
api/etcd/watch.go Outdated Show resolved Hide resolved
api/etcd/watch.go Show resolved Hide resolved
@asdine
Copy link
Contributor Author

asdine commented Jun 5, 2019

🆙

@asdine asdine requested a review from rogpeppe June 5, 2019 10:09
Copy link

@yaziine yaziine left a comment

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@tealeg tealeg left a comment

Choose a reason for hiding this comment

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

Couple of minor things.

api/etcd/watch.go Outdated Show resolved Hide resolved
api/etcd/watch.go Outdated Show resolved Hide resolved
Co-Authored-By: Geoffrey J. Teale <[email protected]>
Copy link
Contributor

@rogpeppe rogpeppe left a comment

Choose a reason for hiding this comment

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

Looks great in general, with one simplification suggestion, and perhaps it's worth improving that Watch test?


// shouldIncludeEvent reports whether the given event should be included
// in the Watch data for the given paths.
func (s *RulesetService) shouldIncludeEvent(ev *clientv3.Event, paths []string) bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a little bit simpler, I think:

if len(paths) == 0 {
	return true
}
key := string(ev.Kv.Key)
key = key[:strings.Index(key, versionSeparator)]
for _, path := range paths {
	if s.rulesPath(path) == key {
		return true
	}
}
return false

Also, I'm a bit concerned that this code will panic if the key doesn't contain versionSeparator. How sure are we of that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Put is the only function responsible for writing in that path, I'd say that as long as Put is tested it should be fine, but I'm open to suggestions

var events api.RulesetEvents
var rev int64
var watchCount int
for len(events.Events) != len(test.expected) && watchCount < 4 {
Copy link
Contributor

Choose a reason for hiding this comment

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

ISTM that this won't fail if the watcher produces more events than you want. Perhaps the loop should wait for a while to check that no more arrive?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It can't produce more events because each call to createBoolRuleset in the goroutine above will produce exactly one event

for len(events.Events) != len(test.expected) && watchCount < 4 {
evs, err := s.Watch(ctx, api.WatchOptions{Paths: test.paths, Revision: rev})
if err != nil {
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

This condition looks redundant to me.


r := rule.New(rule.True(), rule.BoolValue(true))
time.Sleep(time.Second)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you actually need the watch statement to execute before this code? How about adding some rulesets, wait for an event to arrive, then adding some more, so you check both the initial case and the later case? (I'm not entirely sure of the intended semantics when there are rulesets already there though - what's meant to happen?)

// List of paths to watch for changes.
// If the slice is empty, watch all paths.
Paths []string
// Indicates from which revision start watching.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Indicates from which revision start watching.
// Indicates from which revision to start watching.

@@ -50,6 +39,15 @@ func (s *rulesetAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.get(w, r, path)
return
case "POST":
if _, ok := r.URL.Query()["watch"]; ok {
if len(path) != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

It was really the re-purposing of the endpoint for both purposes that felt odd to me. I don't mind much - we should really just be using gRPC and then we wouldn't need to care about such trivialities :)

{"UnexpectedError", "/rulesets/someprefix?list", http.StatusInternalServerError, new(api.Rulesets), api.ListOptions{}, errors.New("unexpected error")},
{"InvalidLimit", "/rulesets/someprefix?list&limit=badlimit", http.StatusBadRequest, nil, api.ListOptions{}, nil},
{"InvalidCursor", "/rulesets/?list&cursor=abc123", http.StatusBadRequest, new(api.Rulesets), api.ListOptions{Cursor: "abc123"}, api.ErrInvalidCursor},
{"UnexpectedError", "/rulesets/?list", http.StatusInternalServerError, new(api.Rulesets), api.ListOptions{}, errors.New("unexpected error")},
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this an internal server error rather than a bad request?

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 500 will not be caused by the URL by the error errors.New("unexpected error") that will be returned by the mock.
Basically, it's to simulate an unexpected error (network, etc.) happening in the store.

@asdine asdine closed this Nov 15, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants