Skip to content

Commit

Permalink
maps: Handle GetValueAndDeleteBatch return code appropiately
Browse files Browse the repository at this point in the history
Long story short, some libbpf APIs that don't return pointers, such as the
batch APIs might return `-1` when there's a 'partial success'. In the case
of these APIs, with errno=ENOENT, we know that we read and deleted less than
`count` entries, but it was still overall sucessful. For more  more background
see the inline comments.

This PR fixes this for `GetValueAndDeleteBatch` only. We aren't addressing
all the other batch calls yet as we haven't used them
yet.

Thanks!
  • Loading branch information
javierhonduco committed Apr 28, 2022
1 parent 64458ba commit f2b3ab8
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 25 deletions.
63 changes: 41 additions & 22 deletions libbpfgo.go
Expand Up @@ -694,19 +694,19 @@ func (b *BPFMap) GetValueBatch(keys unsafe.Pointer, startKey, nextKey unsafe.Poi

// GetValueAndDeleteBatch allows for batch lookup and deletion of elements where each element is deleted after being retrieved from the map.
//
// The first argument, keys. is a pointer to an array or slice of keys which will be populated with the keys returned from this operation.
// The first argument, keys, is a pointer to an array or slice of keys which will be populated with the keys returned from this operation.
// It returns the associated values as a slice of slices of bytes.
//
// This API allows for batch lookups and deletion of multiple keys, potentially in steps over multiple iterations. For example,
// you provide the last key seen (or nil) for the startKey, and the first key to start the next iteration with in nextKey.
// Once the first iteration is complete you can provide the last key seen in the previous iteration as the startKey for the next iteration
// and repeat until nextKey is nil.
//
// The last argument, count, is the number of keys to lookup, and delete. Passing an argument that greater than the number of keys
// in the map will cause the function to return a syscall.EPERM as an error.
// The last argument, count, is the number of keys to lookup and delete. The kernel will update it with the count of the elements that were
// retrieved and deleted.
//
// The API can return partial results even though an error is returned.
// In this case the keys that were successfully retrieved and deleted will be in the result slice.
// The API can return partial results even though an -1 is returned. In this case, errno will be set to `ENOENT` and the values slice and count
// will be filled in with the elements that were read. See the comment below for more context.
func (b *BPFMap) GetValueAndDeleteBatch(keys, startKey, nextKey unsafe.Pointer, count uint32) ([][]byte, error) {
var (
values = make([]byte, b.ValueSize()*int(count))
Expand All @@ -720,19 +720,38 @@ func (b *BPFMap) GetValueAndDeleteBatch(keys, startKey, nextKey unsafe.Pointer,
Flags: C.BPF_ANY,
}

errC := C.bpf_map_lookup_and_delete_batch(b.fd, startKey, nextKey, keys, valuesPtr, &countC, bpfMapBatchOptsToC(opts))
if errC != 0 {
sc := syscall.Errno(-errC)
if sc != syscall.EFAULT {
if uint32(countC) != count {
return collectBatchValues(values, uint32(countC), b.ValueSize()),
fmt.Errorf("failed to retrieve and delete ALL elements in map %s, deleted (%d/%d): %w", b.name, uint32(countC), count, sc)
}
}
return nil, fmt.Errorf("failed to batch lookup and delete values %v in map %s: %w", keys, b.name, syscall.Errno(-errC))
}

return collectBatchValues(values, count, b.ValueSize()), nil
// Before libbpf 1.0 (without LIBBPF_STRICT_DIRECT_ERRS), the return value
// and errno are not modified [1]. On error, we will get a return value of
// -1 and errno will be set accordingly with most BPF calls.
//
// The batch APIs are a bit different in which they can return an error, but
// depending on the errno code, it might mean a complete error (nothing was
// done) or a partial success (some elements were processed).
//
// - On complete sucess, it will return 0, and errno won't be set.
// - On partial sucess, it will return -1, and errno will be set to ENOENT.
// - On error, it will return -1, and an errno different to ENOENT.
//
// [1] https://github.com/libbpf/libbpf/blob/b69f8ee93ef6aa3518f8fbfd9d1df6c2c84fd08f/src/libbpf_internal.h#L496
ret, errC := C.bpf_map_lookup_and_delete_batch(
b.fd,
startKey,
nextKey,
keys,
valuesPtr,
&countC,
bpfMapBatchOptsToC(opts))

processed := uint32(countC)

if ret != 0 && errC != syscall.ENOENT {
// ret = -1 && errno == syscall.ENOENT indicates a partial read and delete.
return nil, fmt.Errorf("failed to batch lookup and delete values %v in map %s: ret %d (err: %s)", keys, b.name, ret, errC)
}

// Either some or all entries were read and deleted.
parsedVals := collectBatchValues(values, processed, b.ValueSize())
return parsedVals, nil
}

func collectBatchValues(values []byte, count uint32, valueSize int) [][]byte {
Expand Down Expand Up @@ -787,7 +806,7 @@ func (b *BPFMap) DeleteKeyBatch(keys unsafe.Pointer, count uint32) error {
ElemFlags: C.BPF_ANY,
Flags: C.BPF_ANY,
}

errC := C.bpf_map_delete_batch(b.fd, keys, &countC, bpfMapBatchOptsToC(opts))
if errC != 0 {
sc := syscall.Errno(-errC)
Expand All @@ -811,9 +830,9 @@ func (b *BPFMap) DeleteKeyBatch(keys unsafe.Pointer, count uint32) error {
// in the slice or array instead of the slice/array itself, as to
// avoid undefined behavior.
func (b *BPFMap) DeleteKey(key unsafe.Pointer) error {
errC := C.bpf_map_delete_elem(b.fd, key)
if errC != 0 {
return fmt.Errorf("failed to get lookup key %d from map %s: %w", key, b.name, syscall.Errno(-errC))
ret, errC := C.bpf_map_delete_elem(b.fd, key)
if ret != 0 {
return fmt.Errorf("failed to get lookup key %d from map %s: %w", key, b.name, errC)
}
return nil
}
Expand Down
32 changes: 29 additions & 3 deletions selftest/map-batch/main.go
Expand Up @@ -74,13 +74,20 @@ func main() {
}

// Test batch lookup and delete.
deleteKeys := make([]uint32, 3)
deleteKeys := make([]uint32, 2)
nextKey = uint32(0)
vals, err := testerMap.GetValueAndDeleteBatch(unsafe.Pointer(&deleteKeys[0]), nil, unsafe.Pointer(&nextKey), uint32(len(deleteKeys)))
requestedCount := len(deleteKeys)

vals, err := testerMap.GetValueAndDeleteBatch(unsafe.Pointer(&deleteKeys[0]), nil, unsafe.Pointer(&nextKey), uint32(requestedCount))
if err != nil {
fmt.Fprintf(os.Stderr, "testerMap.LookupBatch failed: %v", err)
os.Exit(-1)
}
processedCount := len(vals)
if requestedCount != processedCount {
fmt.Fprintf(os.Stderr, "testerMap.LookupBatch failed: %d!=%d", requestedCount, processedCount)
os.Exit(-1)
}
for i, val := range vals {
actual := binary.LittleEndian.Uint32(val)
expected := deleteKeys[i] + 1
Expand All @@ -94,13 +101,32 @@ func main() {
fmt.Fprintf(os.Stderr, "testerMap.UpdateBatch failed: %v", err)
os.Exit(-1)
}

// Test batch delete.
testerMap.DeleteKeyBatch(unsafe.Pointer(&keys[0]), uint32(len(keys)))
testerMap.DeleteKeyBatch(unsafe.Pointer(&keys[0]), uint32(len(keys)-1))

// Ensure value is no longer there.
_, err = testerMap.GetValue(unsafe.Pointer(&keys[0]))
if err == nil {
fmt.Fprintf(os.Stderr, "testerMap.GetValue was expected to fail, but succeeded")
os.Exit(-1)
}

// Test batch lookup and delete when requesting more elements than are in the map.
deleteKeys = make([]uint32, 3)
nextKey = uint32(0)
requestedCount = 5

vals, err = testerMap.GetValueAndDeleteBatch(unsafe.Pointer(&deleteKeys[0]), nil, unsafe.Pointer(&nextKey), uint32(requestedCount))
if err != nil {
fmt.Fprintf(os.Stderr, "testerMap.LookupBatch failed: %v", err)
os.Exit(-1)
}
processedCount = len(vals)

// We removed all but one element in the test case before.
if processedCount != 1 {
fmt.Fprintf(os.Stderr, "testerMap.LookupBatch failed: processedCount=%d", processedCount)
os.Exit(-1)
}
}

0 comments on commit f2b3ab8

Please sign in to comment.