mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-14 07:07:33 -04:00
A local-ai worker no longer opens a bus connection. connectNATS and its
spec are gone; Run registers once, starts its tunnel, arms /readyz on that
tunnel, and heartbeats. The worker's bus credential flags (--nats-jwt,
--nats-user-seed, --nats-require-auth, the three TLS flags) and
Config.NatsAuthRequired go with it. --nats-url stays, accepted and
ignored, so an existing worker command line still parses.
/readyz was the thing most likely to wedge a tunnel-only worker: it
required a live NATS link, so a worker with no bus would have reported
itself unready forever. nodes.NATSReadiness becomes nodes.TunnelReadiness
over a local interface{ Connected() bool }, and worker.Tunnel gains
Connected(), backed by a mutex-guarded session field the loop publishes
and clears. A closed-but-not-yet-cleared session reads as disconnected:
the loop waits for every in-flight stream before it clears the field, and
the probe must answer not-ready through that wait.
The heartbeat gate is DELETED rather than re-pointed at the tunnel. The
heartbeat is the worker's own answer that its process is alive; whether
the frontend can reach it is a separate fact the frontend already holds
and ages against LOCALAI_WORKER_RECONNECT_GRACE. Withholding the
heartbeat would report an unreachable worker as an absent one on the one
path with no grace, where the health monitor marks it offline and its
pending backend ops are deleted behind it. heartbeatLoop is given no view
of the tunnel, so a gate cannot be added back without changing its
signature.
Removing the NATS credential manager from this path also removes a defect
it carried: its refresh loop re-registered on a timer to renew a JWT, and
Register CLEARS a node's NodeModel rows. Any backend worker running on
frontend-minted credentials had its replica rows deleted roughly every
18 hours.
Of core/cli/workerregistry, everything survives. The manager is still
used in full by core/cli/agent_worker.go, which still needs NATS: Acquire,
Provider, RefreshLoop, HasCredentials and TunnelToken are all untouched.
The backend worker simply calls RegisterFullWithRetry directly now.
WorkerPermissions is documented as serving agent nodes, and its non-agent
branch narrowed to _INBOX.> on both sides. It is NOT deleted: NATS reads
an empty allow list as no restriction, so returning nil would upgrade
every JWT the frontend still mints for a backend node from its own inbox
to the whole account.
Agent workers keep the bus everywhere: their CLI flags, their
subscriptions, the agent branch of WorkerPermissions, and the compose
service with its LOCALAI_NATS_URL and depends_on: nats.
Also corrected two flags the Nodes page advertised that do not exist
(--distributed-nats, --distributed-db), and a log line plus several
comments that still named a bus the code no longer touches.
Assisted-by: Claude Opus 5 [claude-code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
148 lines
5.4 KiB
Go
148 lines
5.4 KiB
Go
package worker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/gallery"
|
|
"github.com/mudler/LocalAI/pkg/model"
|
|
"github.com/mudler/LocalAI/pkg/system"
|
|
"github.com/mudler/xlog"
|
|
)
|
|
|
|
// modelInstaller is the subset of gallery.InstallModelFromGallery the prefetch
|
|
// loop needs. Carved out as a function value so tests can substitute a fake
|
|
// installer without touching the real gallery — and so we don't duplicate the
|
|
// full install pipeline (URL resolution, SHA verification, idempotent skip,
|
|
// config-file write) which already lives in core/gallery/models.go.
|
|
type modelInstaller func(
|
|
ctx context.Context,
|
|
modelGalleries, backendGalleries []config.Gallery,
|
|
systemState *system.SystemState,
|
|
modelLoader *model.ModelLoader,
|
|
name string,
|
|
) error
|
|
|
|
// realModelInstaller is the production binding to gallery.InstallModelFromGallery.
|
|
// Kept as a package-level var so tests can swap it; production code never
|
|
// reassigns it.
|
|
var realModelInstaller modelInstaller = func(
|
|
ctx context.Context,
|
|
modelGalleries, backendGalleries []config.Gallery,
|
|
systemState *system.SystemState,
|
|
modelLoader *model.ModelLoader,
|
|
name string,
|
|
) error {
|
|
// enforceScan=false: workers fetch from the same gallery the master already
|
|
// trusts, and the master would have scanned at install time anyway.
|
|
// autoloadBackendGalleries=false: the worker installs backends on demand when
|
|
// the frontend calls its install control route; prefetching one here would race the
|
|
// supervisor's own install path and double-trigger gallery work.
|
|
// requireBackendIntegrity=false: same reason — we're not installing a backend.
|
|
return gallery.InstallModelFromGallery(
|
|
ctx,
|
|
modelGalleries,
|
|
backendGalleries,
|
|
systemState,
|
|
modelLoader,
|
|
name,
|
|
gallery.GalleryModel{},
|
|
nil, /* downloadStatus: silent on the worker; master is the UX surface */
|
|
false /* enforceScan */, false /* autoloadBackendGalleries */, false, /* requireBackendIntegrity */
|
|
)
|
|
}
|
|
|
|
// prefetchModels resolves each configured gallery ID against the model gallery
|
|
// and downloads the artifact into the worker's /models. It is called once at
|
|
// worker startup, BEFORE the worker registers or opens its tunnel, so that the
|
|
// steady state has the file already on disk and the master never needs to
|
|
// stream it.
|
|
//
|
|
// Errors are intentionally non-fatal: on a fresh worker with no outbound
|
|
// connectivity (or a misconfigured gallery JSON), we want the worker to still
|
|
// register and serve traffic — the master will fall back to pushing files
|
|
// on-demand over the worker's file-transfer routes, which is the pre-existing
|
|
// behavior. Per-model
|
|
// failures are logged at warn level and the loop continues with the next ID.
|
|
//
|
|
// Idempotency comes for free from pkg/downloader.URI.DownloadFileWithContext:
|
|
// it stats the target path, hashes it if a SHA is configured, and short-circuits
|
|
// on a match. So restarts against a populated PVC are effectively no-ops.
|
|
func prefetchModels(
|
|
ctx context.Context,
|
|
cfg *Config,
|
|
systemState *system.SystemState,
|
|
ml *model.ModelLoader,
|
|
backendGalleries []config.Gallery,
|
|
installer modelInstaller,
|
|
) {
|
|
models := normalizePrefetchList(cfg.PrefetchModels)
|
|
if len(models) == 0 {
|
|
return
|
|
}
|
|
|
|
modelGalleries, err := parseModelGalleries(cfg.Galleries)
|
|
if err != nil {
|
|
// Without a model-gallery config we cannot resolve gallery IDs. Warn
|
|
// and let the worker proceed — the master can still push files later.
|
|
xlog.Warn("Skipping model prefetch: invalid LOCALAI_GALLERIES", "error", err)
|
|
return
|
|
}
|
|
if len(modelGalleries) == 0 {
|
|
xlog.Warn("Skipping model prefetch: no model galleries configured (set LOCALAI_GALLERIES)", "models", models)
|
|
return
|
|
}
|
|
|
|
if installer == nil {
|
|
installer = realModelInstaller
|
|
}
|
|
|
|
xlog.Info("Prefetching models from gallery before registering", "count", len(models), "models", models)
|
|
for _, name := range models {
|
|
xlog.Info("Prefetching model", "model", name)
|
|
if err := installer(ctx, modelGalleries, backendGalleries, systemState, ml, name); err != nil {
|
|
// Non-fatal: master can still push the file on demand. We log
|
|
// loudly so an operator can spot a misconfigured gallery ID or
|
|
// a missing outbound route without the worker crash-looping.
|
|
xlog.Warn("Model prefetch failed; master will push on demand", "model", name, "error", err)
|
|
continue
|
|
}
|
|
xlog.Info("Prefetched model", "model", name)
|
|
}
|
|
}
|
|
|
|
// normalizePrefetchList trims whitespace and drops empty entries. kong already
|
|
// splits comma-separated env values into []string, but callers using the CLI
|
|
// flag repeatedly (or pasting whitespace) can produce stragglers we don't want
|
|
// to ship into the gallery resolver as "" or " ".
|
|
func normalizePrefetchList(in []string) []string {
|
|
out := make([]string, 0, len(in))
|
|
for _, s := range in {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
continue
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// parseModelGalleries parses the JSON-encoded LOCALAI_GALLERIES value the same
|
|
// way the master does. Returns an empty slice (not nil) and nil error when the
|
|
// input is empty, so callers can treat "" as "not configured" without a
|
|
// secondary check.
|
|
func parseModelGalleries(raw string) ([]config.Gallery, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return []config.Gallery{}, nil
|
|
}
|
|
var galleries []config.Gallery
|
|
if err := json.Unmarshal([]byte(raw), &galleries); err != nil {
|
|
return nil, fmt.Errorf("parsing model galleries JSON: %w", err)
|
|
}
|
|
return galleries, nil
|
|
}
|