/
resource.go
135 lines (106 loc) · 3.26 KB
/
resource.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package resource provides utilities for tracking resource lifetimes.
package resource
import (
"fmt"
"reflect"
"runtime"
"runtime/pprof"
"sync"
"unsafe"
"github.com/FerretDB/FerretDB/internal/util/debugbuild"
)
// Token should be a field of a tracked object.
//
// The underlying type is not struct{} because (from the Go spec)
// "Two distinct zero-size variables may have the same address in memory",
// and they do.
type Token byte
// NewToken returns a new Token.
func NewToken() *Token {
return new(Token)
}
// profilesM protects access to profiles.
var profilesM sync.Mutex
// profileName return pprof profile name for the given object.
func profileName(obj any) string {
return "FerretDB/" + reflect.TypeOf(obj).Elem().String()
}
// Track tracks the lifetime of an object until Untrack is called on it.
//
// Obj should a pointer to a struct with a field "token" of type *Token.
func Track(obj any, token *Token) {
checkArgs(obj, token)
name := profileName(obj)
// fast path
p := pprof.Lookup(name)
if p == nil {
// slow path
profilesM.Lock()
// a concurrent call might have created a profile already; check again
if p = pprof.Lookup(name); p == nil {
p = pprof.NewProfile(name)
}
profilesM.Unlock()
}
// use token instead of obj itself,
// because otherwise profile will hold a reference to obj and finalizer will never run
p.Add(token, 1)
stack := debugbuild.Stack()
// set finalizer on obj, not token
runtime.SetFinalizer(obj, func(obj any) {
// this closure has to use only obj argument and captured "stack" variable
msg := fmt.Sprintf("%T has not been finalized", obj)
if stack != nil {
msg += "\nObject created by " + string(stack)
}
panic(msg)
})
}
// Untrack stops tracking the lifetime of an object.
//
// It is safe to call this function multiple times.
func Untrack(obj any, token *Token) {
checkArgs(obj, token)
p := pprof.Lookup(profileName(obj))
if p == nil {
panic("object is not tracked")
}
p.Remove(token)
runtime.SetFinalizer(obj, nil)
}
// checkArgs checks Track and Untrack arguments.
//
// Other creative misuses of Track should result in panics too, if less clear.
func checkArgs(obj any, token *Token) {
if obj == nil {
panic("obj must not be nil")
}
if token == nil {
panic("token must not be nil")
}
pv := reflect.ValueOf(obj)
if pv.Kind() != reflect.Ptr {
panic(fmt.Sprintf("obj must be a pointer to struct, got %T", obj))
}
v := pv.Elem()
if v.Kind() != reflect.Struct {
panic(fmt.Sprintf("obj must be a pointer to struct, got %T", obj))
}
f := v.FieldByName("token")
if f.Kind() != reflect.Ptr || f.UnsafePointer() != unsafe.Pointer(token) {
panic("token must be a pointer field of a struct")
}
}