72 lines
2.2 KiB
Go
72 lines
2.2 KiB
Go
// pathlock.go — Path-based read/write lock coordination for tool execution.
|
|
//
|
|
// All foreground tool calls acquire a lock before execution based on the
|
|
// tool's declared scope and the file path from its arguments:
|
|
//
|
|
// - scope "read": no lock (reads never conflict)
|
|
// - scope "write": exclusive lock on the file path (serializes writes to same path)
|
|
// - scope "global": exclusive global lock (serializes with everything)
|
|
//
|
|
// This ensures correctness across all agents in a session (and eventually
|
|
// across sessions sharing the same toolsrv), without the agents needing
|
|
// to coordinate explicitly.
|
|
package server
|
|
|
|
import "sync"
|
|
|
|
// pathLockTable provides path-granularity serialization for tool execution.
|
|
type pathLockTable struct {
|
|
mu sync.Mutex // protects the paths map
|
|
global sync.RWMutex // global barrier: write-locked by "global" scope tools
|
|
paths map[string]*sync.Mutex // per-path exclusive locks for "write" scope tools
|
|
}
|
|
|
|
// acquire locks the appropriate resources for a tool call and returns a
|
|
// release function. The caller MUST call release when execution completes.
|
|
//
|
|
// Semantics:
|
|
// - "read": returns immediately (no-op release)
|
|
// - "write": holds global.RLock + exclusive path lock
|
|
// - "global" (or empty): holds global.Lock (exclusive — blocks all writes and other globals)
|
|
func (t *pathLockTable) acquire(scope, path string) func() {
|
|
switch scope {
|
|
case "read":
|
|
return func() {} // reads never conflict
|
|
|
|
case "write":
|
|
if path == "" {
|
|
// No path extractable — fall through to global barrier.
|
|
t.global.Lock()
|
|
return func() { t.global.Unlock() }
|
|
}
|
|
// Shared global lock (allows concurrent writes to different paths,
|
|
// blocks while a global-scope tool is running).
|
|
t.global.RLock()
|
|
pl := t.pathLock(path)
|
|
pl.Lock()
|
|
return func() {
|
|
pl.Unlock()
|
|
t.global.RUnlock()
|
|
}
|
|
|
|
default: // "global", "", or unknown
|
|
t.global.Lock()
|
|
return func() { t.global.Unlock() }
|
|
}
|
|
}
|
|
|
|
// pathLock returns (or creates) the mutex for a given path.
|
|
func (t *pathLockTable) pathLock(path string) *sync.Mutex {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if t.paths == nil {
|
|
t.paths = make(map[string]*sync.Mutex)
|
|
}
|
|
l, ok := t.paths[path]
|
|
if !ok {
|
|
l = &sync.Mutex{}
|
|
t.paths[path] = l
|
|
}
|
|
return l
|
|
}
|