Skip to content

fix(localfile): harden wildcard Read (per-file fd release + final grep line) - #634

Open
sfc-gh-ikryvanos wants to merge 2 commits into
mainfrom
fix/localfile-wildcard-fd-leak
Open

fix(localfile): harden wildcard Read (per-file fd release + final grep line)#634
sfc-gh-ikryvanos wants to merge 2 commits into
mainfrom
fix/localfile-wildcard-fd-leak

Conversation

@sfc-gh-ikryvanos

@sfc-gh-ikryvanos sfc-gh-ikryvanos commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two related availability/correctness fixes in the LocalFile Read path:

  1. Per-file descriptor release in wildcard reads. Wildcard reads processed every matched file inside one goroutine loop, so the open file and dataPrep closer were held by loop-scoped defers that only ran when the whole request finished. A large directory could therefore exhaust the process's file descriptors. Each file is now handled in its own readSingleFile call so its descriptors are released as soon as that file is done. The producer also respects context cancellation so it can't block forever (and leak the goroutine) if the consumers have already exited on error or client disconnect.

  2. Return the final line when grepping files without a trailing newline. The grep path buffers each line in trailingLine until it sees a \n. A file whose last line had no trailing newline left that line in the buffer, and it was silently dropped on EOF — so the end of the file was missing from grep results. It's now flushed through the matcher at EOF (honoring invert-match) before finishing the file. The existing max-line-length guard still bounds buffer growth, so this doesn't reintroduce unbounded memory use.

Test plan

  • TestReadWildcardManyFilesNoFDLeak: reads a directory with many files under a constrained RLIMIT_NOFILE; fails with the old loop-scoped defers, passes now.
  • TestReadDifferentLengthFiles: files of varying lengths around the streaming chunk boundary — byte-exact plain reads; grep of unterminated final lines (including one spanning a chunk boundary); invert-match; and a wildcard total-bytes check.
  • go test ./services/localfile/server/ passes; go vet clean.
  • Verified end-to-end against a local server + client:
    • Old code peaked at ~1605 open FDs for a 1000-file wildcard read; fixed code stays flat at ~16–20.
    • Grepping a file whose last line lacks a trailing newline now returns that line (previously empty); invert-match and plain reads unaffected.

sfc-gh-ikryvanos and others added 2 commits August 6, 2026 12:49
The wildcard Read worker opened each matched file and deferred both the
file close and the dataPrep cleanup inside the consumer loop, so every
descriptor stayed open until the whole request finished. A read over a
large directory could therefore exhaust the process's file-descriptor
limit (on Linux each file also allocates inotify/epoll fds). The producer
also blocked forever on a full channel if consumers exited early, leaking
that goroutine.

Move the per-file body verbatim into readSingleFile so its defers run per
file, make the producer send honor context cancellation, and always close
the channel once. The extracted body is unchanged, so reviewing with
whitespace hidden shows only the real changes.

Adds a regression test that reads a wildcard directory under a constrained
RLIMIT_NOFILE.

Co-authored-by: Cursor <cursoragent@cursor.com>
…g newline

Read's grep path buffers each line in trailingLine until it sees a '\n'.
When a file's last line had no trailing newline, that line stayed in the
buffer and was silently dropped on EOF, so the end of the file was
missing from grep results.

Flush any remaining trailingLine through the matcher at EOF (honoring
invert-match) before finishing the file. The existing max-line-length
guard still bounds buffer growth, so this doesn't reintroduce unbounded
memory use.

Add TestReadDifferentLengthFiles covering files of varying lengths around
the streaming chunk boundary: byte-exact plain reads, grep of
unterminated final lines (including one spanning a chunk), invert-match,
and a wildcard total-bytes check.

Co-authored-by: Cursor <cursoragent@cursor.com>
return status.Error(codes.InvalidArgument, "wildcard reads are not supported in declared compatible API version.")
}
errs.Go(func() error {
if err := s.listFor(path, ctx, func(item *pb.StatReply) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add PR description?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants