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

92 lines
3.7 KiB
Go

package prefixcache
import (
"time"
"github.com/mudler/LocalAI/core/services/messaging"
"github.com/mudler/xlog"
)
// publisher is the minimal slice of messaging.Client that Sync needs.
type publisher interface {
Publish(subject string, v any) error
}
// Sync wraps an Index, broadcasting new/extended observations to peers and
// applying peers' broadcasts. It is the cross-frontend coherence layer.
type Sync struct {
idx Provider
pub publisher
}
func NewSync(idx Provider, pub publisher) *Sync { return &Sync{idx: idx, pub: pub} }
// Observe records locally and, if new/extended, broadcasts to peers. It returns
// whether the local index treated the assignment as new or extended, so Sync
// satisfies prefixcache.Provider.
func (s *Sync) Observe(model string, chain []uint64, key ReplicaKey, now time.Time) bool {
changed := s.idx.Observe(model, chain, key, now)
if changed && s.pub != nil {
ev := messaging.PrefixCacheObserveEvent{Model: model, Chain: chain, NodeID: key.NodeID, Replica: key.Replica}
if err := s.pub.Publish(messaging.SubjectPrefixCacheObserve, ev); err != nil {
xlog.Debug("prefixcache: observe publish failed", "error", err)
}
}
return changed
}
// Invalidate drops the local entry for one replica and broadcasts to peers. The
// local drop is a no-op for models that were never cached (Index.Invalidate does
// not intern a tree). The broadcast is UNCONDITIONAL (when a publisher is
// configured): the registry chokepoint fires for every replica removal, and a
// peer frontend may hold a stale entry for the model even when THIS frontend
// never cached it, so gating the broadcast on local-tree existence would drop
// cross-frontend invalidations and leave peers routing to a removed replica
// until their TTL.
func (s *Sync) Invalidate(model string, key ReplicaKey) {
s.idx.Invalidate(model, key)
if s.pub != nil {
ev := messaging.PrefixCacheInvalidateEvent{Model: model, NodeID: key.NodeID, Replica: key.Replica}
if err := s.pub.Publish(messaging.SubjectPrefixCacheInvalidate, ev); err != nil {
xlog.Debug("prefixcache: invalidate publish failed", "error", err)
}
}
}
// InvalidateNode drops the local entries for ALL replicas of node and broadcasts
// to peers. Like Invalidate the broadcast is unconditional for cross-frontend
// coherence. A negative Replica on the wire means "all replicas of the node".
func (s *Sync) InvalidateNode(model, node string) {
s.idx.InvalidateNode(model, node)
if s.pub != nil {
ev := messaging.PrefixCacheInvalidateEvent{Model: model, NodeID: node, Replica: -1}
if err := s.pub.Publish(messaging.SubjectPrefixCacheInvalidate, ev); err != nil {
xlog.Debug("prefixcache: invalidate-node publish failed", "error", err)
}
}
}
// ApplyObserve applies a peer observe event locally (no re-broadcast).
func (s *Sync) ApplyObserve(ev messaging.PrefixCacheObserveEvent, now time.Time) {
s.idx.Observe(ev.Model, ev.Chain, ReplicaKey{NodeID: ev.NodeID, Replica: ev.Replica}, now)
}
// ApplyInvalidate applies a peer invalidate event locally (no re-broadcast). A
// negative Replica targets all replicas of the node.
func (s *Sync) ApplyInvalidate(ev messaging.PrefixCacheInvalidateEvent) {
if ev.Replica < 0 {
s.idx.InvalidateNode(ev.Model, ev.NodeID)
return
}
s.idx.Invalidate(ev.Model, ReplicaKey{NodeID: ev.NodeID, Replica: ev.Replica})
}
// Decide delegates to the wrapped index.
func (s *Sync) Decide(model string, chain []uint64, candidates []ReplicaKey, now time.Time) PrefixDecision {
return s.idx.Decide(model, chain, candidates, now)
}
// Evict delegates eviction of expired entries to the wrapped index. It does not
// broadcast: each frontend evicts its own copy on its own TTL clock.
func (s *Sync) Evict(now time.Time) { s.idx.Evict(now) }