Files
wehub-resource-sync 1b8708893a
Security Scan / tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:12:26 +08:00

489 lines
15 KiB
Go

package galleryop_test
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/mudler/LocalAI/core/config"
"github.com/mudler/LocalAI/core/services/distributed"
"github.com/mudler/LocalAI/core/services/galleryop"
"github.com/mudler/LocalAI/core/services/messaging"
"github.com/mudler/LocalAI/core/services/testutil"
)
// fakeBus is an in-memory MessagingClient that delivers each published
// message synchronously to every registered subscriber whose subject filter
// matches, including NATS-style wildcard subjects (`*` matches one token).
//
// Synchronous delivery keeps the specs deterministic: the moment Publish
// returns, every subscriber's handler has run, so the spec body can read
// the resulting state without polling.
type fakeBus struct {
mu sync.Mutex
subs []fakeBusSub
}
type fakeBusSub struct {
subject string
handler func([]byte)
}
func newFakeBus() *fakeBus { return &fakeBus{} }
func subjectMatches(filter, subject string) bool {
if filter == subject {
return true
}
fp := strings.Split(filter, ".")
sp := strings.Split(subject, ".")
if len(fp) != len(sp) {
return false
}
for i := range fp {
if fp[i] == "*" {
continue
}
if fp[i] != sp[i] {
return false
}
}
return true
}
func (b *fakeBus) Publish(subject string, data any) error {
payload, err := json.Marshal(data)
if err != nil {
return err
}
b.mu.Lock()
subs := append([]fakeBusSub(nil), b.subs...)
b.mu.Unlock()
for _, s := range subs {
if subjectMatches(s.subject, subject) {
s.handler(payload)
}
}
return nil
}
type fakeBusSubscription struct {
bus *fakeBus
subRef fakeBusSub
}
func (s *fakeBusSubscription) Unsubscribe() error {
s.bus.mu.Lock()
defer s.bus.mu.Unlock()
for i, candidate := range s.bus.subs {
if candidate.subject == s.subRef.subject {
s.bus.subs = append(s.bus.subs[:i], s.bus.subs[i+1:]...)
return nil
}
}
return nil
}
func (b *fakeBus) Subscribe(subject string, handler func([]byte)) (messaging.Subscription, error) {
sub := fakeBusSub{subject: subject, handler: handler}
b.mu.Lock()
b.subs = append(b.subs, sub)
b.mu.Unlock()
return &fakeBusSubscription{bus: b, subRef: sub}, nil
}
func (b *fakeBus) QueueSubscribe(subject, _ string, handler func([]byte)) (messaging.Subscription, error) {
return b.Subscribe(subject, handler)
}
func (b *fakeBus) QueueSubscribeReply(string, string, func([]byte, func([]byte))) (messaging.Subscription, error) {
return &fakeBusSubscription{bus: b}, nil
}
func (b *fakeBus) SubscribeReply(string, func([]byte, func([]byte))) (messaging.Subscription, error) {
return &fakeBusSubscription{bus: b}, nil
}
func (b *fakeBus) Request(string, []byte, time.Duration) ([]byte, error) {
return nil, nil
}
func (b *fakeBus) IsConnected() bool { return true }
func (b *fakeBus) Close() {}
var _ = Describe("OpStatus JSON wire format", func() {
It("round-trips a non-nil Error through Marshal/Unmarshal as a string", func() {
original := &galleryop.OpStatus{
Progress: 42.0,
Message: "downloading",
GalleryElementName: "vllm",
Error: errors.New("disk full"),
Processed: true,
}
raw, err := json.Marshal(original)
Expect(err).ToNot(HaveOccurred())
var got galleryop.OpStatus
Expect(json.Unmarshal(raw, &got)).To(Succeed())
Expect(got.Error).ToNot(BeNil(), "the error must survive the round-trip — peer replicas need to surface the failure")
Expect(got.Error.Error()).To(Equal("disk full"))
Expect(got.Progress).To(Equal(42.0))
Expect(got.GalleryElementName).To(Equal("vllm"))
Expect(got.Processed).To(BeTrue())
})
It("emits no error field when Error is nil", func() {
original := &galleryop.OpStatus{Progress: 10.0}
raw, err := json.Marshal(original)
Expect(err).ToNot(HaveOccurred())
Expect(string(raw)).ToNot(ContainSubstring(`"error":`),
"omitempty must keep nil errors out of the wire payload")
var got galleryop.OpStatus
Expect(json.Unmarshal(raw, &got)).To(Succeed())
Expect(got.Error).To(BeNil())
})
})
var _ = Describe("OpCache distributed sync", func() {
var (
svcA *galleryop.GalleryService
svcB *galleryop.GalleryService
opA *galleryop.OpCache
opB *galleryop.OpCache
bus *fakeBus
)
BeforeEach(func() {
bus = newFakeBus()
svcA = galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
svcB = galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
opA = galleryop.NewOpCache(svcA)
opB = galleryop.NewOpCache(svcB)
opA.SetMessagingClient(bus)
opB.SetMessagingClient(bus)
Expect(opA.Start(context.Background())).To(Succeed())
Expect(opB.Start(context.Background())).To(Succeed())
})
AfterEach(func() {
opA.Close()
opB.Close()
})
It("propagates a Set on replica A to replica B's OpCache", func() {
opA.Set("llama-3-8b", "job-uuid-1")
Expect(opB.Exists("llama-3-8b")).To(BeTrue(),
"replica B should see the operation that replica A admitted")
Expect(opB.Get("llama-3-8b")).To(Equal("job-uuid-1"))
Expect(opB.IsBackendOp("llama-3-8b")).To(BeFalse())
})
It("propagates SetBackend with the backend-op flag set", func() {
opA.SetBackend("official@vllm", "job-uuid-2")
Expect(opB.Exists("official@vllm")).To(BeTrue())
Expect(opB.IsBackendOp("official@vllm")).To(BeTrue(),
"peer must learn that this op is a backend install, not a model install")
})
It("propagates DeleteUUID across replicas", func() {
opA.Set("llama-3-8b", "job-uuid-3")
Expect(opB.Exists("llama-3-8b")).To(BeTrue())
opA.DeleteUUID("job-uuid-3")
Expect(opA.Exists("llama-3-8b")).To(BeFalse())
Expect(opB.Exists("llama-3-8b")).To(BeFalse(),
"a dismissed operation must clear from peer replicas too")
})
It("does not double-write on echo (replica A's broadcast received by replica A)", func() {
// We can't directly observe self-receive but we can confirm the
// resulting state is the same single value, not corrupted or doubled.
opA.Set("model-x", "job-uuid-4")
Expect(opA.Map()).To(HaveLen(1))
Expect(opA.Get("model-x")).To(Equal("job-uuid-4"))
})
})
var _ = Describe("OpCache PostgreSQL hydration", func() {
It("rebuilds the OpCache from active gallery_operations rows on Start", func() {
db := testutil.SetupTestDB()
store, err := distributed.NewGalleryStore(db)
Expect(err).ToNot(HaveOccurred())
// Seed: two active model installs (one with backend flag, one without)
// and one completed op that must NOT resurrect.
Expect(store.UpsertCacheKey("job-A", "llama-3-8b", false)).To(Succeed())
Expect(store.UpsertCacheKey("job-B", "official@vllm", true)).To(Succeed())
Expect(store.UpsertCacheKey("job-C", "old-stale", false)).To(Succeed())
Expect(store.UpdateStatus("job-C", "completed", "")).To(Succeed())
svc := galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
cache := galleryop.NewOpCache(svc)
cache.SetGalleryStore(store)
Expect(cache.Start(context.Background())).To(Succeed())
Expect(cache.Exists("llama-3-8b")).To(BeTrue())
Expect(cache.IsBackendOp("llama-3-8b")).To(BeFalse())
Expect(cache.Get("llama-3-8b")).To(Equal("job-A"))
Expect(cache.Exists("official@vllm")).To(BeTrue())
Expect(cache.IsBackendOp("official@vllm")).To(BeTrue())
Expect(cache.Exists("old-stale")).To(BeFalse(),
"completed operations must not resurrect on hydration")
})
})
var _ = Describe("GalleryService broadcast sync", func() {
var (
svcA *galleryop.GalleryService
svcB *galleryop.GalleryService
bus *fakeBus
)
BeforeEach(func() {
bus = newFakeBus()
svcA = galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
svcB = galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
svcA.SetNATSClient(bus)
svcB.SetNATSClient(bus)
Expect(svcA.SubscribeBroadcasts()).To(Succeed())
Expect(svcB.SubscribeBroadcasts()).To(Succeed())
})
AfterEach(func() {
svcA.CloseBroadcasts()
svcB.CloseBroadcasts()
})
It("delivers an UpdateStatus on A into B's statuses map via the wildcard subscriber", func() {
svcA.UpdateStatus("op-1", &galleryop.OpStatus{
Progress: 50.0,
Message: "halfway",
GalleryElementName: "llama-3-8b",
})
st := svcB.GetStatus("op-1")
Expect(st).ToNot(BeNil(),
"replica B must see the progress its peer just published")
Expect(st.Progress).To(Equal(50.0))
Expect(st.Message).To(Equal("halfway"))
Expect(st.GalleryElementName).To(Equal("llama-3-8b"))
})
It("preserves a peer's accumulated Nodes when a tick arrives with empty Nodes", func() {
// Seed B with a multi-node breakdown via UpdateNodeProgress.
svcB.UpdateNodeProgress("op-2", "n1", galleryop.NodeProgress{
NodeID: "n1", NodeName: "worker-a", Status: galleryop.NodeStatusDownloading, Percentage: 20.0,
})
svcB.UpdateNodeProgress("op-2", "n2", galleryop.NodeProgress{
NodeID: "n2", NodeName: "worker-b", Status: galleryop.NodeStatusDownloading, Percentage: 30.0,
})
// A publishes a single-bar progress tick (no Nodes) — must not wipe B's Nodes.
svcA.UpdateStatus("op-2", &galleryop.OpStatus{Progress: 25.0, Message: "downloading"})
st := svcB.GetStatus("op-2")
Expect(st.Nodes).To(HaveLen(2),
"merged tick must carry forward existing per-node breakdown")
})
It("preserves the error message through a peer's wildcard broadcast", func() {
svcA.UpdateStatus("op-3", &galleryop.OpStatus{
Processed: true,
Error: errors.New("oci pull failed"),
})
st := svcB.GetStatus("op-3")
Expect(st).ToNot(BeNil())
Expect(st.Error).ToNot(BeNil(),
"a failed op's error must survive the broadcast hop")
Expect(st.Error.Error()).To(Equal("oci pull failed"))
Expect(st.Processed).To(BeTrue())
})
It("runs the local cancel func when a peer publishes a cancel event", func() {
var cancelCalled bool
ctx, cancel := context.WithCancel(context.Background())
svcB.StoreCancellation("op-4", func() {
cancelCalled = true
cancel()
})
_ = ctx
// A side fires CancelOperation; the cancel func lives on B and must run.
Expect(svcA.CancelOperation("op-4")).To(Succeed())
Expect(cancelCalled).To(BeTrue(),
"the replica holding the cancel func must run it when a peer requests cancellation")
st := svcB.GetStatus("op-4")
Expect(st).ToNot(BeNil())
Expect(st.Cancelled).To(BeTrue())
})
})
var _ = Describe("GalleryService cache invalidation broadcasts", func() {
var (
svcA *galleryop.GalleryService
svcB *galleryop.GalleryService
bus *fakeBus
)
BeforeEach(func() {
bus = newFakeBus()
svcA = galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
svcB = galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
svcA.SetNATSClient(bus)
svcB.SetNATSClient(bus)
})
AfterEach(func() {
svcA.CloseBroadcasts()
svcB.CloseBroadcasts()
})
It("delivers SubjectCacheInvalidateModels to peer's OnModelsChanged callback", func() {
var (
mu sync.Mutex
seen []messaging.CacheInvalidateEvent
)
svcB.OnModelsChanged = func(evt messaging.CacheInvalidateEvent) {
mu.Lock()
seen = append(seen, evt)
mu.Unlock()
}
Expect(svcA.SubscribeBroadcasts()).To(Succeed())
Expect(svcB.SubscribeBroadcasts()).To(Succeed())
Expect(bus.Publish(messaging.SubjectCacheInvalidateModels, messaging.CacheInvalidateEvent{
Element: "llama-3-8b", Op: "install",
})).To(Succeed())
mu.Lock()
defer mu.Unlock()
// Both replicas subscribed; both callbacks fire (svcA's is nil-callback so no-op).
Expect(seen).To(ContainElement(messaging.CacheInvalidateEvent{
Element: "llama-3-8b", Op: "install",
}))
})
It("delivers SubjectCacheInvalidateBackends to peer's OnBackendOpCompleted callback", func() {
done := make(chan struct{}, 1)
svcB.OnBackendOpCompleted = func() {
select {
case done <- struct{}{}:
default:
}
}
Expect(svcA.SubscribeBroadcasts()).To(Succeed())
Expect(svcB.SubscribeBroadcasts()).To(Succeed())
Expect(bus.Publish(messaging.SubjectCacheInvalidateBackends, messaging.CacheInvalidateEvent{
Element: "vllm", Op: "upgrade",
})).To(Succeed())
Eventually(done, "2s", "10ms").Should(Receive(),
"peer must fire its UpgradeChecker hook when any replica completes a backend op")
})
It("survives a nil OnModelsChanged callback (subscriber set but no handler)", func() {
Expect(svcA.SubscribeBroadcasts()).To(Succeed())
Expect(svcB.SubscribeBroadcasts()).To(Succeed())
// No callbacks registered on either side — just ensure publish does not panic.
Expect(bus.Publish(messaging.SubjectCacheInvalidateModels, messaging.CacheInvalidateEvent{
Element: "x", Op: "install",
})).To(Succeed())
})
It("BroadcastModelsChanged delivers the element and op to a peer's OnModelsChanged", func() {
var (
mu sync.Mutex
seen []messaging.CacheInvalidateEvent
)
svcB.OnModelsChanged = func(evt messaging.CacheInvalidateEvent) {
mu.Lock()
seen = append(seen, evt)
mu.Unlock()
}
Expect(svcA.SubscribeBroadcasts()).To(Succeed())
Expect(svcB.SubscribeBroadcasts()).To(Succeed())
// An admin edit on replica A must reach replica B over the same subject
// the gallery path uses, so B refreshes its in-memory config loader.
svcA.BroadcastModelsChanged("my-alias", "install")
mu.Lock()
defer mu.Unlock()
Expect(seen).To(ContainElement(messaging.CacheInvalidateEvent{
Element: "my-alias", Op: "install",
}))
})
It("BroadcastModelsChanged is a no-op when NATS is not wired (standalone)", func() {
standalone := galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
// No SetNATSClient: must not panic and must simply do nothing.
Expect(func() { standalone.BroadcastModelsChanged("x", "delete") }).ToNot(Panic())
})
})
var _ = Describe("GalleryService PostgreSQL hydration", func() {
It("rebuilds the in-memory statuses map from active rows", func() {
db := testutil.SetupTestDB()
store, err := distributed.NewGalleryStore(db)
Expect(err).ToNot(HaveOccurred())
Expect(store.Create(&distributed.GalleryOperationRecord{
ID: "active-op",
GalleryElementName: "llama-3-8b",
OpType: "model_install",
Status: "downloading",
Progress: 65.0,
Message: "fetching shards",
})).To(Succeed())
Expect(store.Create(&distributed.GalleryOperationRecord{
ID: "failed-op",
GalleryElementName: "broken",
OpType: "backend_install",
Status: "downloading",
Progress: 10.0,
Error: "oci pull failed",
})).To(Succeed())
Expect(store.Create(&distributed.GalleryOperationRecord{
ID: "completed-op",
GalleryElementName: "done",
OpType: "model_install",
Status: "completed",
Progress: 100.0,
})).To(Succeed())
svc := galleryop.NewGalleryService(&config.ApplicationConfig{}, nil)
svc.SetGalleryStore(store)
Expect(svc.Hydrate()).To(Succeed())
active := svc.GetStatus("active-op")
Expect(active).ToNot(BeNil(), "in-flight op must be hydrated")
Expect(active.Progress).To(Equal(65.0))
Expect(active.Message).To(Equal("fetching shards"))
Expect(active.GalleryElementName).To(Equal("llama-3-8b"))
failed := svc.GetStatus("failed-op")
Expect(failed).ToNot(BeNil())
Expect(failed.Error).ToNot(BeNil(),
"the persisted error message must be reconstructed as an error value")
Expect(failed.Error.Error()).To(Equal("oci pull failed"))
Expect(svc.GetStatus("completed-op")).To(BeNil(),
"completed ops are filtered out of ListActive and must not hydrate")
})
})