cloudwego / cloudwego/eino

adk: InMemoryBackend.GrepRaw deadlocks permanently via recursive read lock when racing a writer

Open
#1,254 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
13k
Forks
1.1k
Avg merge
4h 6m
Merged PRs (30d)
41

Description

## Summary

`InMemoryBackend.GrepRaw` holds `b.mu.RLock()` for its entire duration (`adk/filesystem/backend_inmemory.go:202-203`), but when the request asks for context lines (`BeforeLines`/`AfterLines` — i.e. any `grep -A/-B/-C`), it reaches `applyContext`, which acquires `b.mu.RLock()` **again**:

- multi-file path: `GrepRaw` → `applyContext` (line 241) → nested `RLock` (line 538)
- single-file path: `GrepRaw` → `collector.buildResults` → `buildContentResult` → `applyContext` → nested `RLock`

`sync.RWMutex` forbids recursive read locking: once a writer (`Write`/`Edit` → `b.mu.Lock()`) queues behind the outer read lock, the nested `RLock` blocks behind the pending writer while the writer waits for the outer reader. The mutex wedges **permanently** — every later Read/Write/Edit/Grep blocks too, and no context cancellation can unblock a mutex.

## Trigger condition

Any grep with context flags racing a concurrent `write_file`/`edit_file` on the same backend. LLMs use grep context flags constantly, and the DeepAgent task prompt explicitly encourages parallel tool calls, so this interleaving is routine in real DeepAgent usage.

## To Reproduce

```go
b := filesystem.NewInMemoryBackend()
ctx := context.Background()
_ = b.Write(ctx, &filesystem.WriteRequest{FilePath: "/a.txt", Content: "hello\nworld\nhello again\n"})

go func() { // continuous writer
for {
_ = b.Edit(ctx, &filesystem.EditRequest{FilePath: "/a.txt", OldString: "world", NewString: "world"})
}
}()

// never returns once the writer queues between the two read locks
_, _ = b.GrepRaw(ctx, &filesystem.GrepRequest{Pattern: "hello", Path: "/", AfterLines: 1})
```

With a watchdog this reproduces on the first round against current main (`9d983b36`).

## Expected behavior

`GrepRaw` must never deadlock, regardless of concurrent writers.

## Proposed fix

`applyContext` is only ever called under `GrepRaw`'s read lock, so drop the nested `RLock`/`RUnlock` and document the locking contract. I have the fix plus a regression test (deadlock watchdog covering both the single-file and multi-file grep paths) ready and will open a PR shortly.

Contributor guide

Open the contributing guide

Research direction

Start in adk/filesystem/backend_inmemory.go at GrepRaw and applyContext, then trace the single-file collector path described in the issue. Review the existing locking contract and run the proposed regression test with a deadlock watchdog against both single-file and multi-file context grep while concurrent writes run; done means neither path hangs.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, testing
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.