-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Proposal: New way of handling data mutability #6794
Comments
Is it necessary to remove this? I would imagine the ability to enforce what is declared (your "state" proposal) to be useful as an addition, instead of replacing the declaration. If we remove the ability to declare that the component mutates data then every component that can mutate data has to be fixed and the Clone calls must be added. I wonder what the impact is on all existing components someone may have created outside Otel repos. To be efficient for cases like transform processor the processor can claim that it doesn't mutate data and then use Clone when it needs to mutate. It is not a lie since we define I would also like to understand what the performance implications of adding the state field to every pdata struct is (likely small, but still worth checking). |
It is not "necessary" but redundant. We can still provide a "consumer.MutableConsumer(consumer.Consumer)" (equivalent with the component helpers that deal with exposing the Capability function that does the "cloning". So for example in the
Sure they can do half of it, since they always Clone even when not needed. (e.g. they have exclusive access to the data, but they don't know about since this is not expose to the consumers today).
Internally we can check to see if they implement this and as mentioned do some sort of "wrapping" initially. Because we remove the "component.Capabilities" object at one point (after deprecation, etc.) if they don't upgrade to latest they will get compilation error (not ideal, but happens with every breaking change we do), so not worry about having unexpected crashes.
On it. My gut feeling tells me that this is not a problem, since the compiler and CPU branch prediction should be smart enough and identify that these branches are never hit. Update: golang/go#11451 this is implemented as an optimization. Results: as expected is less then a cycle type state int32
const (
stateExclusive state = iota
stateShared
)
type safeStruct struct {
val int
s state
}
func (ss *safeStruct) set(val int) {
if ss.s == stateShared {
panic("")
}
ss.val = val
}
type unsafeStruct struct {
val int
}
func (ss *unsafeStruct) set(val int) {
ss.val = val
}
func BenchmarkSafeStruct(b *testing.B) {
ss := safeStruct{s: stateExclusive}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ss.set(n)
if ss.val != n {
b.Fail()
}
}
}
func BenchmarkUnsafeStruct(b *testing.B) {
ss := unsafeStruct{}
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
ss.set(n)
if ss.val != n {
b.Fail()
}
}
} goos: darwin
goarch: amd64
pkg: go.opentelemetry.io/collector/pdata/ptrace
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkSafeStruct
BenchmarkSafeStruct-16 1000000000 0.3721 ns/op 0 B/op 0 allocs/op
BenchmarkUnsafeStruct
BenchmarkUnsafeStruct-16 1000000000 0.2560 ns/op 0 B/op 0 allocs/op
PASS |
May be we can wrap fan-out components and call |
yes, we can expose our fan-out consumer and they can use that to ensure they do the right thing. |
Sorry if this is obvious, but do you intend to keep the panic's in or would methods that mutate return an error saying the data is unmodifiable? In the scenario that a component like |
I'm also thinking of another possible approach that doesn't require introducing panics and passing the // MutableResourceMetrics returns the MutableResourceMetricsSlice associated with this Metrics object.
// This method should be called to get a slice that can be changed.
// If changing the slice is not required, use ResourceMetrics method.
// This method makes a copy of the whole Metric object if used in a fan-out component with more than one consumer.
func (ms Metrics) MutableResourceMetrics() MutableResourceMetricsSlice {
if ms.state == pcommon.StateShared {
rms := NewResourceMetricsSlice()
newResourceMetricsSlice(&ms.getOrig().ResourceMetrics).CopyTo(rms)
*ms.getOrig() = otlpcollectormetrics.ExportMetricsServiceRequest{
ResourceMetrics: *rms.getOrig(),
}
ms.state = pcommon.StateExclusive
return rms
}
return newMutableResourceMetricsSlice(&ms.getOrig().ResourceMetrics)
} In that case, I tried some prototyping in #6798. It's not polished yet, but I can proceed if you think it's worth trying. |
So I expect to add 3 lines to their func (t *transform) ConsumeTraces(ctx context.Context, td ptrace.Traces) error {
if td.State == pcommon.StateShared {
td = td.Clone()
}
....
} Update after a discussion with @dmitryax, we can offer a helper and user have to do only this. where inside the func (t *transform) ConsumeTraces(ctx context.Context, td ptrace.Traces) error {
td.MakeExclusive()
....
} |
Just to summarize, the proposal is to:
If my understanding is correct, I think it seems like an OK plan. It removes the ability for someone to unexpectedly modify data they shouldn't, and give component author a mechanism to be made aware of the problem before the code is shipped. So long as the performance isn't impacted (based on @bogdandrutu's results above it doesn't appear to be a problem) but it would be good to confirm once the code is in a PR anyways. |
I agree that the current state is undesirable and I applaud this idea to change it. The solution with panics is already so much better than current state. I like the non-panics approach even more though. As a component developer, I'd much prefer being told by the compiler that I cannot modify the data, as opposed to compiling successfully and only finding out during runtime (hopefully when running my tests) that my implementation is incorrect. In case of components that only mutate data in specific circumstances, this means the possibility that I've missed calling the I've looked at Dmitrii's PR, but I'm afraid I'm not able to grasp the scope of changes needed to the API and understand the burden of maintaining the duplication that the non-panics approach would introduce. |
@astencel-sumo thanks for your feedback. I'm working on alternative compile-time solutions that would reduce the amount of duplicated code. Will submit another PR soon. For now, I'd like to add this issue to https://github.com/open-telemetry/opentelemetry-collector/milestone/24 until we make a decision. |
This also is related to https://github.com/open-telemetry/opentelemetry-collector/milestone/31 |
…ns (#8494) This change introduces an option to enable runtime assertions to catch unintentional pdata mutations in components that are claimed as non-mutating pdata. Without these assertions, runtime errors may still occur, but thrown by unrelated components, making it very difficult to troubleshoot. For now, this doesn't change the default behavior. It just introduces a new method on `[Metrics/Traces|Logs].Shared()` that returns pdata marked as shared. The method will be applied to fan-out consumers in the next PR. Later, if we want to remove the need of `MutatesData` capability, we can introduce another method `[Metrics/Traces|Logs].Exclusive()` which returns a copy of the pdata if it's shared. This change unblocks the 1.0 release by implementing the original solution proposed by @bogdandrutu in #6794. Going forward, we may introduce a copy-on-write solution that doesn't require the runtime assertions. That will likely be part of the 2.0 release.
…8634) This change enables the runtime assertions to catch unintentional pdata mutations in components claiming as non-mutating pdata. Without these assertions, runtime errors may still occur, but thrown by unrelated components, making it very difficult to troubleshoot. This required introducing extra API to get the pdata mutability state: - p[metric|trace|log].[Metrics|Traces|Logs].IsReadOnly() Resolves: #6794
Background
Currently we ask every component to declare their "Capabilities". Capabilities are part of the
consumer.Consumer*
interface, to allow "helper consumer" to also declare their own capabilities see in contrib batchperesourceattrThis design proved in the recent weeks to be very limited, see issues/PRs (most of the recent issues are related to sort, but this is not the only mutable func that may cause this):
There are few issues with this design:
Proposal
The current issue/document proposes to replace the current "Capabilities" model from the
consumer.Consumer*
interface and add a notion of "Shared(ReadOnly)" and "Exclusive(Mutable)" (Shared/Exclusive are nice since they are also used in MESI protocol) to the top signal objectsptrace.Traces
/pmetric.Metrics
/plog.Logs
. Here is the example API for traces (similar changes will be applied to all):With this change, consumers/components that need to mutate the data can check first thing if the data are "Shared" or "Exclusive" and do a copy of the data if needed.
This proposal has the following advantages:
This proposal has the following disadvantages:
MarkShared
. Though there should be a very limited number of components that do this.Alternative Options
An alternative is to just provide some testing helpers, these helpers will be very similar in terms of the generated code with the proposed solution, but will not run in production code. The advantage is that it will remove a branch check and will "not crash" immediately (since this is an UB sooner than later will crash randomly as in the provided examples) in production.
The text was updated successfully, but these errors were encountered: