Skip to content

daemon dies with fatal concurrent map writes in lsp.Provider.openDocument via relations.callers #714

Description

@zzet

Summary

A relations.callers call on a Go repo with gopls attached killed the entire daemon with fatal error: concurrent map writes. This is a Go fatal, not a panic, so the per-handler recover firewall in wrapToolHandlerMode cannot catch it: the process dies and every session's MCP transport drops, not just the offending call.

Stack (observed, daemon built from origin/main @ 217391f)

fatal error: concurrent map writes

goroutine 279 [running]:
internal/runtime/maps.fatal(...)
	/usr/local/go/src/runtime/panic.go:1195 +0x20
github.com/zzet/gortex/internal/semantic/lsp.(*Provider).openDocument(...)
	internal/semantic/lsp/provider.go:2789 +0xf8
github.com/zzet/gortex/internal/semantic/lsp.(*Provider).EnsureFileOpen(...)
	internal/semantic/lsp/provider.go:2961 +0x64
github.com/zzet/gortex/internal/semantic/lsp.(*Provider).ConfirmSymbolRefs(...)
	internal/semantic/lsp/provider.go:3007 +0x170
github.com/zzet/gortex/internal/mcp.(*Server).confirmSymbolRefsOnDemand(...)
	internal/mcp/tools_lsp.go:88 +0x144
github.com/zzet/gortex/internal/mcp.(*Server).handleGetCallers(...)
	internal/mcp/tools_core.go:2681 +0x478
github.com/zzet/gortex/internal/mcp.(*Server).invokeFacadeSpec(...)
	internal/mcp/facade_tools.go:1112 +0x11f4
github.com/zzet/gortex/internal/mcp.(*Server).handleFacade(...)
	internal/mcp/facade_tools.go:618 +0x1100
...
	internal/mcp/tool_deadline.go:154 +0x2e4

Root cause

openDocument releases docMu after its check-then-act on openDocs, then writes sourceCache with no lock at all (provider.go:2773-2789):

p.docMu.Lock()
if p.openDocs[absPath] {
    p.docMu.Unlock()
    return nil
}
p.docMu.Unlock()          // <- lock released here

content, err := os.ReadFile(absPath)
if err != nil {
    return err
}
if p.sourceCache == nil {
    p.sourceCache = map[string][]byte{}   // <- unguarded map write
}
p.sourceCache[absPath] = content          // <- unguarded map write

Both facts are documented in the file, which is why this reads as an invariant that quietly stopped holding rather than an oversight:

  • provider.go:91 — "docMu guards docVersions / openDocs / lastDiag" — sourceCache is deliberately not in that list.
  • provider.go:2030 — "sourceCache is the unsynchronised interactive-navigation cache".

sourceCache was safe while it was only touched from a single interactive path. confirmSymbolRefsOnDemand now reaches openDocument from the tool-request path, where two concurrent relations.callers / relations.usages calls on different files race directly on the map. The check-then-act on openDocs makes it worse: two goroutines can both pass the openDocs gate for the same file and race on the same key.

applyDocumentEdit at :2837-2840 and delete(p.sourceCache, ...) at :2864 are the other unguarded writers; the reader at :3134-3137 is unguarded too.

Reproduction

Tracked Go repo, gopls attached, then a relations.callers facade call concurrent with other tool traffic. Observed once during PR #705 validation on a 6-file fixture — small repos are enough, no special load needed.

Diagnosing it from the outside

The fatal lands in the daemon log ($GORTEX_HOME/cache/daemon.log, or the daemon start stdout). The failing call itself surfaces as a timeout or a cancelled request; the first clear symptom is that the next call returns daemon unavailable: dial unix ... connection refused. So the stack trace in the log is the only signal that this happened rather than an ordinary hang.

Suggested fix

Bring sourceCache under docMu (or give it its own mutex) at all four sites, and close the openDocs check-then-act so one file is opened once. Update the two comments that currently promise the opposite.

Unrelated to #705 — found while validating it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions