-
Notifications
You must be signed in to change notification settings - Fork 682
Add mechanism to detect leaked references to mimirpb buffers #13609
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?
Changes from 29 commits
8b48ad9
56fd9b0
0b57a52
f27be92
86258b7
71f14aa
f1ab684
f6a6b41
ef1d54e
69ca33a
9d6d2d0
efc7fec
9012300
a6eb3e6
a6e1c78
de97dd5
da56796
7ca73a3
d9afc39
466a6de
628ee24
2dbb9a6
4180411
de52a8a
4b0636e
c10028f
25fef18
a47d9c2
49fc9a2
b39485d
8bff504
e5f7446
375561b
325f3c6
a3ff570
739133c
fc76511
5785b01
f44250c
ff97b8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,27 +6,57 @@ import ( | |
| "bytes" | ||
| "fmt" | ||
| "math" | ||
| "syscall" | ||
| "testing" | ||
| "unsafe" | ||
|
|
||
| gogoproto "github.com/gogo/protobuf/proto" | ||
| "github.com/prometheus/prometheus/model/histogram" | ||
| "go.uber.org/atomic" | ||
| "google.golang.org/grpc/encoding" | ||
| "google.golang.org/grpc/encoding/proto" | ||
| "google.golang.org/grpc/mem" | ||
| protobufproto "google.golang.org/protobuf/proto" | ||
| "google.golang.org/protobuf/protoadapt" | ||
| ) | ||
|
|
||
| type CustomCodecConfig struct { | ||
| InstrumentRefLeaksPct float64 | ||
| } | ||
|
|
||
| var baseCodecV2Name = encoding.GetCodecV2(proto.Name).Name() | ||
|
|
||
| func (cfg CustomCodecConfig) codec() *codecV2 { | ||
| c := &codecV2{} | ||
| if cfg.InstrumentRefLeaksPct > 0 { | ||
| c.instrumentRefLeaksOneIn = uint64(math.Trunc(100 / cfg.InstrumentRefLeaksPct)) | ||
tcard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| return c | ||
| } | ||
tcard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| var globalCodec encoding.CodecV2 | ||
|
|
||
| func (cfg CustomCodecConfig) RegisterGlobally() { | ||
| globalCodec = cfg.codec() | ||
| encoding.RegisterCodecV2(globalCodec) | ||
| } | ||
|
|
||
| func init() { | ||
| c := encoding.GetCodecV2(proto.Name) | ||
| encoding.RegisterCodecV2(&codecV2{codec: c}) | ||
| config := CustomCodecConfig{} | ||
| if testing.Testing() { | ||
| // Instrument all buffers when testing. | ||
| config.InstrumentRefLeaksPct = 100 | ||
| } | ||
| config.RegisterGlobally() | ||
| } | ||
|
|
||
| // codecV2 customizes gRPC marshalling and unmarshalling. | ||
| // We customize marshalling in order to use optimized paths when possible. | ||
| // We customize unmarshalling in order to use an optimized path when possible, | ||
| // and to retain the unmarshalling buffer when necessary. | ||
| type codecV2 struct { | ||
| codec encoding.CodecV2 | ||
| instrumentRefLeaksOneIn uint64 | ||
| unmarshaledWithBufferRefCount atomic.Uint64 | ||
| } | ||
|
|
||
| var _ encoding.CodecV2 = &codecV2{} | ||
|
|
@@ -113,11 +143,47 @@ func unmarshalSlicePoolSizes() []int { | |
| return sizes | ||
| } | ||
|
|
||
| // Unmarshal unmarshals an object using the global codec. Prefer this over | ||
| // calling the Unmarshal method directly, as it will take advantage of leak | ||
| // detection. | ||
| func Unmarshal(data []byte, v gogoproto.Unmarshaler) error { | ||
| return globalCodec.Unmarshal(mem.BufferSlice{mem.SliceBuffer(data)}, v) | ||
| } | ||
|
|
||
| var pageSize = syscall.Getpagesize() | ||
|
|
||
| // Unmarshal customizes gRPC unmarshalling. | ||
| // If v implements MessageWithBufferRef, its SetBuffer method is called with the unmarshalling buffer and the buffer's reference count gets incremented. | ||
| // The latter means that v's FreeBuffer method should be called when v is no longer used. | ||
| func (c *codecV2) Unmarshal(data mem.BufferSlice, v any) error { | ||
| buf := data.MaterializeToBuffer(unmarshalSlicePool) | ||
| holder, isBufferHolder := v.(MessageWithBufferRef) | ||
| instrumentLeaks := data.Len() > 0 && isBufferHolder && c.instrumentRefLeaksOneIn > 0 && c.unmarshaledWithBufferRefCount.Add(1)%c.instrumentRefLeaksOneIn == 0 | ||
|
|
||
| var buf mem.Buffer | ||
| if instrumentLeaks { | ||
| // Allocate separate pages for this buffer. We'll detect ref leaks by | ||
| // munmaping the pages on Free, after which trying to access them will | ||
| // segfault. | ||
| pageAlignedLen := roundUpToMultiple(data.Len(), pageSize) | ||
| b, err := syscall.Mmap(0, 0, pageAlignedLen, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON) | ||
| if err != nil { | ||
| panic(fmt.Errorf("mmap: %w", err)) | ||
| } | ||
| b = b[:data.Len()] | ||
| data.CopyTo(b) | ||
| buf = mem.NewBuffer(&b, nil) | ||
| instrumentedBuf := &instrumentLeaksBuf{Buffer: buf} | ||
| instrumentedBuf.refCount.Inc() | ||
| buf = instrumentedBuf | ||
| } else if len(data) == 1 { | ||
| // BufferSlice.MaterializeToBuffer already has this behavior when | ||
| // len(data) == 1, but we reproduce it here for explicitness and for | ||
| // ensuring forward-compatibility. | ||
| data[0].Ref() | ||
| buf = data[0] | ||
| } else { | ||
| buf = data.MaterializeToBuffer(unmarshalSlicePool) | ||
| } | ||
| // Decrement buf's reference count. Note though that if v implements MessageWithBufferRef, | ||
| // we increase buf's reference count first so it doesn't go to zero. | ||
| defer buf.Free() | ||
|
|
@@ -133,7 +199,7 @@ func (c *codecV2) Unmarshal(data mem.BufferSlice, v any) error { | |
| } | ||
| } | ||
|
|
||
| if holder, ok := v.(MessageWithBufferRef); ok { | ||
| if isBufferHolder { | ||
| buf.Ref() | ||
| holder.SetBuffer(buf) | ||
| } | ||
|
|
@@ -142,7 +208,7 @@ func (c *codecV2) Unmarshal(data mem.BufferSlice, v any) error { | |
| } | ||
|
|
||
| func (c *codecV2) Name() string { | ||
| return c.codec.Name() | ||
| return baseCodecV2Name | ||
| } | ||
|
|
||
| // MessageWithBufferRef is an unmarshalling buffer retaining protobuf message. | ||
|
|
@@ -158,6 +224,10 @@ type BufferHolder struct { | |
| buffer mem.Buffer | ||
| } | ||
|
|
||
| func (m *BufferHolder) Buffer() mem.Buffer { | ||
| return m.buffer | ||
| } | ||
|
|
||
| func (m *BufferHolder) SetBuffer(buf mem.Buffer) { | ||
| m.buffer = buf | ||
| } | ||
|
|
@@ -171,6 +241,30 @@ func (m *BufferHolder) FreeBuffer() { | |
|
|
||
| var _ MessageWithBufferRef = &BufferHolder{} | ||
|
|
||
| type instrumentLeaksBuf struct { | ||
| mem.Buffer | ||
| refCount atomic.Int64 | ||
| } | ||
|
|
||
| func (b *instrumentLeaksBuf) Ref() { | ||
| b.Buffer.Ref() | ||
| b.refCount.Inc() | ||
| } | ||
|
|
||
| func (b *instrumentLeaksBuf) Free() { | ||
| b.Buffer.Free() | ||
|
|
||
| if b.refCount.Dec() == 0 { | ||
| buf := b.ReadOnlyData() | ||
| ptr := unsafe.SliceData(buf) | ||
| allPages := unsafe.Slice(ptr, roundUpToMultiple(len(buf), pageSize)) | ||
| err := syscall.Munmap(allPages) | ||
|
||
| if err != nil { | ||
| panic(fmt.Errorf("munmap: %w", err)) | ||
| } | ||
| } | ||
| } | ||
tcard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // MinTimestamp returns the minimum timestamp (milliseconds) among all series | ||
| // in the WriteRequest. Returns math.MaxInt64 if the request is empty. | ||
| func (m *WriteRequest) MinTimestamp() int64 { | ||
|
|
@@ -534,3 +628,7 @@ type orderAwareMetricMetadata struct { | |
| // order is the 0-based index of this metadata object in a wider metadata array. | ||
| order int | ||
| } | ||
|
|
||
| func roundUpToMultiple(n, of int) int { | ||
| return ((n + of - 1) / of) * of | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.