Change
This commit is contained in:
@@ -37,43 +37,6 @@ type Score struct {
|
||||
// Peer witnesses
|
||||
witnessChecked int
|
||||
witnessConsistent int
|
||||
// WitnessPool: up to 3 witnesses last reported by this indexer.
|
||||
// Used for indirect probing when the indexer becomes unreachable.
|
||||
// Oldest entry is replaced when the pool is full and a fresher witness arrives.
|
||||
WitnessPool []WitnessCacheEntry
|
||||
}
|
||||
|
||||
// WitnessCacheEntry holds one witness AddrInfo with its last-seen timestamp.
|
||||
const maxWitnessPool = 3
|
||||
|
||||
type WitnessCacheEntry struct {
|
||||
AI pp.AddrInfo
|
||||
SeenAt time.Time
|
||||
}
|
||||
|
||||
// UpdateWitnessPool inserts or refreshes a witness entry.
|
||||
// If the pool is full and the witness is new, the oldest entry is replaced.
|
||||
func (s *Score) UpdateWitnessPool(w pp.AddrInfo) {
|
||||
for i, e := range s.WitnessPool {
|
||||
if e.AI.ID == w.ID {
|
||||
s.WitnessPool[i].AI = w
|
||||
s.WitnessPool[i].SeenAt = time.Now()
|
||||
return
|
||||
}
|
||||
}
|
||||
entry := WitnessCacheEntry{AI: w, SeenAt: time.Now()}
|
||||
if len(s.WitnessPool) < maxWitnessPool {
|
||||
s.WitnessPool = append(s.WitnessPool, entry)
|
||||
return
|
||||
}
|
||||
// Replace oldest.
|
||||
oldest := 0
|
||||
for i, e := range s.WitnessPool {
|
||||
if e.SeenAt.Before(s.WitnessPool[oldest].SeenAt) {
|
||||
oldest = i
|
||||
}
|
||||
}
|
||||
s.WitnessPool[oldest] = entry
|
||||
}
|
||||
|
||||
// computeNodeSideScore computes the node's quality assessment of an indexer from raw metrics.
|
||||
|
||||
@@ -172,46 +172,6 @@ func HandleWitnessQuery(h host.Host, s network.Stream) {
|
||||
json.NewEncoder(s).Encode(report)
|
||||
}
|
||||
|
||||
// IndirectProbeIndexer asks each witness in the cache whether it still sees
|
||||
// the given indexer (by PeerID). Returns true if at least one witness confirms
|
||||
// it is alive — meaning our direct link is asymmetrically broken, not the indexer.
|
||||
// All probes run in parallel; the function blocks at most 5 seconds.
|
||||
func IndirectProbeIndexer(h host.Host, indexerPeerID string, pool []WitnessCacheEntry) bool {
|
||||
if len(pool) == 0 {
|
||||
return false
|
||||
}
|
||||
results := make(chan bool, len(pool))
|
||||
for _, e := range pool {
|
||||
go func(ai pp.AddrInfo) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
s, err := h.NewStream(ctx, ai.ID, ProtocolWitnessQuery)
|
||||
if err != nil {
|
||||
results <- false
|
||||
return
|
||||
}
|
||||
defer s.Reset()
|
||||
s.SetDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := json.NewEncoder(s).Encode(WitnessRequest{IndexerPeerID: indexerPeerID}); err != nil {
|
||||
results <- false
|
||||
return
|
||||
}
|
||||
var rep WitnessReport
|
||||
if err := json.NewDecoder(s).Decode(&rep); err != nil {
|
||||
results <- false
|
||||
return
|
||||
}
|
||||
results <- rep.Seen
|
||||
}(e.AI)
|
||||
}
|
||||
for range pool {
|
||||
if <-results {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SupportsHeartbeat probes pid with a short-lived stream to verify it has
|
||||
// a ProtocolHeartbeat handler (i.e. it is an indexer, not a plain node).
|
||||
// Only protocol negotiation is performed — no data is sent.
|
||||
|
||||
@@ -368,7 +368,6 @@ func SendHeartbeat(ctx context.Context, proto protocol.ID, name string, h host.H
|
||||
|
||||
resp, rtt, err := sendHeartbeat(ctx, h, proto, ai.Info, hb, directory.Streams, interval*time.Second)
|
||||
if err != nil { // Heartbeat fails
|
||||
fmt.Println("EERR", err)
|
||||
HeartbeatFailure(h, proto, directory, ai.Addr, ai.Info, isIndexerHB, maxPool, err)
|
||||
continue
|
||||
}
|
||||
@@ -377,6 +376,7 @@ func SendHeartbeat(ctx context.Context, proto protocol.ID, name string, h host.H
|
||||
// even if the indexer does not support bidirectional heartbeat (Fix 1).
|
||||
if isIndexerHB && score != nil {
|
||||
score.UptimeTracker.RecordHeartbeat()
|
||||
score.UptimeTracker.ConsecutiveFails = 0 // reset on success
|
||||
|
||||
maxRTT := BaseRoundTrip * 10
|
||||
latencyScore := 1.0 - float64(rtt)/float64(maxRTT)
|
||||
@@ -442,11 +442,6 @@ func SendHeartbeat(ctx context.Context, proto protocol.ID, name string, h host.H
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh local witness cache for indirect probing on future failure.
|
||||
for _, w := range resp.Witnesses {
|
||||
score.UpdateWitnessPool(w)
|
||||
}
|
||||
|
||||
// Launch witness cross-check asynchronously (must not hold lock).
|
||||
if len(resp.Witnesses) > 0 {
|
||||
go queryWitnesses(h, ai.Info.ID.String(), resp.BornAt, resp.FillRate, resp.Witnesses, score)
|
||||
@@ -550,16 +545,22 @@ func HeartbeatFailure(h host.Host, proto protocol.ID, directory *Directory,
|
||||
Msg("[pool] seed heartbeat failed — keeping in pool, ticker will retry " + err.Error())
|
||||
return
|
||||
}
|
||||
// Indirect probe: query cached witnesses before declaring the indexer dead.
|
||||
// If a witness confirms it is alive, the failure is a local asymmetric
|
||||
// link — not the indexer. Skip eviction; next tick will retry directly.
|
||||
if len(score.WitnessPool) > 0 {
|
||||
pool := append([]WitnessCacheEntry(nil), score.WitnessPool...)
|
||||
if IndirectProbeIndexer(h, info.ID.String(), pool) {
|
||||
// Indirect probing via other alive indexers:
|
||||
// If other indexers in the pool are still responding, they act as implicit
|
||||
// third-party witnesses confirming our connectivity is fine — the failed
|
||||
// indexer is genuinely dead, evict immediately.
|
||||
// If this is the last indexer, there is no third party. Retry up to 3 times
|
||||
// (consecutive failures tracked in UptimeTracker) before declaring it dead.
|
||||
if len(directory.GetAddrs()) <= 1 {
|
||||
score.UptimeTracker.ConsecutiveFails++
|
||||
if score.UptimeTracker.ConsecutiveFails < 3 {
|
||||
logger.Warn().Str("peer", info.ID.String()).
|
||||
Msg("[indirect] witness confirms indexer alive — asymmetric link, skipping eviction " + err.Error())
|
||||
Int("attempt", score.UptimeTracker.ConsecutiveFails).
|
||||
Msg("[indirect] last indexer failed, retrying before eviction")
|
||||
return
|
||||
}
|
||||
logger.Warn().Str("peer", info.ID.String()).
|
||||
Msg("[indirect] last indexer failed 3 times consecutively, evicting")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@ const MaxPayloadChallenge = 2048
|
||||
const BaseRoundTrip = 400 * time.Millisecond
|
||||
|
||||
type UptimeTracker struct {
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
TotalOnline time.Duration
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
TotalOnline time.Duration
|
||||
ConsecutiveFails int // incremented on each heartbeat failure; reset to 0 on success
|
||||
}
|
||||
|
||||
// RecordHeartbeat accumulates online time gap-aware: only counts the interval if
|
||||
|
||||
@@ -12,6 +12,6 @@ type HeartBeatStreamed interface {
|
||||
}
|
||||
|
||||
type DiscoveryPeer interface {
|
||||
GetPeerRecord(ctx context.Context, key string, search bool) ([]*peer.Peer, error)
|
||||
GetPeerRecord(ctx context.Context, key string) ([]*peer.Peer, error)
|
||||
GetPubSub(topicName string) *pubsub.Topic
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"oc-discovery/daemons/node/common"
|
||||
@@ -94,8 +93,6 @@ func (pr *PeerRecord) ExtractPeer(ourkey string, key string, pubKey crypto.PubKe
|
||||
type GetValue struct {
|
||||
Key string `json:"key"`
|
||||
PeerID string `json:"peer_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Search bool `json:"search,omitempty"`
|
||||
}
|
||||
|
||||
type GetResponse struct {
|
||||
@@ -107,10 +104,6 @@ func (ix *IndexerService) genKey(did string) string {
|
||||
return "/node/" + did
|
||||
}
|
||||
|
||||
func (ix *IndexerService) genNameKey(name string) string {
|
||||
return "/name/" + name
|
||||
}
|
||||
|
||||
func (ix *IndexerService) genPIDKey(peerID string) string {
|
||||
return "/pid/" + peerID
|
||||
}
|
||||
@@ -163,16 +156,10 @@ func (ix *IndexerService) initNodeHandler() {
|
||||
return
|
||||
}
|
||||
cancel()
|
||||
ix.publishNameEvent(NameIndexAdd, rec.Name, rec.PeerID, rec.DID)
|
||||
if rec.Name != "" {
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ix.DHT.PutValue(ctx2, ix.genNameKey(rec.Name), []byte(rec.DID))
|
||||
cancel2()
|
||||
}
|
||||
if rec.PeerID != "" {
|
||||
ctx3, cancel3 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ix.DHT.PutValue(ctx3, ix.genPIDKey(rec.PeerID), []byte(rec.DID))
|
||||
cancel3()
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ix.DHT.PutValue(ctx2, ix.genPIDKey(rec.PeerID), []byte(rec.DID))
|
||||
cancel2()
|
||||
}
|
||||
}
|
||||
ix.Host.SetStreamHandler(common.ProtocolHeartbeat, ix.HandleHeartbeat)
|
||||
@@ -277,24 +264,13 @@ func (ix *IndexerService) handleNodePublish(s network.Stream) {
|
||||
}
|
||||
cancel()
|
||||
|
||||
fmt.Println("publishNameEvent")
|
||||
ix.publishNameEvent(NameIndexAdd, rec.Name, rec.PeerID, rec.DID)
|
||||
|
||||
// Secondary index: /name/<name> → DID, so peers can resolve by human-readable name.
|
||||
if rec.Name != "" {
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
if err := ix.DHT.PutValue(ctx2, ix.genNameKey(rec.Name), []byte(rec.DID)); err != nil {
|
||||
logger.Err(err).Str("name", rec.Name).Msg("indexer: failed to write name index")
|
||||
}
|
||||
cancel2()
|
||||
}
|
||||
// Secondary index: /pid/<peerID> → DID, so peers can resolve by libp2p PeerID.
|
||||
if rec.PeerID != "" {
|
||||
ctx3, cancel3 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
if err := ix.DHT.PutValue(ctx3, ix.genPIDKey(rec.PeerID), []byte(rec.DID)); err != nil {
|
||||
ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
if err := ix.DHT.PutValue(ctx2, ix.genPIDKey(rec.PeerID), []byte(rec.DID)); err != nil {
|
||||
logger.Err(err).Str("pid", rec.PeerID).Msg("indexer: failed to write pid index")
|
||||
}
|
||||
cancel3()
|
||||
cancel2()
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -324,52 +300,30 @@ func (ix *IndexerService) handleNodeGet(s network.Stream) {
|
||||
|
||||
resp := GetResponse{Found: false, Records: map[string]PeerRecord{}}
|
||||
|
||||
fmt.Println("handleNodeGet", req.Search, req.Name)
|
||||
keys := []string{}
|
||||
// Name substring search — scan in-memory connected nodes first, then DHT exact match.
|
||||
if req.Name != "" {
|
||||
if req.Search {
|
||||
for _, did := range ix.LookupNameIndex(strings.ToLower(req.Name)) {
|
||||
keys = append(keys, did)
|
||||
}
|
||||
} else {
|
||||
// 2. DHT exact-name lookup: covers nodes that published but aren't currently connected.
|
||||
nameCtx, nameCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
if ch, err := ix.DHT.SearchValue(nameCtx, ix.genNameKey(req.Name)); err == nil {
|
||||
for did := range ch {
|
||||
keys = append(keys, string(did))
|
||||
break
|
||||
}
|
||||
}
|
||||
nameCancel()
|
||||
}
|
||||
} else if req.PeerID != "" {
|
||||
// Resolve DID key: by PeerID (secondary /pid/ index) or direct DID key.
|
||||
var key string
|
||||
if req.PeerID != "" {
|
||||
pidCtx, pidCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
if did, err := ix.DHT.GetValue(pidCtx, ix.genPIDKey(req.PeerID)); err == nil {
|
||||
keys = append(keys, string(did))
|
||||
}
|
||||
did, err := ix.DHT.GetValue(pidCtx, ix.genPIDKey(req.PeerID))
|
||||
pidCancel()
|
||||
if err == nil {
|
||||
key = string(did)
|
||||
}
|
||||
} else {
|
||||
keys = append(keys, req.Key)
|
||||
key = req.Key
|
||||
}
|
||||
|
||||
// DHT record fetch by DID key (covers exact-name and PeerID paths).
|
||||
if len(keys) > 0 {
|
||||
for _, k := range keys {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
fmt.Println("TRY TO CATCH DID", ix.genKey(k))
|
||||
c, err := ix.DHT.GetValue(ctx, ix.genKey(k))
|
||||
cancel()
|
||||
fmt.Println("TRY TO CATCH DID ERR", ix.genKey(k), c, err)
|
||||
if err == nil {
|
||||
var rec PeerRecord
|
||||
if json.Unmarshal(c, &rec) == nil {
|
||||
fmt.Println("CATCH DID ERR", ix.genKey(k), rec)
|
||||
resp.Records[rec.PeerID] = rec
|
||||
}
|
||||
} else if req.Name == "" && req.PeerID == "" {
|
||||
logger.Err(err).Msg("Failed to fetch PeerRecord from DHT " + req.Key)
|
||||
if key != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
c, err := ix.DHT.GetValue(ctx, ix.genKey(key))
|
||||
cancel()
|
||||
if err == nil {
|
||||
var rec PeerRecord
|
||||
if json.Unmarshal(c, &rec) == nil {
|
||||
resp.Records[rec.PeerID] = rec
|
||||
}
|
||||
} else {
|
||||
logger.Err(err).Msg("Failed to fetch PeerRecord from DHT " + key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"oc-discovery/daemons/node/common"
|
||||
|
||||
oclib "cloud.o-forge.io/core/oc-lib"
|
||||
pubsub "github.com/libp2p/go-libp2p-pubsub"
|
||||
pp "github.com/libp2p/go-libp2p/core/peer"
|
||||
)
|
||||
|
||||
// TopicNameIndex is the GossipSub topic shared by regular indexers to exchange
|
||||
// add/delete events for the distributed name→peerID mapping.
|
||||
const TopicNameIndex = "oc-name-index"
|
||||
|
||||
// nameIndexDedupWindow suppresses re-emission of the same (action, name, peerID)
|
||||
// tuple within this window, reducing duplicate events when a node is registered
|
||||
// with multiple indexers simultaneously.
|
||||
const nameIndexDedupWindow = 30 * time.Second
|
||||
|
||||
// NameIndexAction indicates whether a name mapping is being added or removed.
|
||||
type NameIndexAction string
|
||||
|
||||
const (
|
||||
NameIndexAdd NameIndexAction = "add"
|
||||
NameIndexDelete NameIndexAction = "delete"
|
||||
)
|
||||
|
||||
// NameIndexEvent is published on TopicNameIndex by each indexer when a node
|
||||
// registers (add) or is evicted by the GC (delete).
|
||||
type NameIndexEvent struct {
|
||||
Action NameIndexAction `json:"action"`
|
||||
Name string `json:"name"`
|
||||
PeerID string `json:"peer_id"`
|
||||
DID string `json:"did"`
|
||||
}
|
||||
|
||||
// nameIndexState holds the local in-memory name index and the sender-side
|
||||
// deduplication tracker.
|
||||
//
|
||||
// Search strategy: trigram inverted index.
|
||||
// - byName: lowercased name → peerID → DID (for delete and exact resolution)
|
||||
// - byPeer: peerID → lowercased name (to recompute trigrams on delete)
|
||||
// - trigrams: 3-char substring → set of peerIDs (for O(1) substring lookup)
|
||||
//
|
||||
// For needles shorter than 3 chars the trigram index cannot help; a linear
|
||||
// scan of byName is used as fallback (rare and fast enough at small N).
|
||||
type nameIndexState struct {
|
||||
byName map[string]map[string]string // name → peerID → DID
|
||||
byPeer map[string]string // peerID → name
|
||||
trigrams map[string]map[string]struct{} // trigram → peerID set
|
||||
indexMu sync.RWMutex
|
||||
|
||||
// emitted deduplicates GossipSub emissions within nameIndexDedupWindow.
|
||||
// Purged periodically to prevent unbounded growth.
|
||||
emitted map[string]time.Time
|
||||
emittedMu sync.Mutex
|
||||
}
|
||||
|
||||
// trigramsOf returns all overlapping 3-char substrings of s (already lowercased).
|
||||
// If s is shorter than 3 chars the string itself is returned as the sole token.
|
||||
func trigramsOf(s string) []string {
|
||||
if len(s) < 3 {
|
||||
return []string{s}
|
||||
}
|
||||
out := make([]string, 0, len(s)-2)
|
||||
for i := 0; i <= len(s)-3; i++ {
|
||||
out = append(out, s[i:i+3])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// addTrigrams inserts peerID into every trigram bucket for name.
|
||||
func (s *nameIndexState) addTrigrams(name, peerID string) {
|
||||
for _, tg := range trigramsOf(name) {
|
||||
if s.trigrams[tg] == nil {
|
||||
s.trigrams[tg] = map[string]struct{}{}
|
||||
}
|
||||
s.trigrams[tg][peerID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// removeTrigrams deletes peerID from every trigram bucket for name,
|
||||
// cleaning up empty buckets to keep memory tight.
|
||||
func (s *nameIndexState) removeTrigrams(name, peerID string) {
|
||||
for _, tg := range trigramsOf(name) {
|
||||
if m := s.trigrams[tg]; m != nil {
|
||||
delete(m, peerID)
|
||||
if len(m) == 0 {
|
||||
delete(s.trigrams, tg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// shouldEmit returns true if the (action, name, peerID) tuple has not been
|
||||
// emitted within nameIndexDedupWindow, updating the tracker if so.
|
||||
//
|
||||
// On DELETE: the ADD entry for the same peer is immediately removed — the peer
|
||||
// is gone, keeping it would cause the map to grow with departed peers forever.
|
||||
// The DELETE entry itself is kept for the dedup window to absorb duplicate
|
||||
// delete events, then cleaned by the purgeEmitted ticker.
|
||||
func (s *nameIndexState) shouldEmit(action NameIndexAction, name, peerID string) bool {
|
||||
key := string(action) + ":" + name + ":" + peerID
|
||||
s.emittedMu.Lock()
|
||||
defer s.emittedMu.Unlock()
|
||||
if t, ok := s.emitted[key]; ok && time.Since(t) < nameIndexDedupWindow {
|
||||
return false
|
||||
}
|
||||
s.emitted[key] = time.Now()
|
||||
if action == NameIndexDelete {
|
||||
// Peer is leaving: drop its ADD entry — no longer needed.
|
||||
delete(s.emitted, string(NameIndexAdd)+":"+name+":"+peerID)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// purgeEmitted removes stale DELETE entries from the emitted dedup map.
|
||||
// ADD entries are cleaned eagerly on DELETE, so only short-lived DELETE
|
||||
// entries remain here; the ticker just trims those stragglers.
|
||||
func (s *nameIndexState) purgeEmitted() {
|
||||
now := time.Now()
|
||||
s.emittedMu.Lock()
|
||||
defer s.emittedMu.Unlock()
|
||||
for k, t := range s.emitted {
|
||||
if now.Sub(t) >= nameIndexDedupWindow {
|
||||
delete(s.emitted, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// onEvent applies a received NameIndexEvent to the local index.
|
||||
// "add" inserts/updates the mapping; "delete" removes it.
|
||||
// Operations are idempotent — duplicate events from multiple indexers are harmless.
|
||||
func (s *nameIndexState) onEvent(evt NameIndexEvent) {
|
||||
if evt.Name == "" || evt.PeerID == "" {
|
||||
return
|
||||
}
|
||||
nameLow := strings.ToLower(evt.Name)
|
||||
s.indexMu.Lock()
|
||||
defer s.indexMu.Unlock()
|
||||
switch evt.Action {
|
||||
case NameIndexAdd:
|
||||
// If the peer previously had a different name, clean up old trigrams.
|
||||
if old, ok := s.byPeer[evt.PeerID]; ok && old != nameLow {
|
||||
s.removeTrigrams(old, evt.PeerID)
|
||||
if s.byName[old] != nil {
|
||||
delete(s.byName[old], evt.PeerID)
|
||||
if len(s.byName[old]) == 0 {
|
||||
delete(s.byName, old)
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.byName[nameLow] == nil {
|
||||
s.byName[nameLow] = map[string]string{}
|
||||
}
|
||||
s.byName[nameLow][evt.PeerID] = evt.DID
|
||||
s.byPeer[evt.PeerID] = nameLow
|
||||
s.addTrigrams(nameLow, evt.PeerID)
|
||||
|
||||
case NameIndexDelete:
|
||||
// Use stored name so trigrams match exactly what was indexed.
|
||||
name := nameLow
|
||||
if stored, ok := s.byPeer[evt.PeerID]; ok {
|
||||
name = stored
|
||||
}
|
||||
s.removeTrigrams(name, evt.PeerID)
|
||||
delete(s.byPeer, evt.PeerID)
|
||||
if s.byName[name] != nil {
|
||||
delete(s.byName[name], evt.PeerID)
|
||||
if len(s.byName[name]) == 0 {
|
||||
delete(s.byName, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// initNameIndex joins TopicNameIndex and starts consuming events.
|
||||
// Must be called after ix.PS is ready.
|
||||
func (ix *IndexerService) initNameIndex(ps *pubsub.PubSub) {
|
||||
logger := oclib.GetLogger()
|
||||
state := &nameIndexState{
|
||||
byName: map[string]map[string]string{},
|
||||
byPeer: map[string]string{},
|
||||
trigrams: map[string]map[string]struct{}{},
|
||||
emitted: map[string]time.Time{},
|
||||
}
|
||||
ix.nameIndex = state
|
||||
|
||||
// Periodically purge the emitted dedup map so it doesn't grow forever.
|
||||
go func() {
|
||||
t := time.NewTicker(nameIndexDedupWindow)
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
state.purgeEmitted()
|
||||
}
|
||||
}()
|
||||
|
||||
ps.RegisterTopicValidator(TopicNameIndex, func(_ context.Context, _ pp.ID, _ *pubsub.Message) bool {
|
||||
return true
|
||||
})
|
||||
topic, err := ps.Join(TopicNameIndex)
|
||||
if err != nil {
|
||||
logger.Err(err).Msg("name index: failed to join topic")
|
||||
return
|
||||
}
|
||||
ix.LongLivedStreamRecordedService.LongLivedPubSubService.PubsubMu.Lock()
|
||||
ix.LongLivedStreamRecordedService.LongLivedPubSubService.LongLivedPubSubs[TopicNameIndex] = topic
|
||||
ix.LongLivedStreamRecordedService.LongLivedPubSubService.PubsubMu.Unlock()
|
||||
|
||||
common.SubscribeEvents(
|
||||
ix.LongLivedStreamRecordedService.LongLivedPubSubService,
|
||||
context.Background(),
|
||||
TopicNameIndex,
|
||||
-1,
|
||||
func(_ context.Context, evt NameIndexEvent, _ string) {
|
||||
ix.nameIndex.onEvent(evt)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// publishNameEvent emits a NameIndexEvent on TopicNameIndex, subject to the
|
||||
// sender-side deduplication window.
|
||||
func (ix *IndexerService) publishNameEvent(action NameIndexAction, name, peerID, did string) {
|
||||
if ix.nameIndex == nil || name == "" || peerID == "" {
|
||||
return
|
||||
}
|
||||
if !ix.nameIndex.shouldEmit(action, name, peerID) {
|
||||
return
|
||||
}
|
||||
ix.LongLivedStreamRecordedService.LongLivedPubSubService.PubsubMu.RLock()
|
||||
topic := ix.LongLivedStreamRecordedService.LongLivedPubSubService.LongLivedPubSubs[TopicNameIndex]
|
||||
ix.LongLivedStreamRecordedService.LongLivedPubSubService.PubsubMu.RUnlock()
|
||||
if topic == nil {
|
||||
return
|
||||
}
|
||||
evt := NameIndexEvent{Action: action, Name: name, PeerID: peerID, DID: did}
|
||||
b, err := json.Marshal(evt)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = topic.Publish(context.Background(), b)
|
||||
}
|
||||
|
||||
// LookupNameIndex searches the distributed name index for peers whose name
|
||||
// contains needle (case-insensitive). Returns peerID → DID for matched peers.
|
||||
// Returns nil if the name index is not initialised.
|
||||
//
|
||||
// Algorithm:
|
||||
// - needle ≥ 3 chars: trigram intersection → O(|candidates|) verify pass.
|
||||
// The trigram index immediately narrows the candidate set; false positives
|
||||
// are eliminated by the full-string contains check.
|
||||
// - needle < 3 chars: linear scan of byName (rare, still fast at small N).
|
||||
func (ix *IndexerService) LookupNameIndex(needle string) map[string]string {
|
||||
if ix.nameIndex == nil {
|
||||
return nil
|
||||
}
|
||||
needleLow := strings.ToLower(needle)
|
||||
result := map[string]string{}
|
||||
|
||||
ix.nameIndex.indexMu.RLock()
|
||||
defer ix.nameIndex.indexMu.RUnlock()
|
||||
|
||||
if len(needleLow) < 3 {
|
||||
// Short needle: linear scan fallback.
|
||||
for name, peers := range ix.nameIndex.byName {
|
||||
if strings.Contains(name, needleLow) {
|
||||
for peerID, did := range peers {
|
||||
result[peerID] = did
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Trigram intersection: start with the first trigram's set, then
|
||||
// progressively intersect with each subsequent trigram's set.
|
||||
tgs := trigramsOf(needleLow)
|
||||
var candidates map[string]struct{}
|
||||
for _, tg := range tgs {
|
||||
set := ix.nameIndex.trigrams[tg]
|
||||
if len(set) == 0 {
|
||||
return result // any empty trigram set → no possible match
|
||||
}
|
||||
if candidates == nil {
|
||||
candidates = make(map[string]struct{}, len(set))
|
||||
for pid := range set {
|
||||
candidates[pid] = struct{}{}
|
||||
}
|
||||
} else {
|
||||
for pid := range candidates {
|
||||
if _, ok := set[pid]; !ok {
|
||||
delete(candidates, pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Full-string verification pass: trigrams admit false positives
|
||||
// (e.g. "abc" and "bca" share the trigram "bc_" with a rotated name).
|
||||
for peerID := range candidates {
|
||||
name := ix.nameIndex.byPeer[peerID]
|
||||
if strings.Contains(name, needleLow) {
|
||||
did := ""
|
||||
if m := ix.nameIndex.byName[name]; m != nil {
|
||||
did = m[peerID]
|
||||
}
|
||||
result[peerID] = did
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -48,7 +48,6 @@ type IndexerService struct {
|
||||
DHT *dht.IpfsDHT
|
||||
isStrictIndexer bool
|
||||
mu sync.RWMutex
|
||||
nameIndex *nameIndexState
|
||||
dhtProvideCancel context.CancelFunc
|
||||
bornAt time.Time
|
||||
// Passive DHT cache: refreshed every 2 min in background, used for suggestions.
|
||||
@@ -99,10 +98,7 @@ func NewIndexerService(h host.Host, ps *pubsub.PubSub, maxNode int) *IndexerServ
|
||||
go ix.SubscribeToSearch(ix.PS, nil)
|
||||
}
|
||||
|
||||
logger.Info().Msg("init distributed name index...")
|
||||
ix.initNameIndex(ps)
|
||||
ix.LongLivedStreamRecordedService.AfterDelete = func(pid pp.ID, name, did string) {
|
||||
ix.publishNameEvent(NameIndexDelete, name, pid.String(), did)
|
||||
// Remove behavior state for peers that are no longer connected and
|
||||
// have no active ban — keeps memory bounded to the live node set.
|
||||
ix.behavior.Cleanup(pid)
|
||||
@@ -489,9 +485,6 @@ func (ix *IndexerService) Close() {
|
||||
}
|
||||
ix.DHT.Close()
|
||||
ix.PS.UnregisterTopicValidator(common.TopicPubSubSearch)
|
||||
if ix.nameIndex != nil {
|
||||
ix.PS.UnregisterTopicValidator(TopicNameIndex)
|
||||
}
|
||||
for _, s := range ix.StreamRecords {
|
||||
for _, ss := range s {
|
||||
ss.HeartbeatStream.Stream.Close()
|
||||
|
||||
@@ -115,7 +115,7 @@ func ListenNATS(n *Node) {
|
||||
proto = stream.ProtocolMinioConfigResource
|
||||
}
|
||||
if err := json.Unmarshal(resp.Payload, &m); err == nil {
|
||||
peers, _ := n.GetPeerRecord(context.Background(), m.PeerID, false)
|
||||
peers, _ := n.GetPeerRecord(context.Background(), m.PeerID)
|
||||
for _, p := range peers {
|
||||
n.StreamService.PublishCommon(&resp.Datatype, resp.User,
|
||||
p.PeerID, proto, resp.Payload)
|
||||
@@ -135,7 +135,7 @@ func ListenNATS(n *Node) {
|
||||
var m executionConsidersPayload
|
||||
if err := json.Unmarshal(resp.Payload, &m); err == nil {
|
||||
for _, p := range m.PeerIDs {
|
||||
peers, _ := n.GetPeerRecord(context.Background(), p, false)
|
||||
peers, _ := n.GetPeerRecord(context.Background(), p)
|
||||
for _, pp := range peers {
|
||||
n.StreamService.PublishCommon(&resp.Datatype, resp.User,
|
||||
pp.PeerID, stream.ProtocolConsidersResource, resp.Payload)
|
||||
@@ -148,7 +148,7 @@ func ListenNATS(n *Node) {
|
||||
OriginID string `json:"origin_id"`
|
||||
}
|
||||
if err := json.Unmarshal(propalgation.Payload, &m); err == nil && m.OriginID != "" {
|
||||
peers, _ := n.GetPeerRecord(context.Background(), m.OriginID, false)
|
||||
peers, _ := n.GetPeerRecord(context.Background(), m.OriginID)
|
||||
for _, p := range peers {
|
||||
n.StreamService.PublishCommon(nil, resp.User,
|
||||
p.PeerID, stream.ProtocolConsidersResource, propalgation.Payload)
|
||||
|
||||
@@ -146,7 +146,7 @@ func InitNode(isNode bool, isIndexer bool) (*Node, error) {
|
||||
if err != nil || evt.From == node.PeerID.String() {
|
||||
return
|
||||
}
|
||||
if p, err := node.GetPeerRecord(ctx, evt.From, false); err == nil && len(p) > 0 && m["search"] != nil {
|
||||
if p, err := node.GetPeerRecord(ctx, evt.From); err == nil && len(p) > 0 && m["search"] != nil {
|
||||
node.StreamService.SendResponse(p[0], &evt, fmt.Sprintf("%v", m["search"]))
|
||||
}
|
||||
}
|
||||
@@ -275,21 +275,18 @@ func (d *Node) SearchPeerRecord(userKey, needle string, onResult func(common.Sea
|
||||
func (d *Node) GetPeerRecord(
|
||||
ctx context.Context,
|
||||
pidOrdid string,
|
||||
search bool,
|
||||
) ([]*peer.Peer, error) {
|
||||
var err error
|
||||
var info map[string]indexer.PeerRecord
|
||||
// Build the GetValue request: if pidOrdid is neither a UUID DID nor a libp2p
|
||||
// PeerID, treat it as a human-readable name and let the indexer resolve it.
|
||||
// GetPeerRecord resolves by PeerID or DID only.
|
||||
// Name-based search goes through SearchPeerRecord (ProtocolSearchPeer).
|
||||
getReq := indexer.GetValue{Key: pidOrdid}
|
||||
if pidR, pidErr := pp.Decode(pidOrdid); pidErr == nil {
|
||||
getReq.PeerID = pidR.String()
|
||||
} else if _, uuidErr := uuid.Parse(pidOrdid); uuidErr != nil {
|
||||
// Not a UUID DID → treat pidOrdid as a name substring search.
|
||||
getReq.Name = pidOrdid
|
||||
getReq.Key = ""
|
||||
}
|
||||
getReq.Search = search
|
||||
for _, ad := range common.Indexers.GetAddrs() {
|
||||
if common.Indexers.Streams, err = common.TempStream(d.Host, *ad.Info, common.ProtocolGet, "",
|
||||
common.Indexers.Streams, map[protocol.ID]*common.ProtocolInfo{}, &common.Indexers.MuStream); err != nil {
|
||||
|
||||
@@ -164,7 +164,7 @@ func (ps *StreamService) handleEventFromPartner(evt *common.Event, protocol stri
|
||||
fmt.Println(evt.From, p.GetID(), peers.Data)
|
||||
|
||||
ps.SendResponse(p, evt, fmt.Sprintf("%v", search))
|
||||
} else if p, err := ps.Node.GetPeerRecord(context.Background(), evt.From, false); err == nil && len(p) > 0 { // peer from is peerID
|
||||
} else if p, err := ps.Node.GetPeerRecord(context.Background(), evt.From); err == nil && len(p) > 0 { // peer from is peerID
|
||||
ps.SendResponse(p[0], evt, fmt.Sprintf("%v", search))
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -49,7 +49,7 @@ func (ps *StreamService) PublishCommon(dt *tools.DataType, user string, toPeerID
|
||||
var pe *peer.Peer
|
||||
if len(p.Data) > 0 && p.Data[0].(*peer.Peer).Relation != peer.BLACKLIST {
|
||||
pe = p.Data[0].(*peer.Peer)
|
||||
} else if pps, err := ps.Node.GetPeerRecord(context.Background(), toPeerID, false); err == nil && len(pps) > 0 {
|
||||
} else if pps, err := ps.Node.GetPeerRecord(context.Background(), toPeerID); err == nil && len(pps) > 0 {
|
||||
pe = pps[0]
|
||||
}
|
||||
if pe != nil {
|
||||
|
||||
Reference in New Issue
Block a user