From 550f064f95113e072677b871b7de30ecf25d62b8 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 18 Apr 2026 13:13:25 +0300 Subject: fix task 45: bound pending handle cleanup --- internal/eventloop.go | 32 ++++++++++----- internal/eventloop_cleanup_test.go | 42 ++++++++++++++++++++ internal/eventloop_exit.go | 7 ++-- internal/eventloop_state.go | 80 ++++++++++++++++++++++++++++++++++++++ internal/eventloop_test.go | 4 +- 5 files changed, 151 insertions(+), 14 deletions(-) diff --git a/internal/eventloop.go b/internal/eventloop.go index 645f6af..87f99ed 100644 --- a/internal/eventloop.go +++ b/internal/eventloop.go @@ -13,11 +13,12 @@ import ( const sysEnterNameToHandleAtName = "name_to_handle_at" const ( - defaultCommLookupWorkers = 4 - defaultCommLookupQueueSize = 512 - defaultMaxPendingEnterEvs = 16384 - defaultMaxProcFdCacheSize = 8192 - cacheTrimDivisor = 4 + defaultCommLookupWorkers = 4 + defaultCommLookupQueueSize = 512 + defaultMaxPendingEnterEvs = 16384 + defaultMaxPendingHandleEntries = 8192 + defaultMaxProcFdCacheSize = 8192 + cacheTrimDivisor = 4 ) type eventLoopConfig struct { @@ -38,9 +39,9 @@ type rawEventHandler func(raw []byte, ch chan<- *event.Pair) type eventLoop struct { filter globalfilter.Filter - pairs pairTracker // enter/exit pairing state and inter-syscall duration tracking - pendingHandles map[uint32]string // TID → pathname from name_to_handle_at, for open_by_handle_at correlation - fdTracker *fdTracker // fd table and procfs resolution cache + pairs pairTracker // enter/exit pairing state and inter-syscall duration tracking + pendingHandles *pendingHandleTracker // TID → pathname from name_to_handle_at, for open_by_handle_at correlation + fdTracker *fdTracker // fd table and procfs resolution cache commResolver *commResolver rawHandlers map[types.EventType]rawEventHandler printCb func(ep *event.Pair) // Callback to print the event @@ -66,7 +67,7 @@ func newEventLoop(cfg eventLoopConfig) (*eventLoop, error) { el := &eventLoop{ filter: cfg.filter.Clone(), pairs: newPairTracker(), - pendingHandles: make(map[uint32]string), + pendingHandles: newPendingHandleTracker(), fdTracker: fdState, commResolver: commState, rawHandlers: make(map[types.EventType]rawEventHandler), @@ -118,6 +119,19 @@ func (e *eventLoop) fdState() *fdTracker { return e.fdTracker } +func (e *eventLoop) pendingHandleState() *pendingHandleTracker { + if e.pendingHandles == nil { + e.pendingHandles = newPendingHandleTracker() + } + if e.pendingHandles.paths == nil { + e.pendingHandles.paths = make(map[uint32]string) + } + if e.pendingHandles.pathAges == nil { + e.pendingHandles.pathAges = make(map[uint32]uint64) + } + return e.pendingHandles +} + func (e *eventLoop) commState() *commResolver { if e.commResolver == nil { e.commResolver = newCommResolver(nil) diff --git a/internal/eventloop_cleanup_test.go b/internal/eventloop_cleanup_test.go index 1d3a6fb..7c8604b 100644 --- a/internal/eventloop_cleanup_test.go +++ b/internal/eventloop_cleanup_test.go @@ -3,7 +3,9 @@ package internal import ( "testing" + "ior/internal/event" "ior/internal/file" + "ior/internal/types" ) func TestTracepointEnteredPrunesOldestPendingPairs(t *testing.T) { @@ -88,3 +90,43 @@ func TestProcFdCacheRetainsRecentlyUsedEntries(t *testing.T) { t.Fatalf("proc fd cache metadata size = %d, want 2", got) } } + +func TestPendingHandleTrackerRetainsRecentlyUsedEntries(t *testing.T) { + tracker := newPendingHandleTracker() + tracker.maxCacheSize = 2 + + tracker.set(defaultTid, "/tmp/handle-one") + tracker.set(defaultTid+1, "/tmp/handle-two") + tracker.set(defaultTid+2, "/tmp/handle-three") + + if _, ok := tracker.paths[defaultTid]; ok { + t.Fatalf("expected oldest pending handle to be evicted") + } + if _, ok := tracker.paths[defaultTid+1]; !ok { + t.Fatalf("expected newer pending handle to be retained") + } + if _, ok := tracker.paths[defaultTid+2]; !ok { + t.Fatalf("expected newest pending handle to be retained") + } + if got := len(tracker.pathAges); got != 2 { + t.Fatalf("pending handle metadata size = %d, want 2", got) + } +} + +func TestOpenByHandleAtFailureClearsPendingHandle(t *testing.T) { + el := mustNewEventLoop(t, eventLoopConfig{}) + + _, enterNameRaw := makeEnterPathEvent(t, defaulTime, defaultPid, defaultTid, "/tmp/handle.txt", types.SYS_ENTER_NAME_TO_HANDLE_AT) + el.tracepointEntered(types.NewPathEvent(enterNameRaw)) + _, exitNameRaw := makeExitRetEvent(t, defaulTime+1, defaultPid, defaultTid, types.SYS_EXIT_NAME_TO_HANDLE_AT, 0) + el.tracepointExited(types.NewRetEvent(exitNameRaw), make(chan *event.Pair, 1)) + + _, enterOpenRaw := makeEnterOpenByHandleAtEvent(t, defaulTime+2, defaultPid, defaultTid, 0) + el.tracepointEntered(types.NewOpenByHandleAtEvent(enterOpenRaw)) + _, exitOpenRaw := makeExitRetEvent(t, defaulTime+3, defaultPid, defaultTid, types.SYS_EXIT_OPEN_BY_HANDLE_AT, -1) + el.tracepointExited(types.NewRetEvent(exitOpenRaw), make(chan *event.Pair, 1)) + + if _, ok := el.pendingHandleState().paths[defaultTid]; ok { + t.Fatalf("expected pending handle to be cleared after failed open_by_handle_at") + } +} diff --git a/internal/eventloop_exit.go b/internal/eventloop_exit.go index c74ecea..e97688a 100644 --- a/internal/eventloop_exit.go +++ b/internal/eventloop_exit.go @@ -70,7 +70,7 @@ func (e *eventLoop) handlePathExit(ep *event.Pair, pathEv *types.PathEvent) bool ep.Recycle() return false } - e.pendingHandles[pathEv.GetTid()] = types.StringValue(pathEv.Pathname[:]) + e.pendingHandleState().set(pathEv.GetTid(), types.StringValue(pathEv.Pathname[:])) ep.Recycle() return false } @@ -175,18 +175,19 @@ func (e *eventLoop) handleOpenByHandleAtExit(ep *event.Pair, openByHandleEv *typ tid := openByHandleEv.GetTid() retEvent, ok := ep.ExitEv.(*types.RetEvent) if !ok { + e.pendingHandleState().delete(tid) e.recyclePair(ep, "Dropped malformed open_by_handle_at exit event") return false } fd := int32(retEvent.Ret) if fd < 0 { + e.pendingHandleState().delete(tid) ep.Recycle() return false } - if pathname, ok := e.pendingHandles[tid]; ok { - delete(e.pendingHandles, tid) + if pathname, ok := e.pendingHandleState().consume(tid); ok { fdFile := file.NewFd(fd, pathname, openByHandleEv.Flags) e.fdState().set(fd, fdFile) ep.File = fdFile diff --git a/internal/eventloop_state.go b/internal/eventloop_state.go index 9622fd1..5cb0a78 100644 --- a/internal/eventloop_state.go +++ b/internal/eventloop_state.go @@ -18,6 +18,15 @@ type fdTracker struct { age uint64 // monotonic counter for LRU ordering } +// pendingHandleTracker holds unresolved name_to_handle_at pathnames keyed by +// TID until the corresponding open_by_handle_at exit consumes them. +type pendingHandleTracker struct { + paths map[uint32]string + pathAges map[uint32]uint64 + maxCacheSize int + age uint64 +} + func newFDTracker(files map[int32]file.File) *fdTracker { if files == nil { files = make(map[int32]file.File) @@ -29,6 +38,13 @@ func newFDTracker(files map[int32]file.File) *fdTracker { } } +func newPendingHandleTracker() *pendingHandleTracker { + return &pendingHandleTracker{ + paths: make(map[uint32]string), + pathAges: make(map[uint32]uint64), + } +} + func (t *fdTracker) get(fd int32) (file.File, bool) { f, ok := t.files[fd] return f, ok @@ -135,6 +151,50 @@ func (t *fdTracker) deleteCacheKey(key uint64) { delete(t.procFdAges, key) } +func (t *pendingHandleTracker) set(tid uint32, pathname string) { + if t.paths == nil { + t.paths = make(map[uint32]string) + t.pathAges = make(map[uint32]uint64) + } + t.age++ + t.paths[tid] = pathname + t.pathAges[tid] = t.age + t.prune() +} + +func (t *pendingHandleTracker) consume(tid uint32) (string, bool) { + pathname, ok := t.paths[tid] + if !ok { + return "", false + } + delete(t.paths, tid) + delete(t.pathAges, tid) + return pathname, true +} + +func (t *pendingHandleTracker) delete(tid uint32) { + delete(t.paths, tid) + delete(t.pathAges, tid) +} + +func (t *pendingHandleTracker) prune() { + if t.paths == nil { + return + } + limit := t.limit() + if len(t.paths) <= limit { + return + } + trimOldestPendingHandles(t.paths, t.pathAges, trimTarget(limit)) +} + +func (t *pendingHandleTracker) limit() int { + if t.maxCacheSize > 0 { + return t.maxCacheSize + } + return defaultMaxPendingHandleEntries +} + // pairTracker holds the state for matching sys_enter events to their sys_exit // counterparts and computing inter-syscall durations per TID. type pairTracker struct { @@ -257,6 +317,26 @@ func trimOldestProcFdEntries(state map[uint64]*file.FdFile, ages map[uint64]uint } } +func trimOldestPendingHandles(state map[uint32]string, ages map[uint32]uint64, targetSize int) { + excess := len(state) - targetSize + if excess <= 0 { + return + } + type pendingHandleAge struct { + tid uint32 + age uint64 + } + oldest := make([]pendingHandleAge, 0, len(state)) + for tid := range state { + oldest = append(oldest, pendingHandleAge{tid: tid, age: ages[tid]}) + } + slices.SortFunc(oldest, func(a, b pendingHandleAge) int { return cmp.Compare(a.age, b.age) }) + for _, entry := range oldest[:excess] { + delete(state, entry.tid) + delete(ages, entry.tid) + } +} + func trimTarget(limit int) int { target := limit - limit/cacheTrimDivisor if target < 1 { diff --git a/internal/eventloop_test.go b/internal/eventloop_test.go index 98f2209..473a107 100644 --- a/internal/eventloop_test.go +++ b/internal/eventloop_test.go @@ -2339,7 +2339,7 @@ func makeNameToHandleAtTestData(t *testing.T) (td testData) { verifyFileDescriptor(t, el, fd, pathname) // Verify that the pending handle has been consumed - if _, ok := el.pendingHandles[defaultTid]; ok { + if _, ok := el.pendingHandleState().paths[defaultTid]; ok { t.Errorf("Expected pending handle for tid %d to be consumed", defaultTid) } }) @@ -2376,7 +2376,7 @@ func makeNameToHandleAtFailureTestData(t *testing.T) (td testData) { t.Errorf("Expected open_by_handle_at to not use failed name_to_handle_at path") } - if _, ok := el.pendingHandles[defaultTid]; ok { + if _, ok := el.pendingHandleState().paths[defaultTid]; ok { t.Errorf("Expected no pending handle for tid %d after failure", defaultTid) } }) -- cgit v1.2.3