Skip to content

Commit a2850ad

Browse files
committed
Fix misuse of runtime.AddCleanup
This prevented the file from being unmapped when the `Reader` was garbage collected.
1 parent 4d0333b commit a2850ad

File tree

3 files changed

+35
-2
lines changed

3 files changed

+35
-2
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changes
22

3+
## 2.1.1 - 2025-11-26
4+
5+
- Fixed `runtime.AddCleanup` misuse that prevented the memory-mapped file from
6+
being unmapped when the `Reader` was garbage collected.
7+
38
## 2.1.0 - 2025-11-04
49

510
- Updated `Offset` method on `Decoder` to return the resolved data offset

reader.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ type Reader struct {
141141
ipv4Start uint
142142
ipv4StartBitDepth int
143143
nodeOffsetMult uint
144-
hasMappedFile atomic.Bool
144+
hasMappedFile *atomic.Bool
145145
}
146146

147147
// Metadata holds the metadata decoded from the MaxMind DB file.
@@ -262,7 +262,7 @@ func Open(file string, options ...ReaderOption) (*Reader, error) {
262262
reader.hasMappedFile.Store(true)
263263
cleanup := &mmapCleanup{
264264
data: data,
265-
hasMapped: &reader.hasMappedFile,
265+
hasMapped: reader.hasMappedFile,
266266
}
267267
runtime.AddCleanup(reader, func(mc *mmapCleanup) {
268268
if mc.hasMapped.CompareAndSwap(true, false) {
@@ -356,6 +356,7 @@ func OpenBytes(buffer []byte, options ...ReaderOption) (*Reader, error) {
356356
Metadata: metadata,
357357
ipv4Start: 0,
358358
nodeOffsetMult: metadata.RecordSize / 4,
359+
hasMappedFile: &atomic.Bool{},
359360
}
360361

361362
err = reader.setIPv4Start()

reader_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/netip"
1010
"os"
1111
"path/filepath"
12+
"runtime"
1213
"sync"
1314
"testing"
1415
"time"
@@ -67,6 +68,32 @@ func TestReaderBytes(t *testing.T) {
6768
}
6869
}
6970

71+
func TestReaderLeaks(t *testing.T) {
72+
collected := make(chan struct{})
73+
74+
func() {
75+
r, err := Open(testFile("GeoLite2-City-Test.mmdb"))
76+
require.NoError(t, err)
77+
78+
// We intentionally do NOT call Close() to test if GC picks it up
79+
// and if AddCleanup doesn't prevent it.
80+
81+
runtime.SetFinalizer(r, func(obj *Reader) {
82+
close(collected)
83+
})
84+
}()
85+
86+
require.Eventually(t, func() bool {
87+
runtime.GC()
88+
select {
89+
case <-collected:
90+
return true
91+
default:
92+
return false
93+
}
94+
}, 1*time.Second, 50*time.Millisecond, "Reader was NOT collected (leak detected)")
95+
}
96+
7097
func TestLookupNetwork(t *testing.T) {
7198
bigInt := new(big.Int)
7299
bigInt.SetString("1329227995784915872903807060280344576", 10)

0 commit comments

Comments
 (0)