Files
LocalAI/pkg/model/operation_locks.go
Richard Palethorpe 9c43b2da8f fix(model): make backend shutdown model-scoped (#10865)
Avoid holding the global loader lock across backend lifecycle waits and propagate forced shutdown through distributed workers. Track parallel requests with in-flight counters and reserve worker ports until process termination.

Add focused race tests and an authoritative FizzBee lifecycle model with a fail-closed conformance target.

Assisted-by: Codex:GPT-5 [FizzBee] [Ginkgo]

Signed-off-by: Richard Palethorpe <io@richiejp.com>
2026-07-19 08:43:17 +02:00

72 lines
1.6 KiB
Go

package model
import (
"context"
"math"
"sync"
"golang.org/x/sync/semaphore"
)
// modelOperationLocks serializes destructive lifecycle operations per model
// without coupling unrelated models. Entries are reference-counted so model IDs
// observed from requests do not accumulate forever.
type modelOperationLocks struct {
mu sync.Mutex
entries map[string]*modelOperationLock
}
type modelOperationLock struct {
sem *semaphore.Weighted
refs int
}
func newModelOperationLocks() *modelOperationLocks {
return &modelOperationLocks{entries: make(map[string]*modelOperationLock)}
}
func (l *modelOperationLocks) acquire(modelID string, exclusive bool) func() {
release, err := l.acquireContext(context.Background(), modelID, exclusive)
if err != nil {
panic(err)
}
return release
}
func (l *modelOperationLocks) acquireContext(ctx context.Context, modelID string, exclusive bool) (func(), error) {
if ctx == nil {
ctx = context.Background()
}
l.mu.Lock()
entry := l.entries[modelID]
if entry == nil {
entry = &modelOperationLock{sem: semaphore.NewWeighted(math.MaxInt64)}
l.entries[modelID] = entry
}
entry.refs++
l.mu.Unlock()
weight := int64(1)
if exclusive {
weight = math.MaxInt64
}
if err := entry.sem.Acquire(ctx, weight); err != nil {
l.releaseReference(modelID, entry)
return nil, err
}
return func() {
entry.sem.Release(weight)
l.releaseReference(modelID, entry)
}, nil
}
func (l *modelOperationLocks) releaseReference(modelID string, entry *modelOperationLock) {
l.mu.Lock()
entry.refs--
if entry.refs == 0 {
delete(l.entries, modelID)
}
l.mu.Unlock()
}