Skip to content

Commit e7bd8a0

Browse files
committed
refactoring and consolidation
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 8ef42c7 commit e7bd8a0

35 files changed

Lines changed: 368 additions & 426 deletions

core/application/agent_jobs.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ func (a *Application) RestartAgentJobService() error {
3030
)
3131

3232
// Re-apply distributed wiring if available (matches startup.go logic)
33-
if a.jobDispatcher != nil {
34-
agentJobService.SetDistributedBackends(a.jobDispatcher)
35-
}
36-
if a.jobStore != nil {
37-
agentJobService.SetDistributedJobStore(a.jobStore)
33+
if d := a.Distributed(); d != nil {
34+
if d.Dispatcher != nil {
35+
agentJobService.SetDistributedBackends(d.Dispatcher)
36+
}
37+
if d.JobStore != nil {
38+
agentJobService.SetDistributedJobStore(d.JobStore)
39+
}
3840
}
3941

4042
// Start the service

core/application/application.go

Lines changed: 27 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,16 @@ package application
22

33
import (
44
"context"
5+
"math/rand/v2"
56
"sync"
67
"sync/atomic"
78
"time"
89

910
"github.com/mudler/LocalAI/core/config"
1011
mcpTools "github.com/mudler/LocalAI/core/http/endpoints/mcp"
11-
"github.com/mudler/LocalAI/core/services/agents"
1212
"github.com/mudler/LocalAI/core/services/agentpool"
1313
"github.com/mudler/LocalAI/core/services/galleryop"
14-
"github.com/mudler/LocalAI/core/services/distributed"
15-
"github.com/mudler/LocalAI/core/services/jobs"
16-
"github.com/mudler/LocalAI/core/services/messaging"
1714
"github.com/mudler/LocalAI/core/services/nodes"
18-
"github.com/mudler/LocalAI/core/services/storage"
1915
"github.com/mudler/LocalAI/core/templates"
2016
"github.com/mudler/LocalAI/pkg/model"
2117
"github.com/mudler/xlog"
@@ -40,17 +36,7 @@ type Application struct {
4036
agentJobMutex sync.Mutex
4137

4238
// Distributed mode services (nil when not in distributed mode)
43-
natsClient *messaging.Client
44-
objectStore storage.ObjectStore
45-
nodeRegistry *nodes.NodeRegistry
46-
smartRouter *nodes.SmartRouter
47-
healthMon *nodes.HealthMonitor
48-
jobStore *jobs.JobStore
49-
jobDispatcher *jobs.Dispatcher
50-
agentStore *agents.AgentStore
51-
agentBridge *agents.EventBridge
52-
distStores *distributed.Stores
53-
fileManager *storage.FileManager
39+
distributed *DistributedServices
5440
}
5541

5642
func newApplication(appConfig *config.ApplicationConfig) *Application {
@@ -107,77 +93,27 @@ func (a *Application) StartupConfig() *config.ApplicationConfig {
10793
return a.startupConfig
10894
}
10995

110-
// NatsClient returns the NATS messaging client, or nil if not in distributed mode.
111-
func (a *Application) NatsClient() *messaging.Client {
112-
return a.natsClient
113-
}
114-
115-
// ObjectStore returns the object storage backend, or nil if not in distributed mode.
116-
func (a *Application) ObjectStore() storage.ObjectStore {
117-
return a.objectStore
118-
}
119-
120-
// NodeRegistry returns the node registry, or nil if not in distributed mode.
121-
func (a *Application) NodeRegistry() *nodes.NodeRegistry {
122-
return a.nodeRegistry
123-
}
124-
125-
// SmartRouter returns the smart router, or nil if not in distributed mode.
126-
func (a *Application) SmartRouter() *nodes.SmartRouter {
127-
return a.smartRouter
128-
}
129-
130-
// JobStore returns the distributed job store, or nil if not in distributed mode.
131-
func (a *Application) JobStore() *jobs.JobStore {
132-
return a.jobStore
133-
}
134-
135-
// JobDispatcher returns the distributed job dispatcher, or nil if not in distributed mode.
136-
func (a *Application) JobDispatcher() *jobs.Dispatcher {
137-
return a.jobDispatcher
138-
}
139-
140-
// AgentStore returns the distributed agent store, or nil if not in distributed mode.
141-
func (a *Application) AgentStore() *agents.AgentStore {
142-
return a.agentStore
143-
}
144-
145-
// AgentEventBridge returns the agent event bridge, or nil if not in distributed mode.
146-
func (a *Application) AgentEventBridge() *agents.EventBridge {
147-
return a.agentBridge
148-
}
149-
150-
// DistributedStores returns the Phase 4 distributed stores, or nil if not in distributed mode.
151-
func (a *Application) DistributedStores() *distributed.Stores {
152-
return a.distStores
153-
}
154-
155-
// FileManager returns the file manager for object storage, or nil if not in distributed mode.
156-
func (a *Application) FileManager() *storage.FileManager {
157-
return a.fileManager
158-
}
159-
160-
// HealthMonitor returns the health monitor, or nil if not in distributed mode.
161-
func (a *Application) HealthMonitor() *nodes.HealthMonitor {
162-
return a.healthMon
96+
// Distributed returns the distributed services, or nil if not in distributed mode.
97+
func (a *Application) Distributed() *DistributedServices {
98+
return a.distributed
16399
}
164100

165101
// IsDistributed returns true if the application is running in distributed mode.
166102
func (a *Application) IsDistributed() bool {
167-
return a.applicationConfig.Distributed.Enabled
103+
return a.distributed != nil
168104
}
169105

170106
// waitForHealthyWorker blocks until at least one healthy backend worker is registered.
171107
// This prevents the agent pool from failing during startup when workers haven't connected yet.
172108
func (a *Application) waitForHealthyWorker() {
173109
maxWait := a.applicationConfig.Distributed.WorkerWaitTimeoutOrDefault()
174-
const pollInterval = 2 * time.Second
110+
const basePoll = 2 * time.Second
175111

176112
xlog.Info("Waiting for at least one healthy backend worker before starting agent pool")
177113
deadline := time.Now().Add(maxWait)
178114

179115
for time.Now().Before(deadline) {
180-
registered, err := a.nodeRegistry.List()
116+
registered, err := a.distributed.Registry.List()
181117
if err == nil {
182118
for _, n := range registered {
183119
if n.NodeType == nodes.NodeTypeBackend && n.Status == nodes.StatusHealthy {
@@ -186,10 +122,12 @@ func (a *Application) waitForHealthyWorker() {
186122
}
187123
}
188124
}
125+
// Add 0-1s jitter to prevent thundering-herd on the node registry
126+
jitter := time.Duration(rand.Int64N(int64(time.Second)))
189127
select {
190128
case <-a.applicationConfig.Context.Done():
191129
return
192-
case <-time.After(pollInterval):
130+
case <-time.After(basePoll + jitter):
193131
}
194132
}
195133
xlog.Warn("No healthy backend worker found after waiting, proceeding anyway")
@@ -237,24 +175,23 @@ func (a *Application) StartAgentPool() {
237175
if a.authDB != nil {
238176
aps.SetAuthDB(a.authDB)
239177
}
240-
if a.distStores != nil && a.distStores.Skills != nil {
241-
aps.SetSkillStore(a.distStores.Skills)
242-
}
243178
// Wire distributed mode components
244-
if a.natsClient != nil {
245-
aps.SetNATSClient(a.natsClient)
246-
}
247-
if a.agentBridge != nil {
248-
aps.SetEventBridge(a.agentBridge)
249-
}
250-
if a.agentStore != nil {
251-
aps.SetAgentStore(a.agentStore)
252-
}
253-
// In distributed mode, wait for at least one healthy backend worker before
254-
// starting the agent pool. Collections initialization calls embeddings which
255-
// require a worker to be registered and subscribed to NATS.
256-
if a.IsDistributed() && a.nodeRegistry != nil {
257-
a.waitForHealthyWorker()
179+
if d := a.Distributed(); d != nil {
180+
if d.DistStores != nil && d.DistStores.Skills != nil {
181+
aps.SetSkillStore(d.DistStores.Skills)
182+
}
183+
aps.SetNATSClient(d.Nats)
184+
if d.AgentBridge != nil {
185+
aps.SetEventBridge(d.AgentBridge)
186+
}
187+
if d.AgentStore != nil {
188+
aps.SetAgentStore(d.AgentStore)
189+
}
190+
// Wait for at least one healthy backend worker before starting the agent pool.
191+
// Collections initialization calls embeddings which require a worker.
192+
if d.Registry != nil {
193+
a.waitForHealthyWorker()
194+
}
258195
}
259196

260197
if err := aps.Start(a.applicationConfig.Context); err != nil {

core/application/distributed.go

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,27 @@ import (
1717
"gorm.io/gorm"
1818
)
1919

20-
// distributedServices holds all services initialized for distributed mode.
21-
type distributedServices struct {
22-
nats *messaging.Client
23-
store storage.ObjectStore
24-
registry *nodes.NodeRegistry
25-
router *nodes.SmartRouter
26-
health *nodes.HealthMonitor
27-
jobStore *jobs.JobStore
28-
dispatcher *jobs.Dispatcher
29-
agentStore *agents.AgentStore
30-
agentBridge *agents.EventBridge
31-
distStores *distributed.Stores
32-
fileMgr *storage.FileManager
33-
fileStager nodes.FileStager
34-
modelAdapter *nodes.ModelRouterAdapter
20+
// DistributedServices holds all services initialized for distributed mode.
21+
type DistributedServices struct {
22+
Nats *messaging.Client
23+
Store storage.ObjectStore
24+
Registry *nodes.NodeRegistry
25+
Router *nodes.SmartRouter
26+
Health *nodes.HealthMonitor
27+
JobStore *jobs.JobStore
28+
Dispatcher *jobs.Dispatcher
29+
AgentStore *agents.AgentStore
30+
AgentBridge *agents.EventBridge
31+
DistStores *distributed.Stores
32+
FileMgr *storage.FileManager
33+
FileStager nodes.FileStager
34+
ModelAdapter *nodes.ModelRouterAdapter
3535
}
3636

3737
// initDistributed validates distributed mode prerequisites and initializes
3838
// NATS, object storage, node registry, and instance identity.
3939
// Returns nil if distributed mode is not enabled.
40-
func initDistributed(cfg *config.ApplicationConfig, authDB *gorm.DB) (*distributedServices, error) {
40+
func initDistributed(cfg *config.ApplicationConfig, authDB *gorm.DB) (*DistributedServices, error) {
4141
if !cfg.Distributed.Enabled {
4242
return nil, nil
4343
}
@@ -196,20 +196,20 @@ func initDistributed(cfg *config.ApplicationConfig, authDB *gorm.DB) (*distribut
196196
modelAdapter := nodes.NewModelRouterAdapter(router)
197197

198198
success = true
199-
return &distributedServices{
200-
nats: natsClient,
201-
store: store,
202-
registry: registry,
203-
router: router,
204-
health: healthMon,
205-
jobStore: jobStore,
206-
dispatcher: dispatcher,
207-
agentStore: agentStore,
208-
agentBridge: agentBridge,
209-
distStores: distStores,
210-
fileMgr: fileMgr,
211-
fileStager: fileStager,
212-
modelAdapter: modelAdapter,
199+
return &DistributedServices{
200+
Nats: natsClient,
201+
Store: store,
202+
Registry: registry,
203+
Router: router,
204+
Health: healthMon,
205+
JobStore: jobStore,
206+
Dispatcher: dispatcher,
207+
AgentStore: agentStore,
208+
AgentBridge: agentBridge,
209+
DistStores: distStores,
210+
FileMgr: fileMgr,
211+
FileStager: fileStager,
212+
ModelAdapter: modelAdapter,
213213
}, nil
214214
}
215215

core/application/startup.go

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -143,70 +143,60 @@ func New(opts ...config.AppOption) (*Application, error) {
143143
return nil, fmt.Errorf("distributed mode initialization failed: %w", err)
144144
}
145145
if distSvc != nil {
146-
application.natsClient = distSvc.nats
147-
application.objectStore = distSvc.store
148-
application.nodeRegistry = distSvc.registry
149-
application.smartRouter = distSvc.router
150-
application.healthMon = distSvc.health
151-
application.jobStore = distSvc.jobStore
152-
application.jobDispatcher = distSvc.dispatcher
153-
application.agentStore = distSvc.agentStore
154-
application.agentBridge = distSvc.agentBridge
155-
application.distStores = distSvc.distStores
156-
application.fileManager = distSvc.fileMgr
146+
application.distributed = distSvc
157147
// Wire remote model unloader so ShutdownModel works for remote nodes
158148
// Uses NATS to tell serve-backend nodes to Free + kill their backend process
159-
remoteUnloader := nodes.NewRemoteUnloaderAdapter(distSvc.registry, distSvc.nats)
149+
remoteUnloader := nodes.NewRemoteUnloaderAdapter(distSvc.Registry, distSvc.Nats)
160150
application.modelLoader.SetRemoteUnloader(remoteUnloader)
161-
distSvc.router.SetUnloader(remoteUnloader)
151+
distSvc.Router.SetUnloader(remoteUnloader)
162152
// Wire ModelRouter so grpcModel() delegates to SmartRouter in distributed mode
163-
application.modelLoader.SetModelRouter(distSvc.modelAdapter.AsModelRouter())
153+
application.modelLoader.SetModelRouter(distSvc.ModelAdapter.AsModelRouter())
164154
// Wire DistributedModelStore so shutdown/list/watchdog can find remote models
165155
distStore := nodes.NewDistributedModelStore(
166156
model.NewInMemoryModelStore(),
167-
distSvc.registry,
157+
distSvc.Registry,
168158
)
169159
application.modelLoader.SetModelStore(distStore)
170160
// Start health monitor
171-
distSvc.health.Start(options.Context)
161+
distSvc.Health.Start(options.Context)
172162
// In distributed mode, MCP CI jobs are executed by agent workers (not the frontend)
173163
// because the frontend can't create MCP sessions (e.g., stdio servers using docker).
174164
// The dispatcher still subscribes to jobs.new for persistence (result/progress subs)
175165
// but does NOT set a workerFn — agent workers consume jobs from the same NATS queue.
176166

177167
// Wire model config loader so job events include model config for agent workers
178-
distSvc.dispatcher.SetModelConfigLoader(application.backendLoader)
168+
distSvc.Dispatcher.SetModelConfigLoader(application.backendLoader)
179169

180170
// Start job dispatcher — abort startup if it fails, as jobs would be accepted but never dispatched
181-
if err := distSvc.dispatcher.Start(options.Context); err != nil {
171+
if err := distSvc.Dispatcher.Start(options.Context); err != nil {
182172
return nil, fmt.Errorf("starting job dispatcher: %w", err)
183173
}
184174
// Start ephemeral file cleanup
185-
storage.StartEphemeralCleanup(options.Context, distSvc.fileMgr, 0, 0)
175+
storage.StartEphemeralCleanup(options.Context, distSvc.FileMgr, 0, 0)
186176
// Wire distributed backends into AgentJobService (before Start)
187177
if application.agentJobService != nil {
188-
application.agentJobService.SetDistributedBackends(distSvc.dispatcher)
189-
application.agentJobService.SetDistributedJobStore(distSvc.jobStore)
178+
application.agentJobService.SetDistributedBackends(distSvc.Dispatcher)
179+
application.agentJobService.SetDistributedJobStore(distSvc.JobStore)
190180
}
191181
// Wire skill store into AgentPoolService (wired at pool start time via closure)
192182
// The actual wiring happens in StartAgentPool since the pool doesn't exist yet.
193183

194184
// Wire NATS and gallery store into GalleryService for cross-instance progress/cancel
195185
if application.galleryService != nil {
196-
application.galleryService.SetNATSClient(distSvc.nats)
197-
if distSvc.distStores != nil && distSvc.distStores.Gallery != nil {
186+
application.galleryService.SetNATSClient(distSvc.Nats)
187+
if distSvc.DistStores != nil && distSvc.DistStores.Gallery != nil {
198188
// Clean up stale in-progress operations from previous crashed instances
199-
if err := distSvc.distStores.Gallery.CleanStale(30 * time.Minute); err != nil {
189+
if err := distSvc.DistStores.Gallery.CleanStale(30 * time.Minute); err != nil {
200190
xlog.Warn("Failed to clean stale gallery operations", "error", err)
201191
}
202-
application.galleryService.SetGalleryStore(distSvc.distStores.Gallery)
192+
application.galleryService.SetGalleryStore(distSvc.DistStores.Gallery)
203193
}
204194
// Wire distributed model/backend managers so delete propagates to workers
205195
application.galleryService.SetModelManager(
206196
nodes.NewDistributedModelManager(options, application.modelLoader, remoteUnloader),
207197
)
208198
application.galleryService.SetBackendManager(
209-
nodes.NewDistributedBackendManager(options, application.modelLoader, remoteUnloader, distSvc.registry),
199+
nodes.NewDistributedBackendManager(options, application.modelLoader, remoteUnloader, distSvc.Registry),
210200
)
211201
}
212202
}

0 commit comments

Comments
 (0)