Skip to content

Commit f758c54

Browse files
committed
Add package cleanup
1 parent c4e2197 commit f758c54

2 files changed

Lines changed: 260 additions & 0 deletions

File tree

common/cleanup/cleanup.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package cleanup
2+
3+
import (
4+
"runtime"
5+
"sync"
6+
_ "unsafe"
7+
)
8+
9+
func init() {
10+
registerPoolCleanup(myCleanup)
11+
}
12+
13+
//go:linkname registerPoolCleanup sync.runtime_registerPoolCleanup
14+
func registerPoolCleanup(cleanup func())
15+
16+
//go:linkname poolCleanup sync.poolCleanup
17+
func poolCleanup()
18+
19+
var unsafeCleanupFuncs []func()
20+
21+
func myCleanup() {
22+
poolCleanup()
23+
for _, cleanupFunc := range unsafeCleanupFuncs {
24+
cleanupFunc()
25+
}
26+
}
27+
28+
// AddUnsafe must be called only in init {}
29+
// called in STW, must not allocate or hold a lock
30+
func AddUnsafe(cleanup func()) {
31+
unsafeCleanupFuncs = append(unsafeCleanupFuncs, cleanup)
32+
}
33+
34+
type Cleaner struct {
35+
access sync.Mutex
36+
state *cleanupState
37+
}
38+
39+
func Add(cleanup func()) *Cleaner {
40+
state := &cleanupState{
41+
cleanupFunc: cleanup,
42+
}
43+
newObject(state)
44+
return &Cleaner{
45+
state: state,
46+
}
47+
}
48+
49+
func (c *Cleaner) Close() {
50+
c.access.Lock()
51+
defer c.access.Unlock()
52+
if c.state == nil {
53+
return
54+
}
55+
c.state.close()
56+
c.state = nil
57+
}
58+
59+
type cleanupState struct {
60+
access sync.Mutex
61+
cleanupFunc func()
62+
closed bool
63+
}
64+
65+
func (s *cleanupState) close() {
66+
s.access.Lock()
67+
defer s.access.Unlock()
68+
s.closed = true
69+
}
70+
71+
type Object struct {
72+
state *cleanupState
73+
}
74+
75+
func newObject(state *cleanupState) {
76+
object := &Object{
77+
state: state,
78+
}
79+
runtime.SetFinalizer(object, (*Object).cleanup)
80+
}
81+
82+
func (o *Object) cleanup() {
83+
state := o.state
84+
state.access.Lock()
85+
if state.closed {
86+
state.access.Unlock()
87+
return
88+
}
89+
90+
newObject(state)
91+
cleanupFunc := state.cleanupFunc
92+
state.access.Unlock()
93+
cleanupFunc()
94+
}

common/cleanup/cleanup_test.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//nolint:paralleltest
2+
package cleanup
3+
4+
import (
5+
"runtime"
6+
"runtime/debug"
7+
"sync"
8+
"sync/atomic"
9+
"testing"
10+
"time"
11+
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestCleanup(t *testing.T) {
16+
var didCleanup atomic.Int32
17+
Add(func() {
18+
didCleanup.Add(1)
19+
})
20+
runCleanup(t, func() bool {
21+
return didCleanup.Load() >= 1
22+
})
23+
runCleanup(t, func() bool {
24+
return didCleanup.Load() >= 2
25+
})
26+
}
27+
28+
func TestCleanupUnsafe(t *testing.T) {
29+
var didCleanup atomic.Int32
30+
AddUnsafe(func() {
31+
didCleanup.Add(1)
32+
})
33+
runCleanup(t, func() bool {
34+
return didCleanup.Load() >= 1
35+
})
36+
runCleanup(t, func() bool {
37+
return didCleanup.Load() >= 2
38+
})
39+
}
40+
41+
func TestCleanup1(t *testing.T) {
42+
var didCleanup atomic.Bool
43+
var didReset atomic.Bool
44+
m := newMap(func() {
45+
didCleanup.Store(true)
46+
})
47+
Add(func() {
48+
*m = sync.Map{}
49+
didReset.Store(true)
50+
})
51+
runtime.KeepAlive(&m)
52+
runCleanup(t, func() bool {
53+
return didReset.Load()
54+
})
55+
require.False(t, didCleanup.Load())
56+
runCleanup(t, func() bool {
57+
return didCleanup.Load()
58+
})
59+
}
60+
61+
func TestCleanup1Unsafe(t *testing.T) {
62+
var didCleanup atomic.Bool
63+
m := newMap(func() {
64+
didCleanup.Store(true)
65+
})
66+
AddUnsafe(func() {
67+
*m = sync.Map{}
68+
})
69+
runtime.KeepAlive(&m)
70+
runCleanup(t, func() bool {
71+
return didCleanup.Load()
72+
})
73+
}
74+
75+
type myObj struct {
76+
_ string
77+
}
78+
79+
func newMap(cleanup func()) *sync.Map {
80+
obj := &myObj{}
81+
runtime.SetFinalizer(obj, func(*myObj) {
82+
cleanup()
83+
})
84+
var m sync.Map
85+
m.Store("test", obj)
86+
return &m
87+
}
88+
89+
func TestSafeCleanup(t *testing.T) {
90+
var didCleanup atomic.Int32
91+
safeObject := Add(func() {
92+
didCleanup.Add(1)
93+
})
94+
runCleanup(t, func() bool {
95+
return didCleanup.Load() >= 1
96+
})
97+
safeObject.Close()
98+
didCleanupAfterClose := didCleanup.Load()
99+
debug.FreeOSMemory()
100+
require.Never(t, func() bool {
101+
return didCleanup.Load() != didCleanupAfterClose
102+
}, cleanupWait, cleanupTick)
103+
}
104+
105+
func TestCloseSkipsQueuedCleanup(t *testing.T) {
106+
blockCleanup := make(chan struct{})
107+
cleanupBlocked := make(chan struct{})
108+
var blockOnce sync.Once
109+
blocker := Add(func() {
110+
blockOnce.Do(func() {
111+
close(cleanupBlocked)
112+
<-blockCleanup
113+
})
114+
})
115+
runCleanup(t, func() bool {
116+
select {
117+
case <-cleanupBlocked:
118+
return true
119+
default:
120+
return false
121+
}
122+
})
123+
124+
var didCleanup atomic.Bool
125+
queued := Add(func() {
126+
didCleanup.Store(true)
127+
})
128+
debug.FreeOSMemory()
129+
queued.Close()
130+
131+
close(blockCleanup)
132+
blocker.Close()
133+
require.Never(t, didCleanup.Load, cleanupWait, cleanupTick)
134+
}
135+
136+
func TestCleanupCanCloseItself(t *testing.T) {
137+
ready := make(chan struct{})
138+
done := make(chan struct{})
139+
var cleaner *Cleaner
140+
cleaner = Add(func() {
141+
<-ready
142+
cleaner.Close()
143+
close(done)
144+
})
145+
close(ready)
146+
147+
runCleanup(t, func() bool {
148+
select {
149+
case <-done:
150+
return true
151+
default:
152+
return false
153+
}
154+
})
155+
}
156+
157+
const (
158+
cleanupWait = time.Second
159+
cleanupTick = time.Millisecond
160+
)
161+
162+
func runCleanup(t *testing.T, condition func() bool) {
163+
t.Helper()
164+
debug.FreeOSMemory()
165+
require.Eventually(t, condition, cleanupWait, cleanupTick)
166+
}

0 commit comments

Comments
 (0)