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

Implement strict decoding for JetStream API requests #5858

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

caspervonb
Copy link
Contributor

This implements optional strict JSON decoding for JetStream.

The intent of this is to minimize accidental misalignments between server and clients, we've had numerous of these across the various clients, especially for rarely used fields in the request payloads.

Signed-off-by: Casper Beyer [email protected]

@derekcollison
Copy link
Member

https://app.travis-ci.com/github/nats-io/nats-server/jobs/625767923

Needs a fix

@caspervonb caspervonb force-pushed the optional-strict-json-decoding branch 3 times, most recently from 9beb4f6 to 6b84a9b Compare September 4, 2024 18:37
return err
}

return json.Unmarshal(msg, v)
Copy link
Member

Choose a reason for hiding this comment

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

Not convinced by this block, in that if strict mode is disabled, we can in effect unmarshal twice. (Unmarshalling JSON is actually quite expensive on CPU so need to be careful on hot paths.)

Why this vs just having one decoder and setting DisallowUnknownFields() based on the Strict setting?

Copy link
Contributor Author

@caspervonb caspervonb Sep 5, 2024

Choose a reason for hiding this comment

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

My first pass did just that, but was discussed offline with @ripienaar that we want to log marshaling failures even if strict is set to false to have a softer error path but keep the current behavior for existing deployments where it would at-least surface in the logs.

Copy link
Member

Choose a reason for hiding this comment

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

A misbehaving client is going to a) spam log lines and b) unmarshal twice for each request. I don't think that's wise, needs further discussion.

Copy link
Contributor

Choose a reason for hiding this comment

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

Derek suggested that for 2.11 while we are in soft mode we should still report the problem so client authors can discover and remediate issues so he suggested this approach. It's nice a nice assist before we enable this by default to reject requests but I agree with you it will spam logs until things are fixed.

Copy link
Member

Choose a reason for hiding this comment

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

I think client authors could enable this as an opt-in too, in order to avoid spamming there is rateLimitFormatWarnf which would log less

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah we discussed maybe a header on specific requests - but we have no api behaviours on headers so I am a bit reluctant.

Other option is something at the request level like we have action on consumer or pedantic now - big ripple effect on clients having to support it and ultimately this does not help users. The point is to identify bad clients that perhaps are not compatible or have bugs. So making it opt-in only for clients that support it is a no win

Copy link
Contributor

Choose a reason for hiding this comment

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

rateLimitFormatWarnf is a good idea though for sure

Copy link
Member

Choose a reason for hiding this comment

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

I think that any opt-in for the client misses the point that Derek had - it's about detecting problems with non-tier-1 and older clients, and those will not have such opt-in implemented.

Copy link
Contributor Author

@caspervonb caspervonb Sep 6, 2024

Choose a reason for hiding this comment

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

Changed the log lines to use rate limited warnings.

}

if err != nil {
s.Errorf("Invalid JetStream request '%s > %s': %s", acc, subject, err)
Copy link
Member

Choose a reason for hiding this comment

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

what is a sample line that gets reported here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

[10037] 2024/09/06 14:50:14.087465 [ERR] Invalid JetStream request '$G > $JS.API.STREAM.INFO.ORDERS': json: unknown field "stream_name"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now with rate limit warnings instead of error it is:

[11545] 2024/09/06 15:10:33.449547 [WRN] Invalid JetStream request '$G > $JS.API.STREAM.INFO.ORDERS': json: unknown field "stream_name"

@caspervonb caspervonb changed the title [WIP] Implement strict decoding for JetStream API requests Implement strict decoding for JetStream API requests Sep 6, 2024
@caspervonb caspervonb force-pushed the optional-strict-json-decoding branch 2 times, most recently from a9b2549 to c4418f4 Compare September 6, 2024 14:26
@caspervonb caspervonb marked this pull request as ready for review September 6, 2024 14:27
@caspervonb caspervonb requested a review from a team as a code owner September 6, 2024 14:27
@caspervonb
Copy link
Contributor Author

There's one failing test, but this is failing on main as-well so I don't think that's related.

@@ -453,6 +454,7 @@ func (s *Server) enableJetStream(cfg JetStreamConfig) error {
s.Noticef("")
}
s.Noticef("---------------- JETSTREAM ----------------")
s.Noticef(" Strict: %t", cfg.Strict)
Copy link
Member

Choose a reason for hiding this comment

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

I think we can remove these from banner

Copy link
Contributor Author

@caspervonb caspervonb Sep 9, 2024

Choose a reason for hiding this comment

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

I'd vote to keep it, it's a sanity notice that you got the configuration you expected?

Copy link
Member

Choose a reason for hiding this comment

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

Let's only print it if cfg.Strict == true.

server/jetstream_api.go Outdated Show resolved Hide resolved
@caspervonb
Copy link
Contributor Author

Ran the meta benchmarks on this as they're pretty request heavy:

JetStreamCreate/N=1,R=1,storage=Memory,C=12/resource=Stream-14        593.6µ ± ∞ ¹   618.6µ ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=1,R=1,storage=Memory,C=12/resource=KVBucket-14      1.065m ± ∞ ¹   1.074m ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=1,R=1,storage=Memory,C=12/resource=ObjStore-14      2.889m ± ∞ ¹   2.831m ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=3,R=3,storage=Memory,C=12/resource=Stream-14        51.54m ± ∞ ¹   56.28m ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=3,R=3,storage=Memory,C=12/resource=KVBucket-14      63.51m ± ∞ ¹   58.32m ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=3,R=3,storage=Memory,C=12/resource=ObjStore-14      62.60m ± ∞ ¹   64.33m ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=3,R=3,storage=File,C=12/resource=Stream-14          60.38m ± ∞ ¹   56.13m ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=3,R=3,storage=File,C=12/resource=KVBucket-14        66.11m ± ∞ ¹   56.62m ± ∞ ¹       ~ (p=1.000 n=1) ²
JetStreamCreate/N=3,R=3,storage=File,C=12/resource=ObjStore-14        58.56m ± ∞ ¹   59.47m ± ∞ ¹       ~ (p=1.000 n=1) ²

server/opts.go Outdated
@@ -2317,6 +2318,9 @@ func parseJetStream(v any, opts *Options, errors *[]error, warnings *[]error) er
for mk, mv := range vv {
tk, mv = unwrapValue(mv, &lt)
switch strings.ToLower(mk) {
case "strict":
opts.JetStreamStrict = true
Copy link
Member

Choose a reason for hiding this comment

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

Better to do a type assertion here for s, ok := vv.(bool), return an error if the type assert fails, and then set JetStreamStrict to the bool value. Otherwise setting strict: false in the config does the wrong thing.

derekcollison added a commit that referenced this pull request Sep 11, 2024
Currently failures to unmarshal only give a vague error of "invalid
JSON", this adds the original error to it as context.

Before:

````
invalid JSON
````

After:

```
invalid JSON: invalid character '\"' after object key
```

Related to but **not** dependent on
#5858

Signed-off-by: Casper Beyer <[email protected]>
@neilalexander
Copy link
Member

Mind rebasing this & resolving conflicts please?

@ripienaar
Copy link
Contributor

Please make sure the strict option value is shown in jsz output

@@ -453,6 +454,7 @@ func (s *Server) enableJetStream(cfg JetStreamConfig) error {
s.Noticef("")
}
s.Noticef("---------------- JETSTREAM ----------------")
s.Noticef(" Strict: %t", cfg.Strict)
Copy link
Member

Choose a reason for hiding this comment

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

Let's only print it if cfg.Strict == true.

server/jetstream_test.go Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants