Files
core/dht/Search Client.go
Akilan Selvacoumar ae5f1d2fd7 New features (#110)
* added upload status

* added changes for progress bar with more logs and bug fixes, Documentation yet to be added

* huge changes that need more doucmenting

* added possibility to get profile using NodeID

* added fix profile listing user profile information

* removed profile image from the explore reult struct

* saving current changes

* added filter to search based on NodeID

* Monday bug fixing

* updates to the profile

* changes for tracing the blockchain profile image not shown

* added condition to ensure TAG is not sent and removed debug prints

* updated webapi docs
2023-06-28 00:38:17 +01:00

326 lines
11 KiB
Go

/*
File Username: 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"
"sync"
"sync/atomic"
"time"
)
// 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).
alpha int // Count of concurrent information requests per level.
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.
// Alpha is the number of concurrent requests that will be performed.
func (dht *DHT) NewSearch(Action int, Key []byte, Timeout, TimeoutIR time.Duration, Alpha int) (client *SearchClient) {
client = &SearchClient{
Action: Action,
Key: Key,
dht: dht,
timeoutTotal: Timeout,
timeoutIR: TimeoutIR,
alpha: Alpha,
contactedNodesMap: make(map[string]struct{}),
storing: make(chan []*Node, Alpha*2),
TerminateSignal: make(chan struct{}),
Results: make(chan *SearchResult),
LogStatus: func(function, format string, v ...interface{}) {},
}
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(client.alpha, 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("search.sendInfoRequest", "contact node %s\n", hex.EncodeToString(node.ID))
}
info = client.dht.NewInformationRequest(client.Action, client.Key, nodes)
info.ResultChanExt = resultChan
switch client.Action {
case ActionFindNode:
client.dht.SendRequestFindNode(info)
case ActionFindValue:
client.dht.SendRequestFindValue(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, client.alpha)
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, client.alpha*2)
closestNode := client.list.Nodes[0]
// start an info request
startInfoRequest := func() (info *InformationRequest) {
nodes := client.list.GetUncontacted(client.alpha, true)
if len(nodes) == 0 {
client.LogStatus("search.startSearch", "search in level %d aborted, no new nodes to contact\n", level)
return nil
}
client.LogStatus("search.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("search.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("search.startSearch", "result: 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("search.startSearch", "result: 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("search.startSearch", "result: 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("search.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("search.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("search.startSearch", "level %d last active level, not found, terminate search\n", level)
client.Terminate()
} else {
client.LogStatus("search.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
}
}
}