Skip to content

Commit ca24956

Browse files
committed
fix: use advisory lock for upgrade checker in distributed mode
In distributed mode with multiple frontend instances, use PostgreSQL advisory lock (KeyBackendUpgradeCheck) so only one instance runs periodic upgrade checks and auto-upgrades. Prevents duplicate upgrade operations across replicas. Standalone mode is unchanged (simple ticker loop).
1 parent dd11121 commit ca24956

4 files changed

Lines changed: 66 additions & 19 deletions

File tree

core/application/application.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ func (a *Application) UpgradeChecker() *UpgradeChecker {
8686
return a.upgradeChecker
8787
}
8888

89+
// distributedDB returns the PostgreSQL database for distributed coordination,
90+
// or nil in standalone mode.
91+
func (a *Application) distributedDB() *gorm.DB {
92+
if a.distributed != nil {
93+
return a.authDB
94+
}
95+
return nil
96+
}
97+
8998
func (a *Application) AgentPoolService() *agentpool.AgentPoolService {
9099
return a.agentPoolService.Load()
91100
}

core/application/startup.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,11 @@ func New(opts ...config.AppOption) (*Application, error) {
231231
xlog.Error("error registering external backends", "error", err)
232232
}
233233

234-
// Start background upgrade checker for backends
234+
// Start background upgrade checker for backends.
235+
// In distributed mode, uses PostgreSQL advisory lock so only one frontend
236+
// instance runs periodic checks (avoids duplicate upgrades across replicas).
235237
if len(options.BackendGalleries) > 0 {
236-
uc := NewUpgradeChecker(options, application.ModelLoader())
238+
uc := NewUpgradeChecker(options, application.ModelLoader(), application.distributedDB())
237239
application.upgradeChecker = uc
238240
go uc.Run(options.Context)
239241
}

core/application/upgrade_checker.go

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,25 @@ import (
77

88
"github.com/mudler/LocalAI/core/config"
99
"github.com/mudler/LocalAI/core/gallery"
10+
"github.com/mudler/LocalAI/core/services/advisorylock"
1011
"github.com/mudler/LocalAI/pkg/model"
1112
"github.com/mudler/LocalAI/pkg/system"
1213
"github.com/mudler/xlog"
14+
"gorm.io/gorm"
1315
)
1416

1517
// UpgradeChecker periodically checks for backend upgrades and optionally
1618
// auto-upgrades them. It caches the last check results for API queries.
19+
//
20+
// In standalone mode it runs a simple ticker loop.
21+
// In distributed mode it uses a PostgreSQL advisory lock so that only one
22+
// frontend instance performs periodic checks and auto-upgrades at a time.
1723
type UpgradeChecker struct {
1824
appConfig *config.ApplicationConfig
1925
modelLoader *model.ModelLoader
2026
galleries []config.Gallery
2127
systemState *system.SystemState
28+
db *gorm.DB // non-nil in distributed mode
2229

2330
checkInterval time.Duration
2431
stop chan struct{}
@@ -31,12 +38,15 @@ type UpgradeChecker struct {
3138
}
3239

3340
// NewUpgradeChecker creates a new UpgradeChecker service.
34-
func NewUpgradeChecker(appConfig *config.ApplicationConfig, ml *model.ModelLoader) *UpgradeChecker {
41+
// Pass db=nil for standalone mode, or a *gorm.DB for distributed mode
42+
// (uses advisory locks so only one instance runs periodic checks).
43+
func NewUpgradeChecker(appConfig *config.ApplicationConfig, ml *model.ModelLoader, db *gorm.DB) *UpgradeChecker {
3544
return &UpgradeChecker{
3645
appConfig: appConfig,
3746
modelLoader: ml,
3847
galleries: appConfig.BackendGalleries,
3948
systemState: appConfig.SystemState,
49+
db: db,
4050
checkInterval: 6 * time.Hour,
4151
stop: make(chan struct{}),
4252
done: make(chan struct{}),
@@ -47,6 +57,10 @@ func NewUpgradeChecker(appConfig *config.ApplicationConfig, ml *model.ModelLoade
4757

4858
// Run starts the upgrade checker loop. It waits 30 seconds after startup,
4959
// performs an initial check, then re-checks every 6 hours.
60+
//
61+
// In distributed mode, periodic checks are guarded by a PostgreSQL advisory
62+
// lock so only one frontend instance runs them. On-demand triggers (TriggerCheck)
63+
// and the initial check always run locally for fast API response cache warming.
5064
func (uc *UpgradeChecker) Run(ctx context.Context) {
5165
defer close(uc.done)
5266

@@ -59,23 +73,44 @@ func (uc *UpgradeChecker) Run(ctx context.Context) {
5973
case <-time.After(30 * time.Second):
6074
}
6175

62-
// First check
76+
// First check always runs locally (to warm the cache on this instance)
6377
uc.runCheck(ctx)
6478

65-
// Periodic loop
66-
ticker := time.NewTicker(uc.checkInterval)
67-
defer ticker.Stop()
68-
69-
for {
70-
select {
71-
case <-ctx.Done():
72-
return
73-
case <-uc.stop:
74-
return
75-
case <-ticker.C:
76-
uc.runCheck(ctx)
77-
case <-uc.triggerCh:
79+
if uc.db != nil {
80+
// Distributed mode: use advisory lock for periodic checks.
81+
// RunLeaderLoop ticks every checkInterval; only the lock holder executes.
82+
go advisorylock.RunLeaderLoop(ctx, uc.db, advisorylock.KeyBackendUpgradeCheck, uc.checkInterval, func() {
7883
uc.runCheck(ctx)
84+
})
85+
86+
// Still listen for on-demand triggers (from API / settings change)
87+
// and stop signal — these run on every instance.
88+
for {
89+
select {
90+
case <-ctx.Done():
91+
return
92+
case <-uc.stop:
93+
return
94+
case <-uc.triggerCh:
95+
uc.runCheck(ctx)
96+
}
97+
}
98+
} else {
99+
// Standalone mode: simple ticker loop
100+
ticker := time.NewTicker(uc.checkInterval)
101+
defer ticker.Stop()
102+
103+
for {
104+
select {
105+
case <-ctx.Done():
106+
return
107+
case <-uc.stop:
108+
return
109+
case <-ticker.C:
110+
uc.runCheck(ctx)
111+
case <-uc.triggerCh:
112+
uc.runCheck(ctx)
113+
}
79114
}
80115
}
81116
}
@@ -86,7 +121,7 @@ func (uc *UpgradeChecker) Shutdown() {
86121
<-uc.done
87122
}
88123

89-
// TriggerCheck forces an immediate upgrade check.
124+
// TriggerCheck forces an immediate upgrade check on this instance.
90125
func (uc *UpgradeChecker) TriggerCheck() {
91126
select {
92127
case uc.triggerCh <- struct{}{}:

core/services/advisorylock/keys.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ const (
99
KeyGalleryDedup int64 = 102
1010
KeyAgentScheduler int64 = 103
1111
KeyHealthCheck int64 = 104
12-
KeySchemaMigrate int64 = 105
12+
KeySchemaMigrate int64 = 105
13+
KeyBackendUpgradeCheck int64 = 106
1314
)

0 commit comments

Comments
 (0)