diff --git a/pkg/conn/cluster.go b/pkg/conn/cluster.go new file mode 100644 index 0000000..6496da0 --- /dev/null +++ b/pkg/conn/cluster.go @@ -0,0 +1,73 @@ +package conn + +import ( + "errors" + "math" + "math/rand" + "slices" +) + +// ConnCluster splits nodes into clusters where nodes in a cluster communicate +// frequently and nodes outside of a cluster communicate infrequently +type ConnCluster interface { + GetNeighbours(global []string, selfId string) []string + GetInterCluster(global []string, selfId string) string +} + +type ConnClusterImpl struct { + clusterSize int +} + +func binarySearch(global []string, selfId string, groupSize int) (int, int) { + slices.Sort(global) + + lower := 0 + higher := len(global) - 1 + mid := lower + (lower+higher)/2 + + for (higher+1)-lower > groupSize { + if global[mid] <= selfId { + lower = mid + } else { + higher = mid + } + + mid = lower + (lower+higher)/2 + } + + return lower, higher + 1 +} + +// GetNeighbours return the neighbours 'nearest' to you. In this implementation the +// neighbours aren't actually the ones nearest to you but just the ones nearest +// to you alphabetically. Perform binary search to get the total group +func (i *ConnClusterImpl) GetNeighbours(global []string, selfId string) []string { + slices.Sort(global) + + lower, higher := binarySearch(global, selfId, i.clusterSize) + // slice the list to get the neighbours + return global[lower:higher] +} + +// GetInterCluster get nodes not in your cluster. Every round there is a given chance +// you will communicate with a random node that is not in your cluster. +func (i *ConnClusterImpl) GetInterCluster(global []string, selfId string) string { + // Doesn't matter if not in it. Get index of where the node 'should' be + index, _ := binarySearch(global, selfId, 1) + numClusters := math.Ceil(float64(len(global)) / float64(i.clusterSize)) + + randomCluster := rand.Intn(int(numClusters)-1) + 1 + + neighbourIndex := (index + randomCluster) % len(global) + return global[neighbourIndex] +} + +func NewConnCluster(clusterSize int) (ConnCluster, error) { + log2Cluster := math.Log2(float64(clusterSize)) + + if float64((log2Cluster))-log2Cluster != 0 { + return nil, errors.New("cluster must be a power of 2") + } + + return &ConnClusterImpl{clusterSize: clusterSize}, nil +} diff --git a/pkg/conn/window.go b/pkg/conn/window.go new file mode 100644 index 0000000..75510b5 --- /dev/null +++ b/pkg/conn/window.go @@ -0,0 +1,84 @@ +package conn + +import ( + "errors" + "slices" + + "github.com/tim-beatham/wgmesh/pkg/lib" +) + +// ConnectionWindow maintains a sliding window of connections between users +type ConnectionWindow interface { + // GetWindow is a list of connections to choose from + GetWindow() []string + // SlideConnection removes a node from the window and adds a random node + // not already in the window. connList represents the list of possible + // connections to choose from + SlideConnection(connList []string) error + // PushConneciton is used when connection list less than window size. + PutConnection(conn []string) error + // IsFull returns true if the window is full. In which case we must slide the window + IsFull() bool +} + +type ConnectionWindowImpl struct { + window []string + windowSize int +} + +// GetWindow gets the current list of active connections in +// the window +func (c *ConnectionWindowImpl) GetWindow() []string { + return c.window +} + +// SlideConnection slides the connection window by one shuffling items +// in the windows +func (c *ConnectionWindowImpl) SlideConnection(connList []string) error { + // If the number of peer connections is less than the length of the window + // then exit early. Can't slide the window it should contain all nodes! + if len(c.window) < c.windowSize { + return nil + } + + filter := func(node string) bool { + return !slices.Contains(c.window, node) + } + + pool := lib.Filter(connList, filter) + newNode := lib.RandomSubsetOfLength(pool, 1) + + if len(newNode) == 0 { + return errors.New("could not slide window") + } + + for i := len(c.window) - 1; i >= 1; i-- { + c.window[i] = c.window[i-1] + } + + c.window[0] = newNode[0] + return nil +} + +// PutConnection put random connections in the connection +func (c *ConnectionWindowImpl) PutConnection(connList []string) error { + if len(c.window) >= c.windowSize { + return errors.New("cannot place connection. Window full need to slide") + } + + c.window = lib.RandomSubsetOfLength(connList, c.windowSize) + return nil +} + +func (c *ConnectionWindowImpl) IsFull() bool { + return len(c.window) >= c.windowSize +} + +func NewConnectionWindow(windowLength int) ConnectionWindow { + window := &ConnectionWindowImpl{ + window: make([]string, 0), + windowSize: windowLength, + } + + return window +}