fix(19.7): RA-WR-01 never wait on a session lock under the router lock
Fan-out matching snapshots the sessions (and stateless entries) under the router-wide lock, releases it, and reads each identity through a small identity box that is never held across I/O, so an event no longer waits on a session that is re-filtering while it holds sr.mu or sp.mu. Widening and the fan-out recomputes run their guard checks with guardContext: not cancelled with the triggering request, but bounded by mcp.call_timeout, so a provider that never answers cannot hold a session lock forever. README and docs/services/mcp.md describe the wider meaning of mcp.call_timeout. TestLockScopeUnderWidening blocks a widening guard check, fires a permission event for that session and asserts that another session's call still finishes, and that a never-answering check ends at call_timeout.
This commit is contained in:
@@ -29,8 +29,10 @@ type statelessEntry struct {
|
||||
server *mcp.Server
|
||||
elem *list.Element
|
||||
|
||||
// mu serializes re-filtering and guards fingerprint, tools and prompts
|
||||
// (see localSession); identity has its own box.
|
||||
mu sync.Mutex
|
||||
identity Identity
|
||||
identity identityBox
|
||||
fingerprint string // of the identity the tool set was last filtered for
|
||||
tools map[string]bool
|
||||
prompts map[string]bool
|
||||
@@ -138,7 +140,7 @@ func (sp *statelessPath) getServer(r *http.Request) *mcp.Server {
|
||||
// (A-WR-07). A concurrent request of the same binding may have
|
||||
// inserted an entry meanwhile; that one is adopted.
|
||||
srv, tools, prompts := sp.p.newServer(r.Context(), id, nil)
|
||||
e := &statelessEntry{binding: id.Binding, server: srv, identity: id, fingerprint: id.fingerprint(), tools: tools, prompts: prompts}
|
||||
e := &statelessEntry{binding: id.Binding, server: srv, identity: identityBox{id: id}, fingerprint: id.fingerprint(), tools: tools, prompts: prompts}
|
||||
sp.mu.Lock()
|
||||
if cur, ok := sp.entries[id.Binding]; ok {
|
||||
sp.order.MoveToFront(cur.elem)
|
||||
@@ -184,7 +186,7 @@ func (sp *statelessPath) hit(binding string) *statelessEntry {
|
||||
// server when the identity's fingerprint changed (A-WR-04).
|
||||
func (sp *statelessPath) refresh(ctx context.Context, e *statelessEntry, id Identity) {
|
||||
e.mu.Lock()
|
||||
e.identity = id
|
||||
e.identity.set(id)
|
||||
sp.p.widen(ctx, e.server, id, &e.fingerprint, e.tools, e.prompts)
|
||||
e.mu.Unlock()
|
||||
}
|
||||
@@ -201,16 +203,18 @@ func (sp *statelessPath) matching(guard string, userID uint) []*statelessEntry {
|
||||
return sp.matchingFunc(func(id Identity) bool { return id.has(guard, userID) })
|
||||
}
|
||||
|
||||
// matchingFunc lists the entries whose identity satisfies match.
|
||||
// matchingFunc lists the entries whose identity satisfies match, matched
|
||||
// after the path lock is released (see sessionRouter.matchingFunc).
|
||||
func (sp *statelessPath) matchingFunc(match func(Identity) bool) []*statelessEntry {
|
||||
sp.mu.Lock()
|
||||
defer sp.mu.Unlock()
|
||||
var out []*statelessEntry
|
||||
all := make([]*statelessEntry, 0, len(sp.entries))
|
||||
for _, e := range sp.entries {
|
||||
e.mu.Lock()
|
||||
hit := match(e.identity)
|
||||
e.mu.Unlock()
|
||||
if hit {
|
||||
all = append(all, e)
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
var out []*statelessEntry
|
||||
for _, e := range all {
|
||||
if match(e.identity.get()) {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user