ollie/cmd/toolsrv/internal/server/pathlock.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
}