-
Notifications
You must be signed in to change notification settings - Fork 100
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
Eagerly initialize meta in connect.Error #585
Conversation
Make the connect.Error type safer across goroutines by eagerly initializing meta (so calling Error.Meta() doesn't lazily initialize headers).
I don't think this is safe to do. I'm AFK for a bit, but I'll articulate when I'm back. Just want to make sure this isn't merged without my consideration. |
More context might be needed here - we had a few places where we were initializing commonly used errors as globals (i.e. This still doesn't make concurrent writes to |
@mattrobenolt We'll wait for your full thoughts :) |
Is it expected for a caller or consumer to mutate the return of My initial gut reaction was the empty value Now, with that said, my original question is relevant since why is it not applicable to just return a One option to still be safe would be to not reassign back to I personally think it's a bit silly to allocate in the constructor like this when the use case of these headers is very uncommon. (I wasn't even very aware this existed or why I'd want to use it.) I'd personally prefer committing to make this not expect a mutation. And potentially even do a full copy within I think treating this like Edit: yeah, looking a bit closer at how this is being used: https://github.com/connectrpc/connect-go/blob/main/error_not_modified_example_test.go#L49-L51 I would strongly not expect a caller to ever do something like: err := connect.NewError(...)
err.Meta().Set("foo", "bar") I get that you can, but I don't think I'd expect for this to work personally. I'd maybe expect something like: err.SetMeta(http.Header{"foo": []string{"bar"}) and set it myself. And anywhere internally that we consume |
The main concern with changing You are right on |
I'm not sure what constitutes an API change here, but both of these options, to me, would be considered an API change. Both options are breaking in undefined ways. It doesn't seem to be documented or expected that you can mutate what's returned from Also, breaking So breaking is a bit subjective here, and we have to pick one to break. This is all very very minor in the scheme of things, and I think a minor change in behavior towards our expectations along with documenting that change is likely fine. Arguably, if you're doing either of these things, it's behaving or working in a way that isn't explicitly intended to do so today, so relying on that is a bit sketchy. But ultimately, I'd favor saving the allocation on every Error object, and change the behavior to return a This path entirely exists on the extreme edge case of intending to return a 304 Not Modified. We create lots of errors in our typical hot path for many reasons. I doubt an additional allocation of an empty map is going to kill us, but it's definitely not needed for a vast majority of cases. Another option might be to introduce another Error struct, one with meta and one without meta, but this sorta would also be a different API breaking change. Don't let any of this block your decisions. |
Gonna go ahead and close this PR - thanks @mattrobenolt for the feedback. I agree with the majority of what you've said but I was worried more two separate race detector issues in this area that it felt like this would be a better solve than the current behavior.
One note - this isn't expected to work and any code using the struct initializer is technically broken today. The error code will be set to the zero value, which unlike the gRPC protocol doesn't equate with success. There is no That being said, we can re-introduce the behavior of lazily initializing the meta struct key for this case while still fixing the
This is not completely true - there are a few places that are using |
Yeah, I agree that this is a race condition and without a lock, we need to change some behavior here. I think it's also fair to define and say "this is not thread safe". The entire
This is correct, but I didn't spend the time investigating how this can be broken to make it work. Users do weird things, was my point. I don't actually know what an
That's fair, and that's context you have that I don't. I would still agree and I think we're on the same page, introducing more explicit mutation APIs to do this similar to |
@mattrobenolt, that's actually the only way to set headers from a handler with a unary response (so unary and client-stream RPCs) when there's an error. You create an error, call
A change that is definitely backwards-compatible, even if the user did use zero values of |
Unfortunately, that is definitely a breaking change if you also want |
Yeah, I get it. This is trying to mirror how an I don't have an issue with that, I just also don't have an issue with this simply not being thread safe. So while I'm arguing API changes and whatnot, I am just adding considerations. I don't personally think anything needs to change and we can just document this isn't thread safe. I still think there's a worthwhile consideration of APIs such as I don't like the forced allocation, but to be fair, since internally we call |
I think the main difference is that, intuitively, So, even if documented as not-thread-safe, this still feels like surprising behavior and a possible foot-gun. |
Make the connect.Error type safer across goroutines by eagerly initializing meta (so calling Error.Meta() doesn't lazily initialize headers).