mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-20 13:17:56 -04:00
DiscoverAdvertisedAddr promised to return an error rather than a fallback no peer can dial, but only rejected an unspecified address. With PostgreSQL on the same host or pod as a replica, which is compose, single-node and any sidecar layout, the route to it is loopback, so every replica advertised 127.0.0.1 and a peer dialling that reached itself. Loopback, link-local and zoned source addresses are now rejected with an error naming the remedy, and a port outside 1-65535 is rejected before it becomes an undialable address. Liveness was also measured on each replica's own clock: Register and Heartbeat stamped last_seen from the Go process, and Live compared those rows against the reading replica's time.Now(). Skew therefore shrank or stretched the window by writerBehind+readerAhead, evicting healthy peers or keeping dead ones. Both sides now use the database clock, which is the one clock every replica demonstrably shares. Assisted-by: Claude Opus 5 [claude-code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
116 lines
3.9 KiB
Go
116 lines
3.9 KiB
Go
package cluster_test
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/core/services/cluster"
|
|
"github.com/mudler/LocalAI/core/services/testutil"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var _ = Describe("Instance registry", func() {
|
|
var (
|
|
db *gorm.DB
|
|
reg *cluster.Registry
|
|
ctx context.Context
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
db = testutil.SetupTestDB()
|
|
Expect(db.AutoMigrate(&cluster.Instance{})).To(Succeed())
|
|
reg = cluster.NewRegistry(db)
|
|
ctx = context.Background()
|
|
})
|
|
|
|
It("registers an instance and reads it back", func() {
|
|
Expect(reg.Register(ctx, "inst-a", "10.0.0.1:8080", "v1")).To(Succeed())
|
|
|
|
got, err := reg.Get(ctx, "inst-a")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(got.AdvertisedAddr).To(Equal("10.0.0.1:8080"))
|
|
Expect(got.Version).To(Equal("v1"))
|
|
})
|
|
|
|
It("re-registering the same id updates the address instead of duplicating", func() {
|
|
Expect(reg.Register(ctx, "inst-a", "10.0.0.1:8080", "v1")).To(Succeed())
|
|
Expect(reg.Register(ctx, "inst-a", "10.0.0.9:9090", "v2")).To(Succeed())
|
|
|
|
live, err := reg.Live(ctx, time.Hour)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(live).To(HaveLen(1))
|
|
Expect(live[0].AdvertisedAddr).To(Equal("10.0.0.9:9090"))
|
|
})
|
|
|
|
It("reports a missing instance distinguishably", func() {
|
|
_, err := reg.Get(ctx, "nope")
|
|
Expect(err).To(MatchError(cluster.ErrInstanceNotFound))
|
|
})
|
|
|
|
It("excludes instances whose heartbeat has aged out", func() {
|
|
Expect(reg.Register(ctx, "stale", "10.0.0.1:8080", "v1")).To(Succeed())
|
|
// Age the row directly; sleeping in a spec is forbidden.
|
|
Expect(db.Model(&cluster.Instance{}).Where("id = ?", "stale").
|
|
Update("last_seen", time.Now().Add(-10*time.Minute)).Error).To(Succeed())
|
|
|
|
live, err := reg.Live(ctx, time.Minute)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(live).To(BeEmpty())
|
|
})
|
|
|
|
It("brings a stale instance back with a heartbeat", func() {
|
|
Expect(reg.Register(ctx, "revive", "10.0.0.1:8080", "v1")).To(Succeed())
|
|
Expect(db.Model(&cluster.Instance{}).Where("id = ?", "revive").
|
|
Update("last_seen", time.Now().Add(-10*time.Minute)).Error).To(Succeed())
|
|
Expect(reg.Heartbeat(ctx, "revive")).To(Succeed())
|
|
|
|
live, err := reg.Live(ctx, time.Minute)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(live).To(HaveLen(1))
|
|
})
|
|
|
|
It("heartbeating an unknown instance is an error, not a silent insert", func() {
|
|
Expect(reg.Heartbeat(ctx, "ghost")).To(MatchError(cluster.ErrInstanceNotFound))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("Advertised address discovery", func() {
|
|
// The address itself depends on host networking and is deliberately not
|
|
// asserted. What is portable is the shape: whatever interface routes to the
|
|
// database, the port must be the one the caller asked for, not the
|
|
// database's.
|
|
It("combines a local interface with the caller's port", func() {
|
|
addr, err := cluster.DiscoverAdvertisedAddr("postgres://198.51.100.1:5432/testdb", 8080)
|
|
if err != nil {
|
|
Skip("no route to a database host on this machine: " + err.Error())
|
|
}
|
|
host, port, splitErr := net.SplitHostPort(addr)
|
|
Expect(splitErr).ToNot(HaveOccurred())
|
|
Expect(port).To(Equal("8080"))
|
|
Expect(net.ParseIP(host)).ToNot(BeNil())
|
|
})
|
|
|
|
It("refuses a DSN it cannot derive an address from", func() {
|
|
_, err := cluster.DiscoverAdvertisedAddr("", 8080)
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
|
|
// A database on this same host routes over loopback on every platform, so
|
|
// this is deterministic rather than host-dependent. Returning 127.0.0.1
|
|
// would make a peer dialling this replica reach itself.
|
|
It("refuses a loopback route instead of advertising an address peers cannot use", func() {
|
|
addr, err := cluster.DiscoverAdvertisedAddr("postgres://user@127.0.0.1:5432/testdb", 8080)
|
|
Expect(addr).To(BeEmpty())
|
|
Expect(err).To(MatchError(ContainSubstring("loopback")))
|
|
})
|
|
|
|
It("refuses a port that cannot be dialled", func() {
|
|
_, err := cluster.DiscoverAdvertisedAddr("postgres://198.51.100.1:5432/testdb", 0)
|
|
Expect(err).To(MatchError(ContainSubstring("out of range")))
|
|
})
|
|
})
|