Skip to content

Proposal: New way of handling data mutability #6794

Closed
@bogdandrutu

Description

@bogdandrutu

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 batchperesourceattr

This 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:

  1. Currently there is no way to test components. An possible solution is to provide a different pdata package maybe using a build tag that checks for incorrect access.
  2. In case of crashes like Collector constantly breaking down #6420 (I was lucky that I am very familiar with the pdata and collector code to find) could take a long time to find an the root cause with the current design since we do not crash/log when UB happens (this is an UB since concurrent access to non thread safe data is defined as UB).

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 objects ptrace.Traces/pmetric.Metrics/plog.Logs. Here is the example API for traces (similar changes will be applied to all):

type Traces struct {
  // state is propagated to every "sub struct" only to enforce correctness.
  state pcommon.State
}

// State returns the current access state of the data. If StateShared the object cannot be mutated, and any call that mutates 
// the current instance or a sub-struct (Span, SpanEvent, etc.) will panic. If mutability is required a copy of the object should
// be made using CopyTo (TODO: may consider a Clone func since this will become used quite some time)
func (t Traces) State() pcommon.State {
  return t.state
}

// Changes the state of the StateShared. Must be called only when the object is fan-out to multiple consumers, or in tests.
func (t *Traces) MarkShared() {
  t.state = pcommon.StateShared;
}

func (ms Traces) MoveTo(dest Traces) {
	// Similar changes will be made to every mutable func. It is ok to crash since this is a UB.
	if t.state == pcommon.StateShared {
		panic("invalid access to shared data")
	}
	*dest.getOrig() = *ms.getOrig()
	*ms.getOrig() = otlpcollectortrace.ExportTraceServiceRequest{}
}

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:

  • Easy to test by passing "shared" data to the consumer and ensure no crashes.
  • Easy to debug failures, since the panic stacktrace will be printed and "guilty" components will be present in the stacktrace.
  • Allow components to "delay" cloning if only in some cases the data are mutated. For example a transform processor may delay cloning until a match happens.

This proposal has the following disadvantages:

  • Still require code that authors may forget to add (the initial clone if needed);
  • In case of fan-out components, authors need to manually call 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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions