|
| 1 | +package p2p |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "time" |
| 7 | + |
| 8 | + "github.com/mudler/LocalAI/core/services/messaging" |
| 9 | + "github.com/mudler/LocalAI/core/services/nodes/prefixcache" |
| 10 | + "github.com/mudler/edgevpn/pkg/blockchain" |
| 11 | + "github.com/mudler/edgevpn/pkg/hub" |
| 12 | + "github.com/mudler/edgevpn/pkg/node" |
| 13 | + "github.com/mudler/xlog" |
| 14 | +) |
| 15 | + |
| 16 | +// affinitySubjectKey is the hub.Message annotation carrying the logical subject |
| 17 | +// (observe vs invalidate) so the receiver can dispatch the way a NATS subject |
| 18 | +// would. The generic channel has no subject routing, so we carry it ourselves. |
| 19 | +const affinitySubjectKey = "subject" |
| 20 | + |
| 21 | +// genericChannelPublisher adapts an edgevpn node's generic broadcast channel to |
| 22 | +// the prefixcache publisher interface (Publish(subject, v)). It lets a |
| 23 | +// federation server reuse prefixcache.Sync for cross-server affinity coherence |
| 24 | +// without NATS: each event is JSON-encoded into a hub.Message and broadcast over |
| 25 | +// the generic channel (not the slow blockchain ledger). |
| 26 | +type genericChannelPublisher struct { |
| 27 | + node *node.Node |
| 28 | +} |
| 29 | + |
| 30 | +// Publish satisfies prefixcache's (unexported) publisher interface structurally. |
| 31 | +func (p *genericChannelPublisher) Publish(subject string, v any) error { |
| 32 | + payload, err := json.Marshal(v) |
| 33 | + if err != nil { |
| 34 | + return fmt.Errorf("marshalling affinity event: %w", err) |
| 35 | + } |
| 36 | + return p.node.PublishMessage(&hub.Message{ |
| 37 | + Message: string(payload), |
| 38 | + Annotations: map[string]interface{}{affinitySubjectKey: subject}, |
| 39 | + }) |
| 40 | +} |
| 41 | + |
| 42 | +// applyAffinityMessage decodes a generic-channel affinity message and applies it |
| 43 | +// to sync WITHOUT re-broadcasting (ApplyObserve/ApplyInvalidate). now is the |
| 44 | +// local clock so TTL is measured per server. Unknown subjects, malformed |
| 45 | +// payloads, and nil inputs are ignored (debug-logged), never fatal. |
| 46 | +func applyAffinityMessage(sync *prefixcache.Sync, m *hub.Message, now time.Time) { |
| 47 | + if sync == nil || m == nil { |
| 48 | + return |
| 49 | + } |
| 50 | + subject, _ := m.Annotations[affinitySubjectKey].(string) |
| 51 | + switch subject { |
| 52 | + case messaging.SubjectPrefixCacheObserve: |
| 53 | + var ev messaging.PrefixCacheObserveEvent |
| 54 | + if err := json.Unmarshal([]byte(m.Message), &ev); err != nil { |
| 55 | + xlog.Debug("affinity: bad observe payload", "error", err) |
| 56 | + return |
| 57 | + } |
| 58 | + sync.ApplyObserve(ev, now) |
| 59 | + case messaging.SubjectPrefixCacheInvalidate: |
| 60 | + var ev messaging.PrefixCacheInvalidateEvent |
| 61 | + if err := json.Unmarshal([]byte(m.Message), &ev); err != nil { |
| 62 | + xlog.Debug("affinity: bad invalidate payload", "error", err) |
| 63 | + return |
| 64 | + } |
| 65 | + sync.ApplyInvalidate(ev) |
| 66 | + default: |
| 67 | + // Other generic-channel traffic; not ours. |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +// affinityHandler returns the edgevpn generic-channel handler that applies remote |
| 72 | +// affinity events to this server's index. It is registered at node construction |
| 73 | +// (handlers cannot be added after Start) and reads fs.prefixSync lazily, which is |
| 74 | +// safe because messages only arrive after Start, by which point Start has wired |
| 75 | +// fs.prefixSync. |
| 76 | +func (fs *FederatedServer) affinityHandler() node.Handler { |
| 77 | + return func(_ *blockchain.Ledger, m *hub.Message, _ chan *hub.Message) error { |
| 78 | + applyAffinityMessage(fs.prefixSync, m, time.Now()) |
| 79 | + return nil |
| 80 | + } |
| 81 | +} |
0 commit comments