mirror of
https://github.com/PeernetOfficial/core.git
synced 2026-07-17 02:47:51 +01:00
Add function FilterSearchStatus to get notified on the status of searches for debugging. FindNode uses the new search client.
325 lines
12 KiB
Go
325 lines
12 KiB
Go
/*
|
|
File Name: Search Client.go
|
|
Copyright: 2021 Peernet s.r.o.
|
|
Author: Peter Kleissner
|
|
|
|
A search client runs concurrent information requests for a single query. It solves the query efficiently by using levels.
|
|
Any result that is closer to the target gets pushed down into a new lower level, which contacts nodes closer to the result.
|
|
Level are running concurrently.
|
|
*/
|
|
|
|
package dht
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// MaxConcurrentRequestsPerLevel is the default max count of active concurrent requests per level.
|
|
const MaxConcurrentRequestsPerLevel = 5
|
|
|
|
// MaxConcurrentRequestKnownStore is the default max count of nodes to request the data for ActionFindValue.
|
|
const MaxConcurrentRequestKnownStore = 7
|
|
|
|
// MaxAcceptKnownStore is maximum count accepted of known peers that store the value
|
|
const MaxAcceptKnownStore = 10
|
|
|
|
// MaxClosest is maximum number of closest peers accepted
|
|
const MaxClosest = 3
|
|
|
|
// MaxLevel defines the max level.
|
|
const MaxLevel = 10
|
|
|
|
// SearchClient defines a search in the distributed hash table involving multiple information requests.
|
|
// The search can be created for a node or for a value, both identified by the hash (known as the key).
|
|
type SearchClient struct {
|
|
Action int // ActionX
|
|
Key []byte // Key that is being queried
|
|
IsTerminated bool // If true, it was signaled for termination
|
|
TerminateSignal chan struct{} // gets closed on termination signal, can be used in select via "case _ = <- TerminateSignal:"
|
|
sync.Mutex // for sychronized closing
|
|
timeStart time.Time // When the search started.
|
|
timeEnd time.Time // When the search ended.
|
|
dht *DHT // DHT used
|
|
timeoutTotal time.Duration // Timeout after the entire search will be terminated client-side.
|
|
timeoutIR time.Duration // Timeout for information requests (entire roundtrip).
|
|
Results chan *SearchResult // Result channel
|
|
list *shortList // List of nodes to contact
|
|
contactedNodesMap map[string]struct{} // List of nodes already contacted
|
|
contactedNodesMutex sync.RWMutex // Sync map access
|
|
storing chan []*Node // Internal channel to signal nodes that indicate storing the searched value.
|
|
activeLevels uint64 // demo
|
|
LogStatus func(function, format string, v ...interface{}) // Filter function for status output
|
|
}
|
|
|
|
// SearchResult is a single result to the search. Depending on the search type and parameters, multiple results may be sent.
|
|
type SearchResult struct {
|
|
Key []byte // Original key that was searched for
|
|
Action int // Original action
|
|
SenderID []byte // Sender node ID of the result
|
|
|
|
// data for ActionFindNode
|
|
TargetNode *Node // The node that was requested.
|
|
|
|
// data for ActionFindValue
|
|
Data []byte // Actual data
|
|
}
|
|
|
|
// NewSearch creates a new search client.
|
|
// Action indicates the action to take (from ActionX constants), to either find a node, or a value.
|
|
// Timeout is the total time the search may take, covering all information requests. TimeoutIR is the time an information request may take.
|
|
func (dht *DHT) NewSearch(Action int, Key []byte, Timeout, TimeoutIR time.Duration) (client *SearchClient) {
|
|
client = &SearchClient{
|
|
Action: Action,
|
|
Key: Key,
|
|
dht: dht,
|
|
timeoutTotal: Timeout,
|
|
timeoutIR: TimeoutIR,
|
|
contactedNodesMap: make(map[string]struct{}),
|
|
storing: make(chan []*Node, MaxConcurrentRequestKnownStore*2),
|
|
TerminateSignal: make(chan struct{}),
|
|
Results: make(chan *SearchResult),
|
|
LogStatus: func(function, format string, v ...interface{}) {},
|
|
}
|
|
fmt.Printf("New search for key %s action %d\n", hex.EncodeToString(Key), Action)
|
|
|
|
return
|
|
}
|
|
|
|
// Terminate sends the termination signal to all workers. It is safe to call Terminate multiple times.
|
|
func (client *SearchClient) Terminate() {
|
|
client.Lock()
|
|
defer client.Unlock()
|
|
|
|
if client.IsTerminated {
|
|
return
|
|
}
|
|
|
|
// set the termination signal
|
|
client.IsTerminated = true
|
|
close(client.TerminateSignal) // safety guaranteed via lock
|
|
|
|
client.timeEnd = time.Now()
|
|
|
|
close(client.Results)
|
|
close(client.storing)
|
|
}
|
|
|
|
// isContactedNode checks if a node was contacted
|
|
func (client *SearchClient) isContactedNode(ID []byte, Set bool) (contacted bool) {
|
|
client.contactedNodesMutex.Lock()
|
|
_, contacted = client.contactedNodesMap[string(ID)]
|
|
if Set {
|
|
client.contactedNodesMap[string(ID)] = struct{}{}
|
|
}
|
|
client.contactedNodesMutex.Unlock()
|
|
return contacted
|
|
}
|
|
|
|
// filterUncontactedNodes returns only nodes that were not contacted so far. All nodes will be set to contacted. Limit is optional (0 for no limit).
|
|
func (client *SearchClient) filterUncontactedNodes(input []*Node, limit int) (output []*Node) {
|
|
client.contactedNodesMutex.Lock()
|
|
|
|
for _, node := range input {
|
|
if _, ok := client.contactedNodesMap[string(node.ID)]; !ok {
|
|
output = append(output, node)
|
|
client.contactedNodesMap[string(node.ID)] = struct{}{}
|
|
|
|
if limit > 0 {
|
|
limit--
|
|
if limit == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
client.contactedNodesMutex.Unlock()
|
|
return output
|
|
}
|
|
|
|
// SearchAway starts the search. Non-blocking!
|
|
func (client *SearchClient) SearchAway() {
|
|
client.timeStart = time.Now()
|
|
|
|
// create the first search level and start it
|
|
client.list = client.dht.ht.getClosestContacts(MaxConcurrentRequestsPerLevel, client.Key, nil)
|
|
if len(client.list.Nodes) == 0 {
|
|
client.Terminate()
|
|
return
|
|
}
|
|
|
|
go client.queryNodesKnownStore()
|
|
|
|
// start the first information request
|
|
go client.startSearch(0)
|
|
|
|
// start an automated termination function for the timeout
|
|
go func(client *SearchClient) {
|
|
// sleep + watch for closing
|
|
select {
|
|
case <-client.TerminateSignal: // exit the function on other signal
|
|
return
|
|
case <-time.After(client.timeoutTotal):
|
|
client.Terminate()
|
|
}
|
|
}(client)
|
|
}
|
|
|
|
// sendInfoRequest sends out a new info request to the nodes
|
|
func (client *SearchClient) sendInfoRequest(nodes []*Node, resultChan chan *NodeMessage) (info *InformationRequest) {
|
|
if client.IsTerminated {
|
|
return nil
|
|
}
|
|
|
|
for _, node := range nodes {
|
|
client.LogStatus("sendInfoRequest", "contact node %s\n", hex.EncodeToString(node.ID))
|
|
}
|
|
|
|
info = client.dht.NewInformationRequest(client.Action, client.Key, nodes)
|
|
info.ResultChanExt = resultChan
|
|
client.dht.SendRequestFindNode(info)
|
|
|
|
go func() {
|
|
select {
|
|
case <-client.TerminateSignal:
|
|
case <-time.After(client.timeoutIR):
|
|
}
|
|
info.Terminate()
|
|
}()
|
|
|
|
return info
|
|
}
|
|
|
|
// queryNodesKnownStore queries nodes that are known to store the value. Only for ActionFindValue.
|
|
// Returned 'closest nodes' are ignored, as the queried nodes are expected to store the value. This might be adjusted in the future.
|
|
func (client *SearchClient) queryNodesKnownStore() {
|
|
// all results are redirected to a single channel
|
|
resultChan := make(chan *NodeMessage, MaxConcurrentRequestKnownStore)
|
|
|
|
for {
|
|
select {
|
|
case <-client.TerminateSignal:
|
|
return
|
|
|
|
case nodes := <-client.storing:
|
|
client.sendInfoRequest(nodes, resultChan)
|
|
|
|
case result := <-resultChan:
|
|
if len(result.Data) > 0 {
|
|
client.Results <- &SearchResult{Key: client.Key, Action: client.Action, SenderID: result.SenderID, Data: result.Data}
|
|
client.Terminate()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (client *SearchClient) startSearch(level int) {
|
|
atomic.AddUint64(&client.activeLevels, 1)
|
|
defer atomic.AddUint64(&client.activeLevels, ^uint64(0))
|
|
nestedStarted := false
|
|
|
|
results := make(chan *NodeMessage, MaxConcurrentRequestsPerLevel*2)
|
|
|
|
closestNode := client.list.Nodes[0]
|
|
|
|
// start an info request
|
|
startInfoRequest := func() (info *InformationRequest) {
|
|
nodes := client.list.GetUncontacted(MaxConcurrentRequestsPerLevel, true)
|
|
if len(nodes) == 0 {
|
|
client.LogStatus("startSearch", "search in level %d aborted, no new nodes to contact\n", level)
|
|
return nil
|
|
}
|
|
client.LogStatus("startSearch", "start search in level %d contacting %d nodes\n", level, len(nodes))
|
|
return client.sendInfoRequest(nodes, results)
|
|
}
|
|
|
|
info := startInfoRequest()
|
|
if info == nil {
|
|
return
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-client.TerminateSignal:
|
|
client.LogStatus("startSearch", "search in level %d aborted, search client termination signal\n", level)
|
|
return
|
|
|
|
case result := <-results:
|
|
|
|
switch client.Action {
|
|
case ActionFindValue:
|
|
// search for value and it was found?
|
|
if len(result.Data) > 0 {
|
|
client.LogStatus("startSearch", "find value: sender %s: data found (%d bytes)\n", hex.EncodeToString(result.SenderID), len(result.Data))
|
|
client.Results <- &SearchResult{Key: client.Key, Action: client.Action, SenderID: result.SenderID, Data: result.Data}
|
|
client.Terminate()
|
|
return
|
|
}
|
|
|
|
result.Storing = client.filterUncontactedNodes(result.Storing, MaxAcceptKnownStore)
|
|
result.Closest = client.filterUncontactedNodes(result.Closest, MaxClosest)
|
|
|
|
client.LogStatus("startSearch", "find value: sender %s: %d uncontacted nodes store and %d nodes are close to value\n", hex.EncodeToString(result.SenderID), len(result.Storing), len(result.Closest))
|
|
|
|
// Find value: Nodes known to store the value are queried in a separate function.
|
|
if len(result.Storing) > 0 {
|
|
client.storing <- result.Storing
|
|
}
|
|
|
|
case ActionFindNode:
|
|
// search for node and it was found?
|
|
for _, closePeer := range result.Closest {
|
|
if bytes.Equal(closePeer.ID, client.Key) {
|
|
client.LogStatus("startSearch", "find node: sender %s: node found!\n", hex.EncodeToString(result.SenderID))
|
|
client.Results <- &SearchResult{Key: client.Key, Action: client.Action, SenderID: result.SenderID, TargetNode: closePeer}
|
|
client.Terminate()
|
|
return
|
|
}
|
|
}
|
|
|
|
result.Closest = client.filterUncontactedNodes(result.Closest, MaxClosest)
|
|
|
|
client.LogStatus("startSearch", "find node: sender %s: %d nodes are close to value\n", hex.EncodeToString(result.SenderID), len(result.Closest))
|
|
|
|
}
|
|
|
|
// Add closest to list
|
|
client.list.AppendUniqueNodes(result.Closest...)
|
|
|
|
// If no subsequent level, and there's closer nodes, start one!
|
|
if !nestedStarted && !bytes.Equal(client.list.Nodes[0].ID, closestNode.ID) && level < MaxLevel {
|
|
nestedStarted = true
|
|
go client.startSearch(level + 1)
|
|
}
|
|
|
|
case <-info.TerminateSignal:
|
|
// If highest level (= not nested), and there was no conclusive result, try one more round.
|
|
// This helps against result poisoning.
|
|
|
|
if !nestedStarted {
|
|
client.LogStatus("startSearch", "search in level %d aborted, info request termination signal. Final try.\n", level)
|
|
if info = startInfoRequest(); info != nil {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if client.activeLevels == 1 { // if this was the last level, no more results will appear
|
|
client.LogStatus("startSearch", "level %d last active level, not found, terminate search\n", level)
|
|
client.Terminate()
|
|
} else {
|
|
client.LogStatus("startSearch", "level %d end, info request termination signal\n", level)
|
|
}
|
|
return
|
|
|
|
//case <-time.After(time.Second):
|
|
// Future todo: Launch another routine with the with uncontacted nodes if any, to speed up the query
|
|
}
|
|
}
|
|
}
|