adk: InMemoryBackend.GrepRaw deadlocks permanently via recursive read lock when racing a writer
- 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
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