mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-28 17:18:49 -04:00
* feat(diagnostics): add private opt-in profiler Keep profiling on a dedicated loopback listener rather than the public API. Validate settings and bind synchronously so startup reports configuration and socket failures. Own sampling and shutdown for the run process. Cover private handlers, blocked cmdline access, sampling cleanup, and serving failures with focused Ginkgo tests. Assisted-by: nib:gpt-6-astra * feat(diagnostics): add sanitized phase timing Carry generated request and reload identities through private contexts. Keep timing disabled without allocations and emit only validated fields at Info level when enabled. Cover identity isolation, nested durations, inheritance, and sanitized logging with focused race-tested Ginkgo specs. Assisted-by: nib:gpt-6-astra * fix(diagnostics): retain request observations Reuse an existing request observation across recorders to avoid splitting correlation. Keep the original context, identity, and sink without new allocations. Preserve nil-recorder behavior and fresh reload identities. Verify the regression fails before the fix and passes with the full Diagnostics race suite afterward. Assisted-by: nib:gpt-6-astra * feat(cli): own opt-in diagnostics lifecycle Expose profiling and phase timing flags without enabling them for debug. Bind the private profiler before application initialization and propagate serving failures through the run lifecycle. Bound signal cleanup and preserve preload-only backend startup. Cover flag parsing, preload isolation, cancellation, and once-only shutdown. Document initialization and shutdown cancellation limitations. Assisted-by: nib:gpt-6-astra * feat(config): measure filter and reload phases Separate request lock waits from filtering and held work. Aggregate reload filesystem and parsing durations without logging inside the config lock. Preserve caller option execution under the lock after metadata collection. Extract only the internal diagnostics option before waiting for the lock. Keep direct callers uninstrumented unless they supply diagnostics options. Verify focused Diagnostics examples with the race detector, including option ordering, gated contention, errors, panic propagation, and parity. Assisted-by: nib:gpt-6-astra * fix(config): separate early reload observation Opaque callbacks cannot select timing before their execution point. Carry the startup recorder explicitly through loader construction. Apply late options once under the lock and honor composed last writes. Document early and late phase boundaries and observation identities. Report malformed gallery syntax as an internal parse failure without changing strict or non-strict loader returns. Assisted-by: nib:gpt-6-astra * fix(config): preserve per-model option callbacks The resolved snapshot suppresses existing callbacks during model defaults, even when diagnostics are disabled. Pass original options through the existing defaults pathway without adding diagnostics-driven replay. Test stateful options with two models in enabled and disabled modes. Preserve three calls and thread defaults of two and three. Assisted-by: nib:gpt-6-astra * feat(model): observe discovery phases Carry request diagnostics through config and loose-file discovery to separate directory enumeration from in-memory filtering and fallback. Keep existing APIs as background-context wrappers and preserve discovery policies, ordering, duplicates, errors, and filesystem operation counts. Add focused parity, privacy, phase, and disabled-allocation tests. The focused diagnostics race gate passes for model and galleryop. Assisted-by: nib:gpt-6-astra * feat(http): observe extraction phases Generate diagnostic identity before request discovery, independently of client correlation headers. End each extraction segment before inference so downstream latency does not inflate discovery measurements. Carry observations through discovery helpers and rebuilt request contexts without changing cancellation or model selection. Cover disabled parity, error outcomes, privacy, concurrent identity isolation and route contexts. Assisted-by: nib:gpt-6-astra * fix(http): retain diagnostics across failover Failover restores its entry request before retrying. Initialize the observation before that capture so direct extraction retains one ID. Keep extraction spans per attempt and end them before downstream work. Exercise two real targets without default middleware, including replay, shared identity, phase boundaries, and timing-off response parity. Assisted-by: nib:gpt-6-astra * feat(model): observe initialization and routing Mark loader entry before cache and admission checks. Measure only the ModelRouter callback, without attributing transport or worker latency. Test inherited identity and sanitized events with fake backends. Exercise the actual Echo router to keep profiles private across debug and timing options. Cover capture shutdown, forced close, and lifecycle failures. Assisted-by: nib:gpt-6-astra * docs(diagnostics): guide bounded investigations Document private profiling, sampling ownership, and phase boundaries so operators can interpret captures without assuming request attribution. Add Ginkgo drift checks for flags, defaults, phases, links, and capture warnings. These checks do not certify the operator prose. The focused documentation race test passes after four expected failures. The consolidated Diagnostics race gate passes across all eight packages. This gate does not validate the full suite, auth, or production UI. Assisted-by: nib:gpt-6-astra * refactor(cli): restore inline run lifecycle Remove diagnostic startup hooks so application startup stays readable in Run. Keep the profiler as an optional sidecar with bounded cleanup and an error watcher, without a general lifecycle runner. Replace hook simulations with real CLI startup checks and component capture tests. Document that application cleanup can exceed the private listener budget. The eight-package Diagnostics race gate passes. A cleanup mutation fails the real CLI listener-release regression as expected. Assisted-by: nib:gpt-6-astra * fix(cli): retain concurrent startup errors Cancellation must not hide a public API bind failure. Join the public serve result, cancellation cause, and HTTP shutdown errors inline. Ignore ErrServerClosed only after intentional shutdown. Exercise real startup with an occupied API port and a failed private listener. Isolate the Linux socket fixture in a subprocess and wait for cancellation acknowledgment without timing sleeps. Assisted-by: nib:gpt-6-astra * refactor(http): make request context primary Move context preparation into the request extractor. Diagnostics remain an optional addition, not the entry point for request processing. Preserve context initialization before failover captures the request. Assisted-by: nib:gpt-6-astra --------- Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
132 lines
4.5 KiB
Go
132 lines
4.5 KiB
Go
package modeladmin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/services/messaging"
|
|
)
|
|
|
|
// ApplyRemoteChange refreshes this replica's in-memory model state from a peer
|
|
// replica's model-config change broadcast (messaging.CacheInvalidateEvent on
|
|
// SubjectCacheInvalidateModels). It is the subscriber-side counterpart to
|
|
// GalleryService.BroadcastModelsChanged.
|
|
//
|
|
// The event is only a wake-up signal. Its operation and revision may be stale
|
|
// or reordered, so named changes are always reconciled against the current
|
|
// shared filesystem state.
|
|
//
|
|
// Revision-aware events apply the same idempotent lifecycle transition as the
|
|
// originating frontend. modelsPath and opts are forwarded to
|
|
// LoadModelConfigsFromPath.
|
|
func ApplyRemoteChange(ctx context.Context, cl *config.ModelConfigLoader, modelsPath string, evt messaging.CacheInvalidateEvent, lifecycle ModelRevisionLifecycle, opts ...config.ConfigLoaderOption) error {
|
|
return cl.WithModelConfigMutation(func() error {
|
|
return applyRemoteChange(ctx, cl, modelsPath, evt, lifecycle, opts...)
|
|
})
|
|
}
|
|
|
|
func applyRemoteChange(ctx context.Context, cl *config.ModelConfigLoader, modelsPath string, evt messaging.CacheInvalidateEvent, lifecycle ModelRevisionLifecycle, opts ...config.ConfigLoaderOption) error {
|
|
authoritative := config.NewModelConfigLoader(modelsPath, cl.ReloadDiagnosticsOption())
|
|
if err := authoritative.LoadModelConfigsFromPathStrict(modelsPath, opts...); err != nil {
|
|
return err
|
|
}
|
|
currentConfigs := cl.GetAllModelsConfigs()
|
|
current := make(map[string]config.ModelConfig, len(currentConfigs))
|
|
outside := map[string]struct{}{}
|
|
for _, cfg := range currentConfigs {
|
|
// A config read from outside the models directory (--config-file) is
|
|
// invisible to the snapshot. Treating it as removed would drop it and
|
|
// publish a deletion revision for a model that still exists.
|
|
if cfg.DefinedOutside(modelsPath) {
|
|
outside[cfg.Name] = struct{}{}
|
|
continue
|
|
}
|
|
current[cfg.Name] = cfg
|
|
}
|
|
snapshotConfigs := authoritative.GetAllModelsConfigs()
|
|
snapshot := configsByName(snapshotConfigs)
|
|
for name := range outside {
|
|
delete(snapshot, name)
|
|
}
|
|
named := evt.Element
|
|
if _, isOutside := outside[named]; isOutside {
|
|
named = ""
|
|
}
|
|
changed, err := changedConfigNames(current, snapshot, named)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if lifecycle != nil {
|
|
transitions := make([]ModelRevisionTransition, 0, len(changed))
|
|
for _, name := range changed {
|
|
cfg, exists := snapshot[name]
|
|
revision := DeletedModelConfigRevision(name)
|
|
disabled := true
|
|
if exists {
|
|
var err error
|
|
revision, err = authoritative.RevisionForPath(name, modelsPath, opts...)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve authoritative model config revision for %q: %w", name, err)
|
|
}
|
|
disabled = cfg.IsDisabled()
|
|
}
|
|
transitions = append(transitions, ModelRevisionTransition{ModelName: name, ConfigRevision: revision, Disabled: disabled})
|
|
}
|
|
if len(transitions) > 0 {
|
|
if _, err := lifecycle.ApplyConfigRevisions(ctx, transitions); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
cl.ReplaceModelConfigs(config.MergeDirectorySnapshot(currentConfigs, snapshotConfigs, modelsPath))
|
|
return nil
|
|
}
|
|
|
|
func configsByName(configs []config.ModelConfig) map[string]config.ModelConfig {
|
|
result := make(map[string]config.ModelConfig, len(configs))
|
|
for _, cfg := range configs {
|
|
result[cfg.Name] = cfg
|
|
}
|
|
return result
|
|
}
|
|
|
|
func changedConfigNames(current, snapshot map[string]config.ModelConfig, named string) ([]string, error) {
|
|
changed := map[string]struct{}{}
|
|
for name, cfg := range snapshot {
|
|
previous, exists := current[name]
|
|
if !exists {
|
|
changed[name] = struct{}{}
|
|
continue
|
|
}
|
|
// Both sides come from a loader, so both carry the revision stamped
|
|
// when their file was parsed. Comparing the stamps compares the files.
|
|
if previous.PersistedConfigRevision() != cfg.PersistedConfigRevision() {
|
|
changed[name] = struct{}{}
|
|
}
|
|
}
|
|
for name := range current {
|
|
if _, exists := snapshot[name]; !exists {
|
|
changed[name] = struct{}{}
|
|
}
|
|
}
|
|
if named != "" {
|
|
changed[named] = struct{}{}
|
|
}
|
|
names := make([]string, 0, len(changed))
|
|
for name := range changed {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names, nil
|
|
}
|
|
|
|
// DeletedModelConfigRevision is a stable tombstone generation for an absent
|
|
// model. It lets every frontend derive the same authoritative state regardless
|
|
// of which reordered cache-invalidation event woke it up.
|
|
func DeletedModelConfigRevision(modelName string) string {
|
|
return config.DeletedModelConfigRevision(modelName)
|
|
}
|