-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[PoC] Show how configoptional would work #13018
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
base: main
Are you sure you want to change the base?
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
8704c5e
to
6dcfb20
Compare
6dcfb20
to
e740fab
Compare
Codecov ReportAttention: Patch coverage is
❌ Your patch check has failed because the patch coverage (84.09%) is below the target coverage (95.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #13018 +/- ##
==========================================
- Coverage 91.53% 91.50% -0.04%
==========================================
Files 504 505 +1
Lines 28154 28186 +32
==========================================
+ Hits 25772 25791 +19
- Misses 1873 1884 +11
- Partials 509 511 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
cc @open-telemetry/collector-approvers @yurishkuro @mahadzaryab1 I am undecided on this. Questions for y'all:
|
This looks good to me. I do not mind all the |
Really appreciate the FAQ, that answered a lot of questions I had when reviewing the code. Overall I still like this, it feels more explicit than using pointers at what feels like a fairly minimal cost. The two biggest pieces of boilerplate to me feel acceptable:
I also like how this cleans up the I'm not too worried about the first two cons you outlined. I'm not sure how to address the third one, but it doesn't feel like a major negative to me. The biggest negative to me is the introduction of new APIs that, while optional, component authors will likely need to learn if they use our config structs as inspiration for their own. Overall, I don't feel strongly that we need this, but it seems to have more benefits than detriments. |
I think the fact that it eliminates type-unsafe lookups in conf.Map by property name is a very significant win. I'd prefer a shorter pkg name, ideally same Factory seems overkill. Not sure how it works even, e.g. calling this twice:
with some function would create a local variable with function pointer and then cause that variable to escape to heap in order to take its address. The result is that two factories would not compare equal, meaning the factory needs to be a static var. If external var is the only way then isn't there a smaller API to achieve that?
|
if this is only used for some form of e2e tests (surprising that it's needed) then perhaps it can be a static function named accordingly, not a part of the struct's API. |
@yurishkuro Regarding the Factories: my idea was that requiring the additional step of calling Without an explicit function call in between, I'm worried that users will only look at the type signature of func createDefaultThing() Thing { ... }
func DefaultConfig() Config {
var thingFactory := createDefaultThing
return Config {
ThingField Default(&thingFactory)
}
} which I believe would allocate a new I didn't know you could take a pointer to a /// optional.go
type Factory[T any] struct {
fn func() T // no longer a pointer
}
// Make sure to call this once and store the result in a static! Do it for the tests
func NewFactory[T any](defaultFn func() T) Factory[T] {
return Factory[T]{fn: defaultFn}
}
type Optional[T any] struct {
// [...]
defaultFactory *Factory[T]
}
func Default[T any](factory *Factory[T]) Optional[T] { // takes a pointer to a Factory now
return Optional[T]{defaultFactory: factory}
}
func (o *Optional[T]) GetOrInsertDefault() *T {
if !o.HasValue() && o.defaultFactory != nil {
o.value = o.defaultFactory.fn()
o.defaultFactory = nil
}
// [...]
}
/// user code
func createDefaultThing() Thing { ... }
var thingFactory = NewFactory(createDefaultThing)
func DefaultConfig() Config {
return Config {
ThingField Default(&thingFactory)
}
} Based on my tests it seems as efficient as your proposal, but I think the required function call may make it easier to enforce the intended use. |
@yurishkuro It is also used in the OTLP receiver tests (14 times). Components that wrap the OTLP receiver may also need to use it. I think it's worth having in the struct's API. |
@jade-guiton-dd accepting a function pointer is already a forcing function to read the docs since it is an unusual design and you cannot just take an address of a function, you need to store it in a variable first. |
@mx-psi if it's almost always used in tests I'd keep it out of the struct. You get the same functionality, but don't pollute the main api with testing functions. |
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description <!-- Issue number if applicable --> Adds new `configoptional` module. I left `GetOrInsertDefault` out of this first PR so we can get agreement on the basics first. #### Link to tracking issue Fixes #12981 Fixes #10266 <!--Describe what testing was performed and which tests were added.--> #### Testing <!--Describe the documentation added.--> See #13018 for usage and testing of the package on `confighttp` and `otlpreceiver`.
This PR was marked stale due to lack of activity. It will be closed in 14 days. |
Description
Adds
configoptional
. Tests it onconfighttp
andotlpreceiver
modules. Thanks @jade-guiton-dd for the feedback and help with designing this, and thanks to @yurishkuro for the initial version (see #10260)Sanity check
Sanity check on the drawbacks we set out to fix on the RFC:
✔️ Yes, this is now explicit
✔️ Yes, we can differentiate those now
✔️ Yes, you can see the OTLP receiver unmarshal method.
❌ I don't think there is much of an advantage here
❓ The only way I exposed is a
Get()
andGetOrInsertDefault()
method, this is because it is inconvenient to call methods with pointer receivers if we don't. We could have aValue()
method, that would prevent mutability but I haven't found a use case for this.Cons of doing this
Get
s to access optional fields. That's more annoying than Go automatically de-referencing things for you.GetOrInsertDefault
is a long name and I don't like it.configoptional.NewFactory
locally inside a function and shoot yourself in the foot (or at least make your life inconvenient).FAQ
Why not
Default(t T)
instead ofDefault(f Factory[T])
?To prevent people from passing values with pointers and accidentally sharing those pointers.
Why not
Default(f func() T)
instead ofDefault(f Factory[T])
?So that the function pointer is the same across instances and
assert.Equal
is happy. For example, so you can do:(or even with different factories!)
Okay, but why not
Default(f *func() T)
instead ofDefault(f Factory[T])
?This is invalid:
This is valid
Why store
*DefaultFunc[T]
instead ofDefaultFunc[T]
?So that you can compare optional values easily. Per
reflect.DeepEqual
's doc,so we need to store the pointer instead to compare by address.
Why do you have both
defaultFn
andhasValue
?So that
var none Optional[T]
is equivalent tonone := None[T]
.Why expose the factory via
GetOrInsertDefault()
?On
internal/e2e
you can see cases where you want to get the default outside of the component module. This allows you to get it without exposing extra API.Why
Get() *T
instead ofValue() T
?Apart from avoiding allocations, so that you can easily call methods with pointer receivers.
I don't like the names
Let's bikeshed after we decide if this is a good idea in the first place.
Link to tracking issue
Informs #12981