Skip to content

Commit 0916768

Browse files
committed
introducing Map.Drain API to traverse a map while also deleting entries
This commit introduces the `Map.Drain` API to traverse the map while also removing its entries. It leverages the same `MapIterator` structure, with the introduction of a new unexported method to handle the map draining. The tests make sure that the behavior is as expected, and that this API returns an error while invoked on the wrong map, such as arrays, for which `Map.Iterate` should be used instead. The `LookupAndDelete` system call support has been introduced in: 1. 5.14 for BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_HASH and BPF_MAP_TYPE_LRU_PERCPU_HASH. 2. 4.20 for BPF_MAP_TYPE_QUEUE, BPF_MAP_TYPE_STACK Do not expect the `Map.Drain` API to work on prior versions, according to the target map type. From the user perspective, the usage should be similar to `Map.Iterate`, as shown as follows: ```go m, err := NewMap(&MapSpec{ Type: Hash, KeySize: 4, ValueSize: 8, MaxEntries: 10, }) // populate here the map and defer close it := m.Drain() for it.Next(keyPtr, &value) { // here the entry doesn't exist anymore in the underlying map. ... } ``` Signed-off-by: Simone Magnani <[email protected]>
1 parent ef3a9b3 commit 0916768

File tree

2 files changed

+272
-22
lines changed

2 files changed

+272
-22
lines changed

map.go

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,10 +1287,31 @@ func batchCount(keys, values any) (int, error) {
12871287
//
12881288
// It's not possible to guarantee that all keys in a map will be
12891289
// returned if there are concurrent modifications to the map.
1290+
//
1291+
// Iterating a hash map from which keys are being deleted is not
1292+
// safe. You may see the same key multiple times. Iteration may
1293+
// also abort with an error, see IsIterationAborted.
1294+
//
1295+
// Iterating a queue/stack map returns an error (NextKey) as the
1296+
// Map.Drain API should be used instead.
12901297
func (m *Map) Iterate() *MapIterator {
12911298
return newMapIterator(m)
12921299
}
12931300

1301+
// Drain traverses a map while also removing entries.
1302+
//
1303+
// It's safe to create multiple drainers at the same time,
1304+
// but their respective outputs will differ.
1305+
//
1306+
// Iterating a map that does not support entry removal such as
1307+
// an array return an error (Delete/LookupAndDelete) as the
1308+
// Map.Iterate API should be used instead.
1309+
func (m *Map) Drain() *MapIterator {
1310+
it := newMapIterator(m)
1311+
it.drain = true
1312+
return it
1313+
}
1314+
12941315
// Close the Map's underlying file descriptor, which could unload the
12951316
// Map from the kernel if it is not pinned or in use by a loaded Program.
12961317
func (m *Map) Close() error {
@@ -1549,7 +1570,7 @@ type MapIterator struct {
15491570
// of []byte to avoid allocations.
15501571
cursor any
15511572
count, maxEntries uint32
1552-
done bool
1573+
done, drain bool
15531574
err error
15541575
}
15551576

@@ -1562,10 +1583,6 @@ func newMapIterator(target *Map) *MapIterator {
15621583

15631584
// Next decodes the next key and value.
15641585
//
1565-
// Iterating a hash map from which keys are being deleted is not
1566-
// safe. You may see the same key multiple times. Iteration may
1567-
// also abort with an error, see IsIterationAborted.
1568-
//
15691586
// Returns false if there are no more entries. You must check
15701587
// the result of Err afterwards.
15711588
//
@@ -1574,26 +1591,28 @@ func (mi *MapIterator) Next(keyOut, valueOut interface{}) bool {
15741591
if mi.err != nil || mi.done {
15751592
return false
15761593
}
1594+
if mi.drain {
1595+
return mi.nextDrain(keyOut, valueOut)
1596+
}
1597+
return mi.nextIterate(keyOut, valueOut)
1598+
}
1599+
1600+
func (mi *MapIterator) nextIterate(keyOut, valueOut interface{}) bool {
1601+
var key interface{}
15771602

1578-
// For array-like maps NextKey returns nil only after maxEntries
1579-
// iterations.
1603+
// For array-like maps NextKey returns nil only after maxEntries iterations.
15801604
for mi.count <= mi.maxEntries {
15811605
if mi.cursor == nil {
15821606
// Pass nil interface to NextKey to make sure the Map's first key
15831607
// is returned. If we pass an uninitialized []byte instead, it'll see a
15841608
// non-nil interface and try to marshal it.
15851609
mi.cursor = make([]byte, mi.target.keySize)
1586-
mi.err = mi.target.NextKey(nil, mi.cursor)
1610+
key = nil
15871611
} else {
1588-
mi.err = mi.target.NextKey(mi.cursor, mi.cursor)
1612+
key = mi.cursor
15891613
}
15901614

1591-
if errors.Is(mi.err, ErrKeyNotExist) {
1592-
mi.done = true
1593-
mi.err = nil
1594-
return false
1595-
} else if mi.err != nil {
1596-
mi.err = fmt.Errorf("get next key: %w", mi.err)
1615+
if !mi.fetchNextKey(key) {
15971616
return false
15981617
}
15991618

@@ -1615,20 +1634,84 @@ func (mi *MapIterator) Next(keyOut, valueOut interface{}) bool {
16151634
return false
16161635
}
16171636

1618-
buf := mi.cursor.([]byte)
1619-
if ptr, ok := keyOut.(unsafe.Pointer); ok {
1620-
copy(unsafe.Slice((*byte)(ptr), len(buf)), buf)
1621-
} else {
1622-
mi.err = sysenc.Unmarshal(keyOut, buf)
1637+
return mi.copyCursorToKeyOut(keyOut)
1638+
}
1639+
1640+
mi.err = fmt.Errorf("%w", ErrIterationAborted)
1641+
return false
1642+
}
1643+
1644+
func (mi *MapIterator) nextDrain(keyOut, valueOut interface{}) bool {
1645+
// Handke keyless map, for which mi.cursor (key used in lookupAndDelete) should be nil
1646+
if mi.isKeylessMap() {
1647+
if keyOut != nil {
1648+
mi.err = fmt.Errorf("non-nil keyOut provided for map without a key, must be nil instead")
1649+
return false
16231650
}
1651+
return mi.drainMapEntry(valueOut)
1652+
}
16241653

1625-
return mi.err == nil
1654+
// Allocate only once data for retrieving the next key in the map.
1655+
if mi.cursor == nil {
1656+
mi.cursor = make([]byte, mi.target.keySize)
1657+
}
1658+
1659+
// Always retrieve first key in the map. This should ensure that the whole map
1660+
// is traversed, despite concurrent operations (ordering of items might differ).
1661+
for mi.err == nil && mi.fetchNextKey(nil) {
1662+
if mi.drainMapEntry(valueOut) {
1663+
return mi.copyCursorToKeyOut(keyOut)
1664+
}
1665+
}
1666+
return false
1667+
}
1668+
1669+
func (mi *MapIterator) isKeylessMap() bool {
1670+
return mi.target.keySize == 0
1671+
}
1672+
1673+
func (mi *MapIterator) drainMapEntry(valueOut interface{}) bool {
1674+
mi.err = mi.target.LookupAndDelete(mi.cursor, valueOut)
1675+
if mi.err == nil {
1676+
mi.count++
1677+
return true
1678+
}
1679+
1680+
if errors.Is(mi.err, ErrKeyNotExist) {
1681+
mi.err = nil
1682+
} else {
1683+
mi.err = fmt.Errorf("lookup_and_delete key: %w", mi.err)
16261684
}
16271685

1628-
mi.err = fmt.Errorf("%w", ErrIterationAborted)
16291686
return false
16301687
}
16311688

1689+
func (mi *MapIterator) fetchNextKey(key interface{}) bool {
1690+
mi.err = mi.target.NextKey(key, mi.cursor)
1691+
if mi.err == nil {
1692+
return true
1693+
}
1694+
1695+
if errors.Is(mi.err, ErrKeyNotExist) {
1696+
mi.done = true
1697+
mi.err = nil
1698+
} else {
1699+
mi.err = fmt.Errorf("get next key: %w", mi.err)
1700+
}
1701+
1702+
return false
1703+
}
1704+
1705+
func (mi *MapIterator) copyCursorToKeyOut(keyOut interface{}) bool {
1706+
buf := mi.cursor.([]byte)
1707+
if ptr, ok := keyOut.(unsafe.Pointer); ok {
1708+
copy(unsafe.Slice((*byte)(ptr), len(buf)), buf)
1709+
} else {
1710+
mi.err = sysenc.Unmarshal(keyOut, buf)
1711+
}
1712+
return mi.err == nil
1713+
}
1714+
16321715
// Err returns any encountered error.
16331716
//
16341717
// The method must be called after Next returns nil.

map_test.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,6 +1173,173 @@ func TestMapIteratorAllocations(t *testing.T) {
11731173
qt.Assert(t, qt.Equals(allocs, float64(0)))
11741174
}
11751175

1176+
func TestDrainEmptyMap(t *testing.T) {
1177+
for _, mapType := range []MapType{
1178+
Hash,
1179+
Queue,
1180+
} {
1181+
t.Run(mapType.String(), func(t *testing.T) {
1182+
var (
1183+
keySize = uint32(4)
1184+
key string
1185+
value uint64
1186+
keyPtr interface{} = &key
1187+
)
1188+
1189+
if mapType == Queue {
1190+
testutils.SkipOnOldKernel(t, "4.20", "map type queue")
1191+
keySize = 0
1192+
keyPtr = nil
1193+
}
1194+
1195+
if mapType == Hash {
1196+
testutils.SkipOnOldKernel(t, "5.14", "map type hash")
1197+
}
1198+
1199+
m, err := NewMap(&MapSpec{
1200+
Type: mapType,
1201+
KeySize: keySize,
1202+
ValueSize: 8,
1203+
MaxEntries: 2,
1204+
})
1205+
qt.Assert(t, qt.IsNil(err))
1206+
defer m.Close()
1207+
1208+
entries := m.Drain()
1209+
if entries.Next(keyPtr, &value) {
1210+
t.Errorf("Empty %v should not be drainable", mapType)
1211+
}
1212+
1213+
qt.Assert(t, qt.IsNil(entries.Err()))
1214+
})
1215+
}
1216+
}
1217+
1218+
func TestMapDrain(t *testing.T) {
1219+
for _, mapType := range []MapType{
1220+
Hash,
1221+
Queue,
1222+
} {
1223+
t.Run(Hash.String(), func(t *testing.T) {
1224+
var (
1225+
key, value uint32
1226+
values []uint32
1227+
anyKey interface{}
1228+
keyPtr interface{} = &key
1229+
keySize uint32 = 4
1230+
data = []uint32{0, 1}
1231+
)
1232+
1233+
if mapType == Queue {
1234+
testutils.SkipOnOldKernel(t, "4.20", "map type queue")
1235+
keySize = 0
1236+
keyPtr = nil
1237+
}
1238+
1239+
if mapType == Hash {
1240+
testutils.SkipOnOldKernel(t, "5.14", "map type hash")
1241+
}
1242+
1243+
m, err := NewMap(&MapSpec{
1244+
Type: mapType,
1245+
KeySize: keySize,
1246+
ValueSize: 4,
1247+
MaxEntries: 2,
1248+
})
1249+
qt.Assert(t, qt.IsNil(err))
1250+
defer m.Close()
1251+
1252+
for _, v := range data {
1253+
if keySize != 0 {
1254+
anyKey = uint32(v)
1255+
}
1256+
err := m.Put(anyKey, uint32(v))
1257+
qt.Assert(t, qt.IsNil(err))
1258+
}
1259+
1260+
entries := m.Drain()
1261+
for entries.Next(keyPtr, &value) {
1262+
values = append(values, value)
1263+
}
1264+
qt.Assert(t, qt.IsNil(entries.Err()))
1265+
1266+
sort.Slice(values, func(i, j int) bool { return values[i] < values[j] })
1267+
qt.Assert(t, qt.DeepEquals(values, data))
1268+
})
1269+
}
1270+
}
1271+
1272+
func TestDrainWrongMap(t *testing.T) {
1273+
arr, err := NewMap(&MapSpec{
1274+
Type: Array,
1275+
KeySize: 4,
1276+
ValueSize: 4,
1277+
MaxEntries: 10,
1278+
})
1279+
qt.Assert(t, qt.IsNil(err))
1280+
defer arr.Close()
1281+
1282+
var key, value uint32
1283+
entries := arr.Drain()
1284+
1285+
qt.Assert(t, qt.IsFalse(entries.Next(&key, &value)))
1286+
qt.Assert(t, qt.IsNotNil(entries.Err()))
1287+
}
1288+
1289+
func TestMapDrainerAllocations(t *testing.T) {
1290+
for _, mapType := range []MapType{
1291+
Hash,
1292+
Queue,
1293+
} {
1294+
t.Run(mapType.String(), func(t *testing.T) {
1295+
var (
1296+
key, value uint32
1297+
anyKey interface{}
1298+
keyPtr interface{} = &key
1299+
keySize uint32 = 4
1300+
)
1301+
1302+
if mapType == Queue {
1303+
testutils.SkipOnOldKernel(t, "4.20", "map type queue")
1304+
keySize = 0
1305+
keyPtr = nil
1306+
}
1307+
1308+
if mapType == Hash {
1309+
testutils.SkipOnOldKernel(t, "5.14", "map type hash")
1310+
}
1311+
1312+
m, err := NewMap(&MapSpec{
1313+
Type: mapType,
1314+
KeySize: keySize,
1315+
ValueSize: 4,
1316+
MaxEntries: 10,
1317+
})
1318+
qt.Assert(t, qt.ErrorIs(err, nil))
1319+
defer m.Close()
1320+
1321+
for i := 0; i < int(m.MaxEntries()); i++ {
1322+
if keySize != 0 {
1323+
anyKey = uint32(i)
1324+
}
1325+
if err := m.Put(anyKey, uint32(i)); err != nil {
1326+
t.Fatal(err)
1327+
}
1328+
}
1329+
1330+
iter := m.Drain()
1331+
1332+
allocs := testing.AllocsPerRun(int(m.MaxEntries()-1), func() {
1333+
if !iter.Next(keyPtr, &value) {
1334+
t.Fatal("Next failed while draining: %w", iter.Err())
1335+
}
1336+
})
1337+
1338+
qt.Assert(t, qt.Equals(allocs, float64(0)))
1339+
})
1340+
}
1341+
}
1342+
11761343
func TestMapBatchLookupAllocations(t *testing.T) {
11771344
testutils.SkipIfNotSupported(t, haveBatchAPI())
11781345

0 commit comments

Comments
 (0)