From 3a1507463fe09b2330f05d3c4345a25d975f2c35 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 2 Jul 2026 11:11:40 +0000 Subject: [PATCH] fix(backends): don't force-reinstall LOCALAI_EXTERNAL_BACKENDS on boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The startup loop for LOCALAI_EXTERNAL_BACKENDS runs InstallExternalBackend for each listed backend on every boot, and its gallery-name path hardcoded force=true — so every start re-downloaded and re-extracted each listed backend's OCI image even when it was installed and runnable. Supervising apps that list several backends paid several full OCI pulls per launch. Give InstallExternalBackend an explicit force parameter (it only affects the gallery-name fallback; URI installs always write) and pass: - false from the boot loop and `local-ai backends install` (idempotent ensure — `backends upgrade` is the refresh path), - op.Force from the local manager's external-URI op, - the request's force on the worker install path and true on its upgrade path (behavior unchanged). Assisted-by: Claude:claude-fable-5 [Claude Code] Signed-off-by: Ettore Di Giacinto --- core/application/startup.go | 2 +- core/cli/backends.go | 2 +- core/services/galleryop/backend_force_test.go | 21 +++++++++++++++++-- core/services/galleryop/backends.go | 9 ++++++-- core/services/galleryop/backends_test.go | 6 ++++++ core/services/galleryop/managers_local.go | 2 +- core/services/worker/install.go | 4 ++-- 7 files changed, 37 insertions(+), 9 deletions(-) diff --git a/core/application/startup.go b/core/application/startup.go index 25d965834..cf341dfa6 100644 --- a/core/application/startup.go +++ b/core/application/startup.go @@ -369,7 +369,7 @@ func New(opts ...config.AppOption) (*Application, error) { } for _, backend := range options.ExternalBackends { - if err := galleryop.InstallExternalBackend(options.Context, options.BackendGalleries, options.SystemState, application.ModelLoader(), nil, backend, "", "", options.RequireBackendIntegrity); err != nil { + if err := galleryop.InstallExternalBackend(options.Context, options.BackendGalleries, options.SystemState, application.ModelLoader(), nil, backend, "", "", false, options.RequireBackendIntegrity); err != nil { xlog.Error("error installing external backend", "error", err) } } diff --git a/core/cli/backends.go b/core/cli/backends.go index 88ecc2321..91449032d 100644 --- a/core/cli/backends.go +++ b/core/cli/backends.go @@ -127,7 +127,7 @@ func (bi *BackendsInstall) Run(ctx *cliContext.Context) error { } modelLoader := model.NewModelLoader(systemState) - err = galleryop.InstallExternalBackend(context.Background(), galleries, systemState, modelLoader, progressCallback, bi.BackendArgs, bi.Name, bi.Alias, bi.RequireBackendIntegrity) + err = galleryop.InstallExternalBackend(context.Background(), galleries, systemState, modelLoader, progressCallback, bi.BackendArgs, bi.Name, bi.Alias, false, bi.RequireBackendIntegrity) if err != nil { return err } diff --git a/core/services/galleryop/backend_force_test.go b/core/services/galleryop/backend_force_test.go index 35ee24b4b..d53999f3f 100644 --- a/core/services/galleryop/backend_force_test.go +++ b/core/services/galleryop/backend_force_test.go @@ -25,6 +25,8 @@ var _ = Describe("LocalBackendManager force semantics", func() { backendsDir string srcDir string mgr *galleryop.LocalBackendManager + systemState *system.SystemState + ml *model.ModelLoader ) const installedRunSh = "#!/bin/sh\necho installed\n" @@ -56,13 +58,14 @@ var _ = Describe("LocalBackendManager force semantics", func() { Expect(os.MkdirAll(filepath.Join(backendsDir, "test-backend"), 0o755)).To(Succeed()) Expect(os.WriteFile(installedRunShPath(), []byte(installedRunSh), 0o755)).To(Succeed()) - systemState, err := system.GetSystemState(system.WithBackendPath(backendsDir)) + systemState, err = system.GetSystemState(system.WithBackendPath(backendsDir)) Expect(err).NotTo(HaveOccurred()) appConfig := &config.ApplicationConfig{ SystemState: systemState, BackendGalleries: []config.Gallery{{Name: "test", URL: "file://" + galleryYAML}}, } - mgr = galleryop.NewLocalBackendManager(appConfig, model.NewModelLoader(systemState)) + ml = model.NewModelLoader(systemState) + mgr = galleryop.NewLocalBackendManager(appConfig, ml) }) AfterEach(func() { @@ -94,4 +97,18 @@ var _ = Describe("LocalBackendManager force semantics", func() { Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(galleryRunSh), "install with Force must overwrite the installed backend") }) + + // The LOCALAI_EXTERNAL_BACKENDS boot loop goes through + // InstallExternalBackend's gallery-name path on EVERY startup; it must not + // force, or each boot re-downloads every listed backend. + It("skips an already-installed backend on the non-forced external gallery-name path", func() { + err := galleryop.InstallExternalBackend(context.Background(), + []config.Gallery{{Name: "test", URL: "file://" + filepath.Join(backendsDir, "gallery.yaml")}}, + systemState, ml, nil, "test-backend", "", "", false, false) + Expect(err).NotTo(HaveOccurred()) + + content, err := os.ReadFile(installedRunShPath()) + Expect(err).NotTo(HaveOccurred()) + Expect(string(content)).To(Equal(installedRunSh), "non-forced external install must not overwrite an installed backend") + }) }) diff --git a/core/services/galleryop/backends.go b/core/services/galleryop/backends.go index 4576cf9cb..0ad1e64c9 100644 --- a/core/services/galleryop/backends.go +++ b/core/services/galleryop/backends.go @@ -144,7 +144,12 @@ func (g *GalleryService) backendHandler(op *ManagementOp[gallery.GalleryBackend, // InstallExternalBackend installs a backend from an external source (OCI image, URL, or path). // This method contains the logic to detect the input type and call the appropriate installation function. // It can be used by both CLI and Web UI for installing backends from external sources. -func InstallExternalBackend(ctx context.Context, galleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, downloadStatus func(string, string, string, float64), backend, name, alias string, requireIntegrity bool) error { +// +// force applies only to the gallery-name fallback: a URI install (dir/OCI/file) +// always writes, but a bare gallery name is an "ensure installed" — the +// LOCALAI_EXTERNAL_BACKENDS boot loop runs it on every start and must not +// re-download an installed, runnable backend. +func InstallExternalBackend(ctx context.Context, galleries []config.Gallery, systemState *system.SystemState, modelLoader *model.ModelLoader, downloadStatus func(string, string, string, float64), backend, name, alias string, force, requireIntegrity bool) error { uri := downloader.URI(backend) switch { case uri.LooksLikeDir(): @@ -202,7 +207,7 @@ func InstallExternalBackend(ctx context.Context, galleries []config.Gallery, sys if name != "" || alias != "" { return fmt.Errorf("specifying a name or alias is not supported for gallery backends") } - err := gallery.InstallBackendFromGallery(ctx, galleries, systemState, modelLoader, backend, downloadStatus, true, requireIntegrity) + err := gallery.InstallBackendFromGallery(ctx, galleries, systemState, modelLoader, backend, downloadStatus, force, requireIntegrity) if err != nil { return fmt.Errorf("error installing backend %s: %w", backend, err) } diff --git a/core/services/galleryop/backends_test.go b/core/services/galleryop/backends_test.go index c9e0fbc1c..a7a7b70a3 100644 --- a/core/services/galleryop/backends_test.go +++ b/core/services/galleryop/backends_test.go @@ -70,6 +70,7 @@ var _ = Describe("InstallExternalBackend", func() { "test-backend", // gallery name "custom-name", // name should not be allowed "", + false, // force false, ) Expect(err).To(HaveOccurred()) @@ -86,6 +87,7 @@ var _ = Describe("InstallExternalBackend", func() { "non-existent-backend", "", "", + false, // force false, ) Expect(err).To(HaveOccurred()) @@ -103,6 +105,7 @@ var _ = Describe("InstallExternalBackend", func() { "oci://quay.io/mudler/tests:localai-backend-test", "", // name is required for OCI images "", + false, // force false, ) Expect(err).To(HaveOccurred()) @@ -136,6 +139,7 @@ var _ = Describe("InstallExternalBackend", func() { testBackendPath, "", // name should be inferred as "source-backend" "", + false, // force false, ) // The function should at least attempt to install with the inferred name @@ -155,6 +159,7 @@ var _ = Describe("InstallExternalBackend", func() { testBackendPath, "custom-backend-name", "", + false, // force false, ) // The function should use the provided name @@ -173,6 +178,7 @@ var _ = Describe("InstallExternalBackend", func() { testBackendPath, "custom-backend-name", "custom-alias", + false, // force false, ) // The function should accept alias for directory paths diff --git a/core/services/galleryop/managers_local.go b/core/services/galleryop/managers_local.go index f95d2c482..338a097b4 100644 --- a/core/services/galleryop/managers_local.go +++ b/core/services/galleryop/managers_local.go @@ -110,7 +110,7 @@ func (b *LocalBackendManager) CheckUpgrades(ctx context.Context) (map[string]gal func (b *LocalBackendManager) InstallBackend(ctx context.Context, op *ManagementOp[gallery.GalleryBackend, any], progressCb ProgressCallback) error { if op.ExternalURI != "" { return InstallExternalBackend(ctx, b.backendGalleries, b.systemState, b.modelLoader, - progressCb, op.ExternalURI, op.ExternalName, op.ExternalAlias, b.requireBackendIntegrity) + progressCb, op.ExternalURI, op.ExternalName, op.ExternalAlias, op.Force, b.requireBackendIntegrity) } // op.Force distinguishes an explicit reinstall from an idempotent // "make sure it's installed" op; the latter must not re-download an diff --git a/core/services/worker/install.go b/core/services/worker/install.go index f7b65acf8..bcc7cc9b0 100644 --- a/core/services/worker/install.go +++ b/core/services/worker/install.go @@ -134,7 +134,7 @@ func (s *backendSupervisor) installBackend(req messaging.BackendInstallRequest, if req.URI != "" { xlog.Info("Installing backend from external URI", "backend", req.Backend, "uri", req.URI, "force", force) if err := galleryop.InstallExternalBackend( - context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, s.cfg.RequireBackendIntegrity, + context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, force, s.cfg.RequireBackendIntegrity, ); err != nil { return "", fmt.Errorf("installing backend from gallery: %w", err) } @@ -201,7 +201,7 @@ func (s *backendSupervisor) upgradeBackend(req messaging.BackendUpgradeRequest) if req.URI != "" { xlog.Info("Upgrading backend from external URI", "backend", req.Backend, "uri", req.URI) if err := galleryop.InstallExternalBackend( - context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, s.cfg.RequireBackendIntegrity, + context.Background(), galleries, s.systemState, s.ml, downloadCb, req.URI, req.Name, req.Alias, true, s.cfg.RequireBackendIntegrity, ); err != nil { return fmt.Errorf("upgrading backend from external URI: %w", err) }